blob: 75b917d0e193162ef8ed6d35609e63d4188bfabf [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',
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400376 validatecommand=(self.is_int, '%P'), width=10
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400377 ).grid(row=row, column=1, sticky=NSEW, padx=7)
378
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400379 else: # type == 'str'
380 # Limit size to fit non-expanding space with larger font.
381 Entry(entry_area, textvariable=var, width=15
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400382 ).grid(row=row, column=1, sticky=NSEW, padx=7)
383 return
384
385 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400386 """Return True if the configuration was added or changed.
387
388 If the value is the same as the default, then remove it
389 from user config file.
390 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400391 name = opt['name']
392 default = opt['default']
393 value = opt['var'].get().strip() or default
394 opt['var'].set(value)
395 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400396 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400397 if (value == default):
398 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400399 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400400 return self.ext_userCfg.SetOption(section, name, value)
401
402 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400403 """Save configuration changes to the user config file.
404
405 Attributes accessed:
406 extensions
407
408 Methods:
409 set_extension_value
410 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400411 has_changes = False
412 for ext_name in self.extensions:
413 options = self.extensions[ext_name]
414 for opt in options:
415 if self.set_extension_value(ext_name, opt):
416 has_changes = True
417 if has_changes:
418 self.ext_userCfg.Save()
419
420
csabella6f446be2017-08-01 00:24:07 -0400421# class TabPage(Frame): # A template for Page classes.
422# def __init__(self, master):
423# super().__init__(master)
424# self.create_page_tab()
425# self.load_tab_cfg()
426# def create_page_tab(self):
427# # Define tk vars and register var and callback with tracers.
428# # Create subframes and widgets.
429# # Pack widgets.
430# def load_tab_cfg(self):
431# # Initialize widgets with data from idleConf.
432# def var_changed_var_name():
433# # For each tk var that needs other than default callback.
434# def other_methods():
435# # Define tab-specific behavior.
436
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300437font_sample_text = (
438 '<ASCII/Latin1>\n'
439 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
440 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
441 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
442 '\n<IPA,Greek,Cyrillic>\n'
443 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
444 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
445 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
446 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
447 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
448 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
449 '\n<Hebrew, Arabic>\n'
450 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
451 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
452 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
453 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
454 '\n<Devanagari, Tamil>\n'
455 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
456 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
457 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
458 '\u0b85\u0b87\u0b89\u0b8e\n'
459 '\n<East Asian>\n'
460 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
461 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
462 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
463 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
464 )
465
csabella6f446be2017-08-01 00:24:07 -0400466
csabella9397e2a2017-07-30 13:34:25 -0400467class FontPage(Frame):
468
csabella6f446be2017-08-01 00:24:07 -0400469 def __init__(self, master, highpage):
470 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400471 self.highlight_sample = highpage.highlight_sample
472 self.create_page_font_tab()
473 self.load_font_cfg()
474 self.load_tab_cfg()
475
476 def create_page_font_tab(self):
477 """Return frame of widgets for Font/Tabs tab.
478
479 Fonts: Enable users to provisionally change font face, size, or
480 boldness and to see the consequence of proposed choices. Each
481 action set 3 options in changes structuree and changes the
482 corresponding aspect of the font sample on this page and
483 highlight sample on highlight page.
484
485 Function load_font_cfg initializes font vars and widgets from
486 idleConf entries and tk.
487
488 Fontlist: mouse button 1 click or up or down key invoke
489 on_fontlist_select(), which sets var font_name.
490
491 Sizelist: clicking the menubutton opens the dropdown menu. A
492 mouse button 1 click or return key sets var font_size.
493
494 Bold_toggle: clicking the box toggles var font_bold.
495
496 Changing any of the font vars invokes var_changed_font, which
497 adds all 3 font options to changes and calls set_samples.
498 Set_samples applies a new font constructed from the font vars to
Leo Ariasc3d95082018-02-03 18:36:10 -0600499 font_sample and to highlight_sample on the highlight page.
csabella9397e2a2017-07-30 13:34:25 -0400500
501 Tabs: Enable users to change spaces entered for indent tabs.
502 Changing indent_scale value with the mouse sets Var space_num,
503 which invokes the default callback to add an entry to
504 changes. Load_tab_cfg initializes space_num to default.
505
Cheryl Sabella2f896462017-08-14 21:21:43 -0400506 Widgets for FontPage(Frame): (*) widgets bound to self
507 frame_font: LabelFrame
508 frame_font_name: Frame
509 font_name_title: Label
510 (*)fontlist: ListBox - font_name
511 scroll_font: Scrollbar
512 frame_font_param: Frame
513 font_size_title: Label
514 (*)sizelist: DynOptionMenu - font_size
515 (*)bold_toggle: Checkbutton - font_bold
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400516 frame_sample: LabelFrame
517 (*)font_sample: Label
Cheryl Sabella2f896462017-08-14 21:21:43 -0400518 frame_indent: LabelFrame
519 indent_title: Label
520 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400521 """
csabella6f446be2017-08-01 00:24:07 -0400522 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
523 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
524 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400525 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
526
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400527 # Define frames and widgets.
csabella9397e2a2017-07-30 13:34:25 -0400528 frame_font = LabelFrame(
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400529 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ')
530 frame_sample = LabelFrame(
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300531 self, borderwidth=2, relief=GROOVE,
532 text=' Font Sample (Editable) ')
csabella9397e2a2017-07-30 13:34:25 -0400533 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400534 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400535 # frame_font.
536 frame_font_name = Frame(frame_font)
537 frame_font_param = Frame(frame_font)
538 font_name_title = Label(
539 frame_font_name, justify=LEFT, text='Font Face :')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400540 self.fontlist = Listbox(frame_font_name, height=15,
csabella9397e2a2017-07-30 13:34:25 -0400541 takefocus=True, exportselection=FALSE)
542 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
543 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
544 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
545 scroll_font = Scrollbar(frame_font_name)
546 scroll_font.config(command=self.fontlist.yview)
547 self.fontlist.config(yscrollcommand=scroll_font.set)
548 font_size_title = Label(frame_font_param, text='Size :')
549 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
550 self.bold_toggle = Checkbutton(
551 frame_font_param, variable=self.font_bold,
552 onvalue=1, offvalue=0, text='Bold')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400553 # frame_sample.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300554 self.font_sample = Text(frame_sample, width=20, height=20)
555 self.font_sample.insert(END, font_sample_text)
csabella9397e2a2017-07-30 13:34:25 -0400556 # frame_indent.
557 indent_title = Label(
558 frame_indent, justify=LEFT,
559 text='Python Standard: 4 Spaces!')
560 self.indent_scale = Scale(
561 frame_indent, variable=self.space_num,
562 orient='horizontal', tickinterval=2, from_=2, to=16)
563
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400564 # Grid and pack widgets:
565 self.columnconfigure(1, weight=1)
566 frame_font.grid(row=0, column=0, padx=5, pady=5)
567 frame_sample.grid(row=0, column=1, rowspan=2, padx=5, pady=5,
568 sticky='nsew')
569 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
csabella9397e2a2017-07-30 13:34:25 -0400570 # frame_font.
571 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
572 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
573 font_name_title.pack(side=TOP, anchor=W)
574 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
575 scroll_font.pack(side=LEFT, fill=Y)
576 font_size_title.pack(side=LEFT, anchor=W)
577 self.sizelist.pack(side=LEFT, anchor=W)
578 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400579 # frame_sample.
csabella9397e2a2017-07-30 13:34:25 -0400580 self.font_sample.pack(expand=TRUE, fill=BOTH)
581 # frame_indent.
csabella9397e2a2017-07-30 13:34:25 -0400582 indent_title.pack(side=TOP, anchor=W, padx=5)
583 self.indent_scale.pack(side=TOP, padx=5, fill=X)
584
csabella9397e2a2017-07-30 13:34:25 -0400585 def load_font_cfg(self):
586 """Load current configuration settings for the font options.
587
588 Retrieve current font with idleConf.GetFont and font families
589 from tk. Setup fontlist and set font_name. Setup sizelist,
590 which sets font_size. Set font_bold. Call set_samples.
591 """
592 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
593 font_name = configured_font[0].lower()
594 font_size = configured_font[1]
595 font_bold = configured_font[2]=='bold'
596
597 # Set editor font selection list and font_name.
598 fonts = list(tkFont.families(self))
599 fonts.sort()
600 for font in fonts:
601 self.fontlist.insert(END, font)
602 self.font_name.set(font_name)
603 lc_fonts = [s.lower() for s in fonts]
604 try:
605 current_font_index = lc_fonts.index(font_name)
606 self.fontlist.see(current_font_index)
607 self.fontlist.select_set(current_font_index)
608 self.fontlist.select_anchor(current_font_index)
609 self.fontlist.activate(current_font_index)
610 except ValueError:
611 pass
612 # Set font size dropdown.
613 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
614 '16', '18', '20', '22', '25', '29', '34', '40'),
615 font_size)
616 # Set font weight.
617 self.font_bold.set(font_bold)
618 self.set_samples()
619
620 def var_changed_font(self, *params):
621 """Store changes to font attributes.
622
623 When one font attribute changes, save them all, as they are
624 not independent from each other. In particular, when we are
625 overriding the default font, we need to write out everything.
626 """
627 value = self.font_name.get()
628 changes.add_option('main', 'EditorWindow', 'font', value)
629 value = self.font_size.get()
630 changes.add_option('main', 'EditorWindow', 'font-size', value)
631 value = self.font_bold.get()
632 changes.add_option('main', 'EditorWindow', 'font-bold', value)
633 self.set_samples()
634
635 def on_fontlist_select(self, event):
636 """Handle selecting a font from the list.
637
638 Event can result from either mouse click or Up or Down key.
639 Set font_name and example displays to selection.
640 """
641 font = self.fontlist.get(
642 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
643 self.font_name.set(font.lower())
644
645 def set_samples(self, event=None):
646 """Update update both screen samples with the font settings.
647
648 Called on font initialization and change events.
649 Accesses font_name, font_size, and font_bold Variables.
Leo Ariasc3d95082018-02-03 18:36:10 -0600650 Updates font_sample and highlight page highlight_sample.
csabella9397e2a2017-07-30 13:34:25 -0400651 """
652 font_name = self.font_name.get()
653 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
654 new_font = (font_name, self.font_size.get(), font_weight)
655 self.font_sample['font'] = new_font
656 self.highlight_sample['font'] = new_font
657
658 def load_tab_cfg(self):
659 """Load current configuration settings for the tab options.
660
661 Attributes updated:
662 space_num: Set to value from idleConf.
663 """
664 # Set indent sizes.
665 space_num = idleConf.GetOption(
666 'main', 'Indent', 'num-spaces', default=4, type='int')
667 self.space_num.set(space_num)
668
669 def var_changed_space_num(self, *params):
670 "Store change to indentation size."
671 value = self.space_num.get()
672 changes.add_option('main', 'Indent', 'num-spaces', value)
673
674
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400675class HighPage(Frame):
676
677 def __init__(self, master):
678 super().__init__(master)
679 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400680 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400681 self.create_page_highlight()
682 self.load_theme_cfg()
683
684 def create_page_highlight(self):
685 """Return frame of widgets for Highlighting tab.
686
687 Enable users to provisionally change foreground and background
688 colors applied to textual tags. Color mappings are stored in
689 complete listings called themes. Built-in themes in
690 idlelib/config-highlight.def are fixed as far as the dialog is
691 concerned. Any theme can be used as the base for a new custom
692 theme, stored in .idlerc/config-highlight.cfg.
693
694 Function load_theme_cfg() initializes tk variables and theme
695 lists and calls paint_theme_sample() and set_highlight_target()
696 for the current theme. Radiobuttons builtin_theme_on and
697 custom_theme_on toggle var theme_source, which controls if the
698 current set of colors are from a builtin or custom theme.
699 DynOptionMenus builtinlist and customlist contain lists of the
700 builtin and custom themes, respectively, and the current item
701 from each list is stored in vars builtin_name and custom_name.
702
703 Function paint_theme_sample() applies the colors from the theme
704 to the tags in text widget highlight_sample and then invokes
705 set_color_sample(). Function set_highlight_target() sets the state
706 of the radiobuttons fg_on and bg_on based on the tag and it also
707 invokes set_color_sample().
708
709 Function set_color_sample() sets the background color for the frame
710 holding the color selector. This provides a larger visual of the
711 color for the current tag and plane (foreground/background).
712
713 Note: set_color_sample() is called from many places and is often
714 called more than once when a change is made. It is invoked when
715 foreground or background is selected (radiobuttons), from
716 paint_theme_sample() (theme is changed or load_cfg is called), and
717 from set_highlight_target() (target tag is changed or load_cfg called).
718
719 Button delete_custom invokes delete_custom() to delete
720 a custom theme from idleConf.userCfg['highlight'] and changes.
721 Button save_custom invokes save_as_new_theme() which calls
722 get_new_theme_name() and create_new() to save a custom theme
723 and its colors to idleConf.userCfg['highlight'].
724
725 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
726 if the current selected color for a tag is for the foreground or
727 background.
728
729 DynOptionMenu targetlist contains a readable description of the
730 tags applied to Python source within IDLE. Selecting one of the
731 tags from this list populates highlight_target, which has a callback
732 function set_highlight_target().
733
734 Text widget highlight_sample displays a block of text (which is
735 mock Python code) in which is embedded the defined tags and reflects
736 the color attributes of the current theme and changes for those tags.
737 Mouse button 1 allows for selection of a tag and updates
738 highlight_target with that tag value.
739
740 Note: The font in highlight_sample is set through the config in
741 the fonts tab.
742
743 In other words, a tag can be selected either from targetlist or
744 by clicking on the sample text within highlight_sample. The
745 plane (foreground/background) is selected via the radiobutton.
746 Together, these two (tag and plane) control what color is
747 shown in set_color_sample() for the current theme. Button set_color
748 invokes get_color() which displays a ColorChooser to change the
749 color for the selected tag/plane. If a new color is picked,
750 it will be saved to changes and the highlight_sample and
751 frame background will be updated.
752
753 Tk Variables:
754 color: Color of selected target.
755 builtin_name: Menu variable for built-in theme.
756 custom_name: Menu variable for custom theme.
757 fg_bg_toggle: Toggle for foreground/background color.
758 Note: this has no callback.
759 theme_source: Selector for built-in or custom theme.
760 highlight_target: Menu variable for the highlight tag target.
761
762 Instance Data Attributes:
763 theme_elements: Dictionary of tags for text highlighting.
764 The key is the display name and the value is a tuple of
765 (tag name, display sort order).
766
767 Methods [attachment]:
768 load_theme_cfg: Load current highlight colors.
769 get_color: Invoke colorchooser [button_set_color].
770 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
771 set_highlight_target: set fg_bg_toggle, set_color_sample().
772 set_color_sample: Set frame background to target.
773 on_new_color_set: Set new color and add option.
774 paint_theme_sample: Recolor sample.
775 get_new_theme_name: Get from popup.
776 create_new: Combine theme with changes and save.
777 save_as_new_theme: Save [button_save_custom].
778 set_theme_type: Command for [theme_source].
779 delete_custom: Activate default [button_delete_custom].
780 save_new: Save to userCfg['theme'] (is function).
781
782 Widgets of highlights page frame: (*) widgets bound to self
783 frame_custom: LabelFrame
784 (*)highlight_sample: Text
785 (*)frame_color_set: Frame
786 (*)button_set_color: Button
787 (*)targetlist: DynOptionMenu - highlight_target
788 frame_fg_bg_toggle: Frame
789 (*)fg_on: Radiobutton - fg_bg_toggle
790 (*)bg_on: Radiobutton - fg_bg_toggle
791 (*)button_save_custom: Button
792 frame_theme: LabelFrame
793 theme_type_title: Label
794 (*)builtin_theme_on: Radiobutton - theme_source
795 (*)custom_theme_on: Radiobutton - theme_source
796 (*)builtinlist: DynOptionMenu - builtin_name
797 (*)customlist: DynOptionMenu - custom_name
798 (*)button_delete_custom: Button
799 (*)theme_message: Label
800 """
801 self.theme_elements = {
802 'Normal Text': ('normal', '00'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400803 'Code Context': ('context', '01'),
804 'Python Keywords': ('keyword', '02'),
805 'Python Definitions': ('definition', '03'),
806 'Python Builtins': ('builtin', '04'),
807 'Python Comments': ('comment', '05'),
808 'Python Strings': ('string', '06'),
809 'Selected Text': ('hilite', '07'),
810 'Found Text': ('hit', '08'),
811 'Cursor': ('cursor', '09'),
812 'Editor Breakpoint': ('break', '10'),
813 'Shell Normal Text': ('console', '11'),
814 'Shell Error Text': ('error', '12'),
815 'Shell Stdout Text': ('stdout', '13'),
816 'Shell Stderr Text': ('stderr', '14'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400817 }
818 self.builtin_name = tracers.add(
819 StringVar(self), self.var_changed_builtin_name)
820 self.custom_name = tracers.add(
821 StringVar(self), self.var_changed_custom_name)
822 self.fg_bg_toggle = BooleanVar(self)
823 self.color = tracers.add(
824 StringVar(self), self.var_changed_color)
825 self.theme_source = tracers.add(
826 BooleanVar(self), self.var_changed_theme_source)
827 self.highlight_target = tracers.add(
828 StringVar(self), self.var_changed_highlight_target)
829
830 # Create widgets:
831 # body frame and section frames.
832 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
833 text=' Custom Highlighting ')
834 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
835 text=' Highlighting Theme ')
836 # frame_custom.
837 text = self.highlight_sample = Text(
838 frame_custom, relief=SOLID, borderwidth=1,
839 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
840 takefocus=FALSE, highlightthickness=0, wrap=NONE)
841 text.bind('<Double-Button-1>', lambda e: 'break')
842 text.bind('<B1-Motion>', lambda e: 'break')
wohlganger58fc71c2017-09-10 16:19:47 -0500843 text_and_tags=(
844 ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400845 ('#you can click here', 'comment'), ('\n', 'normal'),
846 ('#to choose items', 'comment'), ('\n', 'normal'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400847 ('code context section', 'context'), ('\n\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400848 ('def', 'keyword'), (' ', 'normal'),
849 ('func', 'definition'), ('(param):\n ', 'normal'),
850 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
851 ("'string'", 'string'), ('\n var1 = ', 'normal'),
852 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
853 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
854 ('list', 'builtin'), ('(', 'normal'),
855 ('None', 'keyword'), (')\n', 'normal'),
856 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
857 (' error ', 'error'), (' ', 'normal'),
858 ('cursor |', 'cursor'), ('\n ', 'normal'),
859 ('shell', 'console'), (' ', 'normal'),
860 ('stdout', 'stdout'), (' ', 'normal'),
861 ('stderr', 'stderr'), ('\n\n', 'normal'))
862 for texttag in text_and_tags:
863 text.insert(END, texttag[0], texttag[1])
864 for element in self.theme_elements:
865 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400866 # event.widget.winfo_top_level().highlight_target.set(elem)
867 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400868 text.tag_bind(
869 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400870 text['state'] = 'disabled'
871 self.style.configure('frame_color_set.TFrame', borderwidth=1,
872 relief='solid')
873 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400874 frame_fg_bg_toggle = Frame(frame_custom)
875 self.button_set_color = Button(
876 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400877 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400878 self.targetlist = DynOptionMenu(
879 self.frame_color_set, self.highlight_target, None,
880 highlightthickness=0) #, command=self.set_highlight_targetBinding
881 self.fg_on = Radiobutton(
882 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
883 text='Foreground', command=self.set_color_sample_binding)
884 self.bg_on = Radiobutton(
885 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
886 text='Background', command=self.set_color_sample_binding)
887 self.fg_bg_toggle.set(1)
888 self.button_save_custom = Button(
889 frame_custom, text='Save as New Custom Theme',
890 command=self.save_as_new_theme)
891 # frame_theme.
892 theme_type_title = Label(frame_theme, text='Select : ')
893 self.builtin_theme_on = Radiobutton(
894 frame_theme, variable=self.theme_source, value=1,
895 command=self.set_theme_type, text='a Built-in Theme')
896 self.custom_theme_on = Radiobutton(
897 frame_theme, variable=self.theme_source, value=0,
898 command=self.set_theme_type, text='a Custom Theme')
899 self.builtinlist = DynOptionMenu(
900 frame_theme, self.builtin_name, None, command=None)
901 self.customlist = DynOptionMenu(
902 frame_theme, self.custom_name, None, command=None)
903 self.button_delete_custom = Button(
904 frame_theme, text='Delete Custom Theme',
905 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400906 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400907 # Pack widgets:
908 # body.
909 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500910 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400911 # frame_custom.
912 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
913 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
914 self.highlight_sample.pack(
915 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
916 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
917 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
918 self.fg_on.pack(side=LEFT, anchor=E)
919 self.bg_on.pack(side=RIGHT, anchor=W)
920 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
921 # frame_theme.
922 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
923 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
924 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
925 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
926 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
927 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
928 self.theme_message.pack(side=TOP, fill=X, pady=5)
929
930 def load_theme_cfg(self):
931 """Load current configuration settings for the theme options.
932
933 Based on the theme_source toggle, the theme is set as
934 either builtin or custom and the initial widget values
935 reflect the current settings from idleConf.
936
937 Attributes updated:
938 theme_source: Set from idleConf.
939 builtinlist: List of default themes from idleConf.
940 customlist: List of custom themes from idleConf.
941 custom_theme_on: Disabled if there are no custom themes.
942 custom_theme: Message with additional information.
943 targetlist: Create menu from self.theme_elements.
944
945 Methods:
946 set_theme_type
947 paint_theme_sample
948 set_highlight_target
949 """
950 # Set current theme type radiobutton.
951 self.theme_source.set(idleConf.GetOption(
952 'main', 'Theme', 'default', type='bool', default=1))
953 # Set current theme.
954 current_option = idleConf.CurrentTheme()
955 # Load available theme option menus.
956 if self.theme_source.get(): # Default theme selected.
957 item_list = idleConf.GetSectionList('default', 'highlight')
958 item_list.sort()
959 self.builtinlist.SetMenu(item_list, current_option)
960 item_list = idleConf.GetSectionList('user', 'highlight')
961 item_list.sort()
962 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400963 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400964 self.custom_name.set('- no custom themes -')
965 else:
966 self.customlist.SetMenu(item_list, item_list[0])
967 else: # User theme selected.
968 item_list = idleConf.GetSectionList('user', 'highlight')
969 item_list.sort()
970 self.customlist.SetMenu(item_list, current_option)
971 item_list = idleConf.GetSectionList('default', 'highlight')
972 item_list.sort()
973 self.builtinlist.SetMenu(item_list, item_list[0])
974 self.set_theme_type()
975 # Load theme element option menu.
976 theme_names = list(self.theme_elements.keys())
977 theme_names.sort(key=lambda x: self.theme_elements[x][1])
978 self.targetlist.SetMenu(theme_names, theme_names[0])
979 self.paint_theme_sample()
980 self.set_highlight_target()
981
982 def var_changed_builtin_name(self, *params):
983 """Process new builtin theme selection.
984
985 Add the changed theme's name to the changed_items and recreate
986 the sample with the values from the selected theme.
987 """
988 old_themes = ('IDLE Classic', 'IDLE New')
989 value = self.builtin_name.get()
990 if value not in old_themes:
991 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
992 changes.add_option('main', 'Theme', 'name', old_themes[0])
993 changes.add_option('main', 'Theme', 'name2', value)
994 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400995 else:
996 changes.add_option('main', 'Theme', 'name', value)
997 changes.add_option('main', 'Theme', 'name2', '')
998 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400999 self.paint_theme_sample()
1000
1001 def var_changed_custom_name(self, *params):
1002 """Process new custom theme selection.
1003
1004 If a new custom theme is selected, add the name to the
1005 changed_items and apply the theme to the sample.
1006 """
1007 value = self.custom_name.get()
1008 if value != '- no custom themes -':
1009 changes.add_option('main', 'Theme', 'name', value)
1010 self.paint_theme_sample()
1011
1012 def var_changed_theme_source(self, *params):
1013 """Process toggle between builtin and custom theme.
1014
1015 Update the default toggle value and apply the newly
1016 selected theme type.
1017 """
1018 value = self.theme_source.get()
1019 changes.add_option('main', 'Theme', 'default', value)
1020 if value:
1021 self.var_changed_builtin_name()
1022 else:
1023 self.var_changed_custom_name()
1024
1025 def var_changed_color(self, *params):
1026 "Process change to color choice."
1027 self.on_new_color_set()
1028
1029 def var_changed_highlight_target(self, *params):
1030 "Process selection of new target tag for highlighting."
1031 self.set_highlight_target()
1032
1033 def set_theme_type(self):
1034 """Set available screen options based on builtin or custom theme.
1035
1036 Attributes accessed:
1037 theme_source
1038
1039 Attributes updated:
1040 builtinlist
1041 customlist
1042 button_delete_custom
1043 custom_theme_on
1044
1045 Called from:
1046 handler for builtin_theme_on and custom_theme_on
1047 delete_custom
1048 create_new
1049 load_theme_cfg
1050 """
1051 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001052 self.builtinlist['state'] = 'normal'
1053 self.customlist['state'] = 'disabled'
1054 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001055 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001056 self.builtinlist['state'] = 'disabled'
1057 self.custom_theme_on.state(('!disabled',))
1058 self.customlist['state'] = 'normal'
1059 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001060
1061 def get_color(self):
1062 """Handle button to select a new color for the target tag.
1063
1064 If a new color is selected while using a builtin theme, a
1065 name must be supplied to create a custom theme.
1066
1067 Attributes accessed:
1068 highlight_target
1069 frame_color_set
1070 theme_source
1071
1072 Attributes updated:
1073 color
1074
1075 Methods:
1076 get_new_theme_name
1077 create_new
1078 """
1079 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001080 prev_color = self.style.lookup(self.frame_color_set['style'],
1081 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001082 rgbTuplet, color_string = tkColorChooser.askcolor(
1083 parent=self, title='Pick new color for : '+target,
1084 initialcolor=prev_color)
1085 if color_string and (color_string != prev_color):
1086 # User didn't cancel and they chose a new color.
1087 if self.theme_source.get(): # Current theme is a built-in.
1088 message = ('Your changes will be saved as a new Custom Theme. '
1089 'Enter a name for your new Custom Theme below.')
1090 new_theme = self.get_new_theme_name(message)
1091 if not new_theme: # User cancelled custom theme creation.
1092 return
1093 else: # Create new custom theme based on previously active theme.
1094 self.create_new(new_theme)
1095 self.color.set(color_string)
1096 else: # Current theme is user defined.
1097 self.color.set(color_string)
1098
1099 def on_new_color_set(self):
1100 "Display sample of new color selection on the dialog."
1101 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001102 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001103 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1104 sample_element = self.theme_elements[self.highlight_target.get()][0]
1105 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1106 theme = self.custom_name.get()
1107 theme_element = sample_element + '-' + plane
1108 changes.add_option('highlight', theme, theme_element, new_color)
1109
1110 def get_new_theme_name(self, message):
1111 "Return name of new theme from query popup."
1112 used_names = (idleConf.GetSectionList('user', 'highlight') +
1113 idleConf.GetSectionList('default', 'highlight'))
1114 new_theme = SectionName(
1115 self, 'New Custom Theme', message, used_names).result
1116 return new_theme
1117
1118 def save_as_new_theme(self):
1119 """Prompt for new theme name and create the theme.
1120
1121 Methods:
1122 get_new_theme_name
1123 create_new
1124 """
1125 new_theme_name = self.get_new_theme_name('New Theme Name:')
1126 if new_theme_name:
1127 self.create_new(new_theme_name)
1128
1129 def create_new(self, new_theme_name):
1130 """Create a new custom theme with the given name.
1131
1132 Create the new theme based on the previously active theme
1133 with the current changes applied. Once it is saved, then
1134 activate the new theme.
1135
1136 Attributes accessed:
1137 builtin_name
1138 custom_name
1139
1140 Attributes updated:
1141 customlist
1142 theme_source
1143
1144 Method:
1145 save_new
1146 set_theme_type
1147 """
1148 if self.theme_source.get():
1149 theme_type = 'default'
1150 theme_name = self.builtin_name.get()
1151 else:
1152 theme_type = 'user'
1153 theme_name = self.custom_name.get()
1154 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1155 # Apply any of the old theme's unsaved changes to the new theme.
1156 if theme_name in changes['highlight']:
1157 theme_changes = changes['highlight'][theme_name]
1158 for element in theme_changes:
1159 new_theme[element] = theme_changes[element]
1160 # Save the new theme.
1161 self.save_new(new_theme_name, new_theme)
1162 # Change GUI over to the new theme.
1163 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1164 custom_theme_list.sort()
1165 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1166 self.theme_source.set(0)
1167 self.set_theme_type()
1168
1169 def set_highlight_target(self):
1170 """Set fg/bg toggle and color based on highlight tag target.
1171
1172 Instance variables accessed:
1173 highlight_target
1174
1175 Attributes updated:
1176 fg_on
1177 bg_on
1178 fg_bg_toggle
1179
1180 Methods:
1181 set_color_sample
1182
1183 Called from:
1184 var_changed_highlight_target
1185 load_theme_cfg
1186 """
1187 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001188 self.fg_on.state(('disabled',))
1189 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001190 self.fg_bg_toggle.set(1)
1191 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001192 self.fg_on.state(('!disabled',))
1193 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001194 self.fg_bg_toggle.set(1)
1195 self.set_color_sample()
1196
1197 def set_color_sample_binding(self, *args):
1198 """Change color sample based on foreground/background toggle.
1199
1200 Methods:
1201 set_color_sample
1202 """
1203 self.set_color_sample()
1204
1205 def set_color_sample(self):
1206 """Set the color of the frame background to reflect the selected target.
1207
1208 Instance variables accessed:
1209 theme_elements
1210 highlight_target
1211 fg_bg_toggle
1212 highlight_sample
1213
1214 Attributes updated:
1215 frame_color_set
1216 """
1217 # Set the color sample area.
1218 tag = self.theme_elements[self.highlight_target.get()][0]
1219 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1220 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001221 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001222
1223 def paint_theme_sample(self):
1224 """Apply the theme colors to each element tag in the sample text.
1225
1226 Instance attributes accessed:
1227 theme_elements
1228 theme_source
1229 builtin_name
1230 custom_name
1231
1232 Attributes updated:
1233 highlight_sample: Set the tag elements to the theme.
1234
1235 Methods:
1236 set_color_sample
1237
1238 Called from:
1239 var_changed_builtin_name
1240 var_changed_custom_name
1241 load_theme_cfg
1242 """
1243 if self.theme_source.get(): # Default theme
1244 theme = self.builtin_name.get()
1245 else: # User theme
1246 theme = self.custom_name.get()
1247 for element_title in self.theme_elements:
1248 element = self.theme_elements[element_title][0]
1249 colors = idleConf.GetHighlight(theme, element)
1250 if element == 'cursor': # Cursor sample needs special painting.
1251 colors['background'] = idleConf.GetHighlight(
1252 theme, 'normal', fgBg='bg')
1253 # Handle any unsaved changes to this theme.
1254 if theme in changes['highlight']:
1255 theme_dict = changes['highlight'][theme]
1256 if element + '-foreground' in theme_dict:
1257 colors['foreground'] = theme_dict[element + '-foreground']
1258 if element + '-background' in theme_dict:
1259 colors['background'] = theme_dict[element + '-background']
1260 self.highlight_sample.tag_config(element, **colors)
1261 self.set_color_sample()
1262
1263 def save_new(self, theme_name, theme):
1264 """Save a newly created theme to idleConf.
1265
1266 theme_name - string, the name of the new theme
1267 theme - dictionary containing the new theme
1268 """
1269 if not idleConf.userCfg['highlight'].has_section(theme_name):
1270 idleConf.userCfg['highlight'].add_section(theme_name)
1271 for element in theme:
1272 value = theme[element]
1273 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1274
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001275 def askyesno(self, *args, **kwargs):
1276 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001277 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001278
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001279 def delete_custom(self):
1280 """Handle event to delete custom theme.
1281
1282 The current theme is deactivated and the default theme is
1283 activated. The custom theme is permanently removed from
1284 the config file.
1285
1286 Attributes accessed:
1287 custom_name
1288
1289 Attributes updated:
1290 custom_theme_on
1291 customlist
1292 theme_source
1293 builtin_name
1294
1295 Methods:
1296 deactivate_current_config
1297 save_all_changed_extensions
1298 activate_config_changes
1299 set_theme_type
1300 """
1301 theme_name = self.custom_name.get()
1302 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001303 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001304 'Delete Theme', delmsg % theme_name, parent=self):
1305 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001306 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001307 # Remove theme from changes, config, and file.
1308 changes.delete_section('highlight', theme_name)
1309 # Reload user theme list.
1310 item_list = idleConf.GetSectionList('user', 'highlight')
1311 item_list.sort()
1312 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001313 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001314 self.customlist.SetMenu(item_list, '- no custom themes -')
1315 else:
1316 self.customlist.SetMenu(item_list, item_list[0])
1317 # Revert to default theme.
1318 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1319 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1320 # User can't back out of these changes, they must be applied now.
1321 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001322 self.cd.save_all_changed_extensions()
1323 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001324 self.set_theme_type()
1325
1326
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001327class KeysPage(Frame):
1328
1329 def __init__(self, master):
1330 super().__init__(master)
1331 self.cd = master.master
1332 self.create_page_keys()
1333 self.load_key_cfg()
1334
1335 def create_page_keys(self):
1336 """Return frame of widgets for Keys tab.
1337
1338 Enable users to provisionally change both individual and sets of
1339 keybindings (shortcut keys). Except for features implemented as
1340 extensions, keybindings are stored in complete sets called
1341 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1342 as far as the dialog is concerned. Any keyset can be used as the
1343 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1344
1345 Function load_key_cfg() initializes tk variables and keyset
1346 lists and calls load_keys_list for the current keyset.
1347 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1348 keyset_source, which controls if the current set of keybindings
1349 are from a builtin or custom keyset. DynOptionMenus builtinlist
1350 and customlist contain lists of the builtin and custom keysets,
1351 respectively, and the current item from each list is stored in
1352 vars builtin_name and custom_name.
1353
1354 Button delete_custom_keys invokes delete_custom_keys() to delete
1355 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1356 save_custom_keys invokes save_as_new_key_set() which calls
1357 get_new_keys_name() and create_new_key_set() to save a custom keyset
1358 and its keybindings to idleConf.userCfg['keys'].
1359
1360 Listbox bindingslist contains all of the keybindings for the
1361 selected keyset. The keybindings are loaded in load_keys_list()
1362 and are pairs of (event, [keys]) where keys can be a list
1363 of one or more key combinations to bind to the same event.
1364 Mouse button 1 click invokes on_bindingslist_select(), which
1365 allows button_new_keys to be clicked.
1366
1367 So, an item is selected in listbindings, which activates
1368 button_new_keys, and clicking button_new_keys calls function
1369 get_new_keys(). Function get_new_keys() gets the key mappings from the
1370 current keyset for the binding event item that was selected. The
1371 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001372 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001373 to be entered for that binding event. If the keys aren't
1374 changed, nothing happens. If the keys are changed and the keyset
1375 is a builtin, function get_new_keys_name() will be called
1376 for input of a custom keyset name. If no name is given, then the
1377 change to the keybinding will abort and no updates will be made. If
1378 a custom name is entered in the prompt or if the current keyset was
1379 already custom (and thus didn't require a prompt), then
1380 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1381 with the change to the event binding. The item listing in bindingslist
1382 is updated with the new keys. Var keybinding is also set which invokes
1383 the callback function, var_changed_keybinding, to add the change to
1384 the 'keys' or 'extensions' changes tracker based on the binding type.
1385
1386 Tk Variables:
1387 keybinding: Action/key bindings.
1388
1389 Methods:
1390 load_keys_list: Reload active set.
1391 create_new_key_set: Combine active keyset and changes.
1392 set_keys_type: Command for keyset_source.
1393 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1394 deactivate_current_config: Remove keys bindings in editors.
1395
1396 Widgets for KeysPage(frame): (*) widgets bound to self
1397 frame_key_sets: LabelFrame
1398 frames[0]: Frame
1399 (*)builtin_keyset_on: Radiobutton - var keyset_source
1400 (*)custom_keyset_on: Radiobutton - var keyset_source
1401 (*)builtinlist: DynOptionMenu - var builtin_name,
1402 func keybinding_selected
1403 (*)customlist: DynOptionMenu - var custom_name,
1404 func keybinding_selected
1405 (*)keys_message: Label
1406 frames[1]: Frame
1407 (*)button_delete_custom_keys: Button - delete_custom_keys
1408 (*)button_save_custom_keys: Button - save_as_new_key_set
1409 frame_custom: LabelFrame
1410 frame_target: Frame
1411 target_title: Label
1412 scroll_target_y: Scrollbar
1413 scroll_target_x: Scrollbar
1414 (*)bindingslist: ListBox - on_bindingslist_select
1415 (*)button_new_keys: Button - get_new_keys & ..._name
1416 """
1417 self.builtin_name = tracers.add(
1418 StringVar(self), self.var_changed_builtin_name)
1419 self.custom_name = tracers.add(
1420 StringVar(self), self.var_changed_custom_name)
1421 self.keyset_source = tracers.add(
1422 BooleanVar(self), self.var_changed_keyset_source)
1423 self.keybinding = tracers.add(
1424 StringVar(self), self.var_changed_keybinding)
1425
1426 # Create widgets:
1427 # body and section frames.
1428 frame_custom = LabelFrame(
1429 self, borderwidth=2, relief=GROOVE,
1430 text=' Custom Key Bindings ')
1431 frame_key_sets = LabelFrame(
1432 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1433 # frame_custom.
1434 frame_target = Frame(frame_custom)
1435 target_title = Label(frame_target, text='Action - Key(s)')
1436 scroll_target_y = Scrollbar(frame_target)
1437 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1438 self.bindingslist = Listbox(
1439 frame_target, takefocus=FALSE, exportselection=FALSE)
1440 self.bindingslist.bind('<ButtonRelease-1>',
1441 self.on_bindingslist_select)
1442 scroll_target_y['command'] = self.bindingslist.yview
1443 scroll_target_x['command'] = self.bindingslist.xview
1444 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1445 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1446 self.button_new_keys = Button(
1447 frame_custom, text='Get New Keys for Selection',
Terry Jan Reedye8f7c782017-11-28 21:52:32 -05001448 command=self.get_new_keys, state='disabled')
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001449 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001450 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001451 for i in range(2)]
1452 self.builtin_keyset_on = Radiobutton(
1453 frames[0], variable=self.keyset_source, value=1,
1454 command=self.set_keys_type, text='Use a Built-in Key Set')
1455 self.custom_keyset_on = Radiobutton(
1456 frames[0], variable=self.keyset_source, value=0,
1457 command=self.set_keys_type, text='Use a Custom Key Set')
1458 self.builtinlist = DynOptionMenu(
1459 frames[0], self.builtin_name, None, command=None)
1460 self.customlist = DynOptionMenu(
1461 frames[0], self.custom_name, None, command=None)
1462 self.button_delete_custom_keys = Button(
1463 frames[1], text='Delete Custom Key Set',
1464 command=self.delete_custom_keys)
1465 self.button_save_custom_keys = Button(
1466 frames[1], text='Save as New Custom Key Set',
1467 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001468 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001469
1470 # Pack widgets:
1471 # body.
1472 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1473 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1474 # frame_custom.
1475 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1476 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1477 # frame_target.
1478 frame_target.columnconfigure(0, weight=1)
1479 frame_target.rowconfigure(1, weight=1)
1480 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1481 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1482 scroll_target_y.grid(row=1, column=1, sticky=NS)
1483 scroll_target_x.grid(row=2, column=0, sticky=EW)
1484 # frame_key_sets.
1485 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1486 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1487 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1488 self.customlist.grid(row=1, column=1, sticky=NSEW)
1489 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1490 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1491 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1492 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1493 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1494
1495 def load_key_cfg(self):
1496 "Load current configuration settings for the keybinding options."
1497 # Set current keys type radiobutton.
1498 self.keyset_source.set(idleConf.GetOption(
1499 'main', 'Keys', 'default', type='bool', default=1))
1500 # Set current keys.
1501 current_option = idleConf.CurrentKeys()
1502 # Load available keyset option menus.
1503 if self.keyset_source.get(): # Default theme selected.
1504 item_list = idleConf.GetSectionList('default', 'keys')
1505 item_list.sort()
1506 self.builtinlist.SetMenu(item_list, current_option)
1507 item_list = idleConf.GetSectionList('user', 'keys')
1508 item_list.sort()
1509 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001510 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001511 self.custom_name.set('- no custom keys -')
1512 else:
1513 self.customlist.SetMenu(item_list, item_list[0])
1514 else: # User key set selected.
1515 item_list = idleConf.GetSectionList('user', 'keys')
1516 item_list.sort()
1517 self.customlist.SetMenu(item_list, current_option)
1518 item_list = idleConf.GetSectionList('default', 'keys')
1519 item_list.sort()
1520 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1521 self.set_keys_type()
1522 # Load keyset element list.
1523 keyset_name = idleConf.CurrentKeys()
1524 self.load_keys_list(keyset_name)
1525
1526 def var_changed_builtin_name(self, *params):
1527 "Process selection of builtin key set."
1528 old_keys = (
1529 'IDLE Classic Windows',
1530 'IDLE Classic Unix',
1531 'IDLE Classic Mac',
1532 'IDLE Classic OSX',
1533 )
1534 value = self.builtin_name.get()
1535 if value not in old_keys:
1536 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1537 changes.add_option('main', 'Keys', 'name', old_keys[0])
1538 changes.add_option('main', 'Keys', 'name2', value)
1539 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001540 else:
1541 changes.add_option('main', 'Keys', 'name', value)
1542 changes.add_option('main', 'Keys', 'name2', '')
1543 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001544 self.load_keys_list(value)
1545
1546 def var_changed_custom_name(self, *params):
1547 "Process selection of custom key set."
1548 value = self.custom_name.get()
1549 if value != '- no custom keys -':
1550 changes.add_option('main', 'Keys', 'name', value)
1551 self.load_keys_list(value)
1552
1553 def var_changed_keyset_source(self, *params):
1554 "Process toggle between builtin key set and custom key set."
1555 value = self.keyset_source.get()
1556 changes.add_option('main', 'Keys', 'default', value)
1557 if value:
1558 self.var_changed_builtin_name()
1559 else:
1560 self.var_changed_custom_name()
1561
1562 def var_changed_keybinding(self, *params):
1563 "Store change to a keybinding."
1564 value = self.keybinding.get()
1565 key_set = self.custom_name.get()
1566 event = self.bindingslist.get(ANCHOR).split()[0]
1567 if idleConf.IsCoreBinding(event):
1568 changes.add_option('keys', key_set, event, value)
1569 else: # Event is an extension binding.
1570 ext_name = idleConf.GetExtnNameForEvent(event)
1571 ext_keybind_section = ext_name + '_cfgBindings'
1572 changes.add_option('extensions', ext_keybind_section, event, value)
1573
1574 def set_keys_type(self):
1575 "Set available screen options based on builtin or custom key set."
1576 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001577 self.builtinlist['state'] = 'normal'
1578 self.customlist['state'] = 'disabled'
1579 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001580 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001581 self.builtinlist['state'] = 'disabled'
1582 self.custom_keyset_on.state(('!disabled',))
1583 self.customlist['state'] = 'normal'
1584 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001585
1586 def get_new_keys(self):
1587 """Handle event to change key binding for selected line.
1588
1589 A selection of a key/binding in the list of current
1590 bindings pops up a dialog to enter a new binding. If
1591 the current key set is builtin and a binding has
1592 changed, then a name for a custom key set needs to be
1593 entered for the change to be applied.
1594 """
1595 list_index = self.bindingslist.index(ANCHOR)
1596 binding = self.bindingslist.get(list_index)
1597 bind_name = binding.split()[0]
1598 if self.keyset_source.get():
1599 current_key_set_name = self.builtin_name.get()
1600 else:
1601 current_key_set_name = self.custom_name.get()
1602 current_bindings = idleConf.GetCurrentKeySet()
1603 if current_key_set_name in changes['keys']: # unsaved changes
1604 key_set_changes = changes['keys'][current_key_set_name]
1605 for event in key_set_changes:
1606 current_bindings[event] = key_set_changes[event].split()
1607 current_key_sequences = list(current_bindings.values())
1608 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1609 current_key_sequences).result
1610 if new_keys:
1611 if self.keyset_source.get(): # Current key set is a built-in.
1612 message = ('Your changes will be saved as a new Custom Key Set.'
1613 ' Enter a name for your new Custom Key Set below.')
1614 new_keyset = self.get_new_keys_name(message)
1615 if not new_keyset: # User cancelled custom key set creation.
1616 self.bindingslist.select_set(list_index)
1617 self.bindingslist.select_anchor(list_index)
1618 return
1619 else: # Create new custom key set based on previously active key set.
1620 self.create_new_key_set(new_keyset)
1621 self.bindingslist.delete(list_index)
1622 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1623 self.bindingslist.select_set(list_index)
1624 self.bindingslist.select_anchor(list_index)
1625 self.keybinding.set(new_keys)
1626 else:
1627 self.bindingslist.select_set(list_index)
1628 self.bindingslist.select_anchor(list_index)
1629
1630 def get_new_keys_name(self, message):
1631 "Return new key set name from query popup."
1632 used_names = (idleConf.GetSectionList('user', 'keys') +
1633 idleConf.GetSectionList('default', 'keys'))
1634 new_keyset = SectionName(
1635 self, 'New Custom Key Set', message, used_names).result
1636 return new_keyset
1637
1638 def save_as_new_key_set(self):
1639 "Prompt for name of new key set and save changes using that name."
1640 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1641 if new_keys_name:
1642 self.create_new_key_set(new_keys_name)
1643
1644 def on_bindingslist_select(self, event):
1645 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001646 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001647
1648 def create_new_key_set(self, new_key_set_name):
1649 """Create a new custom key set with the given name.
1650
1651 Copy the bindings/keys from the previously active keyset
1652 to the new keyset and activate the new custom keyset.
1653 """
1654 if self.keyset_source.get():
1655 prev_key_set_name = self.builtin_name.get()
1656 else:
1657 prev_key_set_name = self.custom_name.get()
1658 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1659 new_keys = {}
1660 for event in prev_keys: # Add key set to changed items.
1661 event_name = event[2:-2] # Trim off the angle brackets.
1662 binding = ' '.join(prev_keys[event])
1663 new_keys[event_name] = binding
1664 # Handle any unsaved changes to prev key set.
1665 if prev_key_set_name in changes['keys']:
1666 key_set_changes = changes['keys'][prev_key_set_name]
1667 for event in key_set_changes:
1668 new_keys[event] = key_set_changes[event]
1669 # Save the new key set.
1670 self.save_new_key_set(new_key_set_name, new_keys)
1671 # Change GUI over to the new key set.
1672 custom_key_list = idleConf.GetSectionList('user', 'keys')
1673 custom_key_list.sort()
1674 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1675 self.keyset_source.set(0)
1676 self.set_keys_type()
1677
1678 def load_keys_list(self, keyset_name):
1679 """Reload the list of action/key binding pairs for the active key set.
1680
1681 An action/key binding can be selected to change the key binding.
1682 """
1683 reselect = False
1684 if self.bindingslist.curselection():
1685 reselect = True
1686 list_index = self.bindingslist.index(ANCHOR)
1687 keyset = idleConf.GetKeySet(keyset_name)
1688 bind_names = list(keyset.keys())
1689 bind_names.sort()
1690 self.bindingslist.delete(0, END)
1691 for bind_name in bind_names:
1692 key = ' '.join(keyset[bind_name])
1693 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1694 if keyset_name in changes['keys']:
1695 # Handle any unsaved changes to this key set.
1696 if bind_name in changes['keys'][keyset_name]:
1697 key = changes['keys'][keyset_name][bind_name]
1698 self.bindingslist.insert(END, bind_name+' - '+key)
1699 if reselect:
1700 self.bindingslist.see(list_index)
1701 self.bindingslist.select_set(list_index)
1702 self.bindingslist.select_anchor(list_index)
1703
1704 @staticmethod
1705 def save_new_key_set(keyset_name, keyset):
1706 """Save a newly created core key set.
1707
1708 Add keyset to idleConf.userCfg['keys'], not to disk.
1709 If the keyset doesn't exist, it is created. The
1710 binding/keys are taken from the keyset argument.
1711
1712 keyset_name - string, the name of the new key set
1713 keyset - dictionary containing the new keybindings
1714 """
1715 if not idleConf.userCfg['keys'].has_section(keyset_name):
1716 idleConf.userCfg['keys'].add_section(keyset_name)
1717 for event in keyset:
1718 value = keyset[event]
1719 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1720
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001721 def askyesno(self, *args, **kwargs):
1722 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001723 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001724
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001725 def delete_custom_keys(self):
1726 """Handle event to delete a custom key set.
1727
1728 Applying the delete deactivates the current configuration and
1729 reverts to the default. The custom key set is permanently
1730 deleted from the config file.
1731 """
1732 keyset_name = self.custom_name.get()
1733 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001734 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001735 'Delete Key Set', delmsg % keyset_name, parent=self):
1736 return
1737 self.cd.deactivate_current_config()
1738 # Remove key set from changes, config, and file.
1739 changes.delete_section('keys', keyset_name)
1740 # Reload user key set list.
1741 item_list = idleConf.GetSectionList('user', 'keys')
1742 item_list.sort()
1743 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001744 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001745 self.customlist.SetMenu(item_list, '- no custom keys -')
1746 else:
1747 self.customlist.SetMenu(item_list, item_list[0])
1748 # Revert to default key set.
1749 self.keyset_source.set(idleConf.defaultCfg['main']
1750 .Get('Keys', 'default'))
1751 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1752 or idleConf.default_keys())
1753 # User can't back out of these changes, they must be applied now.
1754 changes.save_all()
1755 self.cd.save_all_changed_extensions()
1756 self.cd.activate_config_changes()
1757 self.set_keys_type()
1758
1759
csabellae8eb17b2017-07-30 18:39:17 -04001760class GenPage(Frame):
1761
csabella6f446be2017-08-01 00:24:07 -04001762 def __init__(self, master):
1763 super().__init__(master)
csabellae8eb17b2017-07-30 18:39:17 -04001764 self.create_page_general()
1765 self.load_general_cfg()
1766
1767 def create_page_general(self):
1768 """Return frame of widgets for General tab.
1769
1770 Enable users to provisionally change general options. Function
1771 load_general_cfg intializes tk variables and helplist using
1772 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1773 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1774 set var autosave. Entry boxes win_width_int and win_height_int
1775 set var win_width and win_height. Setting var_name invokes the
1776 default callback that adds option to changes.
1777
1778 Helplist: load_general_cfg loads list user_helplist with
1779 name, position pairs and copies names to listbox helplist.
1780 Clicking a name invokes help_source selected. Clicking
1781 button_helplist_name invokes helplist_item_name, which also
1782 changes user_helplist. These functions all call
1783 set_add_delete_state. All but load call update_help_changes to
1784 rewrite changes['main']['HelpFiles'].
1785
Cheryl Sabella2f896462017-08-14 21:21:43 -04001786 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001787 frame_window: LabelFrame
1788 frame_run: Frame
1789 startup_title: Label
1790 (*)startup_editor_on: Radiobutton - startup_edit
1791 (*)startup_shell_on: Radiobutton - startup_edit
1792 frame_win_size: Frame
1793 win_size_title: Label
1794 win_width_title: Label
1795 (*)win_width_int: Entry - win_width
1796 win_height_title: Label
1797 (*)win_height_int: Entry - win_height
Cheryl Sabella845d8642018-02-04 18:15:21 -05001798 frame_autocomplete: Frame
1799 auto_wait_title: Label
1800 (*)auto_wait_int: Entry - autocomplete_wait
1801 frame_paren1: Frame
1802 paren_style_title: Label
1803 (*)paren_style_type: OptionMenu - paren_style
1804 frame_paren2: Frame
1805 paren_time_title: Label
1806 (*)paren_flash_time: Entry - flash_delay
1807 (*)bell_on: Checkbutton - paren_bell
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001808 frame_editor: LabelFrame
1809 frame_save: Frame
1810 run_save_title: Label
1811 (*)save_ask_on: Radiobutton - autosave
1812 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella845d8642018-02-04 18:15:21 -05001813 frame_format: Frame
1814 format_width_title: Label
1815 (*)format_width_int: Entry - format_width
1816 frame_context: Frame
1817 context_title: Label
1818 (*)context_int: Entry - context_lines
Cheryl Sabella2f896462017-08-14 21:21:43 -04001819 frame_help: LabelFrame
1820 frame_helplist: Frame
1821 frame_helplist_buttons: Frame
1822 (*)button_helplist_edit
1823 (*)button_helplist_add
1824 (*)button_helplist_remove
1825 (*)helplist: ListBox
1826 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001827 """
wohlganger58fc71c2017-09-10 16:19:47 -05001828 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001829 self.startup_edit = tracers.add(
1830 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001831 self.win_width = tracers.add(
1832 StringVar(self), ('main', 'EditorWindow', 'width'))
1833 self.win_height = tracers.add(
1834 StringVar(self), ('main', 'EditorWindow', 'height'))
wohlganger58fc71c2017-09-10 16:19:47 -05001835 self.autocomplete_wait = tracers.add(
1836 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1837 self.paren_style = tracers.add(
1838 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1839 self.flash_delay = tracers.add(
1840 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1841 self.paren_bell = tracers.add(
1842 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001843
wohlganger58fc71c2017-09-10 16:19:47 -05001844 self.autosave = tracers.add(
1845 IntVar(self), ('main', 'General', 'autosave'))
1846 self.format_width = tracers.add(
1847 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
1848 self.context_lines = tracers.add(
Cheryl Sabella29996a12018-06-01 19:23:00 -04001849 StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
wohlganger58fc71c2017-09-10 16:19:47 -05001850
1851 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001852 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001853 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1854 text=' Window Preferences')
1855 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1856 text=' Editor Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001857 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1858 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001859 # Frame_window.
1860 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001861 startup_title = Label(frame_run, text='At Startup')
1862 self.startup_editor_on = Radiobutton(
1863 frame_run, variable=self.startup_edit, value=1,
1864 text="Open Edit Window")
1865 self.startup_shell_on = Radiobutton(
1866 frame_run, variable=self.startup_edit, value=0,
1867 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001868
wohlganger58fc71c2017-09-10 16:19:47 -05001869 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001870 win_size_title = Label(
1871 frame_win_size, text='Initial Window Size (in characters)')
1872 win_width_title = Label(frame_win_size, text='Width')
1873 self.win_width_int = Entry(
1874 frame_win_size, textvariable=self.win_width, width=3)
1875 win_height_title = Label(frame_win_size, text='Height')
1876 self.win_height_int = Entry(
1877 frame_win_size, textvariable=self.win_height, width=3)
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001878
wohlganger58fc71c2017-09-10 16:19:47 -05001879 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1880 auto_wait_title = Label(frame_autocomplete,
1881 text='Completions Popup Wait (milliseconds)')
1882 self.auto_wait_int = Entry(frame_autocomplete, width=6,
1883 textvariable=self.autocomplete_wait)
1884
1885 frame_paren1 = Frame(frame_window, borderwidth=0)
1886 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1887 self.paren_style_type = OptionMenu(
1888 frame_paren1, self.paren_style, 'expression',
1889 "opener","parens","expression")
1890 frame_paren2 = Frame(frame_window, borderwidth=0)
1891 paren_time_title = Label(
1892 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1893 '(0 is until next input)')
1894 self.paren_flash_time = Entry(
1895 frame_paren2, textvariable=self.flash_delay, width=6)
1896 self.bell_on = Checkbutton(
1897 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1898
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001899 # Frame_editor.
1900 frame_save = Frame(frame_editor, borderwidth=0)
1901 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1902 self.save_ask_on = Radiobutton(
1903 frame_save, variable=self.autosave, value=0,
1904 text="Prompt to Save")
1905 self.save_auto_on = Radiobutton(
1906 frame_save, variable=self.autosave, value=1,
1907 text='No Prompt')
1908
wohlganger58fc71c2017-09-10 16:19:47 -05001909 frame_format = Frame(frame_editor, borderwidth=0)
1910 format_width_title = Label(frame_format,
1911 text='Format Paragraph Max Width')
1912 self.format_width_int = Entry(
1913 frame_format, textvariable=self.format_width, width=4)
1914
1915 frame_context = Frame(frame_editor, borderwidth=0)
Cheryl Sabella29996a12018-06-01 19:23:00 -04001916 context_title = Label(frame_context, text='Max Context Lines :')
wohlganger58fc71c2017-09-10 16:19:47 -05001917 self.context_int = Entry(
1918 frame_context, textvariable=self.context_lines, width=3)
1919
1920
csabellae8eb17b2017-07-30 18:39:17 -04001921 # frame_help.
1922 frame_helplist = Frame(frame_help)
1923 frame_helplist_buttons = Frame(frame_helplist)
1924 self.helplist = Listbox(
1925 frame_helplist, height=5, takefocus=True,
1926 exportselection=FALSE)
1927 scroll_helplist = Scrollbar(frame_helplist)
1928 scroll_helplist['command'] = self.helplist.yview
1929 self.helplist['yscrollcommand'] = scroll_helplist.set
1930 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1931 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001932 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001933 width=8, command=self.helplist_item_edit)
1934 self.button_helplist_add = Button(
1935 frame_helplist_buttons, text='Add',
1936 width=8, command=self.helplist_item_add)
1937 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04001938 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04001939 width=8, command=self.helplist_item_remove)
1940
1941 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001942 # Body.
1943 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1944 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04001945 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1946 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001947 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001948 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1949 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1950 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04001951 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001952 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04001953 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1954 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1955 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1956 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1957 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001958 # frame_autocomplete.
1959 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
1960 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1961 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
1962 # frame_paren.
1963 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
1964 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1965 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
1966 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
1967 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
1968 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
1969 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
1970
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001971 # frame_save.
1972 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
1973 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1974 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1975 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05001976 # frame_format.
1977 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
1978 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1979 self.format_width_int.pack(side=TOP, padx=10, pady=5)
1980 # frame_context.
1981 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
1982 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1983 self.context_int.pack(side=TOP, padx=5, pady=5)
1984
csabellae8eb17b2017-07-30 18:39:17 -04001985 # frame_help.
1986 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1987 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1988 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1989 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1990 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1991 self.button_helplist_add.pack(side=TOP, anchor=W)
1992 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1993
1994 def load_general_cfg(self):
1995 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05001996 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04001997 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05001998 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04001999 self.win_width.set(idleConf.GetOption(
2000 'main', 'EditorWindow', 'width', type='int'))
2001 self.win_height.set(idleConf.GetOption(
2002 'main', 'EditorWindow', 'height', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002003 self.autocomplete_wait.set(idleConf.GetOption(
2004 'extensions', 'AutoComplete', 'popupwait', type='int'))
2005 self.paren_style.set(idleConf.GetOption(
2006 'extensions', 'ParenMatch', 'style'))
2007 self.flash_delay.set(idleConf.GetOption(
2008 'extensions', 'ParenMatch', 'flash-delay', type='int'))
2009 self.paren_bell.set(idleConf.GetOption(
2010 'extensions', 'ParenMatch', 'bell'))
2011
2012 # Set variables for editor windows.
2013 self.autosave.set(idleConf.GetOption(
2014 'main', 'General', 'autosave', default=0, type='bool'))
2015 self.format_width.set(idleConf.GetOption(
2016 'extensions', 'FormatParagraph', 'max-width', type='int'))
2017 self.context_lines.set(idleConf.GetOption(
Cheryl Sabella29996a12018-06-01 19:23:00 -04002018 'extensions', 'CodeContext', 'maxlines', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002019
csabellae8eb17b2017-07-30 18:39:17 -04002020 # Set additional help sources.
2021 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2022 self.helplist.delete(0, 'end')
2023 for help_item in self.user_helplist:
2024 self.helplist.insert(END, help_item[0])
2025 self.set_add_delete_state()
2026
2027 def help_source_selected(self, event):
2028 "Handle event for selecting additional help."
2029 self.set_add_delete_state()
2030
2031 def set_add_delete_state(self):
2032 "Toggle the state for the help list buttons based on list entries."
2033 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002034 self.button_helplist_edit.state(('disabled',))
2035 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002036 else: # Some entries.
2037 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002038 self.button_helplist_edit.state(('!disabled',))
2039 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002040 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002041 self.button_helplist_edit.state(('disabled',))
2042 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002043
2044 def helplist_item_add(self):
2045 """Handle add button for the help list.
2046
2047 Query for name and location of new help sources and add
2048 them to the list.
2049 """
2050 help_source = HelpSource(self, 'New Help Source').result
2051 if help_source:
2052 self.user_helplist.append(help_source)
2053 self.helplist.insert(END, help_source[0])
2054 self.update_help_changes()
2055
2056 def helplist_item_edit(self):
2057 """Handle edit button for the help list.
2058
2059 Query with existing help source information and update
2060 config if the values are changed.
2061 """
2062 item_index = self.helplist.index(ANCHOR)
2063 help_source = self.user_helplist[item_index]
2064 new_help_source = HelpSource(
2065 self, 'Edit Help Source',
2066 menuitem=help_source[0],
2067 filepath=help_source[1],
2068 ).result
2069 if new_help_source and new_help_source != help_source:
2070 self.user_helplist[item_index] = new_help_source
2071 self.helplist.delete(item_index)
2072 self.helplist.insert(item_index, new_help_source[0])
2073 self.update_help_changes()
2074 self.set_add_delete_state() # Selected will be un-selected
2075
2076 def helplist_item_remove(self):
2077 """Handle remove button for the help list.
2078
2079 Delete the help list item from config.
2080 """
2081 item_index = self.helplist.index(ANCHOR)
2082 del(self.user_helplist[item_index])
2083 self.helplist.delete(item_index)
2084 self.update_help_changes()
2085 self.set_add_delete_state()
2086
2087 def update_help_changes(self):
2088 "Clear and rebuild the HelpFiles section in changes"
2089 changes['main']['HelpFiles'] = {}
2090 for num in range(1, len(self.user_helplist) + 1):
2091 changes.add_option(
2092 'main', 'HelpFiles', str(num),
2093 ';'.join(self.user_helplist[num-1][:2]))
2094
2095
csabella45bf7232017-07-26 19:09:58 -04002096class VarTrace:
2097 """Maintain Tk variables trace state."""
2098
2099 def __init__(self):
2100 """Store Tk variables and callbacks.
2101
2102 untraced: List of tuples (var, callback)
2103 that do not have the callback attached
2104 to the Tk var.
2105 traced: List of tuples (var, callback) where
2106 that callback has been attached to the var.
2107 """
2108 self.untraced = []
2109 self.traced = []
2110
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002111 def clear(self):
2112 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002113 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002114 self.untraced.clear()
2115 self.traced.clear()
2116
csabella45bf7232017-07-26 19:09:58 -04002117 def add(self, var, callback):
2118 """Add (var, callback) tuple to untraced list.
2119
2120 Args:
2121 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002122 callback: Either function name to be used as a callback
2123 or a tuple with IdleConf config-type, section, and
2124 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002125
2126 Return:
2127 Tk variable instance.
2128 """
2129 if isinstance(callback, tuple):
2130 callback = self.make_callback(var, callback)
2131 self.untraced.append((var, callback))
2132 return var
2133
2134 @staticmethod
2135 def make_callback(var, config):
2136 "Return default callback function to add values to changes instance."
2137 def default_callback(*params):
2138 "Add config values to changes instance."
2139 changes.add_option(*config, var.get())
2140 return default_callback
2141
2142 def attach(self):
2143 "Attach callback to all vars that are not traced."
2144 while self.untraced:
2145 var, callback = self.untraced.pop()
2146 var.trace_add('write', callback)
2147 self.traced.append((var, callback))
2148
2149 def detach(self):
2150 "Remove callback from traced vars."
2151 while self.traced:
2152 var, callback = self.traced.pop()
2153 var.trace_remove('write', var.trace_info()[0][1])
2154 self.untraced.append((var, callback))
2155
2156
csabella5b591542017-07-28 14:40:59 -04002157tracers = VarTrace()
2158
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002159help_common = '''\
2160When you click either the Apply or Ok buttons, settings in this
2161dialog that are different from IDLE's default are saved in
2162a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002163these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002164machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002165'''
2166help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002167 'Fonts/Tabs':'''
2168Font sample: This shows what a selection of Basic Multilingual Plane
2169unicode characters look like for the current font selection. If the
2170selected font does not define a character, Tk attempts to find another
2171font that does. Substitute glyphs depend on what is available on a
2172particular system and will not necessarily have the same size as the
2173font selected. Line contains 20 characters up to Devanagari, 14 for
2174Tamil, and 10 for East Asia.
2175
2176Hebrew and Arabic letters should display right to left, starting with
2177alef, \u05d0 and \u0627. Arabic digits display left to right. The
2178Devanagari and Tamil lines start with digits. The East Asian lines
2179are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2180Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002181
2182You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002183''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002184 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002185Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002186The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002187be used with older IDLE releases if it is saved as a custom
2188theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002189''',
2190 'Keys': '''
2191Keys:
2192The IDLE Modern Unix key set is new in June 2016. It can only
2193be used with older IDLE releases if it is saved as a custom
2194key set, with a different name.
2195''',
wohlganger58fc71c2017-09-10 16:19:47 -05002196 'General': '''
2197General:
wohlgangerfae2c352017-06-27 21:36:23 -05002198
wohlganger58fc71c2017-09-10 16:19:47 -05002199AutoComplete: Popupwait is milleseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002200cursor movement, before popping up completion box. Key char is '.' after
2201identifier or a '/' (or '\\' on Windows) within a string.
2202
2203FormatParagraph: Max-width is max chars in lines after re-formatting.
2204Use with paragraphs in both strings and comment blocks.
2205
2206ParenMatch: Style indicates what is highlighted when closer is entered:
2207'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2208'expression' (default) - also everything in between. Flash-delay is how
2209long to highlight if cursor is not moved (0 means forever).
Cheryl Sabella29996a12018-06-01 19:23:00 -04002210
2211CodeContext: Maxlines is the maximum number of code context lines to
2212display when Code Context is turned on for an editor window.
wohlgangerfae2c352017-06-27 21:36:23 -05002213'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002214}
2215
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002216
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002217def is_int(s):
2218 "Return 's is blank or represents an int'"
2219 if not s:
2220 return True
2221 try:
2222 int(s)
2223 return True
2224 except ValueError:
2225 return False
2226
2227
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002228class VerticalScrolledFrame(Frame):
2229 """A pure Tkinter vertically scrollable frame.
2230
2231 * Use the 'interior' attribute to place widgets inside the scrollable frame
2232 * Construct and pack/place/grid normally
2233 * This frame only allows vertical scrolling
2234 """
2235 def __init__(self, parent, *args, **kw):
2236 Frame.__init__(self, parent, *args, **kw)
2237
csabella7eb58832017-07-04 21:30:58 -04002238 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002239 vscrollbar = Scrollbar(self, orient=VERTICAL)
2240 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002241 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002242 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002243 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2244 vscrollbar.config(command=canvas.yview)
2245
csabella7eb58832017-07-04 21:30:58 -04002246 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002247 canvas.xview_moveto(0)
2248 canvas.yview_moveto(0)
2249
csabella7eb58832017-07-04 21:30:58 -04002250 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002251 self.interior = interior = Frame(canvas)
2252 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2253
csabella7eb58832017-07-04 21:30:58 -04002254 # Track changes to the canvas and frame width and sync them,
2255 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002256 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002257 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002258 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2259 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002260 interior.bind('<Configure>', _configure_interior)
2261
2262 def _configure_canvas(event):
2263 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002264 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002265 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2266 canvas.bind('<Configure>', _configure_canvas)
2267
2268 return
2269
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002270
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002271if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04002272 import unittest
2273 unittest.main('idlelib.idle_test.test_configdialog',
2274 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002275 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002276 run(ConfigDialog)