|
"""IDLE Configuration Dialog: support user customization of IDLE by GUI |
|
|
|
Customize font faces, sizes, and colorization attributes. Set indentation |
|
defaults. Customize keybindings. Colorization and keybindings can be |
|
saved as user defined sets. Select startup options including shell/editor |
|
and default window size. Define additional help sources. |
|
|
|
Note that tab width in IDLE is currently fixed at eight due to Tk issues. |
|
Refer to comments in EditorWindow autoindent code for details. |
|
|
|
""" |
|
import re |
|
|
|
from tkinter import (Toplevel, Listbox, Canvas, |
|
StringVar, BooleanVar, IntVar, TRUE, FALSE, |
|
TOP, BOTTOM, RIGHT, LEFT, SOLID, GROOVE, |
|
NONE, BOTH, X, Y, W, E, EW, NS, NSEW, NW, |
|
HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END, TclError) |
|
from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label, |
|
OptionMenu, Notebook, Radiobutton, Scrollbar, Style, |
|
Spinbox, Combobox) |
|
from tkinter import colorchooser |
|
import tkinter.font as tkfont |
|
from tkinter import messagebox |
|
|
|
from idlelib.config import idleConf, ConfigChanges |
|
from idlelib.config_key import GetKeysWindow |
|
from idlelib.dynoption import DynOptionMenu |
|
from idlelib import macosx |
|
from idlelib.query import SectionName, HelpSource |
|
from idlelib.textview import view_text |
|
from idlelib.autocomplete import AutoComplete |
|
from idlelib.codecontext import CodeContext |
|
from idlelib.parenmatch import ParenMatch |
|
from idlelib.format import FormatParagraph |
|
from idlelib.squeezer import Squeezer |
|
from idlelib.textview import ScrollableTextFrame |
|
|
|
changes = ConfigChanges() |
|
|
|
reloadables = (AutoComplete, CodeContext, ParenMatch, FormatParagraph, |
|
Squeezer) |
|
|
|
|
|
class ConfigDialog(Toplevel): |
|
"""Config dialog for IDLE. |
|
""" |
|
|
|
def __init__(self, parent, title='', *, _htest=False, _utest=False): |
|
"""Show the tabbed dialog for user configuration. |
|
|
|
Args: |
|
parent - parent of this dialog |
|
title - string which is the title of this popup dialog |
|
_htest - bool, change box location when running htest |
|
_utest - bool, don't wait_window when running unittest |
|
|
|
Note: Focus set on font page fontlist. |
|
|
|
Methods: |
|
create_widgets |
|
cancel: Bound to DELETE_WINDOW protocol. |
|
""" |
|
Toplevel.__init__(self, parent) |
|
self.parent = parent |
|
if _htest: |
|
parent.instance_dict = {} |
|
if not _utest: |
|
self.withdraw() |
|
|
|
self.title(title or 'IDLE Preferences') |
|
x = parent.winfo_rootx() + 20 |
|
y = parent.winfo_rooty() + (30 if not _htest else 150) |
|
self.geometry(f'+{x}+{y}') |
|
|
|
|
|
|
|
self.create_widgets() |
|
self.resizable(height=FALSE, width=FALSE) |
|
self.transient(parent) |
|
self.protocol("WM_DELETE_WINDOW", self.cancel) |
|
self.fontpage.fontlist.focus_set() |
|
|
|
|
|
|
|
|
|
|
|
|
|
tracers.attach() |
|
|
|
if not _utest: |
|
self.grab_set() |
|
self.wm_deiconify() |
|
self.wait_window() |
|
|
|
def create_widgets(self): |
|
"""Create and place widgets for tabbed dialog. |
|
|
|
Widgets Bound to self: |
|
frame: encloses all other widgets |
|
note: Notebook |
|
highpage: HighPage |
|
fontpage: FontPage |
|
keyspage: KeysPage |
|
winpage: WinPage |
|
shedpage: ShedPage |
|
extpage: ExtPage |
|
|
|
Methods: |
|
create_action_buttons |
|
load_configs: Load pages except for extensions. |
|
activate_config_changes: Tell editors to reload. |
|
""" |
|
self.frame = frame = Frame(self, padding="5px") |
|
self.frame.grid(sticky="nwes") |
|
self.note = note = Notebook(frame) |
|
self.extpage = ExtPage(note) |
|
self.highpage = HighPage(note, self.extpage) |
|
self.fontpage = FontPage(note, self.highpage) |
|
self.keyspage = KeysPage(note, self.extpage) |
|
self.winpage = WinPage(note) |
|
self.shedpage = ShedPage(note) |
|
|
|
note.add(self.fontpage, text=' Fonts ') |
|
note.add(self.highpage, text='Highlights') |
|
note.add(self.keyspage, text=' Keys ') |
|
note.add(self.winpage, text=' Windows ') |
|
note.add(self.shedpage, text=' Shell/Ed ') |
|
note.add(self.extpage, text='Extensions') |
|
note.enable_traversal() |
|
note.pack(side=TOP, expand=TRUE, fill=BOTH) |
|
self.create_action_buttons().pack(side=BOTTOM) |
|
|
|
def create_action_buttons(self): |
|
"""Return frame of action buttons for dialog. |
|
|
|
Methods: |
|
ok |
|
apply |
|
cancel |
|
help |
|
|
|
Widget Structure: |
|
outer: Frame |
|
buttons: Frame |
|
(no assignment): Button (ok) |
|
(no assignment): Button (apply) |
|
(no assignment): Button (cancel) |
|
(no assignment): Button (help) |
|
(no assignment): Frame |
|
""" |
|
if macosx.isAquaTk(): |
|
|
|
|
|
padding_args = {} |
|
else: |
|
padding_args = {'padding': (6, 3)} |
|
outer = Frame(self.frame, padding=2) |
|
buttons_frame = Frame(outer, padding=2) |
|
self.buttons = {} |
|
for txt, cmd in ( |
|
('Ok', self.ok), |
|
('Apply', self.apply), |
|
('Cancel', self.cancel), |
|
('Help', self.help)): |
|
self.buttons[txt] = Button(buttons_frame, text=txt, command=cmd, |
|
takefocus=FALSE, **padding_args) |
|
self.buttons[txt].pack(side=LEFT, padx=5) |
|
|
|
Frame(outer, height=2, borderwidth=0).pack(side=TOP) |
|
buttons_frame.pack(side=BOTTOM) |
|
return outer |
|
|
|
def ok(self): |
|
"""Apply config changes, then dismiss dialog.""" |
|
self.apply() |
|
self.destroy() |
|
|
|
def apply(self): |
|
"""Apply config changes and leave dialog open.""" |
|
self.deactivate_current_config() |
|
changes.save_all() |
|
self.extpage.save_all_changed_extensions() |
|
self.activate_config_changes() |
|
|
|
def cancel(self): |
|
"""Dismiss config dialog. |
|
|
|
Methods: |
|
destroy: inherited |
|
""" |
|
changes.clear() |
|
self.destroy() |
|
|
|
def destroy(self): |
|
global font_sample_text |
|
font_sample_text = self.fontpage.font_sample.get('1.0', 'end') |
|
self.grab_release() |
|
super().destroy() |
|
|
|
def help(self): |
|
"""Create textview for config dialog help. |
|
|
|
Attributes accessed: |
|
note |
|
Methods: |
|
view_text: Method from textview module. |
|
""" |
|
page = self.note.tab(self.note.select(), option='text').strip() |
|
view_text(self, title='Help for IDLE preferences', |
|
contents=help_common+help_pages.get(page, '')) |
|
|
|
def deactivate_current_config(self): |
|
"""Remove current key bindings. |
|
Iterate over window instances defined in parent and remove |
|
the keybindings. |
|
""" |
|
|
|
|
|
win_instances = self.parent.instance_dict.keys() |
|
for instance in win_instances: |
|
instance.RemoveKeybindings() |
|
|
|
def activate_config_changes(self): |
|
"""Apply configuration changes to current windows. |
|
|
|
Dynamically update the current parent window instances |
|
with some of the configuration changes. |
|
""" |
|
win_instances = self.parent.instance_dict.keys() |
|
for instance in win_instances: |
|
instance.ResetColorizer() |
|
instance.ResetFont() |
|
instance.set_notabs_indentwidth() |
|
instance.ApplyKeybindings() |
|
instance.reset_help_menu_entries() |
|
instance.update_cursor_blink() |
|
for klass in reloadables: |
|
klass.reload() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
font_sample_text = ( |
|
'<ASCII/Latin1>\n' |
|
'AaBbCcDdEeFfGgHhIiJj\n1234567890#:+=(){}[]\n' |
|
'\u00a2\u00a3\u00a5\u00a7\u00a9\u00ab\u00ae\u00b6\u00bd\u011e' |
|
'\u00c0\u00c1\u00c2\u00c3\u00c4\u00c5\u00c7\u00d0\u00d8\u00df\n' |
|
'\n<IPA,Greek,Cyrillic>\n' |
|
'\u0250\u0255\u0258\u025e\u025f\u0264\u026b\u026e\u0270\u0277' |
|
'\u027b\u0281\u0283\u0286\u028e\u029e\u02a2\u02ab\u02ad\u02af\n' |
|
'\u0391\u03b1\u0392\u03b2\u0393\u03b3\u0394\u03b4\u0395\u03b5' |
|
'\u0396\u03b6\u0397\u03b7\u0398\u03b8\u0399\u03b9\u039a\u03ba\n' |
|
'\u0411\u0431\u0414\u0434\u0416\u0436\u041f\u043f\u0424\u0444' |
|
'\u0427\u0447\u042a\u044a\u042d\u044d\u0460\u0464\u046c\u04dc\n' |
|
'\n<Hebrew, Arabic>\n' |
|
'\u05d0\u05d1\u05d2\u05d3\u05d4\u05d5\u05d6\u05d7\u05d8\u05d9' |
|
'\u05da\u05db\u05dc\u05dd\u05de\u05df\u05e0\u05e1\u05e2\u05e3\n' |
|
'\u0627\u0628\u062c\u062f\u0647\u0648\u0632\u062d\u0637\u064a' |
|
'\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\n' |
|
'\n<Devanagari, Tamil>\n' |
|
'\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f' |
|
'\u0905\u0906\u0907\u0908\u0909\u090a\u090f\u0910\u0913\u0914\n' |
|
'\u0be6\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef' |
|
'\u0b85\u0b87\u0b89\u0b8e\n' |
|
'\n<East Asian>\n' |
|
'\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\n' |
|
'\u6c49\u5b57\u6f22\u5b57\u4eba\u6728\u706b\u571f\u91d1\u6c34\n' |
|
'\uac00\ub0d0\ub354\ub824\ubaa8\ubd64\uc218\uc720\uc988\uce58\n' |
|
'\u3042\u3044\u3046\u3048\u304a\u30a2\u30a4\u30a6\u30a8\u30aa\n' |
|
) |
|
|
|
|
|
class FontPage(Frame): |
|
|
|
def __init__(self, master, highpage): |
|
super().__init__(master) |
|
self.highlight_sample = highpage.highlight_sample |
|
self.create_page_font() |
|
self.load_font_cfg() |
|
|
|
def create_page_font(self): |
|
"""Return frame of widgets for Font tab. |
|
|
|
Fonts: Enable users to provisionally change font face, size, or |
|
boldness and to see the consequence of proposed choices. Each |
|
action set 3 options in changes structuree and changes the |
|
corresponding aspect of the font sample on this page and |
|
highlight sample on highlight page. |
|
|
|
Function load_font_cfg initializes font vars and widgets from |
|
idleConf entries and tk. |
|
|
|
Fontlist: mouse button 1 click or up or down key invoke |
|
on_fontlist_select(), which sets var font_name. |
|
|
|
Sizelist: clicking the menubutton opens the dropdown menu. A |
|
mouse button 1 click or return key sets var font_size. |
|
|
|
Bold_toggle: clicking the box toggles var font_bold. |
|
|
|
Changing any of the font vars invokes var_changed_font, which |
|
adds all 3 font options to changes and calls set_samples. |
|
Set_samples applies a new font constructed from the font vars to |
|
font_sample and to highlight_sample on the highlight page. |
|
|
|
Widgets for FontPage(Frame): (*) widgets bound to self |
|
frame_font: LabelFrame |
|
frame_font_name: Frame |
|
font_name_title: Label |
|
(*)fontlist: ListBox - font_name |
|
scroll_font: Scrollbar |
|
frame_font_param: Frame |
|
font_size_title: Label |
|
(*)sizelist: DynOptionMenu - font_size |
|
(*)bold_toggle: Checkbutton - font_bold |
|
frame_sample: LabelFrame |
|
(*)font_sample: Label |
|
""" |
|
self.font_name = tracers.add(StringVar(self), self.var_changed_font) |
|
self.font_size = tracers.add(StringVar(self), self.var_changed_font) |
|
self.font_bold = tracers.add(BooleanVar(self), self.var_changed_font) |
|
|
|
|
|
frame_font = LabelFrame(self, borderwidth=2, relief=GROOVE, |
|
text=' Shell/Editor Font ') |
|
frame_sample = LabelFrame(self, borderwidth=2, relief=GROOVE, |
|
text=' Font Sample (Editable) ') |
|
|
|
frame_font_name = Frame(frame_font) |
|
frame_font_param = Frame(frame_font) |
|
font_name_title = Label( |
|
frame_font_name, justify=LEFT, text='Font Face :') |
|
self.fontlist = Listbox(frame_font_name, height=15, |
|
takefocus=True, exportselection=FALSE) |
|
self.fontlist.bind('<ButtonRelease-1>', self.on_fontlist_select) |
|
self.fontlist.bind('<KeyRelease-Up>', self.on_fontlist_select) |
|
self.fontlist.bind('<KeyRelease-Down>', self.on_fontlist_select) |
|
scroll_font = Scrollbar(frame_font_name) |
|
scroll_font.config(command=self.fontlist.yview) |
|
self.fontlist.config(yscrollcommand=scroll_font.set) |
|
font_size_title = Label(frame_font_param, text='Size :') |
|
self.sizelist = DynOptionMenu(frame_font_param, self.font_size, None) |
|
self.bold_toggle = Checkbutton( |
|
frame_font_param, variable=self.font_bold, |
|
onvalue=1, offvalue=0, text='Bold') |
|
|
|
font_sample_frame = ScrollableTextFrame(frame_sample) |
|
self.font_sample = font_sample_frame.text |
|
self.font_sample.config(wrap=NONE, width=1, height=1) |
|
self.font_sample.insert(END, font_sample_text) |
|
|
|
|
|
self.columnconfigure(1, weight=1) |
|
self.rowconfigure(2, weight=1) |
|
frame_font.grid(row=0, column=0, padx=5, pady=5) |
|
frame_sample.grid(row=0, column=1, rowspan=3, padx=5, pady=5, |
|
sticky='nsew') |
|
|
|
frame_font_name.pack(side=TOP, padx=5, pady=5, fill=X) |
|
frame_font_param.pack(side=TOP, padx=5, pady=5, fill=X) |
|
font_name_title.pack(side=TOP, anchor=W) |
|
self.fontlist.pack(side=LEFT, expand=TRUE, fill=X) |
|
scroll_font.pack(side=LEFT, fill=Y) |
|
font_size_title.pack(side=LEFT, anchor=W) |
|
self.sizelist.pack(side=LEFT, anchor=W) |
|
self.bold_toggle.pack(side=LEFT, anchor=W, padx=20) |
|
|
|
font_sample_frame.pack(expand=TRUE, fill=BOTH) |
|
|
|
def load_font_cfg(self): |
|
"""Load current configuration settings for the font options. |
|
|
|
Retrieve current font with idleConf.GetFont and font families |
|
from tk. Setup fontlist and set font_name. Setup sizelist, |
|
which sets font_size. Set font_bold. Call set_samples. |
|
""" |
|
configured_font = idleConf.GetFont(self, 'main', 'EditorWindow') |
|
font_name = configured_font[0].lower() |
|
font_size = configured_font[1] |
|
font_bold = configured_font[2]=='bold' |
|
|
|
|
|
fonts = sorted(set(tkfont.families(self))) |
|
for font in fonts: |
|
self.fontlist.insert(END, font) |
|
self.font_name.set(font_name) |
|
lc_fonts = [s.lower() for s in fonts] |
|
try: |
|
current_font_index = lc_fonts.index(font_name) |
|
self.fontlist.see(current_font_index) |
|
self.fontlist.select_set(current_font_index) |
|
self.fontlist.select_anchor(current_font_index) |
|
self.fontlist.activate(current_font_index) |
|
except ValueError: |
|
pass |
|
|
|
self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14', |
|
'16', '18', '20', '22', '25', '29', '34', '40'), |
|
font_size) |
|
|
|
self.font_bold.set(font_bold) |
|
self.set_samples() |
|
|
|
def var_changed_font(self, *params): |
|
"""Store changes to font attributes. |
|
|
|
When one font attribute changes, save them all, as they are |
|
not independent from each other. In particular, when we are |
|
overriding the default font, we need to write out everything. |
|
""" |
|
value = self.font_name.get() |
|
changes.add_option('main', 'EditorWindow', 'font', value) |
|
value = self.font_size.get() |
|
changes.add_option('main', 'EditorWindow', 'font-size', value) |
|
value = self.font_bold.get() |
|
changes.add_option('main', 'EditorWindow', 'font-bold', value) |
|
self.set_samples() |
|
|
|
def on_fontlist_select(self, event): |
|
"""Handle selecting a font from the list. |
|
|
|
Event can result from either mouse click or Up or Down key. |
|
Set font_name and example displays to selection. |
|
""" |
|
font = self.fontlist.get( |
|
ACTIVE if event.type.name == 'KeyRelease' else ANCHOR) |
|
self.font_name.set(font.lower()) |
|
|
|
def set_samples(self, event=None): |
|
"""Update update both screen samples with the font settings. |
|
|
|
Called on font initialization and change events. |
|
Accesses font_name, font_size, and font_bold Variables. |
|
Updates font_sample and highlight page highlight_sample. |
|
""" |
|
font_name = self.font_name.get() |
|
font_weight = tkfont.BOLD if self.font_bold.get() else tkfont.NORMAL |
|
new_font = (font_name, self.font_size.get(), font_weight) |
|
self.font_sample['font'] = new_font |
|
self.highlight_sample['font'] = new_font |
|
|
|
|
|
class HighPage(Frame): |
|
|
|
def __init__(self, master, extpage): |
|
super().__init__(master) |
|
self.extpage = extpage |
|
self.cd = master.winfo_toplevel() |
|
self.style = Style(master) |
|
self.create_page_highlight() |
|
self.load_theme_cfg() |
|
|
|
def create_page_highlight(self): |
|
"""Return frame of widgets for Highlights tab. |
|
|
|
Enable users to provisionally change foreground and background |
|
colors applied to textual tags. Color mappings are stored in |
|
complete listings called themes. Built-in themes in |
|
idlelib/config-highlight.def are fixed as far as the dialog is |
|
concerned. Any theme can be used as the base for a new custom |
|
theme, stored in .idlerc/config-highlight.cfg. |
|
|
|
Function load_theme_cfg() initializes tk variables and theme |
|
lists and calls paint_theme_sample() and set_highlight_target() |
|
for the current theme. Radiobuttons builtin_theme_on and |
|
custom_theme_on toggle var theme_source, which controls if the |
|
current set of colors are from a builtin or custom theme. |
|
DynOptionMenus builtinlist and customlist contain lists of the |
|
builtin and custom themes, respectively, and the current item |
|
from each list is stored in vars builtin_name and custom_name. |
|
|
|
Function paint_theme_sample() applies the colors from the theme |
|
to the tags in text widget highlight_sample and then invokes |
|
set_color_sample(). Function set_highlight_target() sets the state |
|
of the radiobuttons fg_on and bg_on based on the tag and it also |
|
invokes set_color_sample(). |
|
|
|
Function set_color_sample() sets the background color for the frame |
|
holding the color selector. This provides a larger visual of the |
|
color for the current tag and plane (foreground/background). |
|
|
|
Note: set_color_sample() is called from many places and is often |
|
called more than once when a change is made. It is invoked when |
|
foreground or background is selected (radiobuttons), from |
|
paint_theme_sample() (theme is changed or load_cfg is called), and |
|
from set_highlight_target() (target tag is changed or load_cfg called). |
|
|
|
Button delete_custom invokes delete_custom() to delete |
|
a custom theme from idleConf.userCfg['highlight'] and changes. |
|
Button save_custom invokes save_as_new_theme() which calls |
|
get_new_theme_name() and create_new() to save a custom theme |
|
and its colors to idleConf.userCfg['highlight']. |
|
|
|
Radiobuttons fg_on and bg_on toggle var fg_bg_toggle to control |
|
if the current selected color for a tag is for the foreground or |
|
background. |
|
|
|
DynOptionMenu targetlist contains a readable description of the |
|
tags applied to Python source within IDLE. Selecting one of the |
|
tags from this list populates highlight_target, which has a callback |
|
function set_highlight_target(). |
|
|
|
Text widget highlight_sample displays a block of text (which is |
|
mock Python code) in which is embedded the defined tags and reflects |
|
the color attributes of the current theme and changes for those tags. |
|
Mouse button 1 allows for selection of a tag and updates |
|
highlight_target with that tag value. |
|
|
|
Note: The font in highlight_sample is set through the config in |
|
the fonts tab. |
|
|
|
In other words, a tag can be selected either from targetlist or |
|
by clicking on the sample text within highlight_sample. The |
|
plane (foreground/background) is selected via the radiobutton. |
|
Together, these two (tag and plane) control what color is |
|
shown in set_color_sample() for the current theme. Button set_color |
|
invokes get_color() which displays a ColorChooser to change the |
|
color for the selected tag/plane. If a new color is picked, |
|
it will be saved to changes and the highlight_sample and |
|
frame background will be updated. |
|
|
|
Tk Variables: |
|
color: Color of selected target. |
|
builtin_name: Menu variable for built-in theme. |
|
custom_name: Menu variable for custom theme. |
|
fg_bg_toggle: Toggle for foreground/background color. |
|
Note: this has no callback. |
|
theme_source: Selector for built-in or custom theme. |
|
highlight_target: Menu variable for the highlight tag target. |
|
|
|
Instance Data Attributes: |
|
theme_elements: Dictionary of tags for text highlighting. |
|
The key is the display name and the value is a tuple of |
|
(tag name, display sort order). |
|
|
|
Methods [attachment]: |
|
load_theme_cfg: Load current highlight colors. |
|
get_color: Invoke colorchooser [button_set_color]. |
|
set_color_sample_binding: Call set_color_sample [fg_bg_toggle]. |
|
set_highlight_target: set fg_bg_toggle, set_color_sample(). |
|
set_color_sample: Set frame background to target. |
|
on_new_color_set: Set new color and add option. |
|
paint_theme_sample: Recolor sample. |
|
get_new_theme_name: Get from popup. |
|
create_new: Combine theme with changes and save. |
|
save_as_new_theme: Save [button_save_custom]. |
|
set_theme_type: Command for [theme_source]. |
|
delete_custom: Activate default [button_delete_custom]. |
|
save_new: Save to userCfg['theme'] (is function). |
|
|
|
Widgets of highlights page frame: (*) widgets bound to self |
|
frame_custom: LabelFrame |
|
(*)highlight_sample: Text |
|
(*)frame_color_set: Frame |
|
(*)button_set_color: Button |
|
(*)targetlist: DynOptionMenu - highlight_target |
|
frame_fg_bg_toggle: Frame |
|
(*)fg_on: Radiobutton - fg_bg_toggle |
|
(*)bg_on: Radiobutton - fg_bg_toggle |
|
(*)button_save_custom: Button |
|
frame_theme: LabelFrame |
|
theme_type_title: Label |
|
(*)builtin_theme_on: Radiobutton - theme_source |
|
(*)custom_theme_on: Radiobutton - theme_source |
|
(*)builtinlist: DynOptionMenu - builtin_name |
|
(*)customlist: DynOptionMenu - custom_name |
|
(*)button_delete_custom: Button |
|
(*)theme_message: Label |
|
""" |
|
self.theme_elements = { |
|
'Normal Code or Text': ('normal', '00'), |
|
'Code Context': ('context', '01'), |
|
'Python Keywords': ('keyword', '02'), |
|
'Python Definitions': ('definition', '03'), |
|
'Python Builtins': ('builtin', '04'), |
|
'Python Comments': ('comment', '05'), |
|
'Python Strings': ('string', '06'), |
|
'Selected Text': ('hilite', '07'), |
|
'Found Text': ('hit', '08'), |
|
'Cursor': ('cursor', '09'), |
|
'Editor Breakpoint': ('break', '10'), |
|
'Shell Prompt': ('console', '11'), |
|
'Error Text': ('error', '12'), |
|
'Shell User Output': ('stdout', '13'), |
|
'Shell User Exception': ('stderr', '14'), |
|
'Line Number': ('linenumber', '16'), |
|
} |
|
self.builtin_name = tracers.add( |
|
StringVar(self), self.var_changed_builtin_name) |
|
self.custom_name = tracers.add( |
|
StringVar(self), self.var_changed_custom_name) |
|
self.fg_bg_toggle = BooleanVar(self) |
|
self.color = tracers.add( |
|
StringVar(self), self.var_changed_color) |
|
self.theme_source = tracers.add( |
|
BooleanVar(self), self.var_changed_theme_source) |
|
self.highlight_target = tracers.add( |
|
StringVar(self), self.var_changed_highlight_target) |
|
|
|
|
|
|
|
frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE, |
|
text=' Custom Highlighting ') |
|
frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE, |
|
text=' Highlighting Theme ') |
|
|
|
sample_frame = ScrollableTextFrame( |
|
frame_custom, relief=SOLID, borderwidth=1) |
|
text = self.highlight_sample = sample_frame.text |
|
text.configure( |
|
font=('courier', 12, ''), cursor='hand2', width=1, height=1, |
|
takefocus=FALSE, highlightthickness=0, wrap=NONE) |
|
|
|
text.bind('<Double-Button-1>', lambda e: 'break') |
|
text.bind('<B1-Motion>', lambda e: 'break') |
|
string_tags=( |
|
('# Click selects item.', 'comment'), ('\n', 'normal'), |
|
('code context section', 'context'), ('\n', 'normal'), |
|
('| cursor', 'cursor'), ('\n', 'normal'), |
|
('def', 'keyword'), (' ', 'normal'), |
|
('func', 'definition'), ('(param):\n ', 'normal'), |
|
('"Return None."', 'string'), ('\n var0 = ', 'normal'), |
|
("'string'", 'string'), ('\n var1 = ', 'normal'), |
|
("'selected'", 'hilite'), ('\n var2 = ', 'normal'), |
|
("'found'", 'hit'), ('\n var3 = ', 'normal'), |
|
('list', 'builtin'), ('(', 'normal'), |
|
('None', 'keyword'), (')\n', 'normal'), |
|
(' breakpoint("line")', 'break'), ('\n\n', 'normal'), |
|
('>>>', 'console'), (' 3.14**2\n', 'normal'), |
|
('9.8596', 'stdout'), ('\n', 'normal'), |
|
('>>>', 'console'), (' pri ', 'normal'), |
|
('n', 'error'), ('t(\n', 'normal'), |
|
('SyntaxError', 'stderr'), ('\n', 'normal')) |
|
for string, tag in string_tags: |
|
text.insert(END, string, tag) |
|
n_lines = len(text.get('1.0', END).splitlines()) |
|
for lineno in range(1, n_lines): |
|
text.insert(f'{lineno}.0', |
|
f'{lineno:{len(str(n_lines))}d} ', |
|
'linenumber') |
|
for element in self.theme_elements: |
|
def tem(event, elem=element): |
|
|
|
self.highlight_target.set(elem) |
|
text.tag_bind( |
|
self.theme_elements[element][0], '<ButtonPress-1>', tem) |
|
text['state'] = 'disabled' |
|
self.style.configure('frame_color_set.TFrame', borderwidth=1, |
|
relief='solid') |
|
self.frame_color_set = Frame(frame_custom, style='frame_color_set.TFrame') |
|
frame_fg_bg_toggle = Frame(frame_custom) |
|
self.button_set_color = Button( |
|
self.frame_color_set, text='Choose Color for :', |
|
command=self.get_color) |
|
self.targetlist = DynOptionMenu( |
|
self.frame_color_set, self.highlight_target, None, |
|
highlightthickness=0) |
|
self.fg_on = Radiobutton( |
|
frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=1, |
|
text='Foreground', command=self.set_color_sample_binding) |
|
self.bg_on = Radiobutton( |
|
frame_fg_bg_toggle, variable=self.fg_bg_toggle, value=0, |
|
text='Background', command=self.set_color_sample_binding) |
|
self.fg_bg_toggle.set(1) |
|
self.button_save_custom = Button( |
|
frame_custom, text='Save as New Custom Theme', |
|
command=self.save_as_new_theme) |
|
|
|
theme_type_title = Label(frame_theme, text='Select : ') |
|
self.builtin_theme_on = Radiobutton( |
|
frame_theme, variable=self.theme_source, value=1, |
|
command=self.set_theme_type, text='a Built-in Theme') |
|
self.custom_theme_on = Radiobutton( |
|
frame_theme, variable=self.theme_source, value=0, |
|
command=self.set_theme_type, text='a Custom Theme') |
|
self.builtinlist = DynOptionMenu( |
|
frame_theme, self.builtin_name, None, command=None) |
|
self.customlist = DynOptionMenu( |
|
frame_theme, self.custom_name, None, command=None) |
|
self.button_delete_custom = Button( |
|
frame_theme, text='Delete Custom Theme', |
|
command=self.delete_custom) |
|
self.theme_message = Label(frame_theme, borderwidth=2) |
|
|
|
|
|
frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) |
|
frame_theme.pack(side=TOP, padx=5, pady=5, fill=X) |
|
|
|
self.frame_color_set.pack(side=TOP, padx=5, pady=5, fill=X) |
|
frame_fg_bg_toggle.pack(side=TOP, padx=5, pady=0) |
|
sample_frame.pack( |
|
side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) |
|
self.button_set_color.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=4) |
|
self.targetlist.pack(side=TOP, expand=TRUE, fill=X, padx=8, pady=3) |
|
self.fg_on.pack(side=LEFT, anchor=E) |
|
self.bg_on.pack(side=RIGHT, anchor=W) |
|
self.button_save_custom.pack(side=BOTTOM, fill=X, padx=5, pady=5) |
|
|
|
theme_type_title.pack(side=TOP, anchor=W, padx=5, pady=5) |
|
self.builtin_theme_on.pack(side=TOP, anchor=W, padx=5) |
|
self.custom_theme_on.pack(side=TOP, anchor=W, padx=5, pady=2) |
|
self.builtinlist.pack(side=TOP, fill=X, padx=5, pady=5) |
|
self.customlist.pack(side=TOP, fill=X, anchor=W, padx=5, pady=5) |
|
self.button_delete_custom.pack(side=TOP, fill=X, padx=5, pady=5) |
|
self.theme_message.pack(side=TOP, fill=X, pady=5) |
|
|
|
def load_theme_cfg(self): |
|
"""Load current configuration settings for the theme options. |
|
|
|
Based on the theme_source toggle, the theme is set as |
|
either builtin or custom and the initial widget values |
|
reflect the current settings from idleConf. |
|
|
|
Attributes updated: |
|
theme_source: Set from idleConf. |
|
builtinlist: List of default themes from idleConf. |
|
customlist: List of custom themes from idleConf. |
|
custom_theme_on: Disabled if there are no custom themes. |
|
custom_theme: Message with additional information. |
|
targetlist: Create menu from self.theme_elements. |
|
|
|
Methods: |
|
set_theme_type |
|
paint_theme_sample |
|
set_highlight_target |
|
""" |
|
|
|
self.theme_source.set(idleConf.GetOption( |
|
'main', 'Theme', 'default', type='bool', default=1)) |
|
|
|
current_option = idleConf.CurrentTheme() |
|
|
|
if self.theme_source.get(): |
|
item_list = idleConf.GetSectionList('default', 'highlight') |
|
item_list.sort() |
|
self.builtinlist.SetMenu(item_list, current_option) |
|
item_list = idleConf.GetSectionList('user', 'highlight') |
|
item_list.sort() |
|
if not item_list: |
|
self.custom_theme_on.state(('disabled',)) |
|
self.custom_name.set('- no custom themes -') |
|
else: |
|
self.customlist.SetMenu(item_list, item_list[0]) |
|
else: |
|
item_list = idleConf.GetSectionList('user', 'highlight') |
|
item_list.sort() |
|
self.customlist.SetMenu(item_list, current_option) |
|
item_list = idleConf.GetSectionList('default', 'highlight') |
|
item_list.sort() |
|
self.builtinlist.SetMenu(item_list, item_list[0]) |
|
self.set_theme_type() |
|
|
|
theme_names = list(self.theme_elements.keys()) |
|
theme_names.sort(key=lambda x: self.theme_elements[x][1]) |
|
self.targetlist.SetMenu(theme_names, theme_names[0]) |
|
self.paint_theme_sample() |
|
self.set_highlight_target() |
|
|
|
def var_changed_builtin_name(self, *params): |
|
"""Process new builtin theme selection. |
|
|
|
Add the changed theme's name to the changed_items and recreate |
|
the sample with the values from the selected theme. |
|
""" |
|
old_themes = ('IDLE Classic', 'IDLE New') |
|
value = self.builtin_name.get() |
|
if value not in old_themes: |
|
if idleConf.GetOption('main', 'Theme', 'name') not in old_themes: |
|
changes.add_option('main', 'Theme', 'name', old_themes[0]) |
|
changes.add_option('main', 'Theme', 'name2', value) |
|
self.theme_message['text'] = 'New theme, see Help' |
|
else: |
|
changes.add_option('main', 'Theme', 'name', value) |
|
changes.add_option('main', 'Theme', 'name2', '') |
|
self.theme_message['text'] = '' |
|
self.paint_theme_sample() |
|
|
|
def var_changed_custom_name(self, *params): |
|
"""Process new custom theme selection. |
|
|
|
If a new custom theme is selected, add the name to the |
|
changed_items and apply the theme to the sample. |
|
""" |
|
value = self.custom_name.get() |
|
if value != '- no custom themes -': |
|
changes.add_option('main', 'Theme', 'name', value) |
|
self.paint_theme_sample() |
|
|
|
def var_changed_theme_source(self, *params): |
|
"""Process toggle between builtin and custom theme. |
|
|
|
Update the default toggle value and apply the newly |
|
selected theme type. |
|
""" |
|
value = self.theme_source.get() |
|
changes.add_option('main', 'Theme', 'default', value) |
|
if value: |
|
self.var_changed_builtin_name() |
|
else: |
|
self.var_changed_custom_name() |
|
|
|
def var_changed_color(self, *params): |
|
"Process change to color choice." |
|
self.on_new_color_set() |
|
|
|
def var_changed_highlight_target(self, *params): |
|
"Process selection of new target tag for highlighting." |
|
self.set_highlight_target() |
|
|
|
def set_theme_type(self): |
|
"""Set available screen options based on builtin or custom theme. |
|
|
|
Attributes accessed: |
|
theme_source |
|
|
|
Attributes updated: |
|
builtinlist |
|
customlist |
|
button_delete_custom |
|
custom_theme_on |
|
|
|
Called from: |
|
handler for builtin_theme_on and custom_theme_on |
|
delete_custom |
|
create_new |
|
load_theme_cfg |
|
""" |
|
if self.theme_source.get(): |
|
self.builtinlist['state'] = 'normal' |
|
self.customlist['state'] = 'disabled' |
|
self.button_delete_custom.state(('disabled',)) |
|
else: |
|
self.builtinlist['state'] = 'disabled' |
|
self.custom_theme_on.state(('!disabled',)) |
|
self.customlist['state'] = 'normal' |
|
self.button_delete_custom.state(('!disabled',)) |
|
|
|
def get_color(self): |
|
"""Handle button to select a new color for the target tag. |
|
|
|
If a new color is selected while using a builtin theme, a |
|
name must be supplied to create a custom theme. |
|
|
|
Attributes accessed: |
|
highlight_target |
|
frame_color_set |
|
theme_source |
|
|
|
Attributes updated: |
|
color |
|
|
|
Methods: |
|
get_new_theme_name |
|
create_new |
|
""" |
|
target = self.highlight_target.get() |
|
prev_color = self.style.lookup(self.frame_color_set['style'], |
|
'background') |
|
rgbTuplet, color_string = colorchooser.askcolor( |
|
parent=self, title='Pick new color for : '+target, |
|
initialcolor=prev_color) |
|
if color_string and (color_string != prev_color): |
|
|
|
if self.theme_source.get(): |
|
message = ('Your changes will be saved as a new Custom Theme. ' |
|
'Enter a name for your new Custom Theme below.') |
|
new_theme = self.get_new_theme_name(message) |
|
if not new_theme: |
|
return |
|
else: |
|
self.create_new(new_theme) |
|
self.color.set(color_string) |
|
else: |
|
self.color.set(color_string) |
|
|
|
def on_new_color_set(self): |
|
"Display sample of new color selection on the dialog." |
|
new_color = self.color.get() |
|
self.style.configure('frame_color_set.TFrame', background=new_color) |
|
plane = 'foreground' if self.fg_bg_toggle.get() else 'background' |
|
sample_element = self.theme_elements[self.highlight_target.get()][0] |
|
self.highlight_sample.tag_config(sample_element, **{plane: new_color}) |
|
theme = self.custom_name.get() |
|
theme_element = sample_element + '-' + plane |
|
changes.add_option('highlight', theme, theme_element, new_color) |
|
|
|
def get_new_theme_name(self, message): |
|
"Return name of new theme from query popup." |
|
used_names = (idleConf.GetSectionList('user', 'highlight') + |
|
idleConf.GetSectionList('default', 'highlight')) |
|
new_theme = SectionName( |
|
self, 'New Custom Theme', message, used_names).result |
|
return new_theme |
|
|
|
def save_as_new_theme(self): |
|
"""Prompt for new theme name and create the theme. |
|
|
|
Methods: |
|
get_new_theme_name |
|
create_new |
|
""" |
|
new_theme_name = self.get_new_theme_name('New Theme Name:') |
|
if new_theme_name: |
|
self.create_new(new_theme_name) |
|
|
|
def create_new(self, new_theme_name): |
|
"""Create a new custom theme with the given name. |
|
|
|
Create the new theme based on the previously active theme |
|
with the current changes applied. Once it is saved, then |
|
activate the new theme. |
|
|
|
Attributes accessed: |
|
builtin_name |
|
custom_name |
|
|
|
Attributes updated: |
|
customlist |
|
theme_source |
|
|
|
Method: |
|
save_new |
|
set_theme_type |
|
""" |
|
if self.theme_source.get(): |
|
theme_type = 'default' |
|
theme_name = self.builtin_name.get() |
|
else: |
|
theme_type = 'user' |
|
theme_name = self.custom_name.get() |
|
new_theme = idleConf.GetThemeDict(theme_type, theme_name) |
|
|
|
if theme_name in changes['highlight']: |
|
theme_changes = changes['highlight'][theme_name] |
|
for element in theme_changes: |
|
new_theme[element] = theme_changes[element] |
|
|
|
self.save_new(new_theme_name, new_theme) |
|
|
|
custom_theme_list = idleConf.GetSectionList('user', 'highlight') |
|
custom_theme_list.sort() |
|
self.customlist.SetMenu(custom_theme_list, new_theme_name) |
|
self.theme_source.set(0) |
|
self.set_theme_type() |
|
|
|
def set_highlight_target(self): |
|
"""Set fg/bg toggle and color based on highlight tag target. |
|
|
|
Instance variables accessed: |
|
highlight_target |
|
|
|
Attributes updated: |
|
fg_on |
|
bg_on |
|
fg_bg_toggle |
|
|
|
Methods: |
|
set_color_sample |
|
|
|
Called from: |
|
var_changed_highlight_target |
|
load_theme_cfg |
|
""" |
|
if self.highlight_target.get() == 'Cursor': |
|
self.fg_on.state(('disabled',)) |
|
self.bg_on.state(('disabled',)) |
|
self.fg_bg_toggle.set(1) |
|
else: |
|
self.fg_on.state(('!disabled',)) |
|
self.bg_on.state(('!disabled',)) |
|
self.fg_bg_toggle.set(1) |
|
self.set_color_sample() |
|
|
|
def set_color_sample_binding(self, *args): |
|
"""Change color sample based on foreground/background toggle. |
|
|
|
Methods: |
|
set_color_sample |
|
""" |
|
self.set_color_sample() |
|
|
|
def set_color_sample(self): |
|
"""Set the color of the frame background to reflect the selected target. |
|
|
|
Instance variables accessed: |
|
theme_elements |
|
highlight_target |
|
fg_bg_toggle |
|
highlight_sample |
|
|
|
Attributes updated: |
|
frame_color_set |
|
""" |
|
|
|
tag = self.theme_elements[self.highlight_target.get()][0] |
|
plane = 'foreground' if self.fg_bg_toggle.get() else 'background' |
|
color = self.highlight_sample.tag_cget(tag, plane) |
|
self.style.configure('frame_color_set.TFrame', background=color) |
|
|
|
def paint_theme_sample(self): |
|
"""Apply the theme colors to each element tag in the sample text. |
|
|
|
Instance attributes accessed: |
|
theme_elements |
|
theme_source |
|
builtin_name |
|
custom_name |
|
|
|
Attributes updated: |
|
highlight_sample: Set the tag elements to the theme. |
|
|
|
Methods: |
|
set_color_sample |
|
|
|
Called from: |
|
var_changed_builtin_name |
|
var_changed_custom_name |
|
load_theme_cfg |
|
""" |
|
if self.theme_source.get(): |
|
theme = self.builtin_name.get() |
|
else: |
|
theme = self.custom_name.get() |
|
for element_title in self.theme_elements: |
|
element = self.theme_elements[element_title][0] |
|
colors = idleConf.GetHighlight(theme, element) |
|
if element == 'cursor': |
|
colors['background'] = idleConf.GetHighlight( |
|
theme, 'normal')['background'] |
|
|
|
if theme in changes['highlight']: |
|
theme_dict = changes['highlight'][theme] |
|
if element + '-foreground' in theme_dict: |
|
colors['foreground'] = theme_dict[element + '-foreground'] |
|
if element + '-background' in theme_dict: |
|
colors['background'] = theme_dict[element + '-background'] |
|
self.highlight_sample.tag_config(element, **colors) |
|
self.set_color_sample() |
|
|
|
def save_new(self, theme_name, theme): |
|
"""Save a newly created theme to idleConf. |
|
|
|
theme_name - string, the name of the new theme |
|
theme - dictionary containing the new theme |
|
""" |
|
idleConf.userCfg['highlight'].AddSection(theme_name) |
|
for element in theme: |
|
value = theme[element] |
|
idleConf.userCfg['highlight'].SetOption(theme_name, element, value) |
|
|
|
def askyesno(self, *args, **kwargs): |
|
|
|
return messagebox.askyesno(*args, **kwargs) |
|
|
|
def delete_custom(self): |
|
"""Handle event to delete custom theme. |
|
|
|
The current theme is deactivated and the default theme is |
|
activated. The custom theme is permanently removed from |
|
the config file. |
|
|
|
Attributes accessed: |
|
custom_name |
|
|
|
Attributes updated: |
|
custom_theme_on |
|
customlist |
|
theme_source |
|
builtin_name |
|
|
|
Methods: |
|
deactivate_current_config |
|
save_all_changed_extensions |
|
activate_config_changes |
|
set_theme_type |
|
""" |
|
theme_name = self.custom_name.get() |
|
delmsg = 'Are you sure you wish to delete the theme %r ?' |
|
if not self.askyesno( |
|
'Delete Theme', delmsg % theme_name, parent=self): |
|
return |
|
self.cd.deactivate_current_config() |
|
|
|
changes.delete_section('highlight', theme_name) |
|
|
|
item_list = idleConf.GetSectionList('user', 'highlight') |
|
item_list.sort() |
|
if not item_list: |
|
self.custom_theme_on.state(('disabled',)) |
|
self.customlist.SetMenu(item_list, '- no custom themes -') |
|
else: |
|
self.customlist.SetMenu(item_list, item_list[0]) |
|
|
|
self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default')) |
|
self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name')) |
|
|
|
changes.save_all() |
|
self.extpage.save_all_changed_extensions() |
|
self.cd.activate_config_changes() |
|
self.set_theme_type() |
|
|
|
|
|
class KeysPage(Frame): |
|
|
|
def __init__(self, master, extpage): |
|
super().__init__(master) |
|
self.extpage = extpage |
|
self.cd = master.winfo_toplevel() |
|
self.create_page_keys() |
|
self.load_key_cfg() |
|
|
|
def create_page_keys(self): |
|
"""Return frame of widgets for Keys tab. |
|
|
|
Enable users to provisionally change both individual and sets of |
|
keybindings (shortcut keys). Except for features implemented as |
|
extensions, keybindings are stored in complete sets called |
|
keysets. Built-in keysets in idlelib/config-keys.def are fixed |
|
as far as the dialog is concerned. Any keyset can be used as the |
|
base for a new custom keyset, stored in .idlerc/config-keys.cfg. |
|
|
|
Function load_key_cfg() initializes tk variables and keyset |
|
lists and calls load_keys_list for the current keyset. |
|
Radiobuttons builtin_keyset_on and custom_keyset_on toggle var |
|
keyset_source, which controls if the current set of keybindings |
|
are from a builtin or custom keyset. DynOptionMenus builtinlist |
|
and customlist contain lists of the builtin and custom keysets, |
|
respectively, and the current item from each list is stored in |
|
vars builtin_name and custom_name. |
|
|
|
Button delete_custom_keys invokes delete_custom_keys() to delete |
|
a custom keyset from idleConf.userCfg['keys'] and changes. Button |
|
save_custom_keys invokes save_as_new_key_set() which calls |
|
get_new_keys_name() and create_new_key_set() to save a custom keyset |
|
and its keybindings to idleConf.userCfg['keys']. |
|
|
|
Listbox bindingslist contains all of the keybindings for the |
|
selected keyset. The keybindings are loaded in load_keys_list() |
|
and are pairs of (event, [keys]) where keys can be a list |
|
of one or more key combinations to bind to the same event. |
|
Mouse button 1 click invokes on_bindingslist_select(), which |
|
allows button_new_keys to be clicked. |
|
|
|
So, an item is selected in listbindings, which activates |
|
button_new_keys, and clicking button_new_keys calls function |
|
get_new_keys(). Function get_new_keys() gets the key mappings from the |
|
current keyset for the binding event item that was selected. The |
|
function then displays another dialog, GetKeysDialog, with the |
|
selected binding event and current keys and allows new key sequences |
|
to be entered for that binding event. If the keys aren't |
|
changed, nothing happens. If the keys are changed and the keyset |
|
is a builtin, function get_new_keys_name() will be called |
|
for input of a custom keyset name. If no name is given, then the |
|
change to the keybinding will abort and no updates will be made. If |
|
a custom name is entered in the prompt or if the current keyset was |
|
already custom (and thus didn't require a prompt), then |
|
idleConf.userCfg['keys'] is updated in function create_new_key_set() |
|
with the change to the event binding. The item listing in bindingslist |
|
is updated with the new keys. Var keybinding is also set which invokes |
|
the callback function, var_changed_keybinding, to add the change to |
|
the 'keys' or 'extensions' changes tracker based on the binding type. |
|
|
|
Tk Variables: |
|
keybinding: Action/key bindings. |
|
|
|
Methods: |
|
load_keys_list: Reload active set. |
|
create_new_key_set: Combine active keyset and changes. |
|
set_keys_type: Command for keyset_source. |
|
save_new_key_set: Save to idleConf.userCfg['keys'] (is function). |
|
deactivate_current_config: Remove keys bindings in editors. |
|
|
|
Widgets for KeysPage(frame): (*) widgets bound to self |
|
frame_key_sets: LabelFrame |
|
frames[0]: Frame |
|
(*)builtin_keyset_on: Radiobutton - var keyset_source |
|
(*)custom_keyset_on: Radiobutton - var keyset_source |
|
(*)builtinlist: DynOptionMenu - var builtin_name, |
|
func keybinding_selected |
|
(*)customlist: DynOptionMenu - var custom_name, |
|
func keybinding_selected |
|
(*)keys_message: Label |
|
frames[1]: Frame |
|
(*)button_delete_custom_keys: Button - delete_custom_keys |
|
(*)button_save_custom_keys: Button - save_as_new_key_set |
|
frame_custom: LabelFrame |
|
frame_target: Frame |
|
target_title: Label |
|
scroll_target_y: Scrollbar |
|
scroll_target_x: Scrollbar |
|
(*)bindingslist: ListBox - on_bindingslist_select |
|
(*)button_new_keys: Button - get_new_keys & ..._name |
|
""" |
|
self.builtin_name = tracers.add( |
|
StringVar(self), self.var_changed_builtin_name) |
|
self.custom_name = tracers.add( |
|
StringVar(self), self.var_changed_custom_name) |
|
self.keyset_source = tracers.add( |
|
BooleanVar(self), self.var_changed_keyset_source) |
|
self.keybinding = tracers.add( |
|
StringVar(self), self.var_changed_keybinding) |
|
|
|
|
|
|
|
frame_custom = LabelFrame( |
|
self, borderwidth=2, relief=GROOVE, |
|
text=' Custom Key Bindings ') |
|
frame_key_sets = LabelFrame( |
|
self, borderwidth=2, relief=GROOVE, text=' Key Set ') |
|
|
|
frame_target = Frame(frame_custom) |
|
target_title = Label(frame_target, text='Action - Key(s)') |
|
scroll_target_y = Scrollbar(frame_target) |
|
scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) |
|
self.bindingslist = Listbox( |
|
frame_target, takefocus=FALSE, exportselection=FALSE) |
|
self.bindingslist.bind('<ButtonRelease-1>', |
|
self.on_bindingslist_select) |
|
scroll_target_y['command'] = self.bindingslist.yview |
|
scroll_target_x['command'] = self.bindingslist.xview |
|
self.bindingslist['yscrollcommand'] = scroll_target_y.set |
|
self.bindingslist['xscrollcommand'] = scroll_target_x.set |
|
self.button_new_keys = Button( |
|
frame_custom, text='Get New Keys for Selection', |
|
command=self.get_new_keys, state='disabled') |
|
|
|
frames = [Frame(frame_key_sets, padding=2, borderwidth=0) |
|
for i in range(2)] |
|
self.builtin_keyset_on = Radiobutton( |
|
frames[0], variable=self.keyset_source, value=1, |
|
command=self.set_keys_type, text='Use a Built-in Key Set') |
|
self.custom_keyset_on = Radiobutton( |
|
frames[0], variable=self.keyset_source, value=0, |
|
command=self.set_keys_type, text='Use a Custom Key Set') |
|
self.builtinlist = DynOptionMenu( |
|
frames[0], self.builtin_name, None, command=None) |
|
self.customlist = DynOptionMenu( |
|
frames[0], self.custom_name, None, command=None) |
|
self.button_delete_custom_keys = Button( |
|
frames[1], text='Delete Custom Key Set', |
|
command=self.delete_custom_keys) |
|
self.button_save_custom_keys = Button( |
|
frames[1], text='Save as New Custom Key Set', |
|
command=self.save_as_new_key_set) |
|
self.keys_message = Label(frames[0], borderwidth=2) |
|
|
|
|
|
|
|
frame_custom.pack(side=BOTTOM, padx=5, pady=5, expand=TRUE, fill=BOTH) |
|
frame_key_sets.pack(side=BOTTOM, padx=5, pady=5, fill=BOTH) |
|
|
|
self.button_new_keys.pack(side=BOTTOM, fill=X, padx=5, pady=5) |
|
frame_target.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) |
|
|
|
frame_target.columnconfigure(0, weight=1) |
|
frame_target.rowconfigure(1, weight=1) |
|
target_title.grid(row=0, column=0, columnspan=2, sticky=W) |
|
self.bindingslist.grid(row=1, column=0, sticky=NSEW) |
|
scroll_target_y.grid(row=1, column=1, sticky=NS) |
|
scroll_target_x.grid(row=2, column=0, sticky=EW) |
|
|
|
self.builtin_keyset_on.grid(row=0, column=0, sticky=W+NS) |
|
self.custom_keyset_on.grid(row=1, column=0, sticky=W+NS) |
|
self.builtinlist.grid(row=0, column=1, sticky=NSEW) |
|
self.customlist.grid(row=1, column=1, sticky=NSEW) |
|
self.keys_message.grid(row=0, column=2, sticky=NSEW, padx=5, pady=5) |
|
self.button_delete_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) |
|
self.button_save_custom_keys.pack(side=LEFT, fill=X, expand=True, padx=2) |
|
frames[0].pack(side=TOP, fill=BOTH, expand=True) |
|
frames[1].pack(side=TOP, fill=X, expand=True, pady=2) |
|
|
|
def load_key_cfg(self): |
|
"Load current configuration settings for the keybinding options." |
|
|
|
self.keyset_source.set(idleConf.GetOption( |
|
'main', 'Keys', 'default', type='bool', default=1)) |
|
|
|
current_option = idleConf.CurrentKeys() |
|
|
|
if self.keyset_source.get(): |
|
item_list = idleConf.GetSectionList('default', 'keys') |
|
item_list.sort() |
|
self.builtinlist.SetMenu(item_list, current_option) |
|
item_list = idleConf.GetSectionList('user', 'keys') |
|
item_list.sort() |
|
if not item_list: |
|
self.custom_keyset_on.state(('disabled',)) |
|
self.custom_name.set('- no custom keys -') |
|
else: |
|
self.customlist.SetMenu(item_list, item_list[0]) |
|
else: |
|
item_list = idleConf.GetSectionList('user', 'keys') |
|
item_list.sort() |
|
self.customlist.SetMenu(item_list, current_option) |
|
item_list = idleConf.GetSectionList('default', 'keys') |
|
item_list.sort() |
|
self.builtinlist.SetMenu(item_list, idleConf.default_keys()) |
|
self.set_keys_type() |
|
|
|
keyset_name = idleConf.CurrentKeys() |
|
self.load_keys_list(keyset_name) |
|
|
|
def var_changed_builtin_name(self, *params): |
|
"Process selection of builtin key set." |
|
old_keys = ( |
|
'IDLE Classic Windows', |
|
'IDLE Classic Unix', |
|
'IDLE Classic Mac', |
|
'IDLE Classic OSX', |
|
) |
|
value = self.builtin_name.get() |
|
if value not in old_keys: |
|
if idleConf.GetOption('main', 'Keys', 'name') not in old_keys: |
|
changes.add_option('main', 'Keys', 'name', old_keys[0]) |
|
changes.add_option('main', 'Keys', 'name2', value) |
|
self.keys_message['text'] = 'New key set, see Help' |
|
else: |
|
changes.add_option('main', 'Keys', 'name', value) |
|
changes.add_option('main', 'Keys', 'name2', '') |
|
self.keys_message['text'] = '' |
|
self.load_keys_list(value) |
|
|
|
def var_changed_custom_name(self, *params): |
|
"Process selection of custom key set." |
|
value = self.custom_name.get() |
|
if value != '- no custom keys -': |
|
changes.add_option('main', 'Keys', 'name', value) |
|
self.load_keys_list(value) |
|
|
|
def var_changed_keyset_source(self, *params): |
|
"Process toggle between builtin key set and custom key set." |
|
value = self.keyset_source.get() |
|
changes.add_option('main', 'Keys', 'default', value) |
|
if value: |
|
self.var_changed_builtin_name() |
|
else: |
|
self.var_changed_custom_name() |
|
|
|
def var_changed_keybinding(self, *params): |
|
"Store change to a keybinding." |
|
value = self.keybinding.get() |
|
key_set = self.custom_name.get() |
|
event = self.bindingslist.get(ANCHOR).split()[0] |
|
if idleConf.IsCoreBinding(event): |
|
changes.add_option('keys', key_set, event, value) |
|
else: |
|
ext_name = idleConf.GetExtnNameForEvent(event) |
|
ext_keybind_section = ext_name + '_cfgBindings' |
|
changes.add_option('extensions', ext_keybind_section, event, value) |
|
|
|
def set_keys_type(self): |
|
"Set available screen options based on builtin or custom key set." |
|
if self.keyset_source.get(): |
|
self.builtinlist['state'] = 'normal' |
|
self.customlist['state'] = 'disabled' |
|
self.button_delete_custom_keys.state(('disabled',)) |
|
else: |
|
self.builtinlist['state'] = 'disabled' |
|
self.custom_keyset_on.state(('!disabled',)) |
|
self.customlist['state'] = 'normal' |
|
self.button_delete_custom_keys.state(('!disabled',)) |
|
|
|
def get_new_keys(self): |
|
"""Handle event to change key binding for selected line. |
|
|
|
A selection of a key/binding in the list of current |
|
bindings pops up a dialog to enter a new binding. If |
|
the current key set is builtin and a binding has |
|
changed, then a name for a custom key set needs to be |
|
entered for the change to be applied. |
|
""" |
|
list_index = self.bindingslist.index(ANCHOR) |
|
binding = self.bindingslist.get(list_index) |
|
bind_name = binding.split()[0] |
|
if self.keyset_source.get(): |
|
current_key_set_name = self.builtin_name.get() |
|
else: |
|
current_key_set_name = self.custom_name.get() |
|
current_bindings = idleConf.GetCurrentKeySet() |
|
if current_key_set_name in changes['keys']: |
|
key_set_changes = changes['keys'][current_key_set_name] |
|
for event in key_set_changes: |
|
current_bindings[event] = key_set_changes[event].split() |
|
current_key_sequences = list(current_bindings.values()) |
|
new_keys = GetKeysWindow(self, 'Get New Keys', bind_name, |
|
current_key_sequences).result |
|
if new_keys: |
|
if self.keyset_source.get(): |
|
message = ('Your changes will be saved as a new Custom Key Set.' |
|
' Enter a name for your new Custom Key Set below.') |
|
new_keyset = self.get_new_keys_name(message) |
|
if not new_keyset: |
|
self.bindingslist.select_set(list_index) |
|
self.bindingslist.select_anchor(list_index) |
|
return |
|
else: |
|
self.create_new_key_set(new_keyset) |
|
self.bindingslist.delete(list_index) |
|
self.bindingslist.insert(list_index, bind_name+' - '+new_keys) |
|
self.bindingslist.select_set(list_index) |
|
self.bindingslist.select_anchor(list_index) |
|
self.keybinding.set(new_keys) |
|
else: |
|
self.bindingslist.select_set(list_index) |
|
self.bindingslist.select_anchor(list_index) |
|
|
|
def get_new_keys_name(self, message): |
|
"Return new key set name from query popup." |
|
used_names = (idleConf.GetSectionList('user', 'keys') + |
|
idleConf.GetSectionList('default', 'keys')) |
|
new_keyset = SectionName( |
|
self, 'New Custom Key Set', message, used_names).result |
|
return new_keyset |
|
|
|
def save_as_new_key_set(self): |
|
"Prompt for name of new key set and save changes using that name." |
|
new_keys_name = self.get_new_keys_name('New Key Set Name:') |
|
if new_keys_name: |
|
self.create_new_key_set(new_keys_name) |
|
|
|
def on_bindingslist_select(self, event): |
|
"Activate button to assign new keys to selected action." |
|
self.button_new_keys.state(('!disabled',)) |
|
|
|
def create_new_key_set(self, new_key_set_name): |
|
"""Create a new custom key set with the given name. |
|
|
|
Copy the bindings/keys from the previously active keyset |
|
to the new keyset and activate the new custom keyset. |
|
""" |
|
if self.keyset_source.get(): |
|
prev_key_set_name = self.builtin_name.get() |
|
else: |
|
prev_key_set_name = self.custom_name.get() |
|
prev_keys = idleConf.GetCoreKeys(prev_key_set_name) |
|
new_keys = {} |
|
for event in prev_keys: |
|
event_name = event[2:-2] |
|
binding = ' '.join(prev_keys[event]) |
|
new_keys[event_name] = binding |
|
|
|
if prev_key_set_name in changes['keys']: |
|
key_set_changes = changes['keys'][prev_key_set_name] |
|
for event in key_set_changes: |
|
new_keys[event] = key_set_changes[event] |
|
|
|
self.save_new_key_set(new_key_set_name, new_keys) |
|
|
|
custom_key_list = idleConf.GetSectionList('user', 'keys') |
|
custom_key_list.sort() |
|
self.customlist.SetMenu(custom_key_list, new_key_set_name) |
|
self.keyset_source.set(0) |
|
self.set_keys_type() |
|
|
|
def load_keys_list(self, keyset_name): |
|
"""Reload the list of action/key binding pairs for the active key set. |
|
|
|
An action/key binding can be selected to change the key binding. |
|
""" |
|
reselect = False |
|
if self.bindingslist.curselection(): |
|
reselect = True |
|
list_index = self.bindingslist.index(ANCHOR) |
|
keyset = idleConf.GetKeySet(keyset_name) |
|
bind_names = list(keyset.keys()) |
|
bind_names.sort() |
|
self.bindingslist.delete(0, END) |
|
for bind_name in bind_names: |
|
key = ' '.join(keyset[bind_name]) |
|
bind_name = bind_name[2:-2] |
|
if keyset_name in changes['keys']: |
|
|
|
if bind_name in changes['keys'][keyset_name]: |
|
key = changes['keys'][keyset_name][bind_name] |
|
self.bindingslist.insert(END, bind_name+' - '+key) |
|
if reselect: |
|
self.bindingslist.see(list_index) |
|
self.bindingslist.select_set(list_index) |
|
self.bindingslist.select_anchor(list_index) |
|
|
|
@staticmethod |
|
def save_new_key_set(keyset_name, keyset): |
|
"""Save a newly created core key set. |
|
|
|
Add keyset to idleConf.userCfg['keys'], not to disk. |
|
If the keyset doesn't exist, it is created. The |
|
binding/keys are taken from the keyset argument. |
|
|
|
keyset_name - string, the name of the new key set |
|
keyset - dictionary containing the new keybindings |
|
""" |
|
idleConf.userCfg['keys'].AddSection(keyset_name) |
|
for event in keyset: |
|
value = keyset[event] |
|
idleConf.userCfg['keys'].SetOption(keyset_name, event, value) |
|
|
|
def askyesno(self, *args, **kwargs): |
|
|
|
return messagebox.askyesno(*args, **kwargs) |
|
|
|
def delete_custom_keys(self): |
|
"""Handle event to delete a custom key set. |
|
|
|
Applying the delete deactivates the current configuration and |
|
reverts to the default. The custom key set is permanently |
|
deleted from the config file. |
|
""" |
|
keyset_name = self.custom_name.get() |
|
delmsg = 'Are you sure you wish to delete the key set %r ?' |
|
if not self.askyesno( |
|
'Delete Key Set', delmsg % keyset_name, parent=self): |
|
return |
|
self.cd.deactivate_current_config() |
|
|
|
changes.delete_section('keys', keyset_name) |
|
|
|
item_list = idleConf.GetSectionList('user', 'keys') |
|
item_list.sort() |
|
if not item_list: |
|
self.custom_keyset_on.state(('disabled',)) |
|
self.customlist.SetMenu(item_list, '- no custom keys -') |
|
else: |
|
self.customlist.SetMenu(item_list, item_list[0]) |
|
|
|
self.keyset_source.set(idleConf.defaultCfg['main'] |
|
.Get('Keys', 'default')) |
|
self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name') |
|
or idleConf.default_keys()) |
|
|
|
changes.save_all() |
|
self.extpage.save_all_changed_extensions() |
|
self.cd.activate_config_changes() |
|
self.set_keys_type() |
|
|
|
|
|
class WinPage(Frame): |
|
|
|
def __init__(self, master): |
|
super().__init__(master) |
|
|
|
self.init_validators() |
|
self.create_page_windows() |
|
self.load_windows_cfg() |
|
|
|
def init_validators(self): |
|
digits_or_empty_re = re.compile(r'[0-9]*') |
|
def is_digits_or_empty(s): |
|
"Return 's is blank or contains only digits'" |
|
return digits_or_empty_re.fullmatch(s) is not None |
|
self.digits_only = (self.register(is_digits_or_empty), '%P',) |
|
|
|
def create_page_windows(self): |
|
"""Return frame of widgets for Windows tab. |
|
|
|
Enable users to provisionally change general window options. |
|
Function load_windows_cfg initializes tk variable idleConf. |
|
Radiobuttons startup_shell_on and startup_editor_on set var |
|
startup_edit. Entry boxes win_width_int and win_height_int set var |
|
win_width and win_height. Setting var_name invokes the default |
|
callback that adds option to changes. |
|
|
|
Widgets for WinPage(Frame): > vars, bound to self |
|
frame_window: LabelFrame |
|
frame_run: Frame |
|
startup_title: Label |
|
startup_editor_on: Radiobutton > startup_edit |
|
startup_shell_on: Radiobutton > startup_edit |
|
frame_win_size: Frame |
|
win_size_title: Label |
|
win_width_title: Label |
|
win_width_int: Entry > win_width |
|
win_height_title: Label |
|
win_height_int: Entry > win_height |
|
frame_cursor: Frame |
|
indent_title: Label |
|
indent_chooser: Spinbox (Combobox < 8.5.9) > indent_spaces |
|
blink_on: Checkbutton > cursor_blink |
|
frame_autocomplete: Frame |
|
auto_wait_title: Label |
|
auto_wait_int: Entry > autocomplete_wait |
|
frame_paren1: Frame |
|
paren_style_title: Label |
|
paren_style_type: OptionMenu > paren_style |
|
frame_paren2: Frame |
|
paren_time_title: Label |
|
paren_flash_time: Entry > flash_delay |
|
bell_on: Checkbutton > paren_bell |
|
frame_format: Frame |
|
format_width_title: Label |
|
format_width_int: Entry > format_width |
|
""" |
|
|
|
self.startup_edit = tracers.add( |
|
IntVar(self), ('main', 'General', 'editor-on-startup')) |
|
self.win_width = tracers.add( |
|
StringVar(self), ('main', 'EditorWindow', 'width')) |
|
self.win_height = tracers.add( |
|
StringVar(self), ('main', 'EditorWindow', 'height')) |
|
self.indent_spaces = tracers.add( |
|
StringVar(self), ('main', 'Indent', 'num-spaces')) |
|
self.cursor_blink = tracers.add( |
|
BooleanVar(self), ('main', 'EditorWindow', 'cursor-blink')) |
|
self.autocomplete_wait = tracers.add( |
|
StringVar(self), ('extensions', 'AutoComplete', 'popupwait')) |
|
self.paren_style = tracers.add( |
|
StringVar(self), ('extensions', 'ParenMatch', 'style')) |
|
self.flash_delay = tracers.add( |
|
StringVar(self), ('extensions', 'ParenMatch', 'flash-delay')) |
|
self.paren_bell = tracers.add( |
|
BooleanVar(self), ('extensions', 'ParenMatch', 'bell')) |
|
self.format_width = tracers.add( |
|
StringVar(self), ('extensions', 'FormatParagraph', 'max-width')) |
|
|
|
|
|
frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE, |
|
text=' Window Preferences') |
|
|
|
frame_run = Frame(frame_window, borderwidth=0) |
|
startup_title = Label(frame_run, text='At Startup') |
|
self.startup_editor_on = Radiobutton( |
|
frame_run, variable=self.startup_edit, value=1, |
|
text="Open Edit Window") |
|
self.startup_shell_on = Radiobutton( |
|
frame_run, variable=self.startup_edit, value=0, |
|
text='Open Shell Window') |
|
|
|
frame_win_size = Frame(frame_window, borderwidth=0) |
|
win_size_title = Label( |
|
frame_win_size, text='Initial Window Size (in characters)') |
|
win_width_title = Label(frame_win_size, text='Width') |
|
self.win_width_int = Entry( |
|
frame_win_size, textvariable=self.win_width, width=3, |
|
validatecommand=self.digits_only, validate='key', |
|
) |
|
win_height_title = Label(frame_win_size, text='Height') |
|
self.win_height_int = Entry( |
|
frame_win_size, textvariable=self.win_height, width=3, |
|
validatecommand=self.digits_only, validate='key', |
|
) |
|
|
|
frame_cursor = Frame(frame_window, borderwidth=0) |
|
indent_title = Label(frame_cursor, |
|
text='Indent spaces (4 is standard)') |
|
try: |
|
self.indent_chooser = Spinbox( |
|
frame_cursor, textvariable=self.indent_spaces, |
|
from_=1, to=10, width=2, |
|
validatecommand=self.digits_only, validate='key') |
|
except TclError: |
|
self.indent_chooser = Combobox( |
|
frame_cursor, textvariable=self.indent_spaces, |
|
state="readonly", values=list(range(1,11)), width=3) |
|
cursor_blink_title = Label(frame_cursor, text='Cursor Blink') |
|
self.cursor_blink_bool = Checkbutton(frame_cursor, text="Cursor blink", |
|
variable=self.cursor_blink) |
|
|
|
frame_autocomplete = Frame(frame_window, borderwidth=0,) |
|
auto_wait_title = Label(frame_autocomplete, |
|
text='Completions Popup Wait (milliseconds)') |
|
self.auto_wait_int = Entry( |
|
frame_autocomplete, textvariable=self.autocomplete_wait, |
|
width=6, validatecommand=self.digits_only, validate='key') |
|
|
|
frame_paren1 = Frame(frame_window, borderwidth=0) |
|
paren_style_title = Label(frame_paren1, text='Paren Match Style') |
|
self.paren_style_type = OptionMenu( |
|
frame_paren1, self.paren_style, 'expression', |
|
"opener","parens","expression") |
|
frame_paren2 = Frame(frame_window, borderwidth=0) |
|
paren_time_title = Label( |
|
frame_paren2, text='Time Match Displayed (milliseconds)\n' |
|
'(0 is until next input)') |
|
self.paren_flash_time = Entry( |
|
frame_paren2, textvariable=self.flash_delay, width=6, |
|
validatecommand=self.digits_only, validate='key') |
|
self.bell_on = Checkbutton( |
|
frame_paren2, text="Bell on Mismatch", variable=self.paren_bell) |
|
frame_format = Frame(frame_window, borderwidth=0) |
|
format_width_title = Label(frame_format, |
|
text='Format Paragraph Max Width') |
|
self.format_width_int = Entry( |
|
frame_format, textvariable=self.format_width, width=4, |
|
validatecommand=self.digits_only, validate='key', |
|
) |
|
|
|
|
|
frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) |
|
|
|
frame_run.pack(side=TOP, padx=5, pady=0, fill=X) |
|
startup_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
|
self.startup_shell_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) |
|
self.startup_editor_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) |
|
|
|
frame_win_size.pack(side=TOP, padx=5, pady=0, fill=X) |
|
win_size_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
|
self.win_height_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) |
|
win_height_title.pack(side=RIGHT, anchor=E, pady=5) |
|
self.win_width_int.pack(side=RIGHT, anchor=E, padx=10, pady=5) |
|
win_width_title.pack(side=RIGHT, anchor=E, pady=5) |
|
|
|
frame_cursor.pack(side=TOP, padx=5, pady=0, fill=X) |
|
indent_title.pack(side=LEFT, anchor=W, padx=5) |
|
self.indent_chooser.pack(side=LEFT, anchor=W, padx=10) |
|
self.cursor_blink_bool.pack(side=RIGHT, anchor=E, padx=15, pady=5) |
|
|
|
frame_autocomplete.pack(side=TOP, padx=5, pady=0, fill=X) |
|
auto_wait_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
|
self.auto_wait_int.pack(side=TOP, padx=10, pady=5) |
|
|
|
frame_paren1.pack(side=TOP, padx=5, pady=0, fill=X) |
|
paren_style_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
|
self.paren_style_type.pack(side=TOP, padx=10, pady=5) |
|
frame_paren2.pack(side=TOP, padx=5, pady=0, fill=X) |
|
paren_time_title.pack(side=LEFT, anchor=W, padx=5) |
|
self.bell_on.pack(side=RIGHT, anchor=E, padx=15, pady=5) |
|
self.paren_flash_time.pack(side=TOP, anchor=W, padx=15, pady=5) |
|
|
|
frame_format.pack(side=TOP, padx=5, pady=0, fill=X) |
|
format_width_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
|
self.format_width_int.pack(side=TOP, padx=10, pady=5) |
|
|
|
def load_windows_cfg(self): |
|
|
|
self.startup_edit.set(idleConf.GetOption( |
|
'main', 'General', 'editor-on-startup', type='bool')) |
|
self.win_width.set(idleConf.GetOption( |
|
'main', 'EditorWindow', 'width', type='int')) |
|
self.win_height.set(idleConf.GetOption( |
|
'main', 'EditorWindow', 'height', type='int')) |
|
self.indent_spaces.set(idleConf.GetOption( |
|
'main', 'Indent', 'num-spaces', type='int')) |
|
self.cursor_blink.set(idleConf.GetOption( |
|
'main', 'EditorWindow', 'cursor-blink', type='bool')) |
|
self.autocomplete_wait.set(idleConf.GetOption( |
|
'extensions', 'AutoComplete', 'popupwait', type='int')) |
|
self.paren_style.set(idleConf.GetOption( |
|
'extensions', 'ParenMatch', 'style')) |
|
self.flash_delay.set(idleConf.GetOption( |
|
'extensions', 'ParenMatch', 'flash-delay', type='int')) |
|
self.paren_bell.set(idleConf.GetOption( |
|
'extensions', 'ParenMatch', 'bell')) |
|
self.format_width.set(idleConf.GetOption( |
|
'extensions', 'FormatParagraph', 'max-width', type='int')) |
|
|
|
|
|
class ShedPage(Frame): |
|
|
|
def __init__(self, master): |
|
super().__init__(master) |
|
|
|
self.init_validators() |
|
self.create_page_shed() |
|
self.load_shelled_cfg() |
|
|
|
def init_validators(self): |
|
digits_or_empty_re = re.compile(r'[0-9]*') |
|
def is_digits_or_empty(s): |
|
"Return 's is blank or contains only digits'" |
|
return digits_or_empty_re.fullmatch(s) is not None |
|
self.digits_only = (self.register(is_digits_or_empty), '%P',) |
|
|
|
def create_page_shed(self): |
|
"""Return frame of widgets for Shell/Ed tab. |
|
|
|
Enable users to provisionally change shell and editor options. |
|
Function load_shed_cfg initializes tk variables using idleConf. |
|
Entry box auto_squeeze_min_lines_int sets |
|
auto_squeeze_min_lines_int. Setting var_name invokes the |
|
default callback that adds option to changes. |
|
|
|
Widgets for ShedPage(Frame): (*) widgets bound to self |
|
frame_shell: LabelFrame |
|
frame_auto_squeeze_min_lines: Frame |
|
auto_squeeze_min_lines_title: Label |
|
(*)auto_squeeze_min_lines_int: Entry - |
|
auto_squeeze_min_lines |
|
frame_editor: LabelFrame |
|
frame_save: Frame |
|
run_save_title: Label |
|
(*)save_ask_on: Radiobutton - autosave |
|
(*)save_auto_on: Radiobutton - autosave |
|
frame_format: Frame |
|
format_width_title: Label |
|
(*)format_width_int: Entry - format_width |
|
frame_line_numbers_default: Frame |
|
line_numbers_default_title: Label |
|
(*)line_numbers_default_bool: Checkbutton - line_numbers_default |
|
frame_context: Frame |
|
context_title: Label |
|
(*)context_int: Entry - context_lines |
|
""" |
|
|
|
self.auto_squeeze_min_lines = tracers.add( |
|
StringVar(self), ('main', 'PyShell', 'auto-squeeze-min-lines')) |
|
|
|
self.autosave = tracers.add( |
|
IntVar(self), ('main', 'General', 'autosave')) |
|
self.line_numbers_default = tracers.add( |
|
BooleanVar(self), |
|
('main', 'EditorWindow', 'line-numbers-default')) |
|
self.context_lines = tracers.add( |
|
StringVar(self), ('extensions', 'CodeContext', 'maxlines')) |
|
|
|
|
|
frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE, |
|
text=' Shell Preferences') |
|
frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE, |
|
text=' Editor Preferences') |
|
|
|
frame_auto_squeeze_min_lines = Frame(frame_shell, borderwidth=0) |
|
auto_squeeze_min_lines_title = Label(frame_auto_squeeze_min_lines, |
|
text='Auto-Squeeze Min. Lines:') |
|
self.auto_squeeze_min_lines_int = Entry( |
|
frame_auto_squeeze_min_lines, width=4, |
|
textvariable=self.auto_squeeze_min_lines, |
|
validatecommand=self.digits_only, validate='key', |
|
) |
|
|
|
frame_save = Frame(frame_editor, borderwidth=0) |
|
run_save_title = Label(frame_save, text='At Start of Run (F5) ') |
|
|
|
self.save_ask_on = Radiobutton( |
|
frame_save, variable=self.autosave, value=0, |
|
text="Prompt to Save") |
|
self.save_auto_on = Radiobutton( |
|
frame_save, variable=self.autosave, value=1, |
|
text='No Prompt') |
|
|
|
frame_line_numbers_default = Frame(frame_editor, borderwidth=0) |
|
line_numbers_default_title = Label( |
|
frame_line_numbers_default, text='Show line numbers in new windows') |
|
self.line_numbers_default_bool = Checkbutton( |
|
frame_line_numbers_default, |
|
variable=self.line_numbers_default, |
|
width=1) |
|
|
|
frame_context = Frame(frame_editor, borderwidth=0) |
|
context_title = Label(frame_context, text='Max Context Lines :') |
|
self.context_int = Entry( |
|
frame_context, textvariable=self.context_lines, width=3, |
|
validatecommand=self.digits_only, validate='key', |
|
) |
|
|
|
|
|
frame_shell.pack(side=TOP, padx=5, pady=5, fill=BOTH) |
|
Label(self).pack() |
|
frame_editor.pack(side=TOP, padx=5, pady=5, fill=BOTH) |
|
|
|
frame_auto_squeeze_min_lines.pack(side=TOP, padx=5, pady=0, fill=X) |
|
auto_squeeze_min_lines_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
|
self.auto_squeeze_min_lines_int.pack(side=TOP, padx=5, pady=5) |
|
|
|
frame_save.pack(side=TOP, padx=5, pady=0, fill=X) |
|
run_save_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
|
self.save_auto_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) |
|
self.save_ask_on.pack(side=RIGHT, anchor=W, padx=5, pady=5) |
|
|
|
frame_line_numbers_default.pack(side=TOP, padx=5, pady=0, fill=X) |
|
line_numbers_default_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
|
self.line_numbers_default_bool.pack(side=LEFT, padx=5, pady=5) |
|
|
|
frame_context.pack(side=TOP, padx=5, pady=0, fill=X) |
|
context_title.pack(side=LEFT, anchor=W, padx=5, pady=5) |
|
self.context_int.pack(side=TOP, padx=5, pady=5) |
|
|
|
def load_shelled_cfg(self): |
|
|
|
self.auto_squeeze_min_lines.set(idleConf.GetOption( |
|
'main', 'PyShell', 'auto-squeeze-min-lines', type='int')) |
|
|
|
self.autosave.set(idleConf.GetOption( |
|
'main', 'General', 'autosave', default=0, type='bool')) |
|
self.line_numbers_default.set(idleConf.GetOption( |
|
'main', 'EditorWindow', 'line-numbers-default', type='bool')) |
|
self.context_lines.set(idleConf.GetOption( |
|
'extensions', 'CodeContext', 'maxlines', type='int')) |
|
|
|
|
|
class ExtPage(Frame): |
|
def __init__(self, master): |
|
super().__init__(master) |
|
self.ext_defaultCfg = idleConf.defaultCfg['extensions'] |
|
self.ext_userCfg = idleConf.userCfg['extensions'] |
|
self.is_int = self.register(is_int) |
|
self.load_extensions() |
|
self.create_page_extensions() |
|
|
|
def create_page_extensions(self): |
|
"""Configure IDLE feature extensions and help menu extensions. |
|
|
|
List the feature extensions and a configuration box for the |
|
selected extension. Help menu extensions are in a HelpFrame. |
|
|
|
This code reads the current configuration using idleConf, |
|
supplies a GUI interface to change the configuration values, |
|
and saves the changes using idleConf. |
|
|
|
Some changes may require restarting IDLE. This depends on each |
|
extension's implementation. |
|
|
|
All values are treated as text, and it is up to the user to |
|
supply reasonable values. The only exception to this are the |
|
'enable*' options, which are boolean, and can be toggled with a |
|
True/False button. |
|
|
|
Methods: |
|
extension_selected: Handle selection from list. |
|
create_extension_frame: Hold widgets for one extension. |
|
set_extension_value: Set in userCfg['extensions']. |
|
save_all_changed_extensions: Call extension page Save(). |
|
""" |
|
self.extension_names = StringVar(self) |
|
|
|
frame_ext = LabelFrame(self, borderwidth=2, relief=GROOVE, |
|
text=' Feature Extensions ') |
|
self.frame_help = HelpFrame(self, borderwidth=2, relief=GROOVE, |
|
text=' Help Menu Extensions ') |
|
|
|
frame_ext.rowconfigure(0, weight=1) |
|
frame_ext.columnconfigure(2, weight=1) |
|
self.extension_list = Listbox(frame_ext, listvariable=self.extension_names, |
|
selectmode='browse') |
|
self.extension_list.bind('<<ListboxSelect>>', self.extension_selected) |
|
scroll = Scrollbar(frame_ext, command=self.extension_list.yview) |
|
self.extension_list.yscrollcommand=scroll.set |
|
self.details_frame = LabelFrame(frame_ext, width=250, height=250) |
|
self.extension_list.grid(column=0, row=0, sticky='nws') |
|
scroll.grid(column=1, row=0, sticky='ns') |
|
self.details_frame.grid(column=2, row=0, sticky='nsew', padx=[10, 0]) |
|
frame_ext.configure(padding=10) |
|
self.config_frame = {} |
|
self.current_extension = None |
|
|
|
self.outerframe = self |
|
self.tabbed_page_set = self.extension_list |
|
|
|
|
|
ext_names = '' |
|
for ext_name in sorted(self.extensions): |
|
self.create_extension_frame(ext_name) |
|
ext_names = ext_names + '{' + ext_name + '} ' |
|
self.extension_names.set(ext_names) |
|
self.extension_list.selection_set(0) |
|
self.extension_selected(None) |
|
|
|
|
|
frame_ext.grid(row=0, column=0, sticky='nsew') |
|
Label(self).grid(row=1, column=0) |
|
self.frame_help.grid(row=2, column=0, sticky='sew') |
|
|
|
def load_extensions(self): |
|
"Fill self.extensions with data from the default and user configs." |
|
self.extensions = {} |
|
for ext_name in idleConf.GetExtensions(active_only=False): |
|
|
|
self.extensions[ext_name] = [] |
|
|
|
for ext_name in self.extensions: |
|
opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name)) |
|
|
|
|
|
enables = [opt_name for opt_name in opt_list |
|
if opt_name.startswith('enable')] |
|
for opt_name in enables: |
|
opt_list.remove(opt_name) |
|
opt_list = enables + opt_list |
|
|
|
for opt_name in opt_list: |
|
def_str = self.ext_defaultCfg.Get( |
|
ext_name, opt_name, raw=True) |
|
try: |
|
def_obj = {'True':True, 'False':False}[def_str] |
|
opt_type = 'bool' |
|
except KeyError: |
|
try: |
|
def_obj = int(def_str) |
|
opt_type = 'int' |
|
except ValueError: |
|
def_obj = def_str |
|
opt_type = None |
|
try: |
|
value = self.ext_userCfg.Get( |
|
ext_name, opt_name, type=opt_type, raw=True, |
|
default=def_obj) |
|
except ValueError: |
|
value = def_obj |
|
var = StringVar(self) |
|
var.set(str(value)) |
|
|
|
self.extensions[ext_name].append({'name': opt_name, |
|
'type': opt_type, |
|
'default': def_str, |
|
'value': value, |
|
'var': var, |
|
}) |
|
|
|
def extension_selected(self, event): |
|
"Handle selection of an extension from the list." |
|
newsel = self.extension_list.curselection() |
|
if newsel: |
|
newsel = self.extension_list.get(newsel) |
|
if newsel is None or newsel != self.current_extension: |
|
if self.current_extension: |
|
self.details_frame.config(text='') |
|
self.config_frame[self.current_extension].grid_forget() |
|
self.current_extension = None |
|
if newsel: |
|
self.details_frame.config(text=newsel) |
|
self.config_frame[newsel].grid(column=0, row=0, sticky='nsew') |
|
self.current_extension = newsel |
|
|
|
def create_extension_frame(self, ext_name): |
|
"""Create a frame holding the widgets to configure one extension""" |
|
f = VerticalScrolledFrame(self.details_frame, height=250, width=250) |
|
self.config_frame[ext_name] = f |
|
entry_area = f.interior |
|
|
|
for row, opt in enumerate(self.extensions[ext_name]): |
|
|
|
label = Label(entry_area, text=opt['name']) |
|
label.grid(row=row, column=0, sticky=NW) |
|
var = opt['var'] |
|
if opt['type'] == 'bool': |
|
Checkbutton(entry_area, variable=var, |
|
onvalue='True', offvalue='False', width=8 |
|
).grid(row=row, column=1, sticky=W, padx=7) |
|
elif opt['type'] == 'int': |
|
Entry(entry_area, textvariable=var, validate='key', |
|
validatecommand=(self.is_int, '%P'), width=10 |
|
).grid(row=row, column=1, sticky=NSEW, padx=7) |
|
|
|
else: |
|
|
|
Entry(entry_area, textvariable=var, width=15 |
|
).grid(row=row, column=1, sticky=NSEW, padx=7) |
|
return |
|
|
|
def set_extension_value(self, section, opt): |
|
"""Return True if the configuration was added or changed. |
|
|
|
If the value is the same as the default, then remove it |
|
from user config file. |
|
""" |
|
name = opt['name'] |
|
default = opt['default'] |
|
value = opt['var'].get().strip() or default |
|
opt['var'].set(value) |
|
|
|
|
|
if (value == default): |
|
return self.ext_userCfg.RemoveOption(section, name) |
|
|
|
return self.ext_userCfg.SetOption(section, name, value) |
|
|
|
def save_all_changed_extensions(self): |
|
"""Save configuration changes to the user config file. |
|
|
|
Attributes accessed: |
|
extensions |
|
|
|
Methods: |
|
set_extension_value |
|
""" |
|
has_changes = False |
|
for ext_name in self.extensions: |
|
options = self.extensions[ext_name] |
|
for opt in options: |
|
if self.set_extension_value(ext_name, opt): |
|
has_changes = True |
|
if has_changes: |
|
self.ext_userCfg.Save() |
|
|
|
|
|
class HelpFrame(LabelFrame): |
|
|
|
def __init__(self, master, **cfg): |
|
super().__init__(master, **cfg) |
|
self.create_frame_help() |
|
self.load_helplist() |
|
|
|
def create_frame_help(self): |
|
"""Create LabelFrame for additional help menu sources. |
|
|
|
load_helplist loads list user_helplist with |
|
name, position pairs and copies names to listbox helplist. |
|
Clicking a name invokes help_source selected. Clicking |
|
button_helplist_name invokes helplist_item_name, which also |
|
changes user_helplist. These functions all call |
|
set_add_delete_state. All but load call update_help_changes to |
|
rewrite changes['main']['HelpFiles']. |
|
|
|
Widgets for HelpFrame(LabelFrame): (*) widgets bound to self |
|
frame_helplist: Frame |
|
(*)helplist: ListBox |
|
scroll_helplist: Scrollbar |
|
frame_buttons: Frame |
|
(*)button_helplist_edit |
|
(*)button_helplist_add |
|
(*)button_helplist_remove |
|
""" |
|
|
|
frame_helplist = Frame(self) |
|
self.helplist = Listbox( |
|
frame_helplist, height=5, takefocus=True, |
|
exportselection=FALSE) |
|
scroll_helplist = Scrollbar(frame_helplist) |
|
scroll_helplist['command'] = self.helplist.yview |
|
self.helplist['yscrollcommand'] = scroll_helplist.set |
|
self.helplist.bind('<ButtonRelease-1>', self.help_source_selected) |
|
|
|
frame_buttons = Frame(self) |
|
self.button_helplist_edit = Button( |
|
frame_buttons, text='Edit', state='disabled', |
|
width=8, command=self.helplist_item_edit) |
|
self.button_helplist_add = Button( |
|
frame_buttons, text='Add', |
|
width=8, command=self.helplist_item_add) |
|
self.button_helplist_remove = Button( |
|
frame_buttons, text='Remove', state='disabled', |
|
width=8, command=self.helplist_item_remove) |
|
|
|
|
|
frame_helplist.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH) |
|
self.helplist.pack(side=LEFT, anchor=E, expand=TRUE, fill=BOTH) |
|
scroll_helplist.pack(side=RIGHT, anchor=W, fill=Y) |
|
frame_buttons.pack(side=RIGHT, padx=5, pady=5, fill=Y) |
|
self.button_helplist_edit.pack(side=TOP, anchor=W, pady=5) |
|
self.button_helplist_add.pack(side=TOP, anchor=W) |
|
self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5) |
|
|
|
def help_source_selected(self, event): |
|
"Handle event for selecting additional help." |
|
self.set_add_delete_state() |
|
|
|
def set_add_delete_state(self): |
|
"Toggle the state for the help list buttons based on list entries." |
|
if self.helplist.size() < 1: |
|
self.button_helplist_edit.state(('disabled',)) |
|
self.button_helplist_remove.state(('disabled',)) |
|
else: |
|
if self.helplist.curselection(): |
|
self.button_helplist_edit.state(('!disabled',)) |
|
self.button_helplist_remove.state(('!disabled',)) |
|
else: |
|
self.button_helplist_edit.state(('disabled',)) |
|
self.button_helplist_remove.state(('disabled',)) |
|
|
|
def helplist_item_add(self): |
|
"""Handle add button for the help list. |
|
|
|
Query for name and location of new help sources and add |
|
them to the list. |
|
""" |
|
help_source = HelpSource(self, 'New Help Source').result |
|
if help_source: |
|
self.user_helplist.append(help_source) |
|
self.helplist.insert(END, help_source[0]) |
|
self.update_help_changes() |
|
|
|
def helplist_item_edit(self): |
|
"""Handle edit button for the help list. |
|
|
|
Query with existing help source information and update |
|
config if the values are changed. |
|
""" |
|
item_index = self.helplist.index(ANCHOR) |
|
help_source = self.user_helplist[item_index] |
|
new_help_source = HelpSource( |
|
self, 'Edit Help Source', |
|
menuitem=help_source[0], |
|
filepath=help_source[1], |
|
).result |
|
if new_help_source and new_help_source != help_source: |
|
self.user_helplist[item_index] = new_help_source |
|
self.helplist.delete(item_index) |
|
self.helplist.insert(item_index, new_help_source[0]) |
|
self.update_help_changes() |
|
self.set_add_delete_state() |
|
|
|
def helplist_item_remove(self): |
|
"""Handle remove button for the help list. |
|
|
|
Delete the help list item from config. |
|
""" |
|
item_index = self.helplist.index(ANCHOR) |
|
del(self.user_helplist[item_index]) |
|
self.helplist.delete(item_index) |
|
self.update_help_changes() |
|
self.set_add_delete_state() |
|
|
|
def update_help_changes(self): |
|
"Clear and rebuild the HelpFiles section in changes" |
|
changes['main']['HelpFiles'] = {} |
|
for num in range(1, len(self.user_helplist) + 1): |
|
changes.add_option( |
|
'main', 'HelpFiles', str(num), |
|
';'.join(self.user_helplist[num-1][:2])) |
|
|
|
def load_helplist(self): |
|
|
|
self.user_helplist = idleConf.GetAllExtraHelpSourcesList() |
|
self.helplist.delete(0, 'end') |
|
for help_item in self.user_helplist: |
|
self.helplist.insert(END, help_item[0]) |
|
self.set_add_delete_state() |
|
|
|
|
|
class VarTrace: |
|
"""Maintain Tk variables trace state.""" |
|
|
|
def __init__(self): |
|
"""Store Tk variables and callbacks. |
|
|
|
untraced: List of tuples (var, callback) |
|
that do not have the callback attached |
|
to the Tk var. |
|
traced: List of tuples (var, callback) where |
|
that callback has been attached to the var. |
|
""" |
|
self.untraced = [] |
|
self.traced = [] |
|
|
|
def clear(self): |
|
"Clear lists (for tests)." |
|
|
|
self.untraced.clear() |
|
self.traced.clear() |
|
|
|
def add(self, var, callback): |
|
"""Add (var, callback) tuple to untraced list. |
|
|
|
Args: |
|
var: Tk variable instance. |
|
callback: Either function name to be used as a callback |
|
or a tuple with IdleConf config-type, section, and |
|
option names used in the default callback. |
|
|
|
Return: |
|
Tk variable instance. |
|
""" |
|
if isinstance(callback, tuple): |
|
callback = self.make_callback(var, callback) |
|
self.untraced.append((var, callback)) |
|
return var |
|
|
|
@staticmethod |
|
def make_callback(var, config): |
|
"Return default callback function to add values to changes instance." |
|
def default_callback(*params): |
|
"Add config values to changes instance." |
|
changes.add_option(*config, var.get()) |
|
return default_callback |
|
|
|
def attach(self): |
|
"Attach callback to all vars that are not traced." |
|
while self.untraced: |
|
var, callback = self.untraced.pop() |
|
var.trace_add('write', callback) |
|
self.traced.append((var, callback)) |
|
|
|
def detach(self): |
|
"Remove callback from traced vars." |
|
while self.traced: |
|
var, callback = self.traced.pop() |
|
var.trace_remove('write', var.trace_info()[0][1]) |
|
self.untraced.append((var, callback)) |
|
|
|
|
|
tracers = VarTrace() |
|
|
|
help_common = '''\ |
|
When you click either the Apply or Ok buttons, settings in this |
|
dialog that are different from IDLE's default are saved in |
|
a .idlerc directory in your home directory. Except as noted, |
|
these changes apply to all versions of IDLE installed on this |
|
machine. [Cancel] only cancels changes made since the last save. |
|
''' |
|
help_pages = { |
|
'Fonts/Tabs':''' |
|
Font sample: This shows what a selection of Basic Multilingual Plane |
|
unicode characters look like for the current font selection. If the |
|
selected font does not define a character, Tk attempts to find another |
|
font that does. Substitute glyphs depend on what is available on a |
|
particular system and will not necessarily have the same size as the |
|
font selected. Line contains 20 characters up to Devanagari, 14 for |
|
Tamil, and 10 for East Asia. |
|
|
|
Hebrew and Arabic letters should display right to left, starting with |
|
alef, \u05d0 and \u0627. Arabic digits display left to right. The |
|
Devanagari and Tamil lines start with digits. The East Asian lines |
|
are Chinese digits, Chinese Hanzi, Korean Hangul, and Japanese |
|
Hiragana and Katakana. |
|
|
|
You can edit the font sample. Changes remain until IDLE is closed. |
|
''', |
|
'Highlights': ''' |
|
Highlighting: |
|
The IDLE Dark color theme is new in October 2015. It can only |
|
be used with older IDLE releases if it is saved as a custom |
|
theme, with a different name. |
|
''', |
|
'Keys': ''' |
|
Keys: |
|
The IDLE Modern Unix key set is new in June 2016. It can only |
|
be used with older IDLE releases if it is saved as a custom |
|
key set, with a different name. |
|
''', |
|
'General': ''' |
|
General: |
|
|
|
AutoComplete: Popupwait is milliseconds to wait after key char, without |
|
cursor movement, before popping up completion box. Key char is '.' after |
|
identifier or a '/' (or '\\' on Windows) within a string. |
|
|
|
FormatParagraph: Max-width is max chars in lines after re-formatting. |
|
Use with paragraphs in both strings and comment blocks. |
|
|
|
ParenMatch: Style indicates what is highlighted when closer is entered: |
|
'opener' - opener '({[' corresponding to closer; 'parens' - both chars; |
|
'expression' (default) - also everything in between. Flash-delay is how |
|
long to highlight if cursor is not moved (0 means forever). |
|
|
|
CodeContext: Maxlines is the maximum number of code context lines to |
|
display when Code Context is turned on for an editor window. |
|
|
|
Shell Preferences: Auto-Squeeze Min. Lines is the minimum number of lines |
|
of output to automatically "squeeze". |
|
''', |
|
'Extensions': ''' |
|
ZzDummy: This extension is provided as an example for how to create and |
|
use an extension. Enable indicates whether the extension is active or |
|
not; likewise enable_editor and enable_shell indicate which windows it |
|
will be active on. For this extension, z-text is the text that will be |
|
inserted at or removed from the beginning of the lines of selected text, |
|
or the current line if no selection. |
|
''', |
|
} |
|
|
|
|
|
def is_int(s): |
|
"Return 's is blank or represents an int'" |
|
if not s: |
|
return True |
|
try: |
|
int(s) |
|
return True |
|
except ValueError: |
|
return False |
|
|
|
|
|
class VerticalScrolledFrame(Frame): |
|
"""A pure Tkinter vertically scrollable frame. |
|
|
|
* Use the 'interior' attribute to place widgets inside the scrollable frame |
|
* Construct and pack/place/grid normally |
|
* This frame only allows vertical scrolling |
|
""" |
|
def __init__(self, parent, *args, **kw): |
|
Frame.__init__(self, parent, *args, **kw) |
|
|
|
|
|
vscrollbar = Scrollbar(self, orient=VERTICAL) |
|
vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) |
|
canvas = Canvas(self, borderwidth=0, highlightthickness=0, |
|
yscrollcommand=vscrollbar.set, width=240) |
|
canvas.pack(side=LEFT, fill=BOTH, expand=TRUE) |
|
vscrollbar.config(command=canvas.yview) |
|
|
|
|
|
canvas.xview_moveto(0) |
|
canvas.yview_moveto(0) |
|
|
|
|
|
self.interior = interior = Frame(canvas) |
|
interior_id = canvas.create_window(0, 0, window=interior, anchor=NW) |
|
|
|
|
|
|
|
def _configure_interior(event): |
|
|
|
size = (interior.winfo_reqwidth(), interior.winfo_reqheight()) |
|
canvas.config(scrollregion="0 0 %s %s" % size) |
|
interior.bind('<Configure>', _configure_interior) |
|
|
|
def _configure_canvas(event): |
|
if interior.winfo_reqwidth() != canvas.winfo_width(): |
|
|
|
canvas.itemconfigure(interior_id, width=canvas.winfo_width()) |
|
canvas.bind('<Configure>', _configure_canvas) |
|
|
|
return |
|
|
|
|
|
if __name__ == '__main__': |
|
from unittest import main |
|
main('idlelib.idle_test.test_configdialog', verbosity=2, exit=False) |
|
|
|
from idlelib.idle_test.htest import run |
|
run(ConfigDialog) |
|
|