Mercurial > repos > rliterman > csp2
comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/configdialog.py @ 68:5028fdace37b
planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author | jpayne |
---|---|
date | Tue, 18 Mar 2025 16:23:26 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
67:0e9998148a16 | 68:5028fdace37b |
---|---|
1 """IDLE Configuration Dialog: support user customization of IDLE by GUI | |
2 | |
3 Customize font faces, sizes, and colorization attributes. Set indentation | |
4 defaults. Customize keybindings. Colorization and keybindings can be | |
5 saved as user defined sets. Select startup options including shell/editor | |
6 and default window size. Define additional help sources. | |
7 | |
8 Note that tab width in IDLE is currently fixed at eight due to Tk issues. | |
9 Refer to comments in EditorWindow autoindent code for details. | |
10 | |
11 """ | |
12 import re | |
13 | |
14 from tkinter import (Toplevel, Listbox, Text, Scale, Canvas, | |
15 StringVar, BooleanVar, IntVar, TRUE, FALSE, | |
16 TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE, | |
17 NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW, | |
18 HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END) | |
19 from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label, | |
20 OptionMenu, Notebook, Radiobutton, Scrollbar, Style) | |
21 import tkinter.colorchooser as tkColorChooser | |
22 import tkinter.font as tkFont | |
23 from tkinter import messagebox | |
24 | |
25 from idlelib.config import idleConf, ConfigChanges | |
26 from idlelib.config_key import GetKeysDialog | |
27 from idlelib.dynoption import DynOptionMenu | |
28 from idlelib import macosx | |
29 from idlelib.query import SectionName, HelpSource | |
30 from idlelib.textview import view_text | |
31 from idlelib.autocomplete import AutoComplete | |
32 from idlelib.codecontext import CodeContext | |
33 from idlelib.parenmatch import ParenMatch | |
34 from idlelib.format import FormatParagraph | |
35 from idlelib.squeezer import Squeezer | |
36 from idlelib.textview import ScrollableTextFrame | |
37 | |
38 changes = ConfigChanges() | |
39 # Reload changed options in the following classes. | |
40 reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph, | |
41 Squeezer) | |
42 | |
43 | |
44 class ConfigDialog(Toplevel): | |
45 """Config dialog for IDLE. | |
46 """ | |
47 | |
48 def __init__(self, parent, title='', *, _htest=False, _utest=False): | |
49 """Show the tabbed dialog for user configuration. | |
50 | |
51 Args: | |
52 parent - parent of this dialog | |
53 title - string which is the title of this popup dialog | |
54 _htest - bool, change box location when running htest | |
55 _utest - bool, don't wait_window when running unittest | |
56 | |
57 Note: Focus set on font page fontlist. | |
58 | |
59 Methods: | |
60 create_widgets | |
61 cancel: Bound to DELETE_WINDOW protocol. | |
62 """ | |
63 Toplevel.__init__(self, parent) | |
64 self.parent = parent | |
65 if _htest: | |
66 parent.instance_dict = {} | |
67 if not _utest: | |
68 self.withdraw() | |
69 | |
70 self.configure(borderwidth=5) | |
71 self.title(title or 'IDLE Preferences') | |
72 x = parent.winfo_rootx() + 20 | |
73 y = parent.winfo_rooty() + (30 if not _htest else 150) | |
74 self.geometry(f'+{x}+{y}') | |
75 # Each theme element key is its display name. | |
76 # The first value of the tuple is the sample area tag name. | |
77 # The second value is the display name list sort index. | |
78 self.create_widgets() | |
79 self.resizable(height=FALSE, width=FALSE) | |
80 self.transient(parent) | |
81 self.protocol("WM_DELETE_WINDOW", self.cancel) | |
82 self.fontpage.fontlist.focus_set() | |
83 # XXX Decide whether to keep or delete these key bindings. | |
84 # Key bindings for this dialog. | |
85 # self.bind('<Escape>', self.Cancel) #dismiss dialog, no save | |
86 # self.bind('<Alt-a>', self.Apply) #apply changes, save | |
87 # self.bind('<F1>', self.Help) #context help | |
88 # Attach callbacks after loading config to avoid calling them. | |
89 tracers.attach() | |
90 | |
91 if not _utest: | |
92 self.grab_set() | |
93 self.wm_deiconify() | |
94 self.wait_window() | |
95 | |
96 def create_widgets(self): | |
97 """Create and place widgets for tabbed dialog. | |
98 | |
99 Widgets Bound to self: | |
100 note: Notebook | |
101 highpage: HighPage | |
102 fontpage: FontPage | |
103 keyspage: KeysPage | |
104 genpage: GenPage | |
105 extpage: self.create_page_extensions | |
106 | |
107 Methods: | |
108 create_action_buttons | |
109 load_configs: Load pages except for extensions. | |
110 activate_config_changes: Tell editors to reload. | |
111 """ | |
112 self.note = note = Notebook(self) | |
113 self.highpage = HighPage(note) | |
114 self.fontpage = FontPage(note, self.highpage) | |
115 self.keyspage = KeysPage(note) | |
116 self.genpage = GenPage(note) | |
117 self.extpage = self.create_page_extensions() | |
118 note.add(self.fontpage, text='Fonts/Tabs') | |
119 note.add(self.highpage, text='Highlights') | |
120 note.add(self.keyspage, text=' Keys ') | |
121 note.add(self.genpage, text=' General ') | |
122 note.add(self.extpage, text='Extensions') | |
123 note.enable_traversal() | |
124 note.pack(side=TOP, expand=TRUE, fill=BOTH) | |
125 self.create_action_buttons().pack(side=BOTTOM) | |
126 | |
127 def create_action_buttons(self): | |
128 """Return frame of action buttons for dialog. | |
129 | |
130 Methods: | |
131 ok | |
132 apply | |
133 cancel | |
134 help | |
135 | |
136 Widget Structure: | |
137 outer: Frame | |
138 buttons: Frame | |
139 (no assignment): Button (ok) | |
140 (no assignment): Button (apply) | |
141 (no assignment): Button (cancel) | |
142 (no assignment): Button (help) | |
143 (no assignment): Frame | |
144 """ | |
145 if macosx.isAquaTk(): | |
146 # Changing the default padding on OSX results in unreadable | |
147 # text in the buttons. | |
148 padding_args = {} | |
149 else: | |
150 padding_args = {'padding': (6, 3)} | |
151 outer = Frame(self, padding=2) | |
152 buttons = Frame(outer, padding=2) | |
153 for txt, cmd in ( | |
154 ('Ok', self.ok), | |
155 ('Apply', self.apply), | |
156 ('Cancel', self.cancel), | |
157 ('Help', self.help)): | |
158 Button(buttons, text=txt, command=cmd, takefocus=FALSE, | |
159 **padding_args).pack(side=LEFT, padx=5) | |
160 # Add space above buttons. | |
161 Frame(outer, height=2, borderwidth=0).pack(side=TOP) | |
162 buttons.pack(side=BOTTOM) | |
163 return outer | |
164 | |
165 def ok(self): | |
166 """Apply config changes, then dismiss dialog. | |
167 | |
168 Methods: | |
169 apply | |
170 destroy: inherited | |
171 """ | |
172 self.apply() | |
173 self.destroy() | |
174 | |
175 def apply(self): | |
176 """Apply config changes and leave dialog open. | |
177 | |
178 Methods: | |
179 deactivate_current_config | |
180 save_all_changed_extensions | |
181 activate_config_changes | |
182 """ | |
183 self.deactivate_current_config() | |
184 changes.save_all() | |
185 self.save_all_changed_extensions() | |
186 self.activate_config_changes() | |
187 | |
188 def cancel(self): | |
189 """Dismiss config dialog. | |
190 | |
191 Methods: | |
192 destroy: inherited | |
193 """ | |
194 self.destroy() | |
195 | |
196 def destroy(self): | |
197 global font_sample_text | |
198 font_sample_text = self.fontpage.font_sample.get('1.0', 'end') | |
199 self.grab_release() | |
200 super().destroy() | |
201 | |
202 def help(self): | |
203 """Create textview for config dialog help. | |
204 | |
205 Attributes accessed: | |
206 note | |
207 | |
208 Methods: | |
209 view_text: Method from textview module. | |
210 """ | |
211 page = self.note.tab(self.note.select(), option='text').strip() | |
212 view_text(self, title='Help for IDLE preferences', | |
213 text=help_common+help_pages.get(page, '')) | |
214 | |
215 def deactivate_current_config(self): | |
216 """Remove current key bindings. | |
217 Iterate over window instances defined in parent and remove | |
218 the keybindings. | |
219 """ | |
220 # Before a config is saved, some cleanup of current | |
221 # config must be done - remove the previous keybindings. | |
222 win_instances = self.parent.instance_dict.keys() | |
223 for instance in win_instances: | |
224 instance.RemoveKeybindings() | |
225 | |
226 def activate_config_changes(self): | |
227 """Apply configuration changes to current windows. | |
228 | |
229 Dynamically update the current parent window instances | |
230 with some of the configuration changes. | |
231 """ | |
232 win_instances = self.parent.instance_dict.keys() | |
233 for instance in win_instances: | |
234 instance.ResetColorizer() | |
235 instance.ResetFont() | |
236 instance.set_notabs_indentwidth() | |
237 instance.ApplyKeybindings() | |
238 instance.reset_help_menu_entries() | |
239 instance.update_cursor_blink() | |
240 for klass in reloadables: | |
241 klass.reload() | |
242 | |
243 def create_page_extensions(self): | |
244 """Part of the config dialog used for configuring IDLE extensions. | |
245 | |
246 This code is generic - it works for any and all IDLE extensions. | |
247 | |
248 IDLE extensions save their configuration options using idleConf. | |
249 This code reads the current configuration using idleConf, supplies a | |
250 GUI interface to change the configuration values, and saves the | |
251 changes using idleConf. | |
252 | |
253 Not all changes take effect immediately - some may require restarting IDLE. | |
254 This depends on each extension's implementation. | |
255 | |
256 All values are treated as text, and it is up to the user to supply | |
257 reasonable values. The only exception to this are the 'enable*' options, | |
258 which are boolean, and can be toggled with a True/False button. | |
259 | |
260 Methods: | |
261 load_extensions: | |
262 extension_selected: Handle selection from list. | |
263 create_extension_frame: Hold widgets for one extension. | |
264 set_extension_value: Set in userCfg['extensions']. | |
265 save_all_changed_extensions: Call extension page Save(). | |
266 """ | |
267 parent = self.parent | |
268 frame = Frame(self.note) | |
269 self.ext_defaultCfg = idleConf.defaultCfg['extensions'] | |
270 self.ext_userCfg = idleConf.userCfg['extensions'] | |
271 self.is_int = self.register(is_int) | |
272 self.load_extensions() | |
273 # Create widgets - a listbox shows all available extensions, with the | |
274 # controls for the extension selected in the listbox to the right. | |
275 self.extension_names = StringVar(self) | |
276 frame.rowconfigure(0, weight=1) | |
277 frame.columnconfigure(2, weight=1) | |
278 self.extension_list = Listbox(frame, listvariable=self.extension_names, | |
279 selectmode='browse') | |
280 self.extension_list.bind('<<ListboxSelect>>', self.extension_selected) | |
281 scroll = Scrollbar(frame, command=self.extension_list.yview) | |
282 self.extension_list.yscrollcommand=scroll.set | |
283 self.details_frame = LabelFrame(frame, width=250, height=250) | |
284 self.extension_list.grid(column=0, row=0, sticky='nws') | |
285 scroll.grid(column=1, row=0, sticky='ns') | |
286 self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0]) | |
287 frame.configure(padding=10) | |
288 self.config_frame = {} | |
289 self.current_extension = None | |
290 | |
291 self.outerframe = self # TEMPORARY | |
292 self.tabbed_page_set = self.extension_list # TEMPORARY | |
293 | |
294 # Create the frame holding controls for each extension. | |
295 ext_names = '' | |
296 for ext_name in sorted(self.extensions): | |
297 self.create_extension_frame(ext_name) | |
298 ext_names = ext_names + '{' + ext_name + '} ' | |
299 self.extension_names.set(ext_names) | |
300 self.extension_list.selection_set(0) | |
301 self.extension_selected(None) | |
302 | |
303 return frame | |
304 | |
305 def load_extensions(self): | |
306 "Fill self.extensions with data from the default and user configs." | |
307 self.extensions = {} | |
308 for ext_name in idleConf.GetExtensions(active_only=False): | |
309 # Former built-in extensions are already filtered out. | |
310 self.extensions[ext_name] = [] | |
311 | |
312 for ext_name in self.extensions: | |
313 opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) | |
314 | |
315 # Bring 'enable' options to the beginning of the list. | |
316 enables = [opt_name for opt_name in opt_list | |
317 if opt_name.startswith('enable')] | |
318 for opt_name in enables: | |
319 opt_list.remove(opt_name) | |
320 opt_list = enables + opt_list | |
321 | |
322 for opt_name in opt_list: | |
323 def_str = self.ext_defaultCfg.Get( | |
324 ext_name, opt_name, raw=True) | |
325 try: | |
326 def_obj = {'True':True, 'False':False}[def_str] | |
327 opt_type = 'bool' | |
328 except KeyError: | |
329 try: | |
330 def_obj = int(def_str) | |
331 opt_type = 'int' | |
332 except ValueError: | |
333 def_obj = def_str | |
334 opt_type = None | |
335 try: | |
336 value = self.ext_userCfg.Get( | |
337 ext_name, opt_name, type=opt_type, raw=True, | |
338 default=def_obj) | |
339 except ValueError: # Need this until .Get fixed. | |
340 value = def_obj # Bad values overwritten by entry. | |
341 var = StringVar(self) | |
342 var.set(str(value)) | |
343 | |
344 self.extensions[ext_name].append({'name': opt_name, | |
345 'type': opt_type, | |
346 'default': def_str, | |
347 'value': value, | |
348 'var': var, | |
349 }) | |
350 | |
351 def extension_selected(self, event): | |
352 "Handle selection of an extension from the list." | |
353 newsel = self.extension_list.curselection() | |
354 if newsel: | |
355 newsel = self.extension_list.get(newsel) | |
356 if newsel is None or newsel != self.current_extension: | |
357 if self.current_extension: | |
358 self.details_frame.config(text='') | |
359 self.config_frame[self.current_extension].grid_forget() | |
360 self.current_extension = None | |
361 if newsel: | |
362 self.details_frame.config(text=newsel) | |
363 self.config_frame[newsel].grid(column=0, row=0, sticky='nsew') | |
364 self.current_extension = newsel | |
365 | |
366 def create_extension_frame(self, ext_name): | |
367 """Create a frame holding the widgets to configure one extension""" | |
368 f = VerticalScrolledFrame(self.details_frame, height=250, width=250) | |
369 self.config_frame[ext_name] = f | |
370 entry_area = f.interior | |
371 # Create an entry for each configuration option. | |
372 for row, opt in enumerate(self.extensions[ext_name]): | |
373 # Create a row with a label and entry/checkbutton. | |
374 label = Label(entry_area, text=opt['name']) | |
375 label.grid(row=row, column=0, sticky=NW) | |
376 var = opt['var'] | |
377 if opt['type'] == 'bool': | |
378 Checkbutton(entry_area, variable=var, | |
379 onvalue='True', offvalue='False', width=8 | |
380 ).grid(row=row, column=1, sticky=W, padx=7) | |
381 elif opt['type'] == 'int': | |
382 Entry(entry_area, textvariable=var, validate='key', | |
383 validatecommand=(self.is_int, '%P'), width=10 | |
384 ).grid(row=row, column=1, sticky=NSEW, padx=7) | |
385 | |
386 else: # type == 'str' | |
387 # Limit size to fit non-expanding space with larger font. | |
388 Entry(entry_area, textvariable=var, width=15 | |
389 ).grid(row=row, column=1, sticky=NSEW, padx=7) | |
390 return | |
391 | |
392 def set_extension_value(self, section, opt): | |
393 """Return True if the configuration was added or changed. | |
394 | |
395 If the value is the same as the default, then remove it | |
396 from user config file. | |
397 """ | |
398 name = opt['name'] | |
399 default = opt['default'] | |
400 value = opt['var'].get().strip() or default | |
401 opt['var'].set(value) | |
402 # if self.defaultCfg.has_section(section): | |
403 # Currently, always true; if not, indent to return. | |
404 if (value == default): | |
405 return self.ext_userCfg.RemoveOption(section, name) | |
406 # Set the option. | |
407 return self.ext_userCfg.SetOption(section, name, value) | |
408 | |
409 def save_all_changed_extensions(self): | |
410 """Save configuration changes to the user config file. | |
411 | |
412 Attributes accessed: | |
413 extensions | |
414 | |
415 Methods: | |
416 set_extension_value | |
417 """ | |
418 has_changes = False | |
419 for ext_name in self.extensions: | |
420 options = self.extensions[ext_name] | |
421 for opt in options: | |
422 if self.set_extension_value(ext_name, opt): | |
423 has_changes = True | |
424 if has_changes: | |
425 self.ext_userCfg.Save() | |
426 | |
427 | |
428 # class TabPage(Frame): # A template for Page classes. | |
429 # def __init__(self, master): | |
430 # super().__init__(master) | |
431 # self.create_page_tab() | |
432 # self.load_tab_cfg() | |
433 # def create_page_tab(self): | |
434 # # Define tk vars and register var and callback with tracers. | |
435 # # Create subframes and widgets. | |
436 # # Pack widgets. | |
437 # def load_tab_cfg(self): | |
438 # # Initialize widgets with data from idleConf. | |
439 # def var_changed_var_name(): | |
440 # # For each tk var that needs other than default callback. | |
441 # def other_methods(): | |
442 # # Define tab-specific behavior. | |
443 | |
444 font_sample_text = ( | |
445 '<ASCII/Latin1>\n' | |
446 'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n' | |
447 '\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e' | |
448 '\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n' | |
449 '\n<IPA,Greek,Cyrillic>\n' | |
450 '\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277' | |
451 '\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n' | |
452 '\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5' | |
453 '\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n' | |
454 '\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444' | |
455 '\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n' | |
456 '\n<Hebrew, Arabic>\n' | |
457 '\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9' | |
458 '\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n' | |
459 '\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a' | |
460 '\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n' | |
461 '\n<Devanagari, Tamil>\n' | |
462 '\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f' | |
463 '\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n' | |
464 '\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef' | |
465 '\u0b85\u0b87\u0b89\u0b8e\n' | |
466 '\n<East Asian>\n' | |
467 '\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n' | |
468 '\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n' | |
469 '\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n' | |
470 '\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n' | |
471 ) | |
472 | |
473 | |
474 class FontPage(Frame): | |
475 | |
476 def __init__(self, master, highpage): | |
477 super().__init__(master) | |
478 self.highlight_sample = highpage.highlight_sample | |
479 self.create_page_font_tab() | |
480 self.load_font_cfg() | |
481 self.load_tab_cfg() | |
482 | |
483 def create_page_font_tab(self): | |
484 """Return frame of widgets for Font/Tabs tab. | |
485 | |
486 Fonts: Enable users to provisionally change font face, size, or | |
487 boldness and to see the consequence of proposed choices. Each | |
488 action set 3 options in changes structuree and changes the | |
489 corresponding aspect of the font sample on this page and | |
490 highlight sample on highlight page. | |
491 | |
492 Function load_font_cfg initializes font vars and widgets from | |
493 idleConf entries and tk. | |
494 | |
495 Fontlist: mouse button 1 click or up or down key invoke | |
496 on_fontlist_select(), which sets var font_name. | |
497 | |
498 Sizelist: clicking the menubutton opens the dropdown menu. A | |
499 mouse button 1 click or return key sets var font_size. | |
500 | |
501 Bold_toggle: clicking the box toggles var font_bold. | |
502 | |
503 Changing any of the font vars invokes var_changed_font, which | |
504 adds all 3 font options to changes and calls set_samples. | |
505 Set_samples applies a new font constructed from the font vars to | |
506 font_sample and to highlight_sample on the highlight page. | |
507 | |
508 Tabs: Enable users to change spaces entered for indent tabs. | |
509 Changing indent_scale value with the mouse sets Var space_num, | |
510 which invokes the default callback to add an entry to | |
511 changes. Load_tab_cfg initializes space_num to default. | |
512 | |
513 Widgets for FontPage(Frame): (*) widgets bound to self | |
514 frame_font: LabelFrame | |
515 frame_font_name: Frame | |
516 font_name_title: Label | |
517 (*)fontlist: ListBox - font_name | |
518 scroll_font: Scrollbar | |
519 frame_font_param: Frame | |
520 font_size_title: Label | |
521 (*)sizelist: DynOptionMenu - font_size | |
522 (*)bold_toggle: Checkbutton - font_bold | |
523 frame_sample: LabelFrame | |
524 (*)font_sample: Label | |
525 frame_indent: LabelFrame | |
526 indent_title: Label | |
527 (*)indent_scale: Scale - space_num | |
528 """ | |
529 self.font_name = tracers.add(StringVar(self), self.var_changed_font) | |
530 self.font_size = tracers.add(StringVar(self), self.var_changed_font) | |
531 self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font) | |
532 self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces')) | |
533 | |
534 # Define frames and widgets. | |
535 frame_font = LabelFrame( | |
536 self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ') | |
537 frame_sample = LabelFrame( | |
538 self, borderwidth=2, relief=GROOVE, | |
539 text=' Font Sample (Editable) ') | |
540 frame_indent = LabelFrame( | |
541 self, borderwidth=2, relief=GROOVE, text=' Indentation Width ') | |
542 # frame_font. | |
543 frame_font_name = Frame(frame_font) | |
544 frame_font_param = Frame(frame_font) | |
545 font_name_title = Label( | |
546 frame_font_name, justify=LEFT, text='Font Face :') | |
547 self.fontlist = Listbox(frame_font_name, height=15, | |
548 takefocus=True, exportselection=FALSE) | |
549 self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select) | |
550 self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select) | |
551 self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select) | |
552 scroll_font = Scrollbar(frame_font_name) | |
553 scroll_font.config(command=self.fontlist.yview) | |
554 self.fontlist.config(yscrollcommand=scroll_font.set) | |
555 font_size_title = Label(frame_font_param, text='Size :') | |
556 self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None) | |
557 self.bold_toggle = Checkbutton( | |
558 frame_font_param, variable=self.font_bold, | |
559 onvalue=1, offvalue=0, text='Bold') | |
560 # frame_sample. | |
561 font_sample_frame = ScrollableTextFrame(frame_sample) | |
562 self.font_sample = font_sample_frame.text | |
563 self.font_sample.config(wrap=NONE, width=1, height=1) | |
564 self.font_sample.insert(END, font_sample_text) | |
565 # frame_indent. | |
566 indent_title = Label( | |
567 frame_indent, justify=LEFT, | |
568 text='Python Standard: 4 Spaces!') | |
569 self.indent_scale = Scale( | |
570 frame_indent, variable=self.space_num, | |
571 orient='horizontal', tickinterval=2, from_=2, to=16) | |
572 | |
573 # Grid and pack widgets: | |
574 self.columnconfigure(1, weight=1) | |
575 self.rowconfigure(2, weight=1) | |
576 frame_font.grid(row=0, column=0, padx=5, pady=5) | |
577 frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5, | |
578 sticky='nsew') | |
579 frame_indent.grid(row=1, column=0, padx=5, pady=5, sticky='ew') | |
580 # frame_font. | |
581 frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X) | |
582 frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X) | |
583 font_name_title.pack(side=TOP, anchor=W) | |
584 self.fontlist.pack(side=LEFT, expand=TRUE, fill=X) | |
585 scroll_font.pack(side=LEFT, fill=Y) | |
586 font_size_title.pack(side=LEFT, anchor=W) | |
587 self.sizelist.pack(side=LEFT, anchor=W) | |
588 self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) | |
589 # frame_sample. | |
590 font_sample_frame.pack(expand=TRUE, fill=BOTH) | |
591 # frame_indent. | |
592 indent_title.pack(side=TOP, anchor=W, padx=5) | |
593 self.indent_scale.pack(side=TOP, padx=5, fill=X) | |
594 | |
595 def load_font_cfg(self): | |
596 """Load current configuration settings for the font options. | |
597 | |
598 Retrieve current font with idleConf.GetFont and font families | |
599 from tk. Setup fontlist and set font_name. Setup sizelist, | |
600 which sets font_size. Set font_bold. Call set_samples. | |
601 """ | |
602 configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') | |
603 font_name = configured_font[0].lower() | |
604 font_size = configured_font[1] | |
605 font_bold = configured_font[2]=='bold' | |
606 | |
607 # Set editor font selection list and font_name. | |
608 fonts = list(tkFont.families(self)) | |
609 fonts.sort() | |
610 for font in fonts: | |
611 self.fontlist.insert(END, font) | |
612 self.font_name.set(font_name) | |
613 lc_fonts = [s.lower() for s in fonts] | |
614 try: | |
615 current_font_index = lc_fonts.index(font_name) | |
616 self.fontlist.see(current_font_index) | |
617 self.fontlist.select_set(current_font_index) | |
618 self.fontlist.select_anchor(current_font_index) | |
619 self.fontlist.activate(current_font_index) | |
620 except ValueError: | |
621 pass | |
622 # Set font size dropdown. | |
623 self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14', | |
624 '16', '18', '20', '22', '25', '29', '34', '40'), | |
625 font_size) | |
626 # Set font weight. | |
627 self.font_bold.set(font_bold) | |
628 self.set_samples() | |
629 | |
630 def var_changed_font(self, *params): | |
631 """Store changes to font attributes. | |
632 | |
633 When one font attribute changes, save them all, as they are | |
634 not independent from each other. In particular, when we are | |
635 overriding the default font, we need to write out everything. | |
636 """ | |
637 value = self.font_name.get() | |
638 changes.add_option('main', 'EditorWindow', 'font', value) | |
639 value = self.font_size.get() | |
640 changes.add_option('main', 'EditorWindow', 'font-size', value) | |
641 value = self.font_bold.get() | |
642 changes.add_option('main', 'EditorWindow', 'font-bold', value) | |
643 self.set_samples() | |
644 | |
645 def on_fontlist_select(self, event): | |
646 """Handle selecting a font from the list. | |
647 | |
648 Event can result from either mouse click or Up or Down key. | |
649 Set font_name and example displays to selection. | |
650 """ | |
651 font = self.fontlist.get( | |
652 ACTIVE if event.type.name == 'KeyRelease' else ANCHOR) | |
653 self.font_name.set(font.lower()) | |
654 | |
655 def set_samples(self, event=None): | |
656 """Update update both screen samples with the font settings. | |
657 | |
658 Called on font initialization and change events. | |
659 Accesses font_name, font_size, and font_bold Variables. | |
660 Updates font_sample and highlight page highlight_sample. | |
661 """ | |
662 font_name = self.font_name.get() | |
663 font_weight = tkFont.BOLD if self.font_bold.get() else tkFont.NORMAL | |
664 new_font = (font_name, self.font_size.get(), font_weight) | |
665 self.font_sample['font'] = new_font | |
666 self.highlight_sample['font'] = new_font | |
667 | |
668 def load_tab_cfg(self): | |
669 """Load current configuration settings for the tab options. | |
670 | |
671 Attributes updated: | |
672 space_num: Set to value from idleConf. | |
673 """ | |
674 # Set indent sizes. | |
675 space_num = idleConf.GetOption( | |
676 'main', 'Indent', 'num-spaces', default=4, type='int') | |
677 self.space_num.set(space_num) | |
678 | |
679 def var_changed_space_num(self, *params): | |
680 "Store change to indentation size." | |
681 value = self.space_num.get() | |
682 changes.add_option('main', 'Indent', 'num-spaces', value) | |
683 | |
684 | |
685 class HighPage(Frame): | |
686 | |
687 def __init__(self, master): | |
688 super().__init__(master) | |
689 self.cd = master.master | |
690 self.style = Style(master) | |
691 self.create_page_highlight() | |
692 self.load_theme_cfg() | |
693 | |
694 def create_page_highlight(self): | |
695 """Return frame of widgets for Highlighting tab. | |
696 | |
697 Enable users to provisionally change foreground and background | |
698 colors applied to textual tags. Color mappings are stored in | |
699 complete listings called themes. Built-in themes in | |
700 idlelib/config-highlight.def are fixed as far as the dialog is | |
701 concerned. Any theme can be used as the base for a new custom | |
702 theme, stored in .idlerc/config-highlight.cfg. | |
703 | |
704 Function load_theme_cfg() initializes tk variables and theme | |
705 lists and calls paint_theme_sample() and set_highlight_target() | |
706 for the current theme. Radiobuttons builtin_theme_on and | |
707 custom_theme_on toggle var theme_source, which controls if the | |
708 current set of colors are from a builtin or custom theme. | |
709 DynOptionMenus builtinlist and customlist contain lists of the | |
710 builtin and custom themes, respectively, and the current item | |
711 from each list is stored in vars builtin_name and custom_name. | |
712 | |
713 Function paint_theme_sample() applies the colors from the theme | |
714 to the tags in text widget highlight_sample and then invokes | |
715 set_color_sample(). Function set_highlight_target() sets the state | |
716 of the radiobuttons fg_on and bg_on based on the tag and it also | |
717 invokes set_color_sample(). | |
718 | |
719 Function set_color_sample() sets the background color for the frame | |
720 holding the color selector. This provides a larger visual of the | |
721 color for the current tag and plane (foreground/background). | |
722 | |
723 Note: set_color_sample() is called from many places and is often | |
724 called more than once when a change is made. It is invoked when | |
725 foreground or background is selected (radiobuttons), from | |
726 paint_theme_sample() (theme is changed or load_cfg is called), and | |
727 from set_highlight_target() (target tag is changed or load_cfg called). | |
728 | |
729 Button delete_custom invokes delete_custom() to delete | |
730 a custom theme from idleConf.userCfg['highlight'] and changes. | |
731 Button save_custom invokes save_as_new_theme() which calls | |
732 get_new_theme_name() and create_new() to save a custom theme | |
733 and its colors to idleConf.userCfg['highlight']. | |
734 | |
735 Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control | |
736 if the current selected color for a tag is for the foreground or | |
737 background. | |
738 | |
739 DynOptionMenu targetlist contains a readable description of the | |
740 tags applied to Python source within IDLE. Selecting one of the | |
741 tags from this list populates highlight_target, which has a callback | |
742 function set_highlight_target(). | |
743 | |
744 Text widget highlight_sample displays a block of text (which is | |
745 mock Python code) in which is embedded the defined tags and reflects | |
746 the color attributes of the current theme and changes for those tags. | |
747 Mouse button 1 allows for selection of a tag and updates | |
748 highlight_target with that tag value. | |
749 | |
750 Note: The font in highlight_sample is set through the config in | |
751 the fonts tab. | |
752 | |
753 In other words, a tag can be selected either from targetlist or | |
754 by clicking on the sample text within highlight_sample. The | |
755 plane (foreground/background) is selected via the radiobutton. | |
756 Together, these two (tag and plane) control what color is | |
757 shown in set_color_sample() for the current theme. Button set_color | |
758 invokes get_color() which displays a ColorChooser to change the | |
759 color for the selected tag/plane. If a new color is picked, | |
760 it will be saved to changes and the highlight_sample and | |
761 frame background will be updated. | |
762 | |
763 Tk Variables: | |
764 color: Color of selected target. | |
765 builtin_name: Menu variable for built-in theme. | |
766 custom_name: Menu variable for custom theme. | |
767 fg_bg_toggle: Toggle for foreground/background color. | |
768 Note: this has no callback. | |
769 theme_source: Selector for built-in or custom theme. | |
770 highlight_target: Menu variable for the highlight tag target. | |
771 | |
772 Instance Data Attributes: | |
773 theme_elements: Dictionary of tags for text highlighting. | |
774 The key is the display name and the value is a tuple of | |
775 (tag name, display sort order). | |
776 | |
777 Methods [attachment]: | |
778 load_theme_cfg: Load current highlight colors. | |
779 get_color: Invoke colorchooser [button_set_color]. | |
780 set_color_sample_binding: Call set_color_sample [fg_bg_toggle]. | |
781 set_highlight_target: set fg_bg_toggle, set_color_sample(). | |
782 set_color_sample: Set frame background to target. | |
783 on_new_color_set: Set new color and add option. | |
784 paint_theme_sample: Recolor sample. | |
785 get_new_theme_name: Get from popup. | |
786 create_new: Combine theme with changes and save. | |
787 save_as_new_theme: Save [button_save_custom]. | |
788 set_theme_type: Command for [theme_source]. | |
789 delete_custom: Activate default [button_delete_custom]. | |
790 save_new: Save to userCfg['theme'] (is function). | |
791 | |
792 Widgets of highlights page frame: (*) widgets bound to self | |
793 frame_custom: LabelFrame | |
794 (*)highlight_sample: Text | |
795 (*)frame_color_set: Frame | |
796 (*)button_set_color: Button | |
797 (*)targetlist: DynOptionMenu - highlight_target | |
798 frame_fg_bg_toggle: Frame | |
799 (*)fg_on: Radiobutton - fg_bg_toggle | |
800 (*)bg_on: Radiobutton - fg_bg_toggle | |
801 (*)button_save_custom: Button | |
802 frame_theme: LabelFrame | |
803 theme_type_title: Label | |
804 (*)builtin_theme_on: Radiobutton - theme_source | |
805 (*)custom_theme_on: Radiobutton - theme_source | |
806 (*)builtinlist: DynOptionMenu - builtin_name | |
807 (*)customlist: DynOptionMenu - custom_name | |
808 (*)button_delete_custom: Button | |
809 (*)theme_message: Label | |
810 """ | |
811 self.theme_elements = { | |
812 'Normal Code or Text': ('normal', '00'), | |
813 'Code Context': ('context', '01'), | |
814 'Python Keywords': ('keyword', '02'), | |
815 'Python Definitions': ('definition', '03'), | |
816 'Python Builtins': ('builtin', '04'), | |
817 'Python Comments': ('comment', '05'), | |
818 'Python Strings': ('string', '06'), | |
819 'Selected Text': ('hilite', '07'), | |
820 'Found Text': ('hit', '08'), | |
821 'Cursor': ('cursor', '09'), | |
822 'Editor Breakpoint': ('break', '10'), | |
823 'Shell Prompt': ('console', '11'), | |
824 'Error Text': ('error', '12'), | |
825 'Shell User Output': ('stdout', '13'), | |
826 'Shell User Exception': ('stderr', '14'), | |
827 'Line Number': ('linenumber', '16'), | |
828 } | |
829 self.builtin_name = tracers.add( | |
830 StringVar(self), self.var_changed_builtin_name) | |
831 self.custom_name = tracers.add( | |
832 StringVar(self), self.var_changed_custom_name) | |
833 self.fg_bg_toggle = BooleanVar(self) | |
834 self.color = tracers.add( | |
835 StringVar(self), self.var_changed_color) | |
836 self.theme_source = tracers.add( | |
837 BooleanVar(self), self.var_changed_theme_source) | |
838 self.highlight_target = tracers.add( | |
839 StringVar(self), self.var_changed_highlight_target) | |
840 | |
841 # Create widgets: | |
842 # body frame and section frames. | |
843 frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE, | |
844 text=' Custom Highlighting ') | |
845 frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE, | |
846 text=' Highlighting Theme ') | |
847 # frame_custom. | |
848 sample_frame = ScrollableTextFrame( | |
849 frame_custom, relief=SOLID, borderwidth=1) | |
850 text = self.highlight_sample = sample_frame.text | |
851 text.configure( | |
852 font=('courier', 12, ''), cursor='hand2', width=1, height=1, | |
853 takefocus=FALSE, highlightthickness=0, wrap=NONE) | |
854 text.bind('<Double-Button-1>', lambda e: 'break') | |
855 text.bind('<B1-Motion>', lambda e: 'break') | |
856 string_tags=( | |
857 ('# Click selects item.', 'comment'), ('\n', 'normal'), | |
858 ('code context section', 'context'), ('\n', 'normal'), | |
859 ('| cursor', 'cursor'), ('\n', 'normal'), | |
860 ('def', 'keyword'), (' ', 'normal'), | |
861 ('func', 'definition'), ('(param):\n ', 'normal'), | |
862 ('"Return None."', 'string'), ('\n var0 = ', 'normal'), | |
863 ("'string'", 'string'), ('\n var1 = ', 'normal'), | |
864 ("'selected'", 'hilite'), ('\n var2 = ', 'normal'), | |
865 ("'found'", 'hit'), ('\n var3 = ', 'normal'), | |
866 ('list', 'builtin'), ('(', 'normal'), | |
867 ('None', 'keyword'), (')\n', 'normal'), | |
868 (' breakpoint("line")', 'break'), ('\n\n', 'normal'), | |
869 ('>>>', 'console'), (' 3.14**2\n', 'normal'), | |
870 ('9.8596', 'stdout'), ('\n', 'normal'), | |
871 ('>>>', 'console'), (' pri ', 'normal'), | |
872 ('n', 'error'), ('t(\n', 'normal'), | |
873 ('SyntaxError', 'stderr'), ('\n', 'normal')) | |
874 for string, tag in string_tags: | |
875 text.insert(END, string, tag) | |
876 n_lines = len(text.get('1.0', END).splitlines()) | |
877 for lineno in range(1, n_lines): | |
878 text.insert(f'{lineno}.0', | |
879 f'{lineno:{len(str(n_lines))}d} ', | |
880 'linenumber') | |
881 for element in self.theme_elements: | |
882 def tem(event, elem=element): | |
883 # event.widget.winfo_top_level().highlight_target.set(elem) | |
884 self.highlight_target.set(elem) | |
885 text.tag_bind( | |
886 self.theme_elements[element][0], '<ButtonPress-1>', tem) | |
887 text['state'] = 'disabled' | |
888 self.style.configure('frame_color_set.TFrame', borderwidth=1, | |
889 relief='solid') | |
890 self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame') | |
891 frame_fg_bg_toggle = Frame(frame_custom) | |
892 self.button_set_color = Button( | |
893 self.frame_color_set, text='Choose Color for :', | |
894 command=self.get_color) | |
895 self.targetlist = DynOptionMenu( | |
896 self.frame_color_set, self.highlight_target, None, | |
897 highlightthickness=0) #, command=self.set_highlight_targetBinding | |
898 self.fg_on = Radiobutton( | |
899 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1, | |
900 text='Foreground', command=self.set_color_sample_binding) | |
901 self.bg_on = Radiobutton( | |
902 frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0, | |
903 text='Background', command=self.set_color_sample_binding) | |
904 self.fg_bg_toggle.set(1) | |
905 self.button_save_custom = Button( | |
906 frame_custom, text='Save as New Custom Theme', | |
907 command=self.save_as_new_theme) | |
908 # frame_theme. | |
909 theme_type_title = Label(frame_theme, text='Select : ') | |
910 self.builtin_theme_on = Radiobutton( | |
911 frame_theme, variable=self.theme_source, value=1, | |
912 command=self.set_theme_type, text='a Built-in Theme') | |
913 self.custom_theme_on = Radiobutton( | |
914 frame_theme, variable=self.theme_source, value=0, | |
915 command=self.set_theme_type, text='a Custom Theme') | |
916 self.builtinlist = DynOptionMenu( | |
917 frame_theme, self.builtin_name, None, command=None) | |
918 self.customlist = DynOptionMenu( | |
919 frame_theme, self.custom_name, None, command=None) | |
920 self.button_delete_custom = Button( | |
921 frame_theme, text='Delete Custom Theme', | |
922 command=self.delete_custom) | |
923 self.theme_message = Label(frame_theme, borderwidth=2) | |
924 # Pack widgets: | |
925 # body. | |
926 frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) | |
927 frame_theme.pack(side=TOP, padx=5, pady=5, fill=X) | |
928 # frame_custom. | |
929 self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X) | |
930 frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0) | |
931 sample_frame.pack( | |
932 side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) | |
933 self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) | |
934 self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3) | |
935 self.fg_on.pack(side=LEFT, anchor=E) | |
936 self.bg_on.pack(side=RIGHT, anchor=W) | |
937 self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5) | |
938 # frame_theme. | |
939 theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5) | |
940 self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5) | |
941 self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2) | |
942 self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5) | |
943 self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) | |
944 self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5) | |
945 self.theme_message.pack(side=TOP, fill=X, pady=5) | |
946 | |
947 def load_theme_cfg(self): | |
948 """Load current configuration settings for the theme options. | |
949 | |
950 Based on the theme_source toggle, the theme is set as | |
951 either builtin or custom and the initial widget values | |
952 reflect the current settings from idleConf. | |
953 | |
954 Attributes updated: | |
955 theme_source: Set from idleConf. | |
956 builtinlist: List of default themes from idleConf. | |
957 customlist: List of custom themes from idleConf. | |
958 custom_theme_on: Disabled if there are no custom themes. | |
959 custom_theme: Message with additional information. | |
960 targetlist: Create menu from self.theme_elements. | |
961 | |
962 Methods: | |
963 set_theme_type | |
964 paint_theme_sample | |
965 set_highlight_target | |
966 """ | |
967 # Set current theme type radiobutton. | |
968 self.theme_source.set(idleConf.GetOption( | |
969 'main', 'Theme', 'default', type='bool', default=1)) | |
970 # Set current theme. | |
971 current_option = idleConf.CurrentTheme() | |
972 # Load available theme option menus. | |
973 if self.theme_source.get(): # Default theme selected. | |
974 item_list = idleConf.GetSectionList('default', 'highlight') | |
975 item_list.sort() | |
976 self.builtinlist.SetMenu(item_list, current_option) | |
977 item_list = idleConf.GetSectionList('user', 'highlight') | |
978 item_list.sort() | |
979 if not item_list: | |
980 self.custom_theme_on.state(('disabled',)) | |
981 self.custom_name.set('- no custom themes -') | |
982 else: | |
983 self.customlist.SetMenu(item_list, item_list[0]) | |
984 else: # User theme selected. | |
985 item_list = idleConf.GetSectionList('user', 'highlight') | |
986 item_list.sort() | |
987 self.customlist.SetMenu(item_list, current_option) | |
988 item_list = idleConf.GetSectionList('default', 'highlight') | |
989 item_list.sort() | |
990 self.builtinlist.SetMenu(item_list, item_list[0]) | |
991 self.set_theme_type() | |
992 # Load theme element option menu. | |
993 theme_names = list(self.theme_elements.keys()) | |
994 theme_names.sort(key=lambda x: self.theme_elements[x][1]) | |
995 self.targetlist.SetMenu(theme_names, theme_names[0]) | |
996 self.paint_theme_sample() | |
997 self.set_highlight_target() | |
998 | |
999 def var_changed_builtin_name(self, *params): | |
1000 """Process new builtin theme selection. | |
1001 | |
1002 Add the changed theme's name to the changed_items and recreate | |
1003 the sample with the values from the selected theme. | |
1004 """ | |
1005 old_themes = ('IDLE Classic', 'IDLE New') | |
1006 value = self.builtin_name.get() | |
1007 if value not in old_themes: | |
1008 if idleConf.GetOption('main', 'Theme', 'name') not in old_themes: | |
1009 changes.add_option('main', 'Theme', 'name', old_themes[0]) | |
1010 changes.add_option('main', 'Theme', 'name2', value) | |
1011 self.theme_message['text'] = 'New theme, see Help' | |
1012 else: | |
1013 changes.add_option('main', 'Theme', 'name', value) | |
1014 changes.add_option('main', 'Theme', 'name2', '') | |
1015 self.theme_message['text'] = '' | |
1016 self.paint_theme_sample() | |
1017 | |
1018 def var_changed_custom_name(self, *params): | |
1019 """Process new custom theme selection. | |
1020 | |
1021 If a new custom theme is selected, add the name to the | |
1022 changed_items and apply the theme to the sample. | |
1023 """ | |
1024 value = self.custom_name.get() | |
1025 if value != '- no custom themes -': | |
1026 changes.add_option('main', 'Theme', 'name', value) | |
1027 self.paint_theme_sample() | |
1028 | |
1029 def var_changed_theme_source(self, *params): | |
1030 """Process toggle between builtin and custom theme. | |
1031 | |
1032 Update the default toggle value and apply the newly | |
1033 selected theme type. | |
1034 """ | |
1035 value = self.theme_source.get() | |
1036 changes.add_option('main', 'Theme', 'default', value) | |
1037 if value: | |
1038 self.var_changed_builtin_name() | |
1039 else: | |
1040 self.var_changed_custom_name() | |
1041 | |
1042 def var_changed_color(self, *params): | |
1043 "Process change to color choice." | |
1044 self.on_new_color_set() | |
1045 | |
1046 def var_changed_highlight_target(self, *params): | |
1047 "Process selection of new target tag for highlighting." | |
1048 self.set_highlight_target() | |
1049 | |
1050 def set_theme_type(self): | |
1051 """Set available screen options based on builtin or custom theme. | |
1052 | |
1053 Attributes accessed: | |
1054 theme_source | |
1055 | |
1056 Attributes updated: | |
1057 builtinlist | |
1058 customlist | |
1059 button_delete_custom | |
1060 custom_theme_on | |
1061 | |
1062 Called from: | |
1063 handler for builtin_theme_on and custom_theme_on | |
1064 delete_custom | |
1065 create_new | |
1066 load_theme_cfg | |
1067 """ | |
1068 if self.theme_source.get(): | |
1069 self.builtinlist['state'] = 'normal' | |
1070 self.customlist['state'] = 'disabled' | |
1071 self.button_delete_custom.state(('disabled',)) | |
1072 else: | |
1073 self.builtinlist['state'] = 'disabled' | |
1074 self.custom_theme_on.state(('!disabled',)) | |
1075 self.customlist['state'] = 'normal' | |
1076 self.button_delete_custom.state(('!disabled',)) | |
1077 | |
1078 def get_color(self): | |
1079 """Handle button to select a new color for the target tag. | |
1080 | |
1081 If a new color is selected while using a builtin theme, a | |
1082 name must be supplied to create a custom theme. | |
1083 | |
1084 Attributes accessed: | |
1085 highlight_target | |
1086 frame_color_set | |
1087 theme_source | |
1088 | |
1089 Attributes updated: | |
1090 color | |
1091 | |
1092 Methods: | |
1093 get_new_theme_name | |
1094 create_new | |
1095 """ | |
1096 target = self.highlight_target.get() | |
1097 prev_color = self.style.lookup(self.frame_color_set['style'], | |
1098 'background') | |
1099 rgbTuplet, color_string = tkColorChooser.askcolor( | |
1100 parent=self, title='Pick new color for : '+target, | |
1101 initialcolor=prev_color) | |
1102 if color_string and (color_string != prev_color): | |
1103 # User didn't cancel and they chose a new color. | |
1104 if self.theme_source.get(): # Current theme is a built-in. | |
1105 message = ('Your changes will be saved as a new Custom Theme. ' | |
1106 'Enter a name for your new Custom Theme below.') | |
1107 new_theme = self.get_new_theme_name(message) | |
1108 if not new_theme: # User cancelled custom theme creation. | |
1109 return | |
1110 else: # Create new custom theme based on previously active theme. | |
1111 self.create_new(new_theme) | |
1112 self.color.set(color_string) | |
1113 else: # Current theme is user defined. | |
1114 self.color.set(color_string) | |
1115 | |
1116 def on_new_color_set(self): | |
1117 "Display sample of new color selection on the dialog." | |
1118 new_color = self.color.get() | |
1119 self.style.configure('frame_color_set.TFrame', background=new_color) | |
1120 plane = 'foreground' if self.fg_bg_toggle.get() else 'background' | |
1121 sample_element = self.theme_elements[self.highlight_target.get()][0] | |
1122 self.highlight_sample.tag_config(sample_element, **{plane: new_color}) | |
1123 theme = self.custom_name.get() | |
1124 theme_element = sample_element + '-' + plane | |
1125 changes.add_option('highlight', theme, theme_element, new_color) | |
1126 | |
1127 def get_new_theme_name(self, message): | |
1128 "Return name of new theme from query popup." | |
1129 used_names = (idleConf.GetSectionList('user', 'highlight') + | |
1130 idleConf.GetSectionList('default', 'highlight')) | |
1131 new_theme = SectionName( | |
1132 self, 'New Custom Theme', message, used_names).result | |
1133 return new_theme | |
1134 | |
1135 def save_as_new_theme(self): | |
1136 """Prompt for new theme name and create the theme. | |
1137 | |
1138 Methods: | |
1139 get_new_theme_name | |
1140 create_new | |
1141 """ | |
1142 new_theme_name = self.get_new_theme_name('New Theme Name:') | |
1143 if new_theme_name: | |
1144 self.create_new(new_theme_name) | |
1145 | |
1146 def create_new(self, new_theme_name): | |
1147 """Create a new custom theme with the given name. | |
1148 | |
1149 Create the new theme based on the previously active theme | |
1150 with the current changes applied. Once it is saved, then | |
1151 activate the new theme. | |
1152 | |
1153 Attributes accessed: | |
1154 builtin_name | |
1155 custom_name | |
1156 | |
1157 Attributes updated: | |
1158 customlist | |
1159 theme_source | |
1160 | |
1161 Method: | |
1162 save_new | |
1163 set_theme_type | |
1164 """ | |
1165 if self.theme_source.get(): | |
1166 theme_type = 'default' | |
1167 theme_name = self.builtin_name.get() | |
1168 else: | |
1169 theme_type = 'user' | |
1170 theme_name = self.custom_name.get() | |
1171 new_theme = idleConf.GetThemeDict(theme_type, theme_name) | |
1172 # Apply any of the old theme's unsaved changes to the new theme. | |
1173 if theme_name in changes['highlight']: | |
1174 theme_changes = changes['highlight'][theme_name] | |
1175 for element in theme_changes: | |
1176 new_theme[element] = theme_changes[element] | |
1177 # Save the new theme. | |
1178 self.save_new(new_theme_name, new_theme) | |
1179 # Change GUI over to the new theme. | |
1180 custom_theme_list = idleConf.GetSectionList('user', 'highlight') | |
1181 custom_theme_list.sort() | |
1182 self.customlist.SetMenu(custom_theme_list, new_theme_name) | |
1183 self.theme_source.set(0) | |
1184 self.set_theme_type() | |
1185 | |
1186 def set_highlight_target(self): | |
1187 """Set fg/bg toggle and color based on highlight tag target. | |
1188 | |
1189 Instance variables accessed: | |
1190 highlight_target | |
1191 | |
1192 Attributes updated: | |
1193 fg_on | |
1194 bg_on | |
1195 fg_bg_toggle | |
1196 | |
1197 Methods: | |
1198 set_color_sample | |
1199 | |
1200 Called from: | |
1201 var_changed_highlight_target | |
1202 load_theme_cfg | |
1203 """ | |
1204 if self.highlight_target.get() == 'Cursor': # bg not possible | |
1205 self.fg_on.state(('disabled',)) | |
1206 self.bg_on.state(('disabled',)) | |
1207 self.fg_bg_toggle.set(1) | |
1208 else: # Both fg and bg can be set. | |
1209 self.fg_on.state(('!disabled',)) | |
1210 self.bg_on.state(('!disabled',)) | |
1211 self.fg_bg_toggle.set(1) | |
1212 self.set_color_sample() | |
1213 | |
1214 def set_color_sample_binding(self, *args): | |
1215 """Change color sample based on foreground/background toggle. | |
1216 | |
1217 Methods: | |
1218 set_color_sample | |
1219 """ | |
1220 self.set_color_sample() | |
1221 | |
1222 def set_color_sample(self): | |
1223 """Set the color of the frame background to reflect the selected target. | |
1224 | |
1225 Instance variables accessed: | |
1226 theme_elements | |
1227 highlight_target | |
1228 fg_bg_toggle | |
1229 highlight_sample | |
1230 | |
1231 Attributes updated: | |
1232 frame_color_set | |
1233 """ | |
1234 # Set the color sample area. | |
1235 tag = self.theme_elements[self.highlight_target.get()][0] | |
1236 plane = 'foreground' if self.fg_bg_toggle.get() else 'background' | |
1237 color = self.highlight_sample.tag_cget(tag, plane) | |
1238 self.style.configure('frame_color_set.TFrame', background=color) | |
1239 | |
1240 def paint_theme_sample(self): | |
1241 """Apply the theme colors to each element tag in the sample text. | |
1242 | |
1243 Instance attributes accessed: | |
1244 theme_elements | |
1245 theme_source | |
1246 builtin_name | |
1247 custom_name | |
1248 | |
1249 Attributes updated: | |
1250 highlight_sample: Set the tag elements to the theme. | |
1251 | |
1252 Methods: | |
1253 set_color_sample | |
1254 | |
1255 Called from: | |
1256 var_changed_builtin_name | |
1257 var_changed_custom_name | |
1258 load_theme_cfg | |
1259 """ | |
1260 if self.theme_source.get(): # Default theme | |
1261 theme = self.builtin_name.get() | |
1262 else: # User theme | |
1263 theme = self.custom_name.get() | |
1264 for element_title in self.theme_elements: | |
1265 element = self.theme_elements[element_title][0] | |
1266 colors = idleConf.GetHighlight(theme, element) | |
1267 if element == 'cursor': # Cursor sample needs special painting. | |
1268 colors['background'] = idleConf.GetHighlight( | |
1269 theme, 'normal')['background'] | |
1270 # Handle any unsaved changes to this theme. | |
1271 if theme in changes['highlight']: | |
1272 theme_dict = changes['highlight'][theme] | |
1273 if element + '-foreground' in theme_dict: | |
1274 colors['foreground'] = theme_dict[element + '-foreground'] | |
1275 if element + '-background' in theme_dict: | |
1276 colors['background'] = theme_dict[element + '-background'] | |
1277 self.highlight_sample.tag_config(element, **colors) | |
1278 self.set_color_sample() | |
1279 | |
1280 def save_new(self, theme_name, theme): | |
1281 """Save a newly created theme to idleConf. | |
1282 | |
1283 theme_name - string, the name of the new theme | |
1284 theme - dictionary containing the new theme | |
1285 """ | |
1286 if not idleConf.userCfg['highlight'].has_section(theme_name): | |
1287 idleConf.userCfg['highlight'].add_section(theme_name) | |
1288 for element in theme: | |
1289 value = theme[element] | |
1290 idleConf.userCfg['highlight'].SetOption(theme_name, element, value) | |
1291 | |
1292 def askyesno(self, *args, **kwargs): | |
1293 # Make testing easier. Could change implementation. | |
1294 return messagebox.askyesno(*args, **kwargs) | |
1295 | |
1296 def delete_custom(self): | |
1297 """Handle event to delete custom theme. | |
1298 | |
1299 The current theme is deactivated and the default theme is | |
1300 activated. The custom theme is permanently removed from | |
1301 the config file. | |
1302 | |
1303 Attributes accessed: | |
1304 custom_name | |
1305 | |
1306 Attributes updated: | |
1307 custom_theme_on | |
1308 customlist | |
1309 theme_source | |
1310 builtin_name | |
1311 | |
1312 Methods: | |
1313 deactivate_current_config | |
1314 save_all_changed_extensions | |
1315 activate_config_changes | |
1316 set_theme_type | |
1317 """ | |
1318 theme_name = self.custom_name.get() | |
1319 delmsg = 'Are you sure you wish to delete the theme %r ?' | |
1320 if not self.askyesno( | |
1321 'Delete Theme', delmsg % theme_name, parent=self): | |
1322 return | |
1323 self.cd.deactivate_current_config() | |
1324 # Remove theme from changes, config, and file. | |
1325 changes.delete_section('highlight', theme_name) | |
1326 # Reload user theme list. | |
1327 item_list = idleConf.GetSectionList('user', 'highlight') | |
1328 item_list.sort() | |
1329 if not item_list: | |
1330 self.custom_theme_on.state(('disabled',)) | |
1331 self.customlist.SetMenu(item_list, '- no custom themes -') | |
1332 else: | |
1333 self.customlist.SetMenu(item_list, item_list[0]) | |
1334 # Revert to default theme. | |
1335 self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) | |
1336 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) | |
1337 # User can't back out of these changes, they must be applied now. | |
1338 changes.save_all() | |
1339 self.cd.save_all_changed_extensions() | |
1340 self.cd.activate_config_changes() | |
1341 self.set_theme_type() | |
1342 | |
1343 | |
1344 class KeysPage(Frame): | |
1345 | |
1346 def __init__(self, master): | |
1347 super().__init__(master) | |
1348 self.cd = master.master | |
1349 self.create_page_keys() | |
1350 self.load_key_cfg() | |
1351 | |
1352 def create_page_keys(self): | |
1353 """Return frame of widgets for Keys tab. | |
1354 | |
1355 Enable users to provisionally change both individual and sets of | |
1356 keybindings (shortcut keys). Except for features implemented as | |
1357 extensions, keybindings are stored in complete sets called | |
1358 keysets. Built-in keysets in idlelib/config-keys.def are fixed | |
1359 as far as the dialog is concerned. Any keyset can be used as the | |
1360 base for a new custom keyset, stored in .idlerc/config-keys.cfg. | |
1361 | |
1362 Function load_key_cfg() initializes tk variables and keyset | |
1363 lists and calls load_keys_list for the current keyset. | |
1364 Radiobuttons builtin_keyset_on and custom_keyset_on toggle var | |
1365 keyset_source, which controls if the current set of keybindings | |
1366 are from a builtin or custom keyset. DynOptionMenus builtinlist | |
1367 and customlist contain lists of the builtin and custom keysets, | |
1368 respectively, and the current item from each list is stored in | |
1369 vars builtin_name and custom_name. | |
1370 | |
1371 Button delete_custom_keys invokes delete_custom_keys() to delete | |
1372 a custom keyset from idleConf.userCfg['keys'] and changes. Button | |
1373 save_custom_keys invokes save_as_new_key_set() which calls | |
1374 get_new_keys_name() and create_new_key_set() to save a custom keyset | |
1375 and its keybindings to idleConf.userCfg['keys']. | |
1376 | |
1377 Listbox bindingslist contains all of the keybindings for the | |
1378 selected keyset. The keybindings are loaded in load_keys_list() | |
1379 and are pairs of (event, [keys]) where keys can be a list | |
1380 of one or more key combinations to bind to the same event. | |
1381 Mouse button 1 click invokes on_bindingslist_select(), which | |
1382 allows button_new_keys to be clicked. | |
1383 | |
1384 So, an item is selected in listbindings, which activates | |
1385 button_new_keys, and clicking button_new_keys calls function | |
1386 get_new_keys(). Function get_new_keys() gets the key mappings from the | |
1387 current keyset for the binding event item that was selected. The | |
1388 function then displays another dialog, GetKeysDialog, with the | |
1389 selected binding event and current keys and allows new key sequences | |
1390 to be entered for that binding event. If the keys aren't | |
1391 changed, nothing happens. If the keys are changed and the keyset | |
1392 is a builtin, function get_new_keys_name() will be called | |
1393 for input of a custom keyset name. If no name is given, then the | |
1394 change to the keybinding will abort and no updates will be made. If | |
1395 a custom name is entered in the prompt or if the current keyset was | |
1396 already custom (and thus didn't require a prompt), then | |
1397 idleConf.userCfg['keys'] is updated in function create_new_key_set() | |
1398 with the change to the event binding. The item listing in bindingslist | |
1399 is updated with the new keys. Var keybinding is also set which invokes | |
1400 the callback function, var_changed_keybinding, to add the change to | |
1401 the 'keys' or 'extensions' changes tracker based on the binding type. | |
1402 | |
1403 Tk Variables: | |
1404 keybinding: Action/key bindings. | |
1405 | |
1406 Methods: | |
1407 load_keys_list: Reload active set. | |
1408 create_new_key_set: Combine active keyset and changes. | |
1409 set_keys_type: Command for keyset_source. | |
1410 save_new_key_set: Save to idleConf.userCfg['keys'] (is function). | |
1411 deactivate_current_config: Remove keys bindings in editors. | |
1412 | |
1413 Widgets for KeysPage(frame): (*) widgets bound to self | |
1414 frame_key_sets: LabelFrame | |
1415 frames[0]: Frame | |
1416 (*)builtin_keyset_on: Radiobutton - var keyset_source | |
1417 (*)custom_keyset_on: Radiobutton - var keyset_source | |
1418 (*)builtinlist: DynOptionMenu - var builtin_name, | |
1419 func keybinding_selected | |
1420 (*)customlist: DynOptionMenu - var custom_name, | |
1421 func keybinding_selected | |
1422 (*)keys_message: Label | |
1423 frames[1]: Frame | |
1424 (*)button_delete_custom_keys: Button - delete_custom_keys | |
1425 (*)button_save_custom_keys: Button - save_as_new_key_set | |
1426 frame_custom: LabelFrame | |
1427 frame_target: Frame | |
1428 target_title: Label | |
1429 scroll_target_y: Scrollbar | |
1430 scroll_target_x: Scrollbar | |
1431 (*)bindingslist: ListBox - on_bindingslist_select | |
1432 (*)button_new_keys: Button - get_new_keys & ..._name | |
1433 """ | |
1434 self.builtin_name = tracers.add( | |
1435 StringVar(self), self.var_changed_builtin_name) | |
1436 self.custom_name = tracers.add( | |
1437 StringVar(self), self.var_changed_custom_name) | |
1438 self.keyset_source = tracers.add( | |
1439 BooleanVar(self), self.var_changed_keyset_source) | |
1440 self.keybinding = tracers.add( | |
1441 StringVar(self), self.var_changed_keybinding) | |
1442 | |
1443 # Create widgets: | |
1444 # body and section frames. | |
1445 frame_custom = LabelFrame( | |
1446 self, borderwidth=2, relief=GROOVE, | |
1447 text=' Custom Key Bindings ') | |
1448 frame_key_sets = LabelFrame( | |
1449 self, borderwidth=2, relief=GROOVE, text=' Key Set ') | |
1450 # frame_custom. | |
1451 frame_target = Frame(frame_custom) | |
1452 target_title = Label(frame_target, text='Action - Key(s)') | |
1453 scroll_target_y = Scrollbar(frame_target) | |
1454 scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) | |
1455 self.bindingslist = Listbox( | |
1456 frame_target, takefocus=FALSE, exportselection=FALSE) | |
1457 self.bindingslist.bind('<ButtonRelease-1>', | |
1458 self.on_bindingslist_select) | |
1459 scroll_target_y['command'] = self.bindingslist.yview | |
1460 scroll_target_x['command'] = self.bindingslist.xview | |
1461 self.bindingslist['yscrollcommand'] = scroll_target_y.set | |
1462 self.bindingslist['xscrollcommand'] = scroll_target_x.set | |
1463 self.button_new_keys = Button( | |
1464 frame_custom, text='Get New Keys for Selection', | |
1465 command=self.get_new_keys, state='disabled') | |
1466 # frame_key_sets. | |
1467 frames = [Frame(frame_key_sets, padding=2, borderwidth=0) | |
1468 for i in range(2)] | |
1469 self.builtin_keyset_on = Radiobutton( | |
1470 frames[0], variable=self.keyset_source, value=1, | |
1471 command=self.set_keys_type, text='Use a Built-in Key Set') | |
1472 self.custom_keyset_on = Radiobutton( | |
1473 frames[0], variable=self.keyset_source, value=0, | |
1474 command=self.set_keys_type, text='Use a Custom Key Set') | |
1475 self.builtinlist = DynOptionMenu( | |
1476 frames[0], self.builtin_name, None, command=None) | |
1477 self.customlist = DynOptionMenu( | |
1478 frames[0], self.custom_name, None, command=None) | |
1479 self.button_delete_custom_keys = Button( | |
1480 frames[1], text='Delete Custom Key Set', | |
1481 command=self.delete_custom_keys) | |
1482 self.button_save_custom_keys = Button( | |
1483 frames[1], text='Save as New Custom Key Set', | |
1484 command=self.save_as_new_key_set) | |
1485 self.keys_message = Label(frames[0], borderwidth=2) | |
1486 | |
1487 # Pack widgets: | |
1488 # body. | |
1489 frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) | |
1490 frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) | |
1491 # frame_custom. | |
1492 self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5) | |
1493 frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) | |
1494 # frame_target. | |
1495 frame_target.columnconfigure(0, weight=1) | |
1496 frame_target.rowconfigure(1, weight=1) | |
1497 target_title.grid(row=0, column=0, columnspan=2, sticky=W) | |
1498 self.bindingslist.grid(row=1, column=0, sticky=NSEW) | |
1499 scroll_target_y.grid(row=1, column=1, sticky=NS) | |
1500 scroll_target_x.grid(row=2, column=0, sticky=EW) | |
1501 # frame_key_sets. | |
1502 self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS) | |
1503 self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS) | |
1504 self.builtinlist.grid(row=0, column=1, sticky=NSEW) | |
1505 self.customlist.grid(row=1, column=1, sticky=NSEW) | |
1506 self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5) | |
1507 self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) | |
1508 self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) | |
1509 frames[0].pack(side=TOP, fill=BOTH, expand=True) | |
1510 frames[1].pack(side=TOP, fill=X, expand=True, pady=2) | |
1511 | |
1512 def load_key_cfg(self): | |
1513 "Load current configuration settings for the keybinding options." | |
1514 # Set current keys type radiobutton. | |
1515 self.keyset_source.set(idleConf.GetOption( | |
1516 'main', 'Keys', 'default', type='bool', default=1)) | |
1517 # Set current keys. | |
1518 current_option = idleConf.CurrentKeys() | |
1519 # Load available keyset option menus. | |
1520 if self.keyset_source.get(): # Default theme selected. | |
1521 item_list = idleConf.GetSectionList('default', 'keys') | |
1522 item_list.sort() | |
1523 self.builtinlist.SetMenu(item_list, current_option) | |
1524 item_list = idleConf.GetSectionList('user', 'keys') | |
1525 item_list.sort() | |
1526 if not item_list: | |
1527 self.custom_keyset_on.state(('disabled',)) | |
1528 self.custom_name.set('- no custom keys -') | |
1529 else: | |
1530 self.customlist.SetMenu(item_list, item_list[0]) | |
1531 else: # User key set selected. | |
1532 item_list = idleConf.GetSectionList('user', 'keys') | |
1533 item_list.sort() | |
1534 self.customlist.SetMenu(item_list, current_option) | |
1535 item_list = idleConf.GetSectionList('default', 'keys') | |
1536 item_list.sort() | |
1537 self.builtinlist.SetMenu(item_list, idleConf.default_keys()) | |
1538 self.set_keys_type() | |
1539 # Load keyset element list. | |
1540 keyset_name = idleConf.CurrentKeys() | |
1541 self.load_keys_list(keyset_name) | |
1542 | |
1543 def var_changed_builtin_name(self, *params): | |
1544 "Process selection of builtin key set." | |
1545 old_keys = ( | |
1546 'IDLE Classic Windows', | |
1547 'IDLE Classic Unix', | |
1548 'IDLE Classic Mac', | |
1549 'IDLE Classic OSX', | |
1550 ) | |
1551 value = self.builtin_name.get() | |
1552 if value not in old_keys: | |
1553 if idleConf.GetOption('main', 'Keys', 'name') not in old_keys: | |
1554 changes.add_option('main', 'Keys', 'name', old_keys[0]) | |
1555 changes.add_option('main', 'Keys', 'name2', value) | |
1556 self.keys_message['text'] = 'New key set, see Help' | |
1557 else: | |
1558 changes.add_option('main', 'Keys', 'name', value) | |
1559 changes.add_option('main', 'Keys', 'name2', '') | |
1560 self.keys_message['text'] = '' | |
1561 self.load_keys_list(value) | |
1562 | |
1563 def var_changed_custom_name(self, *params): | |
1564 "Process selection of custom key set." | |
1565 value = self.custom_name.get() | |
1566 if value != '- no custom keys -': | |
1567 changes.add_option('main', 'Keys', 'name', value) | |
1568 self.load_keys_list(value) | |
1569 | |
1570 def var_changed_keyset_source(self, *params): | |
1571 "Process toggle between builtin key set and custom key set." | |
1572 value = self.keyset_source.get() | |
1573 changes.add_option('main', 'Keys', 'default', value) | |
1574 if value: | |
1575 self.var_changed_builtin_name() | |
1576 else: | |
1577 self.var_changed_custom_name() | |
1578 | |
1579 def var_changed_keybinding(self, *params): | |
1580 "Store change to a keybinding." | |
1581 value = self.keybinding.get() | |
1582 key_set = self.custom_name.get() | |
1583 event = self.bindingslist.get(ANCHOR).split()[0] | |
1584 if idleConf.IsCoreBinding(event): | |
1585 changes.add_option('keys', key_set, event, value) | |
1586 else: # Event is an extension binding. | |
1587 ext_name = idleConf.GetExtnNameForEvent(event) | |
1588 ext_keybind_section = ext_name + '_cfgBindings' | |
1589 changes.add_option('extensions', ext_keybind_section, event, value) | |
1590 | |
1591 def set_keys_type(self): | |
1592 "Set available screen options based on builtin or custom key set." | |
1593 if self.keyset_source.get(): | |
1594 self.builtinlist['state'] = 'normal' | |
1595 self.customlist['state'] = 'disabled' | |
1596 self.button_delete_custom_keys.state(('disabled',)) | |
1597 else: | |
1598 self.builtinlist['state'] = 'disabled' | |
1599 self.custom_keyset_on.state(('!disabled',)) | |
1600 self.customlist['state'] = 'normal' | |
1601 self.button_delete_custom_keys.state(('!disabled',)) | |
1602 | |
1603 def get_new_keys(self): | |
1604 """Handle event to change key binding for selected line. | |
1605 | |
1606 A selection of a key/binding in the list of current | |
1607 bindings pops up a dialog to enter a new binding. If | |
1608 the current key set is builtin and a binding has | |
1609 changed, then a name for a custom key set needs to be | |
1610 entered for the change to be applied. | |
1611 """ | |
1612 list_index = self.bindingslist.index(ANCHOR) | |
1613 binding = self.bindingslist.get(list_index) | |
1614 bind_name = binding.split()[0] | |
1615 if self.keyset_source.get(): | |
1616 current_key_set_name = self.builtin_name.get() | |
1617 else: | |
1618 current_key_set_name = self.custom_name.get() | |
1619 current_bindings = idleConf.GetCurrentKeySet() | |
1620 if current_key_set_name in changes['keys']: # unsaved changes | |
1621 key_set_changes = changes['keys'][current_key_set_name] | |
1622 for event in key_set_changes: | |
1623 current_bindings[event] = key_set_changes[event].split() | |
1624 current_key_sequences = list(current_bindings.values()) | |
1625 new_keys = GetKeysDialog(self, 'Get New Keys', bind_name, | |
1626 current_key_sequences).result | |
1627 if new_keys: | |
1628 if self.keyset_source.get(): # Current key set is a built-in. | |
1629 message = ('Your changes will be saved as a new Custom Key Set.' | |
1630 ' Enter a name for your new Custom Key Set below.') | |
1631 new_keyset = self.get_new_keys_name(message) | |
1632 if not new_keyset: # User cancelled custom key set creation. | |
1633 self.bindingslist.select_set(list_index) | |
1634 self.bindingslist.select_anchor(list_index) | |
1635 return | |
1636 else: # Create new custom key set based on previously active key set. | |
1637 self.create_new_key_set(new_keyset) | |
1638 self.bindingslist.delete(list_index) | |
1639 self.bindingslist.insert(list_index, bind_name+' - '+new_keys) | |
1640 self.bindingslist.select_set(list_index) | |
1641 self.bindingslist.select_anchor(list_index) | |
1642 self.keybinding.set(new_keys) | |
1643 else: | |
1644 self.bindingslist.select_set(list_index) | |
1645 self.bindingslist.select_anchor(list_index) | |
1646 | |
1647 def get_new_keys_name(self, message): | |
1648 "Return new key set name from query popup." | |
1649 used_names = (idleConf.GetSectionList('user', 'keys') + | |
1650 idleConf.GetSectionList('default', 'keys')) | |
1651 new_keyset = SectionName( | |
1652 self, 'New Custom Key Set', message, used_names).result | |
1653 return new_keyset | |
1654 | |
1655 def save_as_new_key_set(self): | |
1656 "Prompt for name of new key set and save changes using that name." | |
1657 new_keys_name = self.get_new_keys_name('New Key Set Name:') | |
1658 if new_keys_name: | |
1659 self.create_new_key_set(new_keys_name) | |
1660 | |
1661 def on_bindingslist_select(self, event): | |
1662 "Activate button to assign new keys to selected action." | |
1663 self.button_new_keys.state(('!disabled',)) | |
1664 | |
1665 def create_new_key_set(self, new_key_set_name): | |
1666 """Create a new custom key set with the given name. | |
1667 | |
1668 Copy the bindings/keys from the previously active keyset | |
1669 to the new keyset and activate the new custom keyset. | |
1670 """ | |
1671 if self.keyset_source.get(): | |
1672 prev_key_set_name = self.builtin_name.get() | |
1673 else: | |
1674 prev_key_set_name = self.custom_name.get() | |
1675 prev_keys = idleConf.GetCoreKeys(prev_key_set_name) | |
1676 new_keys = {} | |
1677 for event in prev_keys: # Add key set to changed items. | |
1678 event_name = event[2:-2] # Trim off the angle brackets. | |
1679 binding = ' '.join(prev_keys[event]) | |
1680 new_keys[event_name] = binding | |
1681 # Handle any unsaved changes to prev key set. | |
1682 if prev_key_set_name in changes['keys']: | |
1683 key_set_changes = changes['keys'][prev_key_set_name] | |
1684 for event in key_set_changes: | |
1685 new_keys[event] = key_set_changes[event] | |
1686 # Save the new key set. | |
1687 self.save_new_key_set(new_key_set_name, new_keys) | |
1688 # Change GUI over to the new key set. | |
1689 custom_key_list = idleConf.GetSectionList('user', 'keys') | |
1690 custom_key_list.sort() | |
1691 self.customlist.SetMenu(custom_key_list, new_key_set_name) | |
1692 self.keyset_source.set(0) | |
1693 self.set_keys_type() | |
1694 | |
1695 def load_keys_list(self, keyset_name): | |
1696 """Reload the list of action/key binding pairs for the active key set. | |
1697 | |
1698 An action/key binding can be selected to change the key binding. | |
1699 """ | |
1700 reselect = False | |
1701 if self.bindingslist.curselection(): | |
1702 reselect = True | |
1703 list_index = self.bindingslist.index(ANCHOR) | |
1704 keyset = idleConf.GetKeySet(keyset_name) | |
1705 bind_names = list(keyset.keys()) | |
1706 bind_names.sort() | |
1707 self.bindingslist.delete(0, END) | |
1708 for bind_name in bind_names: | |
1709 key = ' '.join(keyset[bind_name]) | |
1710 bind_name = bind_name[2:-2] # Trim off the angle brackets. | |
1711 if keyset_name in changes['keys']: | |
1712 # Handle any unsaved changes to this key set. | |
1713 if bind_name in changes['keys'][keyset_name]: | |
1714 key = changes['keys'][keyset_name][bind_name] | |
1715 self.bindingslist.insert(END, bind_name+' - '+key) | |
1716 if reselect: | |
1717 self.bindingslist.see(list_index) | |
1718 self.bindingslist.select_set(list_index) | |
1719 self.bindingslist.select_anchor(list_index) | |
1720 | |
1721 @staticmethod | |
1722 def save_new_key_set(keyset_name, keyset): | |
1723 """Save a newly created core key set. | |
1724 | |
1725 Add keyset to idleConf.userCfg['keys'], not to disk. | |
1726 If the keyset doesn't exist, it is created. The | |
1727 binding/keys are taken from the keyset argument. | |
1728 | |
1729 keyset_name - string, the name of the new key set | |
1730 keyset - dictionary containing the new keybindings | |
1731 """ | |
1732 if not idleConf.userCfg['keys'].has_section(keyset_name): | |
1733 idleConf.userCfg['keys'].add_section(keyset_name) | |
1734 for event in keyset: | |
1735 value = keyset[event] | |
1736 idleConf.userCfg['keys'].SetOption(keyset_name, event, value) | |
1737 | |
1738 def askyesno(self, *args, **kwargs): | |
1739 # Make testing easier. Could change implementation. | |
1740 return messagebox.askyesno(*args, **kwargs) | |
1741 | |
1742 def delete_custom_keys(self): | |
1743 """Handle event to delete a custom key set. | |
1744 | |
1745 Applying the delete deactivates the current configuration and | |
1746 reverts to the default. The custom key set is permanently | |
1747 deleted from the config file. | |
1748 """ | |
1749 keyset_name = self.custom_name.get() | |
1750 delmsg = 'Are you sure you wish to delete the key set %r ?' | |
1751 if not self.askyesno( | |
1752 'Delete Key Set', delmsg % keyset_name, parent=self): | |
1753 return | |
1754 self.cd.deactivate_current_config() | |
1755 # Remove key set from changes, config, and file. | |
1756 changes.delete_section('keys', keyset_name) | |
1757 # Reload user key set list. | |
1758 item_list = idleConf.GetSectionList('user', 'keys') | |
1759 item_list.sort() | |
1760 if not item_list: | |
1761 self.custom_keyset_on.state(('disabled',)) | |
1762 self.customlist.SetMenu(item_list, '- no custom keys -') | |
1763 else: | |
1764 self.customlist.SetMenu(item_list, item_list[0]) | |
1765 # Revert to default key set. | |
1766 self.keyset_source.set(idleConf.defaultCfg['main'] | |
1767 .Get('Keys', 'default')) | |
1768 self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name') | |
1769 or idleConf.default_keys()) | |
1770 # User can't back out of these changes, they must be applied now. | |
1771 changes.save_all() | |
1772 self.cd.save_all_changed_extensions() | |
1773 self.cd.activate_config_changes() | |
1774 self.set_keys_type() | |
1775 | |
1776 | |
1777 class GenPage(Frame): | |
1778 | |
1779 def __init__(self, master): | |
1780 super().__init__(master) | |
1781 | |
1782 self.init_validators() | |
1783 self.create_page_general() | |
1784 self.load_general_cfg() | |
1785 | |
1786 def init_validators(self): | |
1787 digits_or_empty_re = re.compile(r'[0-9]*') | |
1788 def is_digits_or_empty(s): | |
1789 "Return 's is blank or contains only digits'" | |
1790 return digits_or_empty_re.fullmatch(s) is not None | |
1791 self.digits_only = (self.register(is_digits_or_empty), '%P',) | |
1792 | |
1793 def create_page_general(self): | |
1794 """Return frame of widgets for General tab. | |
1795 | |
1796 Enable users to provisionally change general options. Function | |
1797 load_general_cfg initializes tk variables and helplist using | |
1798 idleConf. Radiobuttons startup_shell_on and startup_editor_on | |
1799 set var startup_edit. Radiobuttons save_ask_on and save_auto_on | |
1800 set var autosave. Entry boxes win_width_int and win_height_int | |
1801 set var win_width and win_height. Setting var_name invokes the | |
1802 default callback that adds option to changes. | |
1803 | |
1804 Helplist: load_general_cfg loads list user_helplist with | |
1805 name, position pairs and copies names to listbox helplist. | |
1806 Clicking a name invokes help_source selected. Clicking | |
1807 button_helplist_name invokes helplist_item_name, which also | |
1808 changes user_helplist. These functions all call | |
1809 set_add_delete_state. All but load call update_help_changes to | |
1810 rewrite changes['main']['HelpFiles']. | |
1811 | |
1812 Widgets for GenPage(Frame): (*) widgets bound to self | |
1813 frame_window: LabelFrame | |
1814 frame_run: Frame | |
1815 startup_title: Label | |
1816 (*)startup_editor_on: Radiobutton - startup_edit | |
1817 (*)startup_shell_on: Radiobutton - startup_edit | |
1818 frame_win_size: Frame | |
1819 win_size_title: Label | |
1820 win_width_title: Label | |
1821 (*)win_width_int: Entry - win_width | |
1822 win_height_title: Label | |
1823 (*)win_height_int: Entry - win_height | |
1824 frame_cursor_blink: Frame | |
1825 cursor_blink_title: Label | |
1826 (*)cursor_blink_bool: Checkbutton - cursor_blink | |
1827 frame_autocomplete: Frame | |
1828 auto_wait_title: Label | |
1829 (*)auto_wait_int: Entry - autocomplete_wait | |
1830 frame_paren1: Frame | |
1831 paren_style_title: Label | |
1832 (*)paren_style_type: OptionMenu - paren_style | |
1833 frame_paren2: Frame | |
1834 paren_time_title: Label | |
1835 (*)paren_flash_time: Entry - flash_delay | |
1836 (*)bell_on: Checkbutton - paren_bell | |
1837 frame_editor: LabelFrame | |
1838 frame_save: Frame | |
1839 run_save_title: Label | |
1840 (*)save_ask_on: Radiobutton - autosave | |
1841 (*)save_auto_on: Radiobutton - autosave | |
1842 frame_format: Frame | |
1843 format_width_title: Label | |
1844 (*)format_width_int: Entry - format_width | |
1845 frame_line_numbers_default: Frame | |
1846 line_numbers_default_title: Label | |
1847 (*)line_numbers_default_bool: Checkbutton - line_numbers_default | |
1848 frame_context: Frame | |
1849 context_title: Label | |
1850 (*)context_int: Entry - context_lines | |
1851 frame_shell: LabelFrame | |
1852 frame_auto_squeeze_min_lines: Frame | |
1853 auto_squeeze_min_lines_title: Label | |
1854 (*)auto_squeeze_min_lines_int: Entry - auto_squeeze_min_lines | |
1855 frame_help: LabelFrame | |
1856 frame_helplist: Frame | |
1857 frame_helplist_buttons: Frame | |
1858 (*)button_helplist_edit | |
1859 (*)button_helplist_add | |
1860 (*)button_helplist_remove | |
1861 (*)helplist: ListBox | |
1862 scroll_helplist: Scrollbar | |
1863 """ | |
1864 # Integer values need StringVar because int('') raises. | |
1865 self.startup_edit = tracers.add( | |
1866 IntVar(self), ('main', 'General', 'editor-on-startup')) | |
1867 self.win_width = tracers.add( | |
1868 StringVar(self), ('main', 'EditorWindow', 'width')) | |
1869 self.win_height = tracers.add( | |
1870 StringVar(self), ('main', 'EditorWindow', 'height')) | |
1871 self.cursor_blink = tracers.add( | |
1872 BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink')) | |
1873 self.autocomplete_wait = tracers.add( | |
1874 StringVar(self), ('extensions', 'AutoComplete', 'popupwait')) | |
1875 self.paren_style = tracers.add( | |
1876 StringVar(self), ('extensions', 'ParenMatch', 'style')) | |
1877 self.flash_delay = tracers.add( | |
1878 StringVar(self), ('extensions', 'ParenMatch', 'flash-delay')) | |
1879 self.paren_bell = tracers.add( | |
1880 BooleanVar(self), ('extensions', 'ParenMatch', 'bell')) | |
1881 | |
1882 self.auto_squeeze_min_lines = tracers.add( | |
1883 StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines')) | |
1884 | |
1885 self.autosave = tracers.add( | |
1886 IntVar(self), ('main', 'General', 'autosave')) | |
1887 self.format_width = tracers.add( | |
1888 StringVar(self), ('extensions', 'FormatParagraph', 'max-width')) | |
1889 self.line_numbers_default = tracers.add( | |
1890 BooleanVar(self), | |
1891 ('main', 'EditorWindow', 'line-numbers-default')) | |
1892 self.context_lines = tracers.add( | |
1893 StringVar(self), ('extensions', 'CodeContext', 'maxlines')) | |
1894 | |
1895 # Create widgets: | |
1896 # Section frames. | |
1897 frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE, | |
1898 text=' Window Preferences') | |
1899 frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE, | |
1900 text=' Editor Preferences') | |
1901 frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE, | |
1902 text=' Shell Preferences') | |
1903 frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE, | |
1904 text=' Additional Help Sources ') | |
1905 # Frame_window. | |
1906 frame_run = Frame(frame_window, borderwidth=0) | |
1907 startup_title = Label(frame_run, text='At Startup') | |
1908 self.startup_editor_on = Radiobutton( | |
1909 frame_run, variable=self.startup_edit, value=1, | |
1910 text="Open Edit Window") | |
1911 self.startup_shell_on = Radiobutton( | |
1912 frame_run, variable=self.startup_edit, value=0, | |
1913 text='Open Shell Window') | |
1914 | |
1915 frame_win_size = Frame(frame_window, borderwidth=0) | |
1916 win_size_title = Label( | |
1917 frame_win_size, text='Initial Window Size (in characters)') | |
1918 win_width_title = Label(frame_win_size, text='Width') | |
1919 self.win_width_int = Entry( | |
1920 frame_win_size, textvariable=self.win_width, width=3, | |
1921 validatecommand=self.digits_only, validate='key', | |
1922 ) | |
1923 win_height_title = Label(frame_win_size, text='Height') | |
1924 self.win_height_int = Entry( | |
1925 frame_win_size, textvariable=self.win_height, width=3, | |
1926 validatecommand=self.digits_only, validate='key', | |
1927 ) | |
1928 | |
1929 frame_cursor_blink = Frame(frame_window, borderwidth=0) | |
1930 cursor_blink_title = Label(frame_cursor_blink, text='Cursor Blink') | |
1931 self.cursor_blink_bool = Checkbutton(frame_cursor_blink, | |
1932 variable=self.cursor_blink, width=1) | |
1933 | |
1934 frame_autocomplete = Frame(frame_window, borderwidth=0,) | |
1935 auto_wait_title = Label(frame_autocomplete, | |
1936 text='Completions Popup Wait (milliseconds)') | |
1937 self.auto_wait_int = Entry(frame_autocomplete, width=6, | |
1938 textvariable=self.autocomplete_wait, | |
1939 validatecommand=self.digits_only, | |
1940 validate='key', | |
1941 ) | |
1942 | |
1943 frame_paren1 = Frame(frame_window, borderwidth=0) | |
1944 paren_style_title = Label(frame_paren1, text='Paren Match Style') | |
1945 self.paren_style_type = OptionMenu( | |
1946 frame_paren1, self.paren_style, 'expression', | |
1947 "opener","parens","expression") | |
1948 frame_paren2 = Frame(frame_window, borderwidth=0) | |
1949 paren_time_title = Label( | |
1950 frame_paren2, text='Time Match Displayed (milliseconds)\n' | |
1951 '(0 is until next input)') | |
1952 self.paren_flash_time = Entry( | |
1953 frame_paren2, textvariable=self.flash_delay, width=6) | |
1954 self.bell_on = Checkbutton( | |
1955 frame_paren2, text="Bell on Mismatch", variable=self.paren_bell) | |
1956 | |
1957 # Frame_editor. | |
1958 frame_save = Frame(frame_editor, borderwidth=0) | |
1959 run_save_title = Label(frame_save, text='At Start of Run (F5) ') | |
1960 self.save_ask_on = Radiobutton( | |
1961 frame_save, variable=self.autosave, value=0, | |
1962 text="Prompt to Save") | |
1963 self.save_auto_on = Radiobutton( | |
1964 frame_save, variable=self.autosave, value=1, | |
1965 text='No Prompt') | |
1966 | |
1967 frame_format = Frame(frame_editor, borderwidth=0) | |
1968 format_width_title = Label(frame_format, | |
1969 text='Format Paragraph Max Width') | |
1970 self.format_width_int = Entry( | |
1971 frame_format, textvariable=self.format_width, width=4, | |
1972 validatecommand=self.digits_only, validate='key', | |
1973 ) | |
1974 | |
1975 frame_line_numbers_default = Frame(frame_editor, borderwidth=0) | |
1976 line_numbers_default_title = Label( | |
1977 frame_line_numbers_default, text='Show line numbers in new windows') | |
1978 self.line_numbers_default_bool = Checkbutton( | |
1979 frame_line_numbers_default, | |
1980 variable=self.line_numbers_default, | |
1981 width=1) | |
1982 | |
1983 frame_context = Frame(frame_editor, borderwidth=0) | |
1984 context_title = Label(frame_context, text='Max Context Lines :') | |
1985 self.context_int = Entry( | |
1986 frame_context, textvariable=self.context_lines, width=3, | |
1987 validatecommand=self.digits_only, validate='key', | |
1988 ) | |
1989 | |
1990 # Frame_shell. | |
1991 frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0) | |
1992 auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines, | |
1993 text='Auto-Squeeze Min. Lines:') | |
1994 self.auto_squeeze_min_lines_int = Entry( | |
1995 frame_auto_squeeze_min_lines, width=4, | |
1996 textvariable=self.auto_squeeze_min_lines, | |
1997 validatecommand=self.digits_only, validate='key', | |
1998 ) | |
1999 | |
2000 # frame_help. | |
2001 frame_helplist = Frame(frame_help) | |
2002 frame_helplist_buttons = Frame(frame_helplist) | |
2003 self.helplist = Listbox( | |
2004 frame_helplist, height=5, takefocus=True, | |
2005 exportselection=FALSE) | |
2006 scroll_helplist = Scrollbar(frame_helplist) | |
2007 scroll_helplist['command'] = self.helplist.yview | |
2008 self.helplist['yscrollcommand'] = scroll_helplist.set | |
2009 self.helplist.bind('<ButtonRelease-1>', self.help_source_selected) | |
2010 self.button_helplist_edit = Button( | |
2011 frame_helplist_buttons, text='Edit', state='disabled', | |
2012 width=8, command=self.helplist_item_edit) | |
2013 self.button_helplist_add = Button( | |
2014 frame_helplist_buttons, text='Add', | |
2015 width=8, command=self.helplist_item_add) | |
2016 self.button_helplist_remove = Button( | |
2017 frame_helplist_buttons, text='Remove', state='disabled', | |
2018 width=8, command=self.helplist_item_remove) | |
2019 | |
2020 # Pack widgets: | |
2021 # Body. | |
2022 frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) | |
2023 frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) | |
2024 frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) | |
2025 frame_help.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) | |
2026 # frame_run. | |
2027 frame_run.pack(side=TOP, padx=5, pady=0, fill=X) | |
2028 startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2029 self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) | |
2030 self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) | |
2031 # frame_win_size. | |
2032 frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X) | |
2033 win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2034 self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) | |
2035 win_height_title.pack(side=RIGHT, anchor=E, pady=5) | |
2036 self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) | |
2037 win_width_title.pack(side=RIGHT, anchor=E, pady=5) | |
2038 # frame_cursor_blink. | |
2039 frame_cursor_blink.pack(side=TOP, padx=5, pady=0, fill=X) | |
2040 cursor_blink_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2041 self.cursor_blink_bool.pack(side=LEFT, padx=5, pady=5) | |
2042 # frame_autocomplete. | |
2043 frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X) | |
2044 auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2045 self.auto_wait_int.pack(side=TOP, padx=10, pady=5) | |
2046 # frame_paren. | |
2047 frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X) | |
2048 paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2049 self.paren_style_type.pack(side=TOP, padx=10, pady=5) | |
2050 frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X) | |
2051 paren_time_title.pack(side=LEFT, anchor=W, padx=5) | |
2052 self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5) | |
2053 self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5) | |
2054 | |
2055 # frame_save. | |
2056 frame_save.pack(side=TOP, padx=5, pady=0, fill=X) | |
2057 run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2058 self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) | |
2059 self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) | |
2060 # frame_format. | |
2061 frame_format.pack(side=TOP, padx=5, pady=0, fill=X) | |
2062 format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2063 self.format_width_int.pack(side=TOP, padx=10, pady=5) | |
2064 # frame_line_numbers_default. | |
2065 frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X) | |
2066 line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2067 self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5) | |
2068 # frame_context. | |
2069 frame_context.pack(side=TOP, padx=5, pady=0, fill=X) | |
2070 context_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2071 self.context_int.pack(side=TOP, padx=5, pady=5) | |
2072 | |
2073 # frame_auto_squeeze_min_lines | |
2074 frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X) | |
2075 auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5) | |
2076 self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5) | |
2077 | |
2078 # frame_help. | |
2079 frame_helplist_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y) | |
2080 frame_helplist.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) | |
2081 scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y) | |
2082 self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH) | |
2083 self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5) | |
2084 self.button_helplist_add.pack(side=TOP, anchor=W) | |
2085 self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5) | |
2086 | |
2087 def load_general_cfg(self): | |
2088 "Load current configuration settings for the general options." | |
2089 # Set variables for all windows. | |
2090 self.startup_edit.set(idleConf.GetOption( | |
2091 'main', 'General', 'editor-on-startup', type='bool')) | |
2092 self.win_width.set(idleConf.GetOption( | |
2093 'main', 'EditorWindow', 'width', type='int')) | |
2094 self.win_height.set(idleConf.GetOption( | |
2095 'main', 'EditorWindow', 'height', type='int')) | |
2096 self.cursor_blink.set(idleConf.GetOption( | |
2097 'main', 'EditorWindow', 'cursor-blink', type='bool')) | |
2098 self.autocomplete_wait.set(idleConf.GetOption( | |
2099 'extensions', 'AutoComplete', 'popupwait', type='int')) | |
2100 self.paren_style.set(idleConf.GetOption( | |
2101 'extensions', 'ParenMatch', 'style')) | |
2102 self.flash_delay.set(idleConf.GetOption( | |
2103 'extensions', 'ParenMatch', 'flash-delay', type='int')) | |
2104 self.paren_bell.set(idleConf.GetOption( | |
2105 'extensions', 'ParenMatch', 'bell')) | |
2106 | |
2107 # Set variables for editor windows. | |
2108 self.autosave.set(idleConf.GetOption( | |
2109 'main', 'General', 'autosave', default=0, type='bool')) | |
2110 self.format_width.set(idleConf.GetOption( | |
2111 'extensions', 'FormatParagraph', 'max-width', type='int')) | |
2112 self.line_numbers_default.set(idleConf.GetOption( | |
2113 'main', 'EditorWindow', 'line-numbers-default', type='bool')) | |
2114 self.context_lines.set(idleConf.GetOption( | |
2115 'extensions', 'CodeContext', 'maxlines', type='int')) | |
2116 | |
2117 # Set variables for shell windows. | |
2118 self.auto_squeeze_min_lines.set(idleConf.GetOption( | |
2119 'main', 'PyShell', 'auto-squeeze-min-lines', type='int')) | |
2120 | |
2121 # Set additional help sources. | |
2122 self.user_helplist = idleConf.GetAllExtraHelpSourcesList() | |
2123 self.helplist.delete(0, 'end') | |
2124 for help_item in self.user_helplist: | |
2125 self.helplist.insert(END, help_item[0]) | |
2126 self.set_add_delete_state() | |
2127 | |
2128 def help_source_selected(self, event): | |
2129 "Handle event for selecting additional help." | |
2130 self.set_add_delete_state() | |
2131 | |
2132 def set_add_delete_state(self): | |
2133 "Toggle the state for the help list buttons based on list entries." | |
2134 if self.helplist.size() < 1: # No entries in list. | |
2135 self.button_helplist_edit.state(('disabled',)) | |
2136 self.button_helplist_remove.state(('disabled',)) | |
2137 else: # Some entries. | |
2138 if self.helplist.curselection(): # There currently is a selection. | |
2139 self.button_helplist_edit.state(('!disabled',)) | |
2140 self.button_helplist_remove.state(('!disabled',)) | |
2141 else: # There currently is not a selection. | |
2142 self.button_helplist_edit.state(('disabled',)) | |
2143 self.button_helplist_remove.state(('disabled',)) | |
2144 | |
2145 def helplist_item_add(self): | |
2146 """Handle add button for the help list. | |
2147 | |
2148 Query for name and location of new help sources and add | |
2149 them to the list. | |
2150 """ | |
2151 help_source = HelpSource(self, 'New Help Source').result | |
2152 if help_source: | |
2153 self.user_helplist.append(help_source) | |
2154 self.helplist.insert(END, help_source[0]) | |
2155 self.update_help_changes() | |
2156 | |
2157 def helplist_item_edit(self): | |
2158 """Handle edit button for the help list. | |
2159 | |
2160 Query with existing help source information and update | |
2161 config if the values are changed. | |
2162 """ | |
2163 item_index = self.helplist.index(ANCHOR) | |
2164 help_source = self.user_helplist[item_index] | |
2165 new_help_source = HelpSource( | |
2166 self, 'Edit Help Source', | |
2167 menuitem=help_source[0], | |
2168 filepath=help_source[1], | |
2169 ).result | |
2170 if new_help_source and new_help_source != help_source: | |
2171 self.user_helplist[item_index] = new_help_source | |
2172 self.helplist.delete(item_index) | |
2173 self.helplist.insert(item_index, new_help_source[0]) | |
2174 self.update_help_changes() | |
2175 self.set_add_delete_state() # Selected will be un-selected | |
2176 | |
2177 def helplist_item_remove(self): | |
2178 """Handle remove button for the help list. | |
2179 | |
2180 Delete the help list item from config. | |
2181 """ | |
2182 item_index = self.helplist.index(ANCHOR) | |
2183 del(self.user_helplist[item_index]) | |
2184 self.helplist.delete(item_index) | |
2185 self.update_help_changes() | |
2186 self.set_add_delete_state() | |
2187 | |
2188 def update_help_changes(self): | |
2189 "Clear and rebuild the HelpFiles section in changes" | |
2190 changes['main']['HelpFiles'] = {} | |
2191 for num in range(1, len(self.user_helplist) + 1): | |
2192 changes.add_option( | |
2193 'main', 'HelpFiles', str(num), | |
2194 ';'.join(self.user_helplist[num-1][:2])) | |
2195 | |
2196 | |
2197 class VarTrace: | |
2198 """Maintain Tk variables trace state.""" | |
2199 | |
2200 def __init__(self): | |
2201 """Store Tk variables and callbacks. | |
2202 | |
2203 untraced: List of tuples (var, callback) | |
2204 that do not have the callback attached | |
2205 to the Tk var. | |
2206 traced: List of tuples (var, callback) where | |
2207 that callback has been attached to the var. | |
2208 """ | |
2209 self.untraced = [] | |
2210 self.traced = [] | |
2211 | |
2212 def clear(self): | |
2213 "Clear lists (for tests)." | |
2214 # Call after all tests in a module to avoid memory leaks. | |
2215 self.untraced.clear() | |
2216 self.traced.clear() | |
2217 | |
2218 def add(self, var, callback): | |
2219 """Add (var, callback) tuple to untraced list. | |
2220 | |
2221 Args: | |
2222 var: Tk variable instance. | |
2223 callback: Either function name to be used as a callback | |
2224 or a tuple with IdleConf config-type, section, and | |
2225 option names used in the default callback. | |
2226 | |
2227 Return: | |
2228 Tk variable instance. | |
2229 """ | |
2230 if isinstance(callback, tuple): | |
2231 callback = self.make_callback(var, callback) | |
2232 self.untraced.append((var, callback)) | |
2233 return var | |
2234 | |
2235 @staticmethod | |
2236 def make_callback(var, config): | |
2237 "Return default callback function to add values to changes instance." | |
2238 def default_callback(*params): | |
2239 "Add config values to changes instance." | |
2240 changes.add_option(*config, var.get()) | |
2241 return default_callback | |
2242 | |
2243 def attach(self): | |
2244 "Attach callback to all vars that are not traced." | |
2245 while self.untraced: | |
2246 var, callback = self.untraced.pop() | |
2247 var.trace_add('write', callback) | |
2248 self.traced.append((var, callback)) | |
2249 | |
2250 def detach(self): | |
2251 "Remove callback from traced vars." | |
2252 while self.traced: | |
2253 var, callback = self.traced.pop() | |
2254 var.trace_remove('write', var.trace_info()[0][1]) | |
2255 self.untraced.append((var, callback)) | |
2256 | |
2257 | |
2258 tracers = VarTrace() | |
2259 | |
2260 help_common = '''\ | |
2261 When you click either the Apply or Ok buttons, settings in this | |
2262 dialog that are different from IDLE's default are saved in | |
2263 a .idlerc directory in your home directory. Except as noted, | |
2264 these changes apply to all versions of IDLE installed on this | |
2265 machine. [Cancel] only cancels changes made since the last save. | |
2266 ''' | |
2267 help_pages = { | |
2268 'Fonts/Tabs':''' | |
2269 Font sample: This shows what a selection of Basic Multilingual Plane | |
2270 unicode characters look like for the current font selection. If the | |
2271 selected font does not define a character, Tk attempts to find another | |
2272 font that does. Substitute glyphs depend on what is available on a | |
2273 particular system and will not necessarily have the same size as the | |
2274 font selected. Line contains 20 characters up to Devanagari, 14 for | |
2275 Tamil, and 10 for East Asia. | |
2276 | |
2277 Hebrew and Arabic letters should display right to left, starting with | |
2278 alef, \u05d0 and \u0627. Arabic digits display left to right. The | |
2279 Devanagari and Tamil lines start with digits. The East Asian lines | |
2280 are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese | |
2281 Hiragana and Katakana. | |
2282 | |
2283 You can edit the font sample. Changes remain until IDLE is closed. | |
2284 ''', | |
2285 'Highlights': ''' | |
2286 Highlighting: | |
2287 The IDLE Dark color theme is new in October 2015. It can only | |
2288 be used with older IDLE releases if it is saved as a custom | |
2289 theme, with a different name. | |
2290 ''', | |
2291 'Keys': ''' | |
2292 Keys: | |
2293 The IDLE Modern Unix key set is new in June 2016. It can only | |
2294 be used with older IDLE releases if it is saved as a custom | |
2295 key set, with a different name. | |
2296 ''', | |
2297 'General': ''' | |
2298 General: | |
2299 | |
2300 AutoComplete: Popupwait is milliseconds to wait after key char, without | |
2301 cursor movement, before popping up completion box. Key char is '.' after | |
2302 identifier or a '/' (or '\\' on Windows) within a string. | |
2303 | |
2304 FormatParagraph: Max-width is max chars in lines after re-formatting. | |
2305 Use with paragraphs in both strings and comment blocks. | |
2306 | |
2307 ParenMatch: Style indicates what is highlighted when closer is entered: | |
2308 'opener' - opener '({[' corresponding to closer; 'parens' - both chars; | |
2309 'expression' (default) - also everything in between. Flash-delay is how | |
2310 long to highlight if cursor is not moved (0 means forever). | |
2311 | |
2312 CodeContext: Maxlines is the maximum number of code context lines to | |
2313 display when Code Context is turned on for an editor window. | |
2314 | |
2315 Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines | |
2316 of output to automatically "squeeze". | |
2317 ''' | |
2318 } | |
2319 | |
2320 | |
2321 def is_int(s): | |
2322 "Return 's is blank or represents an int'" | |
2323 if not s: | |
2324 return True | |
2325 try: | |
2326 int(s) | |
2327 return True | |
2328 except ValueError: | |
2329 return False | |
2330 | |
2331 | |
2332 class VerticalScrolledFrame(Frame): | |
2333 """A pure Tkinter vertically scrollable frame. | |
2334 | |
2335 * Use the 'interior' attribute to place widgets inside the scrollable frame | |
2336 * Construct and pack/place/grid normally | |
2337 * This frame only allows vertical scrolling | |
2338 """ | |
2339 def __init__(self, parent, *args, **kw): | |
2340 Frame.__init__(self, parent, *args, **kw) | |
2341 | |
2342 # Create a canvas object and a vertical scrollbar for scrolling it. | |
2343 vscrollbar = Scrollbar(self, orient=VERTICAL) | |
2344 vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) | |
2345 canvas = Canvas(self, borderwidth=0, highlightthickness=0, | |
2346 yscrollcommand=vscrollbar.set, width=240) | |
2347 canvas.pack(side=LEFT, fill=BOTH, expand=TRUE) | |
2348 vscrollbar.config(command=canvas.yview) | |
2349 | |
2350 # Reset the view. | |
2351 canvas.xview_moveto(0) | |
2352 canvas.yview_moveto(0) | |
2353 | |
2354 # Create a frame inside the canvas which will be scrolled with it. | |
2355 self.interior = interior = Frame(canvas) | |
2356 interior_id = canvas.create_window(0, 0, window=interior, anchor=NW) | |
2357 | |
2358 # Track changes to the canvas and frame width and sync them, | |
2359 # also updating the scrollbar. | |
2360 def _configure_interior(event): | |
2361 # Update the scrollbars to match the size of the inner frame. | |
2362 size = (interior.winfo_reqwidth(), interior.winfo_reqheight()) | |
2363 canvas.config(scrollregion="0 0 %s %s" % size) | |
2364 interior.bind('<Configure>', _configure_interior) | |
2365 | |
2366 def _configure_canvas(event): | |
2367 if interior.winfo_reqwidth() != canvas.winfo_width(): | |
2368 # Update the inner frame's width to fill the canvas. | |
2369 canvas.itemconfigure(interior_id, width=canvas.winfo_width()) | |
2370 canvas.bind('<Configure>', _configure_canvas) | |
2371 | |
2372 return | |
2373 | |
2374 | |
2375 if __name__ == '__main__': | |
2376 from unittest import main | |
2377 main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False) | |
2378 | |
2379 from idlelib.idle_test.htest import run | |
2380 run(ConfigDialog) |