2415 lines
103 KiB
Python
2415 lines
103 KiB
Python
|
"""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, Scale, 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 GetKeysDialog
|
||
|
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()
|
||
|
# Reload changed options in the following classes.
|
||
|
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}')
|
||
|
# Each theme element key is its display name.
|
||
|
# The first value of the tuple is the sample area tag name.
|
||
|
# The second value is the display name list sort index.
|
||
|
self.create_widgets()
|
||
|
self.resizable(height=FALSE, width=FALSE)
|
||
|
self.transient(parent)
|
||
|
self.protocol("WM_DELETE_WINDOW", self.cancel)
|
||
|
self.fontpage.fontlist.focus_set()
|
||
|
# XXX Decide whether to keep or delete these key bindings.
|
||
|
# Key bindings for this dialog.
|
||
|
# self.bind('<Escape>', self.Cancel) #dismiss dialog, no save
|
||
|
# self.bind('<Alt-a>', self.Apply) #apply changes, save
|
||
|
# self.bind('<F1>', self.Help) #context help
|
||
|
# Attach callbacks after loading config to avoid calling them.
|
||
|
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/Tabs')
|
||
|
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():
|
||
|
# Changing the default padding on OSX results in unreadable
|
||
|
# text in the buttons.
|
||
|
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)
|
||
|
# Add space above buttons.
|
||
|
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.
|
||
|
"""
|
||
|
# Before a config is saved, some cleanup of current
|
||
|
# config must be done - remove the previous 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()
|
||
|
|
||
|
|
||
|
# class TabPage(Frame): # A template for Page classes.
|
||
|
# def __init__(self, master):
|
||
|
# super().__init__(master)
|
||
|
# self.create_page_tab()
|
||
|
# self.load_tab_cfg()
|
||
|
# def create_page_tab(self):
|
||
|
# # Define tk vars and register var and callback with tracers.
|
||
|
# # Create subframes and widgets.
|
||
|
# # Pack widgets.
|
||
|
# def load_tab_cfg(self):
|
||
|
# # Initialize widgets with data from idleConf.
|
||
|
# def var_changed_var_name():
|
||
|
# # For each tk var that needs other than default callback.
|
||
|
# def other_methods():
|
||
|
# # Define tab-specific behavior.
|
||
|
|
||
|
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)
|
||
|
|
||
|
# Define frames and widgets.
|
||
|
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.
|
||
|
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')
|
||
|
# frame_sample.
|
||
|
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)
|
||
|
|
||
|
# Grid and pack widgets:
|
||
|
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.
|
||
|
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)
|
||
|
# frame_sample.
|
||
|
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'
|
||
|
|
||
|
# Set sorted no-duplicate editor font selection list and font_name.
|
||
|
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
|
||
|
# Set font size dropdown.
|
||
|
self.sizelist.SetMenu(('7', '8', '9', '10', '11', '12', '13', '14',
|
||
|
'16', '18', '20', '22', '25', '29', '34', '40'),
|
||
|
font_size)
|
||
|
# Set font weight.
|
||
|
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)
|
||
|
|
||
|
# Create widgets:
|
||
|
# body frame and section frames.
|
||
|
frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE,
|
||
|
text=' Custom Highlighting ')
|
||
|
frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE,
|
||
|
text=' Highlighting Theme ')
|
||
|
# frame_custom.
|
||
|
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)
|
||
|
# Prevent perhaps invisible selection of word or slice.
|
||
|
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):
|
||
|
# event.widget.winfo_top_level().highlight_target.set(elem)
|
||
|
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) #, command=self.set_highlight_targetBinding
|
||
|
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)
|
||
|
# frame_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)
|
||
|
# Pack widgets:
|
||
|
# body.
|
||
|
frame_custom.pack(side=LEFT, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
||
|
frame_theme.pack(side=TOP, padx=5, pady=5, fill=X)
|
||
|
# frame_custom.
|
||
|
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)
|
||
|
# frame_theme.
|
||
|
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
|
||
|
"""
|
||
|
# Set current theme type radiobutton.
|
||
|
self.theme_source.set(idleConf.GetOption(
|
||
|
'main', 'Theme', 'default', type='bool', default=1))
|
||
|
# Set current theme.
|
||
|
current_option = idleConf.CurrentTheme()
|
||
|
# Load available theme option menus.
|
||
|
if self.theme_source.get(): # Default theme selected.
|
||
|
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: # User theme selected.
|
||
|
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()
|
||
|
# Load theme element option menu.
|
||
|
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):
|
||
|
# User didn't cancel and they chose a new color.
|
||
|
if self.theme_source.get(): # Current theme is a built-in.
|
||
|
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: # User cancelled custom theme creation.
|
||
|
return
|
||
|
else: # Create new custom theme based on previously active theme.
|
||
|
self.create_new(new_theme)
|
||
|
self.color.set(color_string)
|
||
|
else: # Current theme is user defined.
|
||
|
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)
|
||
|
# Apply any of the old theme's unsaved changes to the new theme.
|
||
|
if theme_name in changes['highlight']:
|
||
|
theme_changes = changes['highlight'][theme_name]
|
||
|
for element in theme_changes:
|
||
|
new_theme[element] = theme_changes[element]
|
||
|
# Save the new theme.
|
||
|
self.save_new(new_theme_name, new_theme)
|
||
|
# Change GUI over to the 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': # bg not possible
|
||
|
self.fg_on.state(('disabled',))
|
||
|
self.bg_on.state(('disabled',))
|
||
|
self.fg_bg_toggle.set(1)
|
||
|
else: # Both fg and bg can be set.
|
||
|
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
|
||
|
"""
|
||
|
# Set the color sample area.
|
||
|
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(): # Default theme
|
||
|
theme = self.builtin_name.get()
|
||
|
else: # User theme
|
||
|
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': # Cursor sample needs special painting.
|
||
|
colors['background'] = idleConf.GetHighlight(
|
||
|
theme, 'normal')['background']
|
||
|
# Handle any unsaved changes to this theme.
|
||
|
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):
|
||
|
# Make testing easier. Could change implementation.
|
||
|
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()
|
||
|
# Remove theme from changes, config, and file.
|
||
|
changes.delete_section('highlight', theme_name)
|
||
|
# Reload user theme list.
|
||
|
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])
|
||
|
# Revert to default theme.
|
||
|
self.theme_source.set(idleConf.defaultCfg['main'].Get('Theme', 'default'))
|
||
|
self.builtin_name.set(idleConf.defaultCfg['main'].Get('Theme', 'name'))
|
||
|
# User can't back out of these changes, they must be applied now.
|
||
|
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)
|
||
|
|
||
|
# Create widgets:
|
||
|
# body and section frames.
|
||
|
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_custom.
|
||
|
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')
|
||
|
# frame_key_sets.
|
||
|
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)
|
||
|
|
||
|
# Pack widgets:
|
||
|
# body.
|
||
|
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)
|
||
|
# frame_custom.
|
||
|
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.
|
||
|
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)
|
||
|
# frame_key_sets.
|
||
|
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."
|
||
|
# Set current keys type radiobutton.
|
||
|
self.keyset_source.set(idleConf.GetOption(
|
||
|
'main', 'Keys', 'default', type='bool', default=1))
|
||
|
# Set current keys.
|
||
|
current_option = idleConf.CurrentKeys()
|
||
|
# Load available keyset option menus.
|
||
|
if self.keyset_source.get(): # Default theme selected.
|
||
|
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: # User key set selected.
|
||
|
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()
|
||
|
# Load keyset element list.
|
||
|
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: # Event is an extension binding.
|
||
|
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']: # unsaved changes
|
||
|
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 = GetKeysDialog(self, 'Get New Keys', bind_name,
|
||
|
current_key_sequences).result
|
||
|
if new_keys:
|
||
|
if self.keyset_source.get(): # Current key set is a built-in.
|
||
|
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: # User cancelled custom key set creation.
|
||
|
self.bindingslist.select_set(list_index)
|
||
|
self.bindingslist.select_anchor(list_index)
|
||
|
return
|
||
|
else: # Create new custom key set based on previously active key set.
|
||
|
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: # Add key set to changed items.
|
||
|
event_name = event[2:-2] # Trim off the angle brackets.
|
||
|
binding = ' '.join(prev_keys[event])
|
||
|
new_keys[event_name] = binding
|
||
|
# Handle any unsaved changes to prev key set.
|
||
|
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]
|
||
|
# Save the new key set.
|
||
|
self.save_new_key_set(new_key_set_name, new_keys)
|
||
|
# Change GUI over to the new key set.
|
||
|
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] # Trim off the angle brackets.
|
||
|
if keyset_name in changes['keys']:
|
||
|
# Handle any unsaved changes to this key set.
|
||
|
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):
|
||
|
# Make testing easier. Could change implementation.
|
||
|
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()
|
||
|
# Remove key set from changes, config, and file.
|
||
|
changes.delete_section('keys', keyset_name)
|
||
|
# Reload user key set list.
|
||
|
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])
|
||
|
# Revert to default key set.
|
||
|
self.keyset_source.set(idleConf.defaultCfg['main']
|
||
|
.Get('Keys', 'default'))
|
||
|
self.builtin_name.set(idleConf.defaultCfg['main'].Get('Keys', 'name')
|
||
|
or idleConf.default_keys())
|
||
|
# User can't back out of these changes, they must be applied now.
|
||
|
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
|
||
|
"""
|
||
|
# Integer values need StringVar because int('') raises.
|
||
|
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'))
|
||
|
|
||
|
# Create widgets:
|
||
|
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',
|
||
|
)
|
||
|
|
||
|
# Pack widgets:
|
||
|
frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH)
|
||
|
# frame_run.
|
||
|
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.
|
||
|
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.
|
||
|
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.
|
||
|
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_paren.
|
||
|
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.
|
||
|
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):
|
||
|
# Set variables for all windows.
|
||
|
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
|
||
|
"""
|
||
|
# Integer values need StringVar because int('') raises.
|
||
|
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'))
|
||
|
|
||
|
# Create widgets:
|
||
|
frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE,
|
||
|
text=' Shell Preferences')
|
||
|
frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE,
|
||
|
text=' Editor Preferences')
|
||
|
# Frame_shell.
|
||
|
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_editor.
|
||
|
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',
|
||
|
)
|
||
|
|
||
|
# Pack widgets:
|
||
|
frame_shell.pack(side=TOP, padx=5, pady=5, fill=BOTH)
|
||
|
Label(self).pack() # Spacer -- better solution?
|
||
|
frame_editor.pack(side=TOP, padx=5, pady=5, fill=BOTH)
|
||
|
# frame_auto_squeeze_min_lines
|
||
|
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.
|
||
|
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.
|
||
|
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.
|
||
|
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):
|
||
|
# Set variables for shell windows.
|
||
|
self.auto_squeeze_min_lines.set(idleConf.GetOption(
|
||
|
'main', 'PyShell', 'auto-squeeze-min-lines', type='int'))
|
||
|
# Set variables for editor windows.
|
||
|
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() # Requires extension names.
|
||
|
|
||
|
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 # TEMPORARY
|
||
|
self.tabbed_page_set = self.extension_list # TEMPORARY
|
||
|
|
||
|
# Create the frame holding controls for each extension.
|
||
|
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) # Spacer. Replace with config?
|
||
|
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):
|
||
|
# Former built-in extensions are already filtered out.
|
||
|
self.extensions[ext_name] = []
|
||
|
|
||
|
for ext_name in self.extensions:
|
||
|
opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
|
||
|
|
||
|
# Bring 'enable' options to the beginning of the list.
|
||
|
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: # Need this until .Get fixed.
|
||
|
value = def_obj # Bad values overwritten by entry.
|
||
|
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
|
||
|
# Create an entry for each configuration option.
|
||
|
for row, opt in enumerate(self.extensions[ext_name]):
|
||
|
# Create a row with a label and entry/checkbutton.
|
||
|
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: # type == 'str'
|
||
|
# Limit size to fit non-expanding space with larger font.
|
||
|
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 self.defaultCfg.has_section(section):
|
||
|
# Currently, always true; if not, indent to return.
|
||
|
if (value == default):
|
||
|
return self.ext_userCfg.RemoveOption(section, name)
|
||
|
# Set the option.
|
||
|
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
|
||
|
"""
|
||
|
# self = frame_help in dialog (until ExtPage class).
|
||
|
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)
|
||
|
|
||
|
# Pack frame_help.
|
||
|
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: # No entries in list.
|
||
|
self.button_helplist_edit.state(('disabled',))
|
||
|
self.button_helplist_remove.state(('disabled',))
|
||
|
else: # Some entries.
|
||
|
if self.helplist.curselection(): # There currently is a selection.
|
||
|
self.button_helplist_edit.state(('!disabled',))
|
||
|
self.button_helplist_remove.state(('!disabled',))
|
||
|
else: # There currently is not a selection.
|
||
|
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() # Selected will be un-selected
|
||
|
|
||
|
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):
|
||
|
# Set additional help sources.
|
||
|
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)."
|
||
|
# Call after all tests in a module to avoid memory leaks.
|
||
|
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)
|
||
|
|
||
|
# Create a canvas object and a vertical scrollbar for scrolling it.
|
||
|
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)
|
||
|
|
||
|
# Reset the view.
|
||
|
canvas.xview_moveto(0)
|
||
|
canvas.yview_moveto(0)
|
||
|
|
||
|
# Create a frame inside the canvas which will be scrolled with it.
|
||
|
self.interior = interior = Frame(canvas)
|
||
|
interior_id = canvas.create_window(0, 0, window=interior, anchor=NW)
|
||
|
|
||
|
# Track changes to the canvas and frame width and sync them,
|
||
|
# also updating the scrollbar.
|
||
|
def _configure_interior(event):
|
||
|
# Update the scrollbars to match the size of the inner frame.
|
||
|
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():
|
||
|
# Update the inner frame's width to fill the canvas.
|
||
|
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)
|