changeed driectory structure, moved scaling and appearance mode functionality to super classes

This commit is contained in:
Tom Schimansky 2022-10-29 13:11:55 +02:00
parent e5484cb6cd
commit c2a5b4881e
42 changed files with 280 additions and 294 deletions

View File

@ -10,6 +10,8 @@ ToDo:
- image tuple for light/dark mode
- change font attribute in wiki
- add new button attributes to wiki
- cursor configuring
- overwrite winfo methods
## Unreleased - 2022-10-2
### Added

View File

@ -4,11 +4,11 @@ import os
import sys
# import manager classes
from .appearance_mode_tracker import AppearanceModeTracker
from .widgets.font.font_manager import FontManager
from .scaling_tracker import ScalingTracker
from .theme_manager import ThemeManager
from .widgets.core_rendering.draw_engine import DrawEngine
from .windows.widgets.appearance_mode.appearance_mode_tracker import AppearanceModeTracker
from .windows.widgets.font.font_manager import FontManager
from .windows.widgets.scaling.scaling_tracker import ScalingTracker
from .windows.widgets.theme.theme_manager import ThemeManager
from .windows.widgets.core_rendering.draw_engine import DrawEngine
AppearanceModeTracker.init_appearance_mode()
@ -46,23 +46,21 @@ if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Cust
DrawEngine.preferred_drawing_method = "circle_shapes"
# import widgets
from customtkinter.widgets.core_widget_classes.widget_base_class import CTkBaseClass
from .widgets.ctk_button import CTkButton
from .widgets.ctk_checkbox import CTkCheckBox
from .widgets.ctk_entry import CTkEntry
from .widgets.ctk_slider import CTkSlider
from .widgets.ctk_frame import CTkFrame
from .widgets.ctk_progressbar import CTkProgressBar
from .widgets.ctk_label import CTkLabel
from .widgets.ctk_radiobutton import CTkRadioButton
from .widgets.core_rendering.ctk_canvas import CTkCanvas
from .widgets.ctk_switch import CTkSwitch
from .widgets.ctk_optionmenu import CTkOptionMenu
from .widgets.ctk_combobox import CTkComboBox
from .widgets.ctk_scrollbar import CTkScrollbar
from .widgets.ctk_textbox import CTkTextbox
from .widgets.ctk_tabview import CTkTabview
from .widgets.ctk_segmented_button import CTkSegmentedButton
from .windows.widgets.ctk_button import CTkButton
from .windows.widgets.ctk_checkbox import CTkCheckBox
from .windows.widgets.ctk_combobox import CTkComboBox
from .windows.widgets.ctk_entry import CTkEntry
from .windows.widgets.ctk_frame import CTkFrame
from .windows.widgets.ctk_label import CTkLabel
from .windows.widgets.ctk_optionmenu import CTkOptionMenu
from .windows.widgets.ctk_progressbar import CTkProgressBar
from .windows.widgets.ctk_radiobutton import CTkRadioButton
from .windows.widgets.ctk_scrollbar import CTkScrollbar
from .windows.widgets.ctk_segmented_button import CTkSegmentedButton
from .windows.widgets.ctk_slider import CTkSlider
from .windows.widgets.ctk_switch import CTkSwitch
from .windows.widgets.ctk_tabview import CTkTabview
from .windows.widgets.ctk_textbox import CTkTextbox
# import windows
from .windows.ctk_tk import CTk
@ -70,7 +68,7 @@ from .windows.ctk_toplevel import CTkToplevel
from .windows.ctk_input_dialog import CTkInputDialog
# font classes
from .widgets.font.ctk_font import CTkFont
from .windows.widgets.font.ctk_font import CTkFont
def set_appearance_mode(mode_string: str):

View File

@ -1,3 +0,0 @@
from customtkinter.widgets.core_rendering.ctk_canvas import CTkCanvas
CTkCanvas.init_font_character_mapping()

View File

