blob: 587f16ec9df4433b17878e70c5424403568c928e [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"""
csabellabac7d332017-06-26 17:46:26 -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,
Louie Lubb2bae82017-07-10 06:57:18 +080017 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END)
Terry Jan Reedyb331f802017-07-29 00:49:39 -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
terryjreedy349abd92017-07-07 16:00:57 -040023from idlelib.config import idleConf, ConfigChanges
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040024from idlelib.config_key import GetKeysDialog
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040025from idlelib.dynoption import DynOptionMenu
26from idlelib import macosx
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040027from idlelib.query import SectionName, HelpSource
Terry Jan 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
terryjreedy349abd92017-07-07 16:00:57 -040031changes = ConfigChanges()
32
csabella5b591542017-07-28 14:40:59 -040033
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000034class ConfigDialog(Toplevel):
csabella7eb58832017-07-04 21:30:58 -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):
csabella7eb58832017-07-04 21:30:58 -040039 """Show the tabbed dialog for user configuration.
40
csabella36329a42017-07-13 23:32:01 -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 = {}
Louie Lu9b622fb2017-07-14 08:35:48 +080057 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')
csabellabac7d332017-06-26 17:46:26 -040062 x = parent.winfo_rootx() + 20
63 y = parent.winfo_rooty() + (30 if not _htest else 150)
64 self.geometry(f'+{x}+{y}')
csabella7eb58832017-07-04 21:30:58 -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.
csabellabac7d332017-06-26 17:46:26 -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)
csabellabac7d332017-06-26 17:46:26 -040071 self.protocol("WM_DELETE_WINDOW", self.cancel)
csabella9397e2a2017-07-30 13:34:25 -040072 self.fontpage.fontlist.focus_set()
csabella7eb58832017-07-04 21:30:58 -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
csabellabac7d332017-06-26 17:46:26 -040078 self.load_configs()
csabella5b591542017-07-28 14:40:59 -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:
Louie Lu9b622fb2017-07-14 08:35:48 +080083 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
csabella5b591542017-07-28 14:40:59 -040087
csabellabac7d332017-06-26 17:46:26 -040088 def create_widgets(self):
csabella36329a42017-07-13 23:32:01 -040089 """Create and place widgets for tabbed dialog.
90
91 Widgets Bound to self:
csabellae8eb17b2017-07-30 18:39:17 -040092 note: Notebook
93 highpage: self.create_page_highlight
94 fontpage: FontPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -040095 keyspage: KeysPage
csabellae8eb17b2017-07-30 18:39:17 -040096 genpage: GenPage
Cheryl Sabellae36d9f52017-08-15 18:26:23 -040097 extpage: self.create_page_extensions
csabella36329a42017-07-13 23:32:01 -040098
99 Methods:
csabella36329a42017-07-13 23:32:01 -0400100 create_action_buttons
101 load_configs: Load pages except for extensions.
csabella36329a42017-07-13 23:32:01 -0400102 activate_config_changes: Tell editors to reload.
103 """
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400104 self.note = note = Notebook(self, width=450, height=450)
csabella9397e2a2017-07-30 13:34:25 -0400105 self.highpage = self.create_page_highlight()
106 self.fontpage = FontPage(note, self.highpage)
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400107 self.keyspage = KeysPage(note)
csabellae8eb17b2017-07-30 18:39:17 -0400108 self.genpage = GenPage(note)
csabella9397e2a2017-07-30 13:34:25 -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 Reedyb331f802017-07-29 00:49:39 -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 Reedyb1660802017-07-27 18:28:01 -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 """
csabella9397e2a2017-07-30 13:34:25 -0400132 #self.load_font_cfg()
133 #self.load_tab_cfg()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400134 self.load_theme_cfg()
Cheryl Sabellae36d9f52017-08-15 18:26:23 -0400135 # self.load_key_cfg()
csabellae8eb17b2017-07-30 18:39:17 -0400136 # self.load_general_cfg()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400137 # note: extension page handled separately
138
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400139 def create_action_buttons(self):
csabella36329a42017-07-13 23:32:01 -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
csabella7eb58832017-07-04 21:30:58 -0400159 # text in the buttons.
csabellabac7d332017-06-26 17:46:26 -0400160 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000161 else:
csabellabac7d332017-06-26 17:46:26 -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 (
csabellabac7d332017-06-26 17:46:26 -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,
csabellabac7d332017-06-26 17:46:26 -0400171 **padding_args).pack(side=LEFT, padx=5)
csabella7eb58832017-07-04 21:30:58 -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 Reedyb1660802017-07-27 18:28:01 -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 Reedy77e97ca2017-07-24 00:18:25 -0400221
csabellabac7d332017-06-26 17:46:26 -0400222 def create_page_highlight(self):
csabella7eb58832017-07-04 21:30:58 -0400223 """Return frame of widgets for Highlighting tab.
224
Cheryl Sabella82aff622017-08-17 20:39:00 -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
csabella36329a42017-07-13 23:32:01 -0400291 Tk Variables:
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400292 color: Color of selected target.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400293 builtin_name: Menu variable for built-in theme.
294 custom_name: Menu variable for custom theme.
csabella7eb58832017-07-04 21:30:58 -0400295 fg_bg_toggle: Toggle for foreground/background color.
csabella36329a42017-07-13 23:32:01 -0400296 Note: this has no callback.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400297 theme_source: Selector for built-in or custom theme.
csabella7eb58832017-07-04 21:30:58 -0400298 highlight_target: Menu variable for the highlight tag target.
csabella36329a42017-07-13 23:32:01 -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 Reedya54a8f12017-07-21 01:06:58 -0400307 get_color: Invoke colorchooser [button_set_color].
308 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
csabella36329a42017-07-13 23:32:01 -0400309 set_highlight_target: set fg_bg_toggle, set_color_sample().
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400310 set_color_sample: Set frame background to target.
311 on_new_color_set: Set new color and add option.
csabella36329a42017-07-13 23:32:01 -0400312 paint_theme_sample: Recolor sample.
313 get_new_theme_name: Get from popup.
Cheryl Sabella82aff622017-08-17 20:39:00 -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).
csabella36329a42017-07-13 23:32:01 -0400319
Cheryl Sabella2f896462017-08-14 21:21:43 -0400320 Widgets of highlights page frame: (*) widgets bound to self
321 frame_custom: LabelFrame
322 (*)highlight_sample: Text
323 (*)frame_color_set: Frame
Cheryl Sabella82aff622017-08-17 20:39:00 -0400324 (*)button_set_color: Button
325 (*)targetlist: DynOptionMenu - highlight_target
Cheryl Sabella2f896462017-08-14 21:21:43 -0400326 frame_fg_bg_toggle: Frame
Cheryl Sabella82aff622017-08-17 20:39:00 -0400327 (*)fg_on: Radiobutton - fg_bg_toggle
328 (*)bg_on: Radiobutton - fg_bg_toggle
329 (*)button_save_custom: Button
Cheryl Sabella2f896462017-08-14 21:21:43 -0400330 frame_theme: LabelFrame
331 theme_type_title: Label
Cheryl Sabella82aff622017-08-17 20:39:00 -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
csabella7eb58832017-07-04 21:30:58 -0400338 """
csabella36329a42017-07-13 23:32:01 -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
Cheryl Sabella82aff622017-08-17 20:39:00 -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)
csabellabac7d332017-06-26 17:46:26 -0400360 self.fg_bg_toggle = BooleanVar(parent)
csabella5b591542017-07-28 14:40:59 -0400361 self.color = tracers.add(
362 StringVar(parent), self.var_changed_color)
Cheryl Sabella82aff622017-08-17 20:39:00 -0400363 self.theme_source = tracers.add(
364 BooleanVar(parent), self.var_changed_theme_source)
csabella5b591542017-07-28 14:40:59 -0400365 self.highlight_target = tracers.add(
366 StringVar(parent), self.var_changed_highlight_target)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400367
Cheryl Sabella82aff622017-08-17 20:39:00 -0400368 # Create widgets:
369 # body frame and section frames.
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400370 frame = Frame(self.note)
csabellabac7d332017-06-26 17:46:26 -0400371 frame_custom = LabelFrame(frame, borderwidth=2, relief=GROOVE,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400372 text=' Custom Highlighting ')
csabellabac7d332017-06-26 17:46:26 -0400373 frame_theme = LabelFrame(frame, borderwidth=2, relief=GROOVE,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400374 text=' Highlighting Theme ')
Cheryl Sabella82aff622017-08-17 20:39:00 -0400375 # frame_custom.
csabella9397e2a2017-07-30 13:34:25 -0400376 text = self.highlight_sample = frame.highlight_sample = Text(
csabellabac7d332017-06-26 17:46:26 -0400377 frame_custom, relief=SOLID, borderwidth=1,
Terry Jan Reedyb331f802017-07-29 00:49:39 -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 Reedyb331f802017-07-29 00:49:39 -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 Reedyb331f802017-07-29 00:49:39 -0400398 ('stderr', 'stderr'), ('\n\n', 'normal'))
csabellabac7d332017-06-26 17:46:26 -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):
csabellabac7d332017-06-26 17:46:26 -0400403 event.widget.winfo_toplevel().highlight_target.set(elem)
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400404 text.tag_bind(
csabellabac7d332017-06-26 17:46:26 -0400405 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Terry Jan Reedy2bc8f0e2017-07-26 20:54:40 -0400406 text['state'] = DISABLED
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400407 self.frame_color_set = Frame(frame_custom, relief=SOLID, borderwidth=1)
csabellabac7d332017-06-26 17:46:26 -0400408 frame_fg_bg_toggle = Frame(frame_custom)
Cheryl Sabella82aff622017-08-17 20:39:00 -0400409 self.button_set_color = Button(
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400410 self.frame_color_set, text='Choose Color for :',
411 command=self.get_color, highlightthickness=0)
Cheryl Sabella82aff622017-08-17 20:39:00 -0400412 self.targetlist = DynOptionMenu(
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400413 self.frame_color_set, self.highlight_target, None,
csabellabac7d332017-06-26 17:46:26 -0400414 highlightthickness=0) #, command=self.set_highlight_targetBinding
Cheryl Sabella82aff622017-08-17 20:39:00 -0400415 self.fg_on = Radiobutton(
csabellabac7d332017-06-26 17:46:26 -0400416 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400417 text='Foreground', command=self.set_color_sample_binding)
Cheryl Sabella82aff622017-08-17 20:39:00 -0400418 self.bg_on = Radiobutton(
csabellabac7d332017-06-26 17:46:26 -0400419 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400420 text='Background', command=self.set_color_sample_binding)
csabellabac7d332017-06-26 17:46:26 -0400421 self.fg_bg_toggle.set(1)
Cheryl Sabella82aff622017-08-17 20:39:00 -0400422 self.button_save_custom = Button(
csabellabac7d332017-06-26 17:46:26 -0400423 frame_custom, text='Save as New Custom Theme',
424 command=self.save_as_new_theme)
Cheryl Sabella82aff622017-08-17 20:39:00 -0400425 # frame_theme.
csabellabac7d332017-06-26 17:46:26 -0400426 theme_type_title = Label(frame_theme, text='Select : ')
Cheryl Sabella82aff622017-08-17 20:39:00 -0400427 self.builtin_theme_on = Radiobutton(
428 frame_theme, variable=self.theme_source, value=1,
csabellabac7d332017-06-26 17:46:26 -0400429 command=self.set_theme_type, text='a Built-in Theme')
Cheryl Sabella82aff622017-08-17 20:39:00 -0400430 self.custom_theme_on = Radiobutton(
431 frame_theme, variable=self.theme_source, value=0,
csabellabac7d332017-06-26 17:46:26 -0400432 command=self.set_theme_type, text='a Custom Theme')
Cheryl Sabella82aff622017-08-17 20:39:00 -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(
csabellabac7d332017-06-26 17:46:26 -0400438 frame_theme, text='Delete Custom Theme',
Cheryl Sabella82aff622017-08-17 20:39:00 -0400439 command=self.delete_custom)
440 self.theme_message = Label(frame_theme, bd=2)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400441
Cheryl Sabella82aff622017-08-17 20:39:00 -0400442 # Pack widgets:
443 # body.
csabellabac7d332017-06-26 17:46:26 -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)
Cheryl Sabella82aff622017-08-17 20:39:00 -0400446 # frame_custom.
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400447 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
csabellabac7d332017-06-26 17:46:26 -0400448 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
Terry Jan Reedyd0969d62017-07-21 02:20:46 -0400449 self.highlight_sample.pack(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400450 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
Cheryl Sabella82aff622017-08-17 20:39:00 -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.
csabellabac7d332017-06-26 17:46:26 -0400457 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
Cheryl Sabella82aff622017-08-17 20:39:00 -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 Reedyb1660802017-07-27 18:28:01 -0400466 def load_theme_cfg(self):
467 """Load current configuration settings for the theme options.
468
Cheryl Sabella82aff622017-08-17 20:39:00 -0400469 Based on the theme_source toggle, the theme is set as
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400470 either builtin or custom and the initial widget values
471 reflect the current settings from idleConf.
472
473 Attributes updated:
Cheryl Sabella82aff622017-08-17 20:39:00 -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 Reedyb1660802017-07-27 18:28:01 -0400478 custom_theme: Message with additional information.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400479 targetlist: Create menu from self.theme_elements.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400480
481 Methods:
482 set_theme_type
483 paint_theme_sample
484 set_highlight_target
485 """
486 # Set current theme type radiobutton.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400487 self.theme_source.set(idleConf.GetOption(
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400488 'main', 'Theme', 'default', type='bool', default=1))
489 # Set current theme.
490 current_option = idleConf.CurrentTheme()
491 # Load available theme option menus.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400492 if self.theme_source.get(): # Default theme selected.
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400493 item_list = idleConf.GetSectionList('default', 'highlight')
494 item_list.sort()
Cheryl Sabella82aff622017-08-17 20:39:00 -0400495 self.builtinlist.SetMenu(item_list, current_option)
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400496 item_list = idleConf.GetSectionList('user', 'highlight')
497 item_list.sort()
498 if not item_list:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400499 self.custom_theme_on['state'] = DISABLED
500 self.custom_name.set('- no custom themes -')
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400501 else:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400502 self.customlist.SetMenu(item_list, item_list[0])
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400503 else: # User theme selected.
504 item_list = idleConf.GetSectionList('user', 'highlight')
505 item_list.sort()
Cheryl Sabella82aff622017-08-17 20:39:00 -0400506 self.customlist.SetMenu(item_list, current_option)
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400507 item_list = idleConf.GetSectionList('default', 'highlight')
508 item_list.sort()
Cheryl Sabella82aff622017-08-17 20:39:00 -0400509 self.builtinlist.SetMenu(item_list, item_list[0])
Terry Jan Reedyb1660802017-07-27 18:28:01 -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])
Cheryl Sabella82aff622017-08-17 20:39:00 -0400514 self.targetlist.SetMenu(theme_names, theme_names[0])
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400515 self.paint_theme_sample()
516 self.set_highlight_target()
517
Cheryl Sabella82aff622017-08-17 20:39:00 -0400518 def var_changed_builtin_name(self, *params):
Terry Jan Reedyb1660802017-07-27 18:28:01 -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')
Cheryl Sabella82aff622017-08-17 20:39:00 -0400525 value = self.builtin_name.get()
Terry Jan Reedyb1660802017-07-27 18:28:01 -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)
Cheryl Sabella82aff622017-08-17 20:39:00 -0400530 self.theme_message['text'] = 'New theme, see Help'
531 self.theme_message['fg'] = '#500000'
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400532 else:
533 changes.add_option('main', 'Theme', 'name', value)
534 changes.add_option('main', 'Theme', 'name2', '')
Cheryl Sabella82aff622017-08-17 20:39:00 -0400535 self.theme_message['text'] = ''
536 self.theme_message['fg'] = 'black'
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400537 self.paint_theme_sample()
538
Cheryl Sabella82aff622017-08-17 20:39:00 -0400539 def var_changed_custom_name(self, *params):
Terry Jan Reedyb1660802017-07-27 18:28:01 -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 """
Cheryl Sabella82aff622017-08-17 20:39:00 -0400545 value = self.custom_name.get()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400546 if value != '- no custom themes -':
547 changes.add_option('main', 'Theme', 'name', value)
548 self.paint_theme_sample()
549
Cheryl Sabella82aff622017-08-17 20:39:00 -0400550 def var_changed_theme_source(self, *params):
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400551 """Process toggle between builtin and custom theme.
552
553 Update the default toggle value and apply the newly
554 selected theme type.
555 """
Cheryl Sabella82aff622017-08-17 20:39:00 -0400556 value = self.theme_source.get()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400557 changes.add_option('main', 'Theme', 'default', value)
558 if value:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400559 self.var_changed_builtin_name()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400560 else:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400561 self.var_changed_custom_name()
Terry Jan Reedyb1660802017-07-27 18:28:01 -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:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400575 theme_source
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400576
577 Attributes updated:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400578 builtinlist
579 customlist
580 button_delete_custom
581 custom_theme_on
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400582
583 Called from:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400584 handler for builtin_theme_on and custom_theme_on
585 delete_custom
586 create_new
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400587 load_theme_cfg
588 """
Cheryl Sabella82aff622017-08-17 20:39:00 -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 Reedyb1660802017-07-27 18:28:01 -0400593 else:
Cheryl Sabella82aff622017-08-17 20:39:00 -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 Reedyb1660802017-07-27 18:28:01 -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
Cheryl Sabella82aff622017-08-17 20:39:00 -0400608 theme_source
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400609
610 Attributes updated:
611 color
612
613 Methods:
614 get_new_theme_name
Cheryl Sabella82aff622017-08-17 20:39:00 -0400615 create_new
Terry Jan Reedyb1660802017-07-27 18:28:01 -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.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400624 if self.theme_source.get(): # Current theme is a built-in.
Terry Jan Reedyb1660802017-07-27 18:28:01 -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.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400631 self.create_new(new_theme)
Terry Jan Reedyb1660802017-07-27 18:28:01 -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."
Cheryl Sabella82aff622017-08-17 20:39:00 -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 Reedyb1660802017-07-27 18:28:01 -0400641 sample_element = self.theme_elements[self.highlight_target.get()][0]
Cheryl Sabella82aff622017-08-17 20:39:00 -0400642 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
643 theme = self.custom_name.get()
Terry Jan Reedyb1660802017-07-27 18:28:01 -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
Cheryl Sabella82aff622017-08-17 20:39:00 -0400660 create_new
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400661 """
662 new_theme_name = self.get_new_theme_name('New Theme Name:')
663 if new_theme_name:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400664 self.create_new(new_theme_name)
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400665
Cheryl Sabella82aff622017-08-17 20:39:00 -0400666 def create_new(self, new_theme_name):
Terry Jan Reedyb1660802017-07-27 18:28:01 -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:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400674 builtin_name
675 custom_name
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400676
677 Attributes updated:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400678 customlist
679 theme_source
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400680
681 Method:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400682 save_new
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400683 set_theme_type
684 """
Cheryl Sabella82aff622017-08-17 20:39:00 -0400685 if self.theme_source.get():
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400686 theme_type = 'default'
Cheryl Sabella82aff622017-08-17 20:39:00 -0400687 theme_name = self.builtin_name.get()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400688 else:
689 theme_type = 'user'
Cheryl Sabella82aff622017-08-17 20:39:00 -0400690 theme_name = self.custom_name.get()
Terry Jan Reedyb1660802017-07-27 18:28:01 -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.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400698 self.save_new(new_theme_name, new_theme)
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400699 # Change GUI over to the new theme.
700 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
701 custom_theme_list.sort()
Cheryl Sabella82aff622017-08-17 20:39:00 -0400702 self.customlist.SetMenu(custom_theme_list, new_theme_name)
703 self.theme_source.set(0)
Terry Jan Reedyb1660802017-07-27 18:28:01 -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:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400713 fg_on
714 bg_on
Terry Jan Reedyb1660802017-07-27 18:28:01 -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
Cheryl Sabella82aff622017-08-17 20:39:00 -0400725 self.fg_on['state'] = DISABLED
726 self.bg_on['state'] = DISABLED
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400727 self.fg_bg_toggle.set(1)
728 else: # Both fg and bg can be set.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400729 self.fg_on['state'] = NORMAL
730 self.bg_on['state'] = NORMAL
Terry Jan Reedyb1660802017-07-27 18:28:01 -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)
Cheryl Sabella82aff622017-08-17 20:39:00 -0400758 self.frame_color_set['bg'] = color
Terry Jan Reedyb1660802017-07-27 18:28:01 -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
Cheryl Sabella82aff622017-08-17 20:39:00 -0400765 theme_source
766 builtin_name
767 custom_name
Terry Jan Reedyb1660802017-07-27 18:28:01 -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:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400776 var_changed_builtin_name
777 var_changed_custom_name
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400778 load_theme_cfg
779 """
Cheryl Sabella82aff622017-08-17 20:39:00 -0400780 if self.theme_source.get(): # Default theme
781 theme = self.builtin_name.get()
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400782 else: # User theme
Cheryl Sabella82aff622017-08-17 20:39:00 -0400783 theme = self.custom_name.get()
Terry Jan Reedyb1660802017-07-27 18:28:01 -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
Cheryl Sabella82aff622017-08-17 20:39:00 -0400800 def save_new(self, theme_name, theme):
Terry Jan Reedyb1660802017-07-27 18:28:01 -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
Cheryl Sabella82aff622017-08-17 20:39:00 -0400812 def delete_custom(self):
Terry Jan Reedyb1660802017-07-27 18:28:01 -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:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400820 custom_name
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400821
822 Attributes updated:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400823 custom_theme_on
824 customlist
825 theme_source
826 builtin_name
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400827
828 Methods:
829 deactivate_current_config
830 save_all_changed_extensions
831 activate_config_changes
832 set_theme_type
833 """
Cheryl Sabella82aff622017-08-17 20:39:00 -0400834 theme_name = self.custom_name.get()
Terry Jan Reedyb1660802017-07-27 18:28:01 -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:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400846 self.custom_theme_on['state'] = DISABLED
847 self.customlist.SetMenu(item_list, '- no custom themes -')
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400848 else:
Cheryl Sabella82aff622017-08-17 20:39:00 -0400849 self.customlist.SetMenu(item_list, item_list[0])
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400850 # Revert to default theme.
Cheryl Sabella82aff622017-08-17 20:39:00 -0400851 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
852 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
Terry Jan Reedyb1660802017-07-27 18:28:01 -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 Reedyb1660802017-07-27 18:28:01 -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
csabellabac7d332017-06-26 17:46:26 -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.
csabella36329a42017-07-13 23:32:01 -0400901
902 Methods:
Ville Skyttä49b27342017-08-03 09:00:59 +0300903 load_extensions:
csabella36329a42017-07-13 23:32:01 -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 Reedyb331f802017-07-29 00:49:39 -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()
csabella7eb58832017-07-04 21:30:58 -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
csabella7eb58832017-07-04 21:30:58 -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 Reedyb331f802017-07-29 00:49:39 -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
csabella7eb58832017-07-04 21:30:58 -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)
csabella7eb58832017-07-04 21:30:58 -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):
csabella7eb58832017-07-04 21:30:58 -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
csabella7eb58832017-07-04 21:30:58 -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]):
csabella7eb58832017-07-04 21:30:58 -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):
csabella7eb58832017-07-04 21:30:58 -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):
csabella7eb58832017-07-04 21:30:58 -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)
csabella7eb58832017-07-04 21:30:58 -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):
csabella36329a42017-07-13 23:32:01 -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
csabella6f446be2017-08-01 00:24:07 -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
csabella9397e2a2017-07-30 13:34:25 -04001086class FontPage(Frame):
1087
csabella6f446be2017-08-01 00:24:07 -04001088 def __init__(self, master, highpage):
1089 super().__init__(master)
csabella9397e2a2017-07-30 13:34:25 -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
Cheryl Sabella2f896462017-08-14 21:21:43 -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
csabella9397e2a2017-07-30 13:34:25 -04001140 """
csabella6f446be2017-08-01 00:24:07 -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)
csabella9397e2a2017-07-30 13:34:25 -04001144 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
1145
1146 # Create widgets:
1147 # body and body section frames.
csabella9397e2a2017-07-30 13:34:25 -04001148 frame_font = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -04001149 self, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
csabella9397e2a2017-07-30 13:34:25 -04001150 frame_indent = LabelFrame(
csabella6f446be2017-08-01 00:24:07 -04001151 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
csabella9397e2a2017-07-30 13:34:25 -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)
csabella6f446be2017-08-01 00:24:07 -04001171 temp_font = tkFont.Font(self, ('courier', 10, 'normal'))
csabella9397e2a2017-07-30 13:34:25 -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
csabella9397e2a2017-07-30 13:34:25 -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
Cheryl Sabellaa32e4052017-08-18 18:34:55 -04001293class HighPage(Frame):
1294
1295 def __init__(self, master):
1296 super().__init__(master)
1297 self.cd = master.master
1298 self.create_page_highlight()
1299 self.load_theme_cfg()
1300
1301 def create_page_highlight(self):
1302 """Return frame of widgets for Highlighting tab.
1303
1304 Enable users to provisionally change foreground and background
1305 colors applied to textual tags. Color mappings are stored in
1306 complete listings called themes. Built-in themes in
1307 idlelib/config-highlight.def are fixed as far as the dialog is
1308 concerned. Any theme can be used as the base for a new custom
1309 theme, stored in .idlerc/config-highlight.cfg.
1310
1311 Function load_theme_cfg() initializes tk variables and theme
1312 lists and calls paint_theme_sample() and set_highlight_target()
1313 for the current theme. Radiobuttons builtin_theme_on and
1314 custom_theme_on toggle var theme_source, which controls if the
1315 current set of colors are from a builtin or custom theme.
1316 DynOptionMenus builtinlist and customlist contain lists of the
1317 builtin and custom themes, respectively, and the current item
1318 from each list is stored in vars builtin_name and custom_name.
1319
1320 Function paint_theme_sample() applies the colors from the theme
1321 to the tags in text widget highlight_sample and then invokes
1322 set_color_sample(). Function set_highlight_target() sets the state
1323 of the radiobuttons fg_on and bg_on based on the tag and it also
1324 invokes set_color_sample().
1325
1326 Function set_color_sample() sets the background color for the frame
1327 holding the color selector. This provides a larger visual of the
1328 color for the current tag and plane (foreground/background).
1329
1330 Note: set_color_sample() is called from many places and is often
1331 called more than once when a change is made. It is invoked when
1332 foreground or background is selected (radiobuttons), from
1333 paint_theme_sample() (theme is changed or load_cfg is called), and
1334 from set_highlight_target() (target tag is changed or load_cfg called).
1335
1336 Button delete_custom invokes delete_custom() to delete
1337 a custom theme from idleConf.userCfg['highlight'] and changes.
1338 Button save_custom invokes save_as_new_theme() which calls
1339 get_new_theme_name() and create_new() to save a custom theme
1340 and its colors to idleConf.userCfg['highlight'].
1341
1342 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control
1343 if the current selected color for a tag is for the foreground or
1344 background.
1345
1346 DynOptionMenu targetlist contains a readable description of the
1347 tags applied to Python source within IDLE. Selecting one of the
1348 tags from this list populates highlight_target, which has a callback
1349 function set_highlight_target().
1350
1351 Text widget highlight_sample displays a block of text (which is
1352 mock Python code) in which is embedded the defined tags and reflects
1353 the color attributes of the current theme and changes for those tags.
1354 Mouse button 1 allows for selection of a tag and updates
1355 highlight_target with that tag value.
1356
1357 Note: The font in highlight_sample is set through the config in
1358 the fonts tab.
1359
1360 In other words, a tag can be selected either from targetlist or
1361 by clicking on the sample text within highlight_sample. The
1362 plane (foreground/background) is selected via the radiobutton.
1363 Together, these two (tag and plane) control what color is
1364 shown in set_color_sample() for the current theme. Button set_color
1365 invokes get_color() which displays a ColorChooser to change the
1366 color for the selected tag/plane. If a new color is picked,
1367 it will be saved to changes and the highlight_sample and
1368 frame background will be updated.
1369
1370 Tk Variables:
1371 color: Color of selected target.
1372 builtin_name: Menu variable for built-in theme.
1373 custom_name: Menu variable for custom theme.
1374 fg_bg_toggle: Toggle for foreground/background color.
1375 Note: this has no callback.
1376 theme_source: Selector for built-in or custom theme.
1377 highlight_target: Menu variable for the highlight tag target.
1378
1379 Instance Data Attributes:
1380 theme_elements: Dictionary of tags for text highlighting.
1381 The key is the display name and the value is a tuple of
1382 (tag name, display sort order).
1383
1384 Methods [attachment]:
1385 load_theme_cfg: Load current highlight colors.
1386 get_color: Invoke colorchooser [button_set_color].
1387 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
1388 set_highlight_target: set fg_bg_toggle, set_color_sample().
1389 set_color_sample: Set frame background to target.
1390 on_new_color_set: Set new color and add option.
1391 paint_theme_sample: Recolor sample.
1392 get_new_theme_name: Get from popup.
1393 create_new: Combine theme with changes and save.
1394 save_as_new_theme: Save [button_save_custom].
1395 set_theme_type: Command for [theme_source].
1396 delete_custom: Activate default [button_delete_custom].
1397 save_new: Save to userCfg['theme'] (is function).
1398
1399 Widgets of highlights page frame: (*) widgets bound to self
1400 frame_custom: LabelFrame
1401 (*)highlight_sample: Text
1402 (*)frame_color_set: Frame
1403 (*)button_set_color: Button
1404 (*)targetlist: DynOptionMenu - highlight_target
1405 frame_fg_bg_toggle: Frame
1406 (*)fg_on: Radiobutton - fg_bg_toggle
1407 (*)bg_on: Radiobutton - fg_bg_toggle
1408 (*)button_save_custom: Button
1409 frame_theme: LabelFrame
1410 theme_type_title: Label
1411 (*)builtin_theme_on: Radiobutton - theme_source
1412 (*)custom_theme_on: Radiobutton - theme_source
1413 (*)builtinlist: DynOptionMenu - builtin_name
1414 (*)customlist: DynOptionMenu - custom_name
1415 (*)button_delete_custom: Button
1416 (*)theme_message: Label
1417 """
1418 self.theme_elements = {
1419 'Normal Text': ('normal', '00'),
1420 'Python Keywords': ('keyword', '01'),
1421 'Python Definitions': ('definition', '02'),
1422 'Python Builtins': ('builtin', '03'),
1423 'Python Comments': ('comment', '04'),
1424 'Python Strings': ('string', '05'),
1425 'Selected Text': ('hilite', '06'),
1426 'Found Text': ('hit', '07'),
1427 'Cursor': ('cursor', '08'),
1428 'Editor Breakpoint': ('break', '09'),
1429 'Shell Normal Text': ('console', '10'),
1430 'Shell Error Text': ('error', '11'),
1431 'Shell Stdout Text': ('stdout', '12'),
1432 'Shell Stderr Text': ('stderr', '13'),
1433 }
1434 self.builtin_name = tracers.add(
1435 StringVar(self), self.var_changed_builtin_name)
1436 self.custom_name = tracers.add(
1437 StringVar(self), self.var_changed_custom_name)
1438 self.fg_bg_toggle = BooleanVar(self)
1439 self.color = tracers.add(
1440 StringVar(self), self.var_changed_color)
1441 self.theme_source = tracers.add(
1442 BooleanVar(self), self.var_changed_theme_source)
1443 self.highlight_target = tracers.add(
1444 StringVar(self), self.var_changed_highlight_target)
1445
1446 # Create widgets:
1447 # body frame and section frames.
1448 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
1449 text=' Custom Highlighting ')
1450 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
1451 text=' Highlighting Theme ')
1452 # frame_custom.
1453 text = self.highlight_sample = Text(
1454 frame_custom, relief=SOLID, borderwidth=1,
1455 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
1456 takefocus=FALSE, highlightthickness=0, wrap=NONE)
1457 text.bind('<Double-Button-1>', lambda e: 'break')
1458 text.bind('<B1-Motion>', lambda e: 'break')
1459 text_and_tags=(('\n', 'normal'),
1460 ('#you can click here', 'comment'), ('\n', 'normal'),
1461 ('#to choose items', 'comment'), ('\n', 'normal'),
1462 ('def', 'keyword'), (' ', 'normal'),
1463 ('func', 'definition'), ('(param):\n ', 'normal'),
1464 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
1465 ("'string'", 'string'), ('\n var1 = ', 'normal'),
1466 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
1467 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
1468 ('list', 'builtin'), ('(', 'normal'),
1469 ('None', 'keyword'), (')\n', 'normal'),
1470 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
1471 (' error ', 'error'), (' ', 'normal'),
1472 ('cursor |', 'cursor'), ('\n ', 'normal'),
1473 ('shell', 'console'), (' ', 'normal'),
1474 ('stdout', 'stdout'), (' ', 'normal'),
1475 ('stderr', 'stderr'), ('\n\n', 'normal'))
1476 for texttag in text_and_tags:
1477 text.insert(END, texttag[0], texttag[1])
1478 for element in self.theme_elements:
1479 def tem(event, elem=element):
1480 event.widget.winfo_toplevel().highlight_target.set(elem)
1481 text.tag_bind(
1482 self.theme_elements[element][0], '<ButtonPress-1>', tem)
1483 text['state'] = DISABLED
1484 self.frame_color_set = Frame(frame_custom, relief=SOLID, borderwidth=1)
1485 frame_fg_bg_toggle = Frame(frame_custom)
1486 self.button_set_color = Button(
1487 self.frame_color_set, text='Choose Color for :',
1488 command=self.get_color, highlightthickness=0)
1489 self.targetlist = DynOptionMenu(
1490 self.frame_color_set, self.highlight_target, None,
1491 highlightthickness=0) #, command=self.set_highlight_targetBinding
1492 self.fg_on = Radiobutton(
1493 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
1494 text='Foreground', command=self.set_color_sample_binding)
1495 self.bg_on = Radiobutton(
1496 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
1497 text='Background', command=self.set_color_sample_binding)
1498 self.fg_bg_toggle.set(1)
1499 self.button_save_custom = Button(
1500 frame_custom, text='Save as New Custom Theme',
1501 command=self.save_as_new_theme)
1502 # frame_theme.
1503 theme_type_title = Label(frame_theme, text='Select : ')
1504 self.builtin_theme_on = Radiobutton(
1505 frame_theme, variable=self.theme_source, value=1,
1506 command=self.set_theme_type, text='a Built-in Theme')
1507 self.custom_theme_on = Radiobutton(
1508 frame_theme, variable=self.theme_source, value=0,
1509 command=self.set_theme_type, text='a Custom Theme')
1510 self.builtinlist = DynOptionMenu(
1511 frame_theme, self.builtin_name, None, command=None)
1512 self.customlist = DynOptionMenu(
1513 frame_theme, self.custom_name, None, command=None)
1514 self.button_delete_custom = Button(
1515 frame_theme, text='Delete Custom Theme',
1516 command=self.delete_custom)
1517 self.theme_message = Label(frame_theme, bd=2)
1518
1519 # Pack widgets:
1520 # body.
1521 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1522 frame_theme.pack(side=LEFT, padx=5, pady=5, fill=Y)
1523 # frame_custom.
1524 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
1525 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
1526 self.highlight_sample.pack(
1527 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1528 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
1529 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
1530 self.fg_on.pack(side=LEFT, anchor=E)
1531 self.bg_on.pack(side=RIGHT, anchor=W)
1532 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1533 # frame_theme.
1534 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
1535 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5)
1536 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2)
1537 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5)
1538 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
1539 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5)
1540 self.theme_message.pack(side=TOP, fill=X, pady=5)
1541
1542 def load_theme_cfg(self):
1543 """Load current configuration settings for the theme options.
1544
1545 Based on the theme_source toggle, the theme is set as
1546 either builtin or custom and the initial widget values
1547 reflect the current settings from idleConf.
1548
1549 Attributes updated:
1550 theme_source: Set from idleConf.
1551 builtinlist: List of default themes from idleConf.
1552 customlist: List of custom themes from idleConf.
1553 custom_theme_on: Disabled if there are no custom themes.
1554 custom_theme: Message with additional information.
1555 targetlist: Create menu from self.theme_elements.
1556
1557 Methods:
1558 set_theme_type
1559 paint_theme_sample
1560 set_highlight_target
1561 """
1562 # Set current theme type radiobutton.
1563 self.theme_source.set(idleConf.GetOption(
1564 'main', 'Theme', 'default', type='bool', default=1))
1565 # Set current theme.
1566 current_option = idleConf.CurrentTheme()
1567 # Load available theme option menus.
1568 if self.theme_source.get(): # Default theme selected.
1569 item_list = idleConf.GetSectionList('default', 'highlight')
1570 item_list.sort()
1571 self.builtinlist.SetMenu(item_list, current_option)
1572 item_list = idleConf.GetSectionList('user', 'highlight')
1573 item_list.sort()
1574 if not item_list:
1575 self.custom_theme_on['state'] = DISABLED
1576 self.custom_name.set('- no custom themes -')
1577 else:
1578 self.customlist.SetMenu(item_list, item_list[0])
1579 else: # User theme selected.
1580 item_list = idleConf.GetSectionList('user', 'highlight')
1581 item_list.sort()
1582 self.customlist.SetMenu(item_list, current_option)
1583 item_list = idleConf.GetSectionList('default', 'highlight')
1584 item_list.sort()
1585 self.builtinlist.SetMenu(item_list, item_list[0])
1586 self.set_theme_type()
1587 # Load theme element option menu.
1588 theme_names = list(self.theme_elements.keys())
1589 theme_names.sort(key=lambda x: self.theme_elements[x][1])
1590 self.targetlist.SetMenu(theme_names, theme_names[0])
1591 self.paint_theme_sample()
1592 self.set_highlight_target()
1593
1594 def var_changed_builtin_name(self, *params):
1595 """Process new builtin theme selection.
1596
1597 Add the changed theme's name to the changed_items and recreate
1598 the sample with the values from the selected theme.
1599 """
1600 old_themes = ('IDLE Classic', 'IDLE New')
1601 value = self.builtin_name.get()
1602 if value not in old_themes:
1603 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
1604 changes.add_option('main', 'Theme', 'name', old_themes[0])
1605 changes.add_option('main', 'Theme', 'name2', value)
1606 self.theme_message['text'] = 'New theme, see Help'
1607 self.theme_message['fg'] = '#500000'
1608 else:
1609 changes.add_option('main', 'Theme', 'name', value)
1610 changes.add_option('main', 'Theme', 'name2', '')
1611 self.theme_message['text'] = ''
1612 self.theme_message['fg'] = 'black'
1613 self.paint_theme_sample()
1614
1615 def var_changed_custom_name(self, *params):
1616 """Process new custom theme selection.
1617
1618 If a new custom theme is selected, add the name to the
1619 changed_items and apply the theme to the sample.
1620 """
1621 value = self.custom_name.get()
1622 if value != '- no custom themes -':
1623 changes.add_option('main', 'Theme', 'name', value)
1624 self.paint_theme_sample()
1625
1626 def var_changed_theme_source(self, *params):
1627 """Process toggle between builtin and custom theme.
1628
1629 Update the default toggle value and apply the newly
1630 selected theme type.
1631 """
1632 value = self.theme_source.get()
1633 changes.add_option('main', 'Theme', 'default', value)
1634 if value:
1635 self.var_changed_builtin_name()
1636 else:
1637 self.var_changed_custom_name()
1638
1639 def var_changed_color(self, *params):
1640 "Process change to color choice."
1641 self.on_new_color_set()
1642
1643 def var_changed_highlight_target(self, *params):
1644 "Process selection of new target tag for highlighting."
1645 self.set_highlight_target()
1646
1647 def set_theme_type(self):
1648 """Set available screen options based on builtin or custom theme.
1649
1650 Attributes accessed:
1651 theme_source
1652
1653 Attributes updated:
1654 builtinlist
1655 customlist
1656 button_delete_custom
1657 custom_theme_on
1658
1659 Called from:
1660 handler for builtin_theme_on and custom_theme_on
1661 delete_custom
1662 create_new
1663 load_theme_cfg
1664 """
1665 if self.theme_source.get():
1666 self.builtinlist['state'] = NORMAL
1667 self.customlist['state'] = DISABLED
1668 self.button_delete_custom['state'] = DISABLED
1669 else:
1670 self.builtinlist['state'] = DISABLED
1671 self.custom_theme_on['state'] = NORMAL
1672 self.customlist['state'] = NORMAL
1673 self.button_delete_custom['state'] = NORMAL
1674
1675 def get_color(self):
1676 """Handle button to select a new color for the target tag.
1677
1678 If a new color is selected while using a builtin theme, a
1679 name must be supplied to create a custom theme.
1680
1681 Attributes accessed:
1682 highlight_target
1683 frame_color_set
1684 theme_source
1685
1686 Attributes updated:
1687 color
1688
1689 Methods:
1690 get_new_theme_name
1691 create_new
1692 """
1693 target = self.highlight_target.get()
1694 prev_color = self.frame_color_set.cget('bg')
1695 rgbTuplet, color_string = tkColorChooser.askcolor(
1696 parent=self, title='Pick new color for : '+target,
1697 initialcolor=prev_color)
1698 if color_string and (color_string != prev_color):
1699 # User didn't cancel and they chose a new color.
1700 if self.theme_source.get(): # Current theme is a built-in.
1701 message = ('Your changes will be saved as a new Custom Theme. '
1702 'Enter a name for your new Custom Theme below.')
1703 new_theme = self.get_new_theme_name(message)
1704 if not new_theme: # User cancelled custom theme creation.
1705 return
1706 else: # Create new custom theme based on previously active theme.
1707 self.create_new(new_theme)
1708 self.color.set(color_string)
1709 else: # Current theme is user defined.
1710 self.color.set(color_string)
1711
1712 def on_new_color_set(self):
1713 "Display sample of new color selection on the dialog."
1714 new_color = self.color.get()
1715 self.frame_color_set['bg'] = new_color # Set sample.
1716 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1717 sample_element = self.theme_elements[self.highlight_target.get()][0]
1718 self.highlight_sample.tag_config(sample_element, **{plane: new_color})
1719 theme = self.custom_name.get()
1720 theme_element = sample_element + '-' + plane
1721 changes.add_option('highlight', theme, theme_element, new_color)
1722
1723 def get_new_theme_name(self, message):
1724 "Return name of new theme from query popup."
1725 used_names = (idleConf.GetSectionList('user', 'highlight') +
1726 idleConf.GetSectionList('default', 'highlight'))
1727 new_theme = SectionName(
1728 self, 'New Custom Theme', message, used_names).result
1729 return new_theme
1730
1731 def save_as_new_theme(self):
1732 """Prompt for new theme name and create the theme.
1733
1734 Methods:
1735 get_new_theme_name
1736 create_new
1737 """
1738 new_theme_name = self.get_new_theme_name('New Theme Name:')
1739 if new_theme_name:
1740 self.create_new(new_theme_name)
1741
1742 def create_new(self, new_theme_name):
1743 """Create a new custom theme with the given name.
1744
1745 Create the new theme based on the previously active theme
1746 with the current changes applied. Once it is saved, then
1747 activate the new theme.
1748
1749 Attributes accessed:
1750 builtin_name
1751 custom_name
1752
1753 Attributes updated:
1754 customlist
1755 theme_source
1756
1757 Method:
1758 save_new
1759 set_theme_type
1760 """
1761 if self.theme_source.get():
1762 theme_type = 'default'
1763 theme_name = self.builtin_name.get()
1764 else:
1765 theme_type = 'user'
1766 theme_name = self.custom_name.get()
1767 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
1768 # Apply any of the old theme's unsaved changes to the new theme.
1769 if theme_name in changes['highlight']:
1770 theme_changes = changes['highlight'][theme_name]
1771 for element in theme_changes:
1772 new_theme[element] = theme_changes[element]
1773 # Save the new theme.
1774 self.save_new(new_theme_name, new_theme)
1775 # Change GUI over to the new theme.
1776 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
1777 custom_theme_list.sort()
1778 self.customlist.SetMenu(custom_theme_list, new_theme_name)
1779 self.theme_source.set(0)
1780 self.set_theme_type()
1781
1782 def set_highlight_target(self):
1783 """Set fg/bg toggle and color based on highlight tag target.
1784
1785 Instance variables accessed:
1786 highlight_target
1787
1788 Attributes updated:
1789 fg_on
1790 bg_on
1791 fg_bg_toggle
1792
1793 Methods:
1794 set_color_sample
1795
1796 Called from:
1797 var_changed_highlight_target
1798 load_theme_cfg
1799 """
1800 if self.highlight_target.get() == 'Cursor': # bg not possible
1801 self.fg_on['state'] = DISABLED
1802 self.bg_on['state'] = DISABLED
1803 self.fg_bg_toggle.set(1)
1804 else: # Both fg and bg can be set.
1805 self.fg_on['state'] = NORMAL
1806 self.bg_on['state'] = NORMAL
1807 self.fg_bg_toggle.set(1)
1808 self.set_color_sample()
1809
1810 def set_color_sample_binding(self, *args):
1811 """Change color sample based on foreground/background toggle.
1812
1813 Methods:
1814 set_color_sample
1815 """
1816 self.set_color_sample()
1817
1818 def set_color_sample(self):
1819 """Set the color of the frame background to reflect the selected target.
1820
1821 Instance variables accessed:
1822 theme_elements
1823 highlight_target
1824 fg_bg_toggle
1825 highlight_sample
1826
1827 Attributes updated:
1828 frame_color_set
1829 """
1830 # Set the color sample area.
1831 tag = self.theme_elements[self.highlight_target.get()][0]
1832 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
1833 color = self.highlight_sample.tag_cget(tag, plane)
1834 self.frame_color_set['bg'] = color
1835
1836 def paint_theme_sample(self):
1837 """Apply the theme colors to each element tag in the sample text.
1838
1839 Instance attributes accessed:
1840 theme_elements
1841 theme_source
1842 builtin_name
1843 custom_name
1844
1845 Attributes updated:
1846 highlight_sample: Set the tag elements to the theme.
1847
1848 Methods:
1849 set_color_sample
1850
1851 Called from:
1852 var_changed_builtin_name
1853 var_changed_custom_name
1854 load_theme_cfg
1855 """
1856 if self.theme_source.get(): # Default theme
1857 theme = self.builtin_name.get()
1858 else: # User theme
1859 theme = self.custom_name.get()
1860 for element_title in self.theme_elements:
1861 element = self.theme_elements[element_title][0]
1862 colors = idleConf.GetHighlight(theme, element)
1863 if element == 'cursor': # Cursor sample needs special painting.
1864 colors['background'] = idleConf.GetHighlight(
1865 theme, 'normal', fgBg='bg')
1866 # Handle any unsaved changes to this theme.
1867 if theme in changes['highlight']:
1868 theme_dict = changes['highlight'][theme]
1869 if element + '-foreground' in theme_dict:
1870 colors['foreground'] = theme_dict[element + '-foreground']
1871 if element + '-background' in theme_dict:
1872 colors['background'] = theme_dict[element + '-background']
1873 self.highlight_sample.tag_config(element, **colors)
1874 self.set_color_sample()
1875
1876 def save_new(self, theme_name, theme):
1877 """Save a newly created theme to idleConf.
1878
1879 theme_name - string, the name of the new theme
1880 theme - dictionary containing the new theme
1881 """
1882 if not idleConf.userCfg['highlight'].has_section(theme_name):
1883 idleConf.userCfg['highlight'].add_section(theme_name)
1884 for element in theme:
1885 value = theme[element]
1886 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
1887
1888 def delete_custom(self):
1889 """Handle event to delete custom theme.
1890
1891 The current theme is deactivated and the default theme is
1892 activated. The custom theme is permanently removed from
1893 the config file.
1894
1895 Attributes accessed:
1896 custom_name
1897
1898 Attributes updated:
1899 custom_theme_on
1900 customlist
1901 theme_source
1902 builtin_name
1903
1904 Methods:
1905 deactivate_current_config
1906 save_all_changed_extensions
1907 activate_config_changes
1908 set_theme_type
1909 """
1910 theme_name = self.custom_name.get()
1911 delmsg = 'Are you sure you wish to delete the theme %r ?'
1912 if not tkMessageBox.askyesno(
1913 'Delete Theme', delmsg % theme_name, parent=self):
1914 return
1915 cd.deactivate_current_config()
1916 # Remove theme from changes, config, and file.
1917 changes.delete_section('highlight', theme_name)
1918 # Reload user theme list.
1919 item_list = idleConf.GetSectionList('user', 'highlight')
1920 item_list.sort()
1921 if not item_list:
1922 self.custom_theme_on['state'] = DISABLED
1923 self.customlist.SetMenu(item_list, '- no custom themes -')
1924 else:
1925 self.customlist.SetMenu(item_list, item_list[0])
1926 # Revert to default theme.
1927 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
1928 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
1929 # User can't back out of these changes, they must be applied now.
1930 changes.save_all()
1931 cd.save_all_changed_extensions()
1932 cd.activate_config_changes()
1933 self.set_theme_type()
1934
1935
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001936class KeysPage(Frame):
1937
1938 def __init__(self, master):
1939 super().__init__(master)
1940 self.cd = master.master
1941 self.create_page_keys()
1942 self.load_key_cfg()
1943
1944 def create_page_keys(self):
1945 """Return frame of widgets for Keys tab.
1946
1947 Enable users to provisionally change both individual and sets of
1948 keybindings (shortcut keys). Except for features implemented as
1949 extensions, keybindings are stored in complete sets called
1950 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1951 as far as the dialog is concerned. Any keyset can be used as the
1952 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1953
1954 Function load_key_cfg() initializes tk variables and keyset
1955 lists and calls load_keys_list for the current keyset.
1956 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1957 keyset_source, which controls if the current set of keybindings
1958 are from a builtin or custom keyset. DynOptionMenus builtinlist
1959 and customlist contain lists of the builtin and custom keysets,
1960 respectively, and the current item from each list is stored in
1961 vars builtin_name and custom_name.
1962
1963 Button delete_custom_keys invokes delete_custom_keys() to delete
1964 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1965 save_custom_keys invokes save_as_new_key_set() which calls
1966 get_new_keys_name() and create_new_key_set() to save a custom keyset
1967 and its keybindings to idleConf.userCfg['keys'].
1968
1969 Listbox bindingslist contains all of the keybindings for the
1970 selected keyset. The keybindings are loaded in load_keys_list()
1971 and are pairs of (event, [keys]) where keys can be a list
1972 of one or more key combinations to bind to the same event.
1973 Mouse button 1 click invokes on_bindingslist_select(), which
1974 allows button_new_keys to be clicked.
1975
1976 So, an item is selected in listbindings, which activates
1977 button_new_keys, and clicking button_new_keys calls function
1978 get_new_keys(). Function get_new_keys() gets the key mappings from the
1979 current keyset for the binding event item that was selected. The
1980 function then displays another dialog, GetKeysDialog, with the
Cheryl Sabella82aff622017-08-17 20:39:00 -04001981 selected binding event and current keys and allows new key sequences
Cheryl Sabellae36d9f52017-08-15 18:26:23 -04001982 to be entered for that binding event. If the keys aren't
1983 changed, nothing happens. If the keys are changed and the keyset
1984 is a builtin, function get_new_keys_name() will be called
1985 for input of a custom keyset name. If no name is given, then the
1986 change to the keybinding will abort and no updates will be made. If
1987 a custom name is entered in the prompt or if the current keyset was
1988 already custom (and thus didn't require a prompt), then
1989 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1990 with the change to the event binding. The item listing in bindingslist
1991 is updated with the new keys. Var keybinding is also set which invokes
1992 the callback function, var_changed_keybinding, to add the change to
1993 the 'keys' or 'extensions' changes tracker based on the binding type.
1994
1995 Tk Variables:
1996 keybinding: Action/key bindings.
1997
1998 Methods:
1999 load_keys_list: Reload active set.
2000 create_new_key_set: Combine active keyset and changes.
2001 set_keys_type: Command for keyset_source.
2002 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
2003 deactivate_current_config: Remove keys bindings in editors.
2004
2005 Widgets for KeysPage(frame): (*) widgets bound to self
2006 frame_key_sets: LabelFrame
2007 frames[0]: Frame
2008 (*)builtin_keyset_on: Radiobutton - var keyset_source
2009 (*)custom_keyset_on: Radiobutton - var keyset_source
2010 (*)builtinlist: DynOptionMenu - var builtin_name,
2011 func keybinding_selected
2012 (*)customlist: DynOptionMenu - var custom_name,
2013 func keybinding_selected
2014 (*)keys_message: Label
2015 frames[1]: Frame
2016 (*)button_delete_custom_keys: Button - delete_custom_keys
2017 (*)button_save_custom_keys: Button - save_as_new_key_set
2018 frame_custom: LabelFrame
2019 frame_target: Frame
2020 target_title: Label
2021 scroll_target_y: Scrollbar
2022 scroll_target_x: Scrollbar
2023 (*)bindingslist: ListBox - on_bindingslist_select
2024 (*)button_new_keys: Button - get_new_keys & ..._name
2025 """
2026 self.builtin_name = tracers.add(
2027 StringVar(self), self.var_changed_builtin_name)
2028 self.custom_name = tracers.add(
2029 StringVar(self), self.var_changed_custom_name)
2030 self.keyset_source = tracers.add(
2031 BooleanVar(self), self.var_changed_keyset_source)
2032 self.keybinding = tracers.add(
2033 StringVar(self), self.var_changed_keybinding)
2034
2035 # Create widgets:
2036 # body and section frames.
2037 frame_custom = LabelFrame(
2038 self, borderwidth=2, relief=GROOVE,
2039 text=' Custom Key Bindings ')
2040 frame_key_sets = LabelFrame(
2041 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
2042 # frame_custom.
2043 frame_target = Frame(frame_custom)
2044 target_title = Label(frame_target, text='Action - Key(s)')
2045 scroll_target_y = Scrollbar(frame_target)
2046 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
2047 self.bindingslist = Listbox(
2048 frame_target, takefocus=FALSE, exportselection=FALSE)
2049 self.bindingslist.bind('<ButtonRelease-1>',
2050 self.on_bindingslist_select)
2051 scroll_target_y['command'] = self.bindingslist.yview
2052 scroll_target_x['command'] = self.bindingslist.xview
2053 self.bindingslist['yscrollcommand'] = scroll_target_y.set
2054 self.bindingslist['xscrollcommand'] = scroll_target_x.set
2055 self.button_new_keys = Button(
2056 frame_custom, text='Get New Keys for Selection',
2057 command=self.get_new_keys, state=DISABLED)
2058 # frame_key_sets.
2059 frames = [Frame(frame_key_sets, padx=2, pady=2, borderwidth=0)
2060 for i in range(2)]
2061 self.builtin_keyset_on = Radiobutton(
2062 frames[0], variable=self.keyset_source, value=1,
2063 command=self.set_keys_type, text='Use a Built-in Key Set')
2064 self.custom_keyset_on = Radiobutton(
2065 frames[0], variable=self.keyset_source, value=0,
2066 command=self.set_keys_type, text='Use a Custom Key Set')
2067 self.builtinlist = DynOptionMenu(
2068 frames[0], self.builtin_name, None, command=None)
2069 self.customlist = DynOptionMenu(
2070 frames[0], self.custom_name, None, command=None)
2071 self.button_delete_custom_keys = Button(
2072 frames[1], text='Delete Custom Key Set',
2073 command=self.delete_custom_keys)
2074 self.button_save_custom_keys = Button(
2075 frames[1], text='Save as New Custom Key Set',
2076 command=self.save_as_new_key_set)
2077 self.keys_message = Label(frames[0], bd=2)
2078
2079 # Pack widgets:
2080 # body.
2081 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
2082 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
2083 # frame_custom.
2084 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
2085 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
2086 # frame_target.
2087 frame_target.columnconfigure(0, weight=1)
2088 frame_target.rowconfigure(1, weight=1)
2089 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
2090 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
2091 scroll_target_y.grid(row=1, column=1, sticky=NS)
2092 scroll_target_x.grid(row=2, column=0, sticky=EW)
2093 # frame_key_sets.
2094 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
2095 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
2096 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
2097 self.customlist.grid(row=1, column=1, sticky=NSEW)
2098 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
2099 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
2100 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
2101 frames[0].pack(side=TOP, fill=BOTH, expand=True)
2102 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
2103
2104 def load_key_cfg(self):
2105 "Load current configuration settings for the keybinding options."
2106 # Set current keys type radiobutton.
2107 self.keyset_source.set(idleConf.GetOption(
2108 'main', 'Keys', 'default', type='bool', default=1))
2109 # Set current keys.
2110 current_option = idleConf.CurrentKeys()
2111 # Load available keyset option menus.
2112 if self.keyset_source.get(): # Default theme selected.
2113 item_list = idleConf.GetSectionList('default', 'keys')
2114 item_list.sort()
2115 self.builtinlist.SetMenu(item_list, current_option)
2116 item_list = idleConf.GetSectionList('user', 'keys')
2117 item_list.sort()
2118 if not item_list:
2119 self.custom_keyset_on['state'] = DISABLED
2120 self.custom_name.set('- no custom keys -')
2121 else:
2122 self.customlist.SetMenu(item_list, item_list[0])
2123 else: # User key set selected.
2124 item_list = idleConf.GetSectionList('user', 'keys')
2125 item_list.sort()
2126 self.customlist.SetMenu(item_list, current_option)
2127 item_list = idleConf.GetSectionList('default', 'keys')
2128 item_list.sort()
2129 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
2130 self.set_keys_type()
2131 # Load keyset element list.
2132 keyset_name = idleConf.CurrentKeys()
2133 self.load_keys_list(keyset_name)
2134
2135 def var_changed_builtin_name(self, *params):
2136 "Process selection of builtin key set."
2137 old_keys = (
2138 'IDLE Classic Windows',
2139 'IDLE Classic Unix',
2140 'IDLE Classic Mac',
2141 'IDLE Classic OSX',
2142 )
2143 value = self.builtin_name.get()
2144 if value not in old_keys:
2145 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
2146 changes.add_option('main', 'Keys', 'name', old_keys[0])
2147 changes.add_option('main', 'Keys', 'name2', value)
2148 self.keys_message['text'] = 'New key set, see Help'
2149 self.keys_message['fg'] = '#500000'
2150 else:
2151 changes.add_option('main', 'Keys', 'name', value)
2152 changes.add_option('main', 'Keys', 'name2', '')
2153 self.keys_message['text'] = ''
2154 self.keys_message['fg'] = 'black'
2155 self.load_keys_list(value)
2156
2157 def var_changed_custom_name(self, *params):
2158 "Process selection of custom key set."
2159 value = self.custom_name.get()
2160 if value != '- no custom keys -':
2161 changes.add_option('main', 'Keys', 'name', value)
2162 self.load_keys_list(value)
2163
2164 def var_changed_keyset_source(self, *params):
2165 "Process toggle between builtin key set and custom key set."
2166 value = self.keyset_source.get()
2167 changes.add_option('main', 'Keys', 'default', value)
2168 if value:
2169 self.var_changed_builtin_name()
2170 else:
2171 self.var_changed_custom_name()
2172
2173 def var_changed_keybinding(self, *params):
2174 "Store change to a keybinding."
2175 value = self.keybinding.get()
2176 key_set = self.custom_name.get()
2177 event = self.bindingslist.get(ANCHOR).split()[0]
2178 if idleConf.IsCoreBinding(event):
2179 changes.add_option('keys', key_set, event, value)
2180 else: # Event is an extension binding.
2181 ext_name = idleConf.GetExtnNameForEvent(event)
2182 ext_keybind_section = ext_name + '_cfgBindings'
2183 changes.add_option('extensions', ext_keybind_section, event, value)
2184
2185 def set_keys_type(self):
2186 "Set available screen options based on builtin or custom key set."
2187 if self.keyset_source.get():
2188 self.builtinlist['state'] = NORMAL
2189 self.customlist['state'] = DISABLED
2190 self.button_delete_custom_keys['state'] = DISABLED
2191 else:
2192 self.builtinlist['state'] = DISABLED
2193 self.custom_keyset_on['state'] = NORMAL
2194 self.customlist['state'] = NORMAL
2195 self.button_delete_custom_keys['state'] = NORMAL
2196
2197 def get_new_keys(self):
2198 """Handle event to change key binding for selected line.
2199
2200 A selection of a key/binding in the list of current
2201 bindings pops up a dialog to enter a new binding. If
2202 the current key set is builtin and a binding has
2203 changed, then a name for a custom key set needs to be
2204 entered for the change to be applied.
2205 """
2206 list_index = self.bindingslist.index(ANCHOR)
2207 binding = self.bindingslist.get(list_index)
2208 bind_name = binding.split()[0]
2209 if self.keyset_source.get():
2210 current_key_set_name = self.builtin_name.get()
2211 else:
2212 current_key_set_name = self.custom_name.get()
2213 current_bindings = idleConf.GetCurrentKeySet()
2214 if current_key_set_name in changes['keys']: # unsaved changes
2215 key_set_changes = changes['keys'][current_key_set_name]
2216 for event in key_set_changes:
2217 current_bindings[event] = key_set_changes[event].split()
2218 current_key_sequences = list(current_bindings.values())
2219 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
2220 current_key_sequences).result
2221 if new_keys:
2222 if self.keyset_source.get(): # Current key set is a built-in.
2223 message = ('Your changes will be saved as a new Custom Key Set.'
2224 ' Enter a name for your new Custom Key Set below.')
2225 new_keyset = self.get_new_keys_name(message)
2226 if not new_keyset: # User cancelled custom key set creation.
2227 self.bindingslist.select_set(list_index)
2228 self.bindingslist.select_anchor(list_index)
2229 return
2230 else: # Create new custom key set based on previously active key set.
2231 self.create_new_key_set(new_keyset)
2232 self.bindingslist.delete(list_index)
2233 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
2234 self.bindingslist.select_set(list_index)
2235 self.bindingslist.select_anchor(list_index)
2236 self.keybinding.set(new_keys)
2237 else:
2238 self.bindingslist.select_set(list_index)
2239 self.bindingslist.select_anchor(list_index)
2240
2241 def get_new_keys_name(self, message):
2242 "Return new key set name from query popup."
2243 used_names = (idleConf.GetSectionList('user', 'keys') +
2244 idleConf.GetSectionList('default', 'keys'))
2245 new_keyset = SectionName(
2246 self, 'New Custom Key Set', message, used_names).result
2247 return new_keyset
2248
2249 def save_as_new_key_set(self):
2250 "Prompt for name of new key set and save changes using that name."
2251 new_keys_name = self.get_new_keys_name('New Key Set Name:')
2252 if new_keys_name:
2253 self.create_new_key_set(new_keys_name)
2254
2255 def on_bindingslist_select(self, event):
2256 "Activate button to assign new keys to selected action."
2257 self.button_new_keys['state'] = NORMAL
2258
2259 def create_new_key_set(self, new_key_set_name):
2260 """Create a new custom key set with the given name.
2261
2262 Copy the bindings/keys from the previously active keyset
2263 to the new keyset and activate the new custom keyset.
2264 """
2265 if self.keyset_source.get():
2266 prev_key_set_name = self.builtin_name.get()
2267 else:
2268 prev_key_set_name = self.custom_name.get()
2269 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
2270 new_keys = {}
2271 for event in prev_keys: # Add key set to changed items.
2272 event_name = event[2:-2] # Trim off the angle brackets.
2273 binding = ' '.join(prev_keys[event])
2274 new_keys[event_name] = binding
2275 # Handle any unsaved changes to prev key set.
2276 if prev_key_set_name in changes['keys']:
2277 key_set_changes = changes['keys'][prev_key_set_name]
2278 for event in key_set_changes:
2279 new_keys[event] = key_set_changes[event]
2280 # Save the new key set.
2281 self.save_new_key_set(new_key_set_name, new_keys)
2282 # Change GUI over to the new key set.
2283 custom_key_list = idleConf.GetSectionList('user', 'keys')
2284 custom_key_list.sort()
2285 self.customlist.SetMenu(custom_key_list, new_key_set_name)
2286 self.keyset_source.set(0)
2287 self.set_keys_type()
2288
2289 def load_keys_list(self, keyset_name):
2290 """Reload the list of action/key binding pairs for the active key set.
2291
2292 An action/key binding can be selected to change the key binding.
2293 """
2294 reselect = False
2295 if self.bindingslist.curselection():
2296 reselect = True
2297 list_index = self.bindingslist.index(ANCHOR)
2298 keyset = idleConf.GetKeySet(keyset_name)
2299 bind_names = list(keyset.keys())
2300 bind_names.sort()
2301 self.bindingslist.delete(0, END)
2302 for bind_name in bind_names:
2303 key = ' '.join(keyset[bind_name])
2304 bind_name = bind_name[2:-2] # Trim off the angle brackets.
2305 if keyset_name in changes['keys']:
2306 # Handle any unsaved changes to this key set.
2307 if bind_name in changes['keys'][keyset_name]:
2308 key = changes['keys'][keyset_name][bind_name]
2309 self.bindingslist.insert(END, bind_name+' - '+key)
2310 if reselect:
2311 self.bindingslist.see(list_index)
2312 self.bindingslist.select_set(list_index)
2313 self.bindingslist.select_anchor(list_index)
2314
2315 @staticmethod
2316 def save_new_key_set(keyset_name, keyset):
2317 """Save a newly created core key set.
2318
2319 Add keyset to idleConf.userCfg['keys'], not to disk.
2320 If the keyset doesn't exist, it is created. The
2321 binding/keys are taken from the keyset argument.
2322
2323 keyset_name - string, the name of the new key set
2324 keyset - dictionary containing the new keybindings
2325 """
2326 if not idleConf.userCfg['keys'].has_section(keyset_name):
2327 idleConf.userCfg['keys'].add_section(keyset_name)
2328 for event in keyset:
2329 value = keyset[event]
2330 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
2331
2332 def delete_custom_keys(self):
2333 """Handle event to delete a custom key set.
2334
2335 Applying the delete deactivates the current configuration and
2336 reverts to the default. The custom key set is permanently
2337 deleted from the config file.
2338 """
2339 keyset_name = self.custom_name.get()
2340 delmsg = 'Are you sure you wish to delete the key set %r ?'
2341 if not tkMessageBox.askyesno(
2342 'Delete Key Set', delmsg % keyset_name, parent=self):
2343 return
2344 self.cd.deactivate_current_config()
2345 # Remove key set from changes, config, and file.
2346 changes.delete_section('keys', keyset_name)
2347 # Reload user key set list.
2348 item_list = idleConf.GetSectionList('user', 'keys')
2349 item_list.sort()
2350 if not item_list:
2351 self.custom_keyset_on['state'] = DISABLED
2352 self.customlist.SetMenu(item_list, '- no custom keys -')
2353 else:
2354 self.customlist.SetMenu(item_list, item_list[0])
2355 # Revert to default key set.
2356 self.keyset_source.set(idleConf.defaultCfg['main']
2357 .Get('Keys', 'default'))
2358 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
2359 or idleConf.default_keys())
2360 # User can't back out of these changes, they must be applied now.
2361 changes.save_all()
2362 self.cd.save_all_changed_extensions()
2363 self.cd.activate_config_changes()
2364 self.set_keys_type()
2365
2366
csabellae8eb17b2017-07-30 18:39:17 -04002367class GenPage(Frame):
2368
csabella6f446be2017-08-01 00:24:07 -04002369 def __init__(self, master):
2370 super().__init__(master)
csabellae8eb17b2017-07-30 18:39:17 -04002371 self.create_page_general()
2372 self.load_general_cfg()
2373
2374 def create_page_general(self):
2375 """Return frame of widgets for General tab.
2376
2377 Enable users to provisionally change general options. Function
2378 load_general_cfg intializes tk variables and helplist using
2379 idleConf. Radiobuttons startup_shell_on and startup_editor_on
2380 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
2381 set var autosave. Entry boxes win_width_int and win_height_int
2382 set var win_width and win_height. Setting var_name invokes the
2383 default callback that adds option to changes.
2384
2385 Helplist: load_general_cfg loads list user_helplist with
2386 name, position pairs and copies names to listbox helplist.
2387 Clicking a name invokes help_source selected. Clicking
2388 button_helplist_name invokes helplist_item_name, which also
2389 changes user_helplist. These functions all call
2390 set_add_delete_state. All but load call update_help_changes to
2391 rewrite changes['main']['HelpFiles'].
2392
Cheryl Sabella2f896462017-08-14 21:21:43 -04002393 Widgets for GenPage(Frame): (*) widgets bound to self
2394 frame_run: LabelFrame
2395 startup_title: Label
2396 (*)startup_editor_on: Radiobutton - startup_edit
2397 (*)startup_shell_on: Radiobutton - startup_edit
2398 frame_save: LabelFrame
2399 run_save_title: Label
2400 (*)save_ask_on: Radiobutton - autosave
2401 (*)save_auto_on: Radiobutton - autosave
2402 frame_win_size: LabelFrame
2403 win_size_title: Label
2404 win_width_title: Label
2405 (*)win_width_int: Entry - win_width
2406 win_height_title: Label
2407 (*)win_height_int: Entry - win_height
2408 frame_help: LabelFrame
2409 frame_helplist: Frame
2410 frame_helplist_buttons: Frame
2411 (*)button_helplist_edit
2412 (*)button_helplist_add
2413 (*)button_helplist_remove
2414 (*)helplist: ListBox
2415 scroll_helplist: Scrollbar
csabellae8eb17b2017-07-30 18:39:17 -04002416 """
2417 self.startup_edit = tracers.add(
2418 IntVar(self), ('main', 'General', 'editor-on-startup'))
2419 self.autosave = tracers.add(
2420 IntVar(self), ('main', 'General', 'autosave'))
2421 self.win_width = tracers.add(
2422 StringVar(self), ('main', 'EditorWindow', 'width'))
2423 self.win_height = tracers.add(
2424 StringVar(self), ('main', 'EditorWindow', 'height'))
2425
2426 # Create widgets:
2427 # Section frames.
2428 frame_run = LabelFrame(self, borderwidth=2, relief=GROOVE,
2429 text=' Startup Preferences ')
2430 frame_save = LabelFrame(self, borderwidth=2, relief=GROOVE,
2431 text=' autosave Preferences ')
2432 frame_win_size = Frame(self, borderwidth=2, relief=GROOVE)
2433 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
2434 text=' Additional Help Sources ')
2435 # frame_run.
2436 startup_title = Label(frame_run, text='At Startup')
2437 self.startup_editor_on = Radiobutton(
2438 frame_run, variable=self.startup_edit, value=1,
2439 text="Open Edit Window")
2440 self.startup_shell_on = Radiobutton(
2441 frame_run, variable=self.startup_edit, value=0,
2442 text='Open Shell Window')
2443 # frame_save.
2444 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
2445 self.save_ask_on = Radiobutton(
2446 frame_save, variable=self.autosave, value=0,
2447 text="Prompt to Save")
2448 self.save_auto_on = Radiobutton(
2449 frame_save, variable=self.autosave, value=1,
2450 text='No Prompt')
2451 # frame_win_size.
2452 win_size_title = Label(
2453 frame_win_size, text='Initial Window Size (in characters)')
2454 win_width_title = Label(frame_win_size, text='Width')
2455 self.win_width_int = Entry(
2456 frame_win_size, textvariable=self.win_width, width=3)
2457 win_height_title = Label(frame_win_size, text='Height')
2458 self.win_height_int = Entry(
2459 frame_win_size, textvariable=self.win_height, width=3)
2460 # frame_help.
2461 frame_helplist = Frame(frame_help)
2462 frame_helplist_buttons = Frame(frame_helplist)
2463 self.helplist = Listbox(
2464 frame_helplist, height=5, takefocus=True,
2465 exportselection=FALSE)
2466 scroll_helplist = Scrollbar(frame_helplist)
2467 scroll_helplist['command'] = self.helplist.yview
2468 self.helplist['yscrollcommand'] = scroll_helplist.set
2469 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
2470 self.button_helplist_edit = Button(
2471 frame_helplist_buttons, text='Edit', state=DISABLED,
2472 width=8, command=self.helplist_item_edit)
2473 self.button_helplist_add = Button(
2474 frame_helplist_buttons, text='Add',
2475 width=8, command=self.helplist_item_add)
2476 self.button_helplist_remove = Button(
2477 frame_helplist_buttons, text='Remove', state=DISABLED,
2478 width=8, command=self.helplist_item_remove)
2479
2480 # Pack widgets:
2481 # body.
2482 frame_run.pack(side=TOP, padx=5, pady=5, fill=X)
2483 frame_save.pack(side=TOP, padx=5, pady=5, fill=X)
2484 frame_win_size.pack(side=TOP, padx=5, pady=5, fill=X)
2485 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2486 # frame_run.
2487 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2488 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2489 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2490 # frame_save.
2491 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2492 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2493 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
2494 # frame_win_size.
2495 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
2496 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2497 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
2498 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
2499 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
2500 # frame_help.
2501 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
2502 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
2503 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
2504 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
2505 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
2506 self.button_helplist_add.pack(side=TOP, anchor=W)
2507 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
2508
2509 def load_general_cfg(self):
2510 "Load current configuration settings for the general options."
2511 # Set startup state.
2512 self.startup_edit.set(idleConf.GetOption(
2513 'main', 'General', 'editor-on-startup', default=0, type='bool'))
2514 # Set autosave state.
2515 self.autosave.set(idleConf.GetOption(
2516 'main', 'General', 'autosave', default=0, type='bool'))
2517 # Set initial window size.
2518 self.win_width.set(idleConf.GetOption(
2519 'main', 'EditorWindow', 'width', type='int'))
2520 self.win_height.set(idleConf.GetOption(
2521 'main', 'EditorWindow', 'height', type='int'))
2522 # Set additional help sources.
2523 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
2524 self.helplist.delete(0, 'end')
2525 for help_item in self.user_helplist:
2526 self.helplist.insert(END, help_item[0])
2527 self.set_add_delete_state()
2528
2529 def help_source_selected(self, event):
2530 "Handle event for selecting additional help."
2531 self.set_add_delete_state()
2532
2533 def set_add_delete_state(self):
2534 "Toggle the state for the help list buttons based on list entries."
2535 if self.helplist.size() < 1: # No entries in list.
2536 self.button_helplist_edit['state'] = DISABLED
2537 self.button_helplist_remove['state'] = DISABLED
2538 else: # Some entries.
2539 if self.helplist.curselection(): # There currently is a selection.
2540 self.button_helplist_edit['state'] = NORMAL
2541 self.button_helplist_remove['state'] = NORMAL
2542 else: # There currently is not a selection.
2543 self.button_helplist_edit['state'] = DISABLED
2544 self.button_helplist_remove['state'] = DISABLED
2545
2546 def helplist_item_add(self):
2547 """Handle add button for the help list.
2548
2549 Query for name and location of new help sources and add
2550 them to the list.
2551 """
2552 help_source = HelpSource(self, 'New Help Source').result
2553 if help_source:
2554 self.user_helplist.append(help_source)
2555 self.helplist.insert(END, help_source[0])
2556 self.update_help_changes()
2557
2558 def helplist_item_edit(self):
2559 """Handle edit button for the help list.
2560
2561 Query with existing help source information and update
2562 config if the values are changed.
2563 """
2564 item_index = self.helplist.index(ANCHOR)
2565 help_source = self.user_helplist[item_index]
2566 new_help_source = HelpSource(
2567 self, 'Edit Help Source',
2568 menuitem=help_source[0],
2569 filepath=help_source[1],
2570 ).result
2571 if new_help_source and new_help_source != help_source:
2572 self.user_helplist[item_index] = new_help_source
2573 self.helplist.delete(item_index)
2574 self.helplist.insert(item_index, new_help_source[0])
2575 self.update_help_changes()
2576 self.set_add_delete_state() # Selected will be un-selected
2577
2578 def helplist_item_remove(self):
2579 """Handle remove button for the help list.
2580
2581 Delete the help list item from config.
2582 """
2583 item_index = self.helplist.index(ANCHOR)
2584 del(self.user_helplist[item_index])
2585 self.helplist.delete(item_index)
2586 self.update_help_changes()
2587 self.set_add_delete_state()
2588
2589 def update_help_changes(self):
2590 "Clear and rebuild the HelpFiles section in changes"
2591 changes['main']['HelpFiles'] = {}
2592 for num in range(1, len(self.user_helplist) + 1):
2593 changes.add_option(
2594 'main', 'HelpFiles', str(num),
2595 ';'.join(self.user_helplist[num-1][:2]))
2596
2597
csabella45bf7232017-07-26 19:09:58 -04002598class VarTrace:
2599 """Maintain Tk variables trace state."""
2600
2601 def __init__(self):
2602 """Store Tk variables and callbacks.
2603
2604 untraced: List of tuples (var, callback)
2605 that do not have the callback attached
2606 to the Tk var.
2607 traced: List of tuples (var, callback) where
2608 that callback has been attached to the var.
2609 """
2610 self.untraced = []
2611 self.traced = []
2612
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002613 def clear(self):
2614 "Clear lists (for tests)."
Terry Jan Reedy733d0f62017-08-07 14:22:44 -04002615 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04002616 self.untraced.clear()
2617 self.traced.clear()
2618
csabella45bf7232017-07-26 19:09:58 -04002619 def add(self, var, callback):
2620 """Add (var, callback) tuple to untraced list.
2621
2622 Args:
2623 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04002624 callback: Either function name to be used as a callback
2625 or a tuple with IdleConf config-type, section, and
2626 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04002627
2628 Return:
2629 Tk variable instance.
2630 """
2631 if isinstance(callback, tuple):
2632 callback = self.make_callback(var, callback)
2633 self.untraced.append((var, callback))
2634 return var
2635
2636 @staticmethod
2637 def make_callback(var, config):
2638 "Return default callback function to add values to changes instance."
2639 def default_callback(*params):
2640 "Add config values to changes instance."
2641 changes.add_option(*config, var.get())
2642 return default_callback
2643
2644 def attach(self):
2645 "Attach callback to all vars that are not traced."
2646 while self.untraced:
2647 var, callback = self.untraced.pop()
2648 var.trace_add('write', callback)
2649 self.traced.append((var, callback))
2650
2651 def detach(self):
2652 "Remove callback from traced vars."
2653 while self.traced:
2654 var, callback = self.traced.pop()
2655 var.trace_remove('write', var.trace_info()[0][1])
2656 self.untraced.append((var, callback))
2657
2658
csabella5b591542017-07-28 14:40:59 -04002659tracers = VarTrace()
2660
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002661help_common = '''\
2662When you click either the Apply or Ok buttons, settings in this
2663dialog that are different from IDLE's default are saved in
2664a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002665these changes apply to all versions of IDLE installed on this
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002666machine. Some do not take affect until IDLE is restarted.
2667[Cancel] only cancels changes made since the last save.
2668'''
2669help_pages = {
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002670 'Highlighting': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002671Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05002672The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002673be used with older IDLE releases if it is saved as a custom
2674theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04002675''',
2676 'Keys': '''
2677Keys:
2678The IDLE Modern Unix key set is new in June 2016. It can only
2679be used with older IDLE releases if it is saved as a custom
2680key set, with a different name.
2681''',
wohlgangerfae2c352017-06-27 21:36:23 -05002682 'Extensions': '''
2683Extensions:
2684
2685Autocomplete: Popupwait is milleseconds to wait after key char, without
2686cursor movement, before popping up completion box. Key char is '.' after
2687identifier or a '/' (or '\\' on Windows) within a string.
2688
2689FormatParagraph: Max-width is max chars in lines after re-formatting.
2690Use with paragraphs in both strings and comment blocks.
2691
2692ParenMatch: Style indicates what is highlighted when closer is entered:
2693'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
2694'expression' (default) - also everything in between. Flash-delay is how
2695long to highlight if cursor is not moved (0 means forever).
2696'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04002697}
2698
Steven M. Gavac11ccf32001-09-24 09:43:17 +00002699
Terry Jan Reedy93f35422015-10-13 22:03:51 -04002700def is_int(s):
2701 "Return 's is blank or represents an int'"
2702 if not s:
2703 return True
2704 try:
2705 int(s)
2706 return True
2707 except ValueError:
2708 return False
2709
2710
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002711class VerticalScrolledFrame(Frame):
2712 """A pure Tkinter vertically scrollable frame.
2713
2714 * Use the 'interior' attribute to place widgets inside the scrollable frame
2715 * Construct and pack/place/grid normally
2716 * This frame only allows vertical scrolling
2717 """
2718 def __init__(self, parent, *args, **kw):
2719 Frame.__init__(self, parent, *args, **kw)
2720
csabella7eb58832017-07-04 21:30:58 -04002721 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002722 vscrollbar = Scrollbar(self, orient=VERTICAL)
2723 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
2724 canvas = Canvas(self, bd=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002725 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002726 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2727 vscrollbar.config(command=canvas.yview)
2728
csabella7eb58832017-07-04 21:30:58 -04002729 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002730 canvas.xview_moveto(0)
2731 canvas.yview_moveto(0)
2732
csabella7eb58832017-07-04 21:30:58 -04002733 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002734 self.interior = interior = Frame(canvas)
2735 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2736
csabella7eb58832017-07-04 21:30:58 -04002737 # Track changes to the canvas and frame width and sync them,
2738 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002739 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04002740 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002741 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2742 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002743 interior.bind('<Configure>', _configure_interior)
2744
2745 def _configure_canvas(event):
2746 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04002747 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002748 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2749 canvas.bind('<Configure>', _configure_canvas)
2750
2751 return
2752
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002753
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002754if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04002755 import unittest
2756 unittest.main('idlelib.idle_test.test_configdialog',
2757 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002758 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002759 run(ConfigDialog)