diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca4c7c..0d33670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ToDo: - - enforce font size in pixel and enforce CTkFont class - complete other theme files - - auto scaling of images + - auto-scaling of images + - image tuple for light/dark mode - change font attribute in wiki ## Unreleased - 2022-10-2 @@ -18,12 +18,14 @@ ToDo: - Added .cget() method to all widgets and windows - Added .bind() and .focus() methods to almost all widgets - Added 'anchor' option to CTkOptionMenu and 'justify' option to CTkComboBox + - Added CTkFont class ### Changed - Changed 'text_font' attribute to 'font' in all widgets, changed 'dropdown_text_font' to 'dropdown_font' - Changed 'dropdown_color' attribute to 'dropdown_fg_color' for combobox, optionmenu - Changed 'orient' attribute of CTkProgressBar and CTkSlider to 'orientation' - Width and height attributes of CTkCheckBox, CTkRadioButton, CTkSwitch now describe the outer dimensions of the whole widget. The button/switch size is described by separate attributes like checkbox_width, checkbox_height + - font attribute must be tuple or CTkFont now, all size values are measured in pixel now ### Removed - Removed setter and getter functions like set_text in CTkButton diff --git a/customtkinter/widgets/ctk_button.py b/customtkinter/widgets/ctk_button.py index 2bef31a..6a339b4 100644 --- a/customtkinter/widgets/ctk_button.py +++ b/customtkinter/widgets/ctk_button.py @@ -293,8 +293,7 @@ class CTkButton(CTkBaseClass): if isinstance(self._font, CTkFont): self._font.add_size_configure_callback(self._update_font) - if self._text_label is not None: - self._text_label.configure(font=self._apply_font_scaling(self._font)) + self._update_font() if "state" in kwargs: self._state = kwargs.pop("state") diff --git a/customtkinter/widgets/ctk_optionmenu.py b/customtkinter/widgets/ctk_optionmenu.py index d4470bf..187a0b6 100644 --- a/customtkinter/widgets/ctk_optionmenu.py +++ b/customtkinter/widgets/ctk_optionmenu.py @@ -7,6 +7,7 @@ from ..theme_manager import ThemeManager from ..draw_engine import DrawEngine from .widget_base_class import CTkBaseClass from .dropdown_menu import DropdownMenu +from ..utility.ctk_font import CTkFont class CTkOptionMenu(CTkBaseClass): @@ -31,8 +32,8 @@ class CTkOptionMenu(CTkBaseClass): dropdown_hover_color: Union[str, Tuple[str, str]] = "default_theme", dropdown_text_color: Union[str, Tuple[str, str]] = "default_theme", - font: any = "default_theme", - dropdown_font: any = "default_theme", + font: Union[tuple, CTkFont] = "default_theme", + dropdown_font: Union[tuple, CTkFont] = "default_theme", values: list = None, variable: tkinter.Variable = None, state: str = tkinter.NORMAL, @@ -56,7 +57,11 @@ class CTkOptionMenu(CTkBaseClass): # text and font self._text_color = ThemeManager.theme["color"]["text_button"] if text_color == "default_theme" else text_color self._text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled - self._font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if font == "default_theme" else font + + # font + self._font = CTkFont() if font == "default_theme" else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) # callback and hover functionality self._command = command @@ -132,7 +137,7 @@ class CTkOptionMenu(CTkBaseClass): self._text_label.configure(text=self._current_value) def _create_grid(self): - self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew") + self._canvas.grid(row=0, column=0, sticky="nsew") left_section_width = self._current_width - self._current_height self._text_label.grid(row=0, column=0, sticky="ew", @@ -156,10 +161,22 @@ class CTkOptionMenu(CTkBaseClass): height=self._apply_widget_scaling(self._desired_height)) self._draw() + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(row=0, column=0, sticky="nsew") + def destroy(self): if self._variable is not None: # remove old callback self._variable.trace_remove("write", self._variable_callback_name) + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + super().destroy() def _draw(self, no_color_updates=False): @@ -232,8 +249,13 @@ class CTkOptionMenu(CTkBaseClass): self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color")) if "font" in kwargs: - self._font = kwargs.pop("font") - self._text_label.configure(font=self._apply_font_scaling(self._font)) + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() if "command" in kwargs: self._command = kwargs.pop("command") diff --git a/customtkinter/widgets/ctk_radiobutton.py b/customtkinter/widgets/ctk_radiobutton.py index 2064acb..683a0e9 100644 --- a/customtkinter/widgets/ctk_radiobutton.py +++ b/customtkinter/widgets/ctk_radiobutton.py @@ -6,6 +6,7 @@ from .ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from ..draw_engine import DrawEngine from .widget_base_class import CTkBaseClass +from ..utility.ctk_font import CTkFont class CTkRadioButton(CTkBaseClass): @@ -32,7 +33,7 @@ class CTkRadioButton(CTkBaseClass): text_color_disabled: Union[str, Tuple[str, str]] = "default_theme", text: str = "CTkRadioButton", - font: any = "default_theme", + font: Union[tuple, CTkFont] = "default_theme", textvariable: tkinter.Variable = None, variable: tkinter.Variable = None, value: Union[int, str] = 0, @@ -64,7 +65,11 @@ class CTkRadioButton(CTkBaseClass): self._text_label: Union[tkinter.Label, None] = None self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color self._text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled - self._font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if font == "default_theme" else font + + # font + self._font = CTkFont() if font == "default_theme" else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) # callback and control variables self._command = command @@ -139,10 +144,22 @@ class CTkRadioButton(CTkBaseClass): self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._bg_canvas.grid_forget() + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + def destroy(self): if self._variable is not None: self._variable.trace_remove("write", self._variable_callback_name) + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + super().destroy() def _draw(self, no_color_updates=False): @@ -191,8 +208,13 @@ class CTkRadioButton(CTkBaseClass): self._text_label.configure(text=self._text) if "font" in kwargs: - self._font = kwargs.pop("font") - self._text_label.configure(font=self._apply_font_scaling(self._font)) + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() if "state" in kwargs: self._state = kwargs.pop("state") diff --git a/customtkinter/widgets/ctk_switch.py b/customtkinter/widgets/ctk_switch.py index 99e34ab..3376520 100644 --- a/customtkinter/widgets/ctk_switch.py +++ b/customtkinter/widgets/ctk_switch.py @@ -6,6 +6,7 @@ from .ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from ..draw_engine import DrawEngine from .widget_base_class import CTkBaseClass +from ..utility.ctk_font import CTkFont class CTkSwitch(CTkBaseClass): @@ -34,7 +35,7 @@ class CTkSwitch(CTkBaseClass): text_color_disabled: Union[str, Tuple[str, str]] = "default_theme", text: str = "CTkSwitch", - font: any = "default_theme", + font: Union[tuple, CTkFont] = "default_theme", textvariable: tkinter.Variable = None, onvalue: Union[int, str] = 1, offvalue: Union[int, str] = 0, @@ -63,7 +64,11 @@ class CTkSwitch(CTkBaseClass): # text self._text = text self._text_label = None - self._font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if font == "default_theme" else font + + # font + self._font = CTkFont() if font == "default_theme" else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) # shape self._corner_radius = ThemeManager.theme["shape"]["switch_corner_radius"] if corner_radius == "default_theme" else corner_radius @@ -146,11 +151,23 @@ class CTkSwitch(CTkBaseClass): self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._bg_canvas.grid_forget() + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + def destroy(self): # remove variable_callback from variable callbacks if variable exists if self._variable is not None: self._variable.trace_remove("write", self._variable_callback_name) + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + super().destroy() def _set_cursor(self): @@ -241,8 +258,13 @@ class CTkSwitch(CTkBaseClass): self._text_label.configure(text=self._text) if "font" in kwargs: - self._font = kwargs.pop("font") - self._text_label.configure(font=self._apply_font_scaling(self._font)) + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() if "state" in kwargs: self._state = kwargs.pop("state") diff --git a/customtkinter/widgets/ctk_textbox.py b/customtkinter/widgets/ctk_textbox.py index 0e12199..64a12f1 100644 --- a/customtkinter/widgets/ctk_textbox.py +++ b/customtkinter/widgets/ctk_textbox.py @@ -6,6 +6,7 @@ from .ctk_scrollbar import CTkScrollbar from ..theme_manager import ThemeManager from ..draw_engine import DrawEngine from .widget_base_class import CTkBaseClass +from ..utility.ctk_font import CTkFont from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty @@ -46,7 +47,7 @@ class CTkTextbox(CTkBaseClass): scrollbar_color: Union[str, Tuple[str, str]] = "default_theme", scrollbar_hover_color: Union[str, Tuple[str, str]] = "default_theme", - font: any = "default_theme", + font: Union[tuple, CTkFont] = "default_theme", activate_scrollbars: bool = True, **kwargs): @@ -65,14 +66,16 @@ class CTkTextbox(CTkBaseClass): self._border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width self._border_spacing = border_spacing - # text - self._font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if font == "default_theme" else font + # font + self._font = CTkFont() if font == "default_theme" else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) 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=2, columnspan=2, sticky="nsew") + self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew") self._canvas.configure(bg=ThemeManager.single_color(self._bg_color, self._appearance_mode)) self._draw_engine = DrawEngine(self._canvas) @@ -191,6 +194,21 @@ class CTkTextbox(CTkBaseClass): height=self._apply_widget_scaling(self._desired_height)) self._draw() + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._textbox.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew") + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + def _draw(self, no_color_updates=False): if not self._canvas.winfo_exists(): @@ -267,8 +285,13 @@ class CTkTextbox(CTkBaseClass): require_redraw = True if "font" in kwargs: - self._font = kwargs.pop("font") - self._textbox.configure(font=self._apply_font_scaling(self._font)) + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() self._textbox.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes)) super().configure(require_redraw=require_redraw, **kwargs) diff --git a/customtkinter/widgets/dropdown_menu.py b/customtkinter/widgets/dropdown_menu.py index a3edb49..8b28449 100644 --- a/customtkinter/widgets/dropdown_menu.py +++ b/customtkinter/widgets/dropdown_menu.py @@ -7,6 +7,7 @@ from typing import Union, Tuple, Callable, List from ..theme_manager import ThemeManager from ..appearance_mode_tracker import AppearanceModeTracker from ..scaling_tracker import ScalingTracker +from ..utility.ctk_font import CTkFont class DropdownMenu(tkinter.Menu): @@ -17,7 +18,7 @@ class DropdownMenu(tkinter.Menu): hover_color: Union[str, Tuple[str, str]] = "default_theme", text_color: Union[str, Tuple[str, str]] = "default_theme", - font: Union[str, Tuple[str, str]] = "default_theme", + font: Union[tuple, CTkFont] = "default_theme", command: Callable = None, values: List[str] = None, **kwargs): @@ -35,7 +36,11 @@ class DropdownMenu(tkinter.Menu): self._fg_color = ThemeManager.theme["color"]["dropdown_color"] if fg_color == "default_theme" else fg_color self._hover_color = ThemeManager.theme["color"]["dropdown_hover"] if hover_color == "default_theme" else hover_color self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self._font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if font == "default_theme" else font + + # font + self._font = CTkFont() if font == "default_theme" else self._check_font_type(font) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) self._configure_menu_for_platforms() @@ -44,6 +49,15 @@ class DropdownMenu(tkinter.Menu): self._add_menu_commands() + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling """ + super().configure(font=self._apply_font_scaling(self._font)) + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + super().destroy() + def _configure_menu_for_platforms(self): """ apply platform specific appearance attributes, configure all colors """ @@ -120,8 +134,13 @@ class DropdownMenu(tkinter.Menu): super().configure(fg=ThemeManager.single_color(self._text_color, self._appearance_mode)) if "font" in kwargs: - self._font = kwargs.pop("font") - super().configure(font=self._apply_font_scaling(self._font)) + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() if "command" in kwargs: self._command = kwargs.pop("command") @@ -159,27 +178,41 @@ class DropdownMenu(tkinter.Menu): else: return value - def _apply_font_scaling(self, font): - if type(font) == tuple or type(font) == list: - font_list = list(font) - for i in range(len(font_list)): - if (type(font_list[i]) == int or type(font_list[i]) == float) and font_list[i] < 0: - font_list[i] = int(font_list[i] * self._widget_scaling) - return tuple(font_list) + def _apply_font_scaling(self, font: Union[Tuple, CTkFont]) -> tuple: + """ Takes CTkFont object and returns tuple font with scaled size, has to be called again for every change of font object """ + if type(font) == tuple: + if len(font) == 1: + return font + elif len(font) == 2: + return font[0], -abs(round(font[1] * self._widget_scaling)) + elif len(font) == 3: + return font[0], -abs(round(font[1] * self._widget_scaling)), font[2] + else: + raise ValueError(f"Can not scale font {font}. font needs to be tuple of len 1, 2 or 3") - elif type(font) == str: - for negative_number in re.findall(r" -\d* ", font): - font = font.replace(negative_number, f" {int(int(negative_number) * self._widget_scaling)} ") + elif isinstance(font, CTkFont): + return font.create_scaled_tuple(self._widget_scaling) + else: + raise ValueError(f"Can not scale font '{font}' of type {type(font)}. font needs to be tuple or instance of CTkFont") + + @staticmethod + def _check_font_type(font: any): + if isinstance(font, CTkFont): return font - elif isinstance(font, tkinter.font.Font): - new_font_object = copy.copy(font) - if font.cget("size") < 0: - new_font_object.config(size=int(font.cget("size") * self._widget_scaling)) - return new_font_object + elif type(font) == tuple and len(font) == 1: + sys.stderr.write(f"Warning: font {font} given without size, will be extended with default text size of current theme\n") + return font[0], ThemeManager.theme["text"]["size"] + + elif type(font) == tuple and 2 <= len(font) <= 3: + return font else: - return font + raise ValueError(f"Wrong font type {type(font)}\n" + + f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 or 3 or an instance of CTkFont.\n" + + f"\nUsage example:\n" + + f"font=customtkinter.CTkFont(family='', size=)\n" + + f"font=('', )\n") def _set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): self._widget_scaling = new_widget_scaling diff --git a/test/manual_integration_tests/test_font.py b/test/manual_integration_tests/test_font.py index 21f00c7..9b4ff7e 100644 --- a/test/manual_integration_tests/test_font.py +++ b/test/manual_integration_tests/test_font.py @@ -56,12 +56,25 @@ for i in range(30): b.grid(row=i, column=1, pady=1) c = customtkinter.CTkCheckBox(frame_2, font=label_font) c.grid(row=i, column=2, pady=1) - c = customtkinter.CTkComboBox(frame_2, font=label_font, height=15) + c = customtkinter.CTkComboBox(frame_2, font=label_font, dropdown_font=label_font, height=15) c.grid(row=i, column=3, pady=1) e = customtkinter.CTkEntry(frame_2, font=label_font, height=15, placeholder_text="testtest") e.grid(row=i, column=4, pady=1) + o = customtkinter.CTkOptionMenu(frame_2, font=label_font, height=15, width=50) + o.grid(row=i, column=5, pady=1) + r = customtkinter.CTkRadioButton(frame_2, font=label_font, height=15, width=50) + r.grid(row=i, column=6, pady=1) + s = customtkinter.CTkSwitch(frame_2, font=label_font, height=15, width=50) + s.grid(row=i, column=7, pady=1) frame_2.grid_columnconfigure((0, 1, 2, 3, 4), weight=1) -app.after(1500, lambda: label_font.configure(size=10)) -# app.after(1500, lambda: l.configure(text="dshgfldjskhfjdslafhdjsgkkjdaslö")) +def change_font(): + import time + t1 = time.perf_counter() + label_font.configure(size=10, overstrike=True) + t2 = time.perf_counter() + print("change_font:", (t2-t1)*1000, "ms") + +app.after(3000, change_font) +app.after(6000, lambda: label_font.configure(size=8, overstrike=False)) app.mainloop()