blob: 7df69d5c4d64f8bd11eeaf1479c7f1d2c81ea93b [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')
194 super().destroy()
195
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400196 def help(self):
197 """Create textview for config dialog help.
198
199 Attrbutes accessed:
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400200 note
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400201
202 Methods:
203 view_text: Method from textview module.
204 """
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400205 page = self.note.tab(self.note.select(), option='text').strip()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400206 view_text(self, title='Help for IDLE preferences',
207 text=help_common+help_pages.get(page, ''))
208
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400209 def deactivate_current_config(self):
210 """Remove current key bindings.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400211 Iterate over window instances defined in parent and remove
212 the keybindings.
213 """
214 # Before a config is saved, some cleanup of current
215 # config must be done - remove the previous keybindings.
216 win_instances = self.parent.instance_dict.keys()
217 for instance in win_instances:
218 instance.RemoveKeybindings()
219
220 def activate_config_changes(self):
221 """Apply configuration changes to current windows.
222
223 Dynamically update the current parent window instances
224 with some of the configuration changes.
225 """
226 win_instances = self.parent.instance_dict.keys()
227 for instance in win_instances:
228 instance.ResetColorizer()
229 instance.ResetFont()
230 instance.set_notabs_indentwidth()
231 instance.ApplyKeybindings()
232 instance.reset_help_menu_entries()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -0400233 for klass in reloadables:
234 klass.reload()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400235
csabellabac7d332017-06-26 17:46:26 -0400236 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400237 """Part of the config dialog used for configuring IDLE extensions.
238
239 This code is generic - it works for any and all IDLE extensions.
240
241 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400242 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400243 GUI interface to change the configuration values, and saves the
244 changes using idleConf.
245
246 Not all changes take effect immediately - some may require restarting IDLE.
247 This depends on each extension's implementation.
248
249 All values are treated as text, and it is up to the user to supply
250 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300251 which are boolean, and can be toggled with a True/False button.
csabella36329a42017-07-13 23:32:01 -0400252
253 Methods:
Ville Skyttä49b27342017-08-03 09:00:59 +0300254 load_extensions:
csabella36329a42017-07-13 23:32:01 -0400255 extension_selected: Handle selection from list.
256 create_extension_frame: Hold widgets for one extension.
257 set_extension_value: Set in userCfg['extensions'].
258 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400259 """
260 parent = self.parent
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400261 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400262 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
263 self.ext_userCfg = idleConf.userCfg['extensions']
264 self.is_int = self.register(is_int)
265 self.load_extensions()
csabella7eb58832017-07-04 21:30:58 -0400266 # Create widgets - a listbox shows all available extensions, with the
267 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400268 self.extension_names = StringVar(self)
269 frame.rowconfigure(0, weight=1)
270 frame.columnconfigure(2, weight=1)
271 self.extension_list = Listbox(frame, listvariable=self.extension_names,
272 selectmode='browse')
273 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
274 scroll = Scrollbar(frame, command=self.extension_list.yview)
275 self.extension_list.yscrollcommand=scroll.set
276 self.details_frame = LabelFrame(frame, width=250, height=250)
277 self.extension_list.grid(column=0, row=0, sticky='nws')
278 scroll.grid(column=1, row=0, sticky='ns')
279 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
Cheryl Sabella7028e592017-08-26 14:26:02 -0400280 frame.configure(padding=10)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400281 self.config_frame = {}
282 self.current_extension = None
283
284 self.outerframe = self # TEMPORARY
285 self.tabbed_page_set = self.extension_list # TEMPORARY
286
csabella7eb58832017-07-04 21:30:58 -0400287 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400288 ext_names = ''
289 for ext_name in sorted(self.extensions):
290 self.create_extension_frame(ext_name)
291 ext_names = ext_names + '{' + ext_name + '} '
292 self.extension_names.set(ext_names)
293 self.extension_list.selection_set(0)
294 self.extension_selected(None)
295
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400296 return frame
297
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400298 def load_extensions(self):
299 "Fill self.extensions with data from the default and user configs."
300 self.extensions = {}
301 for ext_name in idleConf.GetExtensions(active_only=False):
wohlganger58fc71c2017-09-10 16:19:47 -0500302 # Former built-in extensions are already filtered out.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400303 self.extensions[ext_name] = []
304
305 for ext_name in self.extensions:
306 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
307
csabella7eb58832017-07-04 21:30:58 -0400308 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400309 enables = [opt_name for opt_name in opt_list
310 if opt_name.startswith('enable')]
311 for opt_name in enables:
312 opt_list.remove(opt_name)
313 opt_list = enables + opt_list
314
315 for opt_name in opt_list:
316 def_str = self.ext_defaultCfg.Get(
317 ext_name, opt_name, raw=True)
318 try:
319 def_obj = {'True':True, 'False':False}[def_str]
320 opt_type = 'bool'
321 except KeyError:
322 try:
323 def_obj = int(def_str)
324 opt_type = 'int'
325 except ValueError:
326 def_obj = def_str
327 opt_type = None
328 try:
329 value = self.ext_userCfg.Get(
330 ext_name, opt_name, type=opt_type, raw=True,
331 default=def_obj)
csabella7eb58832017-07-04 21:30:58 -0400332 except ValueError: # Need this until .Get fixed.
333 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400334 var = StringVar(self)
335 var.set(str(value))
336
337 self.extensions[ext_name].append({'name': opt_name,
338 'type': opt_type,
339 'default': def_str,
340 'value': value,
341 'var': var,
342 })
343
344 def extension_selected(self, event):
csabella7eb58832017-07-04 21:30:58 -0400345 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400346 newsel = self.extension_list.curselection()
347 if newsel:
348 newsel = self.extension_list.get(newsel)
349 if newsel is None or newsel != self.current_extension:
350 if self.current_extension:
351 self.details_frame.config(text='')
352 self.config_frame[self.current_extension].grid_forget()
353 self.current_extension = None
354 if newsel:
355 self.details_frame.config(text=newsel)
356 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
357 self.current_extension = newsel
358
359 def create_extension_frame(self, ext_name):
360 """Create a frame holding the widgets to configure one extension"""
361 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
362 self.config_frame[ext_name] = f
363 entry_area = f.interior
csabella7eb58832017-07-04 21:30:58 -0400364 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400365 for row, opt in enumerate(self.extensions[ext_name]):
csabella7eb58832017-07-04 21:30:58 -0400366 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400367 label = Label(entry_area, text=opt['name'])
368 label.grid(row=row, column=0, sticky=NW)
369 var = opt['var']
370 if opt['type'] == 'bool':
Cheryl Sabella7028e592017-08-26 14:26:02 -0400371 Checkbutton(entry_area, variable=var,
372 onvalue='True', offvalue='False', width=8
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400373 ).grid(row=row, column=1, sticky=W, padx=7)
374 elif opt['type'] == 'int':
375 Entry(entry_area, textvariable=var, validate='key',
376 validatecommand=(self.is_int, '%P')
377 ).grid(row=row, column=1, sticky=NSEW, padx=7)
378
379 else:
380 Entry(entry_area, textvariable=var
381 ).grid(row=row, column=1, sticky=NSEW, padx=7)
382 return
383
384 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400385 """Return True if the configuration was added or changed.
386
387 If the value is the same as the default, then remove it
388 from user config file.
389 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400390 name = opt['name']
391 default = opt['default']
392 value = opt['var'].get().strip() or default
393 opt['var'].set(value)
394 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400395 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400396 if (value == default):
397 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400398 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400399 return self.ext_userCfg.SetOption(section, name, value)
400
401 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400402 """Save configuration changes to the user config file.
403
404 Attributes accessed:
405 extensions
406
407 Methods:
408 set_extension_value
409 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400410 has_changes = False
411 for ext_name in self.extensions:
412 options = self.extensions[ext_name]
413 for opt in options:
414 if self.set_extension_value(ext_name, opt):
415 has_changes = True
416 if has_changes:
417 self.ext_userCfg.Save()
418
419
csabella6f446be2017-08-01 00:24:07 -0400420# class TabPage(Frame): # A template for Page classes.
421# def __init__(self, master):
422# super().__init__(master)
423# self.create_page_tab()
424# self.load_tab_cfg()
425# def create_page_tab(self):
426# # Define tk vars and register var and callback with tracers.
427# # Create subframes and widgets.
428# # Pack widgets.
429# def load_tab_cfg(self):
430# # Initialize widgets with data from idleConf.
431# def var_changed_var_name():
432# # For each tk var that needs other than default callback.
433# def other_methods():
434# # Define tab-specific behavior.
435
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300436font_sample_text = (
437 '<ASCII/Latin1>\n'
438 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
439 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
440 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
441 '\n<IPA,Greek,Cyrillic>\n'
442 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
443 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
444 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
445 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
446 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
447 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
448 '\n<Hebrew, Arabic>\n'
449 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
450 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
451 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
452 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
453 '\n<Devanagari, Tamil>\n'
454 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
455 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
456 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
457 '\u0b85\u0b87\u0b89\u0b8e\n'
458 '\n<East Asian>\n'
459 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
460 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
461 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
462 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
463 )
464
csabella6f446be2017-08-01 00:24:07 -0400465
csabella9397e2a2017-07-30 13:34:25 -0400466class FontPage(Frame):
467
csabella6f446be2017-08-01 00:24:07 -0400468 def __init__(self, master, highpage):
469 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400470 self.highlight_sample = highpage.highlight_sample
471 self.create_page_font_tab()
472 self.load_font_cfg()
473 self.load_tab_cfg()
474
475 def create_page_font_tab(self):
476 """Return frame of widgets for Font/Tabs tab.
477
478 Fonts: Enable users to provisionally change font face, size, or
479 boldness and to see the consequence of proposed choices. Each
480 action set 3 options in changes structuree and changes the
481 corresponding aspect of the font sample on this page and
482 highlight sample on highlight page.
483
484 Function load_font_cfg initializes font vars and widgets from
485 idleConf entries and tk.
486
487 Fontlist: mouse button 1 click or up or down key invoke
488 on_fontlist_select(), which sets var font_name.
489
490 Sizelist: clicking the menubutton opens the dropdown menu. A
491 mouse button 1 click or return key sets var font_size.
492
493 Bold_toggle: clicking the box toggles var font_bold.
494
495 Changing any of the font vars invokes var_changed_font, which
496 adds all 3 font options to changes and calls set_samples.
497 Set_samples applies a new font constructed from the font vars to
Miss Islington (bot)e86db342018-02-03 17:41:43 -0800498 font_sample and to highlight_sample on the highlight page.
csabella9397e2a2017-07-30 13:34:25 -0400499
500 Tabs: Enable users to change spaces entered for indent tabs.
501 Changing indent_scale value with the mouse sets Var space_num,
502 which invokes the default callback to add an entry to
503 changes. Load_tab_cfg initializes space_num to default.
504
Cheryl Sabella2f896462017-08-14 21:21:43 -0400505 Widgets for FontPage(Frame): (*) widgets bound to self
506 frame_font: LabelFrame
507 frame_font_name: Frame
508 font_name_title: Label
509 (*)fontlist: ListBox - font_name
510 scroll_font: Scrollbar
511 frame_font_param: Frame
512 font_size_title: Label
513 (*)sizelist: DynOptionMenu - font_size
514 (*)bold_toggle: Checkbutton - font_bold
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400515 frame_sample: LabelFrame
516 (*)font_sample: Label
Cheryl Sabella2f896462017-08-14 21:21:43 -0400517 frame_indent: LabelFrame
518 indent_title: Label
519 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400520 """
csabella6f446be2017-08-01 00:24:07 -0400521 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
522 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
523 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400524 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
525
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400526 # Define frames and widgets.
csabella9397e2a2017-07-30 13:34:25 -0400527 frame_font = LabelFrame(
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400528 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ')
529 frame_sample = LabelFrame(
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300530 self, borderwidth=2, relief=GROOVE,
531 text=' Font Sample (Editable) ')
csabella9397e2a2017-07-30 13:34:25 -0400532 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400533 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400534 # frame_font.
535 frame_font_name = Frame(frame_font)
536 frame_font_param = Frame(frame_font)
537 font_name_title = Label(
538 frame_font_name, justify=LEFT, text='Font Face :')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400539 self.fontlist = Listbox(frame_font_name, height=15,
csabella9397e2a2017-07-30 13:34:25 -0400540 takefocus=True, exportselection=FALSE)
541 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
542 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
543 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
544 scroll_font = Scrollbar(frame_font_name)
545 scroll_font.config(command=self.fontlist.yview)
546 self.fontlist.config(yscrollcommand=scroll_font.set)
547 font_size_title = Label(frame_font_param, text='Size :')
548 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
549 self.bold_toggle = Checkbutton(
550 frame_font_param, variable=self.font_bold,
551 onvalue=1, offvalue=0, text='Bold')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400552 # frame_sample.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300553 self.font_sample = Text(frame_sample, width=20, height=20)
554 self.font_sample.insert(END, font_sample_text)
csabella9397e2a2017-07-30 13:34:25 -0400555 # frame_indent.
556 indent_title = Label(
557 frame_indent, justify=LEFT,
558 text='Python Standard: 4 Spaces!')
559 self.indent_scale = Scale(
560 frame_indent, variable=self.space_num,
561 orient='horizontal', tickinterval=2, from_=2, to=16)
562
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400563 # Grid and pack widgets:
564 self.columnconfigure(1, weight=1)
565 frame_font.grid(row=0, column=0, padx=5, pady=5)
566 frame_sample.grid(row=0, column=1, rowspan=2, padx=5, pady=5,
567 sticky='nsew')
568 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
csabella9397e2a2017-07-30 13:34:25 -0400569 # frame_font.
570 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
571 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
572 font_name_title.pack(side=TOP, anchor=W)
573 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
574 scroll_font.pack(side=LEFT, fill=Y)
575 font_size_title.pack(side=LEFT, anchor=W)
576 self.sizelist.pack(side=LEFT, anchor=W)
577 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400578 # frame_sample.
csabella9397e2a2017-07-30 13:34:25 -0400579 self.font_sample.pack(expand=TRUE, fill=BOTH)
580 # frame_indent.
csabella9397e2a2017-07-30 13:34:25 -0400581 indent_title.pack(side=TOP, anchor=W, padx=5)
582 self.indent_scale.pack(side=TOP, padx=5, fill=X)
583
csabella9397e2a2017-07-30 13:34:25 -0400584 def load_font_cfg(self):
585 """Load current configuration settings for the font options.
586
587 Retrieve current font with idleConf.GetFont and font families
588 from tk. Setup fontlist and set font_name. Setup sizelist,
589 which sets font_size. Set font_bold. Call set_samples.
590 """
591 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
592 font_name = configured_font[0].lower()
593 font_size = configured_font[1]
594 font_bold = configured_font[2]=='bold'
595
596 # Set editor font selection list and font_name.
597 fonts = list(tkFont.families(self))
598 fonts.sort()
599 for font in fonts:
600 self.fontlist.insert(END, font)
601 self.font_name.set(font_name)
602 lc_fonts = [s.lower() for s in fonts]
603 try:
604 current_font_index = lc_fonts.index(font_name)
605 self.fontlist.see(current_font_index)
606 self.fontlist.select_set(current_font_index)
607 self.fontlist.select_anchor(current_font_index)
608 self.fontlist.activate(current_font_index)
609 except ValueError:
610 pass
611 # Set font size dropdown.
612 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
613 '16', '18', '20', '22', '25', '29', '34', '40'),
614 font_size)
615 # Set font weight.
616 self.font_bold.set(font_bold)
617 self.set_samples()
618
619 def var_changed_font(self, *params):
620 """Store changes to font attributes.
621
622 When one font attribute changes, save them all, as they are
623 not independent from each other. In particular, when we are
624 overriding the default font, we need to write out everything.
625 """
626 value = self.font_name.get()
627 changes.add_option('main', 'EditorWindow', 'font', value)
628 value = self.font_size.get()
629 changes.add_option('main', 'EditorWindow', 'font-size', value)
630 value = self.font_bold.get()
631 changes.add_option('main', 'EditorWindow', 'font-bold', value)
632 self.set_samples()
633
634 def on_fontlist_select(self, event):
635 """Handle selecting a font from the list.
636
637 Event can result from either mouse click or Up or Down key.
638 Set font_name and example displays to selection.
639 """
640 font = self.fontlist.get(
641 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
642 self.font_name.set(font.lower())
643
644 def set_samples(self, event=None):
645 """Update update both screen samples with the font settings.
646
647 Called on font initialization and change events.
648 Accesses font_name, font_size, and font_bold Variables.
Miss Islington (bot)e86db342018-02-03 17:41:43 -0800649 Updates font_sample and highlight page highlight_sample.
csabella9397e2a2017-07-30 13:34:25 -0400650 """
651 font_name = self.font_name.get()
652 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
653 new_font = (font_name, self.font_size.get(), font_weight)
654 self.font_sample['font'] = new_font
655 self.highlight_sample['font'] = new_font
656
657 def load_tab_cfg(self):
658 """Load current configuration settings for the tab options.
659
660 Attributes updated:
661 space_num: Set to value from idleConf.
662 """
663 # Set indent sizes.
664 space_num = idleConf.GetOption(
665 'main', 'Indent', 'num-spaces', default=4, type='int')
666 self.space_num.set(space_num)
667
668 def var_changed_space_num(self, *params):
669 "Store change to indentation size."
670 value = self.space_num.get()
671 changes.add_option('main', 'Indent', 'num-spaces', value)
672
673
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400674class HighPage(Frame):
675
676 def __init__(self, master):
677 super().__init__(master)
678 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400679 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400680 self.create_page_highlight()
681 self.load_theme_cfg()
682
683 def create_page_highlight(self):
684 """Return frame of widgets for Highlighting tab.
685
686 Enable users to provisionally change foreground and background
687 colors applied to textual tags. Color mappings are stored in
688 complete listings called themes. Built-in themes in
689 idlelib/config-highlight.def are fixed as far as the dialog is
690 concerned. Any theme can be used as the base for a new custom
691 theme, stored in .idlerc/config-highlight.cfg.
692
693 Function load_theme_cfg() initializes tk variables and theme
694 lists and calls paint_theme_sample() and set_highlight_target()
695 for the current theme. Radiobuttons builtin_theme_on and
696 custom_theme_on toggle var theme_source, which controls if the
697 current set of colors are from a builtin or custom theme.
698 DynOptionMenus builtinlist and customlist contain lists of the
699 builtin and custom themes, respectively, and the current item
700 from each list is stored in vars builtin_name and custom_name.
701
702 Function paint_theme_sample() applies the colors from the theme
703 to the tags in text widget highlight_sample and then invokes
704 set_color_sample(). Function set_highlight_target() sets the state
705 of the radiobuttons fg_on and bg_on based on the tag and it also
706 invokes set_color_sample().
707
708 Function set_color_sample() sets the background color for the frame
709 holding the color selector. This provides a larger visual of the
710 color for the current tag and plane (foreground/background).
711
712 Note: set_color_sample() is called from many places and is often
713 called more than once when a change is made. It is invoked when
714 foreground or background is selected (radiobuttons), from
715 paint_theme_sample() (theme is changed or load_cfg is called), and
716 from set_highlight_target() (target tag is changed or load_cfg called).
717
718 Button delete_custom invokes delete_custom() to delete
719 a custom theme from idleConf.userCfg['highlight'] and changes.
720 Button save_custom invokes save_as_new_theme() which calls
721 get_new_theme_name() and create_new() to save a custom theme
722 and its colors to idleConf.userCfg['highlight'].
723
724 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
725 if the current selected color for a tag is for the foreground or
726 background.
727
728 DynOptionMenu targetlist contains a readable description of the
729 tags applied to Python source within IDLE. Selecting one of the
730 tags from this list populates highlight_target, which has a callback
731 function set_highlight_target().
732
733 Text widget highlight_sample displays a block of text (which is
734 mock Python code) in which is embedded the defined tags and reflects
735 the color attributes of the current theme and changes for those tags.
736 Mouse button 1 allows for selection of a tag and updates
737 highlight_target with that tag value.
738
739 Note: The font in highlight_sample is set through the config in
740 the fonts tab.
741
742 In other words, a tag can be selected either from targetlist or
743 by clicking on the sample text within highlight_sample. The
744 plane (foreground/background) is selected via the radiobutton.
745 Together, these two (tag and plane) control what color is
746 shown in set_color_sample() for the current theme. Button set_color
747 invokes get_color() which displays a ColorChooser to change the
748 color for the selected tag/plane. If a new color is picked,
749 it will be saved to changes and the highlight_sample and
750 frame background will be updated.
751
752 Tk Variables:
753 color: Color of selected target.
754 builtin_name: Menu variable for built-in theme.
755 custom_name: Menu variable for custom theme.
756 fg_bg_toggle: Toggle for foreground/background color.
757 Note: this has no callback.
758 theme_source: Selector for built-in or custom theme.
759 highlight_target: Menu variable for the highlight tag target.
760
761 Instance Data Attributes:
762 theme_elements: Dictionary of tags for text highlighting.
763 The key is the display name and the value is a tuple of
764 (tag name, display sort order).
765
766 Methods [attachment]:
767 load_theme_cfg: Load current highlight colors.
768 get_color: Invoke colorchooser [button_set_color].
769 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
770 set_highlight_target: set fg_bg_toggle, set_color_sample().
771 set_color_sample: Set frame background to target.
772 on_new_color_set: Set new color and add option.
773 paint_theme_sample: Recolor sample.
774 get_new_theme_name: Get from popup.
775 create_new: Combine theme with changes and save.
776 save_as_new_theme: Save [button_save_custom].
777 set_theme_type: Command for [theme_source].
778 delete_custom: Activate default [button_delete_custom].
779 save_new: Save to userCfg['theme'] (is function).
780
781 Widgets of highlights page frame: (*) widgets bound to self
782 frame_custom: LabelFrame
783 (*)highlight_sample: Text
784 (*)frame_color_set: Frame
785 (*)button_set_color: Button
786 (*)targetlist: DynOptionMenu - highlight_target
787 frame_fg_bg_toggle: Frame
788 (*)fg_on: Radiobutton - fg_bg_toggle
789 (*)bg_on: Radiobutton - fg_bg_toggle
790 (*)button_save_custom: Button
791 frame_theme: LabelFrame
792 theme_type_title: Label
793 (*)builtin_theme_on: Radiobutton - theme_source
794 (*)custom_theme_on: Radiobutton - theme_source
795 (*)builtinlist: DynOptionMenu - builtin_name
796 (*)customlist: DynOptionMenu - custom_name
797 (*)button_delete_custom: Button
798 (*)theme_message: Label
799 """
800 self.theme_elements = {
801 'Normal Text': ('normal', '00'),
Miss Islington (bot)4e033c52018-06-01 19:16:04 -0700802 'Code Context': ('context', '01'),
803 'Python Keywords': ('keyword', '02'),
804 'Python Definitions': ('definition', '03'),
805 'Python Builtins': ('builtin', '04'),
806 'Python Comments': ('comment', '05'),
807 'Python Strings': ('string', '06'),
808 'Selected Text': ('hilite', '07'),
809 'Found Text': ('hit', '08'),
810 'Cursor': ('cursor', '09'),
811 'Editor Breakpoint': ('break', '10'),
812 'Shell Normal Text': ('console', '11'),
813 'Shell Error Text': ('error', '12'),
814 'Shell Stdout Text': ('stdout', '13'),
815 'Shell Stderr Text': ('stderr', '14'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400816 }
817 self.builtin_name = tracers.add(
818 StringVar(self), self.var_changed_builtin_name)
819 self.custom_name = tracers.add(
820 StringVar(self), self.var_changed_custom_name)
821 self.fg_bg_toggle = BooleanVar(self)
822 self.color = tracers.add(
823 StringVar(self), self.var_changed_color)
824 self.theme_source = tracers.add(
825 BooleanVar(self), self.var_changed_theme_source)
826 self.highlight_target = tracers.add(
827 StringVar(self), self.var_changed_highlight_target)
828
829 # Create widgets:
830 # body frame and section frames.
831 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
832 text=' Custom Highlighting ')
833 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
834 text=' Highlighting Theme ')
835 # frame_custom.
836 text = self.highlight_sample = Text(
837 frame_custom, relief=SOLID, borderwidth=1,
838 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
839 takefocus=FALSE, highlightthickness=0, wrap=NONE)
840 text.bind('<Double-Button-1>', lambda e: 'break')
841 text.bind('<B1-Motion>', lambda e: 'break')
wohlganger58fc71c2017-09-10 16:19:47 -0500842 text_and_tags=(
843 ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400844 ('#you can click here', 'comment'), ('\n', 'normal'),
845 ('#to choose items', 'comment'), ('\n', 'normal'),
Miss Islington (bot)4e033c52018-06-01 19:16:04 -0700846 ('code context section', 'context'), ('\n\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400847 ('def', 'keyword'), (' ', 'normal'),
848 ('func', 'definition'), ('(param):\n ', 'normal'),
849 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
850 ("'string'", 'string'), ('\n var1 = ', 'normal'),
851 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
852 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
853 ('list', 'builtin'), ('(', 'normal'),
854 ('None', 'keyword'), (')\n', 'normal'),
855 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
856 (' error ', 'error'), (' ', 'normal'),
857 ('cursor |', 'cursor'), ('\n ', 'normal'),
858 ('shell', 'console'), (' ', 'normal'),
859 ('stdout', 'stdout'), (' ', 'normal'),
860 ('stderr', 'stderr'), ('\n\n', 'normal'))
861 for texttag in text_and_tags:
862 text.insert(END, texttag[0], texttag[1])
863 for element in self.theme_elements:
864 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400865 # event.widget.winfo_top_level().highlight_target.set(elem)
866 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400867 text.tag_bind(
868 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400869 text['state'] = 'disabled'
870 self.style.configure('frame_color_set.TFrame', borderwidth=1,
871 relief='solid')
872 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400873 frame_fg_bg_toggle = Frame(frame_custom)
874 self.button_set_color = Button(
875 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400876 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400877 self.targetlist = DynOptionMenu(
878 self.frame_color_set, self.highlight_target, None,
879 highlightthickness=0) #, command=self.set_highlight_targetBinding
880 self.fg_on = Radiobutton(
881 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
882 text='Foreground', command=self.set_color_sample_binding)
883 self.bg_on = Radiobutton(
884 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
885 text='Background', command=self.set_color_sample_binding)
886 self.fg_bg_toggle.set(1)
887 self.button_save_custom = Button(
888 frame_custom, text='Save as New Custom Theme',
889 command=self.save_as_new_theme)
890 # frame_theme.
891 theme_type_title = Label(frame_theme, text='Select : ')
892 self.builtin_theme_on = Radiobutton(
893 frame_theme, variable=self.theme_source, value=1,
894 command=self.set_theme_type, text='a Built-in Theme')
895 self.custom_theme_on = Radiobutton(
896 frame_theme, variable=self.theme_source, value=0,
897 command=self.set_theme_type, text='a Custom Theme')
898 self.builtinlist = DynOptionMenu(
899 frame_theme, self.builtin_name, None, command=None)
900 self.customlist = DynOptionMenu(
901 frame_theme, self.custom_name, None, command=None)
902 self.button_delete_custom = Button(
903 frame_theme, text='Delete Custom Theme',
904 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400905 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400906 # Pack widgets:
907 # body.
908 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500909 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400910 # frame_custom.
911 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
912 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
913 self.highlight_sample.pack(
914 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
915 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
916 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
917 self.fg_on.pack(side=LEFT, anchor=E)
918 self.bg_on.pack(side=RIGHT, anchor=W)
919 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
920 # frame_theme.
921 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
922 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
923 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
924 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
925 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
926 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
927 self.theme_message.pack(side=TOP, fill=X, pady=5)
928
929 def load_theme_cfg(self):
930 """Load current configuration settings for the theme options.
931
932 Based on the theme_source toggle, the theme is set as
933 either builtin or custom and the initial widget values
934 reflect the current settings from idleConf.
935
936 Attributes updated:
937 theme_source: Set from idleConf.
938 builtinlist: List of default themes from idleConf.
939 customlist: List of custom themes from idleConf.
940 custom_theme_on: Disabled if there are no custom themes.
941 custom_theme: Message with additional information.
942 targetlist: Create menu from self.theme_elements.
943
944 Methods:
945 set_theme_type
946 paint_theme_sample
947 set_highlight_target
948 """
949 # Set current theme type radiobutton.
950 self.theme_source.set(idleConf.GetOption(
951 'main', 'Theme', 'default', type='bool', default=1))
952 # Set current theme.
953 current_option = idleConf.CurrentTheme()
954 # Load available theme option menus.
955 if self.theme_source.get(): # Default theme selected.
956 item_list = idleConf.GetSectionList('default', 'highlight')
957 item_list.sort()
958 self.builtinlist.SetMenu(item_list, current_option)
959 item_list = idleConf.GetSectionList('user', 'highlight')
960 item_list.sort()
961 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400962 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400963 self.custom_name.set('- no custom themes -')
964 else:
965 self.customlist.SetMenu(item_list, item_list[0])
966 else: # User theme selected.
967 item_list = idleConf.GetSectionList('user', 'highlight')
968 item_list.sort()
969 self.customlist.SetMenu(item_list, current_option)
970 item_list = idleConf.GetSectionList('default', 'highlight')
971 item_list.sort()
972 self.builtinlist.SetMenu(item_list, item_list[0])
973 self.set_theme_type()
974 # Load theme element option menu.
975 theme_names = list(self.theme_elements.keys())
976 theme_names.sort(key=lambda x: self.theme_elements[x][1])
977 self.targetlist.SetMenu(theme_names, theme_names[0])
978 self.paint_theme_sample()
979 self.set_highlight_target()
980
981 def var_changed_builtin_name(self, *params):
982 """Process new builtin theme selection.
983
984 Add the changed theme's name to the changed_items and recreate
985 the sample with the values from the selected theme.
986 """
987 old_themes = ('IDLE Classic', 'IDLE New')
988 value = self.builtin_name.get()
989 if value not in old_themes:
990 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
991 changes.add_option('main', 'Theme', 'name', old_themes[0])
992 changes.add_option('main', 'Theme', 'name2', value)
993 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400994 else:
995 changes.add_option('main', 'Theme', 'name', value)
996 changes.add_option('main', 'Theme', 'name2', '')
997 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400998 self.paint_theme_sample()
999
1000 def var_changed_custom_name(self, *params):
1001 """Process new custom theme selection.
1002
1003 If a new custom theme is selected, add the name to the
1004 changed_items and apply the theme to the sample.
1005 """
1006 value = self.custom_name.get()
1007 if value != '- no custom themes -':
1008 changes.add_option('main', 'Theme', 'name', value)
1009 self.paint_theme_sample()
1010
1011 def var_changed_theme_source(self, *params):
1012 """Process toggle between builtin and custom theme.
1013
1014 Update the default toggle value and apply the newly
1015 selected theme type.
1016 """
1017 value = self.theme_source.get()
1018 changes.add_option('main', 'Theme', 'default', value)
1019 if value:
1020 self.var_changed_builtin_name()
1021 else:
1022 self.var_changed_custom_name()
1023
1024 def var_changed_color(self, *params):
1025 "Process change to color choice."
1026 self.on_new_color_set()
1027
1028 def var_changed_highlight_target(self, *params):
1029 "Process selection of new target tag for highlighting."
1030 self.set_highlight_target()
1031
1032 def set_theme_type(self):
1033 """Set available screen options based on builtin or custom theme.
1034
1035 Attributes accessed:
1036 theme_source
1037
1038 Attributes updated:
1039 builtinlist
1040 customlist
1041 button_delete_custom
1042 custom_theme_on
1043
1044 Called from:
1045 handler for builtin_theme_on and custom_theme_on
1046 delete_custom
1047 create_new
1048 load_theme_cfg
1049 """
1050 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001051 self.builtinlist['state'] = 'normal'
1052 self.customlist['state'] = 'disabled'
1053 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001054 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001055 self.builtinlist['state'] = 'disabled'
1056 self.custom_theme_on.state(('!disabled',))
1057 self.customlist['state'] = 'normal'
1058 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001059
1060 def get_color(self):
1061 """Handle button to select a new color for the target tag.
1062
1063 If a new color is selected while using a builtin theme, a
1064 name must be supplied to create a custom theme.
1065
1066 Attributes accessed:
1067 highlight_target
1068 frame_color_set
1069 theme_source
1070
1071 Attributes updated:
1072 color
1073
1074 Methods:
1075 get_new_theme_name
1076 create_new
1077 """
1078 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001079 prev_color = self.style.lookup(self.frame_color_set['style'],
1080 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001081 rgbTuplet, color_string = tkColorChooser.askcolor(
1082 parent=self, title='Pick new color for : '+target,
1083 initialcolor=prev_color)
1084 if color_string and (color_string != prev_color):
1085 # User didn't cancel and they chose a new color.
1086 if self.theme_source.get(): # Current theme is a built-in.
1087 message = ('Your changes will be saved as a new Custom Theme. '
1088 'Enter a name for your new Custom Theme below.')
1089 new_theme = self.get_new_theme_name(message)
1090 if not new_theme: # User cancelled custom theme creation.
1091 return
1092 else: # Create new custom theme based on previously active theme.
1093 self.create_new(new_theme)
1094 self.color.set(color_string)
1095 else: # Current theme is user defined.
1096 self.color.set(color_string)
1097
1098 def on_new_color_set(self):
1099 "Display sample of new color selection on the dialog."
1100 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001101 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001102 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1103 sample_element = self.theme_elements[self.highlight_target.get()][0]
1104 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1105 theme = self.custom_name.get()
1106 theme_element = sample_element + '-' + plane
1107 changes.add_option('highlight', theme, theme_element, new_color)
1108
1109 def get_new_theme_name(self, message):
1110 "Return name of new theme from query popup."
1111 used_names = (idleConf.GetSectionList('user', 'highlight') +
1112 idleConf.GetSectionList('default', 'highlight'))
1113 new_theme = SectionName(
1114 self, 'New Custom Theme', message, used_names).result
1115 return new_theme
1116
1117 def save_as_new_theme(self):
1118 """Prompt for new theme name and create the theme.
1119
1120 Methods:
1121 get_new_theme_name
1122 create_new
1123 """
1124 new_theme_name = self.get_new_theme_name('New Theme Name:')
1125 if new_theme_name:
1126 self.create_new(new_theme_name)
1127
1128 def create_new(self, new_theme_name):
1129 """Create a new custom theme with the given name.
1130
1131 Create the new theme based on the previously active theme
1132 with the current changes applied. Once it is saved, then
1133 activate the new theme.
1134
1135 Attributes accessed:
1136 builtin_name
1137 custom_name
1138
1139 Attributes updated:
1140 customlist
1141 theme_source
1142
1143 Method:
1144 save_new
1145 set_theme_type
1146 """
1147 if self.theme_source.get():
1148 theme_type = 'default'
1149 theme_name = self.builtin_name.get()
1150 else:
1151 theme_type = 'user'
1152 theme_name = self.custom_name.get()
1153 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1154 # Apply any of the old theme's unsaved changes to the new theme.
1155 if theme_name in changes['highlight']:
1156 theme_changes = changes['highlight'][theme_name]
1157 for element in theme_changes:
1158 new_theme[element] = theme_changes[element]
1159 # Save the new theme.
1160 self.save_new(new_theme_name, new_theme)
1161 # Change GUI over to the new theme.
1162 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1163 custom_theme_list.sort()
1164 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1165 self.theme_source.set(0)
1166 self.set_theme_type()
1167
1168 def set_highlight_target(self):
1169 """Set fg/bg toggle and color based on highlight tag target.
1170
1171 Instance variables accessed:
1172 highlight_target
1173
1174 Attributes updated:
1175 fg_on
1176 bg_on
1177 fg_bg_toggle
1178
1179 Methods:
1180 set_color_sample
1181
1182 Called from:
1183 var_changed_highlight_target
1184 load_theme_cfg
1185 """
1186 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001187 self.fg_on.state(('disabled',))
1188 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001189 self.fg_bg_toggle.set(1)
1190 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001191 self.fg_on.state(('!disabled',))
1192 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001193 self.fg_bg_toggle.set(1)
1194 self.set_color_sample()
1195
1196 def set_color_sample_binding(self, *args):
1197 """Change color sample based on foreground/background toggle.
1198
1199 Methods:
1200 set_color_sample
1201 """
1202 self.set_color_sample()
1203
1204 def set_color_sample(self):
1205 """Set the color of the frame background to reflect the selected target.
1206
1207 Instance variables accessed:
1208 theme_elements
1209 highlight_target
1210 fg_bg_toggle
1211 highlight_sample
1212
1213 Attributes updated:
1214 frame_color_set
1215 """
1216 # Set the color sample area.
1217 tag = self.theme_elements[self.highlight_target.get()][0]
1218 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1219 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001220 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001221
1222 def paint_theme_sample(self):
1223 """Apply the theme colors to each element tag in the sample text.
1224
1225 Instance attributes accessed:
1226 theme_elements
1227 theme_source
1228 builtin_name
1229 custom_name
1230
1231 Attributes updated:
1232 highlight_sample: Set the tag elements to the theme.
1233
1234 Methods:
1235 set_color_sample
1236
1237 Called from:
1238 var_changed_builtin_name
1239 var_changed_custom_name
1240 load_theme_cfg
1241 """
1242 if self.theme_source.get(): # Default theme
1243 theme = self.builtin_name.get()
1244 else: # User theme
1245 theme = self.custom_name.get()
1246 for element_title in self.theme_elements:
1247 element = self.theme_elements[element_title][0]
1248 colors = idleConf.GetHighlight(theme, element)
1249 if element == 'cursor': # Cursor sample needs special painting.
1250 colors['background'] = idleConf.GetHighlight(
1251 theme, 'normal', fgBg='bg')
1252 # Handle any unsaved changes to this theme.
1253 if theme in changes['highlight']:
1254 theme_dict = changes['highlight'][theme]
1255 if element + '-foreground' in theme_dict:
1256 colors['foreground'] = theme_dict[element + '-foreground']
1257 if element + '-background' in theme_dict:
1258 colors['background'] = theme_dict[element + '-background']
1259 self.highlight_sample.tag_config(element, **colors)
1260 self.set_color_sample()
1261
1262 def save_new(self, theme_name, theme):
1263 """Save a newly created theme to idleConf.
1264
1265 theme_name - string, the name of the new theme
1266 theme - dictionary containing the new theme
1267 """
1268 if not idleConf.userCfg['highlight'].has_section(theme_name):
1269 idleConf.userCfg['highlight'].add_section(theme_name)
1270 for element in theme:
1271 value = theme[element]
1272 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1273
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001274 def askyesno(self, *args, **kwargs):
1275 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001276 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001277
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001278 def delete_custom(self):
1279 """Handle event to delete custom theme.
1280
1281 The current theme is deactivated and the default theme is
1282 activated. The custom theme is permanently removed from
1283 the config file.
1284
1285 Attributes accessed:
1286 custom_name
1287
1288 Attributes updated:
1289 custom_theme_on
1290 customlist
1291 theme_source
1292 builtin_name
1293
1294 Methods:
1295 deactivate_current_config
1296 save_all_changed_extensions
1297 activate_config_changes
1298 set_theme_type
1299 """
1300 theme_name = self.custom_name.get()
1301 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001302 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001303 'Delete Theme', delmsg % theme_name, parent=self):
1304 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001305 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001306 # Remove theme from changes, config, and file.
1307 changes.delete_section('highlight', theme_name)
1308 # Reload user theme list.
1309 item_list = idleConf.GetSectionList('user', 'highlight')
1310 item_list.sort()
1311 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001312 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001313 self.customlist.SetMenu(item_list, '- no custom themes -')
1314 else:
1315 self.customlist.SetMenu(item_list, item_list[0])
1316 # Revert to default theme.
1317 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1318 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1319 # User can't back out of these changes, they must be applied now.
1320 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001321 self.cd.save_all_changed_extensions()
1322 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001323 self.set_theme_type()
1324
1325
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001326class KeysPage(Frame):
1327
1328 def __init__(self, master):
1329 super().__init__(master)
1330 self.cd = master.master
1331 self.create_page_keys()
1332 self.load_key_cfg()
1333
1334 def create_page_keys(self):
1335 """Return frame of widgets for Keys tab.
1336
1337 Enable users to provisionally change both individual and sets of
1338 keybindings (shortcut keys). Except for features implemented as
1339 extensions, keybindings are stored in complete sets called
1340 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1341 as far as the dialog is concerned. Any keyset can be used as the
1342 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1343
1344 Function load_key_cfg() initializes tk variables and keyset
1345 lists and calls load_keys_list for the current keyset.
1346 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1347 keyset_source, which controls if the current set of keybindings
1348 are from a builtin or custom keyset. DynOptionMenus builtinlist
1349 and customlist contain lists of the builtin and custom keysets,
1350 respectively, and the current item from each list is stored in
1351 vars builtin_name and custom_name.
1352
1353 Button delete_custom_keys invokes delete_custom_keys() to delete
1354 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1355 save_custom_keys invokes save_as_new_key_set() which calls
1356 get_new_keys_name() and create_new_key_set() to save a custom keyset
1357 and its keybindings to idleConf.userCfg['keys'].
1358
1359 Listbox bindingslist contains all of the keybindings for the
1360 selected keyset. The keybindings are loaded in load_keys_list()
1361 and are pairs of (event, [keys]) where keys can be a list
1362 of one or more key combinations to bind to the same event.
1363 Mouse button 1 click invokes on_bindingslist_select(), which
1364 allows button_new_keys to be clicked.
1365
1366 So, an item is selected in listbindings, which activates
1367 button_new_keys, and clicking button_new_keys calls function
1368 get_new_keys(). Function get_new_keys() gets the key mappings from the
1369 current keyset for the binding event item that was selected. The
1370 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001371 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001372 to be entered for that binding event. If the keys aren't
1373 changed, nothing happens. If the keys are changed and the keyset
1374 is a builtin, function get_new_keys_name() will be called
1375 for input of a custom keyset name. If no name is given, then the
1376 change to the keybinding will abort and no updates will be made. If
1377 a custom name is entered in the prompt or if the current keyset was
1378 already custom (and thus didn't require a prompt), then
1379 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1380 with the change to the event binding. The item listing in bindingslist
1381 is updated with the new keys. Var keybinding is also set which invokes
1382 the callback function, var_changed_keybinding, to add the change to
1383 the 'keys' or 'extensions' changes tracker based on the binding type.
1384
1385 Tk Variables:
1386 keybinding: Action/key bindings.
1387
1388 Methods:
1389 load_keys_list: Reload active set.
1390 create_new_key_set: Combine active keyset and changes.
1391 set_keys_type: Command for keyset_source.
1392 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1393 deactivate_current_config: Remove keys bindings in editors.
1394
1395 Widgets for KeysPage(frame): (*) widgets bound to self
1396 frame_key_sets: LabelFrame
1397 frames[0]: Frame
1398 (*)builtin_keyset_on: Radiobutton - var keyset_source
1399 (*)custom_keyset_on: Radiobutton - var keyset_source
1400 (*)builtinlist: DynOptionMenu - var builtin_name,
1401 func keybinding_selected
1402 (*)customlist: DynOptionMenu - var custom_name,
1403 func keybinding_selected
1404 (*)keys_message: Label
1405 frames[1]: Frame
1406 (*)button_delete_custom_keys: Button - delete_custom_keys
1407 (*)button_save_custom_keys: Button - save_as_new_key_set
1408 frame_custom: LabelFrame
1409 frame_target: Frame
1410 target_title: Label
1411 scroll_target_y: Scrollbar
1412 scroll_target_x: Scrollbar
1413 (*)bindingslist: ListBox - on_bindingslist_select
1414 (*)button_new_keys: Button - get_new_keys & ..._name
1415 """
1416 self.builtin_name = tracers.add(
1417 StringVar(self), self.var_changed_builtin_name)
1418 self.custom_name = tracers.add(
1419 StringVar(self), self.var_changed_custom_name)
1420 self.keyset_source = tracers.add(
1421 BooleanVar(self), self.var_changed_keyset_source)
1422 self.keybinding = tracers.add(
1423 StringVar(self), self.var_changed_keybinding)
1424
1425 # Create widgets:
1426 # body and section frames.
1427 frame_custom = LabelFrame(
1428 self, borderwidth=2, relief=GROOVE,
1429 text=' Custom Key Bindings ')
1430 frame_key_sets = LabelFrame(
1431 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1432 # frame_custom.
1433 frame_target = Frame(frame_custom)
1434 target_title = Label(frame_target, text='Action - Key(s)')
1435 scroll_target_y = Scrollbar(frame_target)
1436 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1437 self.bindingslist = Listbox(
1438 frame_target, takefocus=FALSE, exportselection=FALSE)
1439 self.bindingslist.bind('<ButtonRelease-1>',
1440 self.on_bindingslist_select)
1441 scroll_target_y['command'] = self.bindingslist.yview
1442 scroll_target_x['command'] = self.bindingslist.xview
1443 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1444 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1445 self.button_new_keys = Button(
1446 frame_custom, text='Get New Keys for Selection',
Terry Jan Reedye8f7c782017-11-28 21:52:32 -05001447 command=self.get_new_keys, state='disabled')
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001448 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001449 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001450 for i in range(2)]
1451 self.builtin_keyset_on = Radiobutton(
1452 frames[0], variable=self.keyset_source, value=1,
1453 command=self.set_keys_type, text='Use a Built-in Key Set')
1454 self.custom_keyset_on = Radiobutton(
1455 frames[0], variable=self.keyset_source, value=0,
1456 command=self.set_keys_type, text='Use a Custom Key Set')
1457 self.builtinlist = DynOptionMenu(
1458 frames[0], self.builtin_name, None, command=None)
1459 self.customlist = DynOptionMenu(
1460 frames[0], self.custom_name, None, command=None)
1461 self.button_delete_custom_keys = Button(
1462 frames[1], text='Delete Custom Key Set',
1463 command=self.delete_custom_keys)
1464 self.button_save_custom_keys = Button(
1465 frames[1], text='Save as New Custom Key Set',
1466 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001467 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001468
1469 # Pack widgets:
1470 # body.
1471 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1472 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1473 # frame_custom.
1474 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1475 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1476 # frame_target.
1477 frame_target.columnconfigure(0, weight=1)
1478 frame_target.rowconfigure(1, weight=1)
1479 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1480 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1481 scroll_target_y.grid(row=1, column=1, sticky=NS)
1482 scroll_target_x.grid(row=2, column=0, sticky=EW)
1483 # frame_key_sets.
1484 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1485 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1486 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1487 self.customlist.grid(row=1, column=1, sticky=NSEW)
1488 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1489 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1490 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1491 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1492 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1493
1494 def load_key_cfg(self):
1495 "Load current configuration settings for the keybinding options."
1496 # Set current keys type radiobutton.
1497 self.keyset_source.set(idleConf.GetOption(
1498 'main', 'Keys', 'default', type='bool', default=1))
1499 # Set current keys.
1500 current_option = idleConf.CurrentKeys()
1501 # Load available keyset option menus.
1502 if self.keyset_source.get(): # Default theme selected.
1503 item_list = idleConf.GetSectionList('default', 'keys')
1504 item_list.sort()
1505 self.builtinlist.SetMenu(item_list, current_option)
1506 item_list = idleConf.GetSectionList('user', 'keys')
1507 item_list.sort()
1508 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001509 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001510 self.custom_name.set('- no custom keys -')
1511 else:
1512 self.customlist.SetMenu(item_list, item_list[0])
1513 else: # User key set selected.
1514 item_list = idleConf.GetSectionList('user', 'keys')
1515 item_list.sort()
1516 self.customlist.SetMenu(item_list, current_option)
1517 item_list = idleConf.GetSectionList('default', 'keys')
1518 item_list.sort()
1519 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1520 self.set_keys_type()
1521 # Load keyset element list.
1522 keyset_name = idleConf.CurrentKeys()
1523 self.load_keys_list(keyset_name)
1524
1525 def var_changed_builtin_name(self, *params):
1526 "Process selection of builtin key set."
1527 old_keys = (
1528 'IDLE Classic Windows',
1529 'IDLE Classic Unix',
1530 'IDLE Classic Mac',
1531 'IDLE Classic OSX',
1532 )
1533 value = self.builtin_name.get()
1534 if value not in old_keys:
1535 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1536 changes.add_option('main', 'Keys', 'name', old_keys[0])
1537 changes.add_option('main', 'Keys', 'name2', value)
1538 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001539 else:
1540 changes.add_option('main', 'Keys', 'name', value)
1541 changes.add_option('main', 'Keys', 'name2', '')
1542 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001543 self.load_keys_list(value)
1544
1545 def var_changed_custom_name(self, *params):
1546 "Process selection of custom key set."
1547 value = self.custom_name.get()
1548 if value != '- no custom keys -':
1549 changes.add_option('main', 'Keys', 'name', value)
1550 self.load_keys_list(value)
1551
1552 def var_changed_keyset_source(self, *params):
1553 "Process toggle between builtin key set and custom key set."
1554 value = self.keyset_source.get()
1555 changes.add_option('main', 'Keys', 'default', value)
1556 if value:
1557 self.var_changed_builtin_name()
1558 else:
1559 self.var_changed_custom_name()
1560
1561 def var_changed_keybinding(self, *params):
1562 "Store change to a keybinding."
1563 value = self.keybinding.get()
1564 key_set = self.custom_name.get()
1565 event = self.bindingslist.get(ANCHOR).split()[0]
1566 if idleConf.IsCoreBinding(event):
1567 changes.add_option('keys', key_set, event, value)
1568 else: # Event is an extension binding.
1569 ext_name = idleConf.GetExtnNameForEvent(event)
1570 ext_keybind_section = ext_name + '_cfgBindings'
1571 changes.add_option('extensions', ext_keybind_section, event, value)
1572
1573 def set_keys_type(self):
1574 "Set available screen options based on builtin or custom key set."
1575 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001576 self.builtinlist['state'] = 'normal'
1577 self.customlist['state'] = 'disabled'
1578 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001579 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001580 self.builtinlist['state'] = 'disabled'
1581 self.custom_keyset_on.state(('!disabled',))
1582 self.customlist['state'] = 'normal'
1583 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001584
1585 def get_new_keys(self):
1586 """Handle event to change key binding for selected line.
1587
1588 A selection of a key/binding in the list of current
1589 bindings pops up a dialog to enter a new binding. If
1590 the current key set is builtin and a binding has
1591 changed, then a name for a custom key set needs to be
1592 entered for the change to be applied.
1593 """
1594 list_index = self.bindingslist.index(ANCHOR)
1595 binding = self.bindingslist.get(list_index)
1596 bind_name = binding.split()[0]
1597 if self.keyset_source.get():
1598 current_key_set_name = self.builtin_name.get()
1599 else:
1600 current_key_set_name = self.custom_name.get()
1601 current_bindings = idleConf.GetCurrentKeySet()
1602 if current_key_set_name in changes['keys']: # unsaved changes
1603 key_set_changes = changes['keys'][current_key_set_name]
1604 for event in key_set_changes:
1605 current_bindings[event] = key_set_changes[event].split()
1606 current_key_sequences = list(current_bindings.values())
1607 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1608 current_key_sequences).result
1609 if new_keys:
1610 if self.keyset_source.get(): # Current key set is a built-in.
1611 message = ('Your changes will be saved as a new Custom Key Set.'
1612 ' Enter a name for your new Custom Key Set below.')
1613 new_keyset = self.get_new_keys_name(message)
1614 if not new_keyset: # User cancelled custom key set creation.
1615 self.bindingslist.select_set(list_index)
1616 self.bindingslist.select_anchor(list_index)
1617 return
1618 else: # Create new custom key set based on previously active key set.
1619 self.create_new_key_set(new_keyset)
1620 self.bindingslist.delete(list_index)
1621 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1622 self.bindingslist.select_set(list_index)
1623 self.bindingslist.select_anchor(list_index)
1624 self.keybinding.set(new_keys)
1625 else:
1626 self.bindingslist.select_set(list_index)
1627 self.bindingslist.select_anchor(list_index)
1628
1629 def get_new_keys_name(self, message):
1630 "Return new key set name from query popup."
1631 used_names = (idleConf.GetSectionList('user', 'keys') +
1632 idleConf.GetSectionList('default', 'keys'))
1633 new_keyset = SectionName(
1634 self, 'New Custom Key Set', message, used_names).result
1635 return new_keyset
1636
1637 def save_as_new_key_set(self):
1638 "Prompt for name of new key set and save changes using that name."
1639 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1640 if new_keys_name:
1641 self.create_new_key_set(new_keys_name)
1642
1643 def on_bindingslist_select(self, event):
1644 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001645 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001646
1647 def create_new_key_set(self, new_key_set_name):
1648 """Create a new custom key set with the given name.
1649
1650 Copy the bindings/keys from the previously active keyset
1651 to the new keyset and activate the new custom keyset.
1652 """
1653 if self.keyset_source.get():
1654 prev_key_set_name = self.builtin_name.get()
1655 else:
1656 prev_key_set_name = self.custom_name.get()
1657 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1658 new_keys = {}
1659 for event in prev_keys: # Add key set to changed items.
1660 event_name = event[2:-2] # Trim off the angle brackets.
1661 binding = ' '.join(prev_keys[event])
1662 new_keys[event_name] = binding
1663 # Handle any unsaved changes to prev key set.
1664 if prev_key_set_name in changes['keys']:
1665 key_set_changes = changes['keys'][prev_key_set_name]
1666 for event in key_set_changes:
1667 new_keys[event] = key_set_changes[event]
1668 # Save the new key set.
1669 self.save_new_key_set(new_key_set_name, new_keys)
1670 # Change GUI over to the new key set.
1671 custom_key_list = idleConf.GetSectionList('user', 'keys')
1672 custom_key_list.sort()
1673 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1674 self.keyset_source.set(0)
1675 self.set_keys_type()
1676
1677 def load_keys_list(self, keyset_name):
1678 """Reload the list of action/key binding pairs for the active key set.
1679
1680 An action/key binding can be selected to change the key binding.
1681 """
1682 reselect = False
1683 if self.bindingslist.curselection():
1684 reselect = True
1685 list_index = self.bindingslist.index(ANCHOR)
1686 keyset = idleConf.GetKeySet(keyset_name)
1687 bind_names = list(keyset.keys())
1688 bind_names.sort()
1689 self.bindingslist.delete(0, END)
1690 for bind_name in bind_names:
1691 key = ' '.join(keyset[bind_name])
1692 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1693 if keyset_name in changes['keys']:
1694 # Handle any unsaved changes to this key set.
1695 if bind_name in changes['keys'][keyset_name]:
1696 key = changes['keys'][keyset_name][bind_name]
1697 self.bindingslist.insert(END, bind_name+' - '+key)
1698 if reselect:
1699 self.bindingslist.see(list_index)
1700 self.bindingslist.select_set(list_index)
1701 self.bindingslist.select_anchor(list_index)
1702
1703 @staticmethod
1704 def save_new_key_set(keyset_name, keyset):
1705 """Save a newly created core key set.
1706
1707 Add keyset to idleConf.userCfg['keys'], not to disk.
1708 If the keyset doesn't exist, it is created. The
1709 binding/keys are taken from the keyset argument.
1710
1711 keyset_name - string, the name of the new key set
1712 keyset - dictionary containing the new keybindings
1713 """
1714 if not idleConf.userCfg['keys'].has_section(keyset_name):
1715 idleConf.userCfg['keys'].add_section(keyset_name)
1716 for event in keyset:
1717 value = keyset[event]
1718 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1719
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001720 def askyesno(self, *args, **kwargs):
1721 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001722 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001723
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001724 def delete_custom_keys(self):
1725 """Handle event to delete a custom key set.
1726
1727 Applying the delete deactivates the current configuration and
1728 reverts to the default. The custom key set is permanently
1729 deleted from the config file.
1730 """
1731 keyset_name = self.custom_name.get()
1732 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001733 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001734 'Delete Key Set', delmsg % keyset_name, parent=self):
1735 return
1736 self.cd.deactivate_current_config()
1737 # Remove key set from changes, config, and file.
1738 changes.delete_section('keys', keyset_name)
1739 # Reload user key set list.
1740 item_list = idleConf.GetSectionList('user', 'keys')
1741 item_list.sort()
1742 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001743 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001744 self.customlist.SetMenu(item_list, '- no custom keys -')
1745 else:
1746 self.customlist.SetMenu(item_list, item_list[0])
1747 # Revert to default key set.
1748 self.keyset_source.set(idleConf.defaultCfg['main']
1749 .Get('Keys', 'default'))
1750 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1751 or idleConf.default_keys())
1752 # User can't back out of these changes, they must be applied now.
1753 changes.save_all()
1754 self.cd.save_all_changed_extensions()
1755 self.cd.activate_config_changes()
1756 self.set_keys_type()
1757
1758
csabellae8eb17b2017-07-30 18:39:17 -04001759class GenPage(Frame):
1760
csabella6f446be2017-08-01 00:24:07 -04001761 def __init__(self, master):
1762 super().__init__(master)
csabellae8eb17b2017-07-30 18:39:17 -04001763 self.create_page_general()
1764 self.load_general_cfg()
1765
1766 def create_page_general(self):
1767 """Return frame of widgets for General tab.
1768
1769 Enable users to provisionally change general options. Function
1770 load_general_cfg intializes tk variables and helplist using
1771 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1772 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1773 set var autosave. Entry boxes win_width_int and win_height_int
1774 set var win_width and win_height. Setting var_name invokes the
1775 default callback that adds option to changes.
1776
1777 Helplist: load_general_cfg loads list user_helplist with
1778 name, position pairs and copies names to listbox helplist.
1779 Clicking a name invokes help_source selected. Clicking
1780 button_helplist_name invokes helplist_item_name, which also
1781 changes user_helplist. These functions all call
1782 set_add_delete_state. All but load call update_help_changes to
1783 rewrite changes['main']['HelpFiles'].
1784
Cheryl Sabella2f896462017-08-14 21:21:43 -04001785 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001786 frame_window: LabelFrame
1787 frame_run: Frame
1788 startup_title: Label
1789 (*)startup_editor_on: Radiobutton - startup_edit
1790 (*)startup_shell_on: Radiobutton - startup_edit
1791 frame_win_size: Frame
1792 win_size_title: Label
1793 win_width_title: Label
1794 (*)win_width_int: Entry - win_width
1795 win_height_title: Label
1796 (*)win_height_int: Entry - win_height
Miss Islington (bot)5b933aa2018-02-04 15:39:22 -08001797 frame_autocomplete: Frame
1798 auto_wait_title: Label
1799 (*)auto_wait_int: Entry - autocomplete_wait
1800 frame_paren1: Frame
1801 paren_style_title: Label
1802 (*)paren_style_type: OptionMenu - paren_style
1803 frame_paren2: Frame
1804 paren_time_title: Label
1805 (*)paren_flash_time: Entry - flash_delay
1806 (*)bell_on: Checkbutton - paren_bell
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001807 frame_editor: LabelFrame
1808 frame_save: Frame
1809 run_save_title: Label
1810 (*)save_ask_on: Radiobutton - autosave
1811 (*)save_auto_on: Radiobutton - autosave
Miss Islington (bot)5b933aa2018-02-04 15:39:22 -08001812 frame_format: Frame
1813 format_width_title: Label
1814 (*)format_width_int: Entry - format_width
1815 frame_context: Frame
1816 context_title: Label
1817 (*)context_int: Entry - context_lines
Cheryl Sabella2f896462017-08-14 21:21:43 -04001818 frame_help: LabelFrame
1819 frame_helplist: Frame
1820 frame_helplist_buttons: Frame
1821 (*)button_helplist_edit
1822 (*)button_helplist_add
1823 (*)button_helplist_remove
1824 (*)helplist: ListBox
1825 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001826 """
wohlganger58fc71c2017-09-10 16:19:47 -05001827 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001828 self.startup_edit = tracers.add(
1829 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001830 self.win_width = tracers.add(
1831 StringVar(self), ('main', 'EditorWindow', 'width'))
1832 self.win_height = tracers.add(
1833 StringVar(self), ('main', 'EditorWindow', 'height'))
wohlganger58fc71c2017-09-10 16:19:47 -05001834 self.autocomplete_wait = tracers.add(
1835 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1836 self.paren_style = tracers.add(
1837 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1838 self.flash_delay = tracers.add(
1839 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1840 self.paren_bell = tracers.add(
1841 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001842
wohlganger58fc71c2017-09-10 16:19:47 -05001843 self.autosave = tracers.add(
1844 IntVar(self), ('main', 'General', 'autosave'))
1845 self.format_width = tracers.add(
1846 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
1847 self.context_lines = tracers.add(
Miss Islington (bot)0800b6c2018-06-01 16:45:54 -07001848 StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
wohlganger58fc71c2017-09-10 16:19:47 -05001849
1850 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001851 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001852 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1853 text=' Window Preferences')
1854 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1855 text=' Editor Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001856 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1857 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001858 # Frame_window.
1859 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001860 startup_title = Label(frame_run, text='At Startup')
1861 self.startup_editor_on = Radiobutton(
1862 frame_run, variable=self.startup_edit, value=1,
1863 text="Open Edit Window")
1864 self.startup_shell_on = Radiobutton(
1865 frame_run, variable=self.startup_edit, value=0,
1866 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001867
wohlganger58fc71c2017-09-10 16:19:47 -05001868 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001869 win_size_title = Label(
1870 frame_win_size, text='Initial Window Size (in characters)')
1871 win_width_title = Label(frame_win_size, text='Width')
1872 self.win_width_int = Entry(
1873 frame_win_size, textvariable=self.win_width, width=3)
1874 win_height_title = Label(frame_win_size, text='Height')
1875 self.win_height_int = Entry(
1876 frame_win_size, textvariable=self.win_height, width=3)
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001877
wohlganger58fc71c2017-09-10 16:19:47 -05001878 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1879 auto_wait_title = Label(frame_autocomplete,
1880 text='Completions Popup Wait (milliseconds)')
1881 self.auto_wait_int = Entry(frame_autocomplete, width=6,
1882 textvariable=self.autocomplete_wait)
1883
1884 frame_paren1 = Frame(frame_window, borderwidth=0)
1885 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1886 self.paren_style_type = OptionMenu(
1887 frame_paren1, self.paren_style, 'expression',
1888 "opener","parens","expression")
1889 frame_paren2 = Frame(frame_window, borderwidth=0)
1890 paren_time_title = Label(
1891 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1892 '(0 is until next input)')
1893 self.paren_flash_time = Entry(
1894 frame_paren2, textvariable=self.flash_delay, width=6)
1895 self.bell_on = Checkbutton(
1896 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1897
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001898 # Frame_editor.
1899 frame_save = Frame(frame_editor, borderwidth=0)
1900 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1901 self.save_ask_on = Radiobutton(
1902 frame_save, variable=self.autosave, value=0,
1903 text="Prompt to Save")
1904 self.save_auto_on = Radiobutton(
1905 frame_save, variable=self.autosave, value=1,
1906 text='No Prompt')
1907
wohlganger58fc71c2017-09-10 16:19:47 -05001908 frame_format = Frame(frame_editor, borderwidth=0)
1909 format_width_title = Label(frame_format,
1910 text='Format Paragraph Max Width')
1911 self.format_width_int = Entry(
1912 frame_format, textvariable=self.format_width, width=4)
1913
1914 frame_context = Frame(frame_editor, borderwidth=0)
Miss Islington (bot)0800b6c2018-06-01 16:45:54 -07001915 context_title = Label(frame_context, text='Max Context Lines :')
wohlganger58fc71c2017-09-10 16:19:47 -05001916 self.context_int = Entry(
1917 frame_context, textvariable=self.context_lines, width=3)
1918
1919
csabellae8eb17b2017-07-30 18:39:17 -04001920 # frame_help.
1921 frame_helplist = Frame(frame_help)
1922 frame_helplist_buttons = Frame(frame_helplist)
1923 self.helplist = Listbox(
1924 frame_helplist, height=5, takefocus=True,
1925 exportselection=FALSE)
1926 scroll_helplist = Scrollbar(frame_helplist)
1927 scroll_helplist['command'] = self.helplist.yview
1928 self.helplist['yscrollcommand'] = scroll_helplist.set
1929 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1930 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001931 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001932 width=8, command=self.helplist_item_edit)
1933 self.button_helplist_add = Button(
1934 frame_helplist_buttons, text='Add',
1935 width=8, command=self.helplist_item_add)
1936 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001937 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001938 width=8, command=self.helplist_item_remove)
1939
1940 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001941 # Body.
1942 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1943 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04001944 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1945 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001946 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001947 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1948 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1949 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04001950 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001951 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001952 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1953 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1954 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1955 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1956 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001957 # frame_autocomplete.
1958 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
1959 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1960 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
1961 # frame_paren.
1962 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
1963 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1964 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
1965 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
1966 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
1967 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
1968 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
1969
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001970 # frame_save.
1971 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
1972 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1973 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1974 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001975 # frame_format.
1976 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
1977 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1978 self.format_width_int.pack(side=TOP, padx=10, pady=5)
1979 # frame_context.
1980 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
1981 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1982 self.context_int.pack(side=TOP, padx=5, pady=5)
1983
csabellae8eb17b2017-07-30 18:39:17 -04001984 # frame_help.
1985 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1986 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1987 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1988 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1989 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1990 self.button_helplist_add.pack(side=TOP, anchor=W)
1991 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1992
1993 def load_general_cfg(self):
1994 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05001995 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04001996 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05001997 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04001998 self.win_width.set(idleConf.GetOption(
1999 'main', 'EditorWindow', 'width', type='int'))
2000 self.win_height.set(idleConf.GetOption(
2001 'main', 'EditorWindow', 'height', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002002 self.autocomplete_wait.set(idleConf.GetOption(
2003 'extensions', 'AutoComplete', 'popupwait', type='int'))
2004 self.paren_style.set(idleConf.GetOption(
2005 'extensions', 'ParenMatch', 'style'))
2006 self.flash_delay.set(idleConf.GetOption(
2007 'extensions', 'ParenMatch', 'flash-delay', type='int'))
2008 self.paren_bell.set(idleConf.GetOption(
2009 'extensions', 'ParenMatch', 'bell'))
2010
2011 # Set variables for editor windows.
2012 self.autosave.set(idleConf.GetOption(
2013 'main', 'General', 'autosave', default=0, type='bool'))
2014 self.format_width.set(idleConf.GetOption(
2015 'extensions', 'FormatParagraph', 'max-width', type='int'))
2016 self.context_lines.set(idleConf.GetOption(
Miss Islington (bot)0800b6c2018-06-01 16:45:54 -07002017 'extensions', 'CodeContext', 'maxlines', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002018
csabellae8eb17b2017-07-30 18:39:17 -04002019 # Set additional help sources.
2020 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2021 self.helplist.delete(0, 'end')
2022 for help_item in self.user_helplist:
2023 self.helplist.insert(END, help_item[0])
2024 self.set_add_delete_state()
2025
2026 def help_source_selected(self, event):
2027 "Handle event for selecting additional help."
2028 self.set_add_delete_state()
2029
2030 def set_add_delete_state(self):
2031 "Toggle the state for the help list buttons based on list entries."
2032 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002033 self.button_helplist_edit.state(('disabled',))
2034 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002035 else: # Some entries.
2036 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002037 self.button_helplist_edit.state(('!disabled',))
2038 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002039 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002040 self.button_helplist_edit.state(('disabled',))
2041 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002042
2043 def helplist_item_add(self):
2044 """Handle add button for the help list.
2045
2046 Query for name and location of new help sources and add
2047 them to the list.
2048 """
2049 help_source = HelpSource(self, 'New Help Source').result
2050 if help_source:
2051 self.user_helplist.append(help_source)
2052 self.helplist.insert(END, help_source[0])
2053 self.update_help_changes()
2054
2055 def helplist_item_edit(self):
2056 """Handle edit button for the help list.
2057
2058 Query with existing help source information and update
2059 config if the values are changed.
2060 """
2061 item_index = self.helplist.index(ANCHOR)
2062 help_source = self.user_helplist[item_index]
2063 new_help_source = HelpSource(
2064 self, 'Edit Help Source',
2065 menuitem=help_source[0],
2066 filepath=help_source[1],
2067 ).result
2068 if new_help_source and new_help_source != help_source:
2069 self.user_helplist[item_index] = new_help_source
2070 self.helplist.delete(item_index)
2071 self.helplist.insert(item_index, new_help_source[0])
2072 self.update_help_changes()
2073 self.set_add_delete_state() # Selected will be un-selected
2074
2075 def helplist_item_remove(self):
2076 """Handle remove button for the help list.
2077
2078 Delete the help list item from config.
2079 """
2080 item_index = self.helplist.index(ANCHOR)
2081 del(self.user_helplist[item_index])
2082 self.helplist.delete(item_index)
2083 self.update_help_changes()
2084 self.set_add_delete_state()
2085
2086 def update_help_changes(self):
2087 "Clear and rebuild the HelpFiles section in changes"
2088 changes['main']['HelpFiles'] = {}
2089 for num in range(1, len(self.user_helplist) + 1):
2090 changes.add_option(
2091 'main', 'HelpFiles', str(num),
2092 ';'.join(self.user_helplist[num-1][:2]))
2093
2094
csabella45bf7232017-07-26 19:09:58 -04002095class VarTrace:
2096 """Maintain Tk variables trace state."""
2097
2098 def __init__(self):
2099 """Store Tk variables and callbacks.
2100
2101 untraced: List of tuples (var, callback)
2102 that do not have the callback attached
2103 to the Tk var.
2104 traced: List of tuples (var, callback) where
2105 that callback has been attached to the var.
2106 """
2107 self.untraced = []
2108 self.traced = []
2109
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002110 def clear(self):
2111 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002112 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002113 self.untraced.clear()
2114 self.traced.clear()
2115
csabella45bf7232017-07-26 19:09:58 -04002116 def add(self, var, callback):
2117 """Add (var, callback) tuple to untraced list.
2118
2119 Args:
2120 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002121 callback: Either function name to be used as a callback
2122 or a tuple with IdleConf config-type, section, and
2123 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002124
2125 Return:
2126 Tk variable instance.
2127 """
2128 if isinstance(callback, tuple):
2129 callback = self.make_callback(var, callback)
2130 self.untraced.append((var, callback))
2131 return var
2132
2133 @staticmethod
2134 def make_callback(var, config):
2135 "Return default callback function to add values to changes instance."
2136 def default_callback(*params):
2137 "Add config values to changes instance."
2138 changes.add_option(*config, var.get())
2139 return default_callback
2140
2141 def attach(self):
2142 "Attach callback to all vars that are not traced."
2143 while self.untraced:
2144 var, callback = self.untraced.pop()
2145 var.trace_add('write', callback)
2146 self.traced.append((var, callback))
2147
2148 def detach(self):
2149 "Remove callback from traced vars."
2150 while self.traced:
2151 var, callback = self.traced.pop()
2152 var.trace_remove('write', var.trace_info()[0][1])
2153 self.untraced.append((var, callback))
2154
2155
csabella5b591542017-07-28 14:40:59 -04002156tracers = VarTrace()
2157
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002158help_common = '''\
2159When you click either the Apply or Ok buttons, settings in this
2160dialog that are different from IDLE's default are saved in
2161a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002162these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002163machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002164'''
2165help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002166 'Fonts/Tabs':'''
2167Font sample: This shows what a selection of Basic Multilingual Plane
2168unicode characters look like for the current font selection. If the
2169selected font does not define a character, Tk attempts to find another
2170font that does. Substitute glyphs depend on what is available on a
2171particular system and will not necessarily have the same size as the
2172font selected. Line contains 20 characters up to Devanagari, 14 for
2173Tamil, and 10 for East Asia.
2174
2175Hebrew and Arabic letters should display right to left, starting with
2176alef, \u05d0 and \u0627. Arabic digits display left to right. The
2177Devanagari and Tamil lines start with digits. The East Asian lines
2178are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2179Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002180
2181You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002182''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002183 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002184Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002185The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002186be used with older IDLE releases if it is saved as a custom
2187theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002188''',
2189 'Keys': '''
2190Keys:
2191The IDLE Modern Unix key set is new in June 2016. It can only
2192be used with older IDLE releases if it is saved as a custom
2193key set, with a different name.
2194''',
wohlganger58fc71c2017-09-10 16:19:47 -05002195 'General': '''
2196General:
wohlgangerfae2c352017-06-27 21:36:23 -05002197
wohlganger58fc71c2017-09-10 16:19:47 -05002198AutoComplete: Popupwait is milleseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002199cursor movement, before popping up completion box. Key char is '.' after
2200identifier or a '/' (or '\\' on Windows) within a string.
2201
2202FormatParagraph: Max-width is max chars in lines after re-formatting.
2203Use with paragraphs in both strings and comment blocks.
2204
2205ParenMatch: Style indicates what is highlighted when closer is entered:
2206'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2207'expression' (default) - also everything in between. Flash-delay is how
2208long to highlight if cursor is not moved (0 means forever).
Miss Islington (bot)0800b6c2018-06-01 16:45:54 -07002209
2210CodeContext: Maxlines is the maximum number of code context lines to
2211display when Code Context is turned on for an editor window.
wohlgangerfae2c352017-06-27 21:36:23 -05002212'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002213}
2214
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002215
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002216def is_int(s):
2217 "Return 's is blank or represents an int'"
2218 if not s:
2219 return True
2220 try:
2221 int(s)
2222 return True
2223 except ValueError:
2224 return False
2225
2226
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002227class VerticalScrolledFrame(Frame):
2228 """A pure Tkinter vertically scrollable frame.
2229
2230 * Use the 'interior' attribute to place widgets inside the scrollable frame
2231 * Construct and pack/place/grid normally
2232 * This frame only allows vertical scrolling
2233 """
2234 def __init__(self, parent, *args, **kw):
2235 Frame.__init__(self, parent, *args, **kw)
2236
csabella7eb58832017-07-04 21:30:58 -04002237 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002238 vscrollbar = Scrollbar(self, orient=VERTICAL)
2239 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002240 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002241 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002242 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2243 vscrollbar.config(command=canvas.yview)
2244
csabella7eb58832017-07-04 21:30:58 -04002245 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002246 canvas.xview_moveto(0)
2247 canvas.yview_moveto(0)
2248
csabella7eb58832017-07-04 21:30:58 -04002249 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002250 self.interior = interior = Frame(canvas)
2251 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2252
csabella7eb58832017-07-04 21:30:58 -04002253 # Track changes to the canvas and frame width and sync them,
2254 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002255 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002256 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002257 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2258 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002259 interior.bind('<Configure>', _configure_interior)
2260
2261 def _configure_canvas(event):
2262 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002263 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002264 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2265 canvas.bind('<Configure>', _configure_canvas)
2266
2267 return
2268
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002269
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002270if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04002271 import unittest
2272 unittest.main('idlelib.idle_test.test_configdialog',
2273 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002274 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002275 run(ConfigDialog)