blob: df216582fe60930c27732c8ab52c32d5ec83511a [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"""
Tal Einat1ebee372019-07-23 13:02:11 +030012import re
13
Cheryl Sabella7028e592017-08-26 14:26:02 -040014from tkinter import (Toplevel, Listbox, Text, Scale, Canvas,
csabellabac7d332017-06-26 17:46:26 -040015 StringVar, BooleanVar, IntVar, TRUE, FALSE,
Terry Jan Reedye8f7c782017-11-28 21:52:32 -050016 TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE,
17 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
Louie Lubb2bae82017-07-10 06:57:18 +080018 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END)
Terry Jan Reedyaff0ada2019-01-02 22:04:06 -050019from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label,
wohlganger58fc71c2017-09-10 16:19:47 -050020 OptionMenu, Notebook, Radiobutton, Scrollbar, Style)
Georg Brandl14fc4272008-05-17 18:39:55 +000021import tkinter.colorchooser as tkColorChooser
22import tkinter.font as tkFont
Terry Jan Reedy3457f422017-08-27 16:39:41 -040023from tkinter import messagebox
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000024
terryjreedy349abd92017-07-07 16:00:57 -040025from idlelib.config import idleConf, ConfigChanges
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040026from idlelib.config_key import GetKeysDialog
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040027from idlelib.dynoption import DynOptionMenu
28from idlelib import macosx
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040029from idlelib.query import SectionName, HelpSource
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040030from idlelib.textview import view_text
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040031from idlelib.autocomplete import AutoComplete
32from idlelib.codecontext import CodeContext
33from idlelib.parenmatch import ParenMatch
Cheryl Sabella82494aa2019-07-17 09:44:44 -040034from idlelib.format import FormatParagraph
Tal Einat604e7b92018-09-25 15:10:14 +030035from idlelib.squeezer import Squeezer
Tal Einat3221a632019-07-27 19:57:48 +030036from idlelib.textview import ScrollableTextFrame
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -040037
terryjreedy349abd92017-07-07 16:00:57 -040038changes = ConfigChanges()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -040039# Reload changed options in the following classes.
Tal Einat604e7b92018-09-25 15:10:14 +030040reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph,
41 Squeezer)
terryjreedy349abd92017-07-07 16:00:57 -040042
csabella5b591542017-07-28 14:40:59 -040043
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000044class ConfigDialog(Toplevel):
csabella7eb58832017-07-04 21:30:58 -040045 """Config dialog for IDLE.
46 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000047
Terry Jan Reedybfebfd82017-09-30 17:37:53 -040048 def __init__(self, parent, title='', *, _htest=False, _utest=False):
csabella7eb58832017-07-04 21:30:58 -040049 """Show the tabbed dialog for user configuration.
50
csabella36329a42017-07-13 23:32:01 -040051 Args:
52 parent - parent of this dialog
53 title - string which is the title of this popup dialog
54 _htest - bool, change box location when running htest
55 _utest - bool, don't wait_window when running unittest
56
57 Note: Focus set on font page fontlist.
58
59 Methods:
60 create_widgets
61 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040062 """
Steven M. Gavad721c482001-07-31 10:46:53 +000063 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040064 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040065 if _htest:
66 parent.instance_dict = {}
Louie Lu9b622fb2017-07-14 08:35:48 +080067 if not _utest:
68 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000069
Steven M. Gavad721c482001-07-31 10:46:53 +000070 self.configure(borderwidth=5)
Terry Jan Reedycd567362014-10-17 01:31:35 -040071 self.title(title or 'IDLE Preferences')
csabellabac7d332017-06-26 17:46:26 -040072 x = parent.winfo_rootx() + 20
73 y = parent.winfo_rooty() + (30 if not _htest else 150)
74 self.geometry(f'+{x}+{y}')
csabella7eb58832017-07-04 21:30:58 -040075 # Each theme element key is its display name.
76 # The first value of the tuple is the sample area tag name.
77 # The second value is the display name list sort index.
csabellabac7d332017-06-26 17:46:26 -040078 self.create_widgets()
Terry Jan Reedy4036d872014-08-03 23:02:58 -040079 self.resizable(height=FALSE, width=FALSE)
Steven M. Gavad721c482001-07-31 10:46:53 +000080 self.transient(parent)
csabellabac7d332017-06-26 17:46:26 -040081 self.protocol("WM_DELETE_WINDOW", self.cancel)
csabella9397e2a2017-07-30 13:34:25 -040082 self.fontpage.fontlist.focus_set()
csabella7eb58832017-07-04 21:30:58 -040083 # XXX Decide whether to keep or delete these key bindings.
84 # Key bindings for this dialog.
85 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
86 # self.bind('<Alt-a>', self.Apply) #apply changes, save
87 # self.bind('<F1>', self.Help) #context help
Cheryl Sabella8f7a7982017-08-19 22:04:40 -040088 # Attach callbacks after loading config to avoid calling them.
csabella5b591542017-07-28 14:40:59 -040089 tracers.attach()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000090
Terry Jan Reedycfa89502014-07-14 23:07:32 -040091 if not _utest:
Louie Lu9b622fb2017-07-14 08:35:48 +080092 self.grab_set()
Terry Jan Reedycfa89502014-07-14 23:07:32 -040093 self.wm_deiconify()
94 self.wait_window()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +000095
csabellabac7d332017-06-26 17:46:26 -040096 def create_widgets(self):
csabella36329a42017-07-13 23:32:01 -040097 """Create and place widgets for tabbed dialog.
98
99 Widgets Bound to self:
csabellae8eb17b2017-07-30 18:39:17 -0400100 note: Notebook
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400101 highpage: HighPage
csabellae8eb17b2017-07-30 18:39:17 -0400102 fontpage: FontPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400103 keyspage: KeysPage
csabellae8eb17b2017-07-30 18:39:17 -0400104 genpage: GenPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400105 extpage: self.create_page_extensions
csabella36329a42017-07-13 23:32:01 -0400106
107 Methods:
csabella36329a42017-07-13 23:32:01 -0400108 create_action_buttons
109 load_configs: Load pages except for extensions.
csabella36329a42017-07-13 23:32:01 -0400110 activate_config_changes: Tell editors to reload.
111 """
Terry Jan Reedyd6e2f262017-09-19 19:01:45 -0400112 self.note = note = Notebook(self)
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400113 self.highpage = HighPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400114 self.fontpage = FontPage(note, self.highpage)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400115 self.keyspage = KeysPage(note)
csabellae8eb17b2017-07-30 18:39:17 -0400116 self.genpage = GenPage(note)
csabella9397e2a2017-07-30 13:34:25 -0400117 self.extpage = self.create_page_extensions()
118 note.add(self.fontpage, text='Fonts/Tabs')
119 note.add(self.highpage, text='Highlights')
120 note.add(self.keyspage, text=' Keys ')
121 note.add(self.genpage, text=' General ')
122 note.add(self.extpage, text='Extensions')
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400123 note.enable_traversal()
124 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400125 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400126
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400127 def create_action_buttons(self):
csabella36329a42017-07-13 23:32:01 -0400128 """Return frame of action buttons for dialog.
129
130 Methods:
131 ok
132 apply
133 cancel
134 help
135
136 Widget Structure:
137 outer: Frame
138 buttons: Frame
139 (no assignment): Button (ok)
140 (no assignment): Button (apply)
141 (no assignment): Button (cancel)
142 (no assignment): Button (help)
143 (no assignment): Frame
144 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400145 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400146 # Changing the default padding on OSX results in unreadable
csabella7eb58832017-07-04 21:30:58 -0400147 # text in the buttons.
csabellabac7d332017-06-26 17:46:26 -0400148 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000149 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400150 padding_args = {'padding': (6, 3)}
151 outer = Frame(self, padding=2)
152 buttons = Frame(outer, padding=2)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400153 for txt, cmd in (
csabellabac7d332017-06-26 17:46:26 -0400154 ('Ok', self.ok),
155 ('Apply', self.apply),
156 ('Cancel', self.cancel),
157 ('Help', self.help)):
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400158 Button(buttons, text=txt, command=cmd, takefocus=FALSE,
csabellabac7d332017-06-26 17:46:26 -0400159 **padding_args).pack(side=LEFT, padx=5)
csabella7eb58832017-07-04 21:30:58 -0400160 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400161 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
162 buttons.pack(side=BOTTOM)
163 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400164
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400165 def ok(self):
166 """Apply config changes, then dismiss dialog.
167
168 Methods:
169 apply
170 destroy: inherited
171 """
172 self.apply()
173 self.destroy()
174
175 def apply(self):
176 """Apply config changes and leave dialog open.
177
178 Methods:
179 deactivate_current_config
180 save_all_changed_extensions
181 activate_config_changes
182 """
183 self.deactivate_current_config()
184 changes.save_all()
185 self.save_all_changed_extensions()
186 self.activate_config_changes()
187
188 def cancel(self):
189 """Dismiss config dialog.
190
191 Methods:
192 destroy: inherited
193 """
194 self.destroy()
195
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300196 def destroy(self):
197 global font_sample_text
198 font_sample_text = self.fontpage.font_sample.get('1.0', 'end')
Tal Einat10ea9402018-08-02 09:18:29 +0300199 self.grab_release()
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300200 super().destroy()
201
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400202 def help(self):
203 """Create textview for config dialog help.
204
Xtreakd9677f32019-06-03 09:51:15 +0530205 Attributes accessed:
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400206 note
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400207
208 Methods:
209 view_text: Method from textview module.
210 """
Cheryl Sabella3866d9b2017-09-10 22:41:10 -0400211 page = self.note.tab(self.note.select(), option='text').strip()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400212 view_text(self, title='Help for IDLE preferences',
213 text=help_common+help_pages.get(page, ''))
214
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400215 def deactivate_current_config(self):
216 """Remove current key bindings.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400217 Iterate over window instances defined in parent and remove
218 the keybindings.
219 """
220 # Before a config is saved, some cleanup of current
221 # config must be done - remove the previous keybindings.
222 win_instances = self.parent.instance_dict.keys()
223 for instance in win_instances:
224 instance.RemoveKeybindings()
225
226 def activate_config_changes(self):
227 """Apply configuration changes to current windows.
228
229 Dynamically update the current parent window instances
230 with some of the configuration changes.
231 """
232 win_instances = self.parent.instance_dict.keys()
233 for instance in win_instances:
234 instance.ResetColorizer()
235 instance.ResetFont()
236 instance.set_notabs_indentwidth()
237 instance.ApplyKeybindings()
238 instance.reset_help_menu_entries()
Terry Jan Reedy5777ecc2017-09-16 01:42:28 -0400239 for klass in reloadables:
240 klass.reload()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400241
csabellabac7d332017-06-26 17:46:26 -0400242 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400243 """Part of the config dialog used for configuring IDLE extensions.
244
245 This code is generic - it works for any and all IDLE extensions.
246
247 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400248 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400249 GUI interface to change the configuration values, and saves the
250 changes using idleConf.
251
252 Not all changes take effect immediately - some may require restarting IDLE.
253 This depends on each extension's implementation.
254
255 All values are treated as text, and it is up to the user to supply
256 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300257 which are boolean, and can be toggled with a True/False button.
csabella36329a42017-07-13 23:32:01 -0400258
259 Methods:
Ville Skyttä49b27342017-08-03 09:00:59 +0300260 load_extensions:
csabella36329a42017-07-13 23:32:01 -0400261 extension_selected: Handle selection from list.
262 create_extension_frame: Hold widgets for one extension.
263 set_extension_value: Set in userCfg['extensions'].
264 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400265 """
266 parent = self.parent
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400267 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400268 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
269 self.ext_userCfg = idleConf.userCfg['extensions']
270 self.is_int = self.register(is_int)
271 self.load_extensions()
csabella7eb58832017-07-04 21:30:58 -0400272 # Create widgets - a listbox shows all available extensions, with the
273 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400274 self.extension_names = StringVar(self)
275 frame.rowconfigure(0, weight=1)
276 frame.columnconfigure(2, weight=1)
277 self.extension_list = Listbox(frame, listvariable=self.extension_names,
278 selectmode='browse')
279 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
280 scroll = Scrollbar(frame, command=self.extension_list.yview)
281 self.extension_list.yscrollcommand=scroll.set
282 self.details_frame = LabelFrame(frame, width=250, height=250)
283 self.extension_list.grid(column=0, row=0, sticky='nws')
284 scroll.grid(column=1, row=0, sticky='ns')
285 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
Cheryl Sabella7028e592017-08-26 14:26:02 -0400286 frame.configure(padding=10)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400287 self.config_frame = {}
288 self.current_extension = None
289
290 self.outerframe = self # TEMPORARY
291 self.tabbed_page_set = self.extension_list # TEMPORARY
292
csabella7eb58832017-07-04 21:30:58 -0400293 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400294 ext_names = ''
295 for ext_name in sorted(self.extensions):
296 self.create_extension_frame(ext_name)
297 ext_names = ext_names + '{' + ext_name + '} '
298 self.extension_names.set(ext_names)
299 self.extension_list.selection_set(0)
300 self.extension_selected(None)
301
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400302 return frame
303
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400304 def load_extensions(self):
305 "Fill self.extensions with data from the default and user configs."
306 self.extensions = {}
307 for ext_name in idleConf.GetExtensions(active_only=False):
wohlganger58fc71c2017-09-10 16:19:47 -0500308 # Former built-in extensions are already filtered out.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400309 self.extensions[ext_name] = []
310
311 for ext_name in self.extensions:
312 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
313
csabella7eb58832017-07-04 21:30:58 -0400314 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400315 enables = [opt_name for opt_name in opt_list
316 if opt_name.startswith('enable')]
317 for opt_name in enables:
318 opt_list.remove(opt_name)
319 opt_list = enables + opt_list
320
321 for opt_name in opt_list:
322 def_str = self.ext_defaultCfg.Get(
323 ext_name, opt_name, raw=True)
324 try:
325 def_obj = {'True':True, 'False':False}[def_str]
326 opt_type = 'bool'
327 except KeyError:
328 try:
329 def_obj = int(def_str)
330 opt_type = 'int'
331 except ValueError:
332 def_obj = def_str
333 opt_type = None
334 try:
335 value = self.ext_userCfg.Get(
336 ext_name, opt_name, type=opt_type, raw=True,
337 default=def_obj)
csabella7eb58832017-07-04 21:30:58 -0400338 except ValueError: # Need this until .Get fixed.
339 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400340 var = StringVar(self)
341 var.set(str(value))
342
343 self.extensions[ext_name].append({'name': opt_name,
344 'type': opt_type,
345 'default': def_str,
346 'value': value,
347 'var': var,
348 })
349
350 def extension_selected(self, event):
csabella7eb58832017-07-04 21:30:58 -0400351 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400352 newsel = self.extension_list.curselection()
353 if newsel:
354 newsel = self.extension_list.get(newsel)
355 if newsel is None or newsel != self.current_extension:
356 if self.current_extension:
357 self.details_frame.config(text='')
358 self.config_frame[self.current_extension].grid_forget()
359 self.current_extension = None
360 if newsel:
361 self.details_frame.config(text=newsel)
362 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
363 self.current_extension = newsel
364
365 def create_extension_frame(self, ext_name):
366 """Create a frame holding the widgets to configure one extension"""
367 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
368 self.config_frame[ext_name] = f
369 entry_area = f.interior
csabella7eb58832017-07-04 21:30:58 -0400370 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400371 for row, opt in enumerate(self.extensions[ext_name]):
csabella7eb58832017-07-04 21:30:58 -0400372 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400373 label = Label(entry_area, text=opt['name'])
374 label.grid(row=row, column=0, sticky=NW)
375 var = opt['var']
376 if opt['type'] == 'bool':
Cheryl Sabella7028e592017-08-26 14:26:02 -0400377 Checkbutton(entry_area, variable=var,
378 onvalue='True', offvalue='False', width=8
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400379 ).grid(row=row, column=1, sticky=W, padx=7)
380 elif opt['type'] == 'int':
381 Entry(entry_area, textvariable=var, validate='key',
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400382 validatecommand=(self.is_int, '%P'), width=10
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400383 ).grid(row=row, column=1, sticky=NSEW, padx=7)
384
Terry Jan Reedy800415e2018-06-11 14:14:32 -0400385 else: # type == 'str'
386 # Limit size to fit non-expanding space with larger font.
387 Entry(entry_area, textvariable=var, width=15
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400388 ).grid(row=row, column=1, sticky=NSEW, padx=7)
389 return
390
391 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -0400392 """Return True if the configuration was added or changed.
393
394 If the value is the same as the default, then remove it
395 from user config file.
396 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400397 name = opt['name']
398 default = opt['default']
399 value = opt['var'].get().strip() or default
400 opt['var'].set(value)
401 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -0400402 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400403 if (value == default):
404 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -0400405 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400406 return self.ext_userCfg.SetOption(section, name, value)
407
408 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -0400409 """Save configuration changes to the user config file.
410
411 Attributes accessed:
412 extensions
413
414 Methods:
415 set_extension_value
416 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400417 has_changes = False
418 for ext_name in self.extensions:
419 options = self.extensions[ext_name]
420 for opt in options:
421 if self.set_extension_value(ext_name, opt):
422 has_changes = True
423 if has_changes:
424 self.ext_userCfg.Save()
425
426
csabella6f446be2017-08-01 00:24:07 -0400427# class TabPage(Frame): # A template for Page classes.
428# def __init__(self, master):
429# super().__init__(master)
430# self.create_page_tab()
431# self.load_tab_cfg()
432# def create_page_tab(self):
433# # Define tk vars and register var and callback with tracers.
434# # Create subframes and widgets.
435# # Pack widgets.
436# def load_tab_cfg(self):
437# # Initialize widgets with data from idleConf.
438# def var_changed_var_name():
439# # For each tk var that needs other than default callback.
440# def other_methods():
441# # Define tab-specific behavior.
442
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300443font_sample_text = (
444 '<ASCII/Latin1>\n'
445 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n'
446 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e'
447 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n'
448 '\n<IPA,Greek,Cyrillic>\n'
449 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277'
450 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n'
451 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5'
452 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n'
453 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444'
454 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n'
455 '\n<Hebrew, Arabic>\n'
456 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9'
457 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n'
458 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a'
459 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n'
460 '\n<Devanagari, Tamil>\n'
461 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f'
462 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n'
463 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef'
464 '\u0b85\u0b87\u0b89\u0b8e\n'
465 '\n<East Asian>\n'
466 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n'
467 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n'
468 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n'
469 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n'
470 )
471
csabella6f446be2017-08-01 00:24:07 -0400472
csabella9397e2a2017-07-30 13:34:25 -0400473class FontPage(Frame):
474
csabella6f446be2017-08-01 00:24:07 -0400475 def __init__(self, master, highpage):
476 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -0400477 self.highlight_sample = highpage.highlight_sample
478 self.create_page_font_tab()
479 self.load_font_cfg()
480 self.load_tab_cfg()
481
482 def create_page_font_tab(self):
483 """Return frame of widgets for Font/Tabs tab.
484
485 Fonts: Enable users to provisionally change font face, size, or
486 boldness and to see the consequence of proposed choices. Each
487 action set 3 options in changes structuree and changes the
488 corresponding aspect of the font sample on this page and
489 highlight sample on highlight page.
490
491 Function load_font_cfg initializes font vars and widgets from
492 idleConf entries and tk.
493
494 Fontlist: mouse button 1 click or up or down key invoke
495 on_fontlist_select(), which sets var font_name.
496
497 Sizelist: clicking the menubutton opens the dropdown menu. A
498 mouse button 1 click or return key sets var font_size.
499
500 Bold_toggle: clicking the box toggles var font_bold.
501
502 Changing any of the font vars invokes var_changed_font, which
503 adds all 3 font options to changes and calls set_samples.
504 Set_samples applies a new font constructed from the font vars to
Leo Ariasc3d95082018-02-03 18:36:10 -0600505 font_sample and to highlight_sample on the highlight page.
csabella9397e2a2017-07-30 13:34:25 -0400506
507 Tabs: Enable users to change spaces entered for indent tabs.
508 Changing indent_scale value with the mouse sets Var space_num,
509 which invokes the default callback to add an entry to
510 changes. Load_tab_cfg initializes space_num to default.
511
Cheryl Sabella2f896462017-08-14 21:21:43 -0400512 Widgets for FontPage(Frame): (*) widgets bound to self
513 frame_font: LabelFrame
514 frame_font_name: Frame
515 font_name_title: Label
516 (*)fontlist: ListBox - font_name
517 scroll_font: Scrollbar
518 frame_font_param: Frame
519 font_size_title: Label
520 (*)sizelist: DynOptionMenu - font_size
521 (*)bold_toggle: Checkbutton - font_bold
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400522 frame_sample: LabelFrame
523 (*)font_sample: Label
Cheryl Sabella2f896462017-08-14 21:21:43 -0400524 frame_indent: LabelFrame
525 indent_title: Label
526 (*)indent_scale: Scale - space_num
csabella9397e2a2017-07-30 13:34:25 -0400527 """
csabella6f446be2017-08-01 00:24:07 -0400528 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
529 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
530 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
csabella9397e2a2017-07-30 13:34:25 -0400531 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
532
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400533 # Define frames and widgets.
csabella9397e2a2017-07-30 13:34:25 -0400534 frame_font = LabelFrame(
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400535 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ')
536 frame_sample = LabelFrame(
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300537 self, borderwidth=2, relief=GROOVE,
538 text=' Font Sample (Editable) ')
csabella9397e2a2017-07-30 13:34:25 -0400539 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -0400540 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -0400541 # frame_font.
542 frame_font_name = Frame(frame_font)
543 frame_font_param = Frame(frame_font)
544 font_name_title = Label(
545 frame_font_name, justify=LEFT, text='Font Face :')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400546 self.fontlist = Listbox(frame_font_name, height=15,
csabella9397e2a2017-07-30 13:34:25 -0400547 takefocus=True, exportselection=FALSE)
548 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
549 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
550 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
551 scroll_font = Scrollbar(frame_font_name)
552 scroll_font.config(command=self.fontlist.yview)
553 self.fontlist.config(yscrollcommand=scroll_font.set)
554 font_size_title = Label(frame_font_param, text='Size :')
555 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
556 self.bold_toggle = Checkbutton(
557 frame_font_param, variable=self.font_bold,
558 onvalue=1, offvalue=0, text='Bold')
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400559 # frame_sample.
Tal Einat3221a632019-07-27 19:57:48 +0300560 font_sample_frame = ScrollableTextFrame(frame_sample)
561 self.font_sample = font_sample_frame.text
562 self.font_sample.config(wrap=NONE, width=1, height=1)
Serhiy Storchakaed6554c2017-10-28 03:22:44 +0300563 self.font_sample.insert(END, font_sample_text)
csabella9397e2a2017-07-30 13:34:25 -0400564 # frame_indent.
565 indent_title = Label(
566 frame_indent, justify=LEFT,
567 text='Python Standard: 4 Spaces!')
568 self.indent_scale = Scale(
569 frame_indent, variable=self.space_num,
570 orient='horizontal', tickinterval=2, from_=2, to=16)
571
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400572 # Grid and pack widgets:
573 self.columnconfigure(1, weight=1)
Tal Einat3221a632019-07-27 19:57:48 +0300574 self.rowconfigure(2, weight=1)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400575 frame_font.grid(row=0, column=0, padx=5, pady=5)
Tal Einat3221a632019-07-27 19:57:48 +0300576 frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5,
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400577 sticky='nsew')
578 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
csabella9397e2a2017-07-30 13:34:25 -0400579 # frame_font.
580 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
581 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
582 font_name_title.pack(side=TOP, anchor=W)
583 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
584 scroll_font.pack(side=LEFT, fill=Y)
585 font_size_title.pack(side=LEFT, anchor=W)
586 self.sizelist.pack(side=LEFT, anchor=W)
587 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
Terry Jan Reedye2e42272017-10-17 18:56:16 -0400588 # frame_sample.
Tal Einat3221a632019-07-27 19:57:48 +0300589 font_sample_frame.pack(expand=TRUE, fill=BOTH)
csabella9397e2a2017-07-30 13:34:25 -0400590 # frame_indent.
csabella9397e2a2017-07-30 13:34:25 -0400591 indent_title.pack(side=TOP, anchor=W, padx=5)
592 self.indent_scale.pack(side=TOP, padx=5, fill=X)
593
csabella9397e2a2017-07-30 13:34:25 -0400594 def load_font_cfg(self):
595 """Load current configuration settings for the font options.
596
597 Retrieve current font with idleConf.GetFont and font families
598 from tk. Setup fontlist and set font_name. Setup sizelist,
599 which sets font_size. Set font_bold. Call set_samples.
600 """
601 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
602 font_name = configured_font[0].lower()
603 font_size = configured_font[1]
604 font_bold = configured_font[2]=='bold'
605
606 # Set editor font selection list and font_name.
607 fonts = list(tkFont.families(self))
608 fonts.sort()
609 for font in fonts:
610 self.fontlist.insert(END, font)
611 self.font_name.set(font_name)
612 lc_fonts = [s.lower() for s in fonts]
613 try:
614 current_font_index = lc_fonts.index(font_name)
615 self.fontlist.see(current_font_index)
616 self.fontlist.select_set(current_font_index)
617 self.fontlist.select_anchor(current_font_index)
618 self.fontlist.activate(current_font_index)
619 except ValueError:
620 pass
621 # Set font size dropdown.
622 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
623 '16', '18', '20', '22', '25', '29', '34', '40'),
624 font_size)
625 # Set font weight.
626 self.font_bold.set(font_bold)
627 self.set_samples()
628
629 def var_changed_font(self, *params):
630 """Store changes to font attributes.
631
632 When one font attribute changes, save them all, as they are
633 not independent from each other. In particular, when we are
634 overriding the default font, we need to write out everything.
635 """
636 value = self.font_name.get()
637 changes.add_option('main', 'EditorWindow', 'font', value)
638 value = self.font_size.get()
639 changes.add_option('main', 'EditorWindow', 'font-size', value)
640 value = self.font_bold.get()
641 changes.add_option('main', 'EditorWindow', 'font-bold', value)
642 self.set_samples()
643
644 def on_fontlist_select(self, event):
645 """Handle selecting a font from the list.
646
647 Event can result from either mouse click or Up or Down key.
648 Set font_name and example displays to selection.
649 """
650 font = self.fontlist.get(
651 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
652 self.font_name.set(font.lower())
653
654 def set_samples(self, event=None):
655 """Update update both screen samples with the font settings.
656
657 Called on font initialization and change events.
658 Accesses font_name, font_size, and font_bold Variables.
Leo Ariasc3d95082018-02-03 18:36:10 -0600659 Updates font_sample and highlight page highlight_sample.
csabella9397e2a2017-07-30 13:34:25 -0400660 """
661 font_name = self.font_name.get()
662 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
663 new_font = (font_name, self.font_size.get(), font_weight)
664 self.font_sample['font'] = new_font
665 self.highlight_sample['font'] = new_font
666
667 def load_tab_cfg(self):
668 """Load current configuration settings for the tab options.
669
670 Attributes updated:
671 space_num: Set to value from idleConf.
672 """
673 # Set indent sizes.
674 space_num = idleConf.GetOption(
675 'main', 'Indent', 'num-spaces', default=4, type='int')
676 self.space_num.set(space_num)
677
678 def var_changed_space_num(self, *params):
679 "Store change to indentation size."
680 value = self.space_num.get()
681 changes.add_option('main', 'Indent', 'num-spaces', value)
682
683
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400684class HighPage(Frame):
685
686 def __init__(self, master):
687 super().__init__(master)
688 self.cd = master.master
Cheryl Sabella7028e592017-08-26 14:26:02 -0400689 self.style = Style(master)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400690 self.create_page_highlight()
691 self.load_theme_cfg()
692
693 def create_page_highlight(self):
694 """Return frame of widgets for Highlighting tab.
695
696 Enable users to provisionally change foreground and background
697 colors applied to textual tags. Color mappings are stored in
698 complete listings called themes. Built-in themes in
699 idlelib/config-highlight.def are fixed as far as the dialog is
700 concerned. Any theme can be used as the base for a new custom
701 theme, stored in .idlerc/config-highlight.cfg.
702
703 Function load_theme_cfg() initializes tk variables and theme
704 lists and calls paint_theme_sample() and set_highlight_target()
705 for the current theme. Radiobuttons builtin_theme_on and
706 custom_theme_on toggle var theme_source, which controls if the
707 current set of colors are from a builtin or custom theme.
708 DynOptionMenus builtinlist and customlist contain lists of the
709 builtin and custom themes, respectively, and the current item
710 from each list is stored in vars builtin_name and custom_name.
711
712 Function paint_theme_sample() applies the colors from the theme
713 to the tags in text widget highlight_sample and then invokes
714 set_color_sample(). Function set_highlight_target() sets the state
715 of the radiobuttons fg_on and bg_on based on the tag and it also
716 invokes set_color_sample().
717
718 Function set_color_sample() sets the background color for the frame
719 holding the color selector. This provides a larger visual of the
720 color for the current tag and plane (foreground/background).
721
722 Note: set_color_sample() is called from many places and is often
723 called more than once when a change is made. It is invoked when
724 foreground or background is selected (radiobuttons), from
725 paint_theme_sample() (theme is changed or load_cfg is called), and
726 from set_highlight_target() (target tag is changed or load_cfg called).
727
728 Button delete_custom invokes delete_custom() to delete
729 a custom theme from idleConf.userCfg['highlight'] and changes.
730 Button save_custom invokes save_as_new_theme() which calls
731 get_new_theme_name() and create_new() to save a custom theme
732 and its colors to idleConf.userCfg['highlight'].
733
734 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
735 if the current selected color for a tag is for the foreground or
736 background.
737
738 DynOptionMenu targetlist contains a readable description of the
739 tags applied to Python source within IDLE. Selecting one of the
740 tags from this list populates highlight_target, which has a callback
741 function set_highlight_target().
742
743 Text widget highlight_sample displays a block of text (which is
744 mock Python code) in which is embedded the defined tags and reflects
745 the color attributes of the current theme and changes for those tags.
746 Mouse button 1 allows for selection of a tag and updates
747 highlight_target with that tag value.
748
749 Note: The font in highlight_sample is set through the config in
750 the fonts tab.
751
752 In other words, a tag can be selected either from targetlist or
753 by clicking on the sample text within highlight_sample. The
754 plane (foreground/background) is selected via the radiobutton.
755 Together, these two (tag and plane) control what color is
756 shown in set_color_sample() for the current theme. Button set_color
757 invokes get_color() which displays a ColorChooser to change the
758 color for the selected tag/plane. If a new color is picked,
759 it will be saved to changes and the highlight_sample and
760 frame background will be updated.
761
762 Tk Variables:
763 color: Color of selected target.
764 builtin_name: Menu variable for built-in theme.
765 custom_name: Menu variable for custom theme.
766 fg_bg_toggle: Toggle for foreground/background color.
767 Note: this has no callback.
768 theme_source: Selector for built-in or custom theme.
769 highlight_target: Menu variable for the highlight tag target.
770
771 Instance Data Attributes:
772 theme_elements: Dictionary of tags for text highlighting.
773 The key is the display name and the value is a tuple of
774 (tag name, display sort order).
775
776 Methods [attachment]:
777 load_theme_cfg: Load current highlight colors.
778 get_color: Invoke colorchooser [button_set_color].
779 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
780 set_highlight_target: set fg_bg_toggle, set_color_sample().
781 set_color_sample: Set frame background to target.
782 on_new_color_set: Set new color and add option.
783 paint_theme_sample: Recolor sample.
784 get_new_theme_name: Get from popup.
785 create_new: Combine theme with changes and save.
786 save_as_new_theme: Save [button_save_custom].
787 set_theme_type: Command for [theme_source].
788 delete_custom: Activate default [button_delete_custom].
789 save_new: Save to userCfg['theme'] (is function).
790
791 Widgets of highlights page frame: (*) widgets bound to self
792 frame_custom: LabelFrame
793 (*)highlight_sample: Text
794 (*)frame_color_set: Frame
795 (*)button_set_color: Button
796 (*)targetlist: DynOptionMenu - highlight_target
797 frame_fg_bg_toggle: Frame
798 (*)fg_on: Radiobutton - fg_bg_toggle
799 (*)bg_on: Radiobutton - fg_bg_toggle
800 (*)button_save_custom: Button
801 frame_theme: LabelFrame
802 theme_type_title: Label
803 (*)builtin_theme_on: Radiobutton - theme_source
804 (*)custom_theme_on: Radiobutton - theme_source
805 (*)builtinlist: DynOptionMenu - builtin_name
806 (*)customlist: DynOptionMenu - custom_name
807 (*)button_delete_custom: Button
808 (*)theme_message: Label
809 """
810 self.theme_elements = {
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400811 'Normal Code or Text': ('normal', '00'),
Cheryl Sabellade651622018-06-01 21:45:54 -0400812 'Code Context': ('context', '01'),
813 'Python Keywords': ('keyword', '02'),
814 'Python Definitions': ('definition', '03'),
815 'Python Builtins': ('builtin', '04'),
816 'Python Comments': ('comment', '05'),
817 'Python Strings': ('string', '06'),
818 'Selected Text': ('hilite', '07'),
819 'Found Text': ('hit', '08'),
820 'Cursor': ('cursor', '09'),
821 'Editor Breakpoint': ('break', '10'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400822 'Shell Prompt': ('console', '11'),
823 'Error Text': ('error', '12'),
824 'Shell User Output': ('stdout', '13'),
825 'Shell User Exception': ('stderr', '14'),
Tal Einat7123ea02019-07-23 15:22:11 +0300826 'Line Number': ('linenumber', '16'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400827 }
828 self.builtin_name = tracers.add(
829 StringVar(self), self.var_changed_builtin_name)
830 self.custom_name = tracers.add(
831 StringVar(self), self.var_changed_custom_name)
832 self.fg_bg_toggle = BooleanVar(self)
833 self.color = tracers.add(
834 StringVar(self), self.var_changed_color)
835 self.theme_source = tracers.add(
836 BooleanVar(self), self.var_changed_theme_source)
837 self.highlight_target = tracers.add(
838 StringVar(self), self.var_changed_highlight_target)
839
840 # Create widgets:
841 # body frame and section frames.
842 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
843 text=' Custom Highlighting ')
844 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
845 text=' Highlighting Theme ')
846 # frame_custom.
Tal Einat3221a632019-07-27 19:57:48 +0300847 sample_frame = ScrollableTextFrame(
848 frame_custom, relief=SOLID, borderwidth=1)
849 text = self.highlight_sample = sample_frame.text
850 text.configure(
851 font=('courier', 12, ''), cursor='hand2', width=1, height=1,
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400852 takefocus=FALSE, highlightthickness=0, wrap=NONE)
853 text.bind('<Double-Button-1>', lambda e: 'break')
854 text.bind('<B1-Motion>', lambda e: 'break')
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400855 string_tags=(
856 ('# Click selects item.', 'comment'), ('\n', 'normal'),
857 ('code context section', 'context'), ('\n', 'normal'),
858 ('| cursor', 'cursor'), ('\n', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400859 ('def', 'keyword'), (' ', 'normal'),
860 ('func', 'definition'), ('(param):\n ', 'normal'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400861 ('"Return None."', 'string'), ('\n var0 = ', 'normal'),
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400862 ("'string'", 'string'), ('\n var1 = ', 'normal'),
863 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
864 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
865 ('list', 'builtin'), ('(', 'normal'),
866 ('None', 'keyword'), (')\n', 'normal'),
867 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
Terry Jan Reedyb2229552019-07-28 12:04:31 -0400868 ('>>>', 'console'), (' 3.14**2\n', 'normal'),
869 ('9.8596', 'stdout'), ('\n', 'normal'),
870 ('>>>', 'console'), (' pri ', 'normal'),
871 ('n', 'error'), ('t(\n', 'normal'),
872 ('SyntaxError', 'stderr'), ('\n', 'normal'))
873 for string, tag in string_tags:
874 text.insert(END, string, tag)
Tal Einat7123ea02019-07-23 15:22:11 +0300875 n_lines = len(text.get('1.0', END).splitlines())
Tal Einat3221a632019-07-27 19:57:48 +0300876 for lineno in range(1, n_lines):
Tal Einat7123ea02019-07-23 15:22:11 +0300877 text.insert(f'{lineno}.0',
878 f'{lineno:{len(str(n_lines))}d} ',
879 'linenumber')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400880 for element in self.theme_elements:
881 def tem(event, elem=element):
Cheryl Sabella8f7a7982017-08-19 22:04:40 -0400882 # event.widget.winfo_top_level().highlight_target.set(elem)
883 self.highlight_target.set(elem)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400884 text.tag_bind(
885 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400886 text['state'] = 'disabled'
887 self.style.configure('frame_color_set.TFrame', borderwidth=1,
888 relief='solid')
889 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400890 frame_fg_bg_toggle = Frame(frame_custom)
891 self.button_set_color = Button(
892 self.frame_color_set, text='Choose Color for :',
Cheryl Sabella7028e592017-08-26 14:26:02 -0400893 command=self.get_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400894 self.targetlist = DynOptionMenu(
895 self.frame_color_set, self.highlight_target, None,
896 highlightthickness=0) #, command=self.set_highlight_targetBinding
897 self.fg_on = Radiobutton(
898 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
899 text='Foreground', command=self.set_color_sample_binding)
900 self.bg_on = Radiobutton(
901 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
902 text='Background', command=self.set_color_sample_binding)
903 self.fg_bg_toggle.set(1)
904 self.button_save_custom = Button(
905 frame_custom, text='Save as New Custom Theme',
906 command=self.save_as_new_theme)
907 # frame_theme.
908 theme_type_title = Label(frame_theme, text='Select : ')
909 self.builtin_theme_on = Radiobutton(
910 frame_theme, variable=self.theme_source, value=1,
911 command=self.set_theme_type, text='a Built-in Theme')
912 self.custom_theme_on = Radiobutton(
913 frame_theme, variable=self.theme_source, value=0,
914 command=self.set_theme_type, text='a Custom Theme')
915 self.builtinlist = DynOptionMenu(
916 frame_theme, self.builtin_name, None, command=None)
917 self.customlist = DynOptionMenu(
918 frame_theme, self.custom_name, None, command=None)
919 self.button_delete_custom = Button(
920 frame_theme, text='Delete Custom Theme',
921 command=self.delete_custom)
Cheryl Sabella7028e592017-08-26 14:26:02 -0400922 self.theme_message = Label(frame_theme, borderwidth=2)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400923 # Pack widgets:
924 # body.
925 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
wohlganger58fc71c2017-09-10 16:19:47 -0500926 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400927 # frame_custom.
Tal Einat3221a632019-07-27 19:57:48 +0300928 self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400929 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
Tal Einat3221a632019-07-27 19:57:48 +0300930 sample_frame.pack(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400931 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
932 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
933 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
934 self.fg_on.pack(side=LEFT, anchor=E)
935 self.bg_on.pack(side=RIGHT, anchor=W)
936 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
937 # frame_theme.
938 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
939 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
940 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
941 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
942 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
943 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
944 self.theme_message.pack(side=TOP, fill=X, pady=5)
945
946 def load_theme_cfg(self):
947 """Load current configuration settings for the theme options.
948
949 Based on the theme_source toggle, the theme is set as
950 either builtin or custom and the initial widget values
951 reflect the current settings from idleConf.
952
953 Attributes updated:
954 theme_source: Set from idleConf.
955 builtinlist: List of default themes from idleConf.
956 customlist: List of custom themes from idleConf.
957 custom_theme_on: Disabled if there are no custom themes.
958 custom_theme: Message with additional information.
959 targetlist: Create menu from self.theme_elements.
960
961 Methods:
962 set_theme_type
963 paint_theme_sample
964 set_highlight_target
965 """
966 # Set current theme type radiobutton.
967 self.theme_source.set(idleConf.GetOption(
968 'main', 'Theme', 'default', type='bool', default=1))
969 # Set current theme.
970 current_option = idleConf.CurrentTheme()
971 # Load available theme option menus.
972 if self.theme_source.get(): # Default theme selected.
973 item_list = idleConf.GetSectionList('default', 'highlight')
974 item_list.sort()
975 self.builtinlist.SetMenu(item_list, current_option)
976 item_list = idleConf.GetSectionList('user', 'highlight')
977 item_list.sort()
978 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -0400979 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -0400980 self.custom_name.set('- no custom themes -')
981 else:
982 self.customlist.SetMenu(item_list, item_list[0])
983 else: # User theme selected.
984 item_list = idleConf.GetSectionList('user', 'highlight')
985 item_list.sort()
986 self.customlist.SetMenu(item_list, current_option)
987 item_list = idleConf.GetSectionList('default', 'highlight')
988 item_list.sort()
989 self.builtinlist.SetMenu(item_list, item_list[0])
990 self.set_theme_type()
991 # Load theme element option menu.
992 theme_names = list(self.theme_elements.keys())
993 theme_names.sort(key=lambda x: self.theme_elements[x][1])
994 self.targetlist.SetMenu(theme_names, theme_names[0])
995 self.paint_theme_sample()
996 self.set_highlight_target()
997
998 def var_changed_builtin_name(self, *params):
999 """Process new builtin theme selection.
1000
1001 Add the changed theme's name to the changed_items and recreate
1002 the sample with the values from the selected theme.
1003 """
1004 old_themes = ('IDLE Classic', 'IDLE New')
1005 value = self.builtin_name.get()
1006 if value not in old_themes:
1007 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
1008 changes.add_option('main', 'Theme', 'name', old_themes[0])
1009 changes.add_option('main', 'Theme', 'name2', value)
1010 self.theme_message['text'] = 'New theme, see Help'
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001011 else:
1012 changes.add_option('main', 'Theme', 'name', value)
1013 changes.add_option('main', 'Theme', 'name2', '')
1014 self.theme_message['text'] = ''
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001015 self.paint_theme_sample()
1016
1017 def var_changed_custom_name(self, *params):
1018 """Process new custom theme selection.
1019
1020 If a new custom theme is selected, add the name to the
1021 changed_items and apply the theme to the sample.
1022 """
1023 value = self.custom_name.get()
1024 if value != '- no custom themes -':
1025 changes.add_option('main', 'Theme', 'name', value)
1026 self.paint_theme_sample()
1027
1028 def var_changed_theme_source(self, *params):
1029 """Process toggle between builtin and custom theme.
1030
1031 Update the default toggle value and apply the newly
1032 selected theme type.
1033 """
1034 value = self.theme_source.get()
1035 changes.add_option('main', 'Theme', 'default', value)
1036 if value:
1037 self.var_changed_builtin_name()
1038 else:
1039 self.var_changed_custom_name()
1040
1041 def var_changed_color(self, *params):
1042 "Process change to color choice."
1043 self.on_new_color_set()
1044
1045 def var_changed_highlight_target(self, *params):
1046 "Process selection of new target tag for highlighting."
1047 self.set_highlight_target()
1048
1049 def set_theme_type(self):
1050 """Set available screen options based on builtin or custom theme.
1051
1052 Attributes accessed:
1053 theme_source
1054
1055 Attributes updated:
1056 builtinlist
1057 customlist
1058 button_delete_custom
1059 custom_theme_on
1060
1061 Called from:
1062 handler for builtin_theme_on and custom_theme_on
1063 delete_custom
1064 create_new
1065 load_theme_cfg
1066 """
1067 if self.theme_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001068 self.builtinlist['state'] = 'normal'
1069 self.customlist['state'] = 'disabled'
1070 self.button_delete_custom.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001071 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001072 self.builtinlist['state'] = 'disabled'
1073 self.custom_theme_on.state(('!disabled',))
1074 self.customlist['state'] = 'normal'
1075 self.button_delete_custom.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001076
1077 def get_color(self):
1078 """Handle button to select a new color for the target tag.
1079
1080 If a new color is selected while using a builtin theme, a
1081 name must be supplied to create a custom theme.
1082
1083 Attributes accessed:
1084 highlight_target
1085 frame_color_set
1086 theme_source
1087
1088 Attributes updated:
1089 color
1090
1091 Methods:
1092 get_new_theme_name
1093 create_new
1094 """
1095 target = self.highlight_target.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001096 prev_color = self.style.lookup(self.frame_color_set['style'],
1097 'background')
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001098 rgbTuplet, color_string = tkColorChooser.askcolor(
1099 parent=self, title='Pick new color for : '+target,
1100 initialcolor=prev_color)
1101 if color_string and (color_string != prev_color):
1102 # User didn't cancel and they chose a new color.
1103 if self.theme_source.get(): # Current theme is a built-in.
1104 message = ('Your changes will be saved as a new Custom Theme. '
1105 'Enter a name for your new Custom Theme below.')
1106 new_theme = self.get_new_theme_name(message)
1107 if not new_theme: # User cancelled custom theme creation.
1108 return
1109 else: # Create new custom theme based on previously active theme.
1110 self.create_new(new_theme)
1111 self.color.set(color_string)
1112 else: # Current theme is user defined.
1113 self.color.set(color_string)
1114
1115 def on_new_color_set(self):
1116 "Display sample of new color selection on the dialog."
1117 new_color = self.color.get()
Cheryl Sabella7028e592017-08-26 14:26:02 -04001118 self.style.configure('frame_color_set.TFrame', background=new_color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001119 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1120 sample_element = self.theme_elements[self.highlight_target.get()][0]
1121 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1122 theme = self.custom_name.get()
1123 theme_element = sample_element + '-' + plane
1124 changes.add_option('highlight', theme, theme_element, new_color)
1125
1126 def get_new_theme_name(self, message):
1127 "Return name of new theme from query popup."
1128 used_names = (idleConf.GetSectionList('user', 'highlight') +
1129 idleConf.GetSectionList('default', 'highlight'))
1130 new_theme = SectionName(
1131 self, 'New Custom Theme', message, used_names).result
1132 return new_theme
1133
1134 def save_as_new_theme(self):
1135 """Prompt for new theme name and create the theme.
1136
1137 Methods:
1138 get_new_theme_name
1139 create_new
1140 """
1141 new_theme_name = self.get_new_theme_name('New Theme Name:')
1142 if new_theme_name:
1143 self.create_new(new_theme_name)
1144
1145 def create_new(self, new_theme_name):
1146 """Create a new custom theme with the given name.
1147
1148 Create the new theme based on the previously active theme
1149 with the current changes applied. Once it is saved, then
1150 activate the new theme.
1151
1152 Attributes accessed:
1153 builtin_name
1154 custom_name
1155
1156 Attributes updated:
1157 customlist
1158 theme_source
1159
1160 Method:
1161 save_new
1162 set_theme_type
1163 """
1164 if self.theme_source.get():
1165 theme_type = 'default'
1166 theme_name = self.builtin_name.get()
1167 else:
1168 theme_type = 'user'
1169 theme_name = self.custom_name.get()
1170 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1171 # Apply any of the old theme's unsaved changes to the new theme.
1172 if theme_name in changes['highlight']:
1173 theme_changes = changes['highlight'][theme_name]
1174 for element in theme_changes:
1175 new_theme[element] = theme_changes[element]
1176 # Save the new theme.
1177 self.save_new(new_theme_name, new_theme)
1178 # Change GUI over to the new theme.
1179 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1180 custom_theme_list.sort()
1181 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1182 self.theme_source.set(0)
1183 self.set_theme_type()
1184
1185 def set_highlight_target(self):
1186 """Set fg/bg toggle and color based on highlight tag target.
1187
1188 Instance variables accessed:
1189 highlight_target
1190
1191 Attributes updated:
1192 fg_on
1193 bg_on
1194 fg_bg_toggle
1195
1196 Methods:
1197 set_color_sample
1198
1199 Called from:
1200 var_changed_highlight_target
1201 load_theme_cfg
1202 """
1203 if self.highlight_target.get() == 'Cursor': # bg not possible
Cheryl Sabella7028e592017-08-26 14:26:02 -04001204 self.fg_on.state(('disabled',))
1205 self.bg_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001206 self.fg_bg_toggle.set(1)
1207 else: # Both fg and bg can be set.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001208 self.fg_on.state(('!disabled',))
1209 self.bg_on.state(('!disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001210 self.fg_bg_toggle.set(1)
1211 self.set_color_sample()
1212
1213 def set_color_sample_binding(self, *args):
1214 """Change color sample based on foreground/background toggle.
1215
1216 Methods:
1217 set_color_sample
1218 """
1219 self.set_color_sample()
1220
1221 def set_color_sample(self):
1222 """Set the color of the frame background to reflect the selected target.
1223
1224 Instance variables accessed:
1225 theme_elements
1226 highlight_target
1227 fg_bg_toggle
1228 highlight_sample
1229
1230 Attributes updated:
1231 frame_color_set
1232 """
1233 # Set the color sample area.
1234 tag = self.theme_elements[self.highlight_target.get()][0]
1235 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1236 color = self.highlight_sample.tag_cget(tag, plane)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001237 self.style.configure('frame_color_set.TFrame', background=color)
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001238
1239 def paint_theme_sample(self):
1240 """Apply the theme colors to each element tag in the sample text.
1241
1242 Instance attributes accessed:
1243 theme_elements
1244 theme_source
1245 builtin_name
1246 custom_name
1247
1248 Attributes updated:
1249 highlight_sample: Set the tag elements to the theme.
1250
1251 Methods:
1252 set_color_sample
1253
1254 Called from:
1255 var_changed_builtin_name
1256 var_changed_custom_name
1257 load_theme_cfg
1258 """
1259 if self.theme_source.get(): # Default theme
1260 theme = self.builtin_name.get()
1261 else: # User theme
1262 theme = self.custom_name.get()
1263 for element_title in self.theme_elements:
1264 element = self.theme_elements[element_title][0]
1265 colors = idleConf.GetHighlight(theme, element)
1266 if element == 'cursor': # Cursor sample needs special painting.
1267 colors['background'] = idleConf.GetHighlight(
Terry Jan Reedyc1419572019-03-22 18:23:41 -04001268 theme, 'normal')['background']
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001269 # Handle any unsaved changes to this theme.
1270 if theme in changes['highlight']:
1271 theme_dict = changes['highlight'][theme]
1272 if element + '-foreground' in theme_dict:
1273 colors['foreground'] = theme_dict[element + '-foreground']
1274 if element + '-background' in theme_dict:
1275 colors['background'] = theme_dict[element + '-background']
1276 self.highlight_sample.tag_config(element, **colors)
1277 self.set_color_sample()
1278
1279 def save_new(self, theme_name, theme):
1280 """Save a newly created theme to idleConf.
1281
1282 theme_name - string, the name of the new theme
1283 theme - dictionary containing the new theme
1284 """
1285 if not idleConf.userCfg['highlight'].has_section(theme_name):
1286 idleConf.userCfg['highlight'].add_section(theme_name)
1287 for element in theme:
1288 value = theme[element]
1289 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1290
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001291 def askyesno(self, *args, **kwargs):
1292 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001293 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001294
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001295 def delete_custom(self):
1296 """Handle event to delete custom theme.
1297
1298 The current theme is deactivated and the default theme is
1299 activated. The custom theme is permanently removed from
1300 the config file.
1301
1302 Attributes accessed:
1303 custom_name
1304
1305 Attributes updated:
1306 custom_theme_on
1307 customlist
1308 theme_source
1309 builtin_name
1310
1311 Methods:
1312 deactivate_current_config
1313 save_all_changed_extensions
1314 activate_config_changes
1315 set_theme_type
1316 """
1317 theme_name = self.custom_name.get()
1318 delmsg = 'Are you sure you wish to delete the theme %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001319 if not self.askyesno(
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001320 'Delete Theme', delmsg % theme_name, parent=self):
1321 return
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001322 self.cd.deactivate_current_config()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001323 # Remove theme from changes, config, and file.
1324 changes.delete_section('highlight', theme_name)
1325 # Reload user theme list.
1326 item_list = idleConf.GetSectionList('user', 'highlight')
1327 item_list.sort()
1328 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001329 self.custom_theme_on.state(('disabled',))
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001330 self.customlist.SetMenu(item_list, '- no custom themes -')
1331 else:
1332 self.customlist.SetMenu(item_list, item_list[0])
1333 # Revert to default theme.
1334 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1335 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1336 # User can't back out of these changes, they must be applied now.
1337 changes.save_all()
Cheryl Sabella8f7a7982017-08-19 22:04:40 -04001338 self.cd.save_all_changed_extensions()
1339 self.cd.activate_config_changes()
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001340 self.set_theme_type()
1341
1342
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001343class KeysPage(Frame):
1344
1345 def __init__(self, master):
1346 super().__init__(master)
1347 self.cd = master.master
1348 self.create_page_keys()
1349 self.load_key_cfg()
1350
1351 def create_page_keys(self):
1352 """Return frame of widgets for Keys tab.
1353
1354 Enable users to provisionally change both individual and sets of
1355 keybindings (shortcut keys). Except for features implemented as
1356 extensions, keybindings are stored in complete sets called
1357 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1358 as far as the dialog is concerned. Any keyset can be used as the
1359 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1360
1361 Function load_key_cfg() initializes tk variables and keyset
1362 lists and calls load_keys_list for the current keyset.
1363 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1364 keyset_source, which controls if the current set of keybindings
1365 are from a builtin or custom keyset. DynOptionMenus builtinlist
1366 and customlist contain lists of the builtin and custom keysets,
1367 respectively, and the current item from each list is stored in
1368 vars builtin_name and custom_name.
1369
1370 Button delete_custom_keys invokes delete_custom_keys() to delete
1371 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1372 save_custom_keys invokes save_as_new_key_set() which calls
1373 get_new_keys_name() and create_new_key_set() to save a custom keyset
1374 and its keybindings to idleConf.userCfg['keys'].
1375
1376 Listbox bindingslist contains all of the keybindings for the
1377 selected keyset. The keybindings are loaded in load_keys_list()
1378 and are pairs of (event, [keys]) where keys can be a list
1379 of one or more key combinations to bind to the same event.
1380 Mouse button 1 click invokes on_bindingslist_select(), which
1381 allows button_new_keys to be clicked.
1382
1383 So, an item is selected in listbindings, which activates
1384 button_new_keys, and clicking button_new_keys calls function
1385 get_new_keys(). Function get_new_keys() gets the key mappings from the
1386 current keyset for the binding event item that was selected. The
1387 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001388 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001389 to be entered for that binding event. If the keys aren't
1390 changed, nothing happens. If the keys are changed and the keyset
1391 is a builtin, function get_new_keys_name() will be called
1392 for input of a custom keyset name. If no name is given, then the
1393 change to the keybinding will abort and no updates will be made. If
1394 a custom name is entered in the prompt or if the current keyset was
1395 already custom (and thus didn't require a prompt), then
1396 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1397 with the change to the event binding. The item listing in bindingslist
1398 is updated with the new keys. Var keybinding is also set which invokes
1399 the callback function, var_changed_keybinding, to add the change to
1400 the 'keys' or 'extensions' changes tracker based on the binding type.
1401
1402 Tk Variables:
1403 keybinding: Action/key bindings.
1404
1405 Methods:
1406 load_keys_list: Reload active set.
1407 create_new_key_set: Combine active keyset and changes.
1408 set_keys_type: Command for keyset_source.
1409 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1410 deactivate_current_config: Remove keys bindings in editors.
1411
1412 Widgets for KeysPage(frame): (*) widgets bound to self
1413 frame_key_sets: LabelFrame
1414 frames[0]: Frame
1415 (*)builtin_keyset_on: Radiobutton - var keyset_source
1416 (*)custom_keyset_on: Radiobutton - var keyset_source
1417 (*)builtinlist: DynOptionMenu - var builtin_name,
1418 func keybinding_selected
1419 (*)customlist: DynOptionMenu - var custom_name,
1420 func keybinding_selected
1421 (*)keys_message: Label
1422 frames[1]: Frame
1423 (*)button_delete_custom_keys: Button - delete_custom_keys
1424 (*)button_save_custom_keys: Button - save_as_new_key_set
1425 frame_custom: LabelFrame
1426 frame_target: Frame
1427 target_title: Label
1428 scroll_target_y: Scrollbar
1429 scroll_target_x: Scrollbar
1430 (*)bindingslist: ListBox - on_bindingslist_select
1431 (*)button_new_keys: Button - get_new_keys & ..._name
1432 """
1433 self.builtin_name = tracers.add(
1434 StringVar(self), self.var_changed_builtin_name)
1435 self.custom_name = tracers.add(
1436 StringVar(self), self.var_changed_custom_name)
1437 self.keyset_source = tracers.add(
1438 BooleanVar(self), self.var_changed_keyset_source)
1439 self.keybinding = tracers.add(
1440 StringVar(self), self.var_changed_keybinding)
1441
1442 # Create widgets:
1443 # body and section frames.
1444 frame_custom = LabelFrame(
1445 self, borderwidth=2, relief=GROOVE,
1446 text=' Custom Key Bindings ')
1447 frame_key_sets = LabelFrame(
1448 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1449 # frame_custom.
1450 frame_target = Frame(frame_custom)
1451 target_title = Label(frame_target, text='Action - Key(s)')
1452 scroll_target_y = Scrollbar(frame_target)
1453 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1454 self.bindingslist = Listbox(
1455 frame_target, takefocus=FALSE, exportselection=FALSE)
1456 self.bindingslist.bind('<ButtonRelease-1>',
1457 self.on_bindingslist_select)
1458 scroll_target_y['command'] = self.bindingslist.yview
1459 scroll_target_x['command'] = self.bindingslist.xview
1460 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1461 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1462 self.button_new_keys = Button(
1463 frame_custom, text='Get New Keys for Selection',
Terry Jan Reedye8f7c782017-11-28 21:52:32 -05001464 command=self.get_new_keys, state='disabled')
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001465 # frame_key_sets.
Cheryl Sabella7028e592017-08-26 14:26:02 -04001466 frames = [Frame(frame_key_sets, padding=2, borderwidth=0)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001467 for i in range(2)]
1468 self.builtin_keyset_on = Radiobutton(
1469 frames[0], variable=self.keyset_source, value=1,
1470 command=self.set_keys_type, text='Use a Built-in Key Set')
1471 self.custom_keyset_on = Radiobutton(
1472 frames[0], variable=self.keyset_source, value=0,
1473 command=self.set_keys_type, text='Use a Custom Key Set')
1474 self.builtinlist = DynOptionMenu(
1475 frames[0], self.builtin_name, None, command=None)
1476 self.customlist = DynOptionMenu(
1477 frames[0], self.custom_name, None, command=None)
1478 self.button_delete_custom_keys = Button(
1479 frames[1], text='Delete Custom Key Set',
1480 command=self.delete_custom_keys)
1481 self.button_save_custom_keys = Button(
1482 frames[1], text='Save as New Custom Key Set',
1483 command=self.save_as_new_key_set)
Cheryl Sabella7028e592017-08-26 14:26:02 -04001484 self.keys_message = Label(frames[0], borderwidth=2)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001485
1486 # Pack widgets:
1487 # body.
1488 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1489 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1490 # frame_custom.
1491 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1492 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1493 # frame_target.
1494 frame_target.columnconfigure(0, weight=1)
1495 frame_target.rowconfigure(1, weight=1)
1496 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1497 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1498 scroll_target_y.grid(row=1, column=1, sticky=NS)
1499 scroll_target_x.grid(row=2, column=0, sticky=EW)
1500 # frame_key_sets.
1501 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1502 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1503 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1504 self.customlist.grid(row=1, column=1, sticky=NSEW)
1505 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1506 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1507 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1508 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1509 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1510
1511 def load_key_cfg(self):
1512 "Load current configuration settings for the keybinding options."
1513 # Set current keys type radiobutton.
1514 self.keyset_source.set(idleConf.GetOption(
1515 'main', 'Keys', 'default', type='bool', default=1))
1516 # Set current keys.
1517 current_option = idleConf.CurrentKeys()
1518 # Load available keyset option menus.
1519 if self.keyset_source.get(): # Default theme selected.
1520 item_list = idleConf.GetSectionList('default', 'keys')
1521 item_list.sort()
1522 self.builtinlist.SetMenu(item_list, current_option)
1523 item_list = idleConf.GetSectionList('user', 'keys')
1524 item_list.sort()
1525 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001526 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001527 self.custom_name.set('- no custom keys -')
1528 else:
1529 self.customlist.SetMenu(item_list, item_list[0])
1530 else: # User key set selected.
1531 item_list = idleConf.GetSectionList('user', 'keys')
1532 item_list.sort()
1533 self.customlist.SetMenu(item_list, current_option)
1534 item_list = idleConf.GetSectionList('default', 'keys')
1535 item_list.sort()
1536 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1537 self.set_keys_type()
1538 # Load keyset element list.
1539 keyset_name = idleConf.CurrentKeys()
1540 self.load_keys_list(keyset_name)
1541
1542 def var_changed_builtin_name(self, *params):
1543 "Process selection of builtin key set."
1544 old_keys = (
1545 'IDLE Classic Windows',
1546 'IDLE Classic Unix',
1547 'IDLE Classic Mac',
1548 'IDLE Classic OSX',
1549 )
1550 value = self.builtin_name.get()
1551 if value not in old_keys:
1552 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1553 changes.add_option('main', 'Keys', 'name', old_keys[0])
1554 changes.add_option('main', 'Keys', 'name2', value)
1555 self.keys_message['text'] = 'New key set, see Help'
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001556 else:
1557 changes.add_option('main', 'Keys', 'name', value)
1558 changes.add_option('main', 'Keys', 'name2', '')
1559 self.keys_message['text'] = ''
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001560 self.load_keys_list(value)
1561
1562 def var_changed_custom_name(self, *params):
1563 "Process selection of custom key set."
1564 value = self.custom_name.get()
1565 if value != '- no custom keys -':
1566 changes.add_option('main', 'Keys', 'name', value)
1567 self.load_keys_list(value)
1568
1569 def var_changed_keyset_source(self, *params):
1570 "Process toggle between builtin key set and custom key set."
1571 value = self.keyset_source.get()
1572 changes.add_option('main', 'Keys', 'default', value)
1573 if value:
1574 self.var_changed_builtin_name()
1575 else:
1576 self.var_changed_custom_name()
1577
1578 def var_changed_keybinding(self, *params):
1579 "Store change to a keybinding."
1580 value = self.keybinding.get()
1581 key_set = self.custom_name.get()
1582 event = self.bindingslist.get(ANCHOR).split()[0]
1583 if idleConf.IsCoreBinding(event):
1584 changes.add_option('keys', key_set, event, value)
1585 else: # Event is an extension binding.
1586 ext_name = idleConf.GetExtnNameForEvent(event)
1587 ext_keybind_section = ext_name + '_cfgBindings'
1588 changes.add_option('extensions', ext_keybind_section, event, value)
1589
1590 def set_keys_type(self):
1591 "Set available screen options based on builtin or custom key set."
1592 if self.keyset_source.get():
Cheryl Sabella7028e592017-08-26 14:26:02 -04001593 self.builtinlist['state'] = 'normal'
1594 self.customlist['state'] = 'disabled'
1595 self.button_delete_custom_keys.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001596 else:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001597 self.builtinlist['state'] = 'disabled'
1598 self.custom_keyset_on.state(('!disabled',))
1599 self.customlist['state'] = 'normal'
1600 self.button_delete_custom_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001601
1602 def get_new_keys(self):
1603 """Handle event to change key binding for selected line.
1604
1605 A selection of a key/binding in the list of current
1606 bindings pops up a dialog to enter a new binding. If
1607 the current key set is builtin and a binding has
1608 changed, then a name for a custom key set needs to be
1609 entered for the change to be applied.
1610 """
1611 list_index = self.bindingslist.index(ANCHOR)
1612 binding = self.bindingslist.get(list_index)
1613 bind_name = binding.split()[0]
1614 if self.keyset_source.get():
1615 current_key_set_name = self.builtin_name.get()
1616 else:
1617 current_key_set_name = self.custom_name.get()
1618 current_bindings = idleConf.GetCurrentKeySet()
1619 if current_key_set_name in changes['keys']: # unsaved changes
1620 key_set_changes = changes['keys'][current_key_set_name]
1621 for event in key_set_changes:
1622 current_bindings[event] = key_set_changes[event].split()
1623 current_key_sequences = list(current_bindings.values())
1624 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1625 current_key_sequences).result
1626 if new_keys:
1627 if self.keyset_source.get(): # Current key set is a built-in.
1628 message = ('Your changes will be saved as a new Custom Key Set.'
1629 ' Enter a name for your new Custom Key Set below.')
1630 new_keyset = self.get_new_keys_name(message)
1631 if not new_keyset: # User cancelled custom key set creation.
1632 self.bindingslist.select_set(list_index)
1633 self.bindingslist.select_anchor(list_index)
1634 return
1635 else: # Create new custom key set based on previously active key set.
1636 self.create_new_key_set(new_keyset)
1637 self.bindingslist.delete(list_index)
1638 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1639 self.bindingslist.select_set(list_index)
1640 self.bindingslist.select_anchor(list_index)
1641 self.keybinding.set(new_keys)
1642 else:
1643 self.bindingslist.select_set(list_index)
1644 self.bindingslist.select_anchor(list_index)
1645
1646 def get_new_keys_name(self, message):
1647 "Return new key set name from query popup."
1648 used_names = (idleConf.GetSectionList('user', 'keys') +
1649 idleConf.GetSectionList('default', 'keys'))
1650 new_keyset = SectionName(
1651 self, 'New Custom Key Set', message, used_names).result
1652 return new_keyset
1653
1654 def save_as_new_key_set(self):
1655 "Prompt for name of new key set and save changes using that name."
1656 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1657 if new_keys_name:
1658 self.create_new_key_set(new_keys_name)
1659
1660 def on_bindingslist_select(self, event):
1661 "Activate button to assign new keys to selected action."
Cheryl Sabella7028e592017-08-26 14:26:02 -04001662 self.button_new_keys.state(('!disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001663
1664 def create_new_key_set(self, new_key_set_name):
1665 """Create a new custom key set with the given name.
1666
1667 Copy the bindings/keys from the previously active keyset
1668 to the new keyset and activate the new custom keyset.
1669 """
1670 if self.keyset_source.get():
1671 prev_key_set_name = self.builtin_name.get()
1672 else:
1673 prev_key_set_name = self.custom_name.get()
1674 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1675 new_keys = {}
1676 for event in prev_keys: # Add key set to changed items.
1677 event_name = event[2:-2] # Trim off the angle brackets.
1678 binding = ' '.join(prev_keys[event])
1679 new_keys[event_name] = binding
1680 # Handle any unsaved changes to prev key set.
1681 if prev_key_set_name in changes['keys']:
1682 key_set_changes = changes['keys'][prev_key_set_name]
1683 for event in key_set_changes:
1684 new_keys[event] = key_set_changes[event]
1685 # Save the new key set.
1686 self.save_new_key_set(new_key_set_name, new_keys)
1687 # Change GUI over to the new key set.
1688 custom_key_list = idleConf.GetSectionList('user', 'keys')
1689 custom_key_list.sort()
1690 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1691 self.keyset_source.set(0)
1692 self.set_keys_type()
1693
1694 def load_keys_list(self, keyset_name):
1695 """Reload the list of action/key binding pairs for the active key set.
1696
1697 An action/key binding can be selected to change the key binding.
1698 """
1699 reselect = False
1700 if self.bindingslist.curselection():
1701 reselect = True
1702 list_index = self.bindingslist.index(ANCHOR)
1703 keyset = idleConf.GetKeySet(keyset_name)
1704 bind_names = list(keyset.keys())
1705 bind_names.sort()
1706 self.bindingslist.delete(0, END)
1707 for bind_name in bind_names:
1708 key = ' '.join(keyset[bind_name])
1709 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1710 if keyset_name in changes['keys']:
1711 # Handle any unsaved changes to this key set.
1712 if bind_name in changes['keys'][keyset_name]:
1713 key = changes['keys'][keyset_name][bind_name]
1714 self.bindingslist.insert(END, bind_name+' - '+key)
1715 if reselect:
1716 self.bindingslist.see(list_index)
1717 self.bindingslist.select_set(list_index)
1718 self.bindingslist.select_anchor(list_index)
1719
1720 @staticmethod
1721 def save_new_key_set(keyset_name, keyset):
1722 """Save a newly created core key set.
1723
1724 Add keyset to idleConf.userCfg['keys'], not to disk.
1725 If the keyset doesn't exist, it is created. The
1726 binding/keys are taken from the keyset argument.
1727
1728 keyset_name - string, the name of the new key set
1729 keyset - dictionary containing the new keybindings
1730 """
1731 if not idleConf.userCfg['keys'].has_section(keyset_name):
1732 idleConf.userCfg['keys'].add_section(keyset_name)
1733 for event in keyset:
1734 value = keyset[event]
1735 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1736
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001737 def askyesno(self, *args, **kwargs):
1738 # Make testing easier. Could change implementation.
Terry Jan Reedy0efc7c62017-09-17 20:13:25 -04001739 return messagebox.askyesno(*args, **kwargs)
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001740
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001741 def delete_custom_keys(self):
1742 """Handle event to delete a custom key set.
1743
1744 Applying the delete deactivates the current configuration and
1745 reverts to the default. The custom key set is permanently
1746 deleted from the config file.
1747 """
1748 keyset_name = self.custom_name.get()
1749 delmsg = 'Are you sure you wish to delete the key set %r ?'
Terry Jan Reedy3457f422017-08-27 16:39:41 -04001750 if not self.askyesno(
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001751 'Delete Key Set', delmsg % keyset_name, parent=self):
1752 return
1753 self.cd.deactivate_current_config()
1754 # Remove key set from changes, config, and file.
1755 changes.delete_section('keys', keyset_name)
1756 # Reload user key set list.
1757 item_list = idleConf.GetSectionList('user', 'keys')
1758 item_list.sort()
1759 if not item_list:
Cheryl Sabella7028e592017-08-26 14:26:02 -04001760 self.custom_keyset_on.state(('disabled',))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001761 self.customlist.SetMenu(item_list, '- no custom keys -')
1762 else:
1763 self.customlist.SetMenu(item_list, item_list[0])
1764 # Revert to default key set.
1765 self.keyset_source.set(idleConf.defaultCfg['main']
Tal Einat604e7b92018-09-25 15:10:14 +03001766 .Get('Keys', 'default'))
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001767 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
Tal Einat604e7b92018-09-25 15:10:14 +03001768 or idleConf.default_keys())
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001769 # User can't back out of these changes, they must be applied now.
1770 changes.save_all()
1771 self.cd.save_all_changed_extensions()
1772 self.cd.activate_config_changes()
1773 self.set_keys_type()
1774
1775
csabellae8eb17b2017-07-30 18:39:17 -04001776class GenPage(Frame):
1777
csabella6f446be2017-08-01 00:24:07 -04001778 def __init__(self, master):
1779 super().__init__(master)
Tal Einat1ebee372019-07-23 13:02:11 +03001780
1781 self.init_validators()
csabellae8eb17b2017-07-30 18:39:17 -04001782 self.create_page_general()
1783 self.load_general_cfg()
1784
Tal Einat1ebee372019-07-23 13:02:11 +03001785 def init_validators(self):
1786 digits_or_empty_re = re.compile(r'[0-9]*')
1787 def is_digits_or_empty(s):
1788 "Return 's is blank or contains only digits'"
1789 return digits_or_empty_re.fullmatch(s) is not None
1790 self.digits_only = (self.register(is_digits_or_empty), '%P',)
1791
csabellae8eb17b2017-07-30 18:39:17 -04001792 def create_page_general(self):
1793 """Return frame of widgets for General tab.
1794
1795 Enable users to provisionally change general options. Function
Terry Jan Reedy0acb6462019-07-30 18:14:58 -04001796 load_general_cfg initializes tk variables and helplist using
csabellae8eb17b2017-07-30 18:39:17 -04001797 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1798 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1799 set var autosave. Entry boxes win_width_int and win_height_int
1800 set var win_width and win_height. Setting var_name invokes the
1801 default callback that adds option to changes.
1802
1803 Helplist: load_general_cfg loads list user_helplist with
1804 name, position pairs and copies names to listbox helplist.
1805 Clicking a name invokes help_source selected. Clicking
1806 button_helplist_name invokes helplist_item_name, which also
1807 changes user_helplist. These functions all call
1808 set_add_delete_state. All but load call update_help_changes to
1809 rewrite changes['main']['HelpFiles'].
1810
Cheryl Sabella2f896462017-08-14 21:21:43 -04001811 Widgets for GenPage(Frame): (*) widgets bound to self
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001812 frame_window: LabelFrame
1813 frame_run: Frame
1814 startup_title: Label
1815 (*)startup_editor_on: Radiobutton - startup_edit
1816 (*)startup_shell_on: Radiobutton - startup_edit
1817 frame_win_size: Frame
1818 win_size_title: Label
1819 win_width_title: Label
1820 (*)win_width_int: Entry - win_width
1821 win_height_title: Label
1822 (*)win_height_int: Entry - win_height
Cheryl Sabella845d8642018-02-04 18:15:21 -05001823 frame_autocomplete: Frame
1824 auto_wait_title: Label
1825 (*)auto_wait_int: Entry - autocomplete_wait
1826 frame_paren1: Frame
1827 paren_style_title: Label
1828 (*)paren_style_type: OptionMenu - paren_style
1829 frame_paren2: Frame
1830 paren_time_title: Label
1831 (*)paren_flash_time: Entry - flash_delay
1832 (*)bell_on: Checkbutton - paren_bell
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001833 frame_editor: LabelFrame
1834 frame_save: Frame
1835 run_save_title: Label
1836 (*)save_ask_on: Radiobutton - autosave
1837 (*)save_auto_on: Radiobutton - autosave
Cheryl Sabella845d8642018-02-04 18:15:21 -05001838 frame_format: Frame
1839 format_width_title: Label
1840 (*)format_width_int: Entry - format_width
Tal Einat7123ea02019-07-23 15:22:11 +03001841 frame_line_numbers_default: Frame
1842 line_numbers_default_title: Label
1843 (*)line_numbers_default_bool: Checkbutton - line_numbers_default
Cheryl Sabella845d8642018-02-04 18:15:21 -05001844 frame_context: Frame
1845 context_title: Label
1846 (*)context_int: Entry - context_lines
Tal Einat604e7b92018-09-25 15:10:14 +03001847 frame_shell: LabelFrame
1848 frame_auto_squeeze_min_lines: Frame
1849 auto_squeeze_min_lines_title: Label
1850 (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines
Cheryl Sabella2f896462017-08-14 21:21:43 -04001851 frame_help: LabelFrame
1852 frame_helplist: Frame
1853 frame_helplist_buttons: Frame
1854 (*)button_helplist_edit
1855 (*)button_helplist_add
1856 (*)button_helplist_remove
1857 (*)helplist: ListBox
1858 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04001859 """
wohlganger58fc71c2017-09-10 16:19:47 -05001860 # Integer values need StringVar because int('') raises.
csabellae8eb17b2017-07-30 18:39:17 -04001861 self.startup_edit = tracers.add(
1862 IntVar(self), ('main', 'General', 'editor-on-startup'))
csabellae8eb17b2017-07-30 18:39:17 -04001863 self.win_width = tracers.add(
1864 StringVar(self), ('main', 'EditorWindow', 'width'))
1865 self.win_height = tracers.add(
1866 StringVar(self), ('main', 'EditorWindow', 'height'))
wohlganger58fc71c2017-09-10 16:19:47 -05001867 self.autocomplete_wait = tracers.add(
1868 StringVar(self), ('extensions', 'AutoComplete', 'popupwait'))
1869 self.paren_style = tracers.add(
1870 StringVar(self), ('extensions', 'ParenMatch', 'style'))
1871 self.flash_delay = tracers.add(
1872 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay'))
1873 self.paren_bell = tracers.add(
1874 BooleanVar(self), ('extensions', 'ParenMatch', 'bell'))
csabellae8eb17b2017-07-30 18:39:17 -04001875
Tal Einat604e7b92018-09-25 15:10:14 +03001876 self.auto_squeeze_min_lines = tracers.add(
1877 StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines'))
1878
wohlganger58fc71c2017-09-10 16:19:47 -05001879 self.autosave = tracers.add(
1880 IntVar(self), ('main', 'General', 'autosave'))
1881 self.format_width = tracers.add(
1882 StringVar(self), ('extensions', 'FormatParagraph', 'max-width'))
Tal Einat7123ea02019-07-23 15:22:11 +03001883 self.line_numbers_default = tracers.add(
1884 BooleanVar(self),
1885 ('main', 'EditorWindow', 'line-numbers-default'))
wohlganger58fc71c2017-09-10 16:19:47 -05001886 self.context_lines = tracers.add(
Cheryl Sabella29996a12018-06-01 19:23:00 -04001887 StringVar(self), ('extensions', 'CodeContext', 'maxlines'))
wohlganger58fc71c2017-09-10 16:19:47 -05001888
1889 # Create widgets:
csabellae8eb17b2017-07-30 18:39:17 -04001890 # Section frames.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001891 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE,
1892 text=' Window Preferences')
1893 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
1894 text=' Editor Preferences')
Tal Einat604e7b92018-09-25 15:10:14 +03001895 frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
1896 text=' Shell Preferences')
csabellae8eb17b2017-07-30 18:39:17 -04001897 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
Tal Einat604e7b92018-09-25 15:10:14 +03001898 text=' Additional Help Sources ')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001899 # Frame_window.
1900 frame_run = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001901 startup_title = Label(frame_run, text='At Startup')
1902 self.startup_editor_on = Radiobutton(
1903 frame_run, variable=self.startup_edit, value=1,
1904 text="Open Edit Window")
1905 self.startup_shell_on = Radiobutton(
1906 frame_run, variable=self.startup_edit, value=0,
1907 text='Open Shell Window')
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001908
wohlganger58fc71c2017-09-10 16:19:47 -05001909 frame_win_size = Frame(frame_window, borderwidth=0)
csabellae8eb17b2017-07-30 18:39:17 -04001910 win_size_title = Label(
1911 frame_win_size, text='Initial Window Size (in characters)')
1912 win_width_title = Label(frame_win_size, text='Width')
1913 self.win_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001914 frame_win_size, textvariable=self.win_width, width=3,
1915 validatecommand=self.digits_only, validate='key',
1916 )
csabellae8eb17b2017-07-30 18:39:17 -04001917 win_height_title = Label(frame_win_size, text='Height')
1918 self.win_height_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001919 frame_win_size, textvariable=self.win_height, width=3,
1920 validatecommand=self.digits_only, validate='key',
1921 )
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001922
wohlganger58fc71c2017-09-10 16:19:47 -05001923 frame_autocomplete = Frame(frame_window, borderwidth=0,)
1924 auto_wait_title = Label(frame_autocomplete,
1925 text='Completions Popup Wait (milliseconds)')
1926 self.auto_wait_int = Entry(frame_autocomplete, width=6,
Tal Einat1ebee372019-07-23 13:02:11 +03001927 textvariable=self.autocomplete_wait,
1928 validatecommand=self.digits_only,
1929 validate='key',
1930 )
wohlganger58fc71c2017-09-10 16:19:47 -05001931
1932 frame_paren1 = Frame(frame_window, borderwidth=0)
1933 paren_style_title = Label(frame_paren1, text='Paren Match Style')
1934 self.paren_style_type = OptionMenu(
1935 frame_paren1, self.paren_style, 'expression',
1936 "opener","parens","expression")
1937 frame_paren2 = Frame(frame_window, borderwidth=0)
1938 paren_time_title = Label(
1939 frame_paren2, text='Time Match Displayed (milliseconds)\n'
1940 '(0 is until next input)')
1941 self.paren_flash_time = Entry(
1942 frame_paren2, textvariable=self.flash_delay, width=6)
1943 self.bell_on = Checkbutton(
1944 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell)
1945
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04001946 # Frame_editor.
1947 frame_save = Frame(frame_editor, borderwidth=0)
1948 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1949 self.save_ask_on = Radiobutton(
1950 frame_save, variable=self.autosave, value=0,
1951 text="Prompt to Save")
1952 self.save_auto_on = Radiobutton(
1953 frame_save, variable=self.autosave, value=1,
1954 text='No Prompt')
1955
wohlganger58fc71c2017-09-10 16:19:47 -05001956 frame_format = Frame(frame_editor, borderwidth=0)
1957 format_width_title = Label(frame_format,
1958 text='Format Paragraph Max Width')
1959 self.format_width_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001960 frame_format, textvariable=self.format_width, width=4,
1961 validatecommand=self.digits_only, validate='key',
1962 )
wohlganger58fc71c2017-09-10 16:19:47 -05001963
Tal Einat7123ea02019-07-23 15:22:11 +03001964 frame_line_numbers_default = Frame(frame_editor, borderwidth=0)
1965 line_numbers_default_title = Label(
1966 frame_line_numbers_default, text='Show line numbers in new windows')
1967 self.line_numbers_default_bool = Checkbutton(
1968 frame_line_numbers_default,
1969 variable=self.line_numbers_default,
1970 width=1)
1971
wohlganger58fc71c2017-09-10 16:19:47 -05001972 frame_context = Frame(frame_editor, borderwidth=0)
Cheryl Sabella29996a12018-06-01 19:23:00 -04001973 context_title = Label(frame_context, text='Max Context Lines :')
wohlganger58fc71c2017-09-10 16:19:47 -05001974 self.context_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001975 frame_context, textvariable=self.context_lines, width=3,
1976 validatecommand=self.digits_only, validate='key',
1977 )
wohlganger58fc71c2017-09-10 16:19:47 -05001978
Tal Einat604e7b92018-09-25 15:10:14 +03001979 # Frame_shell.
1980 frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0)
1981 auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines,
1982 text='Auto-Squeeze Min. Lines:')
1983 self.auto_squeeze_min_lines_int = Entry(
Tal Einat1ebee372019-07-23 13:02:11 +03001984 frame_auto_squeeze_min_lines, width=4,
1985 textvariable=self.auto_squeeze_min_lines,
1986 validatecommand=self.digits_only, validate='key',
1987 )
wohlganger58fc71c2017-09-10 16:19:47 -05001988
csabellae8eb17b2017-07-30 18:39:17 -04001989 # frame_help.
1990 frame_helplist = Frame(frame_help)
1991 frame_helplist_buttons = Frame(frame_helplist)
1992 self.helplist = Listbox(
1993 frame_helplist, height=5, takefocus=True,
1994 exportselection=FALSE)
1995 scroll_helplist = Scrollbar(frame_helplist)
1996 scroll_helplist['command'] = self.helplist.yview
1997 self.helplist['yscrollcommand'] = scroll_helplist.set
1998 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1999 self.button_helplist_edit = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04002000 frame_helplist_buttons, text='Edit', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04002001 width=8, command=self.helplist_item_edit)
2002 self.button_helplist_add = Button(
2003 frame_helplist_buttons, text='Add',
2004 width=8, command=self.helplist_item_add)
2005 self.button_helplist_remove = Button(
Cheryl Sabella7028e592017-08-26 14:26:02 -04002006 frame_helplist_buttons, text='Remove', state='disabled',
csabellae8eb17b2017-07-30 18:39:17 -04002007 width=8, command=self.helplist_item_remove)
2008
2009 # Pack widgets:
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002010 # Body.
2011 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2012 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
Tal Einat604e7b92018-09-25 15:10:14 +03002013 frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
csabellae8eb17b2017-07-30 18:39:17 -04002014 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2015 # frame_run.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002016 frame_run.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002017 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2018 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2019 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
csabellae8eb17b2017-07-30 18:39:17 -04002020 # frame_win_size.
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002021 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X)
csabellae8eb17b2017-07-30 18:39:17 -04002022 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2023 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2024 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
2025 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2026 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002027 # frame_autocomplete.
2028 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X)
2029 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2030 self.auto_wait_int.pack(side=TOP, padx=10, pady=5)
2031 # frame_paren.
2032 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X)
2033 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2034 self.paren_style_type.pack(side=TOP, padx=10, pady=5)
2035 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X)
2036 paren_time_title.pack(side=LEFT, anchor=W, padx=5)
2037 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5)
2038 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5)
2039
Terry Jan Reedy390eadd2017-08-30 00:59:11 -04002040 # frame_save.
2041 frame_save.pack(side=TOP, padx=5, pady=0, fill=X)
2042 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2043 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2044 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002045 # frame_format.
2046 frame_format.pack(side=TOP, padx=5, pady=0, fill=X)
2047 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2048 self.format_width_int.pack(side=TOP, padx=10, pady=5)
Tal Einat7123ea02019-07-23 15:22:11 +03002049 # frame_line_numbers_default.
2050 frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X)
2051 line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2052 self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5)
wohlganger58fc71c2017-09-10 16:19:47 -05002053 # frame_context.
2054 frame_context.pack(side=TOP, padx=5, pady=0, fill=X)
2055 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2056 self.context_int.pack(side=TOP, padx=5, pady=5)
2057
Tal Einat604e7b92018-09-25 15:10:14 +03002058 # frame_auto_squeeze_min_lines
2059 frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X)
2060 auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2061 self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5)
2062
csabellae8eb17b2017-07-30 18:39:17 -04002063 # frame_help.
2064 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
2065 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2066 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
2067 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
2068 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
2069 self.button_helplist_add.pack(side=TOP, anchor=W)
2070 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
2071
2072 def load_general_cfg(self):
2073 "Load current configuration settings for the general options."
wohlganger58fc71c2017-09-10 16:19:47 -05002074 # Set variables for all windows.
csabellae8eb17b2017-07-30 18:39:17 -04002075 self.startup_edit.set(idleConf.GetOption(
wohlganger58fc71c2017-09-10 16:19:47 -05002076 'main', 'General', 'editor-on-startup', type='bool'))
csabellae8eb17b2017-07-30 18:39:17 -04002077 self.win_width.set(idleConf.GetOption(
2078 'main', 'EditorWindow', 'width', type='int'))
2079 self.win_height.set(idleConf.GetOption(
2080 'main', 'EditorWindow', 'height', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002081 self.autocomplete_wait.set(idleConf.GetOption(
2082 'extensions', 'AutoComplete', 'popupwait', type='int'))
2083 self.paren_style.set(idleConf.GetOption(
2084 'extensions', 'ParenMatch', 'style'))
2085 self.flash_delay.set(idleConf.GetOption(
2086 'extensions', 'ParenMatch', 'flash-delay', type='int'))
2087 self.paren_bell.set(idleConf.GetOption(
2088 'extensions', 'ParenMatch', 'bell'))
2089
2090 # Set variables for editor windows.
2091 self.autosave.set(idleConf.GetOption(
2092 'main', 'General', 'autosave', default=0, type='bool'))
2093 self.format_width.set(idleConf.GetOption(
2094 'extensions', 'FormatParagraph', 'max-width', type='int'))
Tal Einat7123ea02019-07-23 15:22:11 +03002095 self.line_numbers_default.set(idleConf.GetOption(
2096 'main', 'EditorWindow', 'line-numbers-default', type='bool'))
wohlganger58fc71c2017-09-10 16:19:47 -05002097 self.context_lines.set(idleConf.GetOption(
Cheryl Sabella29996a12018-06-01 19:23:00 -04002098 'extensions', 'CodeContext', 'maxlines', type='int'))
wohlganger58fc71c2017-09-10 16:19:47 -05002099
Tal Einat604e7b92018-09-25 15:10:14 +03002100 # Set variables for shell windows.
2101 self.auto_squeeze_min_lines.set(idleConf.GetOption(
2102 'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
2103
csabellae8eb17b2017-07-30 18:39:17 -04002104 # Set additional help sources.
2105 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2106 self.helplist.delete(0, 'end')
2107 for help_item in self.user_helplist:
2108 self.helplist.insert(END, help_item[0])
2109 self.set_add_delete_state()
2110
2111 def help_source_selected(self, event):
2112 "Handle event for selecting additional help."
2113 self.set_add_delete_state()
2114
2115 def set_add_delete_state(self):
2116 "Toggle the state for the help list buttons based on list entries."
2117 if self.helplist.size() < 1: # No entries in list.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002118 self.button_helplist_edit.state(('disabled',))
2119 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002120 else: # Some entries.
2121 if self.helplist.curselection(): # There currently is a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002122 self.button_helplist_edit.state(('!disabled',))
2123 self.button_helplist_remove.state(('!disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002124 else: # There currently is not a selection.
Cheryl Sabella7028e592017-08-26 14:26:02 -04002125 self.button_helplist_edit.state(('disabled',))
2126 self.button_helplist_remove.state(('disabled',))
csabellae8eb17b2017-07-30 18:39:17 -04002127
2128 def helplist_item_add(self):
2129 """Handle add button for the help list.
2130
2131 Query for name and location of new help sources and add
2132 them to the list.
2133 """
2134 help_source = HelpSource(self, 'New Help Source').result
2135 if help_source:
2136 self.user_helplist.append(help_source)
2137 self.helplist.insert(END, help_source[0])
2138 self.update_help_changes()
2139
2140 def helplist_item_edit(self):
2141 """Handle edit button for the help list.
2142
2143 Query with existing help source information and update
2144 config if the values are changed.
2145 """
2146 item_index = self.helplist.index(ANCHOR)
2147 help_source = self.user_helplist[item_index]
2148 new_help_source = HelpSource(
2149 self, 'Edit Help Source',
2150 menuitem=help_source[0],
2151 filepath=help_source[1],
2152 ).result
2153 if new_help_source and new_help_source != help_source:
2154 self.user_helplist[item_index] = new_help_source
2155 self.helplist.delete(item_index)
2156 self.helplist.insert(item_index, new_help_source[0])
2157 self.update_help_changes()
2158 self.set_add_delete_state() # Selected will be un-selected
2159
2160 def helplist_item_remove(self):
2161 """Handle remove button for the help list.
2162
2163 Delete the help list item from config.
2164 """
2165 item_index = self.helplist.index(ANCHOR)
2166 del(self.user_helplist[item_index])
2167 self.helplist.delete(item_index)
2168 self.update_help_changes()
2169 self.set_add_delete_state()
2170
2171 def update_help_changes(self):
2172 "Clear and rebuild the HelpFiles section in changes"
2173 changes['main']['HelpFiles'] = {}
2174 for num in range(1, len(self.user_helplist) + 1):
2175 changes.add_option(
2176 'main', 'HelpFiles', str(num),
2177 ';'.join(self.user_helplist[num-1][:2]))
2178
2179
csabella45bf7232017-07-26 19:09:58 -04002180class VarTrace:
2181 """Maintain Tk variables trace state."""
2182
2183 def __init__(self):
2184 """Store Tk variables and callbacks.
2185
2186 untraced: List of tuples (var, callback)
2187 that do not have the callback attached
2188 to the Tk var.
2189 traced: List of tuples (var, callback) where
2190 that callback has been attached to the var.
2191 """
2192 self.untraced = []
2193 self.traced = []
2194
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002195 def clear(self):
2196 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002197 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002198 self.untraced.clear()
2199 self.traced.clear()
2200
csabella45bf7232017-07-26 19:09:58 -04002201 def add(self, var, callback):
2202 """Add (var, callback) tuple to untraced list.
2203
2204 Args:
2205 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002206 callback: Either function name to be used as a callback
2207 or a tuple with IdleConf config-type, section, and
2208 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002209
2210 Return:
2211 Tk variable instance.
2212 """
2213 if isinstance(callback, tuple):
2214 callback = self.make_callback(var, callback)
2215 self.untraced.append((var, callback))
2216 return var
2217
2218 @staticmethod
2219 def make_callback(var, config):
2220 "Return default callback function to add values to changes instance."
2221 def default_callback(*params):
2222 "Add config values to changes instance."
2223 changes.add_option(*config, var.get())
2224 return default_callback
2225
2226 def attach(self):
2227 "Attach callback to all vars that are not traced."
2228 while self.untraced:
2229 var, callback = self.untraced.pop()
2230 var.trace_add('write', callback)
2231 self.traced.append((var, callback))
2232
2233 def detach(self):
2234 "Remove callback from traced vars."
2235 while self.traced:
2236 var, callback = self.traced.pop()
2237 var.trace_remove('write', var.trace_info()[0][1])
2238 self.untraced.append((var, callback))
2239
2240
csabella5b591542017-07-28 14:40:59 -04002241tracers = VarTrace()
2242
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002243help_common = '''\
2244When you click either the Apply or Ok buttons, settings in this
2245dialog that are different from IDLE's default are saved in
2246a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002247these changes apply to all versions of IDLE installed on this
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002248machine. [Cancel] only cancels changes made since the last save.
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002249'''
2250help_pages = {
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002251 'Fonts/Tabs':'''
2252Font sample: This shows what a selection of Basic Multilingual Plane
2253unicode characters look like for the current font selection. If the
2254selected font does not define a character, Tk attempts to find another
2255font that does. Substitute glyphs depend on what is available on a
2256particular system and will not necessarily have the same size as the
2257font selected. Line contains 20 characters up to Devanagari, 14 for
2258Tamil, and 10 for East Asia.
2259
2260Hebrew and Arabic letters should display right to left, starting with
2261alef, \u05d0 and \u0627. Arabic digits display left to right. The
2262Devanagari and Tamil lines start with digits. The East Asian lines
2263are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese
2264Hiragana and Katakana.
Serhiy Storchakaed6554c2017-10-28 03:22:44 +03002265
2266You can edit the font sample. Changes remain until IDLE is closed.
Terry Jan Reedye2e42272017-10-17 18:56:16 -04002267''',
Cheryl Sabella3866d9b2017-09-10 22:41:10 -04002268 'Highlights': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002269Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002270The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002271be used with older IDLE releases if it is saved as a custom
2272theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002273''',
2274 'Keys': '''
2275Keys:
2276The IDLE Modern Unix key set is new in June 2016. It can only
2277be used with older IDLE releases if it is saved as a custom
2278key set, with a different name.
2279''',
wohlganger58fc71c2017-09-10 16:19:47 -05002280 'General': '''
2281General:
wohlgangerfae2c352017-06-27 21:36:23 -05002282
penguindustin96466302019-05-06 14:57:17 -04002283AutoComplete: Popupwait is milliseconds to wait after key char, without
wohlgangerfae2c352017-06-27 21:36:23 -05002284cursor movement, before popping up completion box. Key char is '.' after
2285identifier or a '/' (or '\\' on Windows) within a string.
2286
2287FormatParagraph: Max-width is max chars in lines after re-formatting.
2288Use with paragraphs in both strings and comment blocks.
2289
2290ParenMatch: Style indicates what is highlighted when closer is entered:
2291'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2292'expression' (default) - also everything in between. Flash-delay is how
2293long to highlight if cursor is not moved (0 means forever).
Cheryl Sabella29996a12018-06-01 19:23:00 -04002294
2295CodeContext: Maxlines is the maximum number of code context lines to
2296display when Code Context is turned on for an editor window.
Tal Einat604e7b92018-09-25 15:10:14 +03002297
2298Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines
2299of output to automatically "squeeze".
wohlgangerfae2c352017-06-27 21:36:23 -05002300'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002301}
2302
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002303
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002304def is_int(s):
2305 "Return 's is blank or represents an int'"
2306 if not s:
2307 return True
2308 try:
2309 int(s)
2310 return True
2311 except ValueError:
2312 return False
2313
2314
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002315class VerticalScrolledFrame(Frame):
2316 """A pure Tkinter vertically scrollable frame.
2317
2318 * Use the 'interior' attribute to place widgets inside the scrollable frame
2319 * Construct and pack/place/grid normally
2320 * This frame only allows vertical scrolling
2321 """
2322 def __init__(self, parent, *args, **kw):
2323 Frame.__init__(self, parent, *args, **kw)
2324
csabella7eb58832017-07-04 21:30:58 -04002325 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002326 vscrollbar = Scrollbar(self, orient=VERTICAL)
2327 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
Cheryl Sabella7028e592017-08-26 14:26:02 -04002328 canvas = Canvas(self, borderwidth=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002329 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002330 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2331 vscrollbar.config(command=canvas.yview)
2332
csabella7eb58832017-07-04 21:30:58 -04002333 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002334 canvas.xview_moveto(0)
2335 canvas.yview_moveto(0)
2336
csabella7eb58832017-07-04 21:30:58 -04002337 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002338 self.interior = interior = Frame(canvas)
2339 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2340
csabella7eb58832017-07-04 21:30:58 -04002341 # Track changes to the canvas and frame width and sync them,
2342 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002343 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002344 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002345 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2346 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002347 interior.bind('<Configure>', _configure_interior)
2348
2349 def _configure_canvas(event):
2350 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002351 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002352 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2353 canvas.bind('<Configure>', _configure_canvas)
2354
2355 return
2356
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002357
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002358if __name__ == '__main__':
Terry Jan Reedy4d921582018-06-19 19:12:52 -04002359 from unittest import main
2360 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False)
2361
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002362 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002363 run(ConfigDialog)