""" This module contains various custom scrolling widgets, including `ScrolledText` and `ScrolledFrame`. """ import ttkbootstrap as ttk from ttkbootstrap.constants import * from tkinter import Pack, Place, Grid class ScrolledText(ttk.Frame): """A text widget with optional vertical and horizontal scrollbars. Setting `autohide=True` will cause the scrollbars to hide when the mouse is not over the widget. The vertical scrollbar is on by default, but can be turned off. The horizontal scrollbar can be enabled by setting `vbar=True`. This widget is identical in configuration to the `Text` widget other than the scrolling frame. https://tcl.tk/man/tcl8.6/TkCmd/text.htm ![scrolled text](../../../assets/scrolled/scrolledtext.gif) Examples: ```python import ttkbootstrap as ttk from ttkbootstrap.constants import * from ttkbootstrap.scrolled import ScrolledText app = ttk.Window() # scrolled text with autohide vertical scrollbar st = ScrolledText(app, padding=5, height=10, autohide=True) st.pack(fill=BOTH, expand=YES) # add text st.insert(END, 'Insert your text here.') app.mainloop() ``` """ def __init__( self, master=None, padding=2, bootstyle=DEFAULT, autohide=False, vbar=True, hbar=False, **kwargs, ): """ Parameters: master (Widget): The parent widget. padding (int): The amount of empty space to create on the outside of the widget. bootstyle (str): A style keyword used to set the color and style of the vertical scrollbar. Available options include -> primary, secondary, success, info, warning, danger, dark, light. vbar (bool): A vertical scrollbar is shown when **True** (default). hbar (bool): A horizontal scrollbar is shown when **True**. Turning on this scrollbar will also set `wrap="none"`. This scrollbar is _off_ by default. autohide (bool): When **True**, the scrollbars will hide when the mouse is not within the frame bbox. **kwargs (Dict[str, Any]): Other keyword arguments passed to the `Text` widget. """ super().__init__(master, padding=padding) # setup text widget self._text = ttk.Text(self, padx=50, **kwargs) self._hbar = None self._vbar = None # delegate text methods to frame for method in vars(ttk.Text).keys(): if any(["pack" in method, "grid" in method, "place" in method]): pass else: setattr(self, method, getattr(self._text, method)) # setup scrollbars if vbar: self._vbar = ttk.Scrollbar( master=self, bootstyle=bootstyle, command=self._text.yview, orient=VERTICAL, ) self._vbar.place(relx=1.0, relheight=1.0, anchor=NE) self._text.configure(yscrollcommand=self._vbar.set) if hbar: self._hbar = ttk.Scrollbar( master=self, bootstyle=bootstyle, command=self._text.xview, orient=HORIZONTAL, ) self._hbar.place(rely=1.0, relwidth=1.0, anchor=SW) self._text.configure(xscrollcommand=self._hbar.set, wrap="none") self._text.pack(side=LEFT, fill=BOTH, expand=YES) # position scrollbars if self._hbar: self.update_idletasks() self._text_width = self.winfo_reqwidth() self._scroll_width = self.winfo_reqwidth() self.bind("", self._on_configure) if autohide: self.autohide_scrollbar() self.hide_scrollbars() def _on_configure(self, *_): """Callback for when the configure method is used""" if self._hbar: self.update_idletasks() text_width = self.winfo_width() vbar_width = self._vbar.winfo_width() relx = (text_width - vbar_width) / text_width self._hbar.place(rely=1.0, relwidth=relx) @property def text(self): """Returns the internal text object""" return self._text @property def hbar(self): """Returns the internal horizontal scrollbar""" return self._hbar @property def vbar(self): """Returns the internal vertical scrollbar""" return self._vbar def hide_scrollbars(self, *_): """Hide the scrollbars.""" try: self._vbar.lower(self._text) except: pass try: self._hbar.lower(self._text) except: pass def show_scrollbars(self, *_): """Show the scrollbars.""" try: self._vbar.lift(self._text) except: pass try: self._hbar.lift(self._text) except: pass def autohide_scrollbar(self, *_): """Show the scrollbars when the mouse enters the widget frame, and hide when it leaves the frame.""" self.bind("", self.show_scrollbars) self.bind("", self.hide_scrollbars) class ScrolledFrame(ttk.Frame): """A widget container with a vertical scrollbar. The ScrolledFrame fills the width of its container. The height is either set explicitly or is determined by the content frame's contents. This widget behaves mostly like a normal frame other than the exceptions stated already. Another exception is when packing it into a Notebook or Panedwindow. In this case, you'll need to add the container instead of the content frame. For example, `mynotebook.add(myscrolledframe.container)`. The scrollbar has an autohide feature that can be turned on by setting `autohide=True`. Examples: ```python import ttkbootstrap as ttk from ttkbootstrap.constants import * from ttkbootstrap.scrolled import ScrolledFrame app = ttk.Window() sf = ScrolledFrame(app, autohide=True) sf.pack(fill=BOTH, expand=YES, padx=10, pady=10) # add a large number of checkbuttons into the scrolled frame for x in range(20): ttk.Checkbutton(sf, text=f"Checkbutton {x}").pack(anchor=W) app.mainloop() ```""" def __init__( self, master=None, padding=2, bootstyle=DEFAULT, autohide=False, height=200, width=300, scrollheight=None, **kwargs, ): """ Parameters: master (Widget): The parent widget. padding (int): The amount of empty space to create on the outside of the widget. bootstyle (str): A style keyword used to set the color and style of the vertical scrollbar. Available options include -> primary, secondary, success, info, warning, danger, dark, light. autohide (bool): When **True**, the scrollbars will hide when the mouse is not within the frame bbox. height (int): The height of the container frame in screen units. width (int): The width of the container frame in screen units. scrollheight (int): The height of the content frame in screen units. If None, the height is determined by the frame contents. **kwargs (Dict[str, Any]): Other keyword arguments passed to the content frame. """ # content frame container self.container = ttk.Frame( master=master, relief=FLAT, borderwidth=0, width=width, height=height, ) self.container.bind("", lambda _: self.yview()) self.container.propagate(0) # content frame super().__init__( master=self.container, padding=padding, bootstyle=bootstyle.replace('round', ''), width=width, height=height, **kwargs, ) self.place(rely=0.0, relwidth=1.0, height=scrollheight) # vertical scrollbar self.vscroll = ttk.Scrollbar( master=self.container, command=self.yview, orient=VERTICAL, bootstyle=bootstyle, ) self.vscroll.pack(side=RIGHT, fill=Y) self.winsys = self.tk.call("tk", "windowingsystem") # setup autohide scrollbar self.autohide = autohide if self.autohide: self.hide_scrollbars() # widget event binding self.container.bind("", self._on_enter, "+") self.container.bind("", self._on_leave, "+") self.container.bind("", self._on_map, "+") self.bind("<>", self._on_map_child, "+") # delegate content geometry methods to container frame _methods = vars(Pack).keys() | vars(Grid).keys() | vars(Place).keys() for method in _methods: if any(["pack" in method, "grid" in method, "place" in method]): # prefix content frame methods with 'content_' setattr(self, f"content_{method}", getattr(self, method)) # overwrite content frame methods from container frame setattr(self, method, getattr(self.container, method)) def yview(self, *args): """Update the vertical position of the content frame within the container. Parameters: *args (List[Any, ...]): Optional arguments passed to yview in order to move the content frame within the container frame. """ if not args: first, _ = self.vscroll.get() self.yview_moveto(fraction=first) elif args[0] == "moveto": self.yview_moveto(fraction=float(args[1])) elif args[0] == "scroll": self.yview_scroll(number=int(args[1]), what=args[2]) else: return def yview_moveto(self, fraction: float): """Update the vertical position of the content frame within the container. Parameters: fraction (float): The relative position of the content frame within the container. """ base, thumb = self._measures() if fraction < 0: first = 0.0 elif (fraction + thumb) > 1: first = 1 - thumb else: first = fraction self.vscroll.set(first, first + thumb) self.content_place(rely=-first * base) def yview_scroll(self, number: int, what: str): """Update the vertical position of the content frame within the container. Parameters: number (int): The amount by which the content frame will be moved within the container frame by 'what' units. what (str): The type of units by which the number is to be interpeted. This parameter is currently not used and is assumed to be 'units'. """ first, _ = self.vscroll.get() fraction = (number / 100) + first self.yview_moveto(fraction) def _add_scroll_binding(self, parent): """Recursive adding of scroll binding to all descendants.""" children = parent.winfo_children() for widget in [parent, *children]: bindings = widget.bind() if self.winsys.lower() == "x11": if "" in bindings or "" in bindings: continue else: widget.bind("", self._on_mousewheel, "+") widget.bind("", self._on_mousewheel, "+") else: if "" not in bindings: widget.bind("", self._on_mousewheel, "+") if widget.winfo_children() and widget != parent: self._add_scroll_binding(widget) def _del_scroll_binding(self, parent): """Recursive removal of scrolling binding for all descendants""" children = parent.winfo_children() for widget in [parent, *children]: if self.winsys.lower() == "x11": widget.unbind("") widget.unbind("") else: widget.unbind("") if widget.winfo_children() and widget != parent: self._del_scroll_binding(widget) def enable_scrolling(self): """Enable mousewheel scrolling on the frame and all of its children.""" self._add_scroll_binding(self) def disable_scrolling(self): """Disable mousewheel scrolling on the frame and all of its children.""" self._del_scroll_binding(self) def hide_scrollbars(self): """Hide the scrollbars.""" self.vscroll.pack_forget() def show_scrollbars(self): """Show the scrollbars.""" self.vscroll.pack(side=RIGHT, fill=Y) def autohide_scrollbar(self): """Toggle the autohide funtionality. Show the scrollbars when the mouse enters the widget frame, and hide when it leaves the frame.""" self.autohide = not self.autohide def _measures(self): """Measure the base size of the container and the thumb size for use in the yview methods""" outer = self.container.winfo_height() inner = max([self.winfo_height(), outer]) base = inner / outer if inner == outer: thumb = 1.0 else: thumb = outer / inner return base, thumb def _on_map_child(self, event): """Callback for when a widget is mapped to the content frame.""" if self.container.winfo_ismapped(): self.yview() def _on_enter(self, event): """Callback for when the mouse enters the widget.""" self.enable_scrolling() if self.autohide: self.show_scrollbars() def _on_leave(self, event): """Callback for when the mouse leaves the widget.""" self.disable_scrolling() if self.autohide: self.hide_scrollbars() def _on_configure(self, event): """Callback for when the widget is configured""" self.yview() def _on_map(self, event): self.yview() def _on_mousewheel(self, event): """Callback for when the mouse wheel is scrolled.""" if self.winsys.lower() == "win32": delta = -int(event.delta / 120) elif self.winsys.lower() == "aqua": delta = -event.delta elif event.num == 4: delta = -10 elif event.num == 5: delta = 10 self.yview_scroll(delta, UNITS)