@ -1,11 +1,11 @@
from typing import Union, Tuple
from ..widgets.ctk_label import CTkLabel
from ..widgets.ctk_entry import CTkEntry
from ..windows.ctk_toplevel import CTkToplevel
from ..widgets.ctk_button import CTkButton
from ..appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import ThemeManager
from .widgets.ctk_label import CTkLabel
from .widgets.ctk_entry import CTkEntry
from .ctk_toplevel import CTkToplevel
from .widgets.ctk_button import CTkButton
from .widgets.appearance_mode.appearance_mode_tracker import AppearanceModeTracker
from .widgets.theme.theme_manager import ThemeManager
class CTkInputDialog:

View File

@ -4,17 +4,16 @@ import sys
import os
import platform
import ctypes
import re
from typing import Union, Tuple, List
from typing import Union, Tuple
from ..appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import ThemeManager
from ..scaling_tracker import ScalingTracker
from .widgets.theme.theme_manager import ThemeManager
from .widgets.scaling.scaling_base_class import CTkScalingBaseClass
from .widgets.appearance_mode.appearance_mode_base_class import CTkAppearanceModeBaseClass
from ..utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
class CTk(tkinter.Tk):
class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
"""
Main app window with dark titlebar on Windows and macOS.
For detailed information check out the documentation.
@ -33,21 +32,15 @@ class CTk(tkinter.Tk):
fg_color: Union[str, Tuple[str, str]] = "default_theme",
**kwargs):
ScalingTracker.activate_high_dpi_awareness() # make process DPI aware
self._enable_macos_dark_title_bar()
super().__init__(**pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments))
# call init methods of super classes
tkinter.Tk.__init__(self, **pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="window")
check_kwargs_empty(kwargs, raise_error=True)
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self._set_scaling, self)
self._window_scaling = ScalingTracker.get_window_scaling(self)
self._current_width = 600 # initial window size, always without scaling
self._current_width = 600 # initial window size, independent of scaling
self._current_height = 500
self._min_width: int = 0
self._min_height: int = 0
@ -57,8 +50,11 @@ class CTk(tkinter.Tk):
self._fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
# set bg of tkinter.Tk
super().configure(bg=self._apply_appearance_mode(self._fg_color))
super().title("CTk")
# set title and initial geometry
self.title("CTk")
self.geometry(f"{self._current_width}x{self._current_height}")
self._state_before_windows_set_titlebar_color = None
@ -77,6 +73,14 @@ class CTk(tkinter.Tk):
self._block_update_dimensions_event = False
def destroy(self):
self._disable_macos_dark_title_bar()
# call destroy methods of super classes
tkinter.Tk.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _focus_in_event(self, event):
# sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
if sys.platform == "darwin":
@ -123,12 +127,6 @@ class CTk(tkinter.Tk):
if self._max_width is not None or self._max_height is not None:
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def destroy(self):
AppearanceModeTracker.remove(self._set_appearance_mode)
ScalingTracker.remove_window(self._set_scaling, self)
self._disable_macos_dark_title_bar()
super().destroy()
def withdraw(self):
if self._window_exists is False:
self._withdraw_called_before_window_exists = True
@ -201,59 +199,6 @@ class CTk(tkinter.Tk):
else:
return self._reverse_geometry_scaling(super().geometry())
@staticmethod
def _parse_geometry_string(geometry_string: str) -> tuple:
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
width = int(result.group(2)) if result.group(2) is not None else None
height = int(result.group(3)) if result.group(3) is not None else None
x = int(result.group(5)) if result.group(5) is not None else None
y = int(result.group(6)) if result.group(6) is not None else None
return width, height, x, y
def _apply_geometry_scaling(self, geometry_string: str) -> str:
width, height, x, y = self._parse_geometry_string(geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}+{x}+{y}"
def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
width, height, x, y = self._parse_geometry_string(scaled_geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}+{x}+{y}"
def _apply_window_scaling(self, value):
if isinstance(value, (int, float)):
return int(value * self._window_scaling)
else:
return value
def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str:
""" color can be either a single hex color string or a color name or it can be a
tuple color with (light_color, dark_color). The functions returns
always a single color string """
if type(color) == tuple or type(color) == list:
return color[self._appearance_mode]
else:
return color
def configure(self, **kwargs):
if "fg_color" in kwargs:
self._fg_color = kwargs.pop("fg_color")

View File

@ -4,17 +4,16 @@ import sys
import os
import platform
import ctypes
import re
from typing import Union, Tuple, List
from typing import Union, Tuple
from ..appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import ThemeManager
from ..scaling_tracker import ScalingTracker
from .widgets.theme.theme_manager import ThemeManager
from .widgets.scaling.scaling_base_class import CTkScalingBaseClass
from .widgets.appearance_mode.appearance_mode_base_class import CTkAppearanceModeBaseClass
from ..utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
class CTkToplevel(tkinter.Toplevel):
class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
"""
Toplevel window with dark titlebar on Windows and macOS.
For detailed information check out the documentation.
@ -33,17 +32,12 @@ class CTkToplevel(tkinter.Toplevel):
self._enable_macos_dark_title_bar()
# call init methods of super classees
super().__init__(*args, **pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="window")
check_kwargs_empty(kwargs, raise_error=True)
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self._set_scaling, self)
self._window_scaling = ScalingTracker.get_window_scaling(self)
self._current_width = 200 # initial window size, always without scaling
self._current_height = 200
self._min_width: int = 0
@ -54,7 +48,10 @@ class CTkToplevel(tkinter.Toplevel):
self._fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
# set bg color of tkinter.Toplevel
super().configure(bg=self._apply_appearance_mode(self._fg_color))
# set title of tkinter.Toplevel
super().title("CTkToplevel")
self._state_before_windows_set_titlebar_color = None
@ -71,6 +68,14 @@ class CTkToplevel(tkinter.Toplevel):
self.bind('<Configure>', self._update_dimensions_event)
self.bind('<FocusIn>', self._focus_in_event)
def destroy(self):
self._disable_macos_dark_title_bar()
# call destroy methods of super classes
tkinter.Toplevel.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _focus_in_event(self, event):
# sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
if sys.platform == "darwin":
@ -114,65 +119,6 @@ class CTkToplevel(tkinter.Toplevel):
else:
return self._reverse_geometry_scaling(super().geometry())
@staticmethod
def _parse_geometry_string(geometry_string: str) -> tuple:
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
width = int(result.group(2)) if result.group(2) is not None else None
height = int(result.group(3)) if result.group(3) is not None else None
x = int(result.group(5)) if result.group(5) is not None else None
y = int(result.group(6)) if result.group(6) is not None else None
return width, height, x, y
def _apply_geometry_scaling(self, geometry_string: str) -> str:
width, height, x, y = self._parse_geometry_string(geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}+{x}+{y}"
def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
width, height, x, y = self._parse_geometry_string(scaled_geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}+{x}+{y}"
def _apply_window_scaling(self, value):
if isinstance(value, (int, float)):
return int(value * self._window_scaling)
else:
return value
def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str:
""" color can be either a single hex color string or a color name or it can be a
tuple color with (light_color, dark_color). The functions returns
always a single color string """
if type(color) == tuple or type(color) == list:
return color[self._appearance_mode]
else:
return color
def destroy(self):
AppearanceModeTracker.remove(self._set_appearance_mode)
ScalingTracker.remove_window(self._set_scaling, self)
self._disable_macos_dark_title_bar()
super().destroy()
def withdraw(self):
if self._windows_set_titlebar_color_called:
self._withdraw_called_after_windows_set_titlebar_color = True

