blob: a36bf83ad0f891c0226addf4c1af46d979f37770 [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"""
terryjreedy938e7382017-06-26 20:48:39 -040012from tkinter import (Toplevel, Frame, LabelFrame, Listbox, Label, Button,
13 Entry, Text, Scale, Radiobutton, Checkbutton, Canvas,
14 StringVar, BooleanVar, IntVar, TRUE, FALSE,
15 TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE, NORMAL, DISABLED,
16 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
terryjreedy7ab33422017-07-09 19:26:32 -040017 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END)
Terry Jan Reedy8364fef2017-07-29 01:28:05 -040018from tkinter.ttk import Notebook, Scrollbar
Georg Brandl14fc4272008-05-17 18:39:55 +000019import tkinter.colorchooser as tkColorChooser
20import tkinter.font as tkFont
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040021import tkinter.messagebox as tkMessageBox
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000022
terryjreedyedc03422017-07-07 16:37:39 -040023from idlelib.config import idleConf, ConfigChanges
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040024from idlelib.config_key import GetKeysDialog
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040025from idlelib.dynoption import DynOptionMenu
26from idlelib import macosx
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040027from idlelib.query import SectionName, HelpSource
Terry Jan Reedya9421fb2014-10-22 20:15:18 -040028from idlelib.tabbedpages import TabbedPageSet
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040029from idlelib.textview import view_text
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -040030
terryjreedyedc03422017-07-07 16:37:39 -040031changes = ConfigChanges()
32
Terry Jan Reedy02f88d22017-07-28 15:42:43 -040033
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000034class ConfigDialog(Toplevel):
terryjreedye5bb1122017-07-05 00:54:55 -040035 """Config dialog for IDLE.
36 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000037
Terry Jan Reedycd567362014-10-17 01:31:35 -040038 def __init__(self, parent, title='', _htest=False, _utest=False):
terryjreedye5bb1122017-07-05 00:54:55 -040039 """Show the tabbed dialog for user configuration.
40
terryjreedy9a09c662017-07-13 23:53:30 -040041 Args:
42 parent - parent of this dialog
43 title - string which is the title of this popup dialog
44 _htest - bool, change box location when running htest
45 _utest - bool, don't wait_window when running unittest
46
47 Note: Focus set on font page fontlist.
48
49 Methods:
50 create_widgets
51 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040052 """
Steven M. Gavad721c482001-07-31 10:46:53 +000053 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040054 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040055 if _htest:
56 parent.instance_dict = {}
terryjreedy42abf7f2017-07-13 22:24:55 -040057 if not _utest:
58 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000059
Steven M. Gavad721c482001-07-31 10:46:53 +000060 self.configure(borderwidth=5)
Terry Jan Reedycd567362014-10-17 01:31:35 -040061 self.title(title or 'IDLE Preferences')
terryjreedy938e7382017-06-26 20:48:39 -040062 x = parent.winfo_rootx() + 20
63 y = parent.winfo_rooty() + (30 if not _htest else 150)
64 self.geometry(f'+{x}+{y}')
terryjreedye5bb1122017-07-05 00:54:55 -040065 # Each theme element key is its display name.
66 # The first value of the tuple is the sample area tag name.
67 # The second value is the display name list sort index.
terryjreedy938e7382017-06-26 20:48:39 -040068 self.create_widgets()
Terry Jan Reedy4036d872014-08-03 23:02:58 -040069 self.resizable(height=FALSE, width=FALSE)
Steven M. Gavad721c482001-07-31 10:46:53 +000070 self.transient(parent)
terryjreedy938e7382017-06-26 20:48:39 -040071 self.protocol("WM_DELETE_WINDOW", self.cancel)
Terry Jan Reedy75822262017-07-30 15:00:50 -040072 self.fontpage.fontlist.focus_set()
terryjreedye5bb1122017-07-05 00:54:55 -040073 # XXX Decide whether to keep or delete these key bindings.
74 # Key bindings for this dialog.
75 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
76 # self.bind('<Alt-a>', self.Apply) #apply changes, save
77 # self.bind('<F1>', self.Help) #context help
terryjreedy938e7382017-06-26 20:48:39 -040078 self.load_configs()
Terry Jan Reedy02f88d22017-07-28 15:42:43 -040079 # Avoid callbacks during load_configs.
80 tracers.attach()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000081
Terry Jan Reedycfa89502014-07-14 23:07:32 -040082 if not _utest:
terryjreedy42abf7f2017-07-13 22:24:55 -040083 self.grab_set()
Terry Jan Reedycfa89502014-07-14 23:07:32 -040084 self.wm_deiconify()
85 self.wait_window()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +000086
Terry Jan Reedy02f88d22017-07-28 15:42:43 -040087
terryjreedy938e7382017-06-26 20:48:39 -040088 def create_widgets(self):
terryjreedy9a09c662017-07-13 23:53:30 -040089 """Create and place widgets for tabbed dialog.
90
91 Widgets Bound to self:
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -040092 note: Notebook
93 highpage: self.create_page_highlight
94 fontpage: FontPage
Terry Jan Reedyff4b2222017-08-15 19:13:11 -040095 keyspage: KeysPage
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -040096 genpage: GenPage
Terry Jan Reedyff4b2222017-08-15 19:13:11 -040097 extpage: self.create_page_extensions
terryjreedy9a09c662017-07-13 23:53:30 -040098
99 Methods:
terryjreedy9a09c662017-07-13 23:53:30 -0400100 create_action_buttons
101 load_configs: Load pages except for extensions.
terryjreedy9a09c662017-07-13 23:53:30 -0400102 activate_config_changes: Tell editors to reload.
103 """
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400104 self.note = note = Notebook(self, width=450, height=450)
Terry Jan Reedy75822262017-07-30 15:00:50 -0400105 self.highpage = self.create_page_highlight()
106 self.fontpage = FontPage(note, self.highpage)
Terry Jan Reedyff4b2222017-08-15 19:13:11 -0400107 self.keyspage = KeysPage(note)
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -0400108 self.genpage = GenPage(note)
Terry Jan Reedy75822262017-07-30 15:00:50 -0400109 self.extpage = self.create_page_extensions()
110 note.add(self.fontpage, text='Fonts/Tabs')
111 note.add(self.highpage, text='Highlights')
112 note.add(self.keyspage, text=' Keys ')
113 note.add(self.genpage, text=' General ')
114 note.add(self.extpage, text='Extensions')
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400115 note.enable_traversal()
116 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400117 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400118
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400119 def load_configs(self):
120 """Load configuration for each page.
121
122 Load configuration from default and user config files and populate
123 the widgets on the config dialog pages.
124
125 Methods:
126 load_font_cfg
127 load_tab_cfg
128 load_theme_cfg
129 load_key_cfg
130 load_general_cfg
131 """
Terry Jan Reedy75822262017-07-30 15:00:50 -0400132 #self.load_font_cfg()
133 #self.load_tab_cfg()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400134 self.load_theme_cfg()
Terry Jan Reedyff4b2222017-08-15 19:13:11 -0400135 # self.load_key_cfg()
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -0400136 # self.load_general_cfg()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400137 # note: extension page handled separately
138
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400139 def create_action_buttons(self):
terryjreedy9a09c662017-07-13 23:53:30 -0400140 """Return frame of action buttons for dialog.
141
142 Methods:
143 ok
144 apply
145 cancel
146 help
147
148 Widget Structure:
149 outer: Frame
150 buttons: Frame
151 (no assignment): Button (ok)
152 (no assignment): Button (apply)
153 (no assignment): Button (cancel)
154 (no assignment): Button (help)
155 (no assignment): Frame
156 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400157 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400158 # Changing the default padding on OSX results in unreadable
terryjreedye5bb1122017-07-05 00:54:55 -0400159 # text in the buttons.
terryjreedy938e7382017-06-26 20:48:39 -0400160 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000161 else:
terryjreedy938e7382017-06-26 20:48:39 -0400162 padding_args = {'padx':6, 'pady':3}
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400163 outer = Frame(self, pady=2)
164 buttons = Frame(outer, pady=2)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400165 for txt, cmd in (
terryjreedy938e7382017-06-26 20:48:39 -0400166 ('Ok', self.ok),
167 ('Apply', self.apply),
168 ('Cancel', self.cancel),
169 ('Help', self.help)):
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400170 Button(buttons, text=txt, command=cmd, takefocus=FALSE,
terryjreedy938e7382017-06-26 20:48:39 -0400171 **padding_args).pack(side=LEFT, padx=5)
terryjreedye5bb1122017-07-05 00:54:55 -0400172 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400173 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
174 buttons.pack(side=BOTTOM)
175 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400176
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400177 def ok(self):
178 """Apply config changes, then dismiss dialog.
179
180 Methods:
181 apply
182 destroy: inherited
183 """
184 self.apply()
185 self.destroy()
186
187 def apply(self):
188 """Apply config changes and leave dialog open.
189
190 Methods:
191 deactivate_current_config
192 save_all_changed_extensions
193 activate_config_changes
194 """
195 self.deactivate_current_config()
196 changes.save_all()
197 self.save_all_changed_extensions()
198 self.activate_config_changes()
199
200 def cancel(self):
201 """Dismiss config dialog.
202
203 Methods:
204 destroy: inherited
205 """
206 self.destroy()
207
208 def help(self):
209 """Create textview for config dialog help.
210
211 Attrbutes accessed:
212 tab_pages
213
214 Methods:
215 view_text: Method from textview module.
216 """
217 page = self.tab_pages._current_page
218 view_text(self, title='Help for IDLE preferences',
219 text=help_common+help_pages.get(page, ''))
220
Terry Jan Reedy1daeb252017-07-24 02:50:28 -0400221
terryjreedy938e7382017-06-26 20:48:39 -0400222 def create_page_highlight(self):
terryjreedye5bb1122017-07-05 00:54:55 -0400223 """Return frame of widgets for Highlighting tab.
224
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400225 Enable users to provisionally change foreground and background
226 colors applied to textual tags. Color mappings are stored in
227 complete listings called themes. Built-in themes in
228 idlelib/config-highlight.def are fixed as far as the dialog is
229 concerned. Any theme can be used as the base for a new custom
230 theme, stored in .idlerc/config-highlight.cfg.
231
232 Function load_theme_cfg() initializes tk variables and theme
233 lists and calls paint_theme_sample() and set_highlight_target()
234 for the current theme. Radiobuttons builtin_theme_on and
235 custom_theme_on toggle var theme_source, which controls if the
236 current set of colors are from a builtin or custom theme.
237 DynOptionMenus builtinlist and customlist contain lists of the
238 builtin and custom themes, respectively, and the current item
239 from each list is stored in vars builtin_name and custom_name.
240
241 Function paint_theme_sample() applies the colors from the theme
242 to the tags in text widget highlight_sample and then invokes
243 set_color_sample(). Function set_highlight_target() sets the state
244 of the radiobuttons fg_on and bg_on based on the tag and it also
245 invokes set_color_sample().
246
247 Function set_color_sample() sets the background color for the frame
248 holding the color selector. This provides a larger visual of the
249 color for the current tag and plane (foreground/background).
250
251 Note: set_color_sample() is called from many places and is often
252 called more than once when a change is made. It is invoked when
253 foreground or background is selected (radiobuttons), from
254 paint_theme_sample() (theme is changed or load_cfg is called), and
255 from set_highlight_target() (target tag is changed or load_cfg called).
256
257 Button delete_custom invokes delete_custom() to delete
258 a custom theme from idleConf.userCfg['highlight'] and changes.
259 Button save_custom invokes save_as_new_theme() which calls
260 get_new_theme_name() and create_new() to save a custom theme
261 and its colors to idleConf.userCfg['highlight'].
262
263 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
264 if the current selected color for a tag is for the foreground or
265 background.
266
267 DynOptionMenu targetlist contains a readable description of the
268 tags applied to Python source within IDLE. Selecting one of the
269 tags from this list populates highlight_target, which has a callback
270 function set_highlight_target().
271
272 Text widget highlight_sample displays a block of text (which is
273 mock Python code) in which is embedded the defined tags and reflects
274 the color attributes of the current theme and changes for those tags.
275 Mouse button 1 allows for selection of a tag and updates
276 highlight_target with that tag value.
277
278 Note: The font in highlight_sample is set through the config in
279 the fonts tab.
280
281 In other words, a tag can be selected either from targetlist or
282 by clicking on the sample text within highlight_sample. The
283 plane (foreground/background) is selected via the radiobutton.
284 Together, these two (tag and plane) control what color is
285 shown in set_color_sample() for the current theme. Button set_color
286 invokes get_color() which displays a ColorChooser to change the
287 color for the selected tag/plane. If a new color is picked,
288 it will be saved to changes and the highlight_sample and
289 frame background will be updated.
290
terryjreedy9a09c662017-07-13 23:53:30 -0400291 Tk Variables:
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400292 color: Color of selected target.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400293 builtin_name: Menu variable for built-in theme.
294 custom_name: Menu variable for custom theme.
terryjreedye5bb1122017-07-05 00:54:55 -0400295 fg_bg_toggle: Toggle for foreground/background color.
terryjreedy9a09c662017-07-13 23:53:30 -0400296 Note: this has no callback.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400297 theme_source: Selector for built-in or custom theme.
terryjreedye5bb1122017-07-05 00:54:55 -0400298 highlight_target: Menu variable for the highlight tag target.
terryjreedy9a09c662017-07-13 23:53:30 -0400299
300 Instance Data Attributes:
301 theme_elements: Dictionary of tags for text highlighting.
302 The key is the display name and the value is a tuple of
303 (tag name, display sort order).
304
305 Methods [attachment]:
306 load_theme_cfg: Load current highlight colors.
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400307 get_color: Invoke colorchooser [button_set_color].
308 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
terryjreedy9a09c662017-07-13 23:53:30 -0400309 set_highlight_target: set fg_bg_toggle, set_color_sample().
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400310 set_color_sample: Set frame background to target.
311 on_new_color_set: Set new color and add option.
terryjreedy9a09c662017-07-13 23:53:30 -0400312 paint_theme_sample: Recolor sample.
313 get_new_theme_name: Get from popup.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400314 create_new: Combine theme with changes and save.
315 save_as_new_theme: Save [button_save_custom].
316 set_theme_type: Command for [theme_source].
317 delete_custom: Activate default [button_delete_custom].
318 save_new: Save to userCfg['theme'] (is function).
terryjreedy9a09c662017-07-13 23:53:30 -0400319
Terry Jan Reedya3145902017-08-14 21:45:02 -0400320 Widgets of highlights page frame: (*) widgets bound to self
321 frame_custom: LabelFrame
322 (*)highlight_sample: Text
323 (*)frame_color_set: Frame
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400324 (*)button_set_color: Button
325 (*)targetlist: DynOptionMenu - highlight_target
Terry Jan Reedya3145902017-08-14 21:45:02 -0400326 frame_fg_bg_toggle: Frame
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400327 (*)fg_on: Radiobutton - fg_bg_toggle
328 (*)bg_on: Radiobutton - fg_bg_toggle
329 (*)button_save_custom: Button
Terry Jan Reedya3145902017-08-14 21:45:02 -0400330 frame_theme: LabelFrame
331 theme_type_title: Label
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400332 (*)builtin_theme_on: Radiobutton - theme_source
333 (*)custom_theme_on: Radiobutton - theme_source
334 (*)builtinlist: DynOptionMenu - builtin_name
335 (*)customlist: DynOptionMenu - custom_name
336 (*)button_delete_custom: Button
337 (*)theme_message: Label
terryjreedye5bb1122017-07-05 00:54:55 -0400338 """
terryjreedy9a09c662017-07-13 23:53:30 -0400339 self.theme_elements={
340 'Normal Text': ('normal', '00'),
341 'Python Keywords': ('keyword', '01'),
342 'Python Definitions': ('definition', '02'),
343 'Python Builtins': ('builtin', '03'),
344 'Python Comments': ('comment', '04'),
345 'Python Strings': ('string', '05'),
346 'Selected Text': ('hilite', '06'),
347 'Found Text': ('hit', '07'),
348 'Cursor': ('cursor', '08'),
349 'Editor Breakpoint': ('break', '09'),
350 'Shell Normal Text': ('console', '10'),
351 'Shell Error Text': ('error', '11'),
352 'Shell Stdout Text': ('stdout', '12'),
353 'Shell Stderr Text': ('stderr', '13'),
354 }
Terry Jan Reedy22405332014-07-30 19:24:32 -0400355 parent = self.parent
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400356 self.builtin_name = tracers.add(
357 StringVar(parent), self.var_changed_builtin_name)
358 self.custom_name = tracers.add(
359 StringVar(parent), self.var_changed_custom_name)
terryjreedy938e7382017-06-26 20:48:39 -0400360 self.fg_bg_toggle = BooleanVar(parent)
Terry Jan Reedy02f88d22017-07-28 15:42:43 -0400361 self.color = tracers.add(
362 StringVar(parent), self.var_changed_color)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400363 self.theme_source = tracers.add(
364 BooleanVar(parent), self.var_changed_theme_source)
Terry Jan Reedy02f88d22017-07-28 15:42:43 -0400365 self.highlight_target = tracers.add(
366 StringVar(parent), self.var_changed_highlight_target)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400367
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400368 # Create widgets:
369 # body frame and section frames.
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400370 frame = Frame(self.note)
terryjreedy938e7382017-06-26 20:48:39 -0400371 frame_custom = LabelFrame(frame, borderwidth=2, relief=GROOVE,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400372 text=' Custom Highlighting ')
terryjreedy938e7382017-06-26 20:48:39 -0400373 frame_theme = LabelFrame(frame, borderwidth=2, relief=GROOVE,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400374 text=' Highlighting Theme ')
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400375 # frame_custom.
Terry Jan Reedy75822262017-07-30 15:00:50 -0400376 text = self.highlight_sample = frame.highlight_sample = Text(
terryjreedy938e7382017-06-26 20:48:39 -0400377 frame_custom, relief=SOLID, borderwidth=1,
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400378 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400379 takefocus=FALSE, highlightthickness=0, wrap=NONE)
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400380 text.bind('<Double-Button-1>', lambda e: 'break')
381 text.bind('<B1-Motion>', lambda e: 'break')
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400382 text_and_tags=(('\n', 'normal'),
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400383 ('#you can click here', 'comment'), ('\n', 'normal'),
384 ('#to choose items', 'comment'), ('\n', 'normal'),
385 ('def', 'keyword'), (' ', 'normal'),
386 ('func', 'definition'), ('(param):\n ', 'normal'),
387 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
388 ("'string'", 'string'), ('\n var1 = ', 'normal'),
389 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
390 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
391 ('list', 'builtin'), ('(', 'normal'),
Terry Jan Reedya8aa4d52015-10-02 22:12:17 -0400392 ('None', 'keyword'), (')\n', 'normal'),
393 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400394 (' error ', 'error'), (' ', 'normal'),
395 ('cursor |', 'cursor'), ('\n ', 'normal'),
396 ('shell', 'console'), (' ', 'normal'),
397 ('stdout', 'stdout'), (' ', 'normal'),
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400398 ('stderr', 'stderr'), ('\n\n', 'normal'))
terryjreedy938e7382017-06-26 20:48:39 -0400399 for texttag in text_and_tags:
400 text.insert(END, texttag[0], texttag[1])
401 for element in self.theme_elements:
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400402 def tem(event, elem=element):
terryjreedy938e7382017-06-26 20:48:39 -0400403 event.widget.winfo_toplevel().highlight_target.set(elem)
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400404 text.tag_bind(
terryjreedy938e7382017-06-26 20:48:39 -0400405 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Terry Jan Reedy0c4c6512017-07-26 21:41:26 -0400406 text['state'] = DISABLED
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400407 self.frame_color_set = Frame(frame_custom, relief=SOLID, borderwidth=1)
terryjreedy938e7382017-06-26 20:48:39 -0400408 frame_fg_bg_toggle = Frame(frame_custom)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400409 self.button_set_color = Button(
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400410 self.frame_color_set, text='Choose Color for :',
411 command=self.get_color, highlightthickness=0)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400412 self.targetlist = DynOptionMenu(
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400413 self.frame_color_set, self.highlight_target, None,
terryjreedy938e7382017-06-26 20:48:39 -0400414 highlightthickness=0) #, command=self.set_highlight_targetBinding
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400415 self.fg_on = Radiobutton(
terryjreedy938e7382017-06-26 20:48:39 -0400416 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400417 text='Foreground', command=self.set_color_sample_binding)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400418 self.bg_on = Radiobutton(
terryjreedy938e7382017-06-26 20:48:39 -0400419 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400420 text='Background', command=self.set_color_sample_binding)
terryjreedy938e7382017-06-26 20:48:39 -0400421 self.fg_bg_toggle.set(1)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400422 self.button_save_custom = Button(
terryjreedy938e7382017-06-26 20:48:39 -0400423 frame_custom, text='Save as New Custom Theme',
424 command=self.save_as_new_theme)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400425 # frame_theme.
terryjreedy938e7382017-06-26 20:48:39 -0400426 theme_type_title = Label(frame_theme, text='Select : ')
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400427 self.builtin_theme_on = Radiobutton(
428 frame_theme, variable=self.theme_source, value=1,
terryjreedy938e7382017-06-26 20:48:39 -0400429 command=self.set_theme_type, text='a Built-in Theme')
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400430 self.custom_theme_on = Radiobutton(
431 frame_theme, variable=self.theme_source, value=0,
terryjreedy938e7382017-06-26 20:48:39 -0400432 command=self.set_theme_type, text='a Custom Theme')
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400433 self.builtinlist = DynOptionMenu(
434 frame_theme, self.builtin_name, None, command=None)
435 self.customlist = DynOptionMenu(
436 frame_theme, self.custom_name, None, command=None)
437 self.button_delete_custom = Button(
terryjreedy938e7382017-06-26 20:48:39 -0400438 frame_theme, text='Delete Custom Theme',
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400439 command=self.delete_custom)
440 self.theme_message = Label(frame_theme, bd=2)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400441
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400442 # Pack widgets:
443 # body.
terryjreedy938e7382017-06-26 20:48:39 -0400444 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
445 frame_theme.pack(side=LEFT, padx=5, pady=5, fill=Y)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400446 # frame_custom.
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400447 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
terryjreedy938e7382017-06-26 20:48:39 -0400448 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
Terry Jan Reedy04864b42017-07-22 00:56:18 -0400449 self.highlight_sample.pack(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400450 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400451 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
452 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
453 self.fg_on.pack(side=LEFT, anchor=E)
454 self.bg_on.pack(side=RIGHT, anchor=W)
455 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
456 # frame_theme.
terryjreedy938e7382017-06-26 20:48:39 -0400457 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400458 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
459 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
460 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
461 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
462 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
463 self.theme_message.pack(side=TOP, fill=X, pady=5)
Steven M. Gava952d0a52001-08-03 04:43:44 +0000464 return frame
465
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400466 def load_theme_cfg(self):
467 """Load current configuration settings for the theme options.
468
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400469 Based on the theme_source toggle, the theme is set as
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400470 either builtin or custom and the initial widget values
471 reflect the current settings from idleConf.
472
473 Attributes updated:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400474 theme_source: Set from idleConf.
475 builtinlist: List of default themes from idleConf.
476 customlist: List of custom themes from idleConf.
477 custom_theme_on: Disabled if there are no custom themes.
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400478 custom_theme: Message with additional information.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400479 targetlist: Create menu from self.theme_elements.
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400480
481 Methods:
482 set_theme_type
483 paint_theme_sample
484 set_highlight_target
485 """
486 # Set current theme type radiobutton.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400487 self.theme_source.set(idleConf.GetOption(
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400488 'main', 'Theme', 'default', type='bool', default=1))
489 # Set current theme.
490 current_option = idleConf.CurrentTheme()
491 # Load available theme option menus.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400492 if self.theme_source.get(): # Default theme selected.
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400493 item_list = idleConf.GetSectionList('default', 'highlight')
494 item_list.sort()
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400495 self.builtinlist.SetMenu(item_list, current_option)
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400496 item_list = idleConf.GetSectionList('user', 'highlight')
497 item_list.sort()
498 if not item_list:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400499 self.custom_theme_on['state'] = DISABLED
500 self.custom_name.set('- no custom themes -')
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400501 else:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400502 self.customlist.SetMenu(item_list, item_list[0])
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400503 else: # User theme selected.
504 item_list = idleConf.GetSectionList('user', 'highlight')
505 item_list.sort()
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400506 self.customlist.SetMenu(item_list, current_option)
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400507 item_list = idleConf.GetSectionList('default', 'highlight')
508 item_list.sort()
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400509 self.builtinlist.SetMenu(item_list, item_list[0])
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400510 self.set_theme_type()
511 # Load theme element option menu.
512 theme_names = list(self.theme_elements.keys())
513 theme_names.sort(key=lambda x: self.theme_elements[x][1])
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400514 self.targetlist.SetMenu(theme_names, theme_names[0])
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400515 self.paint_theme_sample()
516 self.set_highlight_target()
517
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400518 def var_changed_builtin_name(self, *params):
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400519 """Process new builtin theme selection.
520
521 Add the changed theme's name to the changed_items and recreate
522 the sample with the values from the selected theme.
523 """
524 old_themes = ('IDLE Classic', 'IDLE New')
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400525 value = self.builtin_name.get()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400526 if value not in old_themes:
527 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
528 changes.add_option('main', 'Theme', 'name', old_themes[0])
529 changes.add_option('main', 'Theme', 'name2', value)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400530 self.theme_message['text'] = 'New theme, see Help'
531 self.theme_message['fg'] = '#500000'
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400532 else:
533 changes.add_option('main', 'Theme', 'name', value)
534 changes.add_option('main', 'Theme', 'name2', '')
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400535 self.theme_message['text'] = ''
536 self.theme_message['fg'] = 'black'
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400537 self.paint_theme_sample()
538
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400539 def var_changed_custom_name(self, *params):
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400540 """Process new custom theme selection.
541
542 If a new custom theme is selected, add the name to the
543 changed_items and apply the theme to the sample.
544 """
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400545 value = self.custom_name.get()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400546 if value != '- no custom themes -':
547 changes.add_option('main', 'Theme', 'name', value)
548 self.paint_theme_sample()
549
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400550 def var_changed_theme_source(self, *params):
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400551 """Process toggle between builtin and custom theme.
552
553 Update the default toggle value and apply the newly
554 selected theme type.
555 """
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400556 value = self.theme_source.get()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400557 changes.add_option('main', 'Theme', 'default', value)
558 if value:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400559 self.var_changed_builtin_name()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400560 else:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400561 self.var_changed_custom_name()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400562
563 def var_changed_color(self, *params):
564 "Process change to color choice."
565 self.on_new_color_set()
566
567 def var_changed_highlight_target(self, *params):
568 "Process selection of new target tag for highlighting."
569 self.set_highlight_target()
570
571 def set_theme_type(self):
572 """Set available screen options based on builtin or custom theme.
573
574 Attributes accessed:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400575 theme_source
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400576
577 Attributes updated:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400578 builtinlist
579 customlist
580 button_delete_custom
581 custom_theme_on
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400582
583 Called from:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400584 handler for builtin_theme_on and custom_theme_on
585 delete_custom
586 create_new
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400587 load_theme_cfg
588 """
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400589 if self.theme_source.get():
590 self.builtinlist['state'] = NORMAL
591 self.customlist['state'] = DISABLED
592 self.button_delete_custom['state'] = DISABLED
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400593 else:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400594 self.builtinlist['state'] = DISABLED
595 self.custom_theme_on['state'] = NORMAL
596 self.customlist['state'] = NORMAL
597 self.button_delete_custom['state'] = NORMAL
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400598
599 def get_color(self):
600 """Handle button to select a new color for the target tag.
601
602 If a new color is selected while using a builtin theme, a
603 name must be supplied to create a custom theme.
604
605 Attributes accessed:
606 highlight_target
607 frame_color_set
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400608 theme_source
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400609
610 Attributes updated:
611 color
612
613 Methods:
614 get_new_theme_name
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400615 create_new
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400616 """
617 target = self.highlight_target.get()
618 prev_color = self.frame_color_set.cget('bg')
619 rgbTuplet, color_string = tkColorChooser.askcolor(
620 parent=self, title='Pick new color for : '+target,
621 initialcolor=prev_color)
622 if color_string and (color_string != prev_color):
623 # User didn't cancel and they chose a new color.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400624 if self.theme_source.get(): # Current theme is a built-in.
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400625 message = ('Your changes will be saved as a new Custom Theme. '
626 'Enter a name for your new Custom Theme below.')
627 new_theme = self.get_new_theme_name(message)
628 if not new_theme: # User cancelled custom theme creation.
629 return
630 else: # Create new custom theme based on previously active theme.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400631 self.create_new(new_theme)
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400632 self.color.set(color_string)
633 else: # Current theme is user defined.
634 self.color.set(color_string)
635
636 def on_new_color_set(self):
637 "Display sample of new color selection on the dialog."
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400638 new_color = self.color.get()
639 self.frame_color_set['bg'] = new_color # Set sample.
640 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400641 sample_element = self.theme_elements[self.highlight_target.get()][0]
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400642 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
643 theme = self.custom_name.get()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400644 theme_element = sample_element + '-' + plane
645 changes.add_option('highlight', theme, theme_element, new_color)
646
647 def get_new_theme_name(self, message):
648 "Return name of new theme from query popup."
649 used_names = (idleConf.GetSectionList('user', 'highlight') +
650 idleConf.GetSectionList('default', 'highlight'))
651 new_theme = SectionName(
652 self, 'New Custom Theme', message, used_names).result
653 return new_theme
654
655 def save_as_new_theme(self):
656 """Prompt for new theme name and create the theme.
657
658 Methods:
659 get_new_theme_name
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400660 create_new
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400661 """
662 new_theme_name = self.get_new_theme_name('New Theme Name:')
663 if new_theme_name:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400664 self.create_new(new_theme_name)
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400665
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400666 def create_new(self, new_theme_name):
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400667 """Create a new custom theme with the given name.
668
669 Create the new theme based on the previously active theme
670 with the current changes applied. Once it is saved, then
671 activate the new theme.
672
673 Attributes accessed:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400674 builtin_name
675 custom_name
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400676
677 Attributes updated:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400678 customlist
679 theme_source
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400680
681 Method:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400682 save_new
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400683 set_theme_type
684 """
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400685 if self.theme_source.get():
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400686 theme_type = 'default'
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400687 theme_name = self.builtin_name.get()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400688 else:
689 theme_type = 'user'
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400690 theme_name = self.custom_name.get()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400691 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
692 # Apply any of the old theme's unsaved changes to the new theme.
693 if theme_name in changes['highlight']:
694 theme_changes = changes['highlight'][theme_name]
695 for element in theme_changes:
696 new_theme[element] = theme_changes[element]
697 # Save the new theme.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400698 self.save_new(new_theme_name, new_theme)
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400699 # Change GUI over to the new theme.
700 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
701 custom_theme_list.sort()
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400702 self.customlist.SetMenu(custom_theme_list, new_theme_name)
703 self.theme_source.set(0)
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400704 self.set_theme_type()
705
706 def set_highlight_target(self):
707 """Set fg/bg toggle and color based on highlight tag target.
708
709 Instance variables accessed:
710 highlight_target
711
712 Attributes updated:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400713 fg_on
714 bg_on
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400715 fg_bg_toggle
716
717 Methods:
718 set_color_sample
719
720 Called from:
721 var_changed_highlight_target
722 load_theme_cfg
723 """
724 if self.highlight_target.get() == 'Cursor': # bg not possible
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400725 self.fg_on['state'] = DISABLED
726 self.bg_on['state'] = DISABLED
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400727 self.fg_bg_toggle.set(1)
728 else: # Both fg and bg can be set.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400729 self.fg_on['state'] = NORMAL
730 self.bg_on['state'] = NORMAL
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400731 self.fg_bg_toggle.set(1)
732 self.set_color_sample()
733
734 def set_color_sample_binding(self, *args):
735 """Change color sample based on foreground/background toggle.
736
737 Methods:
738 set_color_sample
739 """
740 self.set_color_sample()
741
742 def set_color_sample(self):
743 """Set the color of the frame background to reflect the selected target.
744
745 Instance variables accessed:
746 theme_elements
747 highlight_target
748 fg_bg_toggle
749 highlight_sample
750
751 Attributes updated:
752 frame_color_set
753 """
754 # Set the color sample area.
755 tag = self.theme_elements[self.highlight_target.get()][0]
756 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
757 color = self.highlight_sample.tag_cget(tag, plane)
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400758 self.frame_color_set['bg'] = color
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400759
760 def paint_theme_sample(self):
761 """Apply the theme colors to each element tag in the sample text.
762
763 Instance attributes accessed:
764 theme_elements
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400765 theme_source
766 builtin_name
767 custom_name
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400768
769 Attributes updated:
770 highlight_sample: Set the tag elements to the theme.
771
772 Methods:
773 set_color_sample
774
775 Called from:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400776 var_changed_builtin_name
777 var_changed_custom_name
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400778 load_theme_cfg
779 """
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400780 if self.theme_source.get(): # Default theme
781 theme = self.builtin_name.get()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400782 else: # User theme
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400783 theme = self.custom_name.get()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400784 for element_title in self.theme_elements:
785 element = self.theme_elements[element_title][0]
786 colors = idleConf.GetHighlight(theme, element)
787 if element == 'cursor': # Cursor sample needs special painting.
788 colors['background'] = idleConf.GetHighlight(
789 theme, 'normal', fgBg='bg')
790 # Handle any unsaved changes to this theme.
791 if theme in changes['highlight']:
792 theme_dict = changes['highlight'][theme]
793 if element + '-foreground' in theme_dict:
794 colors['foreground'] = theme_dict[element + '-foreground']
795 if element + '-background' in theme_dict:
796 colors['background'] = theme_dict[element + '-background']
797 self.highlight_sample.tag_config(element, **colors)
798 self.set_color_sample()
799
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400800 def save_new(self, theme_name, theme):
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400801 """Save a newly created theme to idleConf.
802
803 theme_name - string, the name of the new theme
804 theme - dictionary containing the new theme
805 """
806 if not idleConf.userCfg['highlight'].has_section(theme_name):
807 idleConf.userCfg['highlight'].add_section(theme_name)
808 for element in theme:
809 value = theme[element]
810 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
811
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400812 def delete_custom(self):
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400813 """Handle event to delete custom theme.
814
815 The current theme is deactivated and the default theme is
816 activated. The custom theme is permanently removed from
817 the config file.
818
819 Attributes accessed:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400820 custom_name
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400821
822 Attributes updated:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400823 custom_theme_on
824 customlist
825 theme_source
826 builtin_name
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400827
828 Methods:
829 deactivate_current_config
830 save_all_changed_extensions
831 activate_config_changes
832 set_theme_type
833 """
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400834 theme_name = self.custom_name.get()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400835 delmsg = 'Are you sure you wish to delete the theme %r ?'
836 if not tkMessageBox.askyesno(
837 'Delete Theme', delmsg % theme_name, parent=self):
838 return
839 self.deactivate_current_config()
840 # Remove theme from changes, config, and file.
841 changes.delete_section('highlight', theme_name)
842 # Reload user theme list.
843 item_list = idleConf.GetSectionList('user', 'highlight')
844 item_list.sort()
845 if not item_list:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400846 self.custom_theme_on['state'] = DISABLED
847 self.customlist.SetMenu(item_list, '- no custom themes -')
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400848 else:
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400849 self.customlist.SetMenu(item_list, item_list[0])
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400850 # Revert to default theme.
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -0400851 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
852 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400853 # User can't back out of these changes, they must be applied now.
854 changes.save_all()
855 self.save_all_changed_extensions()
856 self.activate_config_changes()
857 self.set_theme_type()
858
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400859 def deactivate_current_config(self):
860 """Remove current key bindings.
861
862 Iterate over window instances defined in parent and remove
863 the keybindings.
864 """
865 # Before a config is saved, some cleanup of current
866 # config must be done - remove the previous keybindings.
867 win_instances = self.parent.instance_dict.keys()
868 for instance in win_instances:
869 instance.RemoveKeybindings()
870
871 def activate_config_changes(self):
872 """Apply configuration changes to current windows.
873
874 Dynamically update the current parent window instances
875 with some of the configuration changes.
876 """
877 win_instances = self.parent.instance_dict.keys()
878 for instance in win_instances:
879 instance.ResetColorizer()
880 instance.ResetFont()
881 instance.set_notabs_indentwidth()
882 instance.ApplyKeybindings()
883 instance.reset_help_menu_entries()
884
terryjreedy938e7382017-06-26 20:48:39 -0400885 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400886 """Part of the config dialog used for configuring IDLE extensions.
887
888 This code is generic - it works for any and all IDLE extensions.
889
890 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400891 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400892 GUI interface to change the configuration values, and saves the
893 changes using idleConf.
894
895 Not all changes take effect immediately - some may require restarting IDLE.
896 This depends on each extension's implementation.
897
898 All values are treated as text, and it is up to the user to supply
899 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300900 which are boolean, and can be toggled with a True/False button.
terryjreedy9a09c662017-07-13 23:53:30 -0400901
902 Methods:
Terry Jan Reedyff4b2222017-08-15 19:13:11 -0400903 load_extensions:
terryjreedy9a09c662017-07-13 23:53:30 -0400904 extension_selected: Handle selection from list.
905 create_extension_frame: Hold widgets for one extension.
906 set_extension_value: Set in userCfg['extensions'].
907 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400908 """
909 parent = self.parent
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400910 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400911 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
912 self.ext_userCfg = idleConf.userCfg['extensions']
913 self.is_int = self.register(is_int)
914 self.load_extensions()
terryjreedye5bb1122017-07-05 00:54:55 -0400915 # Create widgets - a listbox shows all available extensions, with the
916 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400917 self.extension_names = StringVar(self)
918 frame.rowconfigure(0, weight=1)
919 frame.columnconfigure(2, weight=1)
920 self.extension_list = Listbox(frame, listvariable=self.extension_names,
921 selectmode='browse')
922 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
923 scroll = Scrollbar(frame, command=self.extension_list.yview)
924 self.extension_list.yscrollcommand=scroll.set
925 self.details_frame = LabelFrame(frame, width=250, height=250)
926 self.extension_list.grid(column=0, row=0, sticky='nws')
927 scroll.grid(column=1, row=0, sticky='ns')
928 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
929 frame.configure(padx=10, pady=10)
930 self.config_frame = {}
931 self.current_extension = None
932
933 self.outerframe = self # TEMPORARY
934 self.tabbed_page_set = self.extension_list # TEMPORARY
935
terryjreedye5bb1122017-07-05 00:54:55 -0400936 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400937 ext_names = ''
938 for ext_name in sorted(self.extensions):
939 self.create_extension_frame(ext_name)
940 ext_names = ext_names + '{' + ext_name + '} '
941 self.extension_names.set(ext_names)
942 self.extension_list.selection_set(0)
943 self.extension_selected(None)
944
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400945 return frame
946
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400947 def load_extensions(self):
948 "Fill self.extensions with data from the default and user configs."
949 self.extensions = {}
950 for ext_name in idleConf.GetExtensions(active_only=False):
951 self.extensions[ext_name] = []
952
953 for ext_name in self.extensions:
954 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
955
terryjreedye5bb1122017-07-05 00:54:55 -0400956 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400957 enables = [opt_name for opt_name in opt_list
958 if opt_name.startswith('enable')]
959 for opt_name in enables:
960 opt_list.remove(opt_name)
961 opt_list = enables + opt_list
962
963 for opt_name in opt_list:
964 def_str = self.ext_defaultCfg.Get(
965 ext_name, opt_name, raw=True)
966 try:
967 def_obj = {'True':True, 'False':False}[def_str]
968 opt_type = 'bool'
969 except KeyError:
970 try:
971 def_obj = int(def_str)
972 opt_type = 'int'
973 except ValueError:
974 def_obj = def_str
975 opt_type = None
976 try:
977 value = self.ext_userCfg.Get(
978 ext_name, opt_name, type=opt_type, raw=True,
979 default=def_obj)
terryjreedye5bb1122017-07-05 00:54:55 -0400980 except ValueError: # Need this until .Get fixed.
981 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400982 var = StringVar(self)
983 var.set(str(value))
984
985 self.extensions[ext_name].append({'name': opt_name,
986 'type': opt_type,
987 'default': def_str,
988 'value': value,
989 'var': var,
990 })
991
992 def extension_selected(self, event):
terryjreedye5bb1122017-07-05 00:54:55 -0400993 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400994 newsel = self.extension_list.curselection()
995 if newsel:
996 newsel = self.extension_list.get(newsel)
997 if newsel is None or newsel != self.current_extension:
998 if self.current_extension:
999 self.details_frame.config(text='')
1000 self.config_frame[self.current_extension].grid_forget()
1001 self.current_extension = None
1002 if newsel:
1003 self.details_frame.config(text=newsel)
1004 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
1005 self.current_extension = newsel
1006
1007 def create_extension_frame(self, ext_name):
1008 """Create a frame holding the widgets to configure one extension"""
1009 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
1010 self.config_frame[ext_name] = f
1011 entry_area = f.interior
terryjreedye5bb1122017-07-05 00:54:55 -04001012 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001013 for row, opt in enumerate(self.extensions[ext_name]):
terryjreedye5bb1122017-07-05 00:54:55 -04001014 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001015 label = Label(entry_area, text=opt['name'])
1016 label.grid(row=row, column=0, sticky=NW)
1017 var = opt['var']
1018 if opt['type'] == 'bool':
1019 Checkbutton(entry_area, textvariable=var, variable=var,
1020 onvalue='True', offvalue='False',
1021 indicatoron=FALSE, selectcolor='', width=8
1022 ).grid(row=row, column=1, sticky=W, padx=7)
1023 elif opt['type'] == 'int':
1024 Entry(entry_area, textvariable=var, validate='key',
1025 validatecommand=(self.is_int, '%P')
1026 ).grid(row=row, column=1, sticky=NSEW, padx=7)
1027
1028 else:
1029 Entry(entry_area, textvariable=var
1030 ).grid(row=row, column=1, sticky=NSEW, padx=7)
1031 return
1032
1033 def set_extension_value(self, section, opt):
terryjreedye5bb1122017-07-05 00:54:55 -04001034 """Return True if the configuration was added or changed.
1035
1036 If the value is the same as the default, then remove it
1037 from user config file.
1038 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001039 name = opt['name']
1040 default = opt['default']
1041 value = opt['var'].get().strip() or default
1042 opt['var'].set(value)
1043 # if self.defaultCfg.has_section(section):
terryjreedye5bb1122017-07-05 00:54:55 -04001044 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001045 if (value == default):
1046 return self.ext_userCfg.RemoveOption(section, name)
terryjreedye5bb1122017-07-05 00:54:55 -04001047 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001048 return self.ext_userCfg.SetOption(section, name, value)
1049
1050 def save_all_changed_extensions(self):
terryjreedy9a09c662017-07-13 23:53:30 -04001051 """Save configuration changes to the user config file.
1052
1053 Attributes accessed:
1054 extensions
1055
1056 Methods:
1057 set_extension_value
1058 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001059 has_changes = False
1060 for ext_name in self.extensions:
1061 options = self.extensions[ext_name]
1062 for opt in options:
1063 if self.set_extension_value(ext_name, opt):
1064 has_changes = True
1065 if has_changes:
1066 self.ext_userCfg.Save()
1067
1068
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001069# class TabPage(Frame): # A template for Page classes.
1070# def __init__(self, master):
1071# super().__init__(master)
1072# self.create_page_tab()
1073# self.load_tab_cfg()
1074# def create_page_tab(self):
1075# # Define tk vars and register var and callback with tracers.
1076# # Create subframes and widgets.
1077# # Pack widgets.
1078# def load_tab_cfg(self):
1079# # Initialize widgets with data from idleConf.
1080# def var_changed_var_name():
1081# # For each tk var that needs other than default callback.
1082# def other_methods():
1083# # Define tab-specific behavior.
1084
1085
Terry Jan Reedy75822262017-07-30 15:00:50 -04001086class FontPage(Frame):
1087
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001088 def __init__(self, master, highpage):
1089 super().__init__(master)
Terry Jan Reedy75822262017-07-30 15:00:50 -04001090 self.highlight_sample = highpage.highlight_sample
1091 self.create_page_font_tab()
1092 self.load_font_cfg()
1093 self.load_tab_cfg()
1094
1095 def create_page_font_tab(self):
1096 """Return frame of widgets for Font/Tabs tab.
1097
1098 Fonts: Enable users to provisionally change font face, size, or
1099 boldness and to see the consequence of proposed choices. Each
1100 action set 3 options in changes structuree and changes the
1101 corresponding aspect of the font sample on this page and
1102 highlight sample on highlight page.
1103
1104 Function load_font_cfg initializes font vars and widgets from
1105 idleConf entries and tk.
1106
1107 Fontlist: mouse button 1 click or up or down key invoke
1108 on_fontlist_select(), which sets var font_name.
1109
1110 Sizelist: clicking the menubutton opens the dropdown menu. A
1111 mouse button 1 click or return key sets var font_size.
1112
1113 Bold_toggle: clicking the box toggles var font_bold.
1114
1115 Changing any of the font vars invokes var_changed_font, which
1116 adds all 3 font options to changes and calls set_samples.
1117 Set_samples applies a new font constructed from the font vars to
1118 font_sample and to highlight_sample on the hightlight page.
1119
1120 Tabs: Enable users to change spaces entered for indent tabs.
1121 Changing indent_scale value with the mouse sets Var space_num,
1122 which invokes the default callback to add an entry to
1123 changes. Load_tab_cfg initializes space_num to default.
1124
Terry Jan Reedya3145902017-08-14 21:45:02 -04001125 Widgets for FontPage(Frame): (*) widgets bound to self
1126 frame_font: LabelFrame
1127 frame_font_name: Frame
1128 font_name_title: Label
1129 (*)fontlist: ListBox - font_name
1130 scroll_font: Scrollbar
1131 frame_font_param: Frame
1132 font_size_title: Label
1133 (*)sizelist: DynOptionMenu - font_size
1134 (*)bold_toggle: Checkbutton - font_bold
1135 frame_font_sample: Frame
1136 (*)font_sample: Label
1137 frame_indent: LabelFrame
1138 indent_title: Label
1139 (*)indent_scale: Scale - space_num
Terry Jan Reedy75822262017-07-30 15:00:50 -04001140 """
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001141 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
1142 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
1143 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
Terry Jan Reedy75822262017-07-30 15:00:50 -04001144 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
1145
1146 # Create widgets:
1147 # body and body section frames.
Terry Jan Reedy75822262017-07-30 15:00:50 -04001148 frame_font = LabelFrame(
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001149 self, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
Terry Jan Reedy75822262017-07-30 15:00:50 -04001150 frame_indent = LabelFrame(
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001151 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
Terry Jan Reedy75822262017-07-30 15:00:50 -04001152 # frame_font.
1153 frame_font_name = Frame(frame_font)
1154 frame_font_param = Frame(frame_font)
1155 font_name_title = Label(
1156 frame_font_name, justify=LEFT, text='Font Face :')
1157 self.fontlist = Listbox(frame_font_name, height=5,
1158 takefocus=True, exportselection=FALSE)
1159 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
1160 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
1161 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
1162 scroll_font = Scrollbar(frame_font_name)
1163 scroll_font.config(command=self.fontlist.yview)
1164 self.fontlist.config(yscrollcommand=scroll_font.set)
1165 font_size_title = Label(frame_font_param, text='Size :')
1166 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
1167 self.bold_toggle = Checkbutton(
1168 frame_font_param, variable=self.font_bold,
1169 onvalue=1, offvalue=0, text='Bold')
1170 frame_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1)
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001171 temp_font = tkFont.Font(self, ('courier', 10, 'normal'))
Terry Jan Reedy75822262017-07-30 15:00:50 -04001172 self.font_sample = Label(
1173 frame_font_sample, justify=LEFT, font=temp_font,
1174 text='AaBbCcDdEe\nFfGgHhIiJj\n1234567890\n#:+=(){}[]')
1175 # frame_indent.
1176 indent_title = Label(
1177 frame_indent, justify=LEFT,
1178 text='Python Standard: 4 Spaces!')
1179 self.indent_scale = Scale(
1180 frame_indent, variable=self.space_num,
1181 orient='horizontal', tickinterval=2, from_=2, to=16)
1182
1183 # Pack widgets:
1184 # body.
1185 frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1186 frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y)
1187 # frame_font.
1188 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
1189 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
1190 font_name_title.pack(side=TOP, anchor=W)
1191 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
1192 scroll_font.pack(side=LEFT, fill=Y)
1193 font_size_title.pack(side=LEFT, anchor=W)
1194 self.sizelist.pack(side=LEFT, anchor=W)
1195 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
1196 frame_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1197 self.font_sample.pack(expand=TRUE, fill=BOTH)
1198 # frame_indent.
1199 frame_indent.pack(side=TOP, fill=X)
1200 indent_title.pack(side=TOP, anchor=W, padx=5)
1201 self.indent_scale.pack(side=TOP, padx=5, fill=X)
1202
Terry Jan Reedy75822262017-07-30 15:00:50 -04001203 def load_font_cfg(self):
1204 """Load current configuration settings for the font options.
1205
1206 Retrieve current font with idleConf.GetFont and font families
1207 from tk. Setup fontlist and set font_name. Setup sizelist,
1208 which sets font_size. Set font_bold. Call set_samples.
1209 """
1210 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
1211 font_name = configured_font[0].lower()
1212 font_size = configured_font[1]
1213 font_bold = configured_font[2]=='bold'
1214
1215 # Set editor font selection list and font_name.
1216 fonts = list(tkFont.families(self))
1217 fonts.sort()
1218 for font in fonts:
1219 self.fontlist.insert(END, font)
1220 self.font_name.set(font_name)
1221 lc_fonts = [s.lower() for s in fonts]
1222 try:
1223 current_font_index = lc_fonts.index(font_name)
1224 self.fontlist.see(current_font_index)
1225 self.fontlist.select_set(current_font_index)
1226 self.fontlist.select_anchor(current_font_index)
1227 self.fontlist.activate(current_font_index)
1228 except ValueError:
1229 pass
1230 # Set font size dropdown.
1231 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
1232 '16', '18', '20', '22', '25', '29', '34', '40'),
1233 font_size)
1234 # Set font weight.
1235 self.font_bold.set(font_bold)
1236 self.set_samples()
1237
1238 def var_changed_font(self, *params):
1239 """Store changes to font attributes.
1240
1241 When one font attribute changes, save them all, as they are
1242 not independent from each other. In particular, when we are
1243 overriding the default font, we need to write out everything.
1244 """
1245 value = self.font_name.get()
1246 changes.add_option('main', 'EditorWindow', 'font', value)
1247 value = self.font_size.get()
1248 changes.add_option('main', 'EditorWindow', 'font-size', value)
1249 value = self.font_bold.get()
1250 changes.add_option('main', 'EditorWindow', 'font-bold', value)
1251 self.set_samples()
1252
1253 def on_fontlist_select(self, event):
1254 """Handle selecting a font from the list.
1255
1256 Event can result from either mouse click or Up or Down key.
1257 Set font_name and example displays to selection.
1258 """
1259 font = self.fontlist.get(
1260 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
1261 self.font_name.set(font.lower())
1262
1263 def set_samples(self, event=None):
1264 """Update update both screen samples with the font settings.
1265
1266 Called on font initialization and change events.
1267 Accesses font_name, font_size, and font_bold Variables.
1268 Updates font_sample and hightlight page highlight_sample.
1269 """
1270 font_name = self.font_name.get()
1271 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
1272 new_font = (font_name, self.font_size.get(), font_weight)
1273 self.font_sample['font'] = new_font
1274 self.highlight_sample['font'] = new_font
1275
1276 def load_tab_cfg(self):
1277 """Load current configuration settings for the tab options.
1278
1279 Attributes updated:
1280 space_num: Set to value from idleConf.
1281 """
1282 # Set indent sizes.
1283 space_num = idleConf.GetOption(
1284 'main', 'Indent', 'num-spaces', default=4, type='int')
1285 self.space_num.set(space_num)
1286
1287 def var_changed_space_num(self, *params):
1288 "Store change to indentation size."
1289 value = self.space_num.get()
1290 changes.add_option('main', 'Indent', 'num-spaces', value)
1291
1292
Terry Jan Reedyff4b2222017-08-15 19:13:11 -04001293class KeysPage(Frame):
1294
1295 def __init__(self, master):
1296 super().__init__(master)
1297 self.cd = master.master
1298 self.create_page_keys()
1299 self.load_key_cfg()
1300
1301 def create_page_keys(self):
1302 """Return frame of widgets for Keys tab.
1303
1304 Enable users to provisionally change both individual and sets of
1305 keybindings (shortcut keys). Except for features implemented as
1306 extensions, keybindings are stored in complete sets called
1307 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1308 as far as the dialog is concerned. Any keyset can be used as the
1309 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1310
1311 Function load_key_cfg() initializes tk variables and keyset
1312 lists and calls load_keys_list for the current keyset.
1313 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1314 keyset_source, which controls if the current set of keybindings
1315 are from a builtin or custom keyset. DynOptionMenus builtinlist
1316 and customlist contain lists of the builtin and custom keysets,
1317 respectively, and the current item from each list is stored in
1318 vars builtin_name and custom_name.
1319
1320 Button delete_custom_keys invokes delete_custom_keys() to delete
1321 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1322 save_custom_keys invokes save_as_new_key_set() which calls
1323 get_new_keys_name() and create_new_key_set() to save a custom keyset
1324 and its keybindings to idleConf.userCfg['keys'].
1325
1326 Listbox bindingslist contains all of the keybindings for the
1327 selected keyset. The keybindings are loaded in load_keys_list()
1328 and are pairs of (event, [keys]) where keys can be a list
1329 of one or more key combinations to bind to the same event.
1330 Mouse button 1 click invokes on_bindingslist_select(), which
1331 allows button_new_keys to be clicked.
1332
1333 So, an item is selected in listbindings, which activates
1334 button_new_keys, and clicking button_new_keys calls function
1335 get_new_keys(). Function get_new_keys() gets the key mappings from the
1336 current keyset for the binding event item that was selected. The
1337 function then displays another dialog, GetKeysDialog, with the
Terry Jan Reedy6db2edb2017-08-17 21:02:20 -04001338 selected binding event and current keys and allows new key sequences
Terry Jan Reedyff4b2222017-08-15 19:13:11 -04001339 to be entered for that binding event. If the keys aren't
1340 changed, nothing happens. If the keys are changed and the keyset
1341 is a builtin, function get_new_keys_name() will be called
1342 for input of a custom keyset name. If no name is given, then the
1343 change to the keybinding will abort and no updates will be made. If
1344 a custom name is entered in the prompt or if the current keyset was
1345 already custom (and thus didn't require a prompt), then
1346 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1347 with the change to the event binding. The item listing in bindingslist
1348 is updated with the new keys. Var keybinding is also set which invokes
1349 the callback function, var_changed_keybinding, to add the change to
1350 the 'keys' or 'extensions' changes tracker based on the binding type.
1351
1352 Tk Variables:
1353 keybinding: Action/key bindings.
1354
1355 Methods:
1356 load_keys_list: Reload active set.
1357 create_new_key_set: Combine active keyset and changes.
1358 set_keys_type: Command for keyset_source.
1359 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1360 deactivate_current_config: Remove keys bindings in editors.
1361
1362 Widgets for KeysPage(frame): (*) widgets bound to self
1363 frame_key_sets: LabelFrame
1364 frames[0]: Frame
1365 (*)builtin_keyset_on: Radiobutton - var keyset_source
1366 (*)custom_keyset_on: Radiobutton - var keyset_source
1367 (*)builtinlist: DynOptionMenu - var builtin_name,
1368 func keybinding_selected
1369 (*)customlist: DynOptionMenu - var custom_name,
1370 func keybinding_selected
1371 (*)keys_message: Label
1372 frames[1]: Frame
1373 (*)button_delete_custom_keys: Button - delete_custom_keys
1374 (*)button_save_custom_keys: Button - save_as_new_key_set
1375 frame_custom: LabelFrame
1376 frame_target: Frame
1377 target_title: Label
1378 scroll_target_y: Scrollbar
1379 scroll_target_x: Scrollbar
1380 (*)bindingslist: ListBox - on_bindingslist_select
1381 (*)button_new_keys: Button - get_new_keys & ..._name
1382 """
1383 self.builtin_name = tracers.add(
1384 StringVar(self), self.var_changed_builtin_name)
1385 self.custom_name = tracers.add(
1386 StringVar(self), self.var_changed_custom_name)
1387 self.keyset_source = tracers.add(
1388 BooleanVar(self), self.var_changed_keyset_source)
1389 self.keybinding = tracers.add(
1390 StringVar(self), self.var_changed_keybinding)
1391
1392 # Create widgets:
1393 # body and section frames.
1394 frame_custom = LabelFrame(
1395 self, borderwidth=2, relief=GROOVE,
1396 text=' Custom Key Bindings ')
1397 frame_key_sets = LabelFrame(
1398 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1399 # frame_custom.
1400 frame_target = Frame(frame_custom)
1401 target_title = Label(frame_target, text='Action - Key(s)')
1402 scroll_target_y = Scrollbar(frame_target)
1403 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1404 self.bindingslist = Listbox(
1405 frame_target, takefocus=FALSE, exportselection=FALSE)
1406 self.bindingslist.bind('<ButtonRelease-1>',
1407 self.on_bindingslist_select)
1408 scroll_target_y['command'] = self.bindingslist.yview
1409 scroll_target_x['command'] = self.bindingslist.xview
1410 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1411 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1412 self.button_new_keys = Button(
1413 frame_custom, text='Get New Keys for Selection',
1414 command=self.get_new_keys, state=DISABLED)
1415 # frame_key_sets.
1416 frames = [Frame(frame_key_sets, padx=2, pady=2, borderwidth=0)
1417 for i in range(2)]
1418 self.builtin_keyset_on = Radiobutton(
1419 frames[0], variable=self.keyset_source, value=1,
1420 command=self.set_keys_type, text='Use a Built-in Key Set')
1421 self.custom_keyset_on = Radiobutton(
1422 frames[0], variable=self.keyset_source, value=0,
1423 command=self.set_keys_type, text='Use a Custom Key Set')
1424 self.builtinlist = DynOptionMenu(
1425 frames[0], self.builtin_name, None, command=None)
1426 self.customlist = DynOptionMenu(
1427 frames[0], self.custom_name, None, command=None)
1428 self.button_delete_custom_keys = Button(
1429 frames[1], text='Delete Custom Key Set',
1430 command=self.delete_custom_keys)
1431 self.button_save_custom_keys = Button(
1432 frames[1], text='Save as New Custom Key Set',
1433 command=self.save_as_new_key_set)
1434 self.keys_message = Label(frames[0], bd=2)
1435
1436 # Pack widgets:
1437 # body.
1438 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1439 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1440 # frame_custom.
1441 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1442 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1443 # frame_target.
1444 frame_target.columnconfigure(0, weight=1)
1445 frame_target.rowconfigure(1, weight=1)
1446 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1447 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1448 scroll_target_y.grid(row=1, column=1, sticky=NS)
1449 scroll_target_x.grid(row=2, column=0, sticky=EW)
1450 # frame_key_sets.
1451 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1452 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1453 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1454 self.customlist.grid(row=1, column=1, sticky=NSEW)
1455 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1456 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1457 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1458 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1459 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1460
1461 def load_key_cfg(self):
1462 "Load current configuration settings for the keybinding options."
1463 # Set current keys type radiobutton.
1464 self.keyset_source.set(idleConf.GetOption(
1465 'main', 'Keys', 'default', type='bool', default=1))
1466 # Set current keys.
1467 current_option = idleConf.CurrentKeys()
1468 # Load available keyset option menus.
1469 if self.keyset_source.get(): # Default theme selected.
1470 item_list = idleConf.GetSectionList('default', 'keys')
1471 item_list.sort()
1472 self.builtinlist.SetMenu(item_list, current_option)
1473 item_list = idleConf.GetSectionList('user', 'keys')
1474 item_list.sort()
1475 if not item_list:
1476 self.custom_keyset_on['state'] = DISABLED
1477 self.custom_name.set('- no custom keys -')
1478 else:
1479 self.customlist.SetMenu(item_list, item_list[0])
1480 else: # User key set selected.
1481 item_list = idleConf.GetSectionList('user', 'keys')
1482 item_list.sort()
1483 self.customlist.SetMenu(item_list, current_option)
1484 item_list = idleConf.GetSectionList('default', 'keys')
1485 item_list.sort()
1486 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1487 self.set_keys_type()
1488 # Load keyset element list.
1489 keyset_name = idleConf.CurrentKeys()
1490 self.load_keys_list(keyset_name)
1491
1492 def var_changed_builtin_name(self, *params):
1493 "Process selection of builtin key set."
1494 old_keys = (
1495 'IDLE Classic Windows',
1496 'IDLE Classic Unix',
1497 'IDLE Classic Mac',
1498 'IDLE Classic OSX',
1499 )
1500 value = self.builtin_name.get()
1501 if value not in old_keys:
1502 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1503 changes.add_option('main', 'Keys', 'name', old_keys[0])
1504 changes.add_option('main', 'Keys', 'name2', value)
1505 self.keys_message['text'] = 'New key set, see Help'
1506 self.keys_message['fg'] = '#500000'
1507 else:
1508 changes.add_option('main', 'Keys', 'name', value)
1509 changes.add_option('main', 'Keys', 'name2', '')
1510 self.keys_message['text'] = ''
1511 self.keys_message['fg'] = 'black'
1512 self.load_keys_list(value)
1513
1514 def var_changed_custom_name(self, *params):
1515 "Process selection of custom key set."
1516 value = self.custom_name.get()
1517 if value != '- no custom keys -':
1518 changes.add_option('main', 'Keys', 'name', value)
1519 self.load_keys_list(value)
1520
1521 def var_changed_keyset_source(self, *params):
1522 "Process toggle between builtin key set and custom key set."
1523 value = self.keyset_source.get()
1524 changes.add_option('main', 'Keys', 'default', value)
1525 if value:
1526 self.var_changed_builtin_name()
1527 else:
1528 self.var_changed_custom_name()
1529
1530 def var_changed_keybinding(self, *params):
1531 "Store change to a keybinding."
1532 value = self.keybinding.get()
1533 key_set = self.custom_name.get()
1534 event = self.bindingslist.get(ANCHOR).split()[0]
1535 if idleConf.IsCoreBinding(event):
1536 changes.add_option('keys', key_set, event, value)
1537 else: # Event is an extension binding.
1538 ext_name = idleConf.GetExtnNameForEvent(event)
1539 ext_keybind_section = ext_name + '_cfgBindings'
1540 changes.add_option('extensions', ext_keybind_section, event, value)
1541
1542 def set_keys_type(self):
1543 "Set available screen options based on builtin or custom key set."
1544 if self.keyset_source.get():
1545 self.builtinlist['state'] = NORMAL
1546 self.customlist['state'] = DISABLED
1547 self.button_delete_custom_keys['state'] = DISABLED
1548 else:
1549 self.builtinlist['state'] = DISABLED
1550 self.custom_keyset_on['state'] = NORMAL
1551 self.customlist['state'] = NORMAL
1552 self.button_delete_custom_keys['state'] = NORMAL
1553
1554 def get_new_keys(self):
1555 """Handle event to change key binding for selected line.
1556
1557 A selection of a key/binding in the list of current
1558 bindings pops up a dialog to enter a new binding. If
1559 the current key set is builtin and a binding has
1560 changed, then a name for a custom key set needs to be
1561 entered for the change to be applied.
1562 """
1563 list_index = self.bindingslist.index(ANCHOR)
1564 binding = self.bindingslist.get(list_index)
1565 bind_name = binding.split()[0]
1566 if self.keyset_source.get():
1567 current_key_set_name = self.builtin_name.get()
1568 else:
1569 current_key_set_name = self.custom_name.get()
1570 current_bindings = idleConf.GetCurrentKeySet()
1571 if current_key_set_name in changes['keys']: # unsaved changes
1572 key_set_changes = changes['keys'][current_key_set_name]
1573 for event in key_set_changes:
1574 current_bindings[event] = key_set_changes[event].split()
1575 current_key_sequences = list(current_bindings.values())
1576 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1577 current_key_sequences).result
1578 if new_keys:
1579 if self.keyset_source.get(): # Current key set is a built-in.
1580 message = ('Your changes will be saved as a new Custom Key Set.'
1581 ' Enter a name for your new Custom Key Set below.')
1582 new_keyset = self.get_new_keys_name(message)
1583 if not new_keyset: # User cancelled custom key set creation.
1584 self.bindingslist.select_set(list_index)
1585 self.bindingslist.select_anchor(list_index)
1586 return
1587 else: # Create new custom key set based on previously active key set.
1588 self.create_new_key_set(new_keyset)
1589 self.bindingslist.delete(list_index)
1590 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1591 self.bindingslist.select_set(list_index)
1592 self.bindingslist.select_anchor(list_index)
1593 self.keybinding.set(new_keys)
1594 else:
1595 self.bindingslist.select_set(list_index)
1596 self.bindingslist.select_anchor(list_index)
1597
1598 def get_new_keys_name(self, message):
1599 "Return new key set name from query popup."
1600 used_names = (idleConf.GetSectionList('user', 'keys') +
1601 idleConf.GetSectionList('default', 'keys'))
1602 new_keyset = SectionName(
1603 self, 'New Custom Key Set', message, used_names).result
1604 return new_keyset
1605
1606 def save_as_new_key_set(self):
1607 "Prompt for name of new key set and save changes using that name."
1608 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1609 if new_keys_name:
1610 self.create_new_key_set(new_keys_name)
1611
1612 def on_bindingslist_select(self, event):
1613 "Activate button to assign new keys to selected action."
1614 self.button_new_keys['state'] = NORMAL
1615
1616 def create_new_key_set(self, new_key_set_name):
1617 """Create a new custom key set with the given name.
1618
1619 Copy the bindings/keys from the previously active keyset
1620 to the new keyset and activate the new custom keyset.
1621 """
1622 if self.keyset_source.get():
1623 prev_key_set_name = self.builtin_name.get()
1624 else:
1625 prev_key_set_name = self.custom_name.get()
1626 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1627 new_keys = {}
1628 for event in prev_keys: # Add key set to changed items.
1629 event_name = event[2:-2] # Trim off the angle brackets.
1630 binding = ' '.join(prev_keys[event])
1631 new_keys[event_name] = binding
1632 # Handle any unsaved changes to prev key set.
1633 if prev_key_set_name in changes['keys']:
1634 key_set_changes = changes['keys'][prev_key_set_name]
1635 for event in key_set_changes:
1636 new_keys[event] = key_set_changes[event]
1637 # Save the new key set.
1638 self.save_new_key_set(new_key_set_name, new_keys)
1639 # Change GUI over to the new key set.
1640 custom_key_list = idleConf.GetSectionList('user', 'keys')
1641 custom_key_list.sort()
1642 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1643 self.keyset_source.set(0)
1644 self.set_keys_type()
1645
1646 def load_keys_list(self, keyset_name):
1647 """Reload the list of action/key binding pairs for the active key set.
1648
1649 An action/key binding can be selected to change the key binding.
1650 """
1651 reselect = False
1652 if self.bindingslist.curselection():
1653 reselect = True
1654 list_index = self.bindingslist.index(ANCHOR)
1655 keyset = idleConf.GetKeySet(keyset_name)
1656 bind_names = list(keyset.keys())
1657 bind_names.sort()
1658 self.bindingslist.delete(0, END)
1659 for bind_name in bind_names:
1660 key = ' '.join(keyset[bind_name])
1661 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1662 if keyset_name in changes['keys']:
1663 # Handle any unsaved changes to this key set.
1664 if bind_name in changes['keys'][keyset_name]:
1665 key = changes['keys'][keyset_name][bind_name]
1666 self.bindingslist.insert(END, bind_name+' - '+key)
1667 if reselect:
1668 self.bindingslist.see(list_index)
1669 self.bindingslist.select_set(list_index)
1670 self.bindingslist.select_anchor(list_index)
1671
1672 @staticmethod
1673 def save_new_key_set(keyset_name, keyset):
1674 """Save a newly created core key set.
1675
1676 Add keyset to idleConf.userCfg['keys'], not to disk.
1677 If the keyset doesn't exist, it is created. The
1678 binding/keys are taken from the keyset argument.
1679
1680 keyset_name - string, the name of the new key set
1681 keyset - dictionary containing the new keybindings
1682 """
1683 if not idleConf.userCfg['keys'].has_section(keyset_name):
1684 idleConf.userCfg['keys'].add_section(keyset_name)
1685 for event in keyset:
1686 value = keyset[event]
1687 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1688
1689 def delete_custom_keys(self):
1690 """Handle event to delete a custom key set.
1691
1692 Applying the delete deactivates the current configuration and
1693 reverts to the default. The custom key set is permanently
1694 deleted from the config file.
1695 """
1696 keyset_name = self.custom_name.get()
1697 delmsg = 'Are you sure you wish to delete the key set %r ?'
1698 if not tkMessageBox.askyesno(
1699 'Delete Key Set', delmsg % keyset_name, parent=self):
1700 return
1701 self.cd.deactivate_current_config()
1702 # Remove key set from changes, config, and file.
1703 changes.delete_section('keys', keyset_name)
1704 # Reload user key set list.
1705 item_list = idleConf.GetSectionList('user', 'keys')
1706 item_list.sort()
1707 if not item_list:
1708 self.custom_keyset_on['state'] = DISABLED
1709 self.customlist.SetMenu(item_list, '- no custom keys -')
1710 else:
1711 self.customlist.SetMenu(item_list, item_list[0])
1712 # Revert to default key set.
1713 self.keyset_source.set(idleConf.defaultCfg['main']
1714 .Get('Keys', 'default'))
1715 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1716 or idleConf.default_keys())
1717 # User can't back out of these changes, they must be applied now.
1718 changes.save_all()
1719 self.cd.save_all_changed_extensions()
1720 self.cd.activate_config_changes()
1721 self.set_keys_type()
1722
1723
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -04001724class GenPage(Frame):
1725
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001726 def __init__(self, master):
1727 super().__init__(master)
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -04001728 self.create_page_general()
1729 self.load_general_cfg()
1730
1731 def create_page_general(self):
1732 """Return frame of widgets for General tab.
1733
1734 Enable users to provisionally change general options. Function
1735 load_general_cfg intializes tk variables and helplist using
1736 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1737 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1738 set var autosave. Entry boxes win_width_int and win_height_int
1739 set var win_width and win_height. Setting var_name invokes the
1740 default callback that adds option to changes.
1741
1742 Helplist: load_general_cfg loads list user_helplist with
1743 name, position pairs and copies names to listbox helplist.
1744 Clicking a name invokes help_source selected. Clicking
1745 button_helplist_name invokes helplist_item_name, which also
1746 changes user_helplist. These functions all call
1747 set_add_delete_state. All but load call update_help_changes to
1748 rewrite changes['main']['HelpFiles'].
1749
Terry Jan Reedya3145902017-08-14 21:45:02 -04001750 Widgets for GenPage(Frame): (*) widgets bound to self
1751 frame_run: LabelFrame
1752 startup_title: Label
1753 (*)startup_editor_on: Radiobutton - startup_edit
1754 (*)startup_shell_on: Radiobutton - startup_edit
1755 frame_save: LabelFrame
1756 run_save_title: Label
1757 (*)save_ask_on: Radiobutton - autosave
1758 (*)save_auto_on: Radiobutton - autosave
1759 frame_win_size: LabelFrame
1760 win_size_title: Label
1761 win_width_title: Label
1762 (*)win_width_int: Entry - win_width
1763 win_height_title: Label
1764 (*)win_height_int: Entry - win_height
1765 frame_help: LabelFrame
1766 frame_helplist: Frame
1767 frame_helplist_buttons: Frame
1768 (*)button_helplist_edit
1769 (*)button_helplist_add
1770 (*)button_helplist_remove
1771 (*)helplist: ListBox
1772 scroll_helplist: Scrollbar
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -04001773 """
1774 self.startup_edit = tracers.add(
1775 IntVar(self), ('main', 'General', 'editor-on-startup'))
1776 self.autosave = tracers.add(
1777 IntVar(self), ('main', 'General', 'autosave'))
1778 self.win_width = tracers.add(
1779 StringVar(self), ('main', 'EditorWindow', 'width'))
1780 self.win_height = tracers.add(
1781 StringVar(self), ('main', 'EditorWindow', 'height'))
1782
1783 # Create widgets:
1784 # Section frames.
1785 frame_run = LabelFrame(self, borderwidth=2, relief=GROOVE,
1786 text=' Startup Preferences ')
1787 frame_save = LabelFrame(self, borderwidth=2, relief=GROOVE,
1788 text=' autosave Preferences ')
1789 frame_win_size = Frame(self, borderwidth=2, relief=GROOVE)
1790 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1791 text=' Additional Help Sources ')
1792 # frame_run.
1793 startup_title = Label(frame_run, text='At Startup')
1794 self.startup_editor_on = Radiobutton(
1795 frame_run, variable=self.startup_edit, value=1,
1796 text="Open Edit Window")
1797 self.startup_shell_on = Radiobutton(
1798 frame_run, variable=self.startup_edit, value=0,
1799 text='Open Shell Window')
1800 # frame_save.
1801 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1802 self.save_ask_on = Radiobutton(
1803 frame_save, variable=self.autosave, value=0,
1804 text="Prompt to Save")
1805 self.save_auto_on = Radiobutton(
1806 frame_save, variable=self.autosave, value=1,
1807 text='No Prompt')
1808 # frame_win_size.
1809 win_size_title = Label(
1810 frame_win_size, text='Initial Window Size (in characters)')
1811 win_width_title = Label(frame_win_size, text='Width')
1812 self.win_width_int = Entry(
1813 frame_win_size, textvariable=self.win_width, width=3)
1814 win_height_title = Label(frame_win_size, text='Height')
1815 self.win_height_int = Entry(
1816 frame_win_size, textvariable=self.win_height, width=3)
1817 # frame_help.
1818 frame_helplist = Frame(frame_help)
1819 frame_helplist_buttons = Frame(frame_helplist)
1820 self.helplist = Listbox(
1821 frame_helplist, height=5, takefocus=True,
1822 exportselection=FALSE)
1823 scroll_helplist = Scrollbar(frame_helplist)
1824 scroll_helplist['command'] = self.helplist.yview
1825 self.helplist['yscrollcommand'] = scroll_helplist.set
1826 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1827 self.button_helplist_edit = Button(
1828 frame_helplist_buttons, text='Edit', state=DISABLED,
1829 width=8, command=self.helplist_item_edit)
1830 self.button_helplist_add = Button(
1831 frame_helplist_buttons, text='Add',
1832 width=8, command=self.helplist_item_add)
1833 self.button_helplist_remove = Button(
1834 frame_helplist_buttons, text='Remove', state=DISABLED,
1835 width=8, command=self.helplist_item_remove)
1836
1837 # Pack widgets:
1838 # body.
1839 frame_run.pack(side=TOP, padx=5, pady=5, fill=X)
1840 frame_save.pack(side=TOP, padx=5, pady=5, fill=X)
1841 frame_win_size.pack(side=TOP, padx=5, pady=5, fill=X)
1842 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1843 # frame_run.
1844 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1845 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1846 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1847 # frame_save.
1848 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1849 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1850 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1851 # frame_win_size.
1852 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1853 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1854 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1855 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1856 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
1857 # frame_help.
1858 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1859 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1860 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1861 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1862 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1863 self.button_helplist_add.pack(side=TOP, anchor=W)
1864 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1865
1866 def load_general_cfg(self):
1867 "Load current configuration settings for the general options."
1868 # Set startup state.
1869 self.startup_edit.set(idleConf.GetOption(
1870 'main', 'General', 'editor-on-startup', default=0, type='bool'))
1871 # Set autosave state.
1872 self.autosave.set(idleConf.GetOption(
1873 'main', 'General', 'autosave', default=0, type='bool'))
1874 # Set initial window size.
1875 self.win_width.set(idleConf.GetOption(
1876 'main', 'EditorWindow', 'width', type='int'))
1877 self.win_height.set(idleConf.GetOption(
1878 'main', 'EditorWindow', 'height', type='int'))
1879 # Set additional help sources.
1880 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
1881 self.helplist.delete(0, 'end')
1882 for help_item in self.user_helplist:
1883 self.helplist.insert(END, help_item[0])
1884 self.set_add_delete_state()
1885
1886 def help_source_selected(self, event):
1887 "Handle event for selecting additional help."
1888 self.set_add_delete_state()
1889
1890 def set_add_delete_state(self):
1891 "Toggle the state for the help list buttons based on list entries."
1892 if self.helplist.size() < 1: # No entries in list.
1893 self.button_helplist_edit['state'] = DISABLED
1894 self.button_helplist_remove['state'] = DISABLED
1895 else: # Some entries.
1896 if self.helplist.curselection(): # There currently is a selection.
1897 self.button_helplist_edit['state'] = NORMAL
1898 self.button_helplist_remove['state'] = NORMAL
1899 else: # There currently is not a selection.
1900 self.button_helplist_edit['state'] = DISABLED
1901 self.button_helplist_remove['state'] = DISABLED
1902
1903 def helplist_item_add(self):
1904 """Handle add button for the help list.
1905
1906 Query for name and location of new help sources and add
1907 them to the list.
1908 """
1909 help_source = HelpSource(self, 'New Help Source').result
1910 if help_source:
1911 self.user_helplist.append(help_source)
1912 self.helplist.insert(END, help_source[0])
1913 self.update_help_changes()
1914
1915 def helplist_item_edit(self):
1916 """Handle edit button for the help list.
1917
1918 Query with existing help source information and update
1919 config if the values are changed.
1920 """
1921 item_index = self.helplist.index(ANCHOR)
1922 help_source = self.user_helplist[item_index]
1923 new_help_source = HelpSource(
1924 self, 'Edit Help Source',
1925 menuitem=help_source[0],
1926 filepath=help_source[1],
1927 ).result
1928 if new_help_source and new_help_source != help_source:
1929 self.user_helplist[item_index] = new_help_source
1930 self.helplist.delete(item_index)
1931 self.helplist.insert(item_index, new_help_source[0])
1932 self.update_help_changes()
1933 self.set_add_delete_state() # Selected will be un-selected
1934
1935 def helplist_item_remove(self):
1936 """Handle remove button for the help list.
1937
1938 Delete the help list item from config.
1939 """
1940 item_index = self.helplist.index(ANCHOR)
1941 del(self.user_helplist[item_index])
1942 self.helplist.delete(item_index)
1943 self.update_help_changes()
1944 self.set_add_delete_state()
1945
1946 def update_help_changes(self):
1947 "Clear and rebuild the HelpFiles section in changes"
1948 changes['main']['HelpFiles'] = {}
1949 for num in range(1, len(self.user_helplist) + 1):
1950 changes.add_option(
1951 'main', 'HelpFiles', str(num),
1952 ';'.join(self.user_helplist[num-1][:2]))
1953
1954
Terry Jan Reedy0243bea2017-07-26 20:53:13 -04001955class VarTrace:
1956 """Maintain Tk variables trace state."""
1957
1958 def __init__(self):
1959 """Store Tk variables and callbacks.
1960
1961 untraced: List of tuples (var, callback)
1962 that do not have the callback attached
1963 to the Tk var.
1964 traced: List of tuples (var, callback) where
1965 that callback has been attached to the var.
1966 """
1967 self.untraced = []
1968 self.traced = []
1969
Terry Jan Reedyecc80b32017-07-28 18:36:30 -04001970 def clear(self):
1971 "Clear lists (for tests)."
Terry Jan Reedy9d7d9282017-08-07 15:20:03 -04001972 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedyecc80b32017-07-28 18:36:30 -04001973 self.untraced.clear()
1974 self.traced.clear()
1975
Terry Jan Reedy0243bea2017-07-26 20:53:13 -04001976 def add(self, var, callback):
1977 """Add (var, callback) tuple to untraced list.
1978
1979 Args:
1980 var: Tk variable instance.
Terry Jan Reedy02f88d22017-07-28 15:42:43 -04001981 callback: Either function name to be used as a callback
1982 or a tuple with IdleConf config-type, section, and
1983 option names used in the default callback.
Terry Jan Reedy0243bea2017-07-26 20:53:13 -04001984
1985 Return:
1986 Tk variable instance.
1987 """
1988 if isinstance(callback, tuple):
1989 callback = self.make_callback(var, callback)
1990 self.untraced.append((var, callback))
1991 return var
1992
1993 @staticmethod
1994 def make_callback(var, config):
1995 "Return default callback function to add values to changes instance."
1996 def default_callback(*params):
1997 "Add config values to changes instance."
1998 changes.add_option(*config, var.get())
1999 return default_callback
2000
2001 def attach(self):
2002 "Attach callback to all vars that are not traced."
2003 while self.untraced:
2004 var, callback = self.untraced.pop()
2005 var.trace_add('write', callback)
2006 self.traced.append((var, callback))
2007
2008 def detach(self):
2009 "Remove callback from traced vars."
2010 while self.traced:
2011 var, callback = self.traced.pop()
2012 var.trace_remove('write', var.trace_info()[0][1])
2013 self.untraced.append((var, callback))
2014
2015
Terry Jan Reedy02f88d22017-07-28 15:42:43 -04002016tracers = VarTrace()
2017
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002018help_common = '''\
2019When you click either the Apply or Ok buttons, settings in this
2020dialog that are different from IDLE's default are saved in
2021a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002022these changes apply to all versions of IDLE installed on this
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002023machine. Some do not take affect until IDLE is restarted.
2024[Cancel] only cancels changes made since the last save.
2025'''
2026help_pages = {
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002027 'Highlighting': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002028Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002029The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002030be used with older IDLE releases if it is saved as a custom
2031theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002032''',
2033 'Keys': '''
2034Keys:
2035The IDLE Modern Unix key set is new in June 2016. It can only
2036be used with older IDLE releases if it is saved as a custom
2037key set, with a different name.
2038''',
terryjreedyaf683822017-06-27 23:02:19 -04002039 'Extensions': '''
2040Extensions:
2041
2042Autocomplete: Popupwait is milleseconds to wait after key char, without
2043cursor movement, before popping up completion box. Key char is '.' after
2044identifier or a '/' (or '\\' on Windows) within a string.
2045
2046FormatParagraph: Max-width is max chars in lines after re-formatting.
2047Use with paragraphs in both strings and comment blocks.
2048
2049ParenMatch: Style indicates what is highlighted when closer is entered:
2050'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2051'expression' (default) - also everything in between. Flash-delay is how
2052long to highlight if cursor is not moved (0 means forever).
2053'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002054}
2055
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002056
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002057def is_int(s):
2058 "Return 's is blank or represents an int'"
2059 if not s:
2060 return True
2061 try:
2062 int(s)
2063 return True
2064 except ValueError:
2065 return False
2066
2067
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002068class VerticalScrolledFrame(Frame):
2069 """A pure Tkinter vertically scrollable frame.
2070
2071 * Use the 'interior' attribute to place widgets inside the scrollable frame
2072 * Construct and pack/place/grid normally
2073 * This frame only allows vertical scrolling
2074 """
2075 def __init__(self, parent, *args, **kw):
2076 Frame.__init__(self, parent, *args, **kw)
2077
terryjreedye5bb1122017-07-05 00:54:55 -04002078 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002079 vscrollbar = Scrollbar(self, orient=VERTICAL)
2080 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
2081 canvas = Canvas(self, bd=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002082 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002083 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2084 vscrollbar.config(command=canvas.yview)
2085
terryjreedye5bb1122017-07-05 00:54:55 -04002086 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002087 canvas.xview_moveto(0)
2088 canvas.yview_moveto(0)
2089
terryjreedye5bb1122017-07-05 00:54:55 -04002090 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002091 self.interior = interior = Frame(canvas)
2092 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2093
terryjreedye5bb1122017-07-05 00:54:55 -04002094 # Track changes to the canvas and frame width and sync them,
2095 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002096 def _configure_interior(event):
terryjreedye5bb1122017-07-05 00:54:55 -04002097 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002098 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2099 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002100 interior.bind('<Configure>', _configure_interior)
2101
2102 def _configure_canvas(event):
2103 if interior.winfo_reqwidth() != canvas.winfo_width():
terryjreedye5bb1122017-07-05 00:54:55 -04002104 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002105 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2106 canvas.bind('<Configure>', _configure_canvas)
2107
2108 return
2109
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002110
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002111if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04002112 import unittest
2113 unittest.main('idlelib.idle_test.test_configdialog',
2114 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002115 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002116 run(ConfigDialog)