diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index c52a04b503adb4a..b3b7dedb1b48a6a 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -11,11 +11,12 @@ """ import re -from tkinter import (Toplevel, Listbox, Scale, Canvas, +from tkinter import (Toplevel, Listbox, Scale, Canvas, TclError, 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) + HORIZONTAL, VERTICAL, ANCHOR, ACTIVE, END, + DISABLED, Text) from tkinter.ttk import (Frame, LabelFrame, Button, Checkbutton, Entry, Label, OptionMenu, Notebook, Radiobutton, Scrollbar, Style) from tkinter import colorchooser @@ -34,6 +35,7 @@ from idlelib.format import FormatParagraph from idlelib.squeezer import Squeezer from idlelib.textview import ScrollableTextFrame +from idlelib.tree import wheel_event changes = ConfigChanges() # Reload changed options in the following classes. @@ -75,7 +77,7 @@ def __init__(self, parent, title='', *, _htest=False, _utest=False): # 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.resizable(height=TRUE, width=FALSE) self.transient(parent) self.protocol("WM_DELETE_WINDOW", self.cancel) self.fontpage.fontlist.focus_set() @@ -110,13 +112,23 @@ def create_widgets(self): activate_config_changes: Tell editors to reload. """ self.frame = frame = Frame(self, padding="5px") - self.frame.grid(sticky="nwes") + self.frame.pack(side=TOP, expand=TRUE, fill=BOTH) + + vscrollables = [] + self.note = note = Notebook(frame) - self.highpage = HighPage(note) - self.fontpage = FontPage(note, self.highpage) - self.keyspage = KeysPage(note) - self.genpage = GenPage(note) + self.highpage = HighPage(note, vscrollables) + self.fontpage = FontPage(note, vscrollables, self.highpage) + self.keyspage = KeysPage(note, vscrollables) + self.genpage = GenPage(note, vscrollables) self.extpage = self.create_page_extensions() + + self._vscrollables_re = re.compile(fr''' + ^ + ({'|'.join(map(re.escape, (x._w for x in vscrollables)))}) + (?:\.|\Z) + ''', re.VERBOSE) + note.add(self.fontpage, text='Fonts/Tabs') note.add(self.highpage, text='Highlights') note.add(self.keyspage, text=' Keys ') @@ -124,7 +136,13 @@ def create_widgets(self): note.add(self.extpage, text='Extensions') note.enable_traversal() note.pack(side=TOP, expand=TRUE, fill=BOTH) - self.create_action_buttons().pack(side=BOTTOM) + buttons_frame = self.create_action_buttons() + buttons_frame.pack(side=BOTTOM, before=note) + self.wm_minsize(1, buttons_frame.winfo_reqheight() + 200) + + self.bind_all('', self.mousewheel_event) + self.bind_all('', self.mousewheel_event) + self.bind_all('', self.mousewheel_event) def create_action_buttons(self): """Return frame of action buttons for dialog. @@ -428,6 +446,27 @@ def save_all_changed_extensions(self): if has_changes: self.ext_userCfg.Save() + def mousewheel_event(self, event): + if self._vscrollables_re.match(event.widget._w): + return + page = self._nametowidget(self.note.select()) + scrollable_frame = getattr(page, 'scrollable_frame', None) + if ( + scrollable_frame is not None and + is_child_widget(event.widget, page.scrollable_frame.canvas) + ): + page.scrollable_frame.mousewheel_event(event) + return "break" + + +def is_child_widget(widget, parent_candidate): + """Check if a Tk widget is direct or indirect child of another widget.""" + return ( + widget._w.removeprefix('.') + .removeprefix(parent_candidate._w.removeprefix('.')) + [:1] in {'', '.'} + ) + # class TabPage(Frame): # A template for Page classes. # def __init__(self, master): @@ -477,14 +516,14 @@ def save_all_changed_extensions(self): class FontPage(Frame): - def __init__(self, master, highpage): + def __init__(self, master, vscrollables, highpage): super().__init__(master) self.highlight_sample = highpage.highlight_sample - self.create_page_font_tab() + self.create_page_font_tab(vscrollables) self.load_font_cfg() self.load_tab_cfg() - def create_page_font_tab(self): + def create_page_font_tab(self, vscrollables): """Return frame of widgets for Font/Tabs tab. Fonts: Enable users to provisionally change font face, size, or @@ -536,13 +575,17 @@ def create_page_font_tab(self): self.space_num = tracers.add(IntVar(self), ('main', 'Indent', 'num-spaces')) # Define frames and widgets. + self.scrollable_frame = VerticalScrolledFrame(self) frame_font = LabelFrame( - self, borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ') + self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, text=' Shell/Editor Font ') frame_sample = LabelFrame( - self, borderwidth=2, relief=GROOVE, - text=' Font Sample (Editable) ') + self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, text=' Font Sample (Editable) ') frame_indent = LabelFrame( - self, borderwidth=2, relief=GROOVE, text=' Indentation Width ') + self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, text=' Indentation Width ') + # frame_font. frame_font_name = Frame(frame_font) frame_font_param = Frame(frame_font) @@ -550,10 +593,12 @@ def create_page_font_tab(self): frame_font_name, justify=LEFT, text='Font Face :') self.fontlist = Listbox(frame_font_name, height=15, takefocus=True, exportselection=FALSE) + vscrollables.append(self.fontlist) self.fontlist.bind('', self.on_fontlist_select) self.fontlist.bind('', self.on_fontlist_select) self.fontlist.bind('', self.on_fontlist_select) scroll_font = Scrollbar(frame_font_name) + vscrollables.append(scroll_font) scroll_font.config(command=self.fontlist.yview) self.fontlist.config(yscrollcommand=scroll_font.set) font_size_title = Label(frame_font_param, text='Size :') @@ -575,8 +620,9 @@ def create_page_font_tab(self): orient='horizontal', tickinterval=2, from_=2, to=16) # Grid and pack widgets: - self.columnconfigure(1, weight=1) - self.rowconfigure(2, weight=1) + self.scrollable_frame.pack(side=TOP, expand=TRUE, fill=BOTH) + self.scrollable_frame.interior.columnconfigure(1, weight=1) + self.scrollable_frame.interior.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') @@ -687,14 +733,14 @@ def var_changed_space_num(self, *params): class HighPage(Frame): - def __init__(self, master): + def __init__(self, master, vscrollables): super().__init__(master) self.cd = master.winfo_toplevel() self.style = Style(master) - self.create_page_highlight() + self.create_page_highlight(vscrollables) self.load_theme_cfg() - def create_page_highlight(self): + def create_page_highlight(self, vscrollables): """Return frame of widgets for Highlighting tab. Enable users to provisionally change foreground and background @@ -842,18 +888,18 @@ def create_page_highlight(self): StringVar(self), self.var_changed_highlight_target) # Create widgets: + self.scrollable_frame = VerticalScrolledFrame(self) + # body frame and section frames. - frame_custom = LabelFrame(self, borderwidth=2, relief=GROOVE, + frame_custom = LabelFrame(self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, text=' Custom Highlighting ') - frame_theme = LabelFrame(self, borderwidth=2, relief=GROOVE, + frame_theme = LabelFrame(self.scrollable_frame.interior, + 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) + text = self.highlight_sample = Text(frame_custom, + relief=SOLID, borderwidth=1) # Prevent perhaps invisible selection of word or slice. text.bind('', lambda e: 'break') text.bind('', lambda e: 'break') @@ -888,7 +934,11 @@ def tem(event, elem=element): self.highlight_target.set(elem) text.tag_bind( self.theme_elements[element][0], '', tem) - text['state'] = 'disabled' + text.configure( + font=('courier', 12, ''), cursor='hand2', + width=1, height=n_lines, + takefocus=FALSE, highlightthickness=0, wrap=NONE, + 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') @@ -925,15 +975,16 @@ def tem(event, elem=element): frame_theme, text='Delete Custom Theme', command=self.delete_custom) self.theme_message = Label(frame_theme, borderwidth=2) + # Pack widgets: # body. + self.scrollable_frame.pack(side=LEFT, expand=TRUE, fill=BOTH) 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) + text.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) @@ -1346,13 +1397,13 @@ def delete_custom(self): class KeysPage(Frame): - def __init__(self, master): + def __init__(self, master, vscrollables): super().__init__(master) self.cd = master.winfo_toplevel() - self.create_page_keys() + self.create_page_keys(vscrollables) self.load_key_cfg() - def create_page_keys(self): + def create_page_keys(self, vscrollables): """Return frame of widgets for Keys tab. Enable users to provisionally change both individual and sets of @@ -1445,11 +1496,13 @@ def create_page_keys(self): # 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 ') + self.scrollable_frame = VerticalScrolledFrame(self) + frame_custom = LabelFrame(self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, + text=' Custom Key Bindings ') + frame_key_sets = LabelFrame(self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, + text=' Key Set ') # frame_custom. frame_target = Frame(frame_custom) target_title = Label(frame_target, text='Action - Key(s)') @@ -1457,6 +1510,7 @@ def create_page_keys(self): scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL) self.bindingslist = Listbox( frame_target, takefocus=FALSE, exportselection=FALSE) + vscrollables.extend([self.bindingslist, scroll_target_y]) self.bindingslist.bind('', self.on_bindingslist_select) scroll_target_y['command'] = self.bindingslist.yview @@ -1489,6 +1543,7 @@ def create_page_keys(self): # Pack widgets: # body. + self.scrollable_frame.pack(side=LEFT, expand=TRUE, fill=BOTH) 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. @@ -1778,11 +1833,11 @@ def delete_custom_keys(self): class GenPage(Frame): - def __init__(self, master): + def __init__(self, master, vscrollables): super().__init__(master) self.init_validators() - self.create_page_general() + self.create_page_general(vscrollables) self.load_general_cfg() def init_validators(self): @@ -1792,7 +1847,7 @@ def is_digits_or_empty(s): return digits_or_empty_re.fullmatch(s) is not None self.digits_only = (self.register(is_digits_or_empty), '%P',) - def create_page_general(self): + def create_page_general(self, vscrollables): """Return frame of widgets for General tab. Enable users to provisionally change general options. Function @@ -1895,14 +1950,20 @@ def create_page_general(self): StringVar(self), ('extensions', 'CodeContext', 'maxlines')) # Create widgets: + self.scrollable_frame = VerticalScrolledFrame(self) + # Section frames. - frame_window = LabelFrame(self, borderwidth=2, relief=GROOVE, + frame_window = LabelFrame(self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, text=' Window Preferences') - frame_editor = LabelFrame(self, borderwidth=2, relief=GROOVE, + frame_editor = LabelFrame(self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, text=' Editor Preferences') - frame_shell = LabelFrame(self, borderwidth=2, relief=GROOVE, + frame_shell = LabelFrame(self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, text=' Shell Preferences') - frame_help = LabelFrame(self, borderwidth=2, relief=GROOVE, + frame_help = LabelFrame(self.scrollable_frame.interior, + borderwidth=2, relief=GROOVE, text=' Additional Help Sources ') # Frame_window. frame_run = Frame(frame_window, borderwidth=0) @@ -2021,6 +2082,7 @@ def create_page_general(self): # Pack widgets: # Body. + self.scrollable_frame.pack(side=TOP, expand=TRUE, fill=BOTH) frame_window.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) frame_editor.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) frame_shell.pack(side=TOP, padx=5, pady=5, expand=TRUE, fill=BOTH) @@ -2350,12 +2412,16 @@ 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) + self.vscrollbar = 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) + self.canvas = 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) + vscrollbar.bind('', self.mousewheel_event) + vscrollbar.bind('', self.mousewheel_event) + vscrollbar.bind('', self.mousewheel_event) # Reset the view. canvas.xview_moveto(0) @@ -2368,19 +2434,28 @@ def __init__(self, parent, *args, **kw): # 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) + try: + # Update the scrollbars to match the size of the inner frame. + canvas.config(scrollregion=canvas.bbox("all")) + except TclError: + pass interior.bind('', _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()) + try: + 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()) + except TclError: + pass canvas.bind('', _configure_canvas) return + def mousewheel_event(self, event): + return wheel_event(event, self.canvas) + if __name__ == '__main__': from unittest import main diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 369004c9d1b3d4c..87c290bf705b285 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -204,7 +204,7 @@ class Event: following attributes (in braces are the event types for which the attribute is valid): - serial - serial number of event + serial - serial number of event num - mouse button pressed (ButtonPress, ButtonRelease) focus - whether the window has the focus (Enter, Leave) height - height of the exposed window (Configure, Expose)