View File

@ -0,0 +1,3 @@
from customtkinter.windows.widgets.core_rendering.ctk_canvas import CTkCanvas
CTkCanvas.init_font_character_mapping()

View File

@ -1,8 +1,16 @@
from typing import Union, Tuple, List
from abc import ABC, abstractmethod
from .appearance_mode_tracker import AppearanceModeTracker
class CTkAppearanceModeBaseClass:
class CTkAppearanceModeBaseClass(ABC):
def __init__(self):
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
def destroy(self):
AppearanceModeTracker.remove(self._set_appearance_mode)
def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str:
""" color can be either a single hex color string or a color name or it can be a
@ -13,3 +21,7 @@ class CTkAppearanceModeBaseClass:
return color[self._appearance_mode]
else:
return color
@abstractmethod
def _set_appearance_mode(self, mode_string: str):
return

View File

@ -5,7 +5,7 @@ import tkinter
from typing import Union, TYPE_CHECKING
if TYPE_CHECKING:
from customtkinter.widgets.core_rendering.ctk_canvas import CTkCanvas
from customtkinter.windows.widgets.core_rendering.ctk_canvas import CTkCanvas
class DrawEngine:

View File

@ -2,13 +2,14 @@ import tkinter
import sys
from typing import Union, Tuple, Callable, List
from ...theme_manager import ThemeManager
from ...appearance_mode_tracker import AppearanceModeTracker
from ...scaling_tracker import ScalingTracker
from ..theme.theme_manager import ThemeManager
from ..scaling.scaling_tracker import ScalingTracker
from ..font.ctk_font import CTkFont
from ..appearance_mode.appearance_mode_tracker import AppearanceModeTracker
from ..appearance_mode.appearance_mode_base_class import CTkAppearanceModeBaseClass
class DropdownMenu(tkinter.Menu):
class DropdownMenu(tkinter.Menu, CTkAppearanceModeBaseClass):
def __init__(self, *args,
min_character_width: int = 18,
@ -21,7 +22,9 @@ class DropdownMenu(tkinter.Menu):
values: List[str] = None,
**kwargs):
super().__init__(*args, **kwargs)
# call init methods of super classes
tkinter.Menu.__init__(self, *args, **kwargs)
CTkAppearanceModeBaseClass.__init__(self)
ScalingTracker.add_widget(self._set_scaling, self)
self._widget_scaling = ScalingTracker.get_widget_scaling(self)
@ -46,14 +49,17 @@ 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()
# call destroy methods of super classes
tkinter.Menu.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling """
super().configure(font=self._apply_font_scaling(self._font))
def _configure_menu_for_platforms(self):
""" apply platform specific appearance attributes, configure all colors """

