blob: 28070a51709dd28fce1fadaab1743882a1f17521 [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
95 keyspage: self.create_page_keys
96 genpage: GenPage
97 extpageL 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)
107 self.keyspage = self.create_page_keys()
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()
135 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
csabella36329a42017-07-13 23:32:01 -0400225 Tk Variables:
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400226 color: Color of selected target.
csabella7eb58832017-07-04 21:30:58 -0400227 builtin_theme: Menu variable for built-in theme.
228 custom_theme: Menu variable for custom theme.
229 fg_bg_toggle: Toggle for foreground/background color.
csabella36329a42017-07-13 23:32:01 -0400230 Note: this has no callback.
csabella7eb58832017-07-04 21:30:58 -0400231 is_builtin_theme: Selector for built-in or custom theme.
232 highlight_target: Menu variable for the highlight tag target.
csabella36329a42017-07-13 23:32:01 -0400233
234 Instance Data Attributes:
235 theme_elements: Dictionary of tags for text highlighting.
236 The key is the display name and the value is a tuple of
237 (tag name, display sort order).
238
239 Methods [attachment]:
240 load_theme_cfg: Load current highlight colors.
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400241 get_color: Invoke colorchooser [button_set_color].
242 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
csabella36329a42017-07-13 23:32:01 -0400243 set_highlight_target: set fg_bg_toggle, set_color_sample().
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400244 set_color_sample: Set frame background to target.
245 on_new_color_set: Set new color and add option.
csabella36329a42017-07-13 23:32:01 -0400246 paint_theme_sample: Recolor sample.
247 get_new_theme_name: Get from popup.
248 create_new_theme: Combine theme with changes and save.
249 save_as_new_theme: Save [button_save_custom_theme].
250 set_theme_type: Command for [is_builtin_theme].
251 delete_custom_theme: Ativate default [button_delete_custom_theme].
252 save_new_theme: Save to userCfg['theme'] (is function).
253
254 Widget Structure: (*) widgets bound to self
255 frame
256 frame_custom: LabelFrame
Terry Jan Reedyd0969d62017-07-21 02:20:46 -0400257 (*)highlight_sample: Text
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400258 (*)frame_color_set: Frame
259 button_set_color: Button
csabella36329a42017-07-13 23:32:01 -0400260 (*)opt_menu_highlight_target: DynOptionMenu - highlight_target
261 frame_fg_bg_toggle: Frame
262 (*)radio_fg: Radiobutton - fg_bg_toggle
263 (*)radio_bg: Radiobutton - fg_bg_toggle
264 button_save_custom_theme: Button
265 frame_theme: LabelFrame
266 theme_type_title: Label
267 (*)radio_theme_builtin: Radiobutton - is_builtin_theme
268 (*)radio_theme_custom: Radiobutton - is_builtin_theme
269 (*)opt_menu_theme_builtin: DynOptionMenu - builtin_theme
270 (*)opt_menu_theme_custom: DynOptionMenu - custom_theme
271 (*)button_delete_custom_theme: Button
272 (*)new_custom_theme: Label
csabella7eb58832017-07-04 21:30:58 -0400273 """
csabella36329a42017-07-13 23:32:01 -0400274 self.theme_elements={
275 'Normal Text': ('normal', '00'),
276 'Python Keywords': ('keyword', '01'),
277 'Python Definitions': ('definition', '02'),
278 'Python Builtins': ('builtin', '03'),
279 'Python Comments': ('comment', '04'),
280 'Python Strings': ('string', '05'),
281 'Selected Text': ('hilite', '06'),
282 'Found Text': ('hit', '07'),
283 'Cursor': ('cursor', '08'),
284 'Editor Breakpoint': ('break', '09'),
285 'Shell Normal Text': ('console', '10'),
286 'Shell Error Text': ('error', '11'),
287 'Shell Stdout Text': ('stdout', '12'),
288 'Shell Stderr Text': ('stderr', '13'),
289 }
Terry Jan Reedy22405332014-07-30 19:24:32 -0400290 parent = self.parent
csabella5b591542017-07-28 14:40:59 -0400291 self.builtin_theme = tracers.add(
292 StringVar(parent), self.var_changed_builtin_theme)
293 self.custom_theme = tracers.add(
294 StringVar(parent), self.var_changed_custom_theme)
csabellabac7d332017-06-26 17:46:26 -0400295 self.fg_bg_toggle = BooleanVar(parent)
csabella5b591542017-07-28 14:40:59 -0400296 self.color = tracers.add(
297 StringVar(parent), self.var_changed_color)
298 self.is_builtin_theme = tracers.add(
299 BooleanVar(parent), self.var_changed_is_builtin_theme)
300 self.highlight_target = tracers.add(
301 StringVar(parent), self.var_changed_highlight_target)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400302
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400303 # Widget creation:
304 # body frame and section frames
305 frame = Frame(self.note)
csabellabac7d332017-06-26 17:46:26 -0400306 frame_custom = LabelFrame(frame, borderwidth=2, relief=GROOVE,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400307 text=' Custom Highlighting ')
csabellabac7d332017-06-26 17:46:26 -0400308 frame_theme = LabelFrame(frame, borderwidth=2, relief=GROOVE,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400309 text=' Highlighting Theme ')
csabellabac7d332017-06-26 17:46:26 -0400310 #frame_custom
csabella9397e2a2017-07-30 13:34:25 -0400311 text = self.highlight_sample = frame.highlight_sample = Text(
csabellabac7d332017-06-26 17:46:26 -0400312 frame_custom, relief=SOLID, borderwidth=1,
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400313 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400314 takefocus=FALSE, highlightthickness=0, wrap=NONE)
Terry Jan Reedyd0969d62017-07-21 02:20:46 -0400315 text=self.highlight_sample
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400316 text.bind('<Double-Button-1>', lambda e: 'break')
317 text.bind('<B1-Motion>', lambda e: 'break')
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400318 text_and_tags=(('\n', 'normal'),
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400319 ('#you can click here', 'comment'), ('\n', 'normal'),
320 ('#to choose items', 'comment'), ('\n', 'normal'),
321 ('def', 'keyword'), (' ', 'normal'),
322 ('func', 'definition'), ('(param):\n ', 'normal'),
323 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
324 ("'string'", 'string'), ('\n var1 = ', 'normal'),
325 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
326 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
327 ('list', 'builtin'), ('(', 'normal'),
Terry Jan Reedya8aa4d52015-10-02 22:12:17 -0400328 ('None', 'keyword'), (')\n', 'normal'),
329 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400330 (' error ', 'error'), (' ', 'normal'),
331 ('cursor |', 'cursor'), ('\n ', 'normal'),
332 ('shell', 'console'), (' ', 'normal'),
333 ('stdout', 'stdout'), (' ', 'normal'),
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400334 ('stderr', 'stderr'), ('\n\n', 'normal'))
csabellabac7d332017-06-26 17:46:26 -0400335 for texttag in text_and_tags:
336 text.insert(END, texttag[0], texttag[1])
337 for element in self.theme_elements:
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400338 def tem(event, elem=element):
csabellabac7d332017-06-26 17:46:26 -0400339 event.widget.winfo_toplevel().highlight_target.set(elem)
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400340 text.tag_bind(
csabellabac7d332017-06-26 17:46:26 -0400341 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Terry Jan Reedy2bc8f0e2017-07-26 20:54:40 -0400342 text['state'] = DISABLED
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400343 self.frame_color_set = Frame(frame_custom, relief=SOLID, borderwidth=1)
csabellabac7d332017-06-26 17:46:26 -0400344 frame_fg_bg_toggle = Frame(frame_custom)
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400345 button_set_color = Button(
346 self.frame_color_set, text='Choose Color for :',
347 command=self.get_color, highlightthickness=0)
csabellabac7d332017-06-26 17:46:26 -0400348 self.opt_menu_highlight_target = DynOptionMenu(
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400349 self.frame_color_set, self.highlight_target, None,
csabellabac7d332017-06-26 17:46:26 -0400350 highlightthickness=0) #, command=self.set_highlight_targetBinding
351 self.radio_fg = Radiobutton(
352 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400353 text='Foreground', command=self.set_color_sample_binding)
csabellabac7d332017-06-26 17:46:26 -0400354 self.radio_bg=Radiobutton(
355 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400356 text='Background', command=self.set_color_sample_binding)
csabellabac7d332017-06-26 17:46:26 -0400357 self.fg_bg_toggle.set(1)
358 button_save_custom_theme = Button(
359 frame_custom, text='Save as New Custom Theme',
360 command=self.save_as_new_theme)
361 #frame_theme
362 theme_type_title = Label(frame_theme, text='Select : ')
363 self.radio_theme_builtin = Radiobutton(
364 frame_theme, variable=self.is_builtin_theme, value=1,
365 command=self.set_theme_type, text='a Built-in Theme')
366 self.radio_theme_custom = Radiobutton(
367 frame_theme, variable=self.is_builtin_theme, value=0,
368 command=self.set_theme_type, text='a Custom Theme')
369 self.opt_menu_theme_builtin = DynOptionMenu(
370 frame_theme, self.builtin_theme, None, command=None)
371 self.opt_menu_theme_custom=DynOptionMenu(
372 frame_theme, self.custom_theme, None, command=None)
373 self.button_delete_custom_theme=Button(
374 frame_theme, text='Delete Custom Theme',
375 command=self.delete_custom_theme)
376 self.new_custom_theme = Label(frame_theme, bd=2)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400377
Steven M. Gava952d0a52001-08-03 04:43:44 +0000378 ##widget packing
379 #body
csabellabac7d332017-06-26 17:46:26 -0400380 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
381 frame_theme.pack(side=LEFT, padx=5, pady=5, fill=Y)
382 #frame_custom
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400383 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
csabellabac7d332017-06-26 17:46:26 -0400384 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
Terry Jan Reedyd0969d62017-07-21 02:20:46 -0400385 self.highlight_sample.pack(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400386 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
Terry Jan Reedya54a8f12017-07-21 01:06:58 -0400387 button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
csabellabac7d332017-06-26 17:46:26 -0400388 self.opt_menu_highlight_target.pack(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400389 side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
csabellabac7d332017-06-26 17:46:26 -0400390 self.radio_fg.pack(side=LEFT, anchor=E)
391 self.radio_bg.pack(side=RIGHT, anchor=W)
392 button_save_custom_theme.pack(side=BOTTOM, fill=X, padx=5, pady=5)
393 #frame_theme
394 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
395 self.radio_theme_builtin.pack(side=TOP, anchor=W, padx=5)
396 self.radio_theme_custom.pack(side=TOP, anchor=W, padx=5, pady=2)
397 self.opt_menu_theme_builtin.pack(side=TOP, fill=X, padx=5, pady=5)
398 self.opt_menu_theme_custom.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
399 self.button_delete_custom_theme.pack(side=TOP, fill=X, padx=5, pady=5)
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -0500400 self.new_custom_theme.pack(side=TOP, fill=X, pady=5)
Steven M. Gava952d0a52001-08-03 04:43:44 +0000401 return frame
402
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400403 def load_theme_cfg(self):
404 """Load current configuration settings for the theme options.
405
406 Based on the is_builtin_theme toggle, the theme is set as
407 either builtin or custom and the initial widget values
408 reflect the current settings from idleConf.
409
410 Attributes updated:
411 is_builtin_theme: Set from idleConf.
412 opt_menu_theme_builtin: List of default themes from idleConf.
413 opt_menu_theme_custom: List of custom themes from idleConf.
414 radio_theme_custom: Disabled if there are no custom themes.
415 custom_theme: Message with additional information.
416 opt_menu_highlight_target: Create menu from self.theme_elements.
417
418 Methods:
419 set_theme_type
420 paint_theme_sample
421 set_highlight_target
422 """
423 # Set current theme type radiobutton.
424 self.is_builtin_theme.set(idleConf.GetOption(
425 'main', 'Theme', 'default', type='bool', default=1))
426 # Set current theme.
427 current_option = idleConf.CurrentTheme()
428 # Load available theme option menus.
429 if self.is_builtin_theme.get(): # Default theme selected.
430 item_list = idleConf.GetSectionList('default', 'highlight')
431 item_list.sort()
432 self.opt_menu_theme_builtin.SetMenu(item_list, current_option)
433 item_list = idleConf.GetSectionList('user', 'highlight')
434 item_list.sort()
435 if not item_list:
436 self.radio_theme_custom['state'] = DISABLED
437 self.custom_theme.set('- no custom themes -')
438 else:
439 self.opt_menu_theme_custom.SetMenu(item_list, item_list[0])
440 else: # User theme selected.
441 item_list = idleConf.GetSectionList('user', 'highlight')
442 item_list.sort()
443 self.opt_menu_theme_custom.SetMenu(item_list, current_option)
444 item_list = idleConf.GetSectionList('default', 'highlight')
445 item_list.sort()
446 self.opt_menu_theme_builtin.SetMenu(item_list, item_list[0])
447 self.set_theme_type()
448 # Load theme element option menu.
449 theme_names = list(self.theme_elements.keys())
450 theme_names.sort(key=lambda x: self.theme_elements[x][1])
451 self.opt_menu_highlight_target.SetMenu(theme_names, theme_names[0])
452 self.paint_theme_sample()
453 self.set_highlight_target()
454
455 def var_changed_builtin_theme(self, *params):
456 """Process new builtin theme selection.
457
458 Add the changed theme's name to the changed_items and recreate
459 the sample with the values from the selected theme.
460 """
461 old_themes = ('IDLE Classic', 'IDLE New')
462 value = self.builtin_theme.get()
463 if value not in old_themes:
464 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
465 changes.add_option('main', 'Theme', 'name', old_themes[0])
466 changes.add_option('main', 'Theme', 'name2', value)
467 self.new_custom_theme.config(text='New theme, see Help',
468 fg='#500000')
469 else:
470 changes.add_option('main', 'Theme', 'name', value)
471 changes.add_option('main', 'Theme', 'name2', '')
472 self.new_custom_theme.config(text='', fg='black')
473 self.paint_theme_sample()
474
475 def var_changed_custom_theme(self, *params):
476 """Process new custom theme selection.
477
478 If a new custom theme is selected, add the name to the
479 changed_items and apply the theme to the sample.
480 """
481 value = self.custom_theme.get()
482 if value != '- no custom themes -':
483 changes.add_option('main', 'Theme', 'name', value)
484 self.paint_theme_sample()
485
486 def var_changed_is_builtin_theme(self, *params):
487 """Process toggle between builtin and custom theme.
488
489 Update the default toggle value and apply the newly
490 selected theme type.
491 """
492 value = self.is_builtin_theme.get()
493 changes.add_option('main', 'Theme', 'default', value)
494 if value:
495 self.var_changed_builtin_theme()
496 else:
497 self.var_changed_custom_theme()
498
499 def var_changed_color(self, *params):
500 "Process change to color choice."
501 self.on_new_color_set()
502
503 def var_changed_highlight_target(self, *params):
504 "Process selection of new target tag for highlighting."
505 self.set_highlight_target()
506
507 def set_theme_type(self):
508 """Set available screen options based on builtin or custom theme.
509
510 Attributes accessed:
511 is_builtin_theme
512
513 Attributes updated:
514 opt_menu_theme_builtin
515 opt_menu_theme_custom
516 button_delete_custom_theme
517 radio_theme_custom
518
519 Called from:
520 handler for radio_theme_builtin and radio_theme_custom
521 delete_custom_theme
522 create_new_theme
523 load_theme_cfg
524 """
525 if self.is_builtin_theme.get():
526 self.opt_menu_theme_builtin['state'] = NORMAL
527 self.opt_menu_theme_custom['state'] = DISABLED
528 self.button_delete_custom_theme['state'] = DISABLED
529 else:
530 self.opt_menu_theme_builtin['state'] = DISABLED
531 self.radio_theme_custom['state'] = NORMAL
532 self.opt_menu_theme_custom['state'] = NORMAL
533 self.button_delete_custom_theme['state'] = NORMAL
534
535 def get_color(self):
536 """Handle button to select a new color for the target tag.
537
538 If a new color is selected while using a builtin theme, a
539 name must be supplied to create a custom theme.
540
541 Attributes accessed:
542 highlight_target
543 frame_color_set
544 is_builtin_theme
545
546 Attributes updated:
547 color
548
549 Methods:
550 get_new_theme_name
551 create_new_theme
552 """
553 target = self.highlight_target.get()
554 prev_color = self.frame_color_set.cget('bg')
555 rgbTuplet, color_string = tkColorChooser.askcolor(
556 parent=self, title='Pick new color for : '+target,
557 initialcolor=prev_color)
558 if color_string and (color_string != prev_color):
559 # User didn't cancel and they chose a new color.
560 if self.is_builtin_theme.get(): # Current theme is a built-in.
561 message = ('Your changes will be saved as a new Custom Theme. '
562 'Enter a name for your new Custom Theme below.')
563 new_theme = self.get_new_theme_name(message)
564 if not new_theme: # User cancelled custom theme creation.
565 return
566 else: # Create new custom theme based on previously active theme.
567 self.create_new_theme(new_theme)
568 self.color.set(color_string)
569 else: # Current theme is user defined.
570 self.color.set(color_string)
571
572 def on_new_color_set(self):
573 "Display sample of new color selection on the dialog."
574 new_color=self.color.get()
575 self.frame_color_set.config(bg=new_color) # Set sample.
576 plane ='foreground' if self.fg_bg_toggle.get() else 'background'
577 sample_element = self.theme_elements[self.highlight_target.get()][0]
578 self.highlight_sample.tag_config(sample_element, **{plane:new_color})
579 theme = self.custom_theme.get()
580 theme_element = sample_element + '-' + plane
581 changes.add_option('highlight', theme, theme_element, new_color)
582
583 def get_new_theme_name(self, message):
584 "Return name of new theme from query popup."
585 used_names = (idleConf.GetSectionList('user', 'highlight') +
586 idleConf.GetSectionList('default', 'highlight'))
587 new_theme = SectionName(
588 self, 'New Custom Theme', message, used_names).result
589 return new_theme
590
591 def save_as_new_theme(self):
592 """Prompt for new theme name and create the theme.
593
594 Methods:
595 get_new_theme_name
596 create_new_theme
597 """
598 new_theme_name = self.get_new_theme_name('New Theme Name:')
599 if new_theme_name:
600 self.create_new_theme(new_theme_name)
601
602 def create_new_theme(self, new_theme_name):
603 """Create a new custom theme with the given name.
604
605 Create the new theme based on the previously active theme
606 with the current changes applied. Once it is saved, then
607 activate the new theme.
608
609 Attributes accessed:
610 builtin_theme
611 custom_theme
612
613 Attributes updated:
614 opt_menu_theme_custom
615 is_builtin_theme
616
617 Method:
618 save_new_theme
619 set_theme_type
620 """
621 if self.is_builtin_theme.get():
622 theme_type = 'default'
623 theme_name = self.builtin_theme.get()
624 else:
625 theme_type = 'user'
626 theme_name = self.custom_theme.get()
627 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
628 # Apply any of the old theme's unsaved changes to the new theme.
629 if theme_name in changes['highlight']:
630 theme_changes = changes['highlight'][theme_name]
631 for element in theme_changes:
632 new_theme[element] = theme_changes[element]
633 # Save the new theme.
634 self.save_new_theme(new_theme_name, new_theme)
635 # Change GUI over to the new theme.
636 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
637 custom_theme_list.sort()
638 self.opt_menu_theme_custom.SetMenu(custom_theme_list, new_theme_name)
639 self.is_builtin_theme.set(0)
640 self.set_theme_type()
641
642 def set_highlight_target(self):
643 """Set fg/bg toggle and color based on highlight tag target.
644
645 Instance variables accessed:
646 highlight_target
647
648 Attributes updated:
649 radio_fg
650 radio_bg
651 fg_bg_toggle
652
653 Methods:
654 set_color_sample
655
656 Called from:
657 var_changed_highlight_target
658 load_theme_cfg
659 """
660 if self.highlight_target.get() == 'Cursor': # bg not possible
661 self.radio_fg['state'] = DISABLED
662 self.radio_bg['state'] = DISABLED
663 self.fg_bg_toggle.set(1)
664 else: # Both fg and bg can be set.
665 self.radio_fg['state'] = NORMAL
666 self.radio_bg['state'] = NORMAL
667 self.fg_bg_toggle.set(1)
668 self.set_color_sample()
669
670 def set_color_sample_binding(self, *args):
671 """Change color sample based on foreground/background toggle.
672
673 Methods:
674 set_color_sample
675 """
676 self.set_color_sample()
677
678 def set_color_sample(self):
679 """Set the color of the frame background to reflect the selected target.
680
681 Instance variables accessed:
682 theme_elements
683 highlight_target
684 fg_bg_toggle
685 highlight_sample
686
687 Attributes updated:
688 frame_color_set
689 """
690 # Set the color sample area.
691 tag = self.theme_elements[self.highlight_target.get()][0]
692 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
693 color = self.highlight_sample.tag_cget(tag, plane)
694 self.frame_color_set.config(bg=color)
695
696 def paint_theme_sample(self):
697 """Apply the theme colors to each element tag in the sample text.
698
699 Instance attributes accessed:
700 theme_elements
701 is_builtin_theme
702 builtin_theme
703 custom_theme
704
705 Attributes updated:
706 highlight_sample: Set the tag elements to the theme.
707
708 Methods:
709 set_color_sample
710
711 Called from:
712 var_changed_builtin_theme
713 var_changed_custom_theme
714 load_theme_cfg
715 """
716 if self.is_builtin_theme.get(): # Default theme
717 theme = self.builtin_theme.get()
718 else: # User theme
719 theme = self.custom_theme.get()
720 for element_title in self.theme_elements:
721 element = self.theme_elements[element_title][0]
722 colors = idleConf.GetHighlight(theme, element)
723 if element == 'cursor': # Cursor sample needs special painting.
724 colors['background'] = idleConf.GetHighlight(
725 theme, 'normal', fgBg='bg')
726 # Handle any unsaved changes to this theme.
727 if theme in changes['highlight']:
728 theme_dict = changes['highlight'][theme]
729 if element + '-foreground' in theme_dict:
730 colors['foreground'] = theme_dict[element + '-foreground']
731 if element + '-background' in theme_dict:
732 colors['background'] = theme_dict[element + '-background']
733 self.highlight_sample.tag_config(element, **colors)
734 self.set_color_sample()
735
736 def save_new_theme(self, theme_name, theme):
737 """Save a newly created theme to idleConf.
738
739 theme_name - string, the name of the new theme
740 theme - dictionary containing the new theme
741 """
742 if not idleConf.userCfg['highlight'].has_section(theme_name):
743 idleConf.userCfg['highlight'].add_section(theme_name)
744 for element in theme:
745 value = theme[element]
746 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
747
748 def delete_custom_theme(self):
749 """Handle event to delete custom theme.
750
751 The current theme is deactivated and the default theme is
752 activated. The custom theme is permanently removed from
753 the config file.
754
755 Attributes accessed:
756 custom_theme
757
758 Attributes updated:
759 radio_theme_custom
760 opt_menu_theme_custom
761 is_builtin_theme
762 builtin_theme
763
764 Methods:
765 deactivate_current_config
766 save_all_changed_extensions
767 activate_config_changes
768 set_theme_type
769 """
770 theme_name = self.custom_theme.get()
771 delmsg = 'Are you sure you wish to delete the theme %r ?'
772 if not tkMessageBox.askyesno(
773 'Delete Theme', delmsg % theme_name, parent=self):
774 return
775 self.deactivate_current_config()
776 # Remove theme from changes, config, and file.
777 changes.delete_section('highlight', theme_name)
778 # Reload user theme list.
779 item_list = idleConf.GetSectionList('user', 'highlight')
780 item_list.sort()
781 if not item_list:
782 self.radio_theme_custom['state'] = DISABLED
783 self.opt_menu_theme_custom.SetMenu(item_list, '- no custom themes -')
784 else:
785 self.opt_menu_theme_custom.SetMenu(item_list, item_list[0])
786 # Revert to default theme.
787 self.is_builtin_theme.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
788 self.builtin_theme.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
789 # User can't back out of these changes, they must be applied now.
790 changes.save_all()
791 self.save_all_changed_extensions()
792 self.activate_config_changes()
793 self.set_theme_type()
794
795
csabellabac7d332017-06-26 17:46:26 -0400796 def create_page_keys(self):
csabella7eb58832017-07-04 21:30:58 -0400797 """Return frame of widgets for Keys tab.
798
csabella36329a42017-07-13 23:32:01 -0400799 Tk Variables:
csabella7eb58832017-07-04 21:30:58 -0400800 builtin_keys: Menu variable for built-in keybindings.
801 custom_keys: Menu variable for custom keybindings.
802 are_keys_builtin: Selector for built-in or custom keybindings.
803 keybinding: Action/key bindings.
csabella36329a42017-07-13 23:32:01 -0400804
805 Methods:
806 load_key_config: Set table.
807 load_keys_list: Reload active set.
808 keybinding_selected: Bound to list_bindings button release.
809 get_new_keys: Command for button_new_keys.
810 get_new_keys_name: Call popup.
811 create_new_key_set: Combine active keyset and changes.
812 set_keys_type: Command for are_keys_builtin.
813 delete_custom_keys: Command for button_delete_custom_keys.
814 save_as_new_key_set: Command for button_save_custom_keys.
815 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
816 deactivate_current_config: Remove keys bindings in editors.
817
818 Widget Structure: (*) widgets bound to self
819 frame
820 frame_custom: LabelFrame
821 frame_target: Frame
822 target_title: Label
823 scroll_target_y: Scrollbar
824 scroll_target_x: Scrollbar
825 (*)list_bindings: ListBox
826 (*)button_new_keys: Button
827 frame_key_sets: LabelFrame
828 frames[0]: Frame
829 (*)radio_keys_builtin: Radiobutton - are_keys_builtin
830 (*)radio_keys_custom: Radiobutton - are_keys_builtin
831 (*)opt_menu_keys_builtin: DynOptionMenu - builtin_keys
832 (*)opt_menu_keys_custom: DynOptionMenu - custom_keys
833 (*)new_custom_keys: Label
834 frames[1]: Frame
835 (*)button_delete_custom_keys: Button
836 button_save_custom_keys: Button
csabella7eb58832017-07-04 21:30:58 -0400837 """
Terry Jan Reedy22405332014-07-30 19:24:32 -0400838 parent = self.parent
csabella5b591542017-07-28 14:40:59 -0400839 self.builtin_keys = tracers.add(
840 StringVar(parent), self.var_changed_builtin_keys)
841 self.custom_keys = tracers.add(
842 StringVar(parent), self.var_changed_custom_keys)
843 self.are_keys_builtin = tracers.add(
844 BooleanVar(parent), self.var_changed_are_keys_builtin)
845 self.keybinding = tracers.add(
846 StringVar(parent), self.var_changed_keybinding)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400847
Terry Jan Reedyb331f802017-07-29 00:49:39 -0400848 # Widget creation:
849 # body and section frames.
850 frame = Frame(self.note)
csabellabac7d332017-06-26 17:46:26 -0400851 frame_custom = LabelFrame(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400852 frame, borderwidth=2, relief=GROOVE,
853 text=' Custom Key Bindings ')
csabellabac7d332017-06-26 17:46:26 -0400854 frame_key_sets = LabelFrame(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400855 frame, borderwidth=2, relief=GROOVE, text=' Key Set ')
csabellabac7d332017-06-26 17:46:26 -0400856 #frame_custom
857 frame_target = Frame(frame_custom)
858 target_title = Label(frame_target, text='Action - Key(s)')
859 scroll_target_y = Scrollbar(frame_target)
860 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
861 self.list_bindings = Listbox(
862 frame_target, takefocus=FALSE, exportselection=FALSE)
863 self.list_bindings.bind('<ButtonRelease-1>', self.keybinding_selected)
864 scroll_target_y.config(command=self.list_bindings.yview)
865 scroll_target_x.config(command=self.list_bindings.xview)
866 self.list_bindings.config(yscrollcommand=scroll_target_y.set)
867 self.list_bindings.config(xscrollcommand=scroll_target_x.set)
868 self.button_new_keys = Button(
869 frame_custom, text='Get New Keys for Selection',
870 command=self.get_new_keys, state=DISABLED)
871 #frame_key_sets
872 frames = [Frame(frame_key_sets, padx=2, pady=2, borderwidth=0)
Christian Heimes9a371592007-12-28 14:08:13 +0000873 for i in range(2)]
csabellabac7d332017-06-26 17:46:26 -0400874 self.radio_keys_builtin = Radiobutton(
875 frames[0], variable=self.are_keys_builtin, value=1,
876 command=self.set_keys_type, text='Use a Built-in Key Set')
877 self.radio_keys_custom = Radiobutton(
878 frames[0], variable=self.are_keys_builtin, value=0,
879 command=self.set_keys_type, text='Use a Custom Key Set')
880 self.opt_menu_keys_builtin = DynOptionMenu(
881 frames[0], self.builtin_keys, None, command=None)
882 self.opt_menu_keys_custom = DynOptionMenu(
883 frames[0], self.custom_keys, None, command=None)
884 self.button_delete_custom_keys = Button(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400885 frames[1], text='Delete Custom Key Set',
csabellabac7d332017-06-26 17:46:26 -0400886 command=self.delete_custom_keys)
887 button_save_custom_keys = Button(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400888 frames[1], text='Save as New Custom Key Set',
csabellabac7d332017-06-26 17:46:26 -0400889 command=self.save_as_new_key_set)
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -0400890 self.new_custom_keys = Label(frames[0], bd=2)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400891
Steven M. Gava60fc7072001-08-04 13:58:22 +0000892 ##widget packing
893 #body
csabellabac7d332017-06-26 17:46:26 -0400894 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
895 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
896 #frame_custom
897 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
898 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
Steven M. Gavafacfc092002-01-19 00:29:54 +0000899 #frame target
csabellabac7d332017-06-26 17:46:26 -0400900 frame_target.columnconfigure(0, weight=1)
901 frame_target.rowconfigure(1, weight=1)
902 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
903 self.list_bindings.grid(row=1, column=0, sticky=NSEW)
904 scroll_target_y.grid(row=1, column=1, sticky=NS)
905 scroll_target_x.grid(row=2, column=0, sticky=EW)
906 #frame_key_sets
907 self.radio_keys_builtin.grid(row=0, column=0, sticky=W+NS)
908 self.radio_keys_custom.grid(row=1, column=0, sticky=W+NS)
909 self.opt_menu_keys_builtin.grid(row=0, column=1, sticky=NSEW)
910 self.opt_menu_keys_custom.grid(row=1, column=1, sticky=NSEW)
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -0400911 self.new_custom_keys.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
csabellabac7d332017-06-26 17:46:26 -0400912 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
913 button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
Christian Heimes9a371592007-12-28 14:08:13 +0000914 frames[0].pack(side=TOP, fill=BOTH, expand=True)
915 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
Steven M. Gava952d0a52001-08-03 04:43:44 +0000916 return frame
917
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400918 def load_key_cfg(self):
919 "Load current configuration settings for the keybinding options."
920 # Set current keys type radiobutton.
921 self.are_keys_builtin.set(idleConf.GetOption(
922 'main', 'Keys', 'default', type='bool', default=1))
923 # Set current keys.
924 current_option = idleConf.CurrentKeys()
925 # Load available keyset option menus.
926 if self.are_keys_builtin.get(): # Default theme selected.
927 item_list = idleConf.GetSectionList('default', 'keys')
928 item_list.sort()
929 self.opt_menu_keys_builtin.SetMenu(item_list, current_option)
930 item_list = idleConf.GetSectionList('user', 'keys')
931 item_list.sort()
932 if not item_list:
933 self.radio_keys_custom['state'] = DISABLED
934 self.custom_keys.set('- no custom keys -')
935 else:
936 self.opt_menu_keys_custom.SetMenu(item_list, item_list[0])
937 else: # User key set selected.
938 item_list = idleConf.GetSectionList('user', 'keys')
939 item_list.sort()
940 self.opt_menu_keys_custom.SetMenu(item_list, current_option)
941 item_list = idleConf.GetSectionList('default', 'keys')
942 item_list.sort()
943 self.opt_menu_keys_builtin.SetMenu(item_list, idleConf.default_keys())
944 self.set_keys_type()
945 # Load keyset element list.
946 keyset_name = idleConf.CurrentKeys()
947 self.load_keys_list(keyset_name)
948
Terry Jan Reedyb1660802017-07-27 18:28:01 -0400949 def var_changed_builtin_keys(self, *params):
950 "Process selection of builtin key set."
951 old_keys = (
952 'IDLE Classic Windows',
953 'IDLE Classic Unix',
954 'IDLE Classic Mac',
955 'IDLE Classic OSX',
956 )
957 value = self.builtin_keys.get()
958 if value not in old_keys:
959 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
960 changes.add_option('main', 'Keys', 'name', old_keys[0])
961 changes.add_option('main', 'Keys', 'name2', value)
962 self.new_custom_keys.config(text='New key set, see Help',
963 fg='#500000')
964 else:
965 changes.add_option('main', 'Keys', 'name', value)
966 changes.add_option('main', 'Keys', 'name2', '')
967 self.new_custom_keys.config(text='', fg='black')
968 self.load_keys_list(value)
969
970 def var_changed_custom_keys(self, *params):
971 "Process selection of custom key set."
972 value = self.custom_keys.get()
973 if value != '- no custom keys -':
974 changes.add_option('main', 'Keys', 'name', value)
975 self.load_keys_list(value)
976
977 def var_changed_are_keys_builtin(self, *params):
978 "Process toggle between builtin key set and custom key set."
979 value = self.are_keys_builtin.get()
980 changes.add_option('main', 'Keys', 'default', value)
981 if value:
982 self.var_changed_builtin_keys()
983 else:
984 self.var_changed_custom_keys()
985
986 def var_changed_keybinding(self, *params):
987 "Store change to a keybinding."
988 value = self.keybinding.get()
989 key_set = self.custom_keys.get()
990 event = self.list_bindings.get(ANCHOR).split()[0]
991 if idleConf.IsCoreBinding(event):
992 changes.add_option('keys', key_set, event, value)
993 else: # Event is an extension binding.
994 ext_name = idleConf.GetExtnNameForEvent(event)
995 ext_keybind_section = ext_name + '_cfgBindings'
996 changes.add_option('extensions', ext_keybind_section, event, value)
997
998 def set_keys_type(self):
999 "Set available screen options based on builtin or custom key set."
1000 if self.are_keys_builtin.get():
1001 self.opt_menu_keys_builtin['state'] = NORMAL
1002 self.opt_menu_keys_custom['state'] = DISABLED
1003 self.button_delete_custom_keys['state'] = DISABLED
1004 else:
1005 self.opt_menu_keys_builtin['state'] = DISABLED
1006 self.radio_keys_custom['state'] = NORMAL
1007 self.opt_menu_keys_custom['state'] = NORMAL
1008 self.button_delete_custom_keys['state'] = NORMAL
1009
1010 def get_new_keys(self):
1011 """Handle event to change key binding for selected line.
1012
1013 A selection of a key/binding in the list of current
1014 bindings pops up a dialog to enter a new binding. If
1015 the current key set is builtin and a binding has
1016 changed, then a name for a custom key set needs to be
1017 entered for the change to be applied.
1018 """
1019 list_index = self.list_bindings.index(ANCHOR)
1020 binding = self.list_bindings.get(list_index)
1021 bind_name = binding.split()[0]
1022 if self.are_keys_builtin.get():
1023 current_key_set_name = self.builtin_keys.get()
1024 else:
1025 current_key_set_name = self.custom_keys.get()
1026 current_bindings = idleConf.GetCurrentKeySet()
1027 if current_key_set_name in changes['keys']: # unsaved changes
1028 key_set_changes = changes['keys'][current_key_set_name]
1029 for event in key_set_changes:
1030 current_bindings[event] = key_set_changes[event].split()
1031 current_key_sequences = list(current_bindings.values())
1032 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1033 current_key_sequences).result
1034 if new_keys:
1035 if self.are_keys_builtin.get(): # Current key set is a built-in.
1036 message = ('Your changes will be saved as a new Custom Key Set.'
1037 ' Enter a name for your new Custom Key Set below.')
1038 new_keyset = self.get_new_keys_name(message)
1039 if not new_keyset: # User cancelled custom key set creation.
1040 self.list_bindings.select_set(list_index)
1041 self.list_bindings.select_anchor(list_index)
1042 return
1043 else: # Create new custom key set based on previously active key set.
1044 self.create_new_key_set(new_keyset)
1045 self.list_bindings.delete(list_index)
1046 self.list_bindings.insert(list_index, bind_name+' - '+new_keys)
1047 self.list_bindings.select_set(list_index)
1048 self.list_bindings.select_anchor(list_index)
1049 self.keybinding.set(new_keys)
1050 else:
1051 self.list_bindings.select_set(list_index)
1052 self.list_bindings.select_anchor(list_index)
1053
1054 def get_new_keys_name(self, message):
1055 "Return new key set name from query popup."
1056 used_names = (idleConf.GetSectionList('user', 'keys') +
1057 idleConf.GetSectionList('default', 'keys'))
1058 new_keyset = SectionName(
1059 self, 'New Custom Key Set', message, used_names).result
1060 return new_keyset
1061
1062 def save_as_new_key_set(self):
1063 "Prompt for name of new key set and save changes using that name."
1064 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1065 if new_keys_name:
1066 self.create_new_key_set(new_keys_name)
1067
1068 def keybinding_selected(self, event):
1069 "Activate button to assign new keys to selected action."
1070 self.button_new_keys['state'] = NORMAL
1071
1072 def create_new_key_set(self, new_key_set_name):
1073 """Create a new custom key set with the given name.
1074
1075 Create the new key set based on the previously active set
1076 with the current changes applied. Once it is saved, then
1077 activate the new key set.
1078 """
1079 if self.are_keys_builtin.get():
1080 prev_key_set_name = self.builtin_keys.get()
1081 else:
1082 prev_key_set_name = self.custom_keys.get()
1083 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1084 new_keys = {}
1085 for event in prev_keys: # Add key set to changed items.
1086 event_name = event[2:-2] # Trim off the angle brackets.
1087 binding = ' '.join(prev_keys[event])
1088 new_keys[event_name] = binding
1089 # Handle any unsaved changes to prev key set.
1090 if prev_key_set_name in changes['keys']:
1091 key_set_changes = changes['keys'][prev_key_set_name]
1092 for event in key_set_changes:
1093 new_keys[event] = key_set_changes[event]
1094 # Save the new key set.
1095 self.save_new_key_set(new_key_set_name, new_keys)
1096 # Change GUI over to the new key set.
1097 custom_key_list = idleConf.GetSectionList('user', 'keys')
1098 custom_key_list.sort()
1099 self.opt_menu_keys_custom.SetMenu(custom_key_list, new_key_set_name)
1100 self.are_keys_builtin.set(0)
1101 self.set_keys_type()
1102
1103 def load_keys_list(self, keyset_name):
1104 """Reload the list of action/key binding pairs for the active key set.
1105
1106 An action/key binding can be selected to change the key binding.
1107 """
1108 reselect = 0
1109 if self.list_bindings.curselection():
1110 reselect = 1
1111 list_index = self.list_bindings.index(ANCHOR)
1112 keyset = idleConf.GetKeySet(keyset_name)
1113 bind_names = list(keyset.keys())
1114 bind_names.sort()
1115 self.list_bindings.delete(0, END)
1116 for bind_name in bind_names:
1117 key = ' '.join(keyset[bind_name])
1118 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1119 if keyset_name in changes['keys']:
1120 # Handle any unsaved changes to this key set.
1121 if bind_name in changes['keys'][keyset_name]:
1122 key = changes['keys'][keyset_name][bind_name]
1123 self.list_bindings.insert(END, bind_name+' - '+key)
1124 if reselect:
1125 self.list_bindings.see(list_index)
1126 self.list_bindings.select_set(list_index)
1127 self.list_bindings.select_anchor(list_index)
1128
1129 def save_new_key_set(self, keyset_name, keyset):
1130 """Save a newly created core key set.
1131
1132 keyset_name - string, the name of the new key set
1133 keyset - dictionary containing the new key set
1134 """
1135 if not idleConf.userCfg['keys'].has_section(keyset_name):
1136 idleConf.userCfg['keys'].add_section(keyset_name)
1137 for event in keyset:
1138 value = keyset[event]
1139 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1140
1141 def delete_custom_keys(self):
1142 """Handle event to delete a custom key set.
1143
1144 Applying the delete deactivates the current configuration and
1145 reverts to the default. The custom key set is permanently
1146 deleted from the config file.
1147 """
1148 keyset_name=self.custom_keys.get()
1149 delmsg = 'Are you sure you wish to delete the key set %r ?'
1150 if not tkMessageBox.askyesno(
1151 'Delete Key Set', delmsg % keyset_name, parent=self):
1152 return
1153 self.deactivate_current_config()
1154 # Remove key set from changes, config, and file.
1155 changes.delete_section('keys', keyset_name)
1156 # Reload user key set list.
1157 item_list = idleConf.GetSectionList('user', 'keys')
1158 item_list.sort()
1159 if not item_list:
1160 self.radio_keys_custom['state'] = DISABLED
1161 self.opt_menu_keys_custom.SetMenu(item_list, '- no custom keys -')
1162 else:
1163 self.opt_menu_keys_custom.SetMenu(item_list, item_list[0])
1164 # Revert to default key set.
1165 self.are_keys_builtin.set(idleConf.defaultCfg['main']
1166 .Get('Keys', 'default'))
1167 self.builtin_keys.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1168 or idleConf.default_keys())
1169 # User can't back out of these changes, they must be applied now.
1170 changes.save_all()
1171 self.save_all_changed_extensions()
1172 self.activate_config_changes()
1173 self.set_keys_type()
1174
1175 def deactivate_current_config(self):
1176 """Remove current key bindings.
1177
1178 Iterate over window instances defined in parent and remove
1179 the keybindings.
1180 """
1181 # Before a config is saved, some cleanup of current
1182 # config must be done - remove the previous keybindings.
1183 win_instances = self.parent.instance_dict.keys()
1184 for instance in win_instances:
1185 instance.RemoveKeybindings()
1186
1187 def activate_config_changes(self):
1188 """Apply configuration changes to current windows.
1189
1190 Dynamically update the current parent window instances
1191 with some of the configuration changes.
1192 """
1193 win_instances = self.parent.instance_dict.keys()
1194 for instance in win_instances:
1195 instance.ResetColorizer()
1196 instance.ResetFont()
1197 instance.set_notabs_indentwidth()
1198 instance.ApplyKeybindings()
1199 instance.reset_help_menu_entries()
1200
csabellabac7d332017-06-26 17:46:26 -04001201 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001202 """Part of the config dialog used for configuring IDLE extensions.
1203
1204 This code is generic - it works for any and all IDLE extensions.
1205
1206 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -04001207 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001208 GUI interface to change the configuration values, and saves the
1209 changes using idleConf.
1210
1211 Not all changes take effect immediately - some may require restarting IDLE.
1212 This depends on each extension's implementation.
1213
1214 All values are treated as text, and it is up to the user to supply
1215 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +03001216 which are boolean, and can be toggled with a True/False button.
csabella36329a42017-07-13 23:32:01 -04001217
1218 Methods:
1219 load_extentions:
1220 extension_selected: Handle selection from list.
1221 create_extension_frame: Hold widgets for one extension.
1222 set_extension_value: Set in userCfg['extensions'].
1223 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001224 """
1225 parent = self.parent
Terry Jan Reedyb331f802017-07-29 00:49:39 -04001226 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001227 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
1228 self.ext_userCfg = idleConf.userCfg['extensions']
1229 self.is_int = self.register(is_int)
1230 self.load_extensions()
csabella7eb58832017-07-04 21:30:58 -04001231 # Create widgets - a listbox shows all available extensions, with the
1232 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001233 self.extension_names = StringVar(self)
1234 frame.rowconfigure(0, weight=1)
1235 frame.columnconfigure(2, weight=1)
1236 self.extension_list = Listbox(frame, listvariable=self.extension_names,
1237 selectmode='browse')
1238 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
1239 scroll = Scrollbar(frame, command=self.extension_list.yview)
1240 self.extension_list.yscrollcommand=scroll.set
1241 self.details_frame = LabelFrame(frame, width=250, height=250)
1242 self.extension_list.grid(column=0, row=0, sticky='nws')
1243 scroll.grid(column=1, row=0, sticky='ns')
1244 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
1245 frame.configure(padx=10, pady=10)
1246 self.config_frame = {}
1247 self.current_extension = None
1248
1249 self.outerframe = self # TEMPORARY
1250 self.tabbed_page_set = self.extension_list # TEMPORARY
1251
csabella7eb58832017-07-04 21:30:58 -04001252 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001253 ext_names = ''
1254 for ext_name in sorted(self.extensions):
1255 self.create_extension_frame(ext_name)
1256 ext_names = ext_names + '{' + ext_name + '} '
1257 self.extension_names.set(ext_names)
1258 self.extension_list.selection_set(0)
1259 self.extension_selected(None)
1260
Terry Jan Reedyb331f802017-07-29 00:49:39 -04001261 return frame
1262
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001263 def load_extensions(self):
1264 "Fill self.extensions with data from the default and user configs."
1265 self.extensions = {}
1266 for ext_name in idleConf.GetExtensions(active_only=False):
1267 self.extensions[ext_name] = []
1268
1269 for ext_name in self.extensions:
1270 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
1271
csabella7eb58832017-07-04 21:30:58 -04001272 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001273 enables = [opt_name for opt_name in opt_list
1274 if opt_name.startswith('enable')]
1275 for opt_name in enables:
1276 opt_list.remove(opt_name)
1277 opt_list = enables + opt_list
1278
1279 for opt_name in opt_list:
1280 def_str = self.ext_defaultCfg.Get(
1281 ext_name, opt_name, raw=True)
1282 try:
1283 def_obj = {'True':True, 'False':False}[def_str]
1284 opt_type = 'bool'
1285 except KeyError:
1286 try:
1287 def_obj = int(def_str)
1288 opt_type = 'int'
1289 except ValueError:
1290 def_obj = def_str
1291 opt_type = None
1292 try:
1293 value = self.ext_userCfg.Get(
1294 ext_name, opt_name, type=opt_type, raw=True,
1295 default=def_obj)
csabella7eb58832017-07-04 21:30:58 -04001296 except ValueError: # Need this until .Get fixed.
1297 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001298 var = StringVar(self)
1299 var.set(str(value))
1300
1301 self.extensions[ext_name].append({'name': opt_name,
1302 'type': opt_type,
1303 'default': def_str,
1304 'value': value,
1305 'var': var,
1306 })
1307
1308 def extension_selected(self, event):
csabella7eb58832017-07-04 21:30:58 -04001309 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001310 newsel = self.extension_list.curselection()
1311 if newsel:
1312 newsel = self.extension_list.get(newsel)
1313 if newsel is None or newsel != self.current_extension:
1314 if self.current_extension:
1315 self.details_frame.config(text='')
1316 self.config_frame[self.current_extension].grid_forget()
1317 self.current_extension = None
1318 if newsel:
1319 self.details_frame.config(text=newsel)
1320 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
1321 self.current_extension = newsel
1322
1323 def create_extension_frame(self, ext_name):
1324 """Create a frame holding the widgets to configure one extension"""
1325 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
1326 self.config_frame[ext_name] = f
1327 entry_area = f.interior
csabella7eb58832017-07-04 21:30:58 -04001328 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001329 for row, opt in enumerate(self.extensions[ext_name]):
csabella7eb58832017-07-04 21:30:58 -04001330 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001331 label = Label(entry_area, text=opt['name'])
1332 label.grid(row=row, column=0, sticky=NW)
1333 var = opt['var']
1334 if opt['type'] == 'bool':
1335 Checkbutton(entry_area, textvariable=var, variable=var,
1336 onvalue='True', offvalue='False',
1337 indicatoron=FALSE, selectcolor='', width=8
1338 ).grid(row=row, column=1, sticky=W, padx=7)
1339 elif opt['type'] == 'int':
1340 Entry(entry_area, textvariable=var, validate='key',
1341 validatecommand=(self.is_int, '%P')
1342 ).grid(row=row, column=1, sticky=NSEW, padx=7)
1343
1344 else:
1345 Entry(entry_area, textvariable=var
1346 ).grid(row=row, column=1, sticky=NSEW, padx=7)
1347 return
1348
1349 def set_extension_value(self, section, opt):
csabella7eb58832017-07-04 21:30:58 -04001350 """Return True if the configuration was added or changed.
1351
1352 If the value is the same as the default, then remove it
1353 from user config file.
1354 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001355 name = opt['name']
1356 default = opt['default']
1357 value = opt['var'].get().strip() or default
1358 opt['var'].set(value)
1359 # if self.defaultCfg.has_section(section):
csabella7eb58832017-07-04 21:30:58 -04001360 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001361 if (value == default):
1362 return self.ext_userCfg.RemoveOption(section, name)
csabella7eb58832017-07-04 21:30:58 -04001363 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001364 return self.ext_userCfg.SetOption(section, name, value)
1365
1366 def save_all_changed_extensions(self):
csabella36329a42017-07-13 23:32:01 -04001367 """Save configuration changes to the user config file.
1368
1369 Attributes accessed:
1370 extensions
1371
1372 Methods:
1373 set_extension_value
1374 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001375 has_changes = False
1376 for ext_name in self.extensions:
1377 options = self.extensions[ext_name]
1378 for opt in options:
1379 if self.set_extension_value(ext_name, opt):
1380 has_changes = True
1381 if has_changes:
1382 self.ext_userCfg.Save()
1383
1384
csabella9397e2a2017-07-30 13:34:25 -04001385class FontPage(Frame):
1386
1387 def __init__(self, parent, highpage):
1388 super().__init__(parent)
1389 self.parent = parent
1390 self.highlight_sample = highpage.highlight_sample
1391 self.create_page_font_tab()
1392 self.load_font_cfg()
1393 self.load_tab_cfg()
1394
1395 def create_page_font_tab(self):
1396 """Return frame of widgets for Font/Tabs tab.
1397
1398 Fonts: Enable users to provisionally change font face, size, or
1399 boldness and to see the consequence of proposed choices. Each
1400 action set 3 options in changes structuree and changes the
1401 corresponding aspect of the font sample on this page and
1402 highlight sample on highlight page.
1403
1404 Function load_font_cfg initializes font vars and widgets from
1405 idleConf entries and tk.
1406
1407 Fontlist: mouse button 1 click or up or down key invoke
1408 on_fontlist_select(), which sets var font_name.
1409
1410 Sizelist: clicking the menubutton opens the dropdown menu. A
1411 mouse button 1 click or return key sets var font_size.
1412
1413 Bold_toggle: clicking the box toggles var font_bold.
1414
1415 Changing any of the font vars invokes var_changed_font, which
1416 adds all 3 font options to changes and calls set_samples.
1417 Set_samples applies a new font constructed from the font vars to
1418 font_sample and to highlight_sample on the hightlight page.
1419
1420 Tabs: Enable users to change spaces entered for indent tabs.
1421 Changing indent_scale value with the mouse sets Var space_num,
1422 which invokes the default callback to add an entry to
1423 changes. Load_tab_cfg initializes space_num to default.
1424
1425 Widget Structure: (*) widgets bound to self
1426 frame (of tab_pages)
1427 frame_font: LabelFrame
1428 frame_font_name: Frame
1429 font_name_title: Label
1430 (*)fontlist: ListBox - font_name
1431 scroll_font: Scrollbar
1432 frame_font_param: Frame
1433 font_size_title: Label
1434 (*)sizelist: DynOptionMenu - font_size
1435 (*)bold_toggle: Checkbutton - font_bold
1436 frame_font_sample: Frame
1437 (*)font_sample: Label
1438 frame_indent: LabelFrame
1439 indent_title: Label
1440 (*)indent_scale: Scale - space_num
1441 """
1442 parent = self.parent
1443 self.font_name = tracers.add(StringVar(parent), self.var_changed_font)
1444 self.font_size = tracers.add(StringVar(parent), self.var_changed_font)
1445 self.font_bold = tracers.add(BooleanVar(parent), self.var_changed_font)
1446 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
1447
1448 # Create widgets:
1449 # body and body section frames.
1450 frame = self
1451 frame_font = LabelFrame(
1452 frame, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
1453 frame_indent = LabelFrame(
1454 frame, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
1455 # frame_font.
1456 frame_font_name = Frame(frame_font)
1457 frame_font_param = Frame(frame_font)
1458 font_name_title = Label(
1459 frame_font_name, justify=LEFT, text='Font Face :')
1460 self.fontlist = Listbox(frame_font_name, height=5,
1461 takefocus=True, exportselection=FALSE)
1462 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
1463 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
1464 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
1465 scroll_font = Scrollbar(frame_font_name)
1466 scroll_font.config(command=self.fontlist.yview)
1467 self.fontlist.config(yscrollcommand=scroll_font.set)
1468 font_size_title = Label(frame_font_param, text='Size :')
1469 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
1470 self.bold_toggle = Checkbutton(
1471 frame_font_param, variable=self.font_bold,
1472 onvalue=1, offvalue=0, text='Bold')
1473 frame_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1)
1474 temp_font = tkFont.Font(parent, ('courier', 10, 'normal'))
1475 self.font_sample = Label(
1476 frame_font_sample, justify=LEFT, font=temp_font,
1477 text='AaBbCcDdEe\nFfGgHhIiJj\n1234567890\n#:+=(){}[]')
1478 # frame_indent.
1479 indent_title = Label(
1480 frame_indent, justify=LEFT,
1481 text='Python Standard: 4 Spaces!')
1482 self.indent_scale = Scale(
1483 frame_indent, variable=self.space_num,
1484 orient='horizontal', tickinterval=2, from_=2, to=16)
1485
1486 # Pack widgets:
1487 # body.
1488 frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1489 frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y)
1490 # frame_font.
1491 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
1492 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
1493 font_name_title.pack(side=TOP, anchor=W)
1494 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
1495 scroll_font.pack(side=LEFT, fill=Y)
1496 font_size_title.pack(side=LEFT, anchor=W)
1497 self.sizelist.pack(side=LEFT, anchor=W)
1498 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
1499 frame_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1500 self.font_sample.pack(expand=TRUE, fill=BOTH)
1501 # frame_indent.
1502 frame_indent.pack(side=TOP, fill=X)
1503 indent_title.pack(side=TOP, anchor=W, padx=5)
1504 self.indent_scale.pack(side=TOP, padx=5, fill=X)
1505
1506 return frame
1507
1508 def load_font_cfg(self):
1509 """Load current configuration settings for the font options.
1510
1511 Retrieve current font with idleConf.GetFont and font families
1512 from tk. Setup fontlist and set font_name. Setup sizelist,
1513 which sets font_size. Set font_bold. Call set_samples.
1514 """
1515 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
1516 font_name = configured_font[0].lower()
1517 font_size = configured_font[1]
1518 font_bold = configured_font[2]=='bold'
1519
1520 # Set editor font selection list and font_name.
1521 fonts = list(tkFont.families(self))
1522 fonts.sort()
1523 for font in fonts:
1524 self.fontlist.insert(END, font)
1525 self.font_name.set(font_name)
1526 lc_fonts = [s.lower() for s in fonts]
1527 try:
1528 current_font_index = lc_fonts.index(font_name)
1529 self.fontlist.see(current_font_index)
1530 self.fontlist.select_set(current_font_index)
1531 self.fontlist.select_anchor(current_font_index)
1532 self.fontlist.activate(current_font_index)
1533 except ValueError:
1534 pass
1535 # Set font size dropdown.
1536 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
1537 '16', '18', '20', '22', '25', '29', '34', '40'),
1538 font_size)
1539 # Set font weight.
1540 self.font_bold.set(font_bold)
1541 self.set_samples()
1542
1543 def var_changed_font(self, *params):
1544 """Store changes to font attributes.
1545
1546 When one font attribute changes, save them all, as they are
1547 not independent from each other. In particular, when we are
1548 overriding the default font, we need to write out everything.
1549 """
1550 value = self.font_name.get()
1551 changes.add_option('main', 'EditorWindow', 'font', value)
1552 value = self.font_size.get()
1553 changes.add_option('main', 'EditorWindow', 'font-size', value)
1554 value = self.font_bold.get()
1555 changes.add_option('main', 'EditorWindow', 'font-bold', value)
1556 self.set_samples()
1557
1558 def on_fontlist_select(self, event):
1559 """Handle selecting a font from the list.
1560
1561 Event can result from either mouse click or Up or Down key.
1562 Set font_name and example displays to selection.
1563 """
1564 font = self.fontlist.get(
1565 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
1566 self.font_name.set(font.lower())
1567
1568 def set_samples(self, event=None):
1569 """Update update both screen samples with the font settings.
1570
1571 Called on font initialization and change events.
1572 Accesses font_name, font_size, and font_bold Variables.
1573 Updates font_sample and hightlight page highlight_sample.
1574 """
1575 font_name = self.font_name.get()
1576 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
1577 new_font = (font_name, self.font_size.get(), font_weight)
1578 self.font_sample['font'] = new_font
1579 self.highlight_sample['font'] = new_font
1580
1581 def load_tab_cfg(self):
1582 """Load current configuration settings for the tab options.
1583
1584 Attributes updated:
1585 space_num: Set to value from idleConf.
1586 """
1587 # Set indent sizes.
1588 space_num = idleConf.GetOption(
1589 'main', 'Indent', 'num-spaces', default=4, type='int')
1590 self.space_num.set(space_num)
1591
1592 def var_changed_space_num(self, *params):
1593 "Store change to indentation size."
1594 value = self.space_num.get()
1595 changes.add_option('main', 'Indent', 'num-spaces', value)
1596
1597
csabellae8eb17b2017-07-30 18:39:17 -04001598class GenPage(Frame):
1599
1600 def __init__(self, parent):
1601 super().__init__(parent)
1602 self.create_page_general()
1603 self.load_general_cfg()
1604
1605 def create_page_general(self):
1606 """Return frame of widgets for General tab.
1607
1608 Enable users to provisionally change general options. Function
1609 load_general_cfg intializes tk variables and helplist using
1610 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1611 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1612 set var autosave. Entry boxes win_width_int and win_height_int
1613 set var win_width and win_height. Setting var_name invokes the
1614 default callback that adds option to changes.
1615
1616 Helplist: load_general_cfg loads list user_helplist with
1617 name, position pairs and copies names to listbox helplist.
1618 Clicking a name invokes help_source selected. Clicking
1619 button_helplist_name invokes helplist_item_name, which also
1620 changes user_helplist. These functions all call
1621 set_add_delete_state. All but load call update_help_changes to
1622 rewrite changes['main']['HelpFiles'].
1623
1624 Widget Structure: (*) widgets bound to self
1625 frame
1626 frame_run: LabelFrame
1627 startup_title: Label
1628 (*)startup_editor_on: Radiobutton - startup_edit
1629 (*)startup_shell_on: Radiobutton - startup_edit
1630 frame_save: LabelFrame
1631 run_save_title: Label
1632 (*)save_ask_on: Radiobutton - autosave
1633 (*)save_auto_on: Radiobutton - autosave
1634 frame_win_size: LabelFrame
1635 win_size_title: Label
1636 win_width_title: Label
1637 (*)win_width_int: Entry - win_width
1638 win_height_title: Label
1639 (*)win_height_int: Entry - win_height
1640 frame_help: LabelFrame
1641 frame_helplist: Frame
1642 frame_helplist_buttons: Frame
1643 (*)button_helplist_edit
1644 (*)button_helplist_add
1645 (*)button_helplist_remove
1646 (*)helplist: ListBox
1647 scroll_helplist: Scrollbar
1648 """
1649 self.startup_edit = tracers.add(
1650 IntVar(self), ('main', 'General', 'editor-on-startup'))
1651 self.autosave = tracers.add(
1652 IntVar(self), ('main', 'General', 'autosave'))
1653 self.win_width = tracers.add(
1654 StringVar(self), ('main', 'EditorWindow', 'width'))
1655 self.win_height = tracers.add(
1656 StringVar(self), ('main', 'EditorWindow', 'height'))
1657
1658 # Create widgets:
1659 # Section frames.
1660 frame_run = LabelFrame(self, borderwidth=2, relief=GROOVE,
1661 text=' Startup Preferences ')
1662 frame_save = LabelFrame(self, borderwidth=2, relief=GROOVE,
1663 text=' autosave Preferences ')
1664 frame_win_size = Frame(self, borderwidth=2, relief=GROOVE)
1665 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1666 text=' Additional Help Sources ')
1667 # frame_run.
1668 startup_title = Label(frame_run, text='At Startup')
1669 self.startup_editor_on = Radiobutton(
1670 frame_run, variable=self.startup_edit, value=1,
1671 text="Open Edit Window")
1672 self.startup_shell_on = Radiobutton(
1673 frame_run, variable=self.startup_edit, value=0,
1674 text='Open Shell Window')
1675 # frame_save.
1676 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1677 self.save_ask_on = Radiobutton(
1678 frame_save, variable=self.autosave, value=0,
1679 text="Prompt to Save")
1680 self.save_auto_on = Radiobutton(
1681 frame_save, variable=self.autosave, value=1,
1682 text='No Prompt')
1683 # frame_win_size.
1684 win_size_title = Label(
1685 frame_win_size, text='Initial Window Size (in characters)')
1686 win_width_title = Label(frame_win_size, text='Width')
1687 self.win_width_int = Entry(
1688 frame_win_size, textvariable=self.win_width, width=3)
1689 win_height_title = Label(frame_win_size, text='Height')
1690 self.win_height_int = Entry(
1691 frame_win_size, textvariable=self.win_height, width=3)
1692 # frame_help.
1693 frame_helplist = Frame(frame_help)
1694 frame_helplist_buttons = Frame(frame_helplist)
1695 self.helplist = Listbox(
1696 frame_helplist, height=5, takefocus=True,
1697 exportselection=FALSE)
1698 scroll_helplist = Scrollbar(frame_helplist)
1699 scroll_helplist['command'] = self.helplist.yview
1700 self.helplist['yscrollcommand'] = scroll_helplist.set
1701 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1702 self.button_helplist_edit = Button(
1703 frame_helplist_buttons, text='Edit', state=DISABLED,
1704 width=8, command=self.helplist_item_edit)
1705 self.button_helplist_add = Button(
1706 frame_helplist_buttons, text='Add',
1707 width=8, command=self.helplist_item_add)
1708 self.button_helplist_remove = Button(
1709 frame_helplist_buttons, text='Remove', state=DISABLED,
1710 width=8, command=self.helplist_item_remove)
1711
1712 # Pack widgets:
1713 # body.
1714 frame_run.pack(side=TOP, padx=5, pady=5, fill=X)
1715 frame_save.pack(side=TOP, padx=5, pady=5, fill=X)
1716 frame_win_size.pack(side=TOP, padx=5, pady=5, fill=X)
1717 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1718 # frame_run.
1719 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1720 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1721 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1722 # frame_save.
1723 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1724 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1725 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1726 # frame_win_size.
1727 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1728 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1729 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1730 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1731 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
1732 # frame_help.
1733 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1734 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1735 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1736 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1737 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1738 self.button_helplist_add.pack(side=TOP, anchor=W)
1739 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1740
1741 def load_general_cfg(self):
1742 "Load current configuration settings for the general options."
1743 # Set startup state.
1744 self.startup_edit.set(idleConf.GetOption(
1745 'main', 'General', 'editor-on-startup', default=0, type='bool'))
1746 # Set autosave state.
1747 self.autosave.set(idleConf.GetOption(
1748 'main', 'General', 'autosave', default=0, type='bool'))
1749 # Set initial window size.
1750 self.win_width.set(idleConf.GetOption(
1751 'main', 'EditorWindow', 'width', type='int'))
1752 self.win_height.set(idleConf.GetOption(
1753 'main', 'EditorWindow', 'height', type='int'))
1754 # Set additional help sources.
1755 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
1756 self.helplist.delete(0, 'end')
1757 for help_item in self.user_helplist:
1758 self.helplist.insert(END, help_item[0])
1759 self.set_add_delete_state()
1760
1761 def help_source_selected(self, event):
1762 "Handle event for selecting additional help."
1763 self.set_add_delete_state()
1764
1765 def set_add_delete_state(self):
1766 "Toggle the state for the help list buttons based on list entries."
1767 if self.helplist.size() < 1: # No entries in list.
1768 self.button_helplist_edit['state'] = DISABLED
1769 self.button_helplist_remove['state'] = DISABLED
1770 else: # Some entries.
1771 if self.helplist.curselection(): # There currently is a selection.
1772 self.button_helplist_edit['state'] = NORMAL
1773 self.button_helplist_remove['state'] = NORMAL
1774 else: # There currently is not a selection.
1775 self.button_helplist_edit['state'] = DISABLED
1776 self.button_helplist_remove['state'] = DISABLED
1777
1778 def helplist_item_add(self):
1779 """Handle add button for the help list.
1780
1781 Query for name and location of new help sources and add
1782 them to the list.
1783 """
1784 help_source = HelpSource(self, 'New Help Source').result
1785 if help_source:
1786 self.user_helplist.append(help_source)
1787 self.helplist.insert(END, help_source[0])
1788 self.update_help_changes()
1789
1790 def helplist_item_edit(self):
1791 """Handle edit button for the help list.
1792
1793 Query with existing help source information and update
1794 config if the values are changed.
1795 """
1796 item_index = self.helplist.index(ANCHOR)
1797 help_source = self.user_helplist[item_index]
1798 new_help_source = HelpSource(
1799 self, 'Edit Help Source',
1800 menuitem=help_source[0],
1801 filepath=help_source[1],
1802 ).result
1803 if new_help_source and new_help_source != help_source:
1804 self.user_helplist[item_index] = new_help_source
1805 self.helplist.delete(item_index)
1806 self.helplist.insert(item_index, new_help_source[0])
1807 self.update_help_changes()
1808 self.set_add_delete_state() # Selected will be un-selected
1809
1810 def helplist_item_remove(self):
1811 """Handle remove button for the help list.
1812
1813 Delete the help list item from config.
1814 """
1815 item_index = self.helplist.index(ANCHOR)
1816 del(self.user_helplist[item_index])
1817 self.helplist.delete(item_index)
1818 self.update_help_changes()
1819 self.set_add_delete_state()
1820
1821 def update_help_changes(self):
1822 "Clear and rebuild the HelpFiles section in changes"
1823 changes['main']['HelpFiles'] = {}
1824 for num in range(1, len(self.user_helplist) + 1):
1825 changes.add_option(
1826 'main', 'HelpFiles', str(num),
1827 ';'.join(self.user_helplist[num-1][:2]))
1828
1829
csabella45bf7232017-07-26 19:09:58 -04001830class VarTrace:
1831 """Maintain Tk variables trace state."""
1832
1833 def __init__(self):
1834 """Store Tk variables and callbacks.
1835
1836 untraced: List of tuples (var, callback)
1837 that do not have the callback attached
1838 to the Tk var.
1839 traced: List of tuples (var, callback) where
1840 that callback has been attached to the var.
1841 """
1842 self.untraced = []
1843 self.traced = []
1844
Terry Jan Reedy5d0f30a2017-07-28 17:00:02 -04001845 def clear(self):
1846 "Clear lists (for tests)."
1847 self.untraced.clear()
1848 self.traced.clear()
1849
csabella45bf7232017-07-26 19:09:58 -04001850 def add(self, var, callback):
1851 """Add (var, callback) tuple to untraced list.
1852
1853 Args:
1854 var: Tk variable instance.
csabella5b591542017-07-28 14:40:59 -04001855 callback: Either function name to be used as a callback
1856 or a tuple with IdleConf config-type, section, and
1857 option names used in the default callback.
csabella45bf7232017-07-26 19:09:58 -04001858
1859 Return:
1860 Tk variable instance.
1861 """
1862 if isinstance(callback, tuple):
1863 callback = self.make_callback(var, callback)
1864 self.untraced.append((var, callback))
1865 return var
1866
1867 @staticmethod
1868 def make_callback(var, config):
1869 "Return default callback function to add values to changes instance."
1870 def default_callback(*params):
1871 "Add config values to changes instance."
1872 changes.add_option(*config, var.get())
1873 return default_callback
1874
1875 def attach(self):
1876 "Attach callback to all vars that are not traced."
1877 while self.untraced:
1878 var, callback = self.untraced.pop()
1879 var.trace_add('write', callback)
1880 self.traced.append((var, callback))
1881
1882 def detach(self):
1883 "Remove callback from traced vars."
1884 while self.traced:
1885 var, callback = self.traced.pop()
1886 var.trace_remove('write', var.trace_info()[0][1])
1887 self.untraced.append((var, callback))
1888
1889
csabella5b591542017-07-28 14:40:59 -04001890tracers = VarTrace()
1891
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001892help_common = '''\
1893When you click either the Apply or Ok buttons, settings in this
1894dialog that are different from IDLE's default are saved in
1895a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05001896these changes apply to all versions of IDLE installed on this
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001897machine. Some do not take affect until IDLE is restarted.
1898[Cancel] only cancels changes made since the last save.
1899'''
1900help_pages = {
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04001901 'Highlighting': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001902Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05001903The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001904be used with older IDLE releases if it is saved as a custom
1905theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04001906''',
1907 'Keys': '''
1908Keys:
1909The IDLE Modern Unix key set is new in June 2016. It can only
1910be used with older IDLE releases if it is saved as a custom
1911key set, with a different name.
1912''',
wohlgangerfae2c352017-06-27 21:36:23 -05001913 'Extensions': '''
1914Extensions:
1915
1916Autocomplete: Popupwait is milleseconds to wait after key char, without
1917cursor movement, before popping up completion box. Key char is '.' after
1918identifier or a '/' (or '\\' on Windows) within a string.
1919
1920FormatParagraph: Max-width is max chars in lines after re-formatting.
1921Use with paragraphs in both strings and comment blocks.
1922
1923ParenMatch: Style indicates what is highlighted when closer is entered:
1924'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
1925'expression' (default) - also everything in between. Flash-delay is how
1926long to highlight if cursor is not moved (0 means forever).
1927'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001928}
1929
Steven M. Gavac11ccf32001-09-24 09:43:17 +00001930
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001931def is_int(s):
1932 "Return 's is blank or represents an int'"
1933 if not s:
1934 return True
1935 try:
1936 int(s)
1937 return True
1938 except ValueError:
1939 return False
1940
1941
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001942class VerticalScrolledFrame(Frame):
1943 """A pure Tkinter vertically scrollable frame.
1944
1945 * Use the 'interior' attribute to place widgets inside the scrollable frame
1946 * Construct and pack/place/grid normally
1947 * This frame only allows vertical scrolling
1948 """
1949 def __init__(self, parent, *args, **kw):
1950 Frame.__init__(self, parent, *args, **kw)
1951
csabella7eb58832017-07-04 21:30:58 -04001952 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001953 vscrollbar = Scrollbar(self, orient=VERTICAL)
1954 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
1955 canvas = Canvas(self, bd=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04001956 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001957 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
1958 vscrollbar.config(command=canvas.yview)
1959
csabella7eb58832017-07-04 21:30:58 -04001960 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001961 canvas.xview_moveto(0)
1962 canvas.yview_moveto(0)
1963
csabella7eb58832017-07-04 21:30:58 -04001964 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001965 self.interior = interior = Frame(canvas)
1966 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
1967
csabella7eb58832017-07-04 21:30:58 -04001968 # Track changes to the canvas and frame width and sync them,
1969 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001970 def _configure_interior(event):
csabella7eb58832017-07-04 21:30:58 -04001971 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001972 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
1973 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001974 interior.bind('<Configure>', _configure_interior)
1975
1976 def _configure_canvas(event):
1977 if interior.winfo_reqwidth() != canvas.winfo_width():
csabella7eb58832017-07-04 21:30:58 -04001978 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001979 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
1980 canvas.bind('<Configure>', _configure_canvas)
1981
1982 return
1983
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04001984
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00001985if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04001986 import unittest
1987 unittest.main('idlelib.idle_test.test_configdialog',
1988 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04001989 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04001990 run(ConfigDialog)