blob: e1ac82b7df3212e0deb8795afd0c078c65f2c2cd [file] [log] [blame]
Kurt B. Kaisere7a161e2003-01-10 20:13:57 +00001"""IDLE Configuration Dialog: support user customization of IDLE by GUI
2
3Customize font faces, sizes, and colorization attributes. Set indentation
4defaults. Customize keybindings. Colorization and keybindings can be
5saved as user defined sets. Select startup options including shell/editor
6and default window size. Define additional help sources.
7
8Note that tab width in IDLE is currently fixed at eight due to Tk issues.
Kurt B. Kaiseracdef852005-01-31 03:34:26 +00009Refer to comments in EditorWindow autoindent code for details.
Kurt B. Kaisere7a161e2003-01-10 20:13:57 +000010
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000011"""
terryjreedy938e7382017-06-26 20:48:39 -040012from tkinter import (Toplevel, Frame, LabelFrame, Listbox, Label, Button,
13 Entry, Text, Scale, Radiobutton, Checkbutton, Canvas,
14 StringVar, BooleanVar, IntVar, TRUE, FALSE,
15 TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE, NORMAL, DISABLED,
16 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW,
terryjreedy7ab33422017-07-09 19:26:32 -040017 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END)
Terry Jan Reedy8364fef2017-07-29 01:28:05 -040018from tkinter.ttk import Notebook, Scrollbar
Georg Brandl14fc4272008-05-17 18:39:55 +000019import tkinter.colorchooser as tkColorChooser
20import tkinter.font as tkFont
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040021import tkinter.messagebox as tkMessageBox
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000022
terryjreedyedc03422017-07-07 16:37:39 -040023from idlelib.config import idleConf, ConfigChanges
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040024from idlelib.config_key import GetKeysDialog
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040025from idlelib.dynoption import DynOptionMenu
26from idlelib import macosx
Terry Jan Reedy8b22c0a2016-07-08 00:22:50 -040027from idlelib.query import SectionName, HelpSource
Terry Jan Reedya9421fb2014-10-22 20:15:18 -040028from idlelib.tabbedpages import TabbedPageSet
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040029from idlelib.textview import view_text
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -040030
terryjreedyedc03422017-07-07 16:37:39 -040031changes = ConfigChanges()
32
Terry Jan Reedy02f88d22017-07-28 15:42:43 -040033
Steven M. Gava44d3d1a2001-07-31 06:59:02 +000034class ConfigDialog(Toplevel):
terryjreedye5bb1122017-07-05 00:54:55 -040035 """Config dialog for IDLE.
36 """
Kurt B. Kaiseracdef852005-01-31 03:34:26 +000037
Terry Jan Reedycd567362014-10-17 01:31:35 -040038 def __init__(self, parent, title='', _htest=False, _utest=False):
terryjreedye5bb1122017-07-05 00:54:55 -040039 """Show the tabbed dialog for user configuration.
40
terryjreedy9a09c662017-07-13 23:53:30 -040041 Args:
42 parent - parent of this dialog
43 title - string which is the title of this popup dialog
44 _htest - bool, change box location when running htest
45 _utest - bool, don't wait_window when running unittest
46
47 Note: Focus set on font page fontlist.
48
49 Methods:
50 create_widgets
51 cancel: Bound to DELETE_WINDOW protocol.
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -040052 """
Steven M. Gavad721c482001-07-31 10:46:53 +000053 Toplevel.__init__(self, parent)
Terry Jan Reedy22405332014-07-30 19:24:32 -040054 self.parent = parent
Terry Jan Reedy4036d872014-08-03 23:02:58 -040055 if _htest:
56 parent.instance_dict = {}
terryjreedy42abf7f2017-07-13 22:24:55 -040057 if not _utest:
58 self.withdraw()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000059
Steven M. Gavad721c482001-07-31 10:46:53 +000060 self.configure(borderwidth=5)
Terry Jan Reedycd567362014-10-17 01:31:35 -040061 self.title(title or 'IDLE Preferences')
terryjreedy938e7382017-06-26 20:48:39 -040062 x = parent.winfo_rootx() + 20
63 y = parent.winfo_rooty() + (30 if not _htest else 150)
64 self.geometry(f'+{x}+{y}')
terryjreedye5bb1122017-07-05 00:54:55 -040065 # Each theme element key is its display name.
66 # The first value of the tuple is the sample area tag name.
67 # The second value is the display name list sort index.
terryjreedy938e7382017-06-26 20:48:39 -040068 self.create_widgets()
Terry Jan Reedy4036d872014-08-03 23:02:58 -040069 self.resizable(height=FALSE, width=FALSE)
Steven M. Gavad721c482001-07-31 10:46:53 +000070 self.transient(parent)
terryjreedy938e7382017-06-26 20:48:39 -040071 self.protocol("WM_DELETE_WINDOW", self.cancel)
Terry Jan Reedy75822262017-07-30 15:00:50 -040072 self.fontpage.fontlist.focus_set()
terryjreedye5bb1122017-07-05 00:54:55 -040073 # XXX Decide whether to keep or delete these key bindings.
74 # Key bindings for this dialog.
75 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
76 # self.bind('<Alt-a>', self.Apply) #apply changes, save
77 # self.bind('<F1>', self.Help) #context help
terryjreedy938e7382017-06-26 20:48:39 -040078 self.load_configs()
Terry Jan Reedy02f88d22017-07-28 15:42:43 -040079 # Avoid callbacks during load_configs.
80 tracers.attach()
Guido van Rossum8ce8a782007-11-01 19:42:39 +000081
Terry Jan Reedycfa89502014-07-14 23:07:32 -040082 if not _utest:
terryjreedy42abf7f2017-07-13 22:24:55 -040083 self.grab_set()
Terry Jan Reedycfa89502014-07-14 23:07:32 -040084 self.wm_deiconify()
85 self.wait_window()
Kurt B. Kaiser6655e4b2002-12-31 16:03:23 +000086
Terry Jan Reedy02f88d22017-07-28 15:42:43 -040087
terryjreedy938e7382017-06-26 20:48:39 -040088 def create_widgets(self):
terryjreedy9a09c662017-07-13 23:53:30 -040089 """Create and place widgets for tabbed dialog.
90
91 Widgets Bound to self:
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -040092 note: Notebook
93 highpage: self.create_page_highlight
94 fontpage: FontPage
Terry Jan Reedyff4b2222017-08-15 19:13:11 -040095 keyspage: KeysPage
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -040096 genpage: GenPage
Terry Jan Reedyff4b2222017-08-15 19:13:11 -040097 extpage: self.create_page_extensions
terryjreedy9a09c662017-07-13 23:53:30 -040098
99 Methods:
terryjreedy9a09c662017-07-13 23:53:30 -0400100 create_action_buttons
101 load_configs: Load pages except for extensions.
terryjreedy9a09c662017-07-13 23:53:30 -0400102 activate_config_changes: Tell editors to reload.
103 """
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400104 self.note = note = Notebook(self, width=450, height=450)
Terry Jan Reedy75822262017-07-30 15:00:50 -0400105 self.highpage = self.create_page_highlight()
106 self.fontpage = FontPage(note, self.highpage)
Terry Jan Reedyff4b2222017-08-15 19:13:11 -0400107 self.keyspage = KeysPage(note)
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -0400108 self.genpage = GenPage(note)
Terry Jan Reedy75822262017-07-30 15:00:50 -0400109 self.extpage = self.create_page_extensions()
110 note.add(self.fontpage, text='Fonts/Tabs')
111 note.add(self.highpage, text='Highlights')
112 note.add(self.keyspage, text=' Keys ')
113 note.add(self.genpage, text=' General ')
114 note.add(self.extpage, text='Extensions')
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400115 note.enable_traversal()
116 note.pack(side=TOP, expand=TRUE, fill=BOTH)
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400117 self.create_action_buttons().pack(side=BOTTOM)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400118
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400119 def load_configs(self):
120 """Load configuration for each page.
121
122 Load configuration from default and user config files and populate
123 the widgets on the config dialog pages.
124
125 Methods:
126 load_font_cfg
127 load_tab_cfg
128 load_theme_cfg
129 load_key_cfg
130 load_general_cfg
131 """
Terry Jan Reedy75822262017-07-30 15:00:50 -0400132 #self.load_font_cfg()
133 #self.load_tab_cfg()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400134 self.load_theme_cfg()
Terry Jan Reedyff4b2222017-08-15 19:13:11 -0400135 # self.load_key_cfg()
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -0400136 # self.load_general_cfg()
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400137 # note: extension page handled separately
138
Terry Jan Reedy92cb0a32014-10-08 20:29:13 -0400139 def create_action_buttons(self):
terryjreedy9a09c662017-07-13 23:53:30 -0400140 """Return frame of action buttons for dialog.
141
142 Methods:
143 ok
144 apply
145 cancel
146 help
147
148 Widget Structure:
149 outer: Frame
150 buttons: Frame
151 (no assignment): Button (ok)
152 (no assignment): Button (apply)
153 (no assignment): Button (cancel)
154 (no assignment): Button (help)
155 (no assignment): Frame
156 """
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400157 if macosx.isAquaTk():
Terry Jan Reedye3416e62014-07-26 19:40:16 -0400158 # Changing the default padding on OSX results in unreadable
terryjreedye5bb1122017-07-05 00:54:55 -0400159 # text in the buttons.
terryjreedy938e7382017-06-26 20:48:39 -0400160 padding_args = {}
Ronald Oussoren9e350042009-02-12 16:02:11 +0000161 else:
terryjreedy938e7382017-06-26 20:48:39 -0400162 padding_args = {'padx':6, 'pady':3}
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400163 outer = Frame(self, pady=2)
164 buttons = Frame(outer, pady=2)
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400165 for txt, cmd in (
terryjreedy938e7382017-06-26 20:48:39 -0400166 ('Ok', self.ok),
167 ('Apply', self.apply),
168 ('Cancel', self.cancel),
169 ('Help', self.help)):
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400170 Button(buttons, text=txt, command=cmd, takefocus=FALSE,
terryjreedy938e7382017-06-26 20:48:39 -0400171 **padding_args).pack(side=LEFT, padx=5)
terryjreedye5bb1122017-07-05 00:54:55 -0400172 # Add space above buttons.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -0400173 Frame(outer, height=2, borderwidth=0).pack(side=TOP)
174 buttons.pack(side=BOTTOM)
175 return outer
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -0400176
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400177 def ok(self):
178 """Apply config changes, then dismiss dialog.
179
180 Methods:
181 apply
182 destroy: inherited
183 """
184 self.apply()
185 self.destroy()
186
187 def apply(self):
188 """Apply config changes and leave dialog open.
189
190 Methods:
191 deactivate_current_config
192 save_all_changed_extensions
193 activate_config_changes
194 """
195 self.deactivate_current_config()
196 changes.save_all()
197 self.save_all_changed_extensions()
198 self.activate_config_changes()
199
200 def cancel(self):
201 """Dismiss config dialog.
202
203 Methods:
204 destroy: inherited
205 """
206 self.destroy()
207
208 def help(self):
209 """Create textview for config dialog help.
210
211 Attrbutes accessed:
212 tab_pages
213
214 Methods:
215 view_text: Method from textview module.
216 """
217 page = self.tab_pages._current_page
218 view_text(self, title='Help for IDLE preferences',
219 text=help_common+help_pages.get(page, ''))
220
Terry Jan Reedy1daeb252017-07-24 02:50:28 -0400221
terryjreedy938e7382017-06-26 20:48:39 -0400222 def create_page_highlight(self):
terryjreedye5bb1122017-07-05 00:54:55 -0400223 """Return frame of widgets for Highlighting tab.
224
terryjreedy9a09c662017-07-13 23:53:30 -0400225 Tk Variables:
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400226 color: Color of selected target.
terryjreedye5bb1122017-07-05 00:54:55 -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.
terryjreedy9a09c662017-07-13 23:53:30 -0400230 Note: this has no callback.
terryjreedye5bb1122017-07-05 00:54:55 -0400231 is_builtin_theme: Selector for built-in or custom theme.
232 highlight_target: Menu variable for the highlight tag target.
terryjreedy9a09c662017-07-13 23:53:30 -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 Reedyac5c1e22017-07-21 01:29:09 -0400241 get_color: Invoke colorchooser [button_set_color].
242 set_color_sample_binding: Call set_color_sample [fg_bg_toggle].
terryjreedy9a09c662017-07-13 23:53:30 -0400243 set_highlight_target: set fg_bg_toggle, set_color_sample().
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400244 set_color_sample: Set frame background to target.
245 on_new_color_set: Set new color and add option.
terryjreedy9a09c662017-07-13 23:53:30 -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
Terry Jan Reedya3145902017-08-14 21:45:02 -0400254 Widgets of highlights page frame: (*) widgets bound to self
255 frame_custom: LabelFrame
256 (*)highlight_sample: Text
257 (*)frame_color_set: Frame
258 button_set_color: Button
259 (*)opt_menu_highlight_target: DynOptionMenu - highlight_target
260 frame_fg_bg_toggle: Frame
261 (*)radio_fg: Radiobutton - fg_bg_toggle
262 (*)radio_bg: Radiobutton - fg_bg_toggle
263 button_save_custom_theme: Button
264 frame_theme: LabelFrame
265 theme_type_title: Label
266 (*)radio_theme_builtin: Radiobutton - is_builtin_theme
267 (*)radio_theme_custom: Radiobutton - is_builtin_theme
268 (*)opt_menu_theme_builtin: DynOptionMenu - builtin_theme
269 (*)opt_menu_theme_custom: DynOptionMenu - custom_theme
270 (*)button_delete_custom_theme: Button
271 (*)new_custom_theme: Label
terryjreedye5bb1122017-07-05 00:54:55 -0400272 """
terryjreedy9a09c662017-07-13 23:53:30 -0400273 self.theme_elements={
274 'Normal Text': ('normal', '00'),
275 'Python Keywords': ('keyword', '01'),
276 'Python Definitions': ('definition', '02'),
277 'Python Builtins': ('builtin', '03'),
278 'Python Comments': ('comment', '04'),
279 'Python Strings': ('string', '05'),
280 'Selected Text': ('hilite', '06'),
281 'Found Text': ('hit', '07'),
282 'Cursor': ('cursor', '08'),
283 'Editor Breakpoint': ('break', '09'),
284 'Shell Normal Text': ('console', '10'),
285 'Shell Error Text': ('error', '11'),
286 'Shell Stdout Text': ('stdout', '12'),
287 'Shell Stderr Text': ('stderr', '13'),
288 }
Terry Jan Reedy22405332014-07-30 19:24:32 -0400289 parent = self.parent
Terry Jan Reedy02f88d22017-07-28 15:42:43 -0400290 self.builtin_theme = tracers.add(
291 StringVar(parent), self.var_changed_builtin_theme)
292 self.custom_theme = tracers.add(
293 StringVar(parent), self.var_changed_custom_theme)
terryjreedy938e7382017-06-26 20:48:39 -0400294 self.fg_bg_toggle = BooleanVar(parent)
Terry Jan Reedy02f88d22017-07-28 15:42:43 -0400295 self.color = tracers.add(
296 StringVar(parent), self.var_changed_color)
297 self.is_builtin_theme = tracers.add(
298 BooleanVar(parent), self.var_changed_is_builtin_theme)
299 self.highlight_target = tracers.add(
300 StringVar(parent), self.var_changed_highlight_target)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400301
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400302 # Widget creation:
303 # body frame and section frames
304 frame = Frame(self.note)
terryjreedy938e7382017-06-26 20:48:39 -0400305 frame_custom = LabelFrame(frame, borderwidth=2, relief=GROOVE,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400306 text=' Custom Highlighting ')
terryjreedy938e7382017-06-26 20:48:39 -0400307 frame_theme = LabelFrame(frame, borderwidth=2, relief=GROOVE,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400308 text=' Highlighting Theme ')
terryjreedy938e7382017-06-26 20:48:39 -0400309 #frame_custom
Terry Jan Reedy75822262017-07-30 15:00:50 -0400310 text = self.highlight_sample = frame.highlight_sample = Text(
terryjreedy938e7382017-06-26 20:48:39 -0400311 frame_custom, relief=SOLID, borderwidth=1,
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400312 font=('courier', 12, ''), cursor='hand2', width=21, height=13,
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400313 takefocus=FALSE, highlightthickness=0, wrap=NONE)
Terry Jan Reedy04864b42017-07-22 00:56:18 -0400314 text=self.highlight_sample
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400315 text.bind('<Double-Button-1>', lambda e: 'break')
316 text.bind('<B1-Motion>', lambda e: 'break')
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400317 text_and_tags=(('\n', 'normal'),
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400318 ('#you can click here', 'comment'), ('\n', 'normal'),
319 ('#to choose items', 'comment'), ('\n', 'normal'),
320 ('def', 'keyword'), (' ', 'normal'),
321 ('func', 'definition'), ('(param):\n ', 'normal'),
322 ('"""string"""', 'string'), ('\n var0 = ', 'normal'),
323 ("'string'", 'string'), ('\n var1 = ', 'normal'),
324 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'),
325 ("'found'", 'hit'), ('\n var3 = ', 'normal'),
326 ('list', 'builtin'), ('(', 'normal'),
Terry Jan Reedya8aa4d52015-10-02 22:12:17 -0400327 ('None', 'keyword'), (')\n', 'normal'),
328 (' breakpoint("line")', 'break'), ('\n\n', 'normal'),
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400329 (' error ', 'error'), (' ', 'normal'),
330 ('cursor |', 'cursor'), ('\n ', 'normal'),
331 ('shell', 'console'), (' ', 'normal'),
332 ('stdout', 'stdout'), (' ', 'normal'),
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400333 ('stderr', 'stderr'), ('\n\n', 'normal'))
terryjreedy938e7382017-06-26 20:48:39 -0400334 for texttag in text_and_tags:
335 text.insert(END, texttag[0], texttag[1])
336 for element in self.theme_elements:
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400337 def tem(event, elem=element):
terryjreedy938e7382017-06-26 20:48:39 -0400338 event.widget.winfo_toplevel().highlight_target.set(elem)
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400339 text.tag_bind(
terryjreedy938e7382017-06-26 20:48:39 -0400340 self.theme_elements[element][0], '<ButtonPress-1>', tem)
Terry Jan Reedy0c4c6512017-07-26 21:41:26 -0400341 text['state'] = DISABLED
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400342 self.frame_color_set = Frame(frame_custom, relief=SOLID, borderwidth=1)
terryjreedy938e7382017-06-26 20:48:39 -0400343 frame_fg_bg_toggle = Frame(frame_custom)
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400344 button_set_color = Button(
345 self.frame_color_set, text='Choose Color for :',
346 command=self.get_color, highlightthickness=0)
terryjreedy938e7382017-06-26 20:48:39 -0400347 self.opt_menu_highlight_target = DynOptionMenu(
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400348 self.frame_color_set, self.highlight_target, None,
terryjreedy938e7382017-06-26 20:48:39 -0400349 highlightthickness=0) #, command=self.set_highlight_targetBinding
350 self.radio_fg = Radiobutton(
351 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1,
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400352 text='Foreground', command=self.set_color_sample_binding)
terryjreedy938e7382017-06-26 20:48:39 -0400353 self.radio_bg=Radiobutton(
354 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0,
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400355 text='Background', command=self.set_color_sample_binding)
terryjreedy938e7382017-06-26 20:48:39 -0400356 self.fg_bg_toggle.set(1)
357 button_save_custom_theme = Button(
358 frame_custom, text='Save as New Custom Theme',
359 command=self.save_as_new_theme)
360 #frame_theme
361 theme_type_title = Label(frame_theme, text='Select : ')
362 self.radio_theme_builtin = Radiobutton(
363 frame_theme, variable=self.is_builtin_theme, value=1,
364 command=self.set_theme_type, text='a Built-in Theme')
365 self.radio_theme_custom = Radiobutton(
366 frame_theme, variable=self.is_builtin_theme, value=0,
367 command=self.set_theme_type, text='a Custom Theme')
368 self.opt_menu_theme_builtin = DynOptionMenu(
369 frame_theme, self.builtin_theme, None, command=None)
370 self.opt_menu_theme_custom=DynOptionMenu(
371 frame_theme, self.custom_theme, None, command=None)
372 self.button_delete_custom_theme=Button(
373 frame_theme, text='Delete Custom Theme',
374 command=self.delete_custom_theme)
375 self.new_custom_theme = Label(frame_theme, bd=2)
Terry Jan Reedy22405332014-07-30 19:24:32 -0400376
Steven M. Gava952d0a52001-08-03 04:43:44 +0000377 ##widget packing
378 #body
terryjreedy938e7382017-06-26 20:48:39 -0400379 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
380 frame_theme.pack(side=LEFT, padx=5, pady=5, fill=Y)
381 #frame_custom
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400382 self.frame_color_set.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=X)
terryjreedy938e7382017-06-26 20:48:39 -0400383 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0)
Terry Jan Reedy04864b42017-07-22 00:56:18 -0400384 self.highlight_sample.pack(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400385 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
Terry Jan Reedyac5c1e22017-07-21 01:29:09 -0400386 button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4)
terryjreedy938e7382017-06-26 20:48:39 -0400387 self.opt_menu_highlight_target.pack(
Terry Jan Reedy4036d872014-08-03 23:02:58 -0400388 side=TOP, expand=TRUE, fill=X, padx=8, pady=3)
terryjreedy938e7382017-06-26 20:48:39 -0400389 self.radio_fg.pack(side=LEFT, anchor=E)
390 self.radio_bg.pack(side=RIGHT, anchor=W)
391 button_save_custom_theme.pack(side=BOTTOM, fill=X, padx=5, pady=5)
392 #frame_theme
393 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5)
394 self.radio_theme_builtin.pack(side=TOP, anchor=W, padx=5)
395 self.radio_theme_custom.pack(side=TOP, anchor=W, padx=5, pady=2)
396 self.opt_menu_theme_builtin.pack(side=TOP, fill=X, padx=5, pady=5)
397 self.opt_menu_theme_custom.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5)
398 self.button_delete_custom_theme.pack(side=TOP, fill=X, padx=5, pady=5)
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -0500399 self.new_custom_theme.pack(side=TOP, fill=X, pady=5)
Steven M. Gava952d0a52001-08-03 04:43:44 +0000400 return frame
401
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400402 def load_theme_cfg(self):
403 """Load current configuration settings for the theme options.
404
405 Based on the is_builtin_theme toggle, the theme is set as
406 either builtin or custom and the initial widget values
407 reflect the current settings from idleConf.
408
409 Attributes updated:
410 is_builtin_theme: Set from idleConf.
411 opt_menu_theme_builtin: List of default themes from idleConf.
412 opt_menu_theme_custom: List of custom themes from idleConf.
413 radio_theme_custom: Disabled if there are no custom themes.
414 custom_theme: Message with additional information.
415 opt_menu_highlight_target: Create menu from self.theme_elements.
416
417 Methods:
418 set_theme_type
419 paint_theme_sample
420 set_highlight_target
421 """
422 # Set current theme type radiobutton.
423 self.is_builtin_theme.set(idleConf.GetOption(
424 'main', 'Theme', 'default', type='bool', default=1))
425 # Set current theme.
426 current_option = idleConf.CurrentTheme()
427 # Load available theme option menus.
428 if self.is_builtin_theme.get(): # Default theme selected.
429 item_list = idleConf.GetSectionList('default', 'highlight')
430 item_list.sort()
431 self.opt_menu_theme_builtin.SetMenu(item_list, current_option)
432 item_list = idleConf.GetSectionList('user', 'highlight')
433 item_list.sort()
434 if not item_list:
435 self.radio_theme_custom['state'] = DISABLED
436 self.custom_theme.set('- no custom themes -')
437 else:
438 self.opt_menu_theme_custom.SetMenu(item_list, item_list[0])
439 else: # User theme selected.
440 item_list = idleConf.GetSectionList('user', 'highlight')
441 item_list.sort()
442 self.opt_menu_theme_custom.SetMenu(item_list, current_option)
443 item_list = idleConf.GetSectionList('default', 'highlight')
444 item_list.sort()
445 self.opt_menu_theme_builtin.SetMenu(item_list, item_list[0])
446 self.set_theme_type()
447 # Load theme element option menu.
448 theme_names = list(self.theme_elements.keys())
449 theme_names.sort(key=lambda x: self.theme_elements[x][1])
450 self.opt_menu_highlight_target.SetMenu(theme_names, theme_names[0])
451 self.paint_theme_sample()
452 self.set_highlight_target()
453
454 def var_changed_builtin_theme(self, *params):
455 """Process new builtin theme selection.
456
457 Add the changed theme's name to the changed_items and recreate
458 the sample with the values from the selected theme.
459 """
460 old_themes = ('IDLE Classic', 'IDLE New')
461 value = self.builtin_theme.get()
462 if value not in old_themes:
463 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes:
464 changes.add_option('main', 'Theme', 'name', old_themes[0])
465 changes.add_option('main', 'Theme', 'name2', value)
466 self.new_custom_theme.config(text='New theme, see Help',
467 fg='#500000')
468 else:
469 changes.add_option('main', 'Theme', 'name', value)
470 changes.add_option('main', 'Theme', 'name2', '')
471 self.new_custom_theme.config(text='', fg='black')
472 self.paint_theme_sample()
473
474 def var_changed_custom_theme(self, *params):
475 """Process new custom theme selection.
476
477 If a new custom theme is selected, add the name to the
478 changed_items and apply the theme to the sample.
479 """
480 value = self.custom_theme.get()
481 if value != '- no custom themes -':
482 changes.add_option('main', 'Theme', 'name', value)
483 self.paint_theme_sample()
484
485 def var_changed_is_builtin_theme(self, *params):
486 """Process toggle between builtin and custom theme.
487
488 Update the default toggle value and apply the newly
489 selected theme type.
490 """
491 value = self.is_builtin_theme.get()
492 changes.add_option('main', 'Theme', 'default', value)
493 if value:
494 self.var_changed_builtin_theme()
495 else:
496 self.var_changed_custom_theme()
497
498 def var_changed_color(self, *params):
499 "Process change to color choice."
500 self.on_new_color_set()
501
502 def var_changed_highlight_target(self, *params):
503 "Process selection of new target tag for highlighting."
504 self.set_highlight_target()
505
506 def set_theme_type(self):
507 """Set available screen options based on builtin or custom theme.
508
509 Attributes accessed:
510 is_builtin_theme
511
512 Attributes updated:
513 opt_menu_theme_builtin
514 opt_menu_theme_custom
515 button_delete_custom_theme
516 radio_theme_custom
517
518 Called from:
519 handler for radio_theme_builtin and radio_theme_custom
520 delete_custom_theme
521 create_new_theme
522 load_theme_cfg
523 """
524 if self.is_builtin_theme.get():
525 self.opt_menu_theme_builtin['state'] = NORMAL
526 self.opt_menu_theme_custom['state'] = DISABLED
527 self.button_delete_custom_theme['state'] = DISABLED
528 else:
529 self.opt_menu_theme_builtin['state'] = DISABLED
530 self.radio_theme_custom['state'] = NORMAL
531 self.opt_menu_theme_custom['state'] = NORMAL
532 self.button_delete_custom_theme['state'] = NORMAL
533
534 def get_color(self):
535 """Handle button to select a new color for the target tag.
536
537 If a new color is selected while using a builtin theme, a
538 name must be supplied to create a custom theme.
539
540 Attributes accessed:
541 highlight_target
542 frame_color_set
543 is_builtin_theme
544
545 Attributes updated:
546 color
547
548 Methods:
549 get_new_theme_name
550 create_new_theme
551 """
552 target = self.highlight_target.get()
553 prev_color = self.frame_color_set.cget('bg')
554 rgbTuplet, color_string = tkColorChooser.askcolor(
555 parent=self, title='Pick new color for : '+target,
556 initialcolor=prev_color)
557 if color_string and (color_string != prev_color):
558 # User didn't cancel and they chose a new color.
559 if self.is_builtin_theme.get(): # Current theme is a built-in.
560 message = ('Your changes will be saved as a new Custom Theme. '
561 'Enter a name for your new Custom Theme below.')
562 new_theme = self.get_new_theme_name(message)
563 if not new_theme: # User cancelled custom theme creation.
564 return
565 else: # Create new custom theme based on previously active theme.
566 self.create_new_theme(new_theme)
567 self.color.set(color_string)
568 else: # Current theme is user defined.
569 self.color.set(color_string)
570
571 def on_new_color_set(self):
572 "Display sample of new color selection on the dialog."
573 new_color=self.color.get()
574 self.frame_color_set.config(bg=new_color) # Set sample.
575 plane ='foreground' if self.fg_bg_toggle.get() else 'background'
576 sample_element = self.theme_elements[self.highlight_target.get()][0]
577 self.highlight_sample.tag_config(sample_element, **{plane:new_color})
578 theme = self.custom_theme.get()
579 theme_element = sample_element + '-' + plane
580 changes.add_option('highlight', theme, theme_element, new_color)
581
582 def get_new_theme_name(self, message):
583 "Return name of new theme from query popup."
584 used_names = (idleConf.GetSectionList('user', 'highlight') +
585 idleConf.GetSectionList('default', 'highlight'))
586 new_theme = SectionName(
587 self, 'New Custom Theme', message, used_names).result
588 return new_theme
589
590 def save_as_new_theme(self):
591 """Prompt for new theme name and create the theme.
592
593 Methods:
594 get_new_theme_name
595 create_new_theme
596 """
597 new_theme_name = self.get_new_theme_name('New Theme Name:')
598 if new_theme_name:
599 self.create_new_theme(new_theme_name)
600
601 def create_new_theme(self, new_theme_name):
602 """Create a new custom theme with the given name.
603
604 Create the new theme based on the previously active theme
605 with the current changes applied. Once it is saved, then
606 activate the new theme.
607
608 Attributes accessed:
609 builtin_theme
610 custom_theme
611
612 Attributes updated:
613 opt_menu_theme_custom
614 is_builtin_theme
615
616 Method:
617 save_new_theme
618 set_theme_type
619 """
620 if self.is_builtin_theme.get():
621 theme_type = 'default'
622 theme_name = self.builtin_theme.get()
623 else:
624 theme_type = 'user'
625 theme_name = self.custom_theme.get()
626 new_theme = idleConf.GetThemeDict(theme_type, theme_name)
627 # Apply any of the old theme's unsaved changes to the new theme.
628 if theme_name in changes['highlight']:
629 theme_changes = changes['highlight'][theme_name]
630 for element in theme_changes:
631 new_theme[element] = theme_changes[element]
632 # Save the new theme.
633 self.save_new_theme(new_theme_name, new_theme)
634 # Change GUI over to the new theme.
635 custom_theme_list = idleConf.GetSectionList('user', 'highlight')
636 custom_theme_list.sort()
637 self.opt_menu_theme_custom.SetMenu(custom_theme_list, new_theme_name)
638 self.is_builtin_theme.set(0)
639 self.set_theme_type()
640
641 def set_highlight_target(self):
642 """Set fg/bg toggle and color based on highlight tag target.
643
644 Instance variables accessed:
645 highlight_target
646
647 Attributes updated:
648 radio_fg
649 radio_bg
650 fg_bg_toggle
651
652 Methods:
653 set_color_sample
654
655 Called from:
656 var_changed_highlight_target
657 load_theme_cfg
658 """
659 if self.highlight_target.get() == 'Cursor': # bg not possible
660 self.radio_fg['state'] = DISABLED
661 self.radio_bg['state'] = DISABLED
662 self.fg_bg_toggle.set(1)
663 else: # Both fg and bg can be set.
664 self.radio_fg['state'] = NORMAL
665 self.radio_bg['state'] = NORMAL
666 self.fg_bg_toggle.set(1)
667 self.set_color_sample()
668
669 def set_color_sample_binding(self, *args):
670 """Change color sample based on foreground/background toggle.
671
672 Methods:
673 set_color_sample
674 """
675 self.set_color_sample()
676
677 def set_color_sample(self):
678 """Set the color of the frame background to reflect the selected target.
679
680 Instance variables accessed:
681 theme_elements
682 highlight_target
683 fg_bg_toggle
684 highlight_sample
685
686 Attributes updated:
687 frame_color_set
688 """
689 # Set the color sample area.
690 tag = self.theme_elements[self.highlight_target.get()][0]
691 plane = 'foreground' if self.fg_bg_toggle.get() else 'background'
692 color = self.highlight_sample.tag_cget(tag, plane)
693 self.frame_color_set.config(bg=color)
694
695 def paint_theme_sample(self):
696 """Apply the theme colors to each element tag in the sample text.
697
698 Instance attributes accessed:
699 theme_elements
700 is_builtin_theme
701 builtin_theme
702 custom_theme
703
704 Attributes updated:
705 highlight_sample: Set the tag elements to the theme.
706
707 Methods:
708 set_color_sample
709
710 Called from:
711 var_changed_builtin_theme
712 var_changed_custom_theme
713 load_theme_cfg
714 """
715 if self.is_builtin_theme.get(): # Default theme
716 theme = self.builtin_theme.get()
717 else: # User theme
718 theme = self.custom_theme.get()
719 for element_title in self.theme_elements:
720 element = self.theme_elements[element_title][0]
721 colors = idleConf.GetHighlight(theme, element)
722 if element == 'cursor': # Cursor sample needs special painting.
723 colors['background'] = idleConf.GetHighlight(
724 theme, 'normal', fgBg='bg')
725 # Handle any unsaved changes to this theme.
726 if theme in changes['highlight']:
727 theme_dict = changes['highlight'][theme]
728 if element + '-foreground' in theme_dict:
729 colors['foreground'] = theme_dict[element + '-foreground']
730 if element + '-background' in theme_dict:
731 colors['background'] = theme_dict[element + '-background']
732 self.highlight_sample.tag_config(element, **colors)
733 self.set_color_sample()
734
735 def save_new_theme(self, theme_name, theme):
736 """Save a newly created theme to idleConf.
737
738 theme_name - string, the name of the new theme
739 theme - dictionary containing the new theme
740 """
741 if not idleConf.userCfg['highlight'].has_section(theme_name):
742 idleConf.userCfg['highlight'].add_section(theme_name)
743 for element in theme:
744 value = theme[element]
745 idleConf.userCfg['highlight'].SetOption(theme_name, element, value)
746
747 def delete_custom_theme(self):
748 """Handle event to delete custom theme.
749
750 The current theme is deactivated and the default theme is
751 activated. The custom theme is permanently removed from
752 the config file.
753
754 Attributes accessed:
755 custom_theme
756
757 Attributes updated:
758 radio_theme_custom
759 opt_menu_theme_custom
760 is_builtin_theme
761 builtin_theme
762
763 Methods:
764 deactivate_current_config
765 save_all_changed_extensions
766 activate_config_changes
767 set_theme_type
768 """
769 theme_name = self.custom_theme.get()
770 delmsg = 'Are you sure you wish to delete the theme %r ?'
771 if not tkMessageBox.askyesno(
772 'Delete Theme', delmsg % theme_name, parent=self):
773 return
774 self.deactivate_current_config()
775 # Remove theme from changes, config, and file.
776 changes.delete_section('highlight', theme_name)
777 # Reload user theme list.
778 item_list = idleConf.GetSectionList('user', 'highlight')
779 item_list.sort()
780 if not item_list:
781 self.radio_theme_custom['state'] = DISABLED
782 self.opt_menu_theme_custom.SetMenu(item_list, '- no custom themes -')
783 else:
784 self.opt_menu_theme_custom.SetMenu(item_list, item_list[0])
785 # Revert to default theme.
786 self.is_builtin_theme.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
787 self.builtin_theme.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
788 # User can't back out of these changes, they must be applied now.
789 changes.save_all()
790 self.save_all_changed_extensions()
791 self.activate_config_changes()
792 self.set_theme_type()
793
Terry Jan Reedy2cbb6732017-07-27 20:50:39 -0400794 def deactivate_current_config(self):
795 """Remove current key bindings.
796
797 Iterate over window instances defined in parent and remove
798 the keybindings.
799 """
800 # Before a config is saved, some cleanup of current
801 # config must be done - remove the previous keybindings.
802 win_instances = self.parent.instance_dict.keys()
803 for instance in win_instances:
804 instance.RemoveKeybindings()
805
806 def activate_config_changes(self):
807 """Apply configuration changes to current windows.
808
809 Dynamically update the current parent window instances
810 with some of the configuration changes.
811 """
812 win_instances = self.parent.instance_dict.keys()
813 for instance in win_instances:
814 instance.ResetColorizer()
815 instance.ResetFont()
816 instance.set_notabs_indentwidth()
817 instance.ApplyKeybindings()
818 instance.reset_help_menu_entries()
819
terryjreedy938e7382017-06-26 20:48:39 -0400820 def create_page_extensions(self):
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400821 """Part of the config dialog used for configuring IDLE extensions.
822
823 This code is generic - it works for any and all IDLE extensions.
824
825 IDLE extensions save their configuration options using idleConf.
Terry Jan Reedyb2f87602015-10-13 22:09:06 -0400826 This code reads the current configuration using idleConf, supplies a
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400827 GUI interface to change the configuration values, and saves the
828 changes using idleConf.
829
830 Not all changes take effect immediately - some may require restarting IDLE.
831 This depends on each extension's implementation.
832
833 All values are treated as text, and it is up to the user to supply
834 reasonable values. The only exception to this are the 'enable*' options,
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300835 which are boolean, and can be toggled with a True/False button.
terryjreedy9a09c662017-07-13 23:53:30 -0400836
837 Methods:
Terry Jan Reedyff4b2222017-08-15 19:13:11 -0400838 load_extensions:
terryjreedy9a09c662017-07-13 23:53:30 -0400839 extension_selected: Handle selection from list.
840 create_extension_frame: Hold widgets for one extension.
841 set_extension_value: Set in userCfg['extensions'].
842 save_all_changed_extensions: Call extension page Save().
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400843 """
844 parent = self.parent
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400845 frame = Frame(self.note)
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400846 self.ext_defaultCfg = idleConf.defaultCfg['extensions']
847 self.ext_userCfg = idleConf.userCfg['extensions']
848 self.is_int = self.register(is_int)
849 self.load_extensions()
terryjreedye5bb1122017-07-05 00:54:55 -0400850 # Create widgets - a listbox shows all available extensions, with the
851 # controls for the extension selected in the listbox to the right.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400852 self.extension_names = StringVar(self)
853 frame.rowconfigure(0, weight=1)
854 frame.columnconfigure(2, weight=1)
855 self.extension_list = Listbox(frame, listvariable=self.extension_names,
856 selectmode='browse')
857 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected)
858 scroll = Scrollbar(frame, command=self.extension_list.yview)
859 self.extension_list.yscrollcommand=scroll.set
860 self.details_frame = LabelFrame(frame, width=250, height=250)
861 self.extension_list.grid(column=0, row=0, sticky='nws')
862 scroll.grid(column=1, row=0, sticky='ns')
863 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0])
864 frame.configure(padx=10, pady=10)
865 self.config_frame = {}
866 self.current_extension = None
867
868 self.outerframe = self # TEMPORARY
869 self.tabbed_page_set = self.extension_list # TEMPORARY
870
terryjreedye5bb1122017-07-05 00:54:55 -0400871 # Create the frame holding controls for each extension.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400872 ext_names = ''
873 for ext_name in sorted(self.extensions):
874 self.create_extension_frame(ext_name)
875 ext_names = ext_names + '{' + ext_name + '} '
876 self.extension_names.set(ext_names)
877 self.extension_list.selection_set(0)
878 self.extension_selected(None)
879
Terry Jan Reedy8364fef2017-07-29 01:28:05 -0400880 return frame
881
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400882 def load_extensions(self):
883 "Fill self.extensions with data from the default and user configs."
884 self.extensions = {}
885 for ext_name in idleConf.GetExtensions(active_only=False):
886 self.extensions[ext_name] = []
887
888 for ext_name in self.extensions:
889 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
890
terryjreedye5bb1122017-07-05 00:54:55 -0400891 # Bring 'enable' options to the beginning of the list.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400892 enables = [opt_name for opt_name in opt_list
893 if opt_name.startswith('enable')]
894 for opt_name in enables:
895 opt_list.remove(opt_name)
896 opt_list = enables + opt_list
897
898 for opt_name in opt_list:
899 def_str = self.ext_defaultCfg.Get(
900 ext_name, opt_name, raw=True)
901 try:
902 def_obj = {'True':True, 'False':False}[def_str]
903 opt_type = 'bool'
904 except KeyError:
905 try:
906 def_obj = int(def_str)
907 opt_type = 'int'
908 except ValueError:
909 def_obj = def_str
910 opt_type = None
911 try:
912 value = self.ext_userCfg.Get(
913 ext_name, opt_name, type=opt_type, raw=True,
914 default=def_obj)
terryjreedye5bb1122017-07-05 00:54:55 -0400915 except ValueError: # Need this until .Get fixed.
916 value = def_obj # Bad values overwritten by entry.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400917 var = StringVar(self)
918 var.set(str(value))
919
920 self.extensions[ext_name].append({'name': opt_name,
921 'type': opt_type,
922 'default': def_str,
923 'value': value,
924 'var': var,
925 })
926
927 def extension_selected(self, event):
terryjreedye5bb1122017-07-05 00:54:55 -0400928 "Handle selection of an extension from the list."
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400929 newsel = self.extension_list.curselection()
930 if newsel:
931 newsel = self.extension_list.get(newsel)
932 if newsel is None or newsel != self.current_extension:
933 if self.current_extension:
934 self.details_frame.config(text='')
935 self.config_frame[self.current_extension].grid_forget()
936 self.current_extension = None
937 if newsel:
938 self.details_frame.config(text=newsel)
939 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew')
940 self.current_extension = newsel
941
942 def create_extension_frame(self, ext_name):
943 """Create a frame holding the widgets to configure one extension"""
944 f = VerticalScrolledFrame(self.details_frame, height=250, width=250)
945 self.config_frame[ext_name] = f
946 entry_area = f.interior
terryjreedye5bb1122017-07-05 00:54:55 -0400947 # Create an entry for each configuration option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400948 for row, opt in enumerate(self.extensions[ext_name]):
terryjreedye5bb1122017-07-05 00:54:55 -0400949 # Create a row with a label and entry/checkbutton.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400950 label = Label(entry_area, text=opt['name'])
951 label.grid(row=row, column=0, sticky=NW)
952 var = opt['var']
953 if opt['type'] == 'bool':
954 Checkbutton(entry_area, textvariable=var, variable=var,
955 onvalue='True', offvalue='False',
956 indicatoron=FALSE, selectcolor='', width=8
957 ).grid(row=row, column=1, sticky=W, padx=7)
958 elif opt['type'] == 'int':
959 Entry(entry_area, textvariable=var, validate='key',
960 validatecommand=(self.is_int, '%P')
961 ).grid(row=row, column=1, sticky=NSEW, padx=7)
962
963 else:
964 Entry(entry_area, textvariable=var
965 ).grid(row=row, column=1, sticky=NSEW, padx=7)
966 return
967
968 def set_extension_value(self, section, opt):
terryjreedye5bb1122017-07-05 00:54:55 -0400969 """Return True if the configuration was added or changed.
970
971 If the value is the same as the default, then remove it
972 from user config file.
973 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400974 name = opt['name']
975 default = opt['default']
976 value = opt['var'].get().strip() or default
977 opt['var'].set(value)
978 # if self.defaultCfg.has_section(section):
terryjreedye5bb1122017-07-05 00:54:55 -0400979 # Currently, always true; if not, indent to return.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400980 if (value == default):
981 return self.ext_userCfg.RemoveOption(section, name)
terryjreedye5bb1122017-07-05 00:54:55 -0400982 # Set the option.
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400983 return self.ext_userCfg.SetOption(section, name, value)
984
985 def save_all_changed_extensions(self):
terryjreedy9a09c662017-07-13 23:53:30 -0400986 """Save configuration changes to the user config file.
987
988 Attributes accessed:
989 extensions
990
991 Methods:
992 set_extension_value
993 """
Terry Jan Reedy93f35422015-10-13 22:03:51 -0400994 has_changes = False
995 for ext_name in self.extensions:
996 options = self.extensions[ext_name]
997 for opt in options:
998 if self.set_extension_value(ext_name, opt):
999 has_changes = True
1000 if has_changes:
1001 self.ext_userCfg.Save()
1002
1003
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001004# class TabPage(Frame): # A template for Page classes.
1005# def __init__(self, master):
1006# super().__init__(master)
1007# self.create_page_tab()
1008# self.load_tab_cfg()
1009# def create_page_tab(self):
1010# # Define tk vars and register var and callback with tracers.
1011# # Create subframes and widgets.
1012# # Pack widgets.
1013# def load_tab_cfg(self):
1014# # Initialize widgets with data from idleConf.
1015# def var_changed_var_name():
1016# # For each tk var that needs other than default callback.
1017# def other_methods():
1018# # Define tab-specific behavior.
1019
1020
Terry Jan Reedy75822262017-07-30 15:00:50 -04001021class FontPage(Frame):
1022
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001023 def __init__(self, master, highpage):
1024 super().__init__(master)
Terry Jan Reedy75822262017-07-30 15:00:50 -04001025 self.highlight_sample = highpage.highlight_sample
1026 self.create_page_font_tab()
1027 self.load_font_cfg()
1028 self.load_tab_cfg()
1029
1030 def create_page_font_tab(self):
1031 """Return frame of widgets for Font/Tabs tab.
1032
1033 Fonts: Enable users to provisionally change font face, size, or
1034 boldness and to see the consequence of proposed choices. Each
1035 action set 3 options in changes structuree and changes the
1036 corresponding aspect of the font sample on this page and
1037 highlight sample on highlight page.
1038
1039 Function load_font_cfg initializes font vars and widgets from
1040 idleConf entries and tk.
1041
1042 Fontlist: mouse button 1 click or up or down key invoke
1043 on_fontlist_select(), which sets var font_name.
1044
1045 Sizelist: clicking the menubutton opens the dropdown menu. A
1046 mouse button 1 click or return key sets var font_size.
1047
1048 Bold_toggle: clicking the box toggles var font_bold.
1049
1050 Changing any of the font vars invokes var_changed_font, which
1051 adds all 3 font options to changes and calls set_samples.
1052 Set_samples applies a new font constructed from the font vars to
1053 font_sample and to highlight_sample on the hightlight page.
1054
1055 Tabs: Enable users to change spaces entered for indent tabs.
1056 Changing indent_scale value with the mouse sets Var space_num,
1057 which invokes the default callback to add an entry to
1058 changes. Load_tab_cfg initializes space_num to default.
1059
Terry Jan Reedya3145902017-08-14 21:45:02 -04001060 Widgets for FontPage(Frame): (*) widgets bound to self
1061 frame_font: LabelFrame
1062 frame_font_name: Frame
1063 font_name_title: Label
1064 (*)fontlist: ListBox - font_name
1065 scroll_font: Scrollbar
1066 frame_font_param: Frame
1067 font_size_title: Label
1068 (*)sizelist: DynOptionMenu - font_size
1069 (*)bold_toggle: Checkbutton - font_bold
1070 frame_font_sample: Frame
1071 (*)font_sample: Label
1072 frame_indent: LabelFrame
1073 indent_title: Label
1074 (*)indent_scale: Scale - space_num
Terry Jan Reedy75822262017-07-30 15:00:50 -04001075 """
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001076 self.font_name = tracers.add(StringVar(self), self.var_changed_font)
1077 self.font_size = tracers.add(StringVar(self), self.var_changed_font)
1078 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font)
Terry Jan Reedy75822262017-07-30 15:00:50 -04001079 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces'))
1080
1081 # Create widgets:
1082 # body and body section frames.
Terry Jan Reedy75822262017-07-30 15:00:50 -04001083 frame_font = LabelFrame(
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001084 self, borderwidth=2, relief=GROOVE, text=' Base Editor Font ')
Terry Jan Reedy75822262017-07-30 15:00:50 -04001085 frame_indent = LabelFrame(
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001086 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ')
Terry Jan Reedy75822262017-07-30 15:00:50 -04001087 # frame_font.
1088 frame_font_name = Frame(frame_font)
1089 frame_font_param = Frame(frame_font)
1090 font_name_title = Label(
1091 frame_font_name, justify=LEFT, text='Font Face :')
1092 self.fontlist = Listbox(frame_font_name, height=5,
1093 takefocus=True, exportselection=FALSE)
1094 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select)
1095 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select)
1096 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select)
1097 scroll_font = Scrollbar(frame_font_name)
1098 scroll_font.config(command=self.fontlist.yview)
1099 self.fontlist.config(yscrollcommand=scroll_font.set)
1100 font_size_title = Label(frame_font_param, text='Size :')
1101 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None)
1102 self.bold_toggle = Checkbutton(
1103 frame_font_param, variable=self.font_bold,
1104 onvalue=1, offvalue=0, text='Bold')
1105 frame_font_sample = Frame(frame_font, relief=SOLID, borderwidth=1)
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001106 temp_font = tkFont.Font(self, ('courier', 10, 'normal'))
Terry Jan Reedy75822262017-07-30 15:00:50 -04001107 self.font_sample = Label(
1108 frame_font_sample, justify=LEFT, font=temp_font,
1109 text='AaBbCcDdEe\nFfGgHhIiJj\n1234567890\n#:+=(){}[]')
1110 # frame_indent.
1111 indent_title = Label(
1112 frame_indent, justify=LEFT,
1113 text='Python Standard: 4 Spaces!')
1114 self.indent_scale = Scale(
1115 frame_indent, variable=self.space_num,
1116 orient='horizontal', tickinterval=2, from_=2, to=16)
1117
1118 # Pack widgets:
1119 # body.
1120 frame_font.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1121 frame_indent.pack(side=LEFT, padx=5, pady=5, fill=Y)
1122 # frame_font.
1123 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X)
1124 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X)
1125 font_name_title.pack(side=TOP, anchor=W)
1126 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X)
1127 scroll_font.pack(side=LEFT, fill=Y)
1128 font_size_title.pack(side=LEFT, anchor=W)
1129 self.sizelist.pack(side=LEFT, anchor=W)
1130 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20)
1131 frame_font_sample.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1132 self.font_sample.pack(expand=TRUE, fill=BOTH)
1133 # frame_indent.
1134 frame_indent.pack(side=TOP, fill=X)
1135 indent_title.pack(side=TOP, anchor=W, padx=5)
1136 self.indent_scale.pack(side=TOP, padx=5, fill=X)
1137
Terry Jan Reedy75822262017-07-30 15:00:50 -04001138 def load_font_cfg(self):
1139 """Load current configuration settings for the font options.
1140
1141 Retrieve current font with idleConf.GetFont and font families
1142 from tk. Setup fontlist and set font_name. Setup sizelist,
1143 which sets font_size. Set font_bold. Call set_samples.
1144 """
1145 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow')
1146 font_name = configured_font[0].lower()
1147 font_size = configured_font[1]
1148 font_bold = configured_font[2]=='bold'
1149
1150 # Set editor font selection list and font_name.
1151 fonts = list(tkFont.families(self))
1152 fonts.sort()
1153 for font in fonts:
1154 self.fontlist.insert(END, font)
1155 self.font_name.set(font_name)
1156 lc_fonts = [s.lower() for s in fonts]
1157 try:
1158 current_font_index = lc_fonts.index(font_name)
1159 self.fontlist.see(current_font_index)
1160 self.fontlist.select_set(current_font_index)
1161 self.fontlist.select_anchor(current_font_index)
1162 self.fontlist.activate(current_font_index)
1163 except ValueError:
1164 pass
1165 # Set font size dropdown.
1166 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
1167 '16', '18', '20', '22', '25', '29', '34', '40'),
1168 font_size)
1169 # Set font weight.
1170 self.font_bold.set(font_bold)
1171 self.set_samples()
1172
1173 def var_changed_font(self, *params):
1174 """Store changes to font attributes.
1175
1176 When one font attribute changes, save them all, as they are
1177 not independent from each other. In particular, when we are
1178 overriding the default font, we need to write out everything.
1179 """
1180 value = self.font_name.get()
1181 changes.add_option('main', 'EditorWindow', 'font', value)
1182 value = self.font_size.get()
1183 changes.add_option('main', 'EditorWindow', 'font-size', value)
1184 value = self.font_bold.get()
1185 changes.add_option('main', 'EditorWindow', 'font-bold', value)
1186 self.set_samples()
1187
1188 def on_fontlist_select(self, event):
1189 """Handle selecting a font from the list.
1190
1191 Event can result from either mouse click or Up or Down key.
1192 Set font_name and example displays to selection.
1193 """
1194 font = self.fontlist.get(
1195 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR)
1196 self.font_name.set(font.lower())
1197
1198 def set_samples(self, event=None):
1199 """Update update both screen samples with the font settings.
1200
1201 Called on font initialization and change events.
1202 Accesses font_name, font_size, and font_bold Variables.
1203 Updates font_sample and hightlight page highlight_sample.
1204 """
1205 font_name = self.font_name.get()
1206 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL
1207 new_font = (font_name, self.font_size.get(), font_weight)
1208 self.font_sample['font'] = new_font
1209 self.highlight_sample['font'] = new_font
1210
1211 def load_tab_cfg(self):
1212 """Load current configuration settings for the tab options.
1213
1214 Attributes updated:
1215 space_num: Set to value from idleConf.
1216 """
1217 # Set indent sizes.
1218 space_num = idleConf.GetOption(
1219 'main', 'Indent', 'num-spaces', default=4, type='int')
1220 self.space_num.set(space_num)
1221
1222 def var_changed_space_num(self, *params):
1223 "Store change to indentation size."
1224 value = self.space_num.get()
1225 changes.add_option('main', 'Indent', 'num-spaces', value)
1226
1227
Terry Jan Reedyff4b2222017-08-15 19:13:11 -04001228class KeysPage(Frame):
1229
1230 def __init__(self, master):
1231 super().__init__(master)
1232 self.cd = master.master
1233 self.create_page_keys()
1234 self.load_key_cfg()
1235
1236 def create_page_keys(self):
1237 """Return frame of widgets for Keys tab.
1238
1239 Enable users to provisionally change both individual and sets of
1240 keybindings (shortcut keys). Except for features implemented as
1241 extensions, keybindings are stored in complete sets called
1242 keysets. Built-in keysets in idlelib/config-keys.def are fixed
1243 as far as the dialog is concerned. Any keyset can be used as the
1244 base for a new custom keyset, stored in .idlerc/config-keys.cfg.
1245
1246 Function load_key_cfg() initializes tk variables and keyset
1247 lists and calls load_keys_list for the current keyset.
1248 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var
1249 keyset_source, which controls if the current set of keybindings
1250 are from a builtin or custom keyset. DynOptionMenus builtinlist
1251 and customlist contain lists of the builtin and custom keysets,
1252 respectively, and the current item from each list is stored in
1253 vars builtin_name and custom_name.
1254
1255 Button delete_custom_keys invokes delete_custom_keys() to delete
1256 a custom keyset from idleConf.userCfg['keys'] and changes. Button
1257 save_custom_keys invokes save_as_new_key_set() which calls
1258 get_new_keys_name() and create_new_key_set() to save a custom keyset
1259 and its keybindings to idleConf.userCfg['keys'].
1260
1261 Listbox bindingslist contains all of the keybindings for the
1262 selected keyset. The keybindings are loaded in load_keys_list()
1263 and are pairs of (event, [keys]) where keys can be a list
1264 of one or more key combinations to bind to the same event.
1265 Mouse button 1 click invokes on_bindingslist_select(), which
1266 allows button_new_keys to be clicked.
1267
1268 So, an item is selected in listbindings, which activates
1269 button_new_keys, and clicking button_new_keys calls function
1270 get_new_keys(). Function get_new_keys() gets the key mappings from the
1271 current keyset for the binding event item that was selected. The
1272 function then displays another dialog, GetKeysDialog, with the
1273 selected binding event and current keys and always new key sequences
1274 to be entered for that binding event. If the keys aren't
1275 changed, nothing happens. If the keys are changed and the keyset
1276 is a builtin, function get_new_keys_name() will be called
1277 for input of a custom keyset name. If no name is given, then the
1278 change to the keybinding will abort and no updates will be made. If
1279 a custom name is entered in the prompt or if the current keyset was
1280 already custom (and thus didn't require a prompt), then
1281 idleConf.userCfg['keys'] is updated in function create_new_key_set()
1282 with the change to the event binding. The item listing in bindingslist
1283 is updated with the new keys. Var keybinding is also set which invokes
1284 the callback function, var_changed_keybinding, to add the change to
1285 the 'keys' or 'extensions' changes tracker based on the binding type.
1286
1287 Tk Variables:
1288 keybinding: Action/key bindings.
1289
1290 Methods:
1291 load_keys_list: Reload active set.
1292 create_new_key_set: Combine active keyset and changes.
1293 set_keys_type: Command for keyset_source.
1294 save_new_key_set: Save to idleConf.userCfg['keys'] (is function).
1295 deactivate_current_config: Remove keys bindings in editors.
1296
1297 Widgets for KeysPage(frame): (*) widgets bound to self
1298 frame_key_sets: LabelFrame
1299 frames[0]: Frame
1300 (*)builtin_keyset_on: Radiobutton - var keyset_source
1301 (*)custom_keyset_on: Radiobutton - var keyset_source
1302 (*)builtinlist: DynOptionMenu - var builtin_name,
1303 func keybinding_selected
1304 (*)customlist: DynOptionMenu - var custom_name,
1305 func keybinding_selected
1306 (*)keys_message: Label
1307 frames[1]: Frame
1308 (*)button_delete_custom_keys: Button - delete_custom_keys
1309 (*)button_save_custom_keys: Button - save_as_new_key_set
1310 frame_custom: LabelFrame
1311 frame_target: Frame
1312 target_title: Label
1313 scroll_target_y: Scrollbar
1314 scroll_target_x: Scrollbar
1315 (*)bindingslist: ListBox - on_bindingslist_select
1316 (*)button_new_keys: Button - get_new_keys & ..._name
1317 """
1318 self.builtin_name = tracers.add(
1319 StringVar(self), self.var_changed_builtin_name)
1320 self.custom_name = tracers.add(
1321 StringVar(self), self.var_changed_custom_name)
1322 self.keyset_source = tracers.add(
1323 BooleanVar(self), self.var_changed_keyset_source)
1324 self.keybinding = tracers.add(
1325 StringVar(self), self.var_changed_keybinding)
1326
1327 # Create widgets:
1328 # body and section frames.
1329 frame_custom = LabelFrame(
1330 self, borderwidth=2, relief=GROOVE,
1331 text=' Custom Key Bindings ')
1332 frame_key_sets = LabelFrame(
1333 self, borderwidth=2, relief=GROOVE, text=' Key Set ')
1334 # frame_custom.
1335 frame_target = Frame(frame_custom)
1336 target_title = Label(frame_target, text='Action - Key(s)')
1337 scroll_target_y = Scrollbar(frame_target)
1338 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
1339 self.bindingslist = Listbox(
1340 frame_target, takefocus=FALSE, exportselection=FALSE)
1341 self.bindingslist.bind('<ButtonRelease-1>',
1342 self.on_bindingslist_select)
1343 scroll_target_y['command'] = self.bindingslist.yview
1344 scroll_target_x['command'] = self.bindingslist.xview
1345 self.bindingslist['yscrollcommand'] = scroll_target_y.set
1346 self.bindingslist['xscrollcommand'] = scroll_target_x.set
1347 self.button_new_keys = Button(
1348 frame_custom, text='Get New Keys for Selection',
1349 command=self.get_new_keys, state=DISABLED)
1350 # frame_key_sets.
1351 frames = [Frame(frame_key_sets, padx=2, pady=2, borderwidth=0)
1352 for i in range(2)]
1353 self.builtin_keyset_on = Radiobutton(
1354 frames[0], variable=self.keyset_source, value=1,
1355 command=self.set_keys_type, text='Use a Built-in Key Set')
1356 self.custom_keyset_on = Radiobutton(
1357 frames[0], variable=self.keyset_source, value=0,
1358 command=self.set_keys_type, text='Use a Custom Key Set')
1359 self.builtinlist = DynOptionMenu(
1360 frames[0], self.builtin_name, None, command=None)
1361 self.customlist = DynOptionMenu(
1362 frames[0], self.custom_name, None, command=None)
1363 self.button_delete_custom_keys = Button(
1364 frames[1], text='Delete Custom Key Set',
1365 command=self.delete_custom_keys)
1366 self.button_save_custom_keys = Button(
1367 frames[1], text='Save as New Custom Key Set',
1368 command=self.save_as_new_key_set)
1369 self.keys_message = Label(frames[0], bd=2)
1370
1371 # Pack widgets:
1372 # body.
1373 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH)
1374 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH)
1375 # frame_custom.
1376 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5)
1377 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
1378 # frame_target.
1379 frame_target.columnconfigure(0, weight=1)
1380 frame_target.rowconfigure(1, weight=1)
1381 target_title.grid(row=0, column=0, columnspan=2, sticky=W)
1382 self.bindingslist.grid(row=1, column=0, sticky=NSEW)
1383 scroll_target_y.grid(row=1, column=1, sticky=NS)
1384 scroll_target_x.grid(row=2, column=0, sticky=EW)
1385 # frame_key_sets.
1386 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS)
1387 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS)
1388 self.builtinlist.grid(row=0, column=1, sticky=NSEW)
1389 self.customlist.grid(row=1, column=1, sticky=NSEW)
1390 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5)
1391 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1392 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2)
1393 frames[0].pack(side=TOP, fill=BOTH, expand=True)
1394 frames[1].pack(side=TOP, fill=X, expand=True, pady=2)
1395
1396 def load_key_cfg(self):
1397 "Load current configuration settings for the keybinding options."
1398 # Set current keys type radiobutton.
1399 self.keyset_source.set(idleConf.GetOption(
1400 'main', 'Keys', 'default', type='bool', default=1))
1401 # Set current keys.
1402 current_option = idleConf.CurrentKeys()
1403 # Load available keyset option menus.
1404 if self.keyset_source.get(): # Default theme selected.
1405 item_list = idleConf.GetSectionList('default', 'keys')
1406 item_list.sort()
1407 self.builtinlist.SetMenu(item_list, current_option)
1408 item_list = idleConf.GetSectionList('user', 'keys')
1409 item_list.sort()
1410 if not item_list:
1411 self.custom_keyset_on['state'] = DISABLED
1412 self.custom_name.set('- no custom keys -')
1413 else:
1414 self.customlist.SetMenu(item_list, item_list[0])
1415 else: # User key set selected.
1416 item_list = idleConf.GetSectionList('user', 'keys')
1417 item_list.sort()
1418 self.customlist.SetMenu(item_list, current_option)
1419 item_list = idleConf.GetSectionList('default', 'keys')
1420 item_list.sort()
1421 self.builtinlist.SetMenu(item_list, idleConf.default_keys())
1422 self.set_keys_type()
1423 # Load keyset element list.
1424 keyset_name = idleConf.CurrentKeys()
1425 self.load_keys_list(keyset_name)
1426
1427 def var_changed_builtin_name(self, *params):
1428 "Process selection of builtin key set."
1429 old_keys = (
1430 'IDLE Classic Windows',
1431 'IDLE Classic Unix',
1432 'IDLE Classic Mac',
1433 'IDLE Classic OSX',
1434 )
1435 value = self.builtin_name.get()
1436 if value not in old_keys:
1437 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys:
1438 changes.add_option('main', 'Keys', 'name', old_keys[0])
1439 changes.add_option('main', 'Keys', 'name2', value)
1440 self.keys_message['text'] = 'New key set, see Help'
1441 self.keys_message['fg'] = '#500000'
1442 else:
1443 changes.add_option('main', 'Keys', 'name', value)
1444 changes.add_option('main', 'Keys', 'name2', '')
1445 self.keys_message['text'] = ''
1446 self.keys_message['fg'] = 'black'
1447 self.load_keys_list(value)
1448
1449 def var_changed_custom_name(self, *params):
1450 "Process selection of custom key set."
1451 value = self.custom_name.get()
1452 if value != '- no custom keys -':
1453 changes.add_option('main', 'Keys', 'name', value)
1454 self.load_keys_list(value)
1455
1456 def var_changed_keyset_source(self, *params):
1457 "Process toggle between builtin key set and custom key set."
1458 value = self.keyset_source.get()
1459 changes.add_option('main', 'Keys', 'default', value)
1460 if value:
1461 self.var_changed_builtin_name()
1462 else:
1463 self.var_changed_custom_name()
1464
1465 def var_changed_keybinding(self, *params):
1466 "Store change to a keybinding."
1467 value = self.keybinding.get()
1468 key_set = self.custom_name.get()
1469 event = self.bindingslist.get(ANCHOR).split()[0]
1470 if idleConf.IsCoreBinding(event):
1471 changes.add_option('keys', key_set, event, value)
1472 else: # Event is an extension binding.
1473 ext_name = idleConf.GetExtnNameForEvent(event)
1474 ext_keybind_section = ext_name + '_cfgBindings'
1475 changes.add_option('extensions', ext_keybind_section, event, value)
1476
1477 def set_keys_type(self):
1478 "Set available screen options based on builtin or custom key set."
1479 if self.keyset_source.get():
1480 self.builtinlist['state'] = NORMAL
1481 self.customlist['state'] = DISABLED
1482 self.button_delete_custom_keys['state'] = DISABLED
1483 else:
1484 self.builtinlist['state'] = DISABLED
1485 self.custom_keyset_on['state'] = NORMAL
1486 self.customlist['state'] = NORMAL
1487 self.button_delete_custom_keys['state'] = NORMAL
1488
1489 def get_new_keys(self):
1490 """Handle event to change key binding for selected line.
1491
1492 A selection of a key/binding in the list of current
1493 bindings pops up a dialog to enter a new binding. If
1494 the current key set is builtin and a binding has
1495 changed, then a name for a custom key set needs to be
1496 entered for the change to be applied.
1497 """
1498 list_index = self.bindingslist.index(ANCHOR)
1499 binding = self.bindingslist.get(list_index)
1500 bind_name = binding.split()[0]
1501 if self.keyset_source.get():
1502 current_key_set_name = self.builtin_name.get()
1503 else:
1504 current_key_set_name = self.custom_name.get()
1505 current_bindings = idleConf.GetCurrentKeySet()
1506 if current_key_set_name in changes['keys']: # unsaved changes
1507 key_set_changes = changes['keys'][current_key_set_name]
1508 for event in key_set_changes:
1509 current_bindings[event] = key_set_changes[event].split()
1510 current_key_sequences = list(current_bindings.values())
1511 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name,
1512 current_key_sequences).result
1513 if new_keys:
1514 if self.keyset_source.get(): # Current key set is a built-in.
1515 message = ('Your changes will be saved as a new Custom Key Set.'
1516 ' Enter a name for your new Custom Key Set below.')
1517 new_keyset = self.get_new_keys_name(message)
1518 if not new_keyset: # User cancelled custom key set creation.
1519 self.bindingslist.select_set(list_index)
1520 self.bindingslist.select_anchor(list_index)
1521 return
1522 else: # Create new custom key set based on previously active key set.
1523 self.create_new_key_set(new_keyset)
1524 self.bindingslist.delete(list_index)
1525 self.bindingslist.insert(list_index, bind_name+' - '+new_keys)
1526 self.bindingslist.select_set(list_index)
1527 self.bindingslist.select_anchor(list_index)
1528 self.keybinding.set(new_keys)
1529 else:
1530 self.bindingslist.select_set(list_index)
1531 self.bindingslist.select_anchor(list_index)
1532
1533 def get_new_keys_name(self, message):
1534 "Return new key set name from query popup."
1535 used_names = (idleConf.GetSectionList('user', 'keys') +
1536 idleConf.GetSectionList('default', 'keys'))
1537 new_keyset = SectionName(
1538 self, 'New Custom Key Set', message, used_names).result
1539 return new_keyset
1540
1541 def save_as_new_key_set(self):
1542 "Prompt for name of new key set and save changes using that name."
1543 new_keys_name = self.get_new_keys_name('New Key Set Name:')
1544 if new_keys_name:
1545 self.create_new_key_set(new_keys_name)
1546
1547 def on_bindingslist_select(self, event):
1548 "Activate button to assign new keys to selected action."
1549 self.button_new_keys['state'] = NORMAL
1550
1551 def create_new_key_set(self, new_key_set_name):
1552 """Create a new custom key set with the given name.
1553
1554 Copy the bindings/keys from the previously active keyset
1555 to the new keyset and activate the new custom keyset.
1556 """
1557 if self.keyset_source.get():
1558 prev_key_set_name = self.builtin_name.get()
1559 else:
1560 prev_key_set_name = self.custom_name.get()
1561 prev_keys = idleConf.GetCoreKeys(prev_key_set_name)
1562 new_keys = {}
1563 for event in prev_keys: # Add key set to changed items.
1564 event_name = event[2:-2] # Trim off the angle brackets.
1565 binding = ' '.join(prev_keys[event])
1566 new_keys[event_name] = binding
1567 # Handle any unsaved changes to prev key set.
1568 if prev_key_set_name in changes['keys']:
1569 key_set_changes = changes['keys'][prev_key_set_name]
1570 for event in key_set_changes:
1571 new_keys[event] = key_set_changes[event]
1572 # Save the new key set.
1573 self.save_new_key_set(new_key_set_name, new_keys)
1574 # Change GUI over to the new key set.
1575 custom_key_list = idleConf.GetSectionList('user', 'keys')
1576 custom_key_list.sort()
1577 self.customlist.SetMenu(custom_key_list, new_key_set_name)
1578 self.keyset_source.set(0)
1579 self.set_keys_type()
1580
1581 def load_keys_list(self, keyset_name):
1582 """Reload the list of action/key binding pairs for the active key set.
1583
1584 An action/key binding can be selected to change the key binding.
1585 """
1586 reselect = False
1587 if self.bindingslist.curselection():
1588 reselect = True
1589 list_index = self.bindingslist.index(ANCHOR)
1590 keyset = idleConf.GetKeySet(keyset_name)
1591 bind_names = list(keyset.keys())
1592 bind_names.sort()
1593 self.bindingslist.delete(0, END)
1594 for bind_name in bind_names:
1595 key = ' '.join(keyset[bind_name])
1596 bind_name = bind_name[2:-2] # Trim off the angle brackets.
1597 if keyset_name in changes['keys']:
1598 # Handle any unsaved changes to this key set.
1599 if bind_name in changes['keys'][keyset_name]:
1600 key = changes['keys'][keyset_name][bind_name]
1601 self.bindingslist.insert(END, bind_name+' - '+key)
1602 if reselect:
1603 self.bindingslist.see(list_index)
1604 self.bindingslist.select_set(list_index)
1605 self.bindingslist.select_anchor(list_index)
1606
1607 @staticmethod
1608 def save_new_key_set(keyset_name, keyset):
1609 """Save a newly created core key set.
1610
1611 Add keyset to idleConf.userCfg['keys'], not to disk.
1612 If the keyset doesn't exist, it is created. The
1613 binding/keys are taken from the keyset argument.
1614
1615 keyset_name - string, the name of the new key set
1616 keyset - dictionary containing the new keybindings
1617 """
1618 if not idleConf.userCfg['keys'].has_section(keyset_name):
1619 idleConf.userCfg['keys'].add_section(keyset_name)
1620 for event in keyset:
1621 value = keyset[event]
1622 idleConf.userCfg['keys'].SetOption(keyset_name, event, value)
1623
1624 def delete_custom_keys(self):
1625 """Handle event to delete a custom key set.
1626
1627 Applying the delete deactivates the current configuration and
1628 reverts to the default. The custom key set is permanently
1629 deleted from the config file.
1630 """
1631 keyset_name = self.custom_name.get()
1632 delmsg = 'Are you sure you wish to delete the key set %r ?'
1633 if not tkMessageBox.askyesno(
1634 'Delete Key Set', delmsg % keyset_name, parent=self):
1635 return
1636 self.cd.deactivate_current_config()
1637 # Remove key set from changes, config, and file.
1638 changes.delete_section('keys', keyset_name)
1639 # Reload user key set list.
1640 item_list = idleConf.GetSectionList('user', 'keys')
1641 item_list.sort()
1642 if not item_list:
1643 self.custom_keyset_on['state'] = DISABLED
1644 self.customlist.SetMenu(item_list, '- no custom keys -')
1645 else:
1646 self.customlist.SetMenu(item_list, item_list[0])
1647 # Revert to default key set.
1648 self.keyset_source.set(idleConf.defaultCfg['main']
1649 .Get('Keys', 'default'))
1650 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
1651 or idleConf.default_keys())
1652 # User can't back out of these changes, they must be applied now.
1653 changes.save_all()
1654 self.cd.save_all_changed_extensions()
1655 self.cd.activate_config_changes()
1656 self.set_keys_type()
1657
1658
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -04001659class GenPage(Frame):
1660
Terry Jan Reedy48fcc722017-08-01 01:00:33 -04001661 def __init__(self, master):
1662 super().__init__(master)
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -04001663 self.create_page_general()
1664 self.load_general_cfg()
1665
1666 def create_page_general(self):
1667 """Return frame of widgets for General tab.
1668
1669 Enable users to provisionally change general options. Function
1670 load_general_cfg intializes tk variables and helplist using
1671 idleConf. Radiobuttons startup_shell_on and startup_editor_on
1672 set var startup_edit. Radiobuttons save_ask_on and save_auto_on
1673 set var autosave. Entry boxes win_width_int and win_height_int
1674 set var win_width and win_height. Setting var_name invokes the
1675 default callback that adds option to changes.
1676
1677 Helplist: load_general_cfg loads list user_helplist with
1678 name, position pairs and copies names to listbox helplist.
1679 Clicking a name invokes help_source selected. Clicking
1680 button_helplist_name invokes helplist_item_name, which also
1681 changes user_helplist. These functions all call
1682 set_add_delete_state. All but load call update_help_changes to
1683 rewrite changes['main']['HelpFiles'].
1684
Terry Jan Reedya3145902017-08-14 21:45:02 -04001685 Widgets for GenPage(Frame): (*) widgets bound to self
1686 frame_run: LabelFrame
1687 startup_title: Label
1688 (*)startup_editor_on: Radiobutton - startup_edit
1689 (*)startup_shell_on: Radiobutton - startup_edit
1690 frame_save: LabelFrame
1691 run_save_title: Label
1692 (*)save_ask_on: Radiobutton - autosave
1693 (*)save_auto_on: Radiobutton - autosave
1694 frame_win_size: LabelFrame
1695 win_size_title: Label
1696 win_width_title: Label
1697 (*)win_width_int: Entry - win_width
1698 win_height_title: Label
1699 (*)win_height_int: Entry - win_height
1700 frame_help: LabelFrame
1701 frame_helplist: Frame
1702 frame_helplist_buttons: Frame
1703 (*)button_helplist_edit
1704 (*)button_helplist_add
1705 (*)button_helplist_remove
1706 (*)helplist: ListBox
1707 scroll_helplist: Scrollbar
Terry Jan Reedy8c4e5be2017-07-30 19:02:51 -04001708 """
1709 self.startup_edit = tracers.add(
1710 IntVar(self), ('main', 'General', 'editor-on-startup'))
1711 self.autosave = tracers.add(
1712 IntVar(self), ('main', 'General', 'autosave'))
1713 self.win_width = tracers.add(
1714 StringVar(self), ('main', 'EditorWindow', 'width'))
1715 self.win_height = tracers.add(
1716 StringVar(self), ('main', 'EditorWindow', 'height'))
1717
1718 # Create widgets:
1719 # Section frames.
1720 frame_run = LabelFrame(self, borderwidth=2, relief=GROOVE,
1721 text=' Startup Preferences ')
1722 frame_save = LabelFrame(self, borderwidth=2, relief=GROOVE,
1723 text=' autosave Preferences ')
1724 frame_win_size = Frame(self, borderwidth=2, relief=GROOVE)
1725 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE,
1726 text=' Additional Help Sources ')
1727 # frame_run.
1728 startup_title = Label(frame_run, text='At Startup')
1729 self.startup_editor_on = Radiobutton(
1730 frame_run, variable=self.startup_edit, value=1,
1731 text="Open Edit Window")
1732 self.startup_shell_on = Radiobutton(
1733 frame_run, variable=self.startup_edit, value=0,
1734 text='Open Shell Window')
1735 # frame_save.
1736 run_save_title = Label(frame_save, text='At Start of Run (F5) ')
1737 self.save_ask_on = Radiobutton(
1738 frame_save, variable=self.autosave, value=0,
1739 text="Prompt to Save")
1740 self.save_auto_on = Radiobutton(
1741 frame_save, variable=self.autosave, value=1,
1742 text='No Prompt')
1743 # frame_win_size.
1744 win_size_title = Label(
1745 frame_win_size, text='Initial Window Size (in characters)')
1746 win_width_title = Label(frame_win_size, text='Width')
1747 self.win_width_int = Entry(
1748 frame_win_size, textvariable=self.win_width, width=3)
1749 win_height_title = Label(frame_win_size, text='Height')
1750 self.win_height_int = Entry(
1751 frame_win_size, textvariable=self.win_height, width=3)
1752 # frame_help.
1753 frame_helplist = Frame(frame_help)
1754 frame_helplist_buttons = Frame(frame_helplist)
1755 self.helplist = Listbox(
1756 frame_helplist, height=5, takefocus=True,
1757 exportselection=FALSE)
1758 scroll_helplist = Scrollbar(frame_helplist)
1759 scroll_helplist['command'] = self.helplist.yview
1760 self.helplist['yscrollcommand'] = scroll_helplist.set
1761 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
1762 self.button_helplist_edit = Button(
1763 frame_helplist_buttons, text='Edit', state=DISABLED,
1764 width=8, command=self.helplist_item_edit)
1765 self.button_helplist_add = Button(
1766 frame_helplist_buttons, text='Add',
1767 width=8, command=self.helplist_item_add)
1768 self.button_helplist_remove = Button(
1769 frame_helplist_buttons, text='Remove', state=DISABLED,
1770 width=8, command=self.helplist_item_remove)
1771
1772 # Pack widgets:
1773 # body.
1774 frame_run.pack(side=TOP, padx=5, pady=5, fill=X)
1775 frame_save.pack(side=TOP, padx=5, pady=5, fill=X)
1776 frame_win_size.pack(side=TOP, padx=5, pady=5, fill=X)
1777 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1778 # frame_run.
1779 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1780 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1781 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1782 # frame_save.
1783 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1784 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1785 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5)
1786 # frame_win_size.
1787 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5)
1788 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1789 win_height_title.pack(side=RIGHT, anchor=E, pady=5)
1790 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5)
1791 win_width_title.pack(side=RIGHT, anchor=E, pady=5)
1792 # frame_help.
1793 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y)
1794 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
1795 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y)
1796 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH)
1797 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5)
1798 self.button_helplist_add.pack(side=TOP, anchor=W)
1799 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
1800
1801 def load_general_cfg(self):
1802 "Load current configuration settings for the general options."
1803 # Set startup state.
1804 self.startup_edit.set(idleConf.GetOption(
1805 'main', 'General', 'editor-on-startup', default=0, type='bool'))
1806 # Set autosave state.
1807 self.autosave.set(idleConf.GetOption(
1808 'main', 'General', 'autosave', default=0, type='bool'))
1809 # Set initial window size.
1810 self.win_width.set(idleConf.GetOption(
1811 'main', 'EditorWindow', 'width', type='int'))
1812 self.win_height.set(idleConf.GetOption(
1813 'main', 'EditorWindow', 'height', type='int'))
1814 # Set additional help sources.
1815 self.user_helplist = idleConf.GetAllExtraHelpSourcesList()
1816 self.helplist.delete(0, 'end')
1817 for help_item in self.user_helplist:
1818 self.helplist.insert(END, help_item[0])
1819 self.set_add_delete_state()
1820
1821 def help_source_selected(self, event):
1822 "Handle event for selecting additional help."
1823 self.set_add_delete_state()
1824
1825 def set_add_delete_state(self):
1826 "Toggle the state for the help list buttons based on list entries."
1827 if self.helplist.size() < 1: # No entries in list.
1828 self.button_helplist_edit['state'] = DISABLED
1829 self.button_helplist_remove['state'] = DISABLED
1830 else: # Some entries.
1831 if self.helplist.curselection(): # There currently is a selection.
1832 self.button_helplist_edit['state'] = NORMAL
1833 self.button_helplist_remove['state'] = NORMAL
1834 else: # There currently is not a selection.
1835 self.button_helplist_edit['state'] = DISABLED
1836 self.button_helplist_remove['state'] = DISABLED
1837
1838 def helplist_item_add(self):
1839 """Handle add button for the help list.
1840
1841 Query for name and location of new help sources and add
1842 them to the list.
1843 """
1844 help_source = HelpSource(self, 'New Help Source').result
1845 if help_source:
1846 self.user_helplist.append(help_source)
1847 self.helplist.insert(END, help_source[0])
1848 self.update_help_changes()
1849
1850 def helplist_item_edit(self):
1851 """Handle edit button for the help list.
1852
1853 Query with existing help source information and update
1854 config if the values are changed.
1855 """
1856 item_index = self.helplist.index(ANCHOR)
1857 help_source = self.user_helplist[item_index]
1858 new_help_source = HelpSource(
1859 self, 'Edit Help Source',
1860 menuitem=help_source[0],
1861 filepath=help_source[1],
1862 ).result
1863 if new_help_source and new_help_source != help_source:
1864 self.user_helplist[item_index] = new_help_source
1865 self.helplist.delete(item_index)
1866 self.helplist.insert(item_index, new_help_source[0])
1867 self.update_help_changes()
1868 self.set_add_delete_state() # Selected will be un-selected
1869
1870 def helplist_item_remove(self):
1871 """Handle remove button for the help list.
1872
1873 Delete the help list item from config.
1874 """
1875 item_index = self.helplist.index(ANCHOR)
1876 del(self.user_helplist[item_index])
1877 self.helplist.delete(item_index)
1878 self.update_help_changes()
1879 self.set_add_delete_state()
1880
1881 def update_help_changes(self):
1882 "Clear and rebuild the HelpFiles section in changes"
1883 changes['main']['HelpFiles'] = {}
1884 for num in range(1, len(self.user_helplist) + 1):
1885 changes.add_option(
1886 'main', 'HelpFiles', str(num),
1887 ';'.join(self.user_helplist[num-1][:2]))
1888
1889
Terry Jan Reedy0243bea2017-07-26 20:53:13 -04001890class VarTrace:
1891 """Maintain Tk variables trace state."""
1892
1893 def __init__(self):
1894 """Store Tk variables and callbacks.
1895
1896 untraced: List of tuples (var, callback)
1897 that do not have the callback attached
1898 to the Tk var.
1899 traced: List of tuples (var, callback) where
1900 that callback has been attached to the var.
1901 """
1902 self.untraced = []
1903 self.traced = []
1904
Terry Jan Reedyecc80b32017-07-28 18:36:30 -04001905 def clear(self):
1906 "Clear lists (for tests)."
Terry Jan Reedy9d7d9282017-08-07 15:20:03 -04001907 # Call after all tests in a module to avoid memory leaks.
Terry Jan Reedyecc80b32017-07-28 18:36:30 -04001908 self.untraced.clear()
1909 self.traced.clear()
1910
Terry Jan Reedy0243bea2017-07-26 20:53:13 -04001911 def add(self, var, callback):
1912 """Add (var, callback) tuple to untraced list.
1913
1914 Args:
1915 var: Tk variable instance.
Terry Jan Reedy02f88d22017-07-28 15:42:43 -04001916 callback: Either function name to be used as a callback
1917 or a tuple with IdleConf config-type, section, and
1918 option names used in the default callback.
Terry Jan Reedy0243bea2017-07-26 20:53:13 -04001919
1920 Return:
1921 Tk variable instance.
1922 """
1923 if isinstance(callback, tuple):
1924 callback = self.make_callback(var, callback)
1925 self.untraced.append((var, callback))
1926 return var
1927
1928 @staticmethod
1929 def make_callback(var, config):
1930 "Return default callback function to add values to changes instance."
1931 def default_callback(*params):
1932 "Add config values to changes instance."
1933 changes.add_option(*config, var.get())
1934 return default_callback
1935
1936 def attach(self):
1937 "Attach callback to all vars that are not traced."
1938 while self.untraced:
1939 var, callback = self.untraced.pop()
1940 var.trace_add('write', callback)
1941 self.traced.append((var, callback))
1942
1943 def detach(self):
1944 "Remove callback from traced vars."
1945 while self.traced:
1946 var, callback = self.traced.pop()
1947 var.trace_remove('write', var.trace_info()[0][1])
1948 self.untraced.append((var, callback))
1949
1950
Terry Jan Reedy02f88d22017-07-28 15:42:43 -04001951tracers = VarTrace()
1952
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001953help_common = '''\
1954When you click either the Apply or Ok buttons, settings in this
1955dialog that are different from IDLE's default are saved in
1956a .idlerc directory in your home directory. Except as noted,
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05001957these changes apply to all versions of IDLE installed on this
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001958machine. Some do not take affect until IDLE is restarted.
1959[Cancel] only cancels changes made since the last save.
1960'''
1961help_pages = {
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04001962 'Highlighting': '''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001963Highlighting:
Terry Jan Reedyd0c0f002015-11-12 15:02:57 -05001964The IDLE Dark color theme is new in October 2015. It can only
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001965be used with older IDLE releases if it is saved as a custom
1966theme, with a different name.
Terry Jan Reedy9bdb1ed2016-07-10 13:46:34 -04001967''',
1968 'Keys': '''
1969Keys:
1970The IDLE Modern Unix key set is new in June 2016. It can only
1971be used with older IDLE releases if it is saved as a custom
1972key set, with a different name.
1973''',
terryjreedyaf683822017-06-27 23:02:19 -04001974 'Extensions': '''
1975Extensions:
1976
1977Autocomplete: Popupwait is milleseconds to wait after key char, without
1978cursor movement, before popping up completion box. Key char is '.' after
1979identifier or a '/' (or '\\' on Windows) within a string.
1980
1981FormatParagraph: Max-width is max chars in lines after re-formatting.
1982Use with paragraphs in both strings and comment blocks.
1983
1984ParenMatch: Style indicates what is highlighted when closer is entered:
1985'opener' - opener '({[' corresponding to closer; 'parens' - both chars;
1986'expression' (default) - also everything in between. Flash-delay is how
1987long to highlight if cursor is not moved (0 means forever).
1988'''
Terry Jan Reedyd0cadba2015-10-11 22:07:31 -04001989}
1990
Steven M. Gavac11ccf32001-09-24 09:43:17 +00001991
Terry Jan Reedy93f35422015-10-13 22:03:51 -04001992def is_int(s):
1993 "Return 's is blank or represents an int'"
1994 if not s:
1995 return True
1996 try:
1997 int(s)
1998 return True
1999 except ValueError:
2000 return False
2001
2002
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002003class VerticalScrolledFrame(Frame):
2004 """A pure Tkinter vertically scrollable frame.
2005
2006 * Use the 'interior' attribute to place widgets inside the scrollable frame
2007 * Construct and pack/place/grid normally
2008 * This frame only allows vertical scrolling
2009 """
2010 def __init__(self, parent, *args, **kw):
2011 Frame.__init__(self, parent, *args, **kw)
2012
terryjreedye5bb1122017-07-05 00:54:55 -04002013 # Create a canvas object and a vertical scrollbar for scrolling it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002014 vscrollbar = Scrollbar(self, orient=VERTICAL)
2015 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE)
2016 canvas = Canvas(self, bd=0, highlightthickness=0,
Terry Jan Reedyd0812292015-10-22 03:27:31 -04002017 yscrollcommand=vscrollbar.set, width=240)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002018 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
2019 vscrollbar.config(command=canvas.yview)
2020
terryjreedye5bb1122017-07-05 00:54:55 -04002021 # Reset the view.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002022 canvas.xview_moveto(0)
2023 canvas.yview_moveto(0)
2024
terryjreedye5bb1122017-07-05 00:54:55 -04002025 # Create a frame inside the canvas which will be scrolled with it.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002026 self.interior = interior = Frame(canvas)
2027 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
2028
terryjreedye5bb1122017-07-05 00:54:55 -04002029 # Track changes to the canvas and frame width and sync them,
2030 # also updating the scrollbar.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002031 def _configure_interior(event):
terryjreedye5bb1122017-07-05 00:54:55 -04002032 # Update the scrollbars to match the size of the inner frame.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002033 size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
2034 canvas.config(scrollregion="0 0 %s %s" % size)
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002035 interior.bind('<Configure>', _configure_interior)
2036
2037 def _configure_canvas(event):
2038 if interior.winfo_reqwidth() != canvas.winfo_width():
terryjreedye5bb1122017-07-05 00:54:55 -04002039 # Update the inner frame's width to fill the canvas.
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002040 canvas.itemconfigure(interior_id, width=canvas.winfo_width())
2041 canvas.bind('<Configure>', _configure_canvas)
2042
2043 return
2044
Terry Jan Reedya9421fb2014-10-22 20:15:18 -04002045
Steven M. Gava44d3d1a2001-07-31 06:59:02 +00002046if __name__ == '__main__':
Terry Jan Reedycfa89502014-07-14 23:07:32 -04002047 import unittest
2048 unittest.main('idlelib.idle_test.test_configdialog',
2049 verbosity=2, exit=False)
Terry Jan Reedy2e8234a2014-05-29 01:46:26 -04002050 from idlelib.idle_test.htest import run
Terry Jan Reedy47304c02015-10-20 02:15:28 -04002051 run(ConfigDialog)