blob: 4e8394be928112bb3c9b6aa09c6af4a70c46ca89 [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
498 font_sample and to highlight_sample on the hightlight page.
499
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.
649 Updates font_sample and hightlight page highlight_sample.
650 """
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'),
802 'Python Keywords': ('keyword', '01'),
803 'Python Definitions': ('definition', '02'),
804 'Python Builtins': ('builtin', '03'),
805 'Python Comments': ('comment', '04'),
806 'Python Strings': ('string', '05'),
807 'Selected Text': ('hilite', '06'),
808 'Found Text': ('hit', '07'),
809 'Cursor': ('cursor', '08'),
810 'Editor Breakpoint': ('break', '09'),
811 'Shell Normal Text': ('console', '10'),
812 'Shell Error Text': ('error', '11'),
813 'Shell Stdout Text': ('stdout', '12'),
814 'Shell Stderr Text': ('stderr', '13'),
815 }
816 self.builtin_name = tracers.add(
817 StringVar(self), self.var_changed_builtin_name)
818 self.custom_name = tracers.add(
819 StringVar(self), self.var_changed_custom_name)
820 self.fg_bg_toggle = BooleanVar(self)
821 self.color = tracers.add(
822 StringVar(self), self.var_changed_color)
823 self.theme_source = tracers.add(
824 BooleanVar(self), self.var_changed_theme_source)
825 self.highlight_target = tracers.add(
826 StringVar(self), self.var_changed_highlight_target)
827
828 # Create widgets:
829 # body frame and section frames.
830 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
831 text=' Custom Highlighting ')
832 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
833 text=' Highlighting Theme ')
834 # frame_custom.
835 text = self.highlight_sample = Text(
836 frame_custom, relief=SOLID, borderwidth=1,
837 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
838 takefocus=FALSE, highlightthickness=0, wrap=NONE)
839 text.bind('<Double-Button-1>', lambda e: 'break')
840 text.bind('<B1-Motion>', lambda e: 'break')
wohlganger58fc71c2017-09-10 16:19:47 -0500841 text_and_tags=(
842 ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400843 ('#you can click here', 'comment'), ('\n', 'normal'),
844 ('#to choose items', 'comment'), ('\n', 'normal'),
845 ('def', 'keyword'), (' ', 'normal'),
846 ('func', 'definition'), ('(param):\n ', 'normal'),
847 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
848 ("'string'", 'string'), ('\n var1 = ', 'normal'),
849 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
850 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
851 ('list', 'builtin'), ('(', 'normal'),
852 ('None', 'keyword'), (')\n', 'normal'),
853 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
854 (' error ', 'error'), (' ', 'normal'),
855 ('cursor |', 'cursor'), ('\n ', 'normal'),
856 ('shell', 'console'), (' ', 'normal'),
857 ('stdout', 'stdout'), (' ', 'normal'),
858 ('stderr', 'stderr'), ('\n\n', 'normal'))
859 for texttag in text_and_tags:
860 text.insert(END, texttag[0], texttag[1])
861 for element in self.theme_elements:
862 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400863 # event.widget.winfo_top_level().highlight_target.set(elem)
864 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400865 text.tag_bind(
866 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400867 text['state'] = 'disabled'
868 self.style.configure('frame_color_set.TFrame', borderwidth=1,
869 relief='solid')
870 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400871 frame_fg_bg_toggle = Frame(frame_custom)
872 self.button_set_color = Button(
873 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400874 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400875 self.targetlist = DynOptionMenu(
876 self.frame_color_set, self.highlight_target, None,
877 highlightthickness=0) #, command=self.set_highlight_targetBinding
878 self.fg_on = Radiobutton(
879 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
880 text='Foreground', command=self.set_color_sample_binding)
881 self.bg_on = Radiobutton(
882 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
883 text='Background', command=self.set_color_sample_binding)
884 self.fg_bg_toggle.set(1)
885 self.button_save_custom = Button(
886 frame_custom, text='Save as New Custom Theme',
887 command=self.save_as_new_theme)
888 # frame_theme.
889 theme_type_title = Label(frame_theme, text='Select : ')
890 self.builtin_theme_on = Radiobutton(
891 frame_theme, variable=self.theme_source, value=1,
892 command=self.set_theme_type, text='a Built-in Theme')
893 self.custom_theme_on = Radiobutton(
894 frame_theme, variable=self.theme_source, value=0,
895 command=self.set_theme_type, text='a Custom Theme')
896 self.builtinlist = DynOptionMenu(
897 frame_theme, self.builtin_name, None, command=None)
898 self.customlist = DynOptionMenu(
899 frame_theme, self.custom_name, None, command=None)
900 self.button_delete_custom = Button(
901 frame_theme, text='Delete Custom Theme',
902 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400903 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400904 # Pack widgets:
905 # body.
906 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500907 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400908 # frame_custom.
909 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
910 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
911 self.highlight_sample.pack(
912 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
913 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
914 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
915 self.fg_on.pack(side=LEFT, anchor=E)
916 self.bg_on.pack(side=RIGHT, anchor=W)
917 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
918 # frame_theme.
919 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
920 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
921 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
922 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
923 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
924 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
925 self.theme_message.pack(side=TOP, fill=X, pady=5)
926
927 def load_theme_cfg(self):
928 """Load current configuration settings for the theme options.
929
930 Based on the theme_source toggle, the theme is set as
931 either builtin or custom and the initial widget values
932 reflect the current settings from idleConf.
933
934 Attributes updated:
935 theme_source: Set from idleConf.
936 builtinlist: List of default themes from idleConf.
937 customlist: List of custom themes from idleConf.
938 custom_theme_on: Disabled if there are no custom themes.
939 custom_theme: Message with additional information.
940 targetlist: Create menu from self.theme_elements.
941
942 Methods:
943 set_theme_type
944 paint_theme_sample
945 set_highlight_target
946 """
947 # Set current theme type radiobutton.
948 self.theme_source.set(idleConf.GetOption(
949 'main', 'Theme', 'default', type='bool', default=1))
950 # Set current theme.
951 current_option = idleConf.CurrentTheme()
952 # Load available theme option menus.
953 if self.theme_source.get(): # Default theme selected.
954 item_list = idleConf.GetSectionList('default', 'highlight')
955 item_list.sort()
956 self.builtinlist.SetMenu(item_list, current_option)
957 item_list = idleConf.GetSectionList('user', 'highlight')
958 item_list.sort()
959 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400960 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400961 self.custom_name.set('- no custom themes -')
962 else:
963 self.customlist.SetMenu(item_list, item_list[0])
964 else: # User theme selected.
965 item_list = idleConf.GetSectionList('user', 'highlight')
966 item_list.sort()
967 self.customlist.SetMenu(item_list, current_option)
968 item_list = idleConf.GetSectionList('default', 'highlight')
969 item_list.sort()
970 self.builtinlist.SetMenu(item_list, item_list[0])
971 self.set_theme_type()
972 # Load theme element option menu.
973 theme_names = list(self.theme_elements.keys())
974 theme_names.sort(key=lambda x: self.theme_elements[x][1])
975 self.targetlist.SetMenu(theme_names, theme_names[0])
976 self.paint_theme_sample()
977 self.set_highlight_target()
978
979 def var_changed_builtin_name(self, *params):
980 """Process new builtin theme selection.
981
982 Add the changed theme's name to the changed_items and recreate
983 the sample with the values from the selected theme.
984 """
985 old_themes = ('IDLE Classic', 'IDLE New')
986 value = self.builtin_name.get()
987 if value not in old_themes:
988 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
989 changes.add_option('main', 'Theme', 'name', old_themes[0])
990 changes.add_option('main', 'Theme', 'name2', value)
991 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400992 else:
993 changes.add_option('main', 'Theme', 'name', value)
994 changes.add_option('main', 'Theme', 'name2', '')
995 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400996 self.paint_theme_sample()
997
998 def var_changed_custom_name(self, *params):
999 """Process new custom theme selection.
1000
1001 If a new custom theme is selected, add the name to the
1002 changed_items and apply the theme to the sample.
1003 """
1004 value = self.custom_name.get()
1005 if value != '- no custom themes -':
1006 changes.add_option('main', 'Theme', 'name', value)
1007 self.paint_theme_sample()
1008
1009 def var_changed_theme_source(self, *params):
1010 """Process toggle between builtin and custom theme.
1011
1012 Update the default toggle value and apply the newly
1013 selected theme type.
1014 """
1015 value = self.theme_source.get()
1016 changes.add_option('main', 'Theme', 'default', value)
1017 if value:
1018 self.var_changed_builtin_name()
1019 else:
1020 self.var_changed_custom_name()
1021
1022 def var_changed_color(self, *params):
1023 "Process change to color choice."
1024 self.on_new_color_set()
1025
1026 def var_changed_highlight_target(self, *params):
1027 "Process selection of new target tag for highlighting."
1028 self.set_highlight_target()
1029
1030 def set_theme_type(self):
1031 """Set available screen options based on builtin or custom theme.
1032
1033 Attributes accessed:
1034 theme_source
1035
1036 Attributes updated:
1037 builtinlist
1038 customlist
1039 button_delete_custom
1040 custom_theme_on
1041
1042 Called from:
1043 handler for builtin_theme_on and custom_theme_on
1044 delete_custom
1045 create_new
1046 load_theme_cfg
1047 """
1048 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001049 self.builtinlist['state'] = 'normal'
1050 self.customlist['state'] = 'disabled'
1051 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001052 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001053 self.builtinlist['state'] = 'disabled'
1054 self.custom_theme_on.state(('!disabled',))
1055 self.customlist['state'] = 'normal'
1056 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001057
1058 def get_color(self):
1059 """Handle button to select a new color for the target tag.
1060
1061 If a new color is selected while using a builtin theme, a
1062 name must be supplied to create a custom theme.
1063
1064 Attributes accessed:
1065 highlight_target
1066 frame_color_set
1067 theme_source
1068
1069 Attributes updated:
1070 color
1071
1072 Methods:
1073 get_new_theme_name
1074 create_new
1075 """
1076 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001077 prev_color = self.style.lookup(self.frame_color_set['style'],
1078 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001079 rgbTuplet, color_string = tkColorChooser.askcolor(
1080 parent=self, title='Pick new color for : '+target,
1081 initialcolor=prev_color)
1082 if color_string and (color_string != prev_color):
1083 # User didn't cancel and they chose a new color.
1084 if self.theme_source.get(): # Current theme is a built-in.
1085 message = ('Your changes will be saved as a new Custom Theme. '
1086 'Enter a name for your new Custom Theme below.')
1087 new_theme = self.get_new_theme_name(message)
1088 if not new_theme: # User cancelled custom theme creation.
1089 return
1090 else: # Create new custom theme based on previously active theme.
1091 self.create_new(new_theme)
1092 self.color.set(color_string)
1093 else: # Current theme is user defined.
1094 self.color.set(color_string)
1095
1096 def on_new_color_set(self):
1097 "Display sample of new color selection on the dialog."
1098 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001099 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001100 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1101 sample_element = self.theme_elements[self.highlight_target.get()][0]
1102 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1103 theme = self.custom_name.get()
1104 theme_element = sample_element + '-' + plane
1105 changes.add_option('highlight', theme, theme_element, new_color)
1106
1107 def get_new_theme_name(self, message):
1108 "Return name of new theme from query popup."
1109 used_names = (idleConf.GetSectionList('user', 'highlight') +
1110 idleConf.GetSectionList('default', 'highlight'))
1111 new_theme = SectionName(
1112 self, 'New Custom Theme', message, used_names).result
1113 return new_theme
1114
1115 def save_as_new_theme(self):
1116 """Prompt for new theme name and create the theme.
1117
1118 Methods:
1119 get_new_theme_name
1120 create_new
1121 """
1122 new_theme_name = self.get_new_theme_name('New Theme Name:')
1123 if new_theme_name:
1124 self.create_new(new_theme_name)
1125
1126 def create_new(self, new_theme_name):
1127 """Create a new custom theme with the given name.
1128
1129 Create the new theme based on the previously active theme
1130 with the current changes applied. Once it is saved, then
1131 activate the new theme.
1132
1133 Attributes accessed:
1134 builtin_name
1135 custom_name
1136
1137 Attributes updated:
1138 customlist
1139 theme_source
1140
1141 Method:
1142 save_new
1143 set_theme_type
1144 """
1145 if self.theme_source.get():
1146 theme_type = 'default'
1147 theme_name = self.builtin_name.get()
1148 else:
1149 theme_type = 'user'
1150 theme_name = self.custom_name.get()
1151 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1152 # Apply any of the old theme's unsaved changes to the new theme.
1153 if theme_name in changes['highlight']:
1154 theme_changes = changes['highlight'][theme_name]
1155 for element in theme_changes:
1156 new_theme[element] = theme_changes[element]
1157 # Save the new theme.
1158 self.save_new(new_theme_name, new_theme)
1159 # Change GUI over to the new theme.
1160 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1161 custom_theme_list.sort()
1162 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1163 self.theme_source.set(0)
1164 self.set_theme_type()
1165
1166 def set_highlight_target(self):
1167 """Set fg/bg toggle and color based on highlight tag target.
1168
1169 Instance variables accessed:
1170 highlight_target
1171
1172 Attributes updated:
1173 fg_on
1174 bg_on
1175 fg_bg_toggle
1176
1177 Methods:
1178 set_color_sample
1179
1180 Called from:
1181 var_changed_highlight_target
1182 load_theme_cfg
1183 """
1184 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001185 self.fg_on.state(('disabled',))
1186 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001187 self.fg_bg_toggle.set(1)
1188 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001189 self.fg_on.state(('!disabled',))
1190 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001191 self.fg_bg_toggle.set(1)
1192 self.set_color_sample()
1193
1194 def set_color_sample_binding(self, *args):
1195 """Change color sample based on foreground/background toggle.
1196
1197 Methods:
1198 set_color_sample
1199 """
1200 self.set_color_sample()
1201
1202 def set_color_sample(self):
1203 """Set the color of the frame background to reflect the selected target.
1204
1205 Instance variables accessed:
1206 theme_elements
1207 highlight_target
1208 fg_bg_toggle
1209 highlight_sample
1210
1211 Attributes updated:
1212 frame_color_set
1213 """
1214 # Set the color sample area.
1215 tag = self.theme_elements[self.highlight_target.get()][0]
1216 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1217 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001218 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001219
1220 def paint_theme_sample(self):
1221 """Apply the theme colors to each element tag in the sample text.
1222
1223 Instance attributes accessed:
1224 theme_elements
1225 theme_source
1226 builtin_name
1227 custom_name
1228
1229 Attributes updated:
1230 highlight_sample: Set the tag elements to the theme.
1231
1232 Methods:
1233 set_color_sample
1234
1235 Called from:
1236 var_changed_builtin_name
1237 var_changed_custom_name
1238 load_theme_cfg
1239 """
1240 if self.theme_source.get(): # Default theme
1241 theme = self.builtin_name.get()
1242 else: # User theme
1243 theme = self.custom_name.get()
1244 for element_title in self.theme_elements:
1245 element = self.theme_elements[element_title][0]
1246 colors = idleConf.GetHighlight(theme, element)
1247 if element == 'cursor': # Cursor sample needs special painting.
1248 colors['background'] = idleConf.GetHighlight(
1249 theme, 'normal', fgBg='bg')
1250 # Handle any unsaved changes to this theme.
1251 if theme in changes['highlight']:
1252 theme_dict = changes['highlight'][theme]
1253 if element + '-foreground' in theme_dict:
1254 colors['foreground'] = theme_dict[element + '-foreground']
1255 if element + '-background' in theme_dict:
1256 colors['background'] = theme_dict[element + '-background']
1257 self.highlight_sample.tag_config(element, **colors)
1258 self.set_color_sample()
1259
1260 def save_new(self, theme_name, theme):
1261 """Save a newly created theme to idleConf.
1262
1263 theme_name - string, the name of the new theme
1264 theme - dictionary containing the new theme
1265 """
1266 if not idleConf.userCfg['highlight'].has_section(theme_name):
1267 idleConf.userCfg['highlight'].add_section(theme_name)
1268 for element in theme:
1269 value = theme[element]
1270 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1271
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001272 def askyesno(self, *args, **kwargs):
1273 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001274 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001275
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001276 def delete_custom(self):
1277 """Handle event to delete custom theme.
1278
1279 The current theme is deactivated and the default theme is
1280 activated. The custom theme is permanently removed from
1281 the config file.
1282
1283 Attributes accessed:
1284 custom_name
1285
1286 Attributes updated:
1287 custom_theme_on
1288 customlist
1289 theme_source
1290 builtin_name
1291
1292 Methods:
1293 deactivate_current_config
1294 save_all_changed_extensions
1295 activate_config_changes
1296 set_theme_type
1297 """
1298 theme_name = self.custom_name.get()
1299 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001300 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001301 'Delete Theme', delmsg % theme_name, parent=self):
1302 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001303 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001304 # Remove theme from changes, config, and file.
1305 changes.delete_section('highlight', theme_name)
1306 # Reload user theme list.
1307 item_list = idleConf.GetSectionList('user', 'highlight')
1308 item_list.sort()
1309 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001310 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001311 self.customlist.SetMenu(item_list, '- no custom themes -')
1312 else:
1313 self.customlist.SetMenu(item_list, item_list[0])
1314 # Revert to default theme.
1315 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1316 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1317 # User can't back out of these changes, they must be applied now.
1318 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001319 self.cd.save_all_changed_extensions()
1320 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001321 self.set_theme_type()
1322
1323
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001324class KeysPage(Frame):
1325
1326 def __init__(self, master):
1327 super().__init__(master)
1328 self.cd = master.master
1329 self.create_page_keys()
1330 self.load_key_cfg()
1331
1332 def create_page_keys(self):
1333 """Return frame of widgets for Keys tab.
1334
1335 Enable users to provisionally change both individual and sets of
1336 keybindings (shortcut keys). Except for features implemented as
1337 extensions, keybindings are stored in complete sets called
1338 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1339 as far as the dialog is concerned. Any keyset can be used as the
1340 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1341
1342 Function load_key_cfg() initializes tk variables and keyset
1343 lists and calls load_keys_list for the current keyset.
1344 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1345 keyset_source, which controls if the current set of keybindings
1346 are from a builtin or custom keyset. DynOptionMenus builtinlist
1347 and customlist contain lists of the builtin and custom keysets,
1348 respectively, and the current item from each list is stored in
1349 vars builtin_name and custom_name.
1350
1351 Button delete_custom_keys invokes delete_custom_keys() to delete
1352 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1353 save_custom_keys invokes save_as_new_key_set() which calls
1354 get_new_keys_name() and create_new_key_set() to save a custom keyset
1355 and its keybindings to idleConf.userCfg['keys'].
1356
1357 Listbox bindingslist contains all of the keybindings for the
1358 selected keyset. The keybindings are loaded in load_keys_list()
1359 and are pairs of (event, [keys]) where keys can be a list
1360 of one or more key combinations to bind to the same event.
1361 Mouse button 1 click invokes on_bindingslist_select(), which
1362 allows button_new_keys to be clicked.
1363
1364 So, an item is selected in listbindings, which activates
1365 button_new_keys, and clicking button_new_keys calls function
1366 get_new_keys(). Function get_new_keys() gets the key mappings from the
1367 current keyset for the binding event item that was selected. The
1368 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001369 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001370 to be entered for that binding event. If the keys aren't
1371 changed, nothing happens. If the keys are changed and the keyset
1372 is a builtin, function get_new_keys_name() will be called
1373 for input of a custom keyset name. If no name is given, then the
1374 change to the keybinding will abort and no updates will be made. If
1375 a custom name is entered in the prompt or if the current keyset was
1376 already custom (and thus didn't require a prompt), then
1377 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1378 with the change to the event binding. The item listing in bindingslist
1379 is updated with the new keys. Var keybinding is also set which invokes
1380 the callback function, var_changed_keybinding, to add the change to
1381 the 'keys' or 'extensions' changes tracker based on the binding type.
1382
1383 Tk Variables:
1384 keybinding: Action/key bindings.
1385
1386 Methods:
1387 load_keys_list: Reload active set.
1388 create_new_key_set: Combine active keyset and changes.
1389 set_keys_type: Command for keyset_source.
1390 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1391 deactivate_current_config: Remove keys bindings in editors.
1392
1393 Widgets for KeysPage(frame): (*) widgets bound to self
1394 frame_key_sets: LabelFrame
1395 frames[0]: Frame
1396 (*)builtin_keyset_on: Radiobutton - var keyset_source
1397 (*)custom_keyset_on: Radiobutton - var keyset_source
1398 (*)builtinlist: DynOptionMenu - var builtin_name,
1399 func keybinding_selected
1400 (*)customlist: DynOptionMenu - var custom_name,
1401 func keybinding_selected
1402 (*)keys_message: Label
1403 frames[1]: Frame
1404 (*)button_delete_custom_keys: Button - delete_custom_keys
1405 (*)button_save_custom_keys: Button - save_as_new_key_set
1406 frame_custom: LabelFrame
1407 frame_target: Frame
1408 target_title: Label
1409 scroll_target_y: Scrollbar
1410 scroll_target_x: Scrollbar
1411 (*)bindingslist: ListBox - on_bindingslist_select
1412 (*)button_new_keys: Button - get_new_keys & ..._name
1413 """
1414 self.builtin_name = tracers.add(
1415 StringVar(self), self.var_changed_builtin_name)
1416 self.custom_name = tracers.add(
1417 StringVar(self), self.var_changed_custom_name)
1418 self.keyset_source = tracers.add(
1419 BooleanVar(self), self.var_changed_keyset_source)
1420 self.keybinding = tracers.add(
1421 StringVar(self), self.var_changed_keybinding)
1422
1423 # Create widgets:
1424 # body and section frames.
1425 frame_custom = LabelFrame(
1426 self, borderwidth=2, relief=GROOVE,
1427 text=' Custom Key Bindings ')
1428 frame_key_sets = LabelFrame(
1429 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1430 # frame_custom.
1431 frame_target = Frame(frame_custom)
1432 target_title = Label(frame_target, text='Action - Key(s)')
1433 scroll_target_y = Scrollbar(frame_target)
1434 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1435 self.bindingslist = Listbox(
1436 frame_target, takefocus=FALSE, exportselection=FALSE)
1437 self.bindingslist.bind('<ButtonRelease-1>',
1438 self.on_bindingslist_select)
1439 scroll_target_y['command'] = self.bindingslist.yview
1440 scroll_target_x['command'] = self.bindingslist.xview
1441 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1442 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1443 self.button_new_keys = Button(
1444 frame_custom, text='Get New Keys for Selection',
Terry Jan Reedye8f7c782017-11-28 21:52:32 -05001445 command=self.get_new_keys, state='disabled')
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001446 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001447 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001448 for i in range(2)]
1449 self.builtin_keyset_on = Radiobutton(
1450 frames[0], variable=self.keyset_source, value=1,
1451 command=self.set_keys_type, text='Use a Built-in Key Set')
1452 self.custom_keyset_on = Radiobutton(
1453 frames[0], variable=self.keyset_source, value=0,
1454 command=self.set_keys_type, text='Use a Custom Key Set')
1455 self.builtinlist = DynOptionMenu(
1456 frames[0], self.builtin_name, None, command=None)
1457 self.customlist = DynOptionMenu(
1458 frames[0], self.custom_name, None, command=None)
1459 self.button_delete_custom_keys = Button(
1460 frames[1], text='Delete Custom Key Set',
1461 command=self.delete_custom_keys)
1462 self.button_save_custom_keys = Button(
1463 frames[1], text='Save as New Custom Key Set',
1464 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001465 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001466
1467 # Pack widgets:
1468 # body.
1469 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1470 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1471 # frame_custom.
1472 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1473 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1474 # frame_target.
1475 frame_target.columnconfigure(0, weight=1)
1476 frame_target.rowconfigure(1, weight=1)
1477 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1478 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1479 scroll_target_y.grid(row=1, column=1, sticky=NS)
1480 scroll_target_x.grid(row=2, column=0, sticky=EW)
1481 # frame_key_sets.
1482 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1483 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1484 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1485 self.customlist.grid(row=1, column=1, sticky=NSEW)
1486 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1487 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1488 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1489 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1490 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1491
1492 def load_key_cfg(self):
1493 "Load current configuration settings for the keybinding options."
1494 # Set current keys type radiobutton.
1495 self.keyset_source.set(idleConf.GetOption(
1496 'main', 'Keys', 'default', type='bool', default=1))
1497 # Set current keys.
1498 current_option = idleConf.CurrentKeys()
1499 # Load available keyset option menus.
1500 if self.keyset_source.get(): # Default theme selected.
1501 item_list = idleConf.GetSectionList('default', 'keys')
1502 item_list.sort()
1503 self.builtinlist.SetMenu(item_list, current_option)
1504 item_list = idleConf.GetSectionList('user', 'keys')
1505 item_list.sort()
1506 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001507 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001508 self.custom_name.set('- no custom keys -')
1509 else:
1510 self.customlist.SetMenu(item_list, item_list[0])
1511 else: # User key set selected.
1512 item_list = idleConf.GetSectionList('user', 'keys')
1513 item_list.sort()
1514 self.customlist.SetMenu(item_list, current_option)
1515 item_list = idleConf.GetSectionList('default', 'keys')
1516 item_list.sort()
1517 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1518 self.set_keys_type()
1519 # Load keyset element list.
1520 keyset_name = idleConf.CurrentKeys()
1521 self.load_keys_list(keyset_name)
1522
1523 def var_changed_builtin_name(self, *params):
1524 "Process selection of builtin key set."
1525 old_keys = (
1526 'IDLE Classic Windows',
1527 'IDLE Classic Unix',
1528 'IDLE Classic Mac',
1529 'IDLE Classic OSX',
1530 )
1531 value = self.builtin_name.get()
1532 if value not in old_keys:
1533 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1534 changes.add_option('main', 'Keys', 'name', old_keys[0])
1535 changes.add_option('main', 'Keys', 'name2', value)
1536 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001537 else:
1538 changes.add_option('main', 'Keys', 'name', value)
1539 changes.add_option('main', 'Keys', 'name2', '')
1540 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001541 self.load_keys_list(value)
1542
1543 def var_changed_custom_name(self, *params):
1544 "Process selection of custom key set."
1545 value = self.custom_name.get()
1546 if value != '- no custom keys -':
1547 changes.add_option('main', 'Keys', 'name', value)
1548 self.load_keys_list(value)
1549
1550 def var_changed_keyset_source(self, *params):
1551 "Process toggle between builtin key set and custom key set."
1552 value = self.keyset_source.get()
1553 changes.add_option('main', 'Keys', 'default', value)
1554 if value:
1555 self.var_changed_builtin_name()
1556 else:
1557 self.var_changed_custom_name()
1558
1559 def var_changed_keybinding(self, *params):
1560 "Store change to a keybinding."
1561 value = self.keybinding.get()
1562 key_set = self.custom_name.get()
1563 event = self.bindingslist.get(ANCHOR).split()[0]
1564 if idleConf.IsCoreBinding(event):
1565 changes.add_option('keys', key_set, event, value)
1566 else: # Event is an extension binding.
1567 ext_name = idleConf.GetExtnNameForEvent(event)
1568 ext_keybind_section = ext_name + '_cfgBindings'
1569 changes.add_option('extensions', ext_keybind_section, event, value)
1570
1571 def set_keys_type(self):
1572 "Set available screen options based on builtin or custom key set."
1573 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001574 self.builtinlist['state'] = 'normal'
1575 self.customlist['state'] = 'disabled'
1576 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001577 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001578 self.builtinlist['state'] = 'disabled'
1579 self.custom_keyset_on.state(('!disabled',))
1580 self.customlist['state'] = 'normal'
1581 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001582
1583 def get_new_keys(self):
1584 """Handle event to change key binding for selected line.
1585
1586 A selection of a key/binding in the list of current
1587 bindings pops up a dialog to enter a new binding. If
1588 the current key set is builtin and a binding has
1589 changed, then a name for a custom key set needs to be
1590 entered for the change to be applied.
1591 """
1592 list_index = self.bindingslist.index(ANCHOR)
1593 binding = self.bindingslist.get(list_index)
1594 bind_name = binding.split()[0]
1595 if self.keyset_source.get():
1596 current_key_set_name = self.builtin_name.get()
1597 else:
1598 current_key_set_name = self.custom_name.get()
1599 current_bindings = idleConf.GetCurrentKeySet()
1600 if current_key_set_name in changes['keys']: # unsaved changes
1601 key_set_changes = changes['keys'][current_key_set_name]
1602 for event in key_set_changes:
1603 current_bindings[event] = key_set_changes[event].split()
1604 current_key_sequences = list(current_bindings.values())
1605 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1606 current_key_sequences).result
1607 if new_keys:
1608 if self.keyset_source.get(): # Current key set is a built-in.
1609 message = ('Your changes will be saved as a new Custom Key Set.'
1610 ' Enter a name for your new Custom Key Set below.')
1611 new_keyset = self.get_new_keys_name(message)
1612 if not new_keyset: # User cancelled custom key set creation.
1613 self.bindingslist.select_set(list_index)
1614 self.bindingslist.select_anchor(list_index)
1615 return
1616 else: # Create new custom key set based on previously active key set.
1617 self.create_new_key_set(new_keyset)
1618 self.bindingslist.delete(list_index)
1619 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1620 self.bindingslist.select_set(list_index)
1621 self.bindingslist.select_anchor(list_index)
1622 self.keybinding.set(new_keys)
1623 else:
1624 self.bindingslist.select_set(list_index)
1625 self.bindingslist.select_anchor(list_index)
1626
1627 def get_new_keys_name(self, message):
1628 "Return new key set name from query popup."
1629 used_names = (idleConf.GetSectionList('user', 'keys') +
1630 idleConf.GetSectionList('default', 'keys'))
1631 new_keyset = SectionName(
1632 self, 'New Custom Key Set', message, used_names).result
1633 return new_keyset
1634
1635 def save_as_new_key_set(self):
1636 "Prompt for name of new key set and save changes using that name."
1637 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1638 if new_keys_name:
1639 self.create_new_key_set(new_keys_name)
1640
1641 def on_bindingslist_select(self, event):
1642 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001643 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001644
1645 def create_new_key_set(self, new_key_set_name):
1646 """Create a new custom key set with the given name.
1647
1648 Copy the bindings/keys from the previously active keyset
1649 to the new keyset and activate the new custom keyset.
1650 """
1651 if self.keyset_source.get():
1652 prev_key_set_name = self.builtin_name.get()
1653 else:
1654 prev_key_set_name = self.custom_name.get()
1655 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1656 new_keys = {}
1657 for event in prev_keys: # Add key set to changed items.
1658 event_name = event[2:-2] # Trim off the angle brackets.
1659 binding = ' '.join(prev_keys[event])
1660 new_keys[event_name] = binding
1661 # Handle any unsaved changes to prev key set.
1662 if prev_key_set_name in changes['keys']:
1663 key_set_changes = changes['keys'][prev_key_set_name]
1664 for event in key_set_changes:
1665 new_keys[event] = key_set_changes[event]
1666 # Save the new key set.
1667 self.save_new_key_set(new_key_set_name, new_keys)
1668 # Change GUI over to the new key set.
1669 custom_key_list = idleConf.GetSectionList('user', 'keys')
1670 custom_key_list.sort()
1671 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1672 self.keyset_source.set(0)
1673 self.set_keys_type()
1674
1675 def load_keys_list(self, keyset_name):
1676 """Reload the list of action/key binding pairs for the active key set.
1677
1678 An action/key binding can be selected to change the key binding.
1679 """
1680 reselect = False
1681 if self.bindingslist.curselection():
1682 reselect = True
1683 list_index = self.bindingslist.index(ANCHOR)
1684 keyset = idleConf.GetKeySet(keyset_name)
1685 bind_names = list(keyset.keys())
1686 bind_names.sort()
1687 self.bindingslist.delete(0, END)
1688 for bind_name in bind_names:
1689 key = ' '.join(keyset[bind_name])
1690 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1691 if keyset_name in changes['keys']:
1692 # Handle any unsaved changes to this key set.
1693 if bind_name in changes['keys'][keyset_name]:
1694 key = changes['keys'][keyset_name][bind_name]
1695 self.bindingslist.insert(END, bind_name+' - '+key)
1696 if reselect:
1697 self.bindingslist.see(list_index)
1698 self.bindingslist.select_set(list_index)
1699 self.bindingslist.select_anchor(list_index)
1700
1701 @staticmethod
1702 def save_new_key_set(keyset_name, keyset):
1703 """Save a newly created core key set.
1704
1705 Add keyset to idleConf.userCfg['keys'], not to disk.
1706 If the keyset doesn't exist, it is created. The
1707 binding/keys are taken from the keyset argument.
1708
1709 keyset_name - string, the name of the new key set
1710 keyset - dictionary containing the new keybindings
1711 """
1712 if not idleConf.userCfg['keys'].has_section(keyset_name):
1713 idleConf.userCfg['keys'].add_section(keyset_name)
1714 for event in keyset:
1715 value = keyset[event]
1716 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1717
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001718 def askyesno(self, *args, **kwargs):
1719 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001720 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001721
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001722 def delete_custom_keys(self):
1723 """Handle event to delete a custom key set.
1724
1725 Applying the delete deactivates the current configuration and
1726 reverts to the default. The custom key set is permanently
1727 deleted from the config file.
1728 """
1729 keyset_name = self.custom_name.get()
1730 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001731 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001732 'Delete Key Set', delmsg % keyset_name, parent=self):
1733 return
1734 self.cd.deactivate_current_config()
1735 # Remove key set from changes, config, and file.
1736 changes.delete_section('keys', keyset_name)
1737 # Reload user key set list.
1738 item_list = idleConf.GetSectionList('user', 'keys')
1739 item_list.sort()
1740 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001741 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001742 self.customlist.SetMenu(item_list, '- no custom keys -')
1743 else:
1744 self.customlist.SetMenu(item_list, item_list[0])
1745 # Revert to default key set.
1746 self.keyset_source.set(idleConf.defaultCfg['main']
1747 .Get('Keys', 'default'))
1748 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1749 or idleConf.default_keys())
1750 # User can't back out of these changes, they must be applied now.
1751 changes.save_all()
1752 self.cd.save_all_changed_extensions()
1753 self.cd.activate_config_changes()
1754 self.set_keys_type()
1755
1756
csabellae8eb17b2017-07-30 18:39:17 -04001757class GenPage(Frame):
1758
csabella6f446be2017-08-01 00:24:07 -04001759 def __init__(self, master):
1760 super().__init__(master)
csabellae8eb17b2017-07-30 18:39:17 -04001761 self.create_page_general()
1762 self.load_general_cfg()
1763
1764 def create_page_general(self):
1765 """Return frame of widgets for General tab.
1766
1767 Enable users to provisionally change general options. Function
1768 load_general_cfg intializes tk variables and helplist using
1769 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1770 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1771 set var autosave. Entry boxes win_width_int and win_height_int
1772 set var win_width and win_height. Setting var_name invokes the
1773 default callback that adds option to changes.
1774
1775 Helplist: load_general_cfg loads list user_helplist with
1776 name, position pairs and copies names to listbox helplist.
1777 Clicking a name invokes help_source selected. Clicking
1778 button_helplist_name invokes helplist_item_name, which also
1779 changes user_helplist. These functions all call
1780 set_add_delete_state. All but load call update_help_changes to
1781 rewrite changes['main']['HelpFiles'].
1782
Cheryl Sabella2f896462017-08-14 21:21:43 -04001783 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001784 frame_window: LabelFrame
1785 frame_run: Frame
1786 startup_title: Label
1787 (*)startup_editor_on: Radiobutton - startup_edit
1788 (*)startup_shell_on: Radiobutton - startup_edit
1789 frame_win_size: Frame
1790 win_size_title: Label
1791 win_width_title: Label
1792 (*)win_width_int: Entry - win_width
1793 win_height_title: Label
1794 (*)win_height_int: Entry - win_height
1795 frame_editor: LabelFrame
1796 frame_save: Frame
1797 run_save_title: Label
1798 (*)save_ask_on: Radiobutton - autosave
1799 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella2f896462017-08-14 21:21:43 -04001800 frame_help: LabelFrame
1801 frame_helplist: Frame
1802 frame_helplist_buttons: Frame
1803 (*)button_helplist_edit
1804 (*)button_helplist_add
1805 (*)button_helplist_remove
1806 (*)helplist: ListBox
1807 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001808 """
wohlganger58fc71c2017-09-10 16:19:47 -05001809 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001810 self.startup_edit = tracers.add(
1811 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001812 self.win_width = tracers.add(
1813 StringVar(self), ('main', 'EditorWindow', 'width'))
1814 self.win_height = tracers.add(
1815 StringVar(self), ('main', 'EditorWindow', 'height'))
wohlganger58fc71c2017-09-10 16:19:47 -05001816 self.autocomplete_wait = tracers.add(
1817 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1818 self.paren_style = tracers.add(
1819 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1820 self.flash_delay = tracers.add(
1821 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1822 self.paren_bell = tracers.add(
1823 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001824
wohlganger58fc71c2017-09-10 16:19:47 -05001825 self.autosave = tracers.add(
1826 IntVar(self), ('main', 'General', 'autosave'))
1827 self.format_width = tracers.add(
1828 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
1829 self.context_lines = tracers.add(
1830 StringVar(self), ('extensions', 'CodeContext', 'numlines'))
1831
1832 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001833 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001834 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1835 text=' Window Preferences')
1836 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1837 text=' Editor Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001838 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1839 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001840 # Frame_window.
1841 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001842 startup_title = Label(frame_run, text='At Startup')
1843 self.startup_editor_on = Radiobutton(
1844 frame_run, variable=self.startup_edit, value=1,
1845 text="Open Edit Window")
1846 self.startup_shell_on = Radiobutton(
1847 frame_run, variable=self.startup_edit, value=0,
1848 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001849
wohlganger58fc71c2017-09-10 16:19:47 -05001850 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001851 win_size_title = Label(
1852 frame_win_size, text='Initial Window Size (in characters)')
1853 win_width_title = Label(frame_win_size, text='Width')
1854 self.win_width_int = Entry(
1855 frame_win_size, textvariable=self.win_width, width=3)
1856 win_height_title = Label(frame_win_size, text='Height')
1857 self.win_height_int = Entry(
1858 frame_win_size, textvariable=self.win_height, width=3)
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001859
wohlganger58fc71c2017-09-10 16:19:47 -05001860 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1861 auto_wait_title = Label(frame_autocomplete,
1862 text='Completions Popup Wait (milliseconds)')
1863 self.auto_wait_int = Entry(frame_autocomplete, width=6,
1864 textvariable=self.autocomplete_wait)
1865
1866 frame_paren1 = Frame(frame_window, borderwidth=0)
1867 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1868 self.paren_style_type = OptionMenu(
1869 frame_paren1, self.paren_style, 'expression',
1870 "opener","parens","expression")
1871 frame_paren2 = Frame(frame_window, borderwidth=0)
1872 paren_time_title = Label(
1873 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1874 '(0 is until next input)')
1875 self.paren_flash_time = Entry(
1876 frame_paren2, textvariable=self.flash_delay, width=6)
1877 self.bell_on = Checkbutton(
1878 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1879
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001880 # Frame_editor.
1881 frame_save = Frame(frame_editor, borderwidth=0)
1882 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1883 self.save_ask_on = Radiobutton(
1884 frame_save, variable=self.autosave, value=0,
1885 text="Prompt to Save")
1886 self.save_auto_on = Radiobutton(
1887 frame_save, variable=self.autosave, value=1,
1888 text='No Prompt')
1889
wohlganger58fc71c2017-09-10 16:19:47 -05001890 frame_format = Frame(frame_editor, borderwidth=0)
1891 format_width_title = Label(frame_format,
1892 text='Format Paragraph Max Width')
1893 self.format_width_int = Entry(
1894 frame_format, textvariable=self.format_width, width=4)
1895
1896 frame_context = Frame(frame_editor, borderwidth=0)
1897 context_title = Label(frame_context, text='Context Lines :')
1898 self.context_int = Entry(
1899 frame_context, textvariable=self.context_lines, width=3)
1900
1901
csabellae8eb17b2017-07-30 18:39:17 -04001902 # frame_help.
1903 frame_helplist = Frame(frame_help)
1904 frame_helplist_buttons = Frame(frame_helplist)
1905 self.helplist = Listbox(
1906 frame_helplist, height=5, takefocus=True,
1907 exportselection=FALSE)
1908 scroll_helplist = Scrollbar(frame_helplist)
1909 scroll_helplist['command'] = self.helplist.yview
1910 self.helplist['yscrollcommand'] = scroll_helplist.set
1911 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1912 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001913 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001914 width=8, command=self.helplist_item_edit)
1915 self.button_helplist_add = Button(
1916 frame_helplist_buttons, text='Add',
1917 width=8, command=self.helplist_item_add)
1918 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001919 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001920 width=8, command=self.helplist_item_remove)
1921
1922 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001923 # Body.
1924 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1925 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04001926 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1927 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001928 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001929 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1930 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1931 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04001932 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001933 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001934 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1935 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1936 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1937 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1938 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001939 # frame_autocomplete.
1940 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
1941 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1942 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
1943 # frame_paren.
1944 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
1945 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1946 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
1947 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
1948 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
1949 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
1950 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
1951
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001952 # frame_save.
1953 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
1954 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1955 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1956 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001957 # frame_format.
1958 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
1959 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1960 self.format_width_int.pack(side=TOP, padx=10, pady=5)
1961 # frame_context.
1962 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
1963 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1964 self.context_int.pack(side=TOP, padx=5, pady=5)
1965
csabellae8eb17b2017-07-30 18:39:17 -04001966 # frame_help.
1967 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1968 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1969 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1970 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1971 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1972 self.button_helplist_add.pack(side=TOP, anchor=W)
1973 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1974
1975 def load_general_cfg(self):
1976 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05001977 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04001978 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05001979 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04001980 self.win_width.set(idleConf.GetOption(
1981 'main', 'EditorWindow', 'width', type='int'))
1982 self.win_height.set(idleConf.GetOption(
1983 'main', 'EditorWindow', 'height', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05001984 self.autocomplete_wait.set(idleConf.GetOption(
1985 'extensions', 'AutoComplete', 'popupwait', type='int'))
1986 self.paren_style.set(idleConf.GetOption(
1987 'extensions', 'ParenMatch', 'style'))
1988 self.flash_delay.set(idleConf.GetOption(
1989 'extensions', 'ParenMatch', 'flash-delay', type='int'))
1990 self.paren_bell.set(idleConf.GetOption(
1991 'extensions', 'ParenMatch', 'bell'))
1992
1993 # Set variables for editor windows.
1994 self.autosave.set(idleConf.GetOption(
1995 'main', 'General', 'autosave', default=0, type='bool'))
1996 self.format_width.set(idleConf.GetOption(
1997 'extensions', 'FormatParagraph', 'max-width', type='int'))
1998 self.context_lines.set(idleConf.GetOption(
1999 'extensions', 'CodeContext', 'numlines', type='int'))
2000
csabellae8eb17b2017-07-30 18:39:17 -04002001 # Set additional help sources.
2002 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2003 self.helplist.delete(0, 'end')
2004 for help_item in self.user_helplist:
2005 self.helplist.insert(END, help_item[0])
2006 self.set_add_delete_state()
2007
2008 def help_source_selected(self, event):
2009 "Handle event for selecting additional help."
2010 self.set_add_delete_state()
2011
2012 def set_add_delete_state(self):
2013 "Toggle the state for the help list buttons based on list entries."
2014 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002015 self.button_helplist_edit.state(('disabled',))
2016 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002017 else: # Some entries.
2018 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002019 self.button_helplist_edit.state(('!disabled',))
2020 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002021 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002022 self.button_helplist_edit.state(('disabled',))
2023 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002024
2025 def helplist_item_add(self):
2026 """Handle add button for the help list.
2027
2028 Query for name and location of new help sources and add
2029 them to the list.
2030 """
2031 help_source = HelpSource(self, 'New Help Source').result
2032 if help_source:
2033 self.user_helplist.append(help_source)
2034 self.helplist.insert(END, help_source[0])
2035 self.update_help_changes()
2036
2037 def helplist_item_edit(self):
2038 """Handle edit button for the help list.
2039
2040 Query with existing help source information and update
2041 config if the values are changed.
2042 """
2043 item_index = self.helplist.index(ANCHOR)
2044 help_source = self.user_helplist[item_index]
2045 new_help_source = HelpSource(
2046 self, 'Edit Help Source',
2047 menuitem=help_source[0],
2048 filepath=help_source[1],
2049 ).result
2050 if new_help_source and new_help_source != help_source:
2051 self.user_helplist[item_index] = new_help_source
2052 self.helplist.delete(item_index)
2053 self.helplist.insert(item_index, new_help_source[0])
2054 self.update_help_changes()
2055 self.set_add_delete_state() # Selected will be un-selected
2056
2057 def helplist_item_remove(self):
2058 """Handle remove button for the help list.
2059
2060 Delete the help list item from config.
2061 """
2062 item_index = self.helplist.index(ANCHOR)
2063 del(self.user_helplist[item_index])
2064 self.helplist.delete(item_index)
2065 self.update_help_changes()
2066 self.set_add_delete_state()
2067
2068 def update_help_changes(self):
2069 "Clear and rebuild the HelpFiles section in changes"
2070 changes['main']['HelpFiles'] = {}
2071 for num in range(1, len(self.user_helplist) + 1):
2072 changes.add_option(
2073 'main', 'HelpFiles', str(num),
2074 ';'.join(self.user_helplist[num-1][:2]))
2075
2076
csabella45bf7232017-07-26 19:09:58 -04002077class VarTrace:
2078 """Maintain Tk variables trace state."""
2079
2080 def __init__(self):
2081 """Store Tk variables and callbacks.
2082
2083 untraced: List of tuples (var, callback)
2084 that do not have the callback attached
2085 to the Tk var.
2086 traced: List of tuples (var, callback) where
2087 that callback has been attached to the var.
2088 """
2089 self.untraced = []
2090 self.traced = []
2091
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002092 def clear(self):
2093 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002094 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002095 self.untraced.clear()
2096 self.traced.clear()
2097
csabella45bf7232017-07-26 19:09:58 -04002098 def add(self, var, callback):
2099 """Add (var, callback) tuple to untraced list.
2100
2101 Args:
2102 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002103 callback: Either function name to be used as a callback
2104 or a tuple with IdleConf config-type, section, and
2105 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002106
2107 Return:
2108 Tk variable instance.
2109 """
2110 if isinstance(callback, tuple):
2111 callback = self.make_callback(var, callback)
2112 self.untraced.append((var, callback))
2113 return var
2114
2115 @staticmethod
2116 def make_callback(var, config):
2117 "Return default callback function to add values to changes instance."
2118 def default_callback(*params):
2119 "Add config values to changes instance."
2120 changes.add_option(*config, var.get())
2121 return default_callback
2122
2123 def attach(self):
2124 "Attach callback to all vars that are not traced."
2125 while self.untraced:
2126 var, callback = self.untraced.pop()
2127 var.trace_add('write', callback)
2128 self.traced.append((var, callback))
2129
2130 def detach(self):
2131 "Remove callback from traced vars."
2132 while self.traced:
2133 var, callback = self.traced.pop()
2134 var.trace_remove('write', var.trace_info()[0][1])
2135 self.untraced.append((var, callback))
2136
2137
csabella5b591542017-07-28 14:40:59 -04002138tracers = VarTrace()
2139
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002140help_common = '''\
2141When you click either the Apply or Ok buttons, settings in this
2142dialog that are different from IDLE's default are saved in
2143a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002144these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002145machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002146'''
2147help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002148 'Fonts/Tabs':'''
2149Font sample: This shows what a selection of Basic Multilingual Plane
2150unicode characters look like for the current font selection. If the
2151selected font does not define a character, Tk attempts to find another
2152font that does. Substitute glyphs depend on what is available on a
2153particular system and will not necessarily have the same size as the
2154font selected. Line contains 20 characters up to Devanagari, 14 for
2155Tamil, and 10 for East Asia.
2156
2157Hebrew and Arabic letters should display right to left, starting with
2158alef, \u05d0 and \u0627. Arabic digits display left to right. The
2159Devanagari and Tamil lines start with digits. The East Asian lines
2160are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2161Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002162
2163You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002164''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002165 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002166Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002167The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002168be used with older IDLE releases if it is saved as a custom
2169theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002170''',
2171 'Keys': '''
2172Keys:
2173The IDLE Modern Unix key set is new in June 2016. It can only
2174be used with older IDLE releases if it is saved as a custom
2175key set, with a different name.
2176''',
wohlganger58fc71c2017-09-10 16:19:47 -05002177 'General': '''
2178General:
wohlgangerfae2c352017-06-27 21:36:23 -05002179
wohlganger58fc71c2017-09-10 16:19:47 -05002180AutoComplete: Popupwait is milleseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002181cursor movement, before popping up completion box. Key char is '.' after
2182identifier or a '/' (or '\\' on Windows) within a string.
2183
2184FormatParagraph: Max-width is max chars in lines after re-formatting.
2185Use with paragraphs in both strings and comment blocks.
2186
2187ParenMatch: Style indicates what is highlighted when closer is entered:
2188'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2189'expression' (default) - also everything in between. Flash-delay is how
2190long to highlight if cursor is not moved (0 means forever).
2191'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002192}
2193
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002194
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002195def is_int(s):
2196 "Return 's is blank or represents an int'"
2197 if not s:
2198 return True
2199 try:
2200 int(s)
2201 return True
2202 except ValueError:
2203 return False
2204
2205
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002206class VerticalScrolledFrame(Frame):
2207 """A pure Tkinter vertically scrollable frame.
2208
2209 * Use the 'interior' attribute to place widgets inside the scrollable frame
2210 * Construct and pack/place/grid normally
2211 * This frame only allows vertical scrolling
2212 """
2213 def __init__(self, parent, *args, **kw):
2214 Frame.__init__(self, parent, *args, **kw)
2215
csabella7eb58832017-07-04 21:30:58 -04002216 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002217 vscrollbar = Scrollbar(self, orient=VERTICAL)
2218 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002219 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002220 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002221 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2222 vscrollbar.config(command=canvas.yview)
2223
csabella7eb58832017-07-04 21:30:58 -04002224 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002225 canvas.xview_moveto(0)
2226 canvas.yview_moveto(0)
2227
csabella7eb58832017-07-04 21:30:58 -04002228 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002229 self.interior = interior = Frame(canvas)
2230 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2231
csabella7eb58832017-07-04 21:30:58 -04002232 # Track changes to the canvas and frame width and sync them,
2233 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002234 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002235 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002236 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2237 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002238 interior.bind('<Configure>', _configure_interior)
2239
2240 def _configure_canvas(event):
2241 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002242 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002243 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2244 canvas.bind('<Configure>', _configure_canvas)
2245
2246 return
2247
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002248
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002249if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04002250 import unittest
2251 unittest.main('idlelib.idle_test.test_configdialog',
2252 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002253 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002254 run(ConfigDialog)