import tkinter from typing import Union, Tuple from .ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from ..draw_engine import DrawEngine from .widget_base_class import CTkBaseClass from .widget_helper_functions import pop_from_dict_by_set class CTkTextbox(CTkBaseClass): """ Textbox with rounded corners, and all text features of tkinter.Text widget. For detailed information check out the documentation. Detailed methods and parameters of the underlaying tkinter.Text widget can be found here: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/text.html (most of them are implemented here too) """ # attributes that are passed to and managed by the tkinter textbox only: _valid_tk_text_attributes = {"autoseparators", "cursor", "exportselection", "insertborderwidth", "insertofftime", "insertontime", "insertwidth", "maxundo", "padx", "pady", "selectborderwidth", "spacing1", "spacing2", "spacing3", "state", "tabs", "takefocus", "undo", "wrap", "xscrollcommand", "yscrollcommand"} def __init__(self, *args, width: int = 200, height: int = 200, corner_radius: Union[int, str] = "default_theme", border_width: Union[int, str] = "default_theme", bg_color: Union[str, Tuple[str, str], None] = None, fg_color: Union[str, Tuple[str, str], None] = "default_theme", border_color: Union[str, Tuple[str, str]] = "default_theme", text_color: Union[str, str] = "default_theme", font: any = "default_theme", **kwargs): # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass if "master" in kwargs: super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master")) else: super().__init__(*args, bg_color=bg_color, width=width, height=height) # color self._fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color self._border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color # shape self._corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius self._border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width # text self._font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if font == "default_theme" else font # configure 1x1 grid self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) self._canvas = CTkCanvas(master=self, highlightthickness=0, width=self._apply_widget_scaling(self._current_width), height=self._apply_widget_scaling(self._current_height)) self._canvas.grid(row=0, column=0, padx=0, pady=0, rowspan=1, columnspan=1, sticky="nsew") self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode)) self._draw_engine = DrawEngine(self._canvas) self._textbox = tkinter.Text(self, fg=ThemeManager.single_color(self._text_color, self._appearance_mode), width=0, height=0, font=self._font, highlightthickness=0, relief="flat", insertbackground=ThemeManager.single_color(self._text_color, self._appearance_mode), bg=ThemeManager.single_color(self._fg_color, self._appearance_mode), **pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes)) self._textbox.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew", padx=self._apply_widget_scaling(self._corner_radius), pady=self._apply_widget_scaling(self._corner_radius)) self._check_kwargs_empty(kwargs, raise_error=True) super().bind('', self._update_dimensions_event) self._draw() def _set_scaling(self, *args, **kwargs): super()._set_scaling(*args, **kwargs) self._textbox.configure(font=self._apply_font_scaling(self._font)) self._textbox.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew", padx=self._apply_widget_scaling(self._corner_radius), pady=self._apply_widget_scaling(self._corner_radius)) self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) self._draw() def _set_dimensions(self, width=None, height=None): super()._set_dimensions(width, height) self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) self._draw() def _draw(self, no_color_updates=False): requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), self._apply_widget_scaling(self._current_height), self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)) if no_color_updates is False or requires_recoloring: if self._fg_color is None: self._canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self._bg_color, self._appearance_mode), outline=ThemeManager.single_color(self._bg_color, self._appearance_mode)) else: self._canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self._fg_color, self._appearance_mode), outline=ThemeManager.single_color(self._fg_color, self._appearance_mode)) self._canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self._border_color, self._appearance_mode), outline=ThemeManager.single_color(self._border_color, self._appearance_mode)) self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode)) self._textbox.configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode), bg=ThemeManager.single_color(self._fg_color, self._appearance_mode), insertbackground=ThemeManager.single_color(self._text_color, self._appearance_mode)) self._canvas.tag_lower("inner_parts") self._canvas.tag_lower("border_parts") def configure(self, require_redraw=False, **kwargs): if "fg_color" in kwargs: self._fg_color = kwargs.pop("fg_color") require_redraw = True # check if CTk widgets are children of the frame and change their _bg_color to new frame fg_color for child in self.winfo_children(): if isinstance(child, CTkBaseClass) and hasattr(child, "_fg_color"): child.configure(bg_color=self._fg_color) if "border_color" in kwargs: self._border_color = kwargs.pop("border_color") require_redraw = True if "text_color" in kwargs: self._text_color = kwargs.pop("text_color") require_redraw = True if "corner_radius" in kwargs: self._corner_radius = kwargs.pop("corner_radius") self._textbox.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew", padx=self._apply_widget_scaling(self._corner_radius), pady=self._apply_widget_scaling(self._corner_radius)) require_redraw = True if "border_width" in kwargs: self._border_width = kwargs.pop("border_width") require_redraw = True if "width" in kwargs: self._set_dimensions(width=kwargs.pop("width")) if "height" in kwargs: self._set_dimensions(height=kwargs.pop("height")) if "font" in kwargs: self._font = kwargs.pop("font") self._textbox.configure(font=self._apply_font_scaling(self._font)) self._textbox.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes)) super().configure(require_redraw=require_redraw, **kwargs) def cget(self, attribute_name: str) -> any: if attribute_name == "corner_radius": return self._corner_radius elif attribute_name == "border_width": return self._border_width elif attribute_name == "fg_color": return self._fg_color elif attribute_name == "border_color": return self._border_color elif attribute_name == "text_color": return self._text_color elif attribute_name == "font": return self._font else: return super().cget(attribute_name) def bind(self, sequence=None, command=None, add=None): """ called on the tkinter.Text """ return self._textbox.bind(sequence, command, add) def unbind(self, sequence, funcid=None): """ called on the tkinter.Text """ return self._textbox.unbind(sequence, funcid) def insert(self, index, text, tags=None): return self._textbox.insert(index, text, tags) def get(self, index1, index2=None): return self._textbox.get(index1, index2) def bbox(self, index): return self._textbox.bbox(index) def compare(self, index, op, index2): return self._textbox.compare(index, op, index2) def dlineinfo(self, index): return self._textbox.dlineinfo(index) def edit_modified(self, arg=None): return self._textbox.edit_modified(arg) def edit_redo(self): return self._textbox.edit_redo() def edit_reset(self): return self._textbox.edit_reset() def edit_separator(self): return self._textbox.edit_separator() def edit_undo(self): return self._textbox.edit_undo() def image_create(self, index, **kwargs): raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") def image_cget(self, index, option): raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") def image_configure(self, index): raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") def image_names(self): raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") def index(self, i): return self._textbox.index(i) def mark_gravity(self, mark, gravity=None): return self._textbox.mark_gravity(mark, gravity) def mark_names(self): return self._textbox.mark_names() def mark_next(self, index): return self._textbox.mark_next(index) def mark_previous(self, index): return self._textbox.mark_previous(index) def mark_set(self, mark, index): return self._textbox.mark_set(mark, index) def mark_unset(self, mark): return self._textbox.mark_unset(mark) def scan_dragto(self, x, y): return self._textbox.scan_dragto(x, y) def scan_mark(self, x, y): return self._textbox.scan_mark(x, y) def search(self, pattern, index, *args, **kwargs): return self._textbox.search(pattern, index, *args, **kwargs) def see(self, index): return self._textbox.see(index) def tag_add(self, tagName, index1, index2=None): return self._textbox.tag_add(tagName, index1, index2) def tag_bind(self, tagName, sequence, func, add=None): return self._textbox.tag_bind(tagName, sequence, func, add) def tag_cget(self, tagName, option): return self._textbox.tag_cget(tagName, option) def tag_config(self, tagName, **kwargs): if "font" in kwargs: raise AttributeError("'font' option forbidden, because would be incompatible with scaling") return self._textbox.tag_config(tagName, **kwargs) def tag_delete(self, *tagName): return self._textbox.tag_delete(*tagName) def tag_lower(self, tagName, belowThis=None): return self._textbox.tag_lower(tagName, belowThis) def tag_names(self, index=None): return self._textbox.tag_names(index) def tag_nextrange(self, tagName, index1, index2=None): return self._textbox.tag_nextrange(tagName, index1, index2) def tag_prevrange(self, tagName, index1, index2=None): return self._textbox.tag_prevrange(tagName, index1, index2) def tag_raise(self, tagName, aboveThis=None): return self._textbox.tag_raise(tagName, aboveThis) def tag_ranges(self, tagName): return self._textbox.tag_ranges(tagName) def tag_remove(self, tagName, index1, index2=None): return self._textbox.tag_remove(tagName, index1, index2) def tag_unbind(self, tagName, sequence, funcid=None): return self._textbox.tag_unbind(tagName, sequence, funcid) def window_cget(self, index, option): raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") def window_configure(self, index, option): raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") def window_create(self, index, **kwargs): raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") def window_names(self): raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") def xview(self, *args): return self._textbox.xview(*args) def xview_moveto(self, fraction): return self._textbox.xview_moveto(fraction) def xview_scroll(self, n, what): return self._textbox.xview_scroll(n, what) def yview(self, *args): return self._textbox.yview(*args) def yview_moveto(self, fraction): return self._textbox.yview_moveto(fraction) def yview_scroll(self, n, what): return self._textbox.yview_scroll(n, what)