View File

@ -1,26 +1,24 @@
import sys
import tkinter
import tkinter.ttk as ttk
import copy
from typing import Union, Callable, Tuple, List
from abc import ABC, abstractmethod
from typing import Union, Callable, Tuple
try:
from typing import TypedDict
except ImportError:
from typing_extensions import TypedDict
from ...windows.ctk_tk import CTk
from ...windows.ctk_toplevel import CTkToplevel
from ...appearance_mode_tracker import AppearanceModeTracker
from ...scaling_tracker import ScalingTracker
from ...theme_manager import ThemeManager
from ...ctk_tk import CTk
from ...ctk_toplevel import CTkToplevel
from ..theme.theme_manager import ThemeManager
from ..font.ctk_font import CTkFont
from ..appearance_mode.appearance_mode_base_class import CTkAppearanceModeBaseClass
from ..scaling.scaling_base_class import CTkScalingBaseClass
from ...utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
from ....utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
class CTkBaseClass(tkinter.Frame, ABC):
class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
""" Base class of every CTk widget, handles the dimensions, _bg_color,
appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """
@ -37,21 +35,21 @@ class CTkBaseClass(tkinter.Frame, ABC):
bg_color: Union[str, Tuple[str, str], None] = None,
**kwargs):
super().__init__(master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))
# call init methods of super classes
tkinter.Frame.__init__(self, master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="widget")
# check if kwargs is empty, if not raise error for unsupported arguments
check_kwargs_empty(kwargs, raise_error=True)
# dimensions
# dimensions independent of scaling
self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget
self._current_height = height # _current_width and _current_height are independent of the scale
self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height
self._desired_height = height
# scaling
ScalingTracker.add_widget(self._set_scaling, self) # add callback for automatic scaling changes
self._widget_scaling = ScalingTracker.get_widget_scaling(self)
# set width and height of tkinter.Frame
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
@ -59,17 +57,15 @@ class CTkBaseClass(tkinter.Frame, ABC):
class GeometryCallDict(TypedDict):
function: Callable
kwargs: dict
self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
# background color
self._bg_color: Union[str, Tuple[str, str]] = self._detect_color_of_master() if bg_color is None else bg_color
# set bg color of tkinter.Frame
super().configure(bg=self._apply_appearance_mode(self._bg_color))
# add configure callback to tkinter.Frame
super().bind('<Configure>', self._update_dimensions_event)
# overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well
@ -93,7 +89,14 @@ class CTkBaseClass(tkinter.Frame, ABC):
self.master.config = new_configure
self.master.configure = new_configure
@abstractmethod
def destroy(self):
""" Destroy this and all descendants widgets. """
# call destroy methods of super classes
tkinter.Frame.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _draw(self, no_color_updates: bool = False):
return
@ -221,66 +224,6 @@ class CTkBaseClass(tkinter.Frame, ABC):
super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height))
def _apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
if isinstance(value, (int, float)):
return value * self._widget_scaling
else:
return value
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 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")
def _apply_argument_scaling(self, kwargs: dict) -> dict:
scaled_kwargs = copy.copy(kwargs)
if "pady" in scaled_kwargs:
if isinstance(scaled_kwargs["pady"], (int, float, str)):
scaled_kwargs["pady"] = self._apply_widget_scaling(scaled_kwargs["pady"])
elif isinstance(scaled_kwargs["pady"], tuple):
scaled_kwargs["pady"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["pady"]])
if "padx" in kwargs:
if isinstance(scaled_kwargs["padx"], (int, float, str)):
scaled_kwargs["padx"] = self._apply_widget_scaling(scaled_kwargs["padx"])
elif isinstance(scaled_kwargs["padx"], tuple):
scaled_kwargs["padx"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["padx"]])
if "x" in scaled_kwargs:
scaled_kwargs["x"] = self._apply_widget_scaling(scaled_kwargs["x"])
if "y" in scaled_kwargs:
scaled_kwargs["y"] = self._apply_widget_scaling(scaled_kwargs["y"])
return scaled_kwargs
def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str:
""" color can be either a single hex color string or a color name or it can be a
tuple color with (light_color, dark_color). The functions returns
always a single color string """
if type(color) == tuple or type(color) == list:
return color[self._appearance_mode]
else:
return color
def destroy(self):
""" Destroy this and all descendants widgets. """
AppearanceModeTracker.remove(self._set_appearance_mode)
ScalingTracker.remove_widget(self._set_scaling, self)
super().destroy()
def place(self, **kwargs):
"""
Place a widget in the parent widget. Use as options:

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont

View File

@ -4,7 +4,7 @@ from typing import Union, Tuple, Callable, List
from .core_widget_classes.dropdown_menu import DropdownMenu
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont

View File

@ -2,7 +2,7 @@ import tkinter
from typing import Union, Tuple
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont

View File

@ -1,7 +1,7 @@
from typing import Union, Tuple, List
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass

View File

@ -2,7 +2,7 @@ import tkinter
from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass
from .core_widget_classes.dropdown_menu import DropdownMenu

View File

@ -3,7 +3,7 @@ import math
from typing import Union, Tuple
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont

View File

@ -2,7 +2,7 @@ import sys
from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass

View File

@ -1,7 +1,7 @@
import tkinter
from typing import Union, Tuple, List, Dict, Callable
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .ctk_button import CTkButton
from .ctk_frame import CTkFrame
from .font.ctk_font import CTkFont

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont

View File

@ -1,6 +1,6 @@
from typing import Union, Tuple, Dict, List, Callable
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .ctk_frame import CTkFrame
from .core_widget_classes.widget_base_class import CTkBaseClass
from .ctk_segmented_button import CTkSegmentedButton

View File

@ -3,7 +3,7 @@ from typing import Union, Tuple
from .core_rendering.ctk_canvas import CTkCanvas
from .ctk_scrollbar import CTkScrollbar
from ..theme_manager import ThemeManager
from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont

View File

@ -2,7 +2,7 @@ from tkinter.font import Font
import copy
from typing import List, Callable, Tuple
from customtkinter.theme_manager import ThemeManager
from ..theme.theme_manager import ThemeManager
class CTkFont(Font):

View File

@ -0,0 +1,133 @@
from typing import Union, Tuple
from abc import ABC, abstractmethod
import copy
import re
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from .scaling_tracker import ScalingTracker
from ..font.ctk_font import CTkFont
class CTkScalingBaseClass(ABC):
def __init__(self, scaling_type: Literal["widget", "window"] = "widget"):
self._scaling_type = scaling_type
if self._scaling_type == "widget":
ScalingTracker.add_widget(self._set_scaling, self) # add callback for automatic scaling changes
self._widget_scaling = ScalingTracker.get_widget_scaling(self)
elif self._scaling_type == "window":
ScalingTracker.activate_high_dpi_awareness() # make process DPI aware
ScalingTracker.add_window(self._set_scaling, self) # add callback for automatic scaling changes
self._window_scaling = ScalingTracker.get_window_scaling(self)
def destroy(self):
if self._scaling_type == "widget":
ScalingTracker.remove_widget(self._set_scaling, self)
elif self._scaling_type == "window":
ScalingTracker.remove_window(self._set_scaling, self)
def _apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
assert self._scaling_type == "widget"
if isinstance(value, (int, float)):
return value * self._widget_scaling
else:
return value
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 """
assert self._scaling_type == "widget"
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 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")
def _apply_argument_scaling(self, kwargs: dict) -> dict:
assert self._scaling_type == "widget"
scaled_kwargs = copy.copy(kwargs)
if "pady" in scaled_kwargs:
if isinstance(scaled_kwargs["pady"], (int, float, str)):
scaled_kwargs["pady"] = self._apply_widget_scaling(scaled_kwargs["pady"])
elif isinstance(scaled_kwargs["pady"], tuple):
scaled_kwargs["pady"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["pady"]])
if "padx" in kwargs:
if isinstance(scaled_kwargs["padx"], (int, float, str)):
scaled_kwargs["padx"] = self._apply_widget_scaling(scaled_kwargs["padx"])
elif isinstance(scaled_kwargs["padx"], tuple):
scaled_kwargs["padx"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["padx"]])
if "x" in scaled_kwargs:
scaled_kwargs["x"] = self._apply_widget_scaling(scaled_kwargs["x"])
if "y" in scaled_kwargs:
scaled_kwargs["y"] = self._apply_widget_scaling(scaled_kwargs["y"])
return scaled_kwargs
@staticmethod
def _parse_geometry_string(geometry_string: str) -> tuple:
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
width = int(result.group(2)) if result.group(2) is not None else None
height = int(result.group(3)) if result.group(3) is not None else None
x = int(result.group(5)) if result.group(5) is not None else None
y = int(result.group(6)) if result.group(6) is not None else None
return width, height, x, y
def _apply_geometry_scaling(self, geometry_string: str) -> str:
assert self._scaling_type == "window"
width, height, x, y = self._parse_geometry_string(geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}+{x}+{y}"
def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
assert self._scaling_type == "window"
width, height, x, y = self._parse_geometry_string(scaled_geometry_string)
if x is None and y is None: # no <x> and <y> in geometry_string
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}"
elif width is None and height is None: # no <width> and <height> in geometry_string
return f"+{x}+{y}"
else:
return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}+{x}+{y}"
def _apply_window_scaling(self, value):
assert self._scaling_type == "window"
if isinstance(value, (int, float)):
return int(value * self._window_scaling)
else:
return value
@abstractmethod
def _set_scaling(self, new_widget_scaling, new_window_scaling):
return

View File

@ -13,7 +13,7 @@ class ThemeManager:
script_directory = os.path.dirname(os.path.abspath(__file__))
if theme_name_or_path in cls.built_in_themes:
with open(os.path.join(script_directory, "assets", "themes", f"{theme_name_or_path}.json"), "r") as f:
with open(os.path.join(script_directory, "../../../assets", "themes", f"{theme_name_or_path}.json"), "r") as f:
cls.theme = json.load(f)
else:
with open(theme_name_or_path, "r") as f:

View File

@ -1 +1,2 @@
darkdetect~=0.3.1
darkdetect~=0.3.1
typing-extensions~=4.4.0