mirror of
https://github.com/TomSchimansky/CustomTkinter.git
synced 2023-08-10 21:13:13 +03:00
changeed driectory structure, moved scaling and appearance mode functionality to super classes
This commit is contained in:
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
3
customtkinter/windows/widgets/__init__.py
Normal file
3
customtkinter/windows/widgets/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from customtkinter.windows.widgets.core_rendering.ctk_canvas import CTkCanvas
|
||||
|
||||
CTkCanvas.init_font_character_mapping()
|
@ -0,0 +1,27 @@
|
||||
from typing import Union, Tuple, List
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from .appearance_mode_tracker import AppearanceModeTracker
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@abstractmethod
|
||||
def _set_appearance_mode(self, mode_string: str):
|
||||
return
|
@ -0,0 +1,135 @@
|
||||
import sys
|
||||
import tkinter
|
||||
from distutils.version import StrictVersion as Version
|
||||
from typing import Callable
|
||||
|
||||
try:
|
||||
import darkdetect
|
||||
|
||||
if Version(darkdetect.__version__) < Version("0.3.1"):
|
||||
sys.stderr.write("WARNING: You have to upgrade the darkdetect library: pip3 install --upgrade darkdetect\n")
|
||||
if sys.platform != "darwin":
|
||||
exit()
|
||||
except ImportError as err:
|
||||
raise err
|
||||
except Exception:
|
||||
sys.stderr.write("customtkinter.appearance_mode_tracker warning: failed to import darkdetect")
|
||||
|
||||
|
||||
class AppearanceModeTracker:
|
||||
|
||||
callback_list = []
|
||||
app_list = []
|
||||
update_loop_running = False
|
||||
update_loop_interval = 500 # milliseconds
|
||||
|
||||
appearance_mode_set_by = "system"
|
||||
appearance_mode = 0 # Light (standard)
|
||||
|
||||
@classmethod
|
||||
def init_appearance_mode(cls):
|
||||
if cls.appearance_mode_set_by == "system":
|
||||
new_appearance_mode = cls.detect_appearance_mode()
|
||||
|
||||
if new_appearance_mode != cls.appearance_mode:
|
||||
cls.appearance_mode = new_appearance_mode
|
||||
cls.update_callbacks()
|
||||
|
||||
@classmethod
|
||||
def add(cls, callback: Callable, widget=None):
|
||||
cls.callback_list.append(callback)
|
||||
|
||||
if widget is not None:
|
||||
app = cls.get_tk_root_of_widget(widget)
|
||||
if app not in cls.app_list:
|
||||
cls.app_list.append(app)
|
||||
|
||||
if not cls.update_loop_running:
|
||||
app.after(cls.update_loop_interval, cls.update)
|
||||
cls.update_loop_running = True
|
||||
|
||||
@classmethod
|
||||
def remove(cls, callback: Callable):
|
||||
try:
|
||||
cls.callback_list.remove(callback)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def detect_appearance_mode() -> int:
|
||||
try:
|
||||
if darkdetect.theme() == "Dark":
|
||||
return 1 # Dark
|
||||
else:
|
||||
return 0 # Light
|
||||
except NameError:
|
||||
return 0 # Light
|
||||
|
||||
@classmethod
|
||||
def get_tk_root_of_widget(cls, widget):
|
||||
current_widget = widget
|
||||
|
||||
while isinstance(current_widget, tkinter.Tk) is False:
|
||||
current_widget = current_widget.master
|
||||
|
||||
return current_widget
|
||||
|
||||
@classmethod
|
||||
def update_callbacks(cls):
|
||||
if cls.appearance_mode == 0:
|
||||
for callback in cls.callback_list:
|
||||
try:
|
||||
callback("Light")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
elif cls.appearance_mode == 1:
|
||||
for callback in cls.callback_list:
|
||||
try:
|
||||
callback("Dark")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
@classmethod
|
||||
def update(cls):
|
||||
if cls.appearance_mode_set_by == "system":
|
||||
new_appearance_mode = cls.detect_appearance_mode()
|
||||
|
||||
if new_appearance_mode != cls.appearance_mode:
|
||||
cls.appearance_mode = new_appearance_mode
|
||||
cls.update_callbacks()
|
||||
|
||||
# find an existing tkinter.Tk object for the next call of .after()
|
||||
for app in cls.app_list:
|
||||
try:
|
||||
app.after(cls.update_loop_interval, cls.update)
|
||||
return
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
cls.update_loop_running = False
|
||||
|
||||
@classmethod
|
||||
def get_mode(cls) -> int:
|
||||
return cls.appearance_mode
|
||||
|
||||
@classmethod
|
||||
def set_appearance_mode(cls, mode_string: str):
|
||||
if mode_string.lower() == "dark":
|
||||
cls.appearance_mode_set_by = "user"
|
||||
new_appearance_mode = 1
|
||||
|
||||
if new_appearance_mode != cls.appearance_mode:
|
||||
cls.appearance_mode = new_appearance_mode
|
||||
cls.update_callbacks()
|
||||
|
||||
elif mode_string.lower() == "light":
|
||||
cls.appearance_mode_set_by = "user"
|
||||
new_appearance_mode = 0
|
||||
|
||||
if new_appearance_mode != cls.appearance_mode:
|
||||
cls.appearance_mode = new_appearance_mode
|
||||
cls.update_callbacks()
|
||||
|
||||
elif mode_string.lower() == "system":
|
||||
cls.appearance_mode_set_by = "system"
|
117
customtkinter/windows/widgets/core_rendering/ctk_canvas.py
Normal file
117
customtkinter/windows/widgets/core_rendering/ctk_canvas.py
Normal file
@ -0,0 +1,117 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Union, Tuple
|
||||
|
||||
|
||||
class CTkCanvas(tkinter.Canvas):
|
||||
"""
|
||||
Canvas with additional functionality to draw antialiased circles on Windows/Linux.
|
||||
|
||||
Call .init_font_character_mapping() at program start to load the correct character
|
||||
dictionary according to the operating system. Characters (circle sizes) are optimised
|
||||
to look best for rendering CustomTkinter shapes on the different operating systems.
|
||||
|
||||
- .create_aa_circle() creates antialiased circle and returns int identifier.
|
||||
- .coords() is modified to support the aa-circle shapes correctly like you would expect.
|
||||
- .itemconfig() is also modified to support aa-cricle shapes.
|
||||
|
||||
The aa-circles are created by choosing a character from the custom created and loaded
|
||||
font 'CustomTkinter_shapes_font'. It contains circle shapes with different sizes filling
|
||||
either the whole character space or just pert of it (characters A to R). Circles with a smaller
|
||||
radius need a smaller circle character to look correct when rendered on the canvas.
|
||||
|
||||
For an optimal result, the draw-engine creates two aa-circles on top of each other, while
|
||||
one is rotated by 90 degrees. This helps to make the circle look more symetric, which is
|
||||
not can be a problem when using only a single circle character.
|
||||
"""
|
||||
|
||||
radius_to_char_fine: dict = None # dict to map radius to font circle character
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._aa_circle_canvas_ids = set()
|
||||
|
||||
@classmethod
|
||||
def init_font_character_mapping(cls):
|
||||
""" optimizations made for Windows 10, 11 only """
|
||||
|
||||
radius_to_char_warped = {19: 'B', 18: 'B', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'B', 12: 'B', 11: 'B',
|
||||
10: 'B',
|
||||
9: 'C', 8: 'D', 7: 'C', 6: 'E', 5: 'F', 4: 'G', 3: 'H', 2: 'H', 1: 'H', 0: 'A'}
|
||||
|
||||
radius_to_char_fine_windows_10 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
|
||||
11: 'C', 10: 'C',
|
||||
9: 'D', 8: 'D', 7: 'D', 6: 'C', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H',
|
||||
0: 'A'}
|
||||
|
||||
radius_to_char_fine_windows_11 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
|
||||
11: 'D', 10: 'D',
|
||||
9: 'E', 8: 'F', 7: 'C', 6: 'I', 5: 'E', 4: 'G', 3: 'P', 2: 'R', 1: 'R',
|
||||
0: 'A'}
|
||||
|
||||
radius_to_char_fine_linux = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'F', 12: 'C',
|
||||
11: 'F', 10: 'C',
|
||||
9: 'D', 8: 'G', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'M', 2: 'H', 1: 'H',
|
||||
0: 'A'}
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
if sys.getwindowsversion().build > 20000: # Windows 11
|
||||
cls.radius_to_char_fine = radius_to_char_fine_windows_11
|
||||
else: # < Windows 11
|
||||
cls.radius_to_char_fine = radius_to_char_fine_windows_10
|
||||
elif sys.platform.startswith("linux"): # Optimized on Kali Linux
|
||||
cls.radius_to_char_fine = radius_to_char_fine_linux
|
||||
else:
|
||||
cls.radius_to_char_fine = radius_to_char_fine_windows_10
|
||||
|
||||
def _get_char_from_radius(self, radius: int) -> str:
|
||||
if radius >= 20:
|
||||
return "A"
|
||||
else:
|
||||
return self.radius_to_char_fine[radius]
|
||||
|
||||
def create_aa_circle(self, x_pos: int, y_pos: int, radius: int, angle: int = 0, fill: str = "white",
|
||||
tags: Union[str, Tuple[str, ...]] = "", anchor: str = tkinter.CENTER) -> int:
|
||||
# create a circle with a font element
|
||||
circle_1 = self.create_text(x_pos, y_pos, text=self._get_char_from_radius(radius), anchor=anchor, fill=fill,
|
||||
font=("CustomTkinter_shapes_font", -radius * 2), tags=tags, angle=angle)
|
||||
self.addtag_withtag("ctk_aa_circle_font_element", circle_1)
|
||||
self._aa_circle_canvas_ids.add(circle_1)
|
||||
|
||||
return circle_1
|
||||
|
||||
def coords(self, tag_or_id, *args):
|
||||
|
||||
if type(tag_or_id) == str and "ctk_aa_circle_font_element" in self.gettags(tag_or_id):
|
||||
coords_id = self.find_withtag(tag_or_id)[0] # take the lowest id for the given tag
|
||||
super().coords(coords_id, *args[:2])
|
||||
|
||||
if len(args) == 3:
|
||||
super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self._get_char_from_radius(args[2]))
|
||||
|
||||
elif type(tag_or_id) == int and tag_or_id in self._aa_circle_canvas_ids:
|
||||
super().coords(tag_or_id, *args[:2])
|
||||
|
||||
if len(args) == 3:
|
||||
super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self._get_char_from_radius(args[2]))
|
||||
|
||||
else:
|
||||
super().coords(tag_or_id, *args)
|
||||
|
||||
def itemconfig(self, tag_or_id, *args, **kwargs):
|
||||
kwargs_except_outline = kwargs.copy()
|
||||
if "outline" in kwargs_except_outline:
|
||||
del kwargs_except_outline["outline"]
|
||||
|
||||
if type(tag_or_id) == int:
|
||||
if tag_or_id in self._aa_circle_canvas_ids:
|
||||
super().itemconfigure(tag_or_id, *args, **kwargs_except_outline)
|
||||
else:
|
||||
super().itemconfigure(tag_or_id, *args, **kwargs)
|
||||
else:
|
||||
configure_ids = self.find_withtag(tag_or_id)
|
||||
for configure_id in configure_ids:
|
||||
if configure_id in self._aa_circle_canvas_ids:
|
||||
super().itemconfigure(configure_id, *args, **kwargs_except_outline)
|
||||
else:
|
||||
super().itemconfigure(configure_id, *args, **kwargs)
|
1235
customtkinter/windows/widgets/core_rendering/draw_engine.py
Normal file
1235
customtkinter/windows/widgets/core_rendering/draw_engine.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,233 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Union, Tuple, Callable, List
|
||||
|
||||
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, CTkAppearanceModeBaseClass):
|
||||
def __init__(self, *args,
|
||||
min_character_width: int = 18,
|
||||
|
||||
fg_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
command: Callable = None,
|
||||
values: List[str] = None,
|
||||
**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)
|
||||
|
||||
AppearanceModeTracker.add(self._set_appearance_mode, self)
|
||||
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
|
||||
|
||||
self._min_character_width = min_character_width
|
||||
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
|
||||
|
||||
# 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()
|
||||
|
||||
self._values = values
|
||||
self._command = command
|
||||
|
||||
self._add_menu_commands()
|
||||
|
||||
def destroy(self):
|
||||
if isinstance(self._font, CTkFont):
|
||||
self._font.remove_size_configure_callback(self._update_font)
|
||||
|
||||
# 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 """
|
||||
|
||||
if sys.platform == "darwin":
|
||||
super().configure(tearoff=False,
|
||||
font=self._apply_font_scaling(self._font))
|
||||
|
||||
elif sys.platform.startswith("win"):
|
||||
super().configure(tearoff=False,
|
||||
relief="flat",
|
||||
activebackground=ThemeManager._apply_appearance_mode(self._hover_color, self._appearance_mode),
|
||||
borderwidth=self._apply_widget_scaling(4),
|
||||
activeborderwidth=self._apply_widget_scaling(4),
|
||||
bg=ThemeManager._apply_appearance_mode(self._fg_color, self._appearance_mode),
|
||||
fg=ThemeManager._apply_appearance_mode(self._text_color, self._appearance_mode),
|
||||
activeforeground=ThemeManager._apply_appearance_mode(self._text_color, self._appearance_mode),
|
||||
font=self._apply_font_scaling(self._font),
|
||||
cursor="hand2")
|
||||
|
||||
else:
|
||||
super().configure(tearoff=False,
|
||||
relief="flat",
|
||||
activebackground=ThemeManager._apply_appearance_mode(self._hover_color, self._appearance_mode),
|
||||
borderwidth=0,
|
||||
activeborderwidth=0,
|
||||
bg=ThemeManager._apply_appearance_mode(self._fg_color, self._appearance_mode),
|
||||
fg=ThemeManager._apply_appearance_mode(self._text_color, self._appearance_mode),
|
||||
activeforeground=ThemeManager._apply_appearance_mode(self._text_color, self._appearance_mode),
|
||||
font=self._apply_font_scaling(self._font))
|
||||
|
||||
def _add_menu_commands(self):
|
||||
""" delete existing menu labels and createe new labels with command according to values list """
|
||||
|
||||
self.delete(0, "end") # delete all old commands
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
for value in self._values:
|
||||
self.add_command(label=" " + value.ljust(self._min_character_width) + " ",
|
||||
command=lambda v=value: self._button_callback(v),
|
||||
compound="left")
|
||||
else:
|
||||
for value in self._values:
|
||||
self.add_command(label=value.ljust(self._min_character_width),
|
||||
command=lambda v=value: self._button_callback(v),
|
||||
compound="left")
|
||||
|
||||
def _button_callback(self, value):
|
||||
if self._command is not None:
|
||||
self._command(value)
|
||||
|
||||
def open(self, x: Union[int, float], y: Union[int, float]):
|
||||
|
||||
if sys.platform == "darwin":
|
||||
y += self._apply_widget_scaling(8)
|
||||
else:
|
||||
y += self._apply_widget_scaling(3)
|
||||
|
||||
if sys.platform == "darwin" or sys.platform.startswith("win"):
|
||||
self.post(int(x), int(y))
|
||||
else: # Linux
|
||||
self.tk_popup(int(x), int(y))
|
||||
|
||||
def configure(self, **kwargs):
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
super().configure(bg=ThemeManager._apply_appearance_mode(self._fg_color, self._appearance_mode))
|
||||
|
||||
if "hover_color" in kwargs:
|
||||
self._hover_color = kwargs.pop("hover_color")
|
||||
super().configure(activebackground=ThemeManager._apply_appearance_mode(self._hover_color, self._appearance_mode))
|
||||
|
||||
if "text_color" in kwargs:
|
||||
self._text_color = kwargs.pop("text_color")
|
||||
super().configure(fg=ThemeManager._apply_appearance_mode(self._text_color, self._appearance_mode))
|
||||
|
||||
if "font" in kwargs:
|
||||
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")
|
||||
|
||||
if "values" in kwargs:
|
||||
self._values = kwargs.pop("values")
|
||||
self._add_menu_commands()
|
||||
|
||||
super().configure(**kwargs)
|
||||
|
||||
def cget(self, attribute_name: str) -> any:
|
||||
if attribute_name == "min_character_width":
|
||||
return self._min_character_width
|
||||
|
||||
elif attribute_name == "fg_color":
|
||||
return self._fg_color
|
||||
elif attribute_name == "hover_color":
|
||||
return self._hover_color
|
||||
elif attribute_name == "text_color":
|
||||
return self._text_color
|
||||
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
elif attribute_name == "values":
|
||||
return self._values
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
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")
|
||||
|
||||
@staticmethod
|
||||
def _check_font_type(font: any):
|
||||
if isinstance(font, CTkFont):
|
||||
return font
|
||||
|
||||
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:
|
||||
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='<name>', size=<size in px>)\n" +
|
||||
f"font=('<name>', <size in px>)\n")
|
||||
|
||||
def _set_scaling(self, new_widget_scaling, new_window_scaling):
|
||||
self._widget_scaling = new_widget_scaling
|
||||
|
||||
self._configure_menu_for_platforms()
|
||||
|
||||
def _set_appearance_mode(self, mode_string):
|
||||
""" colors won't update on appearance mode change when dropdown is open, because it's not necessary """
|
||||
|
||||
if mode_string.lower() == "dark":
|
||||
self._appearance_mode = 1
|
||||
elif mode_string.lower() == "light":
|
||||
self._appearance_mode = 0
|
||||
|
||||
self._configure_menu_for_platforms()
|
@ -0,0 +1,296 @@
|
||||
import sys
|
||||
import tkinter
|
||||
import tkinter.ttk as ttk
|
||||
from typing import Union, Callable, Tuple
|
||||
|
||||
try:
|
||||
from typing import TypedDict
|
||||
except ImportError:
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 """
|
||||
|
||||
# attributes that are passed to and managed by the tkinter frame only:
|
||||
_valid_tk_frame_attributes: set = {"cursor"}
|
||||
|
||||
_cursor_manipulation_enabled: bool = True
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 0,
|
||||
height: int = 0,
|
||||
|
||||
bg_color: Union[str, Tuple[str, str], None] = None,
|
||||
**kwargs):
|
||||
|
||||
# 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 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
|
||||
|
||||
# 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))
|
||||
|
||||
# save latest geometry function and kwargs
|
||||
class GeometryCallDict(TypedDict):
|
||||
function: Callable
|
||||
kwargs: dict
|
||||
self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
|
||||
|
||||
# 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
|
||||
if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame)) and not isinstance(self.master, (CTkBaseClass, CTk, CTkToplevel)):
|
||||
master_old_configure = self.master.config
|
||||
|
||||
def new_configure(*args, **kwargs):
|
||||
if "bg" in kwargs:
|
||||
self.configure(bg_color=kwargs["bg"])
|
||||
elif "background" in kwargs:
|
||||
self.configure(bg_color=kwargs["background"])
|
||||
|
||||
# args[0] is dict when attribute gets changed by widget[<attribute>] syntax
|
||||
elif len(args) > 0 and type(args[0]) == dict:
|
||||
if "bg" in args[0]:
|
||||
self.configure(bg_color=args[0]["bg"])
|
||||
elif "background" in args[0]:
|
||||
self.configure(bg_color=args[0]["background"])
|
||||
master_old_configure(*args, **kwargs)
|
||||
|
||||
self.master.config = new_configure
|
||||
self.master.configure = new_configure
|
||||
|
||||
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
|
||||
|
||||
def config(self, *args, **kwargs):
|
||||
raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
""" basic configure with bg_color, width, height support, calls configure of tkinter.Frame, updates in the end """
|
||||
|
||||
if "width" in kwargs:
|
||||
self._set_dimensions(width=kwargs.pop("width"))
|
||||
|
||||
if "height" in kwargs:
|
||||
self._set_dimensions(height=kwargs.pop("height"))
|
||||
|
||||
if "bg_color" in kwargs:
|
||||
new_bg_color = kwargs.pop("bg_color")
|
||||
if new_bg_color is None:
|
||||
self._bg_color = self._detect_color_of_master()
|
||||
else:
|
||||
self._bg_color = new_bg_color
|
||||
require_redraw = True
|
||||
|
||||
super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes)) # configure tkinter.Frame
|
||||
|
||||
# if there are still items in the kwargs dict, raise ValueError
|
||||
check_kwargs_empty(kwargs, raise_error=True)
|
||||
|
||||
if require_redraw:
|
||||
self._draw()
|
||||
|
||||
def cget(self, attribute_name: str):
|
||||
""" basic cget with bg_color, width, height support, calls cget of tkinter.Frame """
|
||||
|
||||
if attribute_name == "bg_color":
|
||||
return self._bg_color
|
||||
elif attribute_name == "width":
|
||||
return self._desired_width
|
||||
elif attribute_name == "height":
|
||||
return self._desired_height
|
||||
|
||||
elif attribute_name in self._valid_tk_frame_attributes:
|
||||
return super().cget(attribute_name) # cget of tkinter.Frame
|
||||
else:
|
||||
raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.")
|
||||
|
||||
@staticmethod
|
||||
def _check_font_type(font: any):
|
||||
if isinstance(font, CTkFont):
|
||||
return font
|
||||
|
||||
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:
|
||||
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='<name>', size=<size in px>)\n" +
|
||||
f"font=('<name>', <size in px>)\n")
|
||||
|
||||
def _update_dimensions_event(self, event):
|
||||
# only redraw if dimensions changed (for performance), independent of scaling
|
||||
if round(self._current_width) != round(event.width / self._widget_scaling) or round(self._current_height) != round(event.height / self._widget_scaling):
|
||||
self._current_width = (event.width / self._widget_scaling) # adjust current size according to new size given by event
|
||||
self._current_height = (event.height / self._widget_scaling) # _current_width and _current_height are independent of the scale
|
||||
|
||||
self._draw(no_color_updates=True) # faster drawing without color changes
|
||||
|
||||
def _detect_color_of_master(self, master_widget=None) -> Union[str, Tuple[str, str]]:
|
||||
""" detect color of self.master widget to set correct _bg_color """
|
||||
|
||||
if master_widget is None:
|
||||
master_widget = self.master
|
||||
|
||||
if isinstance(master_widget, (CTkBaseClass, CTk, CTkToplevel)) and hasattr(master_widget, "_fg_color"):
|
||||
if master_widget.cget("fg_color") is not None:
|
||||
return master_widget.cget("fg_color")
|
||||
|
||||
# if fg_color of master is None, try to retrieve fg_color from master of master
|
||||
elif hasattr(master_widget.master, "master"):
|
||||
return self._detect_color_of_master(master_widget.master)
|
||||
|
||||
elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget
|
||||
try:
|
||||
ttk_style = ttk.Style()
|
||||
return ttk_style.lookup(master_widget.winfo_class(), 'background')
|
||||
except Exception:
|
||||
return "#FFFFFF", "#000000"
|
||||
|
||||
else: # master is normal tkinter widget
|
||||
try:
|
||||
return master_widget.cget("bg") # try to get bg color by .cget() method
|
||||
except Exception:
|
||||
return "#FFFFFF", "#000000"
|
||||
|
||||
def _set_appearance_mode(self, mode_string):
|
||||
if mode_string.lower() == "dark":
|
||||
self._appearance_mode = 1
|
||||
elif mode_string.lower() == "light":
|
||||
self._appearance_mode = 0
|
||||
|
||||
super().configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
self._draw()
|
||||
|
||||
def _set_scaling(self, new_widget_scaling, new_window_scaling):
|
||||
self._widget_scaling = new_widget_scaling
|
||||
|
||||
super().configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
|
||||
if self._last_geometry_manager_call is not None:
|
||||
self._last_geometry_manager_call["function"](**self._apply_argument_scaling(self._last_geometry_manager_call["kwargs"]))
|
||||
|
||||
def _set_dimensions(self, width=None, height=None):
|
||||
if width is not None:
|
||||
self._desired_width = width
|
||||
if height is not None:
|
||||
self._desired_height = height
|
||||
|
||||
super().configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
|
||||
def place(self, **kwargs):
|
||||
"""
|
||||
Place a widget in the parent widget. Use as options:
|
||||
in=master - master relative to which the widget is placed
|
||||
in_=master - see 'in' option description
|
||||
x=amount - locate anchor of this widget at position x of master
|
||||
y=amount - locate anchor of this widget at position y of master
|
||||
relx=amount - locate anchor of this widget between 0.0 and 1.0 relative to width of master (1.0 is right edge)
|
||||
rely=amount - locate anchor of this widget between 0.0 and 1.0 relative to height of master (1.0 is bottom edge)
|
||||
anchor=NSEW (or subset) - position anchor according to given direction
|
||||
width=amount - width of this widget in pixel
|
||||
height=amount - height of this widget in pixel
|
||||
relwidth=amount - width of this widget between 0.0 and 1.0 relative to width of master (1.0 is the same width as the master)
|
||||
relheight=amount - height of this widget between 0.0 and 1.0 relative to height of master (1.0 is the same height as the master)
|
||||
bordermode="inside" or "outside" - whether to take border width of master widget into account
|
||||
"""
|
||||
self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs}
|
||||
return super().place(**self._apply_argument_scaling(kwargs))
|
||||
|
||||
def place_forget(self):
|
||||
""" Unmap this widget. """
|
||||
self._last_geometry_manager_call = None
|
||||
return super().place_forget()
|
||||
|
||||
def pack(self, **kwargs):
|
||||
"""
|
||||
Pack a widget in the parent widget. Use as options:
|
||||
after=widget - pack it after you have packed widget
|
||||
anchor=NSEW (or subset) - position widget according to given direction
|
||||
before=widget - pack it before you will pack widget
|
||||
expand=bool - expand widget if parent size grows
|
||||
fill=NONE or X or Y or BOTH - fill widget if widget grows
|
||||
in=master - use master to contain this widget
|
||||
in_=master - see 'in' option description
|
||||
ipadx=amount - add internal padding in x direction
|
||||
ipady=amount - add internal padding in y direction
|
||||
padx=amount - add padding in x direction
|
||||
pady=amount - add padding in y direction
|
||||
side=TOP or BOTTOM or LEFT or RIGHT - where to add this widget.
|
||||
"""
|
||||
self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
|
||||
return super().pack(**self._apply_argument_scaling(kwargs))
|
||||
|
||||
def pack_forget(self):
|
||||
""" Unmap this widget and do not use it for the packing order. """
|
||||
self._last_geometry_manager_call = None
|
||||
return super().pack_forget()
|
||||
|
||||
def grid(self, **kwargs):
|
||||
"""
|
||||
Position a widget in the parent widget in a grid. Use as options:
|
||||
column=number - use cell identified with given column (starting with 0)
|
||||
columnspan=number - this widget will span several columns
|
||||
in=master - use master to contain this widget
|
||||
in_=master - see 'in' option description
|
||||
ipadx=amount - add internal padding in x direction
|
||||
ipady=amount - add internal padding in y direction
|
||||
padx=amount - add padding in x direction
|
||||
pady=amount - add padding in y direction
|
||||
row=number - use cell identified with given row (starting with 0)
|
||||
rowspan=number - this widget will span several rows
|
||||
sticky=NSEW - if cell is larger on which sides will this widget stick to the cell boundary
|
||||
"""
|
||||
self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
|
||||
return super().grid(**self._apply_argument_scaling(kwargs))
|
||||
|
||||
def grid_forget(self):
|
||||
""" Unmap this widget. """
|
||||
self._last_geometry_manager_call = None
|
||||
return super().grid_forget()
|
530
customtkinter/windows/widgets/ctk_button.py
Normal file
530
customtkinter/windows/widgets/ctk_button.py
Normal file
@ -0,0 +1,530 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Union, Tuple, Callable
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
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
|
||||
from .image.ctk_image import CTkImage
|
||||
|
||||
|
||||
class CTkButton(CTkBaseClass):
|
||||
"""
|
||||
Button with rounded corners, border, hover effect, image support, click command and textvariable.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
_image_label_spacing = 6
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 140,
|
||||
height: int = 28,
|
||||
corner_radius: Union[int, str] = "default_theme",
|
||||
border_width: Union[int, str] = "default_theme",
|
||||
border_spacing: int = 2,
|
||||
|
||||
bg_color: Union[str, Tuple[str, str], None] = None,
|
||||
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
|
||||
hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
border_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
background_corner_colors: Tuple[Union[str, Tuple[str, str]]] = None,
|
||||
round_width_to_even_numbers: bool = True,
|
||||
round_height_to_even_numbers: bool = True,
|
||||
|
||||
text: str = "CTkButton",
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
textvariable: tkinter.Variable = None,
|
||||
image: Union[tkinter.PhotoImage, CTkImage] = None,
|
||||
state: str = "normal",
|
||||
hover: bool = True,
|
||||
command: Callable = None,
|
||||
compound: str = "left",
|
||||
anchor: str = "center",
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# color
|
||||
self._fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
|
||||
self._hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
|
||||
self._border_color = ThemeManager.theme["color"]["button_border"] if border_color == "default_theme" else border_color
|
||||
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._background_corner_colors = background_corner_colors # rendering options for DrawEngine
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
self._border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width
|
||||
self._round_width_to_even_numbers = round_width_to_even_numbers # rendering options for DrawEngine
|
||||
self._round_height_to_even_numbers = round_height_to_even_numbers # rendering options for DrawEngine
|
||||
self._corner_radius = min(self._corner_radius, round(self._current_height/2))
|
||||
self._compound = compound
|
||||
self._anchor = anchor
|
||||
self._border_spacing = border_spacing
|
||||
|
||||
# text, image
|
||||
self._image = image
|
||||
self._image_label: Union[tkinter.Label, None] = None
|
||||
self._text = text
|
||||
self._text_label: Union[tkinter.Label, None] = None
|
||||
|
||||
# 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
|
||||
self._textvariable = textvariable
|
||||
self._state = state
|
||||
self._hover = hover
|
||||
self._click_animation_running: bool = False
|
||||
|
||||
# canvas
|
||||
self._canvas = CTkCanvas(master=self,
|
||||
highlightthickness=0,
|
||||
width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew")
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
self._draw_engine.set_round_to_even_numbers(self._round_width_to_even_numbers, self._round_height_to_even_numbers) # rendering options
|
||||
|
||||
# canvas event bindings
|
||||
self._canvas.bind("<Enter>", self._on_enter)
|
||||
self._canvas.bind("<Leave>", self._on_leave)
|
||||
self._canvas.bind("<Button-1>", self._clicked)
|
||||
self._canvas.bind("<Button-1>", self._clicked)
|
||||
|
||||
# configure cursor and initial draw
|
||||
self._set_cursor()
|
||||
self._draw()
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self._create_grid()
|
||||
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(font=self._apply_font_scaling(self._font))
|
||||
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
def _set_dimensions(self, width: int = None, height: int = 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 _update_font(self):
|
||||
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
|
||||
if self._text_label is not None:
|
||||
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, rowspan=5, columnspan=5, 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 self._background_corner_colors is not None:
|
||||
self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
|
||||
self._apply_widget_scaling(self._current_height))
|
||||
self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0]))
|
||||
self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1]))
|
||||
self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2]))
|
||||
self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3]))
|
||||
else:
|
||||
self._canvas.delete("background_parts")
|
||||
|
||||
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:
|
||||
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
# set color for the button border parts (outline)
|
||||
self._canvas.itemconfig("border_parts",
|
||||
outline=self._apply_appearance_mode(self._border_color),
|
||||
fill=self._apply_appearance_mode(self._border_color))
|
||||
|
||||
# set color for inner button parts
|
||||
if self._fg_color is None:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
outline=self._apply_appearance_mode(self._bg_color),
|
||||
fill=self._apply_appearance_mode(self._bg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
outline=self._apply_appearance_mode(self._fg_color),
|
||||
fill=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
# create text label if text given
|
||||
if self._text is not None and self._text != "":
|
||||
|
||||
if self._text_label is None:
|
||||
self._text_label = tkinter.Label(master=self,
|
||||
font=self._apply_font_scaling(self._font),
|
||||
text=self._text,
|
||||
padx=0,
|
||||
pady=0,
|
||||
borderwidth=1,
|
||||
textvariable=self._textvariable)
|
||||
self._create_grid()
|
||||
|
||||
self._text_label.bind("<Enter>", self._on_enter)
|
||||
self._text_label.bind("<Leave>", self._on_leave)
|
||||
self._text_label.bind("<Button-1>", self._clicked)
|
||||
self._text_label.bind("<Button-1>", self._clicked)
|
||||
|
||||
if no_color_updates is False:
|
||||
# set text_label fg color (text color)
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
if self._state == tkinter.DISABLED:
|
||||
self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
|
||||
else:
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
if self._fg_color is None:
|
||||
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
else:
|
||||
self._text_label.configure(bg=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
else:
|
||||
# delete text_label if no text given
|
||||
if self._text_label is not None:
|
||||
self._text_label.destroy()
|
||||
self._text_label = None
|
||||
self._create_grid()
|
||||
|
||||
# create image label if image given
|
||||
if self._image is not None:
|
||||
|
||||
if self._image_label is None:
|
||||
self._image_label = tkinter.Label(master=self)
|
||||
self._create_grid()
|
||||
|
||||
self._image_label.bind("<Enter>", self._on_enter)
|
||||
self._image_label.bind("<Leave>", self._on_leave)
|
||||
self._image_label.bind("<Button-1>", self._clicked)
|
||||
self._image_label.bind("<Button-1>", self._clicked)
|
||||
|
||||
if no_color_updates is False:
|
||||
# set image_label bg color (background color of label)
|
||||
if self._fg_color is None:
|
||||
self._image_label.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
else:
|
||||
self._image_label.configure(bg=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
self._image_label.configure(image=self._image) # set image
|
||||
|
||||
else:
|
||||
# delete text_label if no text given
|
||||
if self._image_label is not None:
|
||||
self._image_label.destroy()
|
||||
self._image_label = None
|
||||
self._create_grid()
|
||||
|
||||
def _create_grid(self):
|
||||
""" configure grid system (5x5) """
|
||||
|
||||
# Outer rows and columns have weight of 1000 to overpower the rows and columns of the label and image with weight 1.
|
||||
# Rows and columns of image and label need weight of 1 to collapse in case of missing space on the button,
|
||||
# so image and label need sticky option to stick together in the center, and therefore outer rows and columns
|
||||
# need weight of 100 in case of other anchor than center.
|
||||
n_padding_weight, s_padding_weight, e_padding_weight, w_padding_weight = 1000, 1000, 1000, 1000
|
||||
if self._anchor != "center":
|
||||
if "n" in self._anchor:
|
||||
n_padding_weight, s_padding_weight = 0, 1000
|
||||
if "s" in self._anchor:
|
||||
n_padding_weight, s_padding_weight = 1000, 0
|
||||
if "e" in self._anchor:
|
||||
e_padding_weight, w_padding_weight = 1000, 0
|
||||
if "w" in self._anchor:
|
||||
e_padding_weight, w_padding_weight = 0, 1000
|
||||
|
||||
scaled_minsize_rows = self._apply_widget_scaling(max(self._border_width + 1, self._border_spacing))
|
||||
scaled_minsize_columns = self._apply_widget_scaling(max(self._corner_radius, self._border_width + 1, self._border_spacing))
|
||||
|
||||
self.grid_rowconfigure(0, weight=n_padding_weight, minsize=scaled_minsize_rows)
|
||||
self.grid_rowconfigure(4, weight=s_padding_weight, minsize=scaled_minsize_rows)
|
||||
self.grid_columnconfigure(0, weight=e_padding_weight, minsize=scaled_minsize_columns)
|
||||
self.grid_columnconfigure(4, weight=w_padding_weight, minsize=scaled_minsize_columns)
|
||||
|
||||
if self._compound in ("right", "left"):
|
||||
self.grid_rowconfigure(2, weight=1)
|
||||
if self._image_label is not None and self._text_label is not None:
|
||||
self.grid_columnconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing))
|
||||
else:
|
||||
self.grid_columnconfigure(2, weight=0)
|
||||
|
||||
self.grid_rowconfigure((1, 3), weight=0)
|
||||
self.grid_columnconfigure((1, 3), weight=1)
|
||||
else:
|
||||
self.grid_columnconfigure(2, weight=1)
|
||||
if self._image_label is not None and self._text_label is not None:
|
||||
self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing))
|
||||
else:
|
||||
self.grid_rowconfigure(2, weight=0)
|
||||
|
||||
self.grid_columnconfigure((1, 3), weight=0)
|
||||
self.grid_rowconfigure((1, 3), weight=1)
|
||||
|
||||
if self._compound == "right":
|
||||
if self._image_label is not None:
|
||||
self._image_label.grid(row=2, column=3, sticky="w")
|
||||
if self._text_label is not None:
|
||||
self._text_label.grid(row=2, column=1, sticky="e")
|
||||
elif self._compound == "left":
|
||||
if self._image_label is not None:
|
||||
self._image_label.grid(row=2, column=1, sticky="e")
|
||||
if self._text_label is not None:
|
||||
self._text_label.grid(row=2, column=3, sticky="w")
|
||||
elif self._compound == "top":
|
||||
if self._image_label is not None:
|
||||
self._image_label.grid(row=1, column=2, sticky="s")
|
||||
if self._text_label is not None:
|
||||
self._text_label.grid(row=3, column=2, sticky="n")
|
||||
elif self._compound == "bottom":
|
||||
if self._image_label is not None:
|
||||
self._image_label.grid(row=3, column=2, sticky="n")
|
||||
if self._text_label is not None:
|
||||
self._text_label.grid(row=1, column=2, sticky="s")
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "corner_radius" in kwargs:
|
||||
self._corner_radius = kwargs.pop("corner_radius")
|
||||
self._create_grid()
|
||||
require_redraw = True
|
||||
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
self._create_grid()
|
||||
require_redraw = True
|
||||
|
||||
if "border_spacing" in kwargs:
|
||||
self._border_spacing = kwargs.pop("border_spacing")
|
||||
self._create_grid()
|
||||
require_redraw = True
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "hover_color" in kwargs:
|
||||
self._hover_color = kwargs.pop("hover_color")
|
||||
require_redraw = True
|
||||
|
||||
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 "text_color_disabled" in kwargs:
|
||||
self._text_color_disabled = kwargs.pop("text_color_disabled")
|
||||
require_redraw = True
|
||||
|
||||
if "background_corner_colors" in kwargs:
|
||||
self._background_corner_colors = kwargs.pop("background_corner_colors")
|
||||
require_redraw = True
|
||||
|
||||
if "text" in kwargs:
|
||||
self._text = kwargs.pop("text")
|
||||
if self._text_label is None:
|
||||
require_redraw = True # text_label will be created in .draw()
|
||||
else:
|
||||
self._text_label.configure(text=self._text)
|
||||
self._create_grid()
|
||||
|
||||
if "font" in kwargs:
|
||||
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 "textvariable" in kwargs:
|
||||
self._textvariable = kwargs.pop("textvariable")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(textvariable=self._textvariable)
|
||||
|
||||
if "image" in kwargs:
|
||||
self._image = kwargs.pop("image")
|
||||
self._create_grid()
|
||||
require_redraw = True
|
||||
|
||||
if "state" in kwargs:
|
||||
self._state = kwargs.pop("state")
|
||||
self._set_cursor()
|
||||
require_redraw = True
|
||||
|
||||
if "hover" in kwargs:
|
||||
self._hover = kwargs.pop("hover")
|
||||
|
||||
if "command" in kwargs:
|
||||
self._command = kwargs.pop("command")
|
||||
|
||||
if "compound" in kwargs:
|
||||
self._compound = kwargs.pop("compound")
|
||||
require_redraw = True
|
||||
|
||||
if "anchor" in kwargs:
|
||||
self._anchor = kwargs.pop("anchor")
|
||||
require_redraw = True
|
||||
|
||||
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 == "border_spacing":
|
||||
return self._border_spacing
|
||||
|
||||
elif attribute_name == "fg_color":
|
||||
return self._fg_color
|
||||
elif attribute_name == "hover_color":
|
||||
return self._hover_color
|
||||
elif attribute_name == "border_color":
|
||||
return self._border_color
|
||||
elif attribute_name == "text_color":
|
||||
return self._text_color
|
||||
elif attribute_name == "text_color_disabled":
|
||||
return self._text_color_disabled
|
||||
elif attribute_name == "background_corner_colors":
|
||||
return self._background_corner_colors
|
||||
|
||||
elif attribute_name == "text":
|
||||
return self._text
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "textvariable":
|
||||
return self._textvariable
|
||||
elif attribute_name == "image":
|
||||
return self._image
|
||||
elif attribute_name == "state":
|
||||
return self._state
|
||||
elif attribute_name == "hover":
|
||||
return self._hover
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
elif attribute_name == "compound":
|
||||
return self._compound
|
||||
elif attribute_name == "anchor":
|
||||
return self._anchor
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def _set_cursor(self):
|
||||
if self._cursor_manipulation_enabled:
|
||||
if self._state == tkinter.DISABLED:
|
||||
if sys.platform == "darwin" and self._command is not None:
|
||||
self.configure(cursor="arrow")
|
||||
elif sys.platform.startswith("win") and self._command is not None:
|
||||
self.configure(cursor="arrow")
|
||||
|
||||
elif self._state == tkinter.NORMAL:
|
||||
if sys.platform == "darwin" and self._command is not None:
|
||||
self.configure(cursor="pointinghand")
|
||||
elif sys.platform.startswith("win") and self._command is not None:
|
||||
self.configure(cursor="hand2")
|
||||
|
||||
def _on_enter(self, event=None):
|
||||
if self._hover is True and self._state == "normal":
|
||||
if self._hover_color is None:
|
||||
inner_parts_color = self._fg_color
|
||||
else:
|
||||
inner_parts_color = self._hover_color
|
||||
|
||||
# set color of inner button parts to hover color
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
outline=self._apply_appearance_mode(inner_parts_color),
|
||||
fill=self._apply_appearance_mode(inner_parts_color))
|
||||
|
||||
# set text_label bg color to button hover color
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
|
||||
|
||||
# set image_label bg color to button hover color
|
||||
if self._image_label is not None:
|
||||
self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
|
||||
|
||||
def _on_leave(self, event=None):
|
||||
self._click_animation_running = False
|
||||
|
||||
if self._fg_color is None:
|
||||
inner_parts_color = self._bg_color
|
||||
else:
|
||||
inner_parts_color = self._fg_color
|
||||
|
||||
# set color of inner button parts
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
outline=self._apply_appearance_mode(inner_parts_color),
|
||||
fill=self._apply_appearance_mode(inner_parts_color))
|
||||
|
||||
# set text_label bg color (label color)
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
|
||||
|
||||
# set image_label bg color (image bg color)
|
||||
if self._image_label is not None:
|
||||
self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color))
|
||||
|
||||
def _click_animation(self):
|
||||
if self._click_animation_running:
|
||||
self._on_enter()
|
||||
|
||||
def _clicked(self, event=None):
|
||||
if self._command is not None:
|
||||
if self._state != tkinter.DISABLED:
|
||||
|
||||
# click animation: change color with .on_leave() and back to normal after 100ms with click_animation()
|
||||
self._on_leave()
|
||||
self._click_animation_running = True
|
||||
self.after(100, self._click_animation)
|
||||
|
||||
self._command()
|
||||
|
||||
def bind(self, sequence: str = None, command: Callable = None, add: str = None) -> str:
|
||||
""" called on the tkinter.Label and tkinter.Canvas """
|
||||
canvas_bind_return = self._canvas.bind(sequence, command, add)
|
||||
label_bind_return = self._text_label.bind(sequence, command, add)
|
||||
return canvas_bind_return + " + " + label_bind_return
|
||||
|
||||
def unbind(self, sequence: str, funcid: str = None):
|
||||
""" called on the tkinter.Label and tkinter.Canvas """
|
||||
canvas_bind_return, label_bind_return = funcid.split(" + ")
|
||||
self._canvas.unbind(sequence, canvas_bind_return)
|
||||
self._text_label.unbind(sequence, label_bind_return)
|
||||
|
||||
def focus(self):
|
||||
return self._text_label.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._text_label.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._text_label.focus_force()
|
438
customtkinter/windows/widgets/ctk_checkbox.py
Normal file
438
customtkinter/windows/widgets/ctk_checkbox.py
Normal file
@ -0,0 +1,438 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Union, Tuple, Callable
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
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
|
||||
|
||||
|
||||
class CTkCheckBox(CTkBaseClass):
|
||||
"""
|
||||
Checkbox with rounded corners, border, variable support and hover effect.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 100,
|
||||
height: int = 24,
|
||||
checkbox_width: int = 24,
|
||||
checkbox_height: int = 24,
|
||||
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]] = "default_theme",
|
||||
hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
border_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
checkmark_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
text: str = "CTkCheckBox",
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
textvariable: tkinter.Variable = None,
|
||||
state: str = tkinter.NORMAL,
|
||||
hover: bool = True,
|
||||
command: Callable = None,
|
||||
onvalue: Union[int, str] = 1,
|
||||
offvalue: Union[int, str] = 0,
|
||||
variable: tkinter.Variable = None,
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# dimensions
|
||||
self._checkbox_width = checkbox_width
|
||||
self._checkbox_height = checkbox_height
|
||||
|
||||
# color
|
||||
self._fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
|
||||
self._hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
|
||||
self._border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
|
||||
self._checkmark_color = ThemeManager.theme["color"]["checkmark"] if checkmark_color == "default_theme" else checkmark_color
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["checkbox_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
self._border_width = ThemeManager.theme["shape"]["checkbox_border_width"] if border_width == "default_theme" else border_width
|
||||
|
||||
# text
|
||||
self._text = text
|
||||
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
|
||||
|
||||
# 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
|
||||
self._state = state
|
||||
self._hover = hover
|
||||
self._check_state = False
|
||||
|
||||
self._onvalue = onvalue
|
||||
self._offvalue = offvalue
|
||||
self._variable: tkinter.Variable = variable
|
||||
self._variable_callback_blocked = False
|
||||
self._textvariable: tkinter.Variable = textvariable
|
||||
self._variable_callback_name = None
|
||||
|
||||
# configure grid system (1x3)
|
||||
self.grid_columnconfigure(0, weight=0)
|
||||
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
|
||||
self.grid_columnconfigure(2, weight=1)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
|
||||
self._bg_canvas = CTkCanvas(master=self,
|
||||
highlightthickness=0,
|
||||
width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
|
||||
|
||||
self._canvas = CTkCanvas(master=self,
|
||||
highlightthickness=0,
|
||||
width=self._apply_widget_scaling(self._checkbox_width),
|
||||
height=self._apply_widget_scaling(self._checkbox_height))
|
||||
self._canvas.grid(row=0, column=0, sticky="e")
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._canvas.bind("<Enter>", self._on_enter)
|
||||
self._canvas.bind("<Leave>", self._on_leave)
|
||||
self._canvas.bind("<Button-1>", self.toggle)
|
||||
|
||||
self._text_label = tkinter.Label(master=self,
|
||||
bd=0,
|
||||
padx=0,
|
||||
pady=0,
|
||||
text=self._text,
|
||||
justify=tkinter.LEFT,
|
||||
font=self._apply_font_scaling(self._font),
|
||||
textvariable=self._textvariable)
|
||||
self._text_label.grid(row=0, column=2, sticky="w")
|
||||
self._text_label["anchor"] = "w"
|
||||
|
||||
self._text_label.bind("<Enter>", self._on_enter)
|
||||
self._text_label.bind("<Leave>", self._on_leave)
|
||||
self._text_label.bind("<Button-1>", self.toggle)
|
||||
|
||||
# register variable callback and set state according to variable
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self._check_state = True if self._variable.get() == self._onvalue else False
|
||||
|
||||
self._draw() # initial draw
|
||||
self._set_cursor()
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
|
||||
self._text_label.configure(font=self._apply_font_scaling(self._font))
|
||||
|
||||
self._canvas.delete("checkmark")
|
||||
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width),
|
||||
height=self._apply_widget_scaling(self._checkbox_height))
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
def _set_dimensions(self, width: int = None, height: int = None):
|
||||
super()._set_dimensions(width, height)
|
||||
|
||||
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 """
|
||||
if self._text_label is not None:
|
||||
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):
|
||||
requires_recoloring_1 = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._checkbox_width),
|
||||
self._apply_widget_scaling(self._checkbox_height),
|
||||
self._apply_widget_scaling(self._corner_radius),
|
||||
self._apply_widget_scaling(self._border_width))
|
||||
|
||||
if self._check_state is True:
|
||||
requires_recoloring_2 = self._draw_engine.draw_checkmark(self._apply_widget_scaling(self._checkbox_width),
|
||||
self._apply_widget_scaling(self._checkbox_height),
|
||||
self._apply_widget_scaling(self._checkbox_height * 0.58))
|
||||
else:
|
||||
requires_recoloring_2 = False
|
||||
self._canvas.delete("checkmark")
|
||||
|
||||
if no_color_updates is False or requires_recoloring_1 or requires_recoloring_2:
|
||||
self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
if self._check_state is True:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
outline=self._apply_appearance_mode(self._fg_color),
|
||||
fill=self._apply_appearance_mode(self._fg_color))
|
||||
self._canvas.itemconfig("border_parts",
|
||||
outline=self._apply_appearance_mode(self._fg_color),
|
||||
fill=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
if "create_line" in self._canvas.gettags("checkmark"):
|
||||
self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color))
|
||||
else:
|
||||
self._canvas.itemconfig("checkmark", fill=self._apply_appearance_mode(self._checkmark_color))
|
||||
else:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
outline=self._apply_appearance_mode(self._bg_color),
|
||||
fill=self._apply_appearance_mode(self._bg_color))
|
||||
self._canvas.itemconfig("border_parts",
|
||||
outline=self._apply_appearance_mode(self._border_color),
|
||||
fill=self._apply_appearance_mode(self._border_color))
|
||||
|
||||
if self._state == tkinter.DISABLED:
|
||||
self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
|
||||
else:
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "checkbox_width" in kwargs:
|
||||
self._checkbox_width = kwargs.pop("checkbox_width")
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width))
|
||||
require_redraw = True
|
||||
|
||||
if "checkbox_height" in kwargs:
|
||||
self._checkbox_height = kwargs.pop("checkbox_height")
|
||||
self._canvas.configure(height=self._apply_widget_scaling(self._checkbox_height))
|
||||
require_redraw = True
|
||||
|
||||
if "text" in kwargs:
|
||||
self._text = kwargs.pop("text")
|
||||
self._text_label.configure(text=self._text)
|
||||
|
||||
if "font" in kwargs:
|
||||
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")
|
||||
self._set_cursor()
|
||||
require_redraw = True
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "hover_color" in kwargs:
|
||||
self._hover_color = kwargs.pop("hover_color")
|
||||
require_redraw = True
|
||||
|
||||
if "text_color" in kwargs:
|
||||
self._text_color = kwargs.pop("text_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_color" in kwargs:
|
||||
self._border_color = kwargs.pop("border_color")
|
||||
require_redraw = True
|
||||
|
||||
if "hover" in kwargs:
|
||||
self._hover = kwargs.pop("hover")
|
||||
|
||||
if "command" in kwargs:
|
||||
self._command = kwargs.pop("command")
|
||||
|
||||
if "textvariable" in kwargs:
|
||||
self._textvariable = kwargs.pop("textvariable")
|
||||
self._text_label.configure(textvariable=self._textvariable)
|
||||
|
||||
if "variable" in kwargs:
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable.trace_remove("write", self._variable_callback_name) # remove old variable callback
|
||||
|
||||
self._variable = kwargs.pop("variable")
|
||||
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self._check_state = True if self._variable.get() == self._onvalue else False
|
||||
require_redraw = True
|
||||
|
||||
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 == "checkbox_width":
|
||||
return self._checkbox_width
|
||||
elif attribute_name == "checkbox_height":
|
||||
return self._checkbox_height
|
||||
|
||||
elif attribute_name == "fg_color":
|
||||
return self._fg_color
|
||||
elif attribute_name == "hover_color":
|
||||
return self._hover_color
|
||||
elif attribute_name == "border_color":
|
||||
return self._border_color
|
||||
elif attribute_name == "checkmark_color":
|
||||
return self._checkmark_color
|
||||
elif attribute_name == "text_color":
|
||||
return self._text_color
|
||||
elif attribute_name == "text_color_disabled":
|
||||
return self._text_color_disabled
|
||||
|
||||
elif attribute_name == "text":
|
||||
return self._text
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "textvariable":
|
||||
return self._textvariable
|
||||
elif attribute_name == "state":
|
||||
return self._state
|
||||
elif attribute_name == "hover":
|
||||
return self._hover
|
||||
elif attribute_name == "onvalue":
|
||||
return self._onvalue
|
||||
elif attribute_name == "offvalue":
|
||||
return self._offvalue
|
||||
elif attribute_name == "variable":
|
||||
return self._variable
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def _set_cursor(self):
|
||||
if self._cursor_manipulation_enabled:
|
||||
if self._state == tkinter.DISABLED:
|
||||
if sys.platform == "darwin":
|
||||
self._canvas.configure(cursor="arrow")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="arrow")
|
||||
elif sys.platform.startswith("win"):
|
||||
self._canvas.configure(cursor="arrow")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="arrow")
|
||||
|
||||
elif self._state == tkinter.NORMAL:
|
||||
if sys.platform == "darwin":
|
||||
self._canvas.configure(cursor="pointinghand")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="pointinghand")
|
||||
elif sys.platform.startswith("win"):
|
||||
self._canvas.configure(cursor="hand2")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="hand2")
|
||||
|
||||
def _on_enter(self, event=0):
|
||||
if self._hover is True and self._state == tkinter.NORMAL:
|
||||
if self._check_state is True:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._hover_color),
|
||||
outline=self._apply_appearance_mode(self._hover_color))
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._hover_color),
|
||||
outline=self._apply_appearance_mode(self._hover_color))
|
||||
else:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._hover_color),
|
||||
outline=self._apply_appearance_mode(self._hover_color))
|
||||
|
||||
def _on_leave(self, event=0):
|
||||
if self._check_state is True:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._bg_color),
|
||||
outline=self._apply_appearance_mode(self._bg_color))
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._border_color),
|
||||
outline=self._apply_appearance_mode(self._border_color))
|
||||
|
||||
def _variable_callback(self, var_name, index, mode):
|
||||
if not self._variable_callback_blocked:
|
||||
if self._variable.get() == self._onvalue:
|
||||
self.select(from_variable_callback=True)
|
||||
elif self._variable.get() == self._offvalue:
|
||||
self.deselect(from_variable_callback=True)
|
||||
|
||||
def toggle(self, event=0):
|
||||
if self._state == tkinter.NORMAL:
|
||||
if self._check_state is True:
|
||||
self._check_state = False
|
||||
self._draw()
|
||||
else:
|
||||
self._check_state = True
|
||||
self._draw()
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
if self._command is not None:
|
||||
self._command()
|
||||
|
||||
def select(self, from_variable_callback=False):
|
||||
self._check_state = True
|
||||
self._draw()
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(self._onvalue)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def deselect(self, from_variable_callback=False):
|
||||
self._check_state = False
|
||||
self._draw()
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(self._offvalue)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def get(self) -> Union[int, str]:
|
||||
return self._onvalue if self._check_state is True else self._offvalue
|
||||
|
||||
def bind(self, sequence=None, command=None, add=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.bind(sequence, command, add)
|
||||
|
||||
def unbind(self, sequence, funcid=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.unbind(sequence, funcid)
|
||||
|
||||
def focus(self):
|
||||
return self._text_label.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._text_label.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._text_label.focus_force()
|
413
customtkinter/windows/widgets/ctk_combobox.py
Normal file
413
customtkinter/windows/widgets/ctk_combobox.py
Normal file
@ -0,0 +1,413 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Union, Tuple, Callable, List
|
||||
|
||||
from .core_widget_classes.dropdown_menu import DropdownMenu
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
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
|
||||
|
||||
|
||||
class CTkComboBox(CTkBaseClass):
|
||||
"""
|
||||
Combobox with dropdown menu, rounded corners, border, variable support.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 140,
|
||||
height: int = 28,
|
||||
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]] = "default_theme",
|
||||
border_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
button_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
button_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
dropdown_fg_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
dropdown_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
dropdown_text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
dropdown_font: Union[tuple, CTkFont] = "default_theme",
|
||||
values: List[str] = None,
|
||||
state: str = tkinter.NORMAL,
|
||||
hover: bool = True,
|
||||
variable: tkinter.Variable = None,
|
||||
command: Callable = None,
|
||||
justify: str = "left",
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# color variables
|
||||
self._fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
|
||||
self._border_color = ThemeManager.theme["color"]["combobox_border"] if border_color == "default_theme" else border_color
|
||||
self._button_color = ThemeManager.theme["color"]["combobox_border"] if button_color == "default_theme" else button_color
|
||||
self._button_hover_color = ThemeManager.theme["color"]["combobox_button_hover"] if button_hover_color == "default_theme" else button_hover_color
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
self._border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width
|
||||
|
||||
# text and font
|
||||
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
|
||||
|
||||
# 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
|
||||
self._variable = variable
|
||||
self._state = state
|
||||
self._hover = hover
|
||||
|
||||
if values is None:
|
||||
self._values = ["CTkComboBox"]
|
||||
else:
|
||||
self._values = values
|
||||
|
||||
self._dropdown_menu = DropdownMenu(master=self,
|
||||
values=self._values,
|
||||
command=self._dropdown_callback,
|
||||
fg_color=dropdown_fg_color,
|
||||
hover_color=dropdown_hover_color,
|
||||
text_color=dropdown_text_color,
|
||||
font=dropdown_font)
|
||||
|
||||
# configure grid system (1x1)
|
||||
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._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self.draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._entry = tkinter.Entry(master=self,
|
||||
state=self._state,
|
||||
width=1,
|
||||
bd=0,
|
||||
justify=justify,
|
||||
highlightthickness=0,
|
||||
font=self._apply_font_scaling(self._font))
|
||||
|
||||
self._create_grid()
|
||||
|
||||
# insert default value
|
||||
if len(self._values) > 0:
|
||||
self._entry.insert(0, self._values[0])
|
||||
else:
|
||||
self._entry.insert(0, "CTkComboBox")
|
||||
|
||||
self._draw() # initial draw
|
||||
|
||||
# event bindings
|
||||
self._canvas.tag_bind("right_parts", "<Enter>", self._on_enter)
|
||||
self._canvas.tag_bind("dropdown_arrow", "<Enter>", self._on_enter)
|
||||
self._canvas.tag_bind("right_parts", "<Leave>", self._on_leave)
|
||||
self._canvas.tag_bind("dropdown_arrow", "<Leave>", self._on_leave)
|
||||
self._canvas.tag_bind("right_parts", "<Button-1>", self._clicked)
|
||||
self._canvas.tag_bind("dropdown_arrow", "<Button-1>", self._clicked)
|
||||
|
||||
if self._variable is not None:
|
||||
self._entry.configure(textvariable=self._variable)
|
||||
|
||||
def _create_grid(self):
|
||||
self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew")
|
||||
|
||||
left_section_width = self._current_width - self._current_height
|
||||
self._entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew",
|
||||
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
|
||||
max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))),
|
||||
pady=self._apply_widget_scaling(self._border_width))
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
# change entry font size and grid padding
|
||||
self._entry.configure(font=self._apply_font_scaling(self._font))
|
||||
self._create_grid()
|
||||
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
def _set_dimensions(self, width: int = None, height: int = 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 _update_font(self):
|
||||
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
|
||||
self._entry.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=1, columnspan=1, 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):
|
||||
left_section_width = self._current_width - self._current_height
|
||||
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(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),
|
||||
self._apply_widget_scaling(left_section_width))
|
||||
|
||||
requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)),
|
||||
self._apply_widget_scaling(self._current_height / 2),
|
||||
self._apply_widget_scaling(self._current_height / 3))
|
||||
|
||||
if no_color_updates is False or requires_recoloring or requires_recoloring_2:
|
||||
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
self._canvas.itemconfig("inner_parts_left",
|
||||
outline=self._apply_appearance_mode(self._fg_color),
|
||||
fill=self._apply_appearance_mode(self._fg_color))
|
||||
self._canvas.itemconfig("border_parts_left",
|
||||
outline=self._apply_appearance_mode(self._border_color),
|
||||
fill=self._apply_appearance_mode(self._border_color))
|
||||
self._canvas.itemconfig("inner_parts_right",
|
||||
outline=self._apply_appearance_mode(self._border_color),
|
||||
fill=self._apply_appearance_mode(self._border_color))
|
||||
self._canvas.itemconfig("border_parts_right",
|
||||
outline=self._apply_appearance_mode(self._border_color),
|
||||
fill=self._apply_appearance_mode(self._border_color))
|
||||
|
||||
self._entry.configure(bg=self._apply_appearance_mode(self._fg_color),
|
||||
fg=self._apply_appearance_mode(self._text_color),
|
||||
disabledbackground=self._apply_appearance_mode(self._fg_color),
|
||||
disabledforeground=self._apply_appearance_mode(self._text_color_disabled),
|
||||
highlightcolor=self._apply_appearance_mode(self._fg_color),
|
||||
insertbackground=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
if self._state == tkinter.DISABLED:
|
||||
self._canvas.itemconfig("dropdown_arrow",
|
||||
fill=self._apply_appearance_mode(self._text_color_disabled))
|
||||
else:
|
||||
self._canvas.itemconfig("dropdown_arrow",
|
||||
fill=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
def _open_dropdown_menu(self):
|
||||
self._dropdown_menu.open(self.winfo_rootx(),
|
||||
self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "corner_radius" in kwargs:
|
||||
self._corner_radius = kwargs.pop("corner_radius")
|
||||
require_redraw = True
|
||||
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
self._create_grid()
|
||||
require_redraw = True
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_color" in kwargs:
|
||||
self._border_color = kwargs.pop("border_color")
|
||||
require_redraw = True
|
||||
|
||||
if "button_color" in kwargs:
|
||||
self._button_color = kwargs.pop("button_color")
|
||||
require_redraw = True
|
||||
|
||||
if "button_hover_color" in kwargs:
|
||||
self._button_hover_color = kwargs.pop("button_hover_color")
|
||||
require_redraw = True
|
||||
|
||||
if "dropdown_fg_color" in kwargs:
|
||||
self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_fg_color"))
|
||||
|
||||
if "dropdown_hover_color" in kwargs:
|
||||
self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
|
||||
|
||||
if "dropdown_text_color" in kwargs:
|
||||
self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
|
||||
|
||||
if "text_color" in kwargs:
|
||||
self._text_color = kwargs.pop("text_color")
|
||||
require_redraw = True
|
||||
|
||||
if "text_color_disabled" in kwargs:
|
||||
self._text_color_disabled = kwargs.pop("text_color_disabled")
|
||||
require_redraw = True
|
||||
|
||||
if "font" in kwargs:
|
||||
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 "dropdown_font" in kwargs:
|
||||
self._dropdown_menu.configure(font=kwargs.pop("dropdown_font"))
|
||||
|
||||
if "values" in kwargs:
|
||||
self._values = kwargs.pop("values")
|
||||
self._dropdown_menu.configure(values=self._values)
|
||||
|
||||
if "state" in kwargs:
|
||||
self._state = kwargs.pop("state")
|
||||
self._entry.configure(state=self._state)
|
||||
require_redraw = True
|
||||
|
||||
if "hover" in kwargs:
|
||||
self._hover = kwargs.pop("hover")
|
||||
|
||||
if "variable" in kwargs:
|
||||
self._variable = kwargs.pop("variable")
|
||||
self._entry.configure(textvariable=self._variable)
|
||||
|
||||
if "command" in kwargs:
|
||||
self._command = kwargs.pop("command")
|
||||
|
||||
if "justify" in kwargs:
|
||||
self._entry.configure(justify=kwargs.pop("justify"))
|
||||
|
||||
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 == "button_color":
|
||||
return self._button_color
|
||||
elif attribute_name == "button_hover_color":
|
||||
return self._button_hover_color
|
||||
elif attribute_name == "dropdown_fg_color":
|
||||
return self._dropdown_menu.cget("fg_color")
|
||||
elif attribute_name == "dropdown_hover_color":
|
||||
return self._dropdown_menu.cget("hover_color")
|
||||
elif attribute_name == "dropdown_text_color":
|
||||
return self._dropdown_menu.cget("text_color")
|
||||
elif attribute_name == "text_color":
|
||||
return self._text_color
|
||||
elif attribute_name == "text_color_disabled":
|
||||
return self._text_color_disabled
|
||||
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "dropdown_font":
|
||||
return self._dropdown_menu.cget("font")
|
||||
elif attribute_name == "values":
|
||||
return self._values
|
||||
elif attribute_name == "state":
|
||||
return self._state
|
||||
elif attribute_name == "hover":
|
||||
return self._hover
|
||||
elif attribute_name == "variable":
|
||||
return self._variable
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
elif attribute_name == "justify":
|
||||
return self._entry.cget("justify")
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def _on_enter(self, event=0):
|
||||
if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0:
|
||||
if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled:
|
||||
self._canvas.configure(cursor="pointinghand")
|
||||
elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled:
|
||||
self._canvas.configure(cursor="hand2")
|
||||
|
||||
# set color of inner button parts to hover color
|
||||
self._canvas.itemconfig("inner_parts_right",
|
||||
outline=self._apply_appearance_mode(self._button_hover_color),
|
||||
fill=self._apply_appearance_mode(self._button_hover_color))
|
||||
self._canvas.itemconfig("border_parts_right",
|
||||
outline=self._apply_appearance_mode(self._button_hover_color),
|
||||
fill=self._apply_appearance_mode(self._button_hover_color))
|
||||
|
||||
def _on_leave(self, event=0):
|
||||
if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled:
|
||||
self._canvas.configure(cursor="arrow")
|
||||
elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled:
|
||||
self._canvas.configure(cursor="arrow")
|
||||
|
||||
# set color of inner button parts
|
||||
self._canvas.itemconfig("inner_parts_right",
|
||||
outline=self._apply_appearance_mode(self._button_color),
|
||||
fill=self._apply_appearance_mode(self._button_color))
|
||||
self._canvas.itemconfig("border_parts_right",
|
||||
outline=self._apply_appearance_mode(self._button_color),
|
||||
fill=self._apply_appearance_mode(self._button_color))
|
||||
|
||||
def _dropdown_callback(self, value: str):
|
||||
if self._state == "readonly":
|
||||
self._entry.configure(state="normal")
|
||||
self._entry.delete(0, tkinter.END)
|
||||
self._entry.insert(0, value)
|
||||
self._entry.configure(state="readonly")
|
||||
else:
|
||||
self._entry.delete(0, tkinter.END)
|
||||
self._entry.insert(0, value)
|
||||
|
||||
if self._command is not None:
|
||||
self._command(value)
|
||||
|
||||
def set(self, value: str):
|
||||
if self._state == "readonly":
|
||||
self._entry.configure(state="normal")
|
||||
self._entry.delete(0, tkinter.END)
|
||||
self._entry.insert(0, value)
|
||||
self._entry.configure(state="readonly")
|
||||
else:
|
||||
self._entry.delete(0, tkinter.END)
|
||||
self._entry.insert(0, value)
|
||||
|
||||
def get(self) -> str:
|
||||
return self._entry.get()
|
||||
|
||||
def _clicked(self, event=0):
|
||||
if self._state is not tkinter.DISABLED and len(self._values) > 0:
|
||||
self._open_dropdown_menu()
|
||||
|
||||
def bind(self, sequence=None, command=None, add=None):
|
||||
""" called on the tkinter.Entry """
|
||||
return self._entry.bind(sequence, command, add)
|
||||
|
||||
def unbind(self, sequence, funcid=None):
|
||||
""" called on the tkinter.Entry """
|
||||
return self._entry.unbind(sequence, funcid)
|
||||
|
||||
def focus(self):
|
||||
return self._entry.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._entry.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._entry.focus_force()
|
368
customtkinter/windows/widgets/ctk_entry.py
Normal file
368
customtkinter/windows/widgets/ctk_entry.py
Normal file
@ -0,0 +1,368 @@
|
||||
import tkinter
|
||||
from typing import Union, Tuple
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
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
|
||||
|
||||
from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
|
||||
|
||||
|
||||
class CTkEntry(CTkBaseClass):
|
||||
"""
|
||||
Entry with rounded corners, border, textvariable support, focus and placeholder.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
_minimum_x_padding = 6 # minimum padding between tkinter entry and frame border
|
||||
|
||||
# attributes that are passed to and managed by the tkinter entry only:
|
||||
_valid_tk_entry_attributes = {"exportselection", "insertborderwidth", "insertofftime",
|
||||
"insertontime", "insertwidth", "justify", "selectborderwidth",
|
||||
"show", "takefocus", "validate", "validatecommand", "xscrollcommand"}
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 140,
|
||||
height: int = 28,
|
||||
corner_radius: int = "default_theme",
|
||||
border_width: int = "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, Tuple[str, str]] = "default_theme",
|
||||
placeholder_text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
textvariable: tkinter.Variable = None,
|
||||
placeholder_text: str = None,
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
state: str = tkinter.NORMAL,
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height)
|
||||
|
||||
# configure grid system (1x1)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# color
|
||||
self._fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color
|
||||
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
|
||||
self._placeholder_text_color = ThemeManager.theme["color"]["entry_placeholder_text"] if placeholder_text_color == "default_theme" else placeholder_text_color
|
||||
self._border_color = ThemeManager.theme["color"]["entry_border"] if border_color == "default_theme" else border_color
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
self._border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width
|
||||
|
||||
# text and state
|
||||
self._is_focused: bool = True
|
||||
self._placeholder_text = placeholder_text
|
||||
self._placeholder_text_active = False
|
||||
self._pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back
|
||||
self._textvariable = textvariable
|
||||
self._state = state
|
||||
self._textvariable_callback_name: str = ""
|
||||
|
||||
# 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)
|
||||
|
||||
if not (self._textvariable is None or self._textvariable == ""):
|
||||
self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback)
|
||||
|
||||
self._canvas = CTkCanvas(master=self,
|
||||
highlightthickness=0,
|
||||
width=self._apply_widget_scaling(self._current_width),
|
||||
height=self._apply_widget_scaling(self._current_height))
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._entry = tkinter.Entry(master=self,
|
||||
bd=0,
|
||||
width=1,
|
||||
highlightthickness=0,
|
||||
font=self._apply_font_scaling(self._font),
|
||||
state=self._state,
|
||||
textvariable=self._textvariable,
|
||||
**pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes))
|
||||
|
||||
self._create_grid()
|
||||
|
||||
check_kwargs_empty(kwargs, raise_error=True)
|
||||
|
||||
self._entry.bind('<FocusOut>', self._entry_focus_out)
|
||||
self._entry.bind('<FocusIn>', self._entry_focus_in)
|
||||
|
||||
self._activate_placeholder()
|
||||
self._draw()
|
||||
|
||||
def _create_grid(self):
|
||||
self._canvas.grid(column=0, row=0, sticky="nswe")
|
||||
|
||||
if self._corner_radius >= self._minimum_x_padding:
|
||||
self._entry.grid(column=0, row=0, sticky="nswe",
|
||||
padx=min(self._apply_widget_scaling(self._corner_radius), round(self._apply_widget_scaling(self._current_height/2))),
|
||||
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1)))
|
||||
else:
|
||||
self._entry.grid(column=0, row=0, sticky="nswe",
|
||||
padx=self._apply_widget_scaling(self._minimum_x_padding),
|
||||
pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1)))
|
||||
|
||||
def _textvariable_callback(self, var_name, index, mode):
|
||||
if self._textvariable.get() == "":
|
||||
self._activate_placeholder()
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self._entry.configure(font=self._apply_font_scaling(self._font))
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height))
|
||||
self._create_grid()
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
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 _update_font(self):
|
||||
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """
|
||||
self._entry.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(column=0, row=0, sticky="nswe")
|
||||
|
||||
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):
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
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 requires_recoloring or no_color_updates is False:
|
||||
if self._apply_appearance_mode(self._fg_color) is not None:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
self._entry.configure(bg=self._apply_appearance_mode(self._fg_color),
|
||||
fg=self._apply_appearance_mode(self._text_color),
|
||||
disabledbackground=self._apply_appearance_mode(self._fg_color),
|
||||
disabledforeground=self._apply_appearance_mode(self._text_color),
|
||||
highlightcolor=self._apply_appearance_mode(self._fg_color),
|
||||
insertbackground=self._apply_appearance_mode(self._text_color))
|
||||
else:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._bg_color),
|
||||
outline=self._apply_appearance_mode(self._bg_color))
|
||||
self._entry.configure(bg=self._apply_appearance_mode(self._bg_color),
|
||||
fg=self._apply_appearance_mode(self._text_color),
|
||||
disabledbackground=self._apply_appearance_mode(self._bg_color),
|
||||
disabledforeground=self._apply_appearance_mode(self._text_color),
|
||||
highlightcolor=self._apply_appearance_mode(self._bg_color),
|
||||
insertbackground=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._border_color),
|
||||
outline=self._apply_appearance_mode(self._border_color))
|
||||
|
||||
if self._placeholder_text_active:
|
||||
self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "state" in kwargs:
|
||||
self._state = kwargs.pop("state")
|
||||
self._entry.configure(state=self._state)
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "text_color" in kwargs:
|
||||
self._text_color = kwargs.pop("text_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_color" in kwargs:
|
||||
self._border_color = kwargs.pop("border_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
self._create_grid()
|
||||
require_redraw = True
|
||||
|
||||
if "corner_radius" in kwargs:
|
||||
self._corner_radius = kwargs.pop("corner_radius")
|
||||
self._create_grid()
|
||||
require_redraw = True
|
||||
|
||||
if "placeholder_text" in kwargs:
|
||||
self._placeholder_text = kwargs.pop("placeholder_text")
|
||||
if self._placeholder_text_active:
|
||||
self._entry.delete(0, tkinter.END)
|
||||
self._entry.insert(0, self._placeholder_text)
|
||||
else:
|
||||
self._activate_placeholder()
|
||||
|
||||
if "placeholder_text_color" in kwargs:
|
||||
self._placeholder_text_color = kwargs.pop("placeholder_text_color")
|
||||
require_redraw = True
|
||||
|
||||
if "textvariable" in kwargs:
|
||||
self._textvariable = kwargs.pop("textvariable")
|
||||
self._entry.configure(textvariable=self._textvariable)
|
||||
|
||||
if "font" in kwargs:
|
||||
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 "show" in kwargs:
|
||||
if self._placeholder_text_active:
|
||||
self._pre_placeholder_arguments["show"] = kwargs.pop("show") # remember show argument for when placeholder gets deactivated
|
||||
else:
|
||||
self._entry.configure(show=kwargs.pop("show"))
|
||||
|
||||
self._entry.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes)) # configure Tkinter.Entry
|
||||
super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass
|
||||
|
||||
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 == "placeholder_text_color":
|
||||
return self._placeholder_text_color
|
||||
|
||||
elif attribute_name == "textvariable":
|
||||
return self._textvariable
|
||||
elif attribute_name == "placeholder_text":
|
||||
return self._placeholder_text
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "state":
|
||||
return self._state
|
||||
|
||||
elif attribute_name in self._valid_tk_entry_attributes:
|
||||
return self._entry.cget(attribute_name) # cget of tkinter.Entry
|
||||
else:
|
||||
return super().cget(attribute_name) # cget of CTkBaseClass
|
||||
|
||||
def bind(self, sequence=None, command=None, add=None):
|
||||
""" called on the tkinter.Entry """
|
||||
return self._entry.bind(sequence, command, add)
|
||||
|
||||
def unbind(self, sequence, funcid=None):
|
||||
""" called on the tkinter.Entry """
|
||||
return self._entry.unbind(sequence, funcid)
|
||||
|
||||
def _activate_placeholder(self):
|
||||
if self._entry.get() == "" and self._placeholder_text is not None and (self._textvariable is None or self._textvariable == ""):
|
||||
self._placeholder_text_active = True
|
||||
|
||||
self._pre_placeholder_arguments = {"show": self._entry.cget("show")}
|
||||
self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color), show="")
|
||||
self._entry.delete(0, tkinter.END)
|
||||
self._entry.insert(0, self._placeholder_text)
|
||||
|
||||
def _deactivate_placeholder(self):
|
||||
if self._placeholder_text_active:
|
||||
self._placeholder_text_active = False
|
||||
|
||||
self._entry.config(fg=self._apply_appearance_mode(self._text_color))
|
||||
self._entry.delete(0, tkinter.END)
|
||||
for argument, value in self._pre_placeholder_arguments.items():
|
||||
self._entry[argument] = value
|
||||
|
||||
def _entry_focus_out(self, event=None):
|
||||
self._activate_placeholder()
|
||||
self._is_focused = False
|
||||
|
||||
def _entry_focus_in(self, event=None):
|
||||
self._deactivate_placeholder()
|
||||
self._is_focused = True
|
||||
|
||||
def delete(self, first_index, last_index=None):
|
||||
self._entry.delete(first_index, last_index)
|
||||
|
||||
if not self._is_focused and self._entry.get() == "":
|
||||
self._activate_placeholder()
|
||||
|
||||
def insert(self, index, string):
|
||||
self._deactivate_placeholder()
|
||||
|
||||
return self._entry.insert(index, string)
|
||||
|
||||
def get(self):
|
||||
if self._placeholder_text_active:
|
||||
return ""
|
||||
else:
|
||||
return self._entry.get()
|
||||
|
||||
def focus(self):
|
||||
return self._entry.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._entry.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._entry.focus_force()
|
||||
|
||||
def index(self, index):
|
||||
return self._entry.index(index)
|
||||
|
||||
def icursor(self, index):
|
||||
return self._entry.icursor(index)
|
||||
|
||||
def select_adjust(self, index):
|
||||
return self._entry.select_adjust(index)
|
||||
|
||||
def select_from(self, index):
|
||||
return self._entry.icursor(index)
|
||||
|
||||
def select_clear(self):
|
||||
return self._entry.select_clear()
|
||||
|
||||
def select_present(self):
|
||||
return self._entry.select_present()
|
||||
|
||||
def select_range(self, start_index, end_index):
|
||||
return self._entry.select_range(start_index, end_index)
|
||||
|
||||
def select_to(self, index):
|
||||
return self._entry.select_to(index)
|
||||
|
||||
def xview(self, index):
|
||||
return self._entry.xview(index)
|
||||
|
||||
def xview_moveto(self, f):
|
||||
return self._entry.xview_moveto(f)
|
||||
|
||||
def xview_scroll(self, number, what):
|
||||
return self._entry.xview_scroll(number, what)
|
188
customtkinter/windows/widgets/ctk_frame.py
Normal file
188
customtkinter/windows/widgets/ctk_frame.py
Normal file
@ -0,0 +1,188 @@
|
||||
from typing import Union, Tuple, List
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
from .theme.theme_manager import ThemeManager
|
||||
from .core_rendering.draw_engine import DrawEngine
|
||||
from .core_widget_classes.widget_base_class import CTkBaseClass
|
||||
|
||||
|
||||
class CTkFrame(CTkBaseClass):
|
||||
"""
|
||||
Frame with rounded corners and border.
|
||||
Default foreground colors are set according to theme.
|
||||
To make the frame transparent set fg_color=None.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
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",
|
||||
background_corner_colors: Tuple[Union[str, Tuple[str, str]]] = None,
|
||||
|
||||
overwrite_preferred_drawing_method: str = None,
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# color
|
||||
self._border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
|
||||
|
||||
# determine fg_color of frame
|
||||
if fg_color == "default_theme":
|
||||
if isinstance(self.master, CTkFrame):
|
||||
if self.master._fg_color == ThemeManager.theme["color"]["frame_low"]:
|
||||
self._fg_color = ThemeManager.theme["color"]["frame_high"]
|
||||
else:
|
||||
self._fg_color = ThemeManager.theme["color"]["frame_low"]
|
||||
else:
|
||||
self._fg_color = ThemeManager.theme["color"]["frame_low"]
|
||||
else:
|
||||
self._fg_color = fg_color
|
||||
|
||||
self._background_corner_colors = background_corner_colors # rendering options for DrawEngine
|
||||
|
||||
# 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
|
||||
|
||||
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.place(x=0, y=0, relwidth=1, relheight=1)
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method
|
||||
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
def winfo_children(self) -> List[any]:
|
||||
"""
|
||||
winfo_children of CTkFrame without self.canvas widget,
|
||||
because it's not a child but part of the CTkFrame itself
|
||||
"""
|
||||
|
||||
child_widgets = super().winfo_children()
|
||||
try:
|
||||
child_widgets.remove(self._canvas)
|
||||
return child_widgets
|
||||
except ValueError:
|
||||
return child_widgets
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
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):
|
||||
if not self._canvas.winfo_exists():
|
||||
return
|
||||
|
||||
if self._background_corner_colors is not None:
|
||||
self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width),
|
||||
self._apply_widget_scaling(self._current_height))
|
||||
self._canvas.itemconfig("background_corner_top_left", fill=self._apply_appearance_mode(self._background_corner_colors[0]))
|
||||
self._canvas.itemconfig("background_corner_top_right", fill=self._apply_appearance_mode(self._background_corner_colors[1]))
|
||||
self._canvas.itemconfig("background_corner_bottom_right", fill=self._apply_appearance_mode(self._background_corner_colors[2]))
|
||||
self._canvas.itemconfig("background_corner_bottom_left", fill=self._apply_appearance_mode(self._background_corner_colors[3]))
|
||||
else:
|
||||
self._canvas.delete("background_parts")
|
||||
|
||||
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),
|
||||
overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method)
|
||||
|
||||
if no_color_updates is False or requires_recoloring:
|
||||
if self._fg_color is None:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._bg_color),
|
||||
outline=self._apply_appearance_mode(self._bg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._border_color),
|
||||
outline=self._apply_appearance_mode(self._border_color))
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
# self._canvas.tag_lower("inner_parts") # maybe unnecessary, I don't know ???
|
||||
# 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):
|
||||
child.configure(bg_color=self._fg_color)
|
||||
|
||||
if "bg_color" in kwargs:
|
||||
# pass bg_color change to children if fg_color is None
|
||||
if self._fg_color is None:
|
||||
for child in self.winfo_children():
|
||||
if isinstance(child, CTkBaseClass):
|
||||
child.configure(bg_color=self._fg_color)
|
||||
|
||||
if "border_color" in kwargs:
|
||||
self._border_color = kwargs.pop("border_color")
|
||||
require_redraw = True
|
||||
|
||||
if "background_corner_colors" in kwargs:
|
||||
self._background_corner_colors = kwargs.pop("background_corner_colors")
|
||||
require_redraw = True
|
||||
|
||||
if "corner_radius" in kwargs:
|
||||
self._corner_radius = kwargs.pop("corner_radius")
|
||||
require_redraw = True
|
||||
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
require_redraw = True
|
||||
|
||||
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 == "background_corner_colors":
|
||||
return self._background_corner_colors
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def bind(self, sequence=None, command=None, add=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.bind(sequence, command, add)
|
||||
|
||||
def unbind(self, sequence, funcid=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.unbind(sequence, funcid)
|
221
customtkinter/windows/widgets/ctk_label.py
Normal file
221
customtkinter/windows/widgets/ctk_label.py
Normal file
@ -0,0 +1,221 @@
|
||||
import tkinter
|
||||
from typing import Union, Tuple, Callable
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
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
|
||||
|
||||
from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
|
||||
|
||||
|
||||
class CTkLabel(CTkBaseClass):
|
||||
"""
|
||||
Label with rounded corners. Default is fg_color=None (transparent fg_color).
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
# attributes that are passed to and managed by the tkinter entry only:
|
||||
_valid_tk_label_attributes = {"compound", "cursor", "image", "justify", "padx", "pady",
|
||||
"textvariable", "state", "takefocus", "underline", "wraplength"}
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 140,
|
||||
height: int = 28,
|
||||
corner_radius: Union[int, str] = "default_theme",
|
||||
|
||||
bg_color: Union[str, Tuple[str, str], None] = None,
|
||||
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
text: str = "CTkLabel",
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
anchor: str = "center", # label anchor: center, n, e, s, w
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height)
|
||||
|
||||
# color
|
||||
self._fg_color = ThemeManager.theme["color"]["label"] if fg_color == "default_theme" else fg_color
|
||||
self._text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["label_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
|
||||
# text
|
||||
self._anchor = anchor
|
||||
self._text = text
|
||||
|
||||
# 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)
|
||||
|
||||
# configure grid system (1x1)
|
||||
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._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._canvas.grid(row=0, column=0, sticky="nswe")
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._text_label = tkinter.Label(master=self,
|
||||
highlightthickness=0,
|
||||
padx=0,
|
||||
pady=0,
|
||||
borderwidth=1,
|
||||
anchor=self._anchor,
|
||||
text=self._text,
|
||||
font=self._apply_font_scaling(self._font))
|
||||
self._text_label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes))
|
||||
|
||||
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
|
||||
self._text_label.grid(row=0, column=0, sticky=text_label_grid_sticky,
|
||||
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height/2))))
|
||||
|
||||
check_kwargs_empty(kwargs, raise_error=True)
|
||||
|
||||
self._draw()
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height))
|
||||
self._text_label.configure(font=self._apply_font_scaling(self._font))
|
||||
|
||||
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
|
||||
self._text_label.grid(row=0, column=0, sticky=text_label_grid_sticky,
|
||||
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height/2))))
|
||||
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
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 _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="nswe")
|
||||
|
||||
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):
|
||||
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),
|
||||
0)
|
||||
|
||||
if no_color_updates is False or requires_recoloring:
|
||||
if self._apply_appearance_mode(self._fg_color) is not None:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color),
|
||||
bg=self._apply_appearance_mode(self._fg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._bg_color),
|
||||
outline=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color),
|
||||
bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "anchor" in kwargs:
|
||||
self._anchor = kwargs.pop("anchor")
|
||||
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
|
||||
self._text_label.grid(row=0, column=0, sticky=text_label_grid_sticky,
|
||||
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height/2))))
|
||||
|
||||
if "text" in kwargs:
|
||||
self._text = kwargs.pop("text")
|
||||
self._text_label.configure(text=self._text)
|
||||
|
||||
if "font" in kwargs:
|
||||
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 "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_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")
|
||||
text_label_grid_sticky = self._anchor if self._anchor != "center" else ""
|
||||
self._text_label.grid(row=0, column=0, sticky=text_label_grid_sticky,
|
||||
padx=self._apply_widget_scaling(min(self._corner_radius, round(self._current_height/2))))
|
||||
require_redraw = True
|
||||
|
||||
self._text_label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes)) # configure tkinter.Label
|
||||
super().configure(require_redraw=require_redraw, **kwargs) # configure CTkBaseClass
|
||||
|
||||
def cget(self, attribute_name: str) -> any:
|
||||
if attribute_name == "corner_radius":
|
||||
return self._corner_radius
|
||||
|
||||
elif attribute_name == "fg_color":
|
||||
return self._fg_color
|
||||
elif attribute_name == "text_color":
|
||||
return self._text_color
|
||||
|
||||
elif attribute_name == "text":
|
||||
return self._text
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "anchor":
|
||||
return self._anchor
|
||||
|
||||
elif attribute_name in self._valid_tk_label_attributes:
|
||||
return self._text_label.cget(attribute_name) # cget of tkinter.Label
|
||||
else:
|
||||
return super().cget(attribute_name) # cget of CTkBaseClass
|
||||
|
||||
def bind(self, sequence: str = None, command: Callable = None, add: str = None) -> str:
|
||||
""" called on the tkinter.Label and tkinter.Canvas """
|
||||
canvas_bind_return = self._canvas.bind(sequence, command, add)
|
||||
label_bind_return = self._text_label.bind(sequence, command, add)
|
||||
return canvas_bind_return + " + " + label_bind_return
|
||||
|
||||
def unbind(self, sequence: str, funcid: str = None):
|
||||
""" called on the tkinter.Label and tkinter.Canvas """
|
||||
canvas_bind_return, label_bind_return = funcid.split(" + ")
|
||||
self._canvas.unbind(sequence, canvas_bind_return)
|
||||
self._text_label.unbind(sequence, label_bind_return)
|
||||
|
||||
def focus(self):
|
||||
return self._text_label.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._text_label.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._text_label.focus_force()
|
414
customtkinter/windows/widgets/ctk_optionmenu.py
Normal file
414
customtkinter/windows/widgets/ctk_optionmenu.py
Normal file
@ -0,0 +1,414 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Union, Tuple, Callable
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
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
|
||||
from .font.ctk_font import CTkFont
|
||||
|
||||
|
||||
class CTkOptionMenu(CTkBaseClass):
|
||||
"""
|
||||
Optionmenu with rounded corners, dropdown menu, variable support, command.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 140,
|
||||
height: int = 28,
|
||||
corner_radius: Union[int, str] = "default_theme",
|
||||
|
||||
bg_color: Union[str, Tuple[str, str], None] = None,
|
||||
fg_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
button_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
button_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
|
||||
dropdown_fg_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
dropdown_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
dropdown_text_color: Union[str, Tuple[str, str]] = "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,
|
||||
hover: bool = True,
|
||||
command: Callable[[str], None] = None,
|
||||
dynamic_resizing: bool = True,
|
||||
anchor: str = "w",
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# color variables
|
||||
self._fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
|
||||
self._button_color = ThemeManager.theme["color"]["optionmenu_button"] if button_color == "default_theme" else button_color
|
||||
self._button_hover_color = ThemeManager.theme["color"]["optionmenu_button_hover"] if button_hover_color == "default_theme" else button_hover_color
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
self._variable = variable
|
||||
self._variable_callback_blocked: bool = False
|
||||
self._variable_callback_name: Union[str, None] = None
|
||||
self._state = state
|
||||
self._hover = hover
|
||||
self._dynamic_resizing = dynamic_resizing
|
||||
|
||||
if values is None:
|
||||
self._values = ["CTkOptionMenu"]
|
||||
else:
|
||||
self._values = values
|
||||
|
||||
if len(self._values) > 0:
|
||||
self._current_value = self._values[0]
|
||||
else:
|
||||
self._current_value = "CTkOptionMenu"
|
||||
|
||||
self._dropdown_menu = DropdownMenu(master=self,
|
||||
values=self._values,
|
||||
command=self._dropdown_callback,
|
||||
fg_color=dropdown_fg_color,
|
||||
hover_color=dropdown_hover_color,
|
||||
text_color=dropdown_text_color,
|
||||
font=dropdown_font)
|
||||
|
||||
# configure grid system (1x1)
|
||||
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._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._text_label = tkinter.Label(master=self,
|
||||
font=self._apply_font_scaling(self._font),
|
||||
anchor=anchor,
|
||||
padx=0,
|
||||
pady=0,
|
||||
borderwidth=1,
|
||||
text=self._current_value)
|
||||
self._create_grid()
|
||||
|
||||
if not self._dynamic_resizing:
|
||||
self.grid_propagate(0)
|
||||
|
||||
if self._cursor_manipulation_enabled:
|
||||
if sys.platform == "darwin":
|
||||
self.configure(cursor="pointinghand")
|
||||
elif sys.platform.startswith("win"):
|
||||
self.configure(cursor="hand2")
|
||||
|
||||
# event bindings
|
||||
self._canvas.bind("<Enter>", self._on_enter)
|
||||
self._canvas.bind("<Leave>", self._on_leave)
|
||||
self._canvas.bind("<Button-1>", self._clicked)
|
||||
self._canvas.bind("<Button-1>", self._clicked)
|
||||
|
||||
self._text_label.bind("<Enter>", self._on_enter)
|
||||
self._text_label.bind("<Leave>", self._on_leave)
|
||||
self._text_label.bind("<Button-1>", self._clicked)
|
||||
self._text_label.bind("<Button-1>", self._clicked)
|
||||
|
||||
self._draw() # initial draw
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self._current_value = self._variable.get()
|
||||
self._text_label.configure(text=self._current_value)
|
||||
|
||||
def _create_grid(self):
|
||||
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",
|
||||
padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)),
|
||||
max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))))
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
# change label font size and grid padding
|
||||
self._text_label.configure(font=self._apply_font_scaling(self._font))
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._create_grid()
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
def _set_dimensions(self, width: int = None, height: int = 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 _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):
|
||||
left_section_width = self._current_width - self._current_height
|
||||
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border_vertical_split(self._apply_widget_scaling(self._current_width),
|
||||
self._apply_widget_scaling(self._current_height),
|
||||
self._apply_widget_scaling(self._corner_radius),
|
||||
0,
|
||||
self._apply_widget_scaling(left_section_width))
|
||||
|
||||
requires_recoloring_2 = self._draw_engine.draw_dropdown_arrow(self._apply_widget_scaling(self._current_width - (self._current_height / 2)),
|
||||
self._apply_widget_scaling(self._current_height / 2),
|
||||
self._apply_widget_scaling(self._current_height / 3))
|
||||
|
||||
if no_color_updates is False or requires_recoloring or requires_recoloring_2:
|
||||
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
self._canvas.itemconfig("inner_parts_left",
|
||||
outline=self._apply_appearance_mode(self._fg_color),
|
||||
fill=self._apply_appearance_mode(self._fg_color))
|
||||
self._canvas.itemconfig("inner_parts_right",
|
||||
outline=self._apply_appearance_mode(self._button_color),
|
||||
fill=self._apply_appearance_mode(self._button_color))
|
||||
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
if self._state == tkinter.DISABLED:
|
||||
self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
|
||||
self._canvas.itemconfig("dropdown_arrow",
|
||||
fill=self._apply_appearance_mode(self._text_color_disabled))
|
||||
else:
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
|
||||
self._canvas.itemconfig("dropdown_arrow",
|
||||
fill=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
self._text_label.configure(bg=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
self._canvas.update_idletasks()
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "corner_radius" in kwargs:
|
||||
self._corner_radius = kwargs.pop("corner_radius")
|
||||
self._create_grid()
|
||||
require_redraw = True
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "button_color" in kwargs:
|
||||
self._button_color = kwargs.pop("button_color")
|
||||
require_redraw = True
|
||||
|
||||
if "button_hover_color" in kwargs:
|
||||
self._button_hover_color = kwargs.pop("button_hover_color")
|
||||
require_redraw = True
|
||||
|
||||
if "text_color" in kwargs:
|
||||
self._text_color = kwargs.pop("text_color")
|
||||
require_redraw = True
|
||||
|
||||
if "dropdown_color" in kwargs:
|
||||
self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color"))
|
||||
|
||||
if "dropdown_hover_color" in kwargs:
|
||||
self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color"))
|
||||
|
||||
if "dropdown_text_color" in kwargs:
|
||||
self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color"))
|
||||
|
||||
if "font" in kwargs:
|
||||
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")
|
||||
|
||||
if "variable" in kwargs:
|
||||
if self._variable is not None: # remove old callback
|
||||
self._variable.trace_remove("write", self._variable_callback_name)
|
||||
|
||||
self._variable = kwargs.pop("variable")
|
||||
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self._current_value = self._variable.get()
|
||||
self._text_label.configure(text=self._current_value)
|
||||
else:
|
||||
self._variable = None
|
||||
|
||||
if "values" in kwargs:
|
||||
self._values = kwargs.pop("values")
|
||||
self._dropdown_menu.configure(values=self._values)
|
||||
|
||||
if "dropdown_font" in kwargs:
|
||||
self._dropdown_menu.configure(font=kwargs.pop("dropdown_font"))
|
||||
|
||||
if "hover" in kwargs:
|
||||
self._hover = kwargs.pop("hover")
|
||||
|
||||
if "state" in kwargs:
|
||||
self._state = kwargs.pop("state")
|
||||
require_redraw = True
|
||||
|
||||
if "dynamic_resizing" in kwargs:
|
||||
self._dynamic_resizing = kwargs.pop("dynamic_resizing")
|
||||
if not self._dynamic_resizing:
|
||||
self.grid_propagate(0)
|
||||
else:
|
||||
self.grid_propagate(1)
|
||||
|
||||
if "anchor" in kwargs:
|
||||
self._text_label.configure(anchor=kwargs.pop("anchor"))
|
||||
|
||||
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 == "fg_color":
|
||||
return self._fg_color
|
||||
elif attribute_name == "button_color":
|
||||
return self._button_color
|
||||
elif attribute_name == "button_hover_color":
|
||||
return self._button_hover_color
|
||||
elif attribute_name == "text_color":
|
||||
return self._text_color
|
||||
elif attribute_name == "text_color_disabled":
|
||||
return self._text_color_disabled
|
||||
elif attribute_name == "dropdown_fg_color":
|
||||
return self._dropdown_menu.cget("fg_color")
|
||||
elif attribute_name == "dropdown_hover_color":
|
||||
return self._dropdown_menu.cget("hover_color")
|
||||
elif attribute_name == "dropdown_text_color":
|
||||
return self._dropdown_menu.cget("text_color")
|
||||
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "dropdown_font":
|
||||
return self._dropdown_menu.cget("font")
|
||||
elif attribute_name == "values":
|
||||
return self._values
|
||||
elif attribute_name == "variable":
|
||||
return self._variable
|
||||
elif attribute_name == "state":
|
||||
return self._state
|
||||
elif attribute_name == "hover":
|
||||
return self._hover
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
elif attribute_name == "dynamic_resizing":
|
||||
return self._dynamic_resizing
|
||||
elif attribute_name == "anchor":
|
||||
return self._text_label.cget("anchor")
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def _open_dropdown_menu(self):
|
||||
self._dropdown_menu.open(self.winfo_rootx(),
|
||||
self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0))
|
||||
|
||||
def _on_enter(self, event=0):
|
||||
if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0:
|
||||
# set color of inner button parts to hover color
|
||||
self._canvas.itemconfig("inner_parts_right",
|
||||
outline=self._apply_appearance_mode(self._button_hover_color),
|
||||
fill=self._apply_appearance_mode(self._button_hover_color))
|
||||
|
||||
def _on_leave(self, event=0):
|
||||
# set color of inner button parts
|
||||
self._canvas.itemconfig("inner_parts_right",
|
||||
outline=self._apply_appearance_mode(self._button_color),
|
||||
fill=self._apply_appearance_mode(self._button_color))
|
||||
|
||||
def _variable_callback(self, var_name, index, mode):
|
||||
if not self._variable_callback_blocked:
|
||||
self._current_value = self._variable.get()
|
||||
self._text_label.configure(text=self._current_value)
|
||||
|
||||
def _dropdown_callback(self, value: str):
|
||||
self._current_value = value
|
||||
self._text_label.configure(text=self._current_value)
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(self._current_value)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
if self._command is not None:
|
||||
self._command(self._current_value)
|
||||
|
||||
def set(self, value: str):
|
||||
self._current_value = value
|
||||
self._text_label.configure(text=self._current_value)
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(self._current_value)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def get(self) -> str:
|
||||
return self._current_value
|
||||
|
||||
def _clicked(self, event=0):
|
||||
if self._state is not tkinter.DISABLED and len(self._values) > 0:
|
||||
self._open_dropdown_menu()
|
||||
|
||||
def bind(self, sequence: str = None, command: Callable = None, add: str = None) -> str:
|
||||
""" called on the tkinter.Label and tkinter.Canvas """
|
||||
canvas_bind_return = self._canvas.bind(sequence, command, add)
|
||||
label_bind_return = self._text_label.bind(sequence, command, add)
|
||||
return canvas_bind_return + " + " + label_bind_return
|
||||
|
||||
def unbind(self, sequence: str, funcid: str = None):
|
||||
""" called on the tkinter.Label and tkinter.Canvas """
|
||||
canvas_bind_return, label_bind_return = funcid.split(" + ")
|
||||
self._canvas.unbind(sequence, canvas_bind_return)
|
||||
self._text_label.unbind(sequence, label_bind_return)
|
||||
|
||||
def focus(self):
|
||||
return self._text_label.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._text_label.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._text_label.focus_force()
|
293
customtkinter/windows/widgets/ctk_progressbar.py
Normal file
293
customtkinter/windows/widgets/ctk_progressbar.py
Normal file
@ -0,0 +1,293 @@
|
||||
import tkinter
|
||||
import math
|
||||
from typing import Union, Tuple
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
from .theme.theme_manager import ThemeManager
|
||||
from .core_rendering.draw_engine import DrawEngine
|
||||
from .core_widget_classes.widget_base_class import CTkBaseClass
|
||||
|
||||
|
||||
class CTkProgressBar(CTkBaseClass):
|
||||
"""
|
||||
Progressbar with rounded corners, border, variable support,
|
||||
indeterminate mode, vertical orientation.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: Union[int, str] = "default_init",
|
||||
height: Union[int, str] = "default_init",
|
||||
corner_radius: Union[str, Tuple[str, 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]] = "default_theme",
|
||||
border_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
progress_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
variable: tkinter.Variable = None,
|
||||
orientation: str = "horizontal",
|
||||
mode: str = "determinate",
|
||||
determinate_speed: float = 1,
|
||||
indeterminate_speed: float = 1,
|
||||
**kwargs):
|
||||
|
||||
# set default dimensions according to orientation
|
||||
if width == "default_init":
|
||||
if orientation.lower() == "vertical":
|
||||
width = 8
|
||||
else:
|
||||
width = 200
|
||||
if height == "default_init":
|
||||
if orientation.lower() == "vertical":
|
||||
height = 200
|
||||
else:
|
||||
height = 8
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# color
|
||||
self._border_color = ThemeManager.theme["color"]["progressbar_border"] if border_color == "default_theme" else border_color
|
||||
self._fg_color = ThemeManager.theme["color"]["progressbar"] if fg_color == "default_theme" else fg_color
|
||||
self._progress_color = ThemeManager.theme["color"]["progressbar_progress"] if progress_color == "default_theme" else progress_color
|
||||
|
||||
# control variable
|
||||
self._variable = variable
|
||||
self._variable_callback_blocked = False
|
||||
self._variable_callback_name = None
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["progressbar_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
self._border_width = ThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width
|
||||
self._determinate_value: float = 0.5 # range 0-1
|
||||
self._determinate_speed = determinate_speed # range 0-1
|
||||
self._indeterminate_value: float = 0 # range 0-inf
|
||||
self._indeterminate_width: float = 0.4 # range 0-1
|
||||
self._indeterminate_speed = indeterminate_speed # range 0-1 to travel in 50ms
|
||||
self._loop_running: bool = False
|
||||
self._orientation = orientation
|
||||
self._mode = mode # "determinate" or "indeterminate"
|
||||
|
||||
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._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe")
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._draw() # initial draw
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self._variable_callback_blocked = True
|
||||
self.set(self._variable.get(), from_variable_callback=True)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
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 destroy(self):
|
||||
if self._variable is not None:
|
||||
self._variable.trace_remove("write", self._variable_callback_name)
|
||||
|
||||
super().destroy()
|
||||
|
||||
def _draw(self, no_color_updates=False):
|
||||
if self._orientation.lower() == "horizontal":
|
||||
orientation = "w"
|
||||
elif self._orientation.lower() == "vertical":
|
||||
orientation = "s"
|
||||
else:
|
||||
orientation = "w"
|
||||
|
||||
if self._mode == "determinate":
|
||||
requires_recoloring = self._draw_engine.draw_rounded_progress_bar_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),
|
||||
0,
|
||||
self._determinate_value,
|
||||
orientation)
|
||||
else: # indeterminate mode
|
||||
progress_value = (math.sin(self._indeterminate_value * math.pi / 40) + 1) / 2
|
||||
progress_value_1 = min(1.0, progress_value + (self._indeterminate_width / 2))
|
||||
progress_value_2 = max(0.0, progress_value - (self._indeterminate_width / 2))
|
||||
|
||||
requires_recoloring = self._draw_engine.draw_rounded_progress_bar_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),
|
||||
progress_value_1,
|
||||
progress_value_2,
|
||||
orientation)
|
||||
|
||||
if no_color_updates is False or requires_recoloring:
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._border_color),
|
||||
outline=self._apply_appearance_mode(self._border_color))
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
self._canvas.itemconfig("progress_parts",
|
||||
fill=self._apply_appearance_mode(self._progress_color),
|
||||
outline=self._apply_appearance_mode(self._progress_color))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_color" in kwargs:
|
||||
self._border_color = kwargs.pop("border_color")
|
||||
require_redraw = True
|
||||
|
||||
if "progress_color" in kwargs:
|
||||
self._progress_color = kwargs.pop("progress_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
require_redraw = True
|
||||
|
||||
if "variable" in kwargs:
|
||||
if self._variable is not None:
|
||||
self._variable.trace_remove("write", self._variable_callback_name)
|
||||
|
||||
self._variable = kwargs.pop("variable")
|
||||
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self.set(self._variable.get(), from_variable_callback=True)
|
||||
else:
|
||||
self._variable = None
|
||||
|
||||
if "mode" in kwargs:
|
||||
self._mode = kwargs.pop("mode")
|
||||
require_redraw = True
|
||||
|
||||
if "determinate_speed" in kwargs:
|
||||
self._determinate_speed = kwargs.pop("determinate_speed")
|
||||
|
||||
if "indeterminate_speed" in kwargs:
|
||||
self._indeterminate_speed = kwargs.pop("indeterminate_speed")
|
||||
|
||||
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 == "progress_color":
|
||||
return self._progress_color
|
||||
|
||||
elif attribute_name == "variable":
|
||||
return self._variable
|
||||
elif attribute_name == "orientation":
|
||||
return self._orientation
|
||||
elif attribute_name == "mode":
|
||||
return self._mode
|
||||
elif attribute_name == "determinate_speed":
|
||||
return self._determinate_speed
|
||||
elif attribute_name == "indeterminate_speed":
|
||||
return self._indeterminate_speed
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def _variable_callback(self, var_name, index, mode):
|
||||
if not self._variable_callback_blocked:
|
||||
self.set(self._variable.get(), from_variable_callback=True)
|
||||
|
||||
def set(self, value, from_variable_callback=False):
|
||||
""" set determinate value """
|
||||
self._determinate_value = value
|
||||
|
||||
if self._determinate_value > 1:
|
||||
self._determinate_value = 1
|
||||
elif self._determinate_value < 0:
|
||||
self._determinate_value = 0
|
||||
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(round(self._determinate_value) if isinstance(self._variable, tkinter.IntVar) else self._determinate_value)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def get(self) -> float:
|
||||
""" get determinate value """
|
||||
return self._determinate_value
|
||||
|
||||
def start(self):
|
||||
""" start indeterminate mode """
|
||||
if not self._loop_running:
|
||||
self._loop_running = True
|
||||
self._internal_loop()
|
||||
|
||||
def stop(self):
|
||||
""" stop indeterminate mode """
|
||||
self._loop_running = False
|
||||
|
||||
def _internal_loop(self):
|
||||
if self._loop_running:
|
||||
if self._mode == "determinate":
|
||||
self._determinate_value += self._determinate_speed / 50
|
||||
if self._determinate_value > 1:
|
||||
self._determinate_value -= 1
|
||||
self._draw()
|
||||
self.after(20, self._internal_loop)
|
||||
else:
|
||||
self._indeterminate_value += self._indeterminate_speed
|
||||
self._draw()
|
||||
self.after(20, self._internal_loop)
|
||||
|
||||
def step(self):
|
||||
if self._mode == "determinate":
|
||||
self._determinate_value += self._determinate_speed / 50
|
||||
if self._determinate_value > 1:
|
||||
self._determinate_value -= 1
|
||||
self._draw()
|
||||
else:
|
||||
self._indeterminate_value += self._indeterminate_speed
|
||||
self._draw()
|
||||
|
||||
def bind(self, sequence=None, command=None, add=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.bind(sequence, command, add)
|
||||
|
||||
def unbind(self, sequence, funcid=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.unbind(sequence, funcid)
|
||||
|
||||
def focus(self):
|
||||
return self._canvas.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._canvas.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._canvas.focus_force()
|
399
customtkinter/windows/widgets/ctk_radiobutton.py
Normal file
399
customtkinter/windows/widgets/ctk_radiobutton.py
Normal file
@ -0,0 +1,399 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Union, Tuple, Callable
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
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
|
||||
|
||||
|
||||
class CTkRadioButton(CTkBaseClass):
|
||||
"""
|
||||
Radiobutton with rounded corners, border, label, variable support, command.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 100,
|
||||
height: int = 22,
|
||||
radiobutton_width: int = 22,
|
||||
radiobutton_height: int = 22,
|
||||
corner_radius: Union[int, str] = "default_theme",
|
||||
border_width_unchecked: Union[int, str] = "default_theme",
|
||||
border_width_checked: Union[int, str] = "default_theme",
|
||||
|
||||
bg_color: Union[str, Tuple[str, str], None] = None,
|
||||
fg_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
border_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
text: str = "CTkRadioButton",
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
textvariable: tkinter.Variable = None,
|
||||
variable: tkinter.Variable = None,
|
||||
value: Union[int, str] = 0,
|
||||
state: str = tkinter.NORMAL,
|
||||
hover: bool = True,
|
||||
command: Callable = None,
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# dimensions
|
||||
self._radiobutton_width = radiobutton_width
|
||||
self._radiobutton_height = radiobutton_height
|
||||
|
||||
# color
|
||||
self._fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
|
||||
self._hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
|
||||
self._border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["radiobutton_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
self._border_width_unchecked = ThemeManager.theme["shape"]["radiobutton_border_width_unchecked"] if border_width_unchecked == "default_theme" else border_width_unchecked
|
||||
self._border_width_checked = ThemeManager.theme["shape"]["radiobutton_border_width_checked"] if border_width_checked == "default_theme" else border_width_checked
|
||||
self._border_width = self._border_width_unchecked
|
||||
|
||||
# text
|
||||
self._text = text
|
||||
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
|
||||
|
||||
# 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
|
||||
self._state = state
|
||||
self._hover = hover
|
||||
self._check_state: bool = False
|
||||
self._value = value
|
||||
self._variable: tkinter.Variable = variable
|
||||
self._variable_callback_blocked: bool = False
|
||||
self._textvariable = textvariable
|
||||
self._variable_callback_name: Union[str, None] = None
|
||||
|
||||
# configure grid system (3x1)
|
||||
self.grid_columnconfigure(0, weight=0)
|
||||
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
|
||||
self.grid_columnconfigure(2, weight=1)
|
||||
|
||||
self._bg_canvas = CTkCanvas(master=self,
|
||||
highlightthickness=0,
|
||||
width=self._apply_widget_scaling(self._current_width),
|
||||
height=self._apply_widget_scaling(self._current_height))
|
||||
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
|
||||
|
||||
self._canvas = CTkCanvas(master=self,
|
||||
highlightthickness=0,
|
||||
width=self._apply_widget_scaling(self._radiobutton_width),
|
||||
height=self._apply_widget_scaling(self._radiobutton_height))
|
||||
self._canvas.grid(row=0, column=0)
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._canvas.bind("<Enter>", self._on_enter)
|
||||
self._canvas.bind("<Leave>", self._on_leave)
|
||||
self._canvas.bind("<Button-1>", self.invoke)
|
||||
|
||||
self._text_label = tkinter.Label(master=self,
|
||||
bd=0,
|
||||
padx=0,
|
||||
pady=0,
|
||||
text=self._text,
|
||||
justify=tkinter.LEFT,
|
||||
font=self._apply_font_scaling(self._font),
|
||||
textvariable=self._textvariable)
|
||||
self._text_label.grid(row=0, column=2, sticky="w")
|
||||
self._text_label["anchor"] = "w"
|
||||
|
||||
self._text_label.bind("<Enter>", self._on_enter)
|
||||
self._text_label.bind("<Leave>", self._on_leave)
|
||||
self._text_label.bind("<Button-1>", self.invoke)
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self._check_state = True if self._variable.get() == self._value else False
|
||||
|
||||
self._draw() # initial draw
|
||||
self._set_cursor()
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
|
||||
self._text_label.configure(font=self._apply_font_scaling(self._font))
|
||||
|
||||
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._radiobutton_width),
|
||||
height=self._apply_widget_scaling(self._radiobutton_height))
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
def _set_dimensions(self, width: int = None, height: int = None):
|
||||
super()._set_dimensions(width, height)
|
||||
|
||||
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):
|
||||
requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._radiobutton_width),
|
||||
self._apply_widget_scaling(self._radiobutton_height),
|
||||
self._apply_widget_scaling(self._corner_radius),
|
||||
self._apply_widget_scaling(self._border_width))
|
||||
|
||||
if no_color_updates is False or requires_recoloring:
|
||||
self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
if self._check_state is False:
|
||||
self._canvas.itemconfig("border_parts",
|
||||
outline=self._apply_appearance_mode(self._border_color),
|
||||
fill=self._apply_appearance_mode(self._border_color))
|
||||
else:
|
||||
self._canvas.itemconfig("border_parts",
|
||||
outline=self._apply_appearance_mode(self._fg_color),
|
||||
fill=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
outline=self._apply_appearance_mode(self._bg_color),
|
||||
fill=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
if self._state == tkinter.DISABLED:
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color_disabled))
|
||||
else:
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "radiobutton_width" in kwargs:
|
||||
self._radiobutton_width = kwargs.pop("radiobutton_width")
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._radiobutton_width))
|
||||
require_redraw = True
|
||||
|
||||
if "radiobutton_height" in kwargs:
|
||||
self._radiobutton_height = kwargs.pop("radiobutton_height")
|
||||
self._canvas.configure(height=self._apply_widget_scaling(self._radiobutton_height))
|
||||
require_redraw = True
|
||||
|
||||
if "text" in kwargs:
|
||||
self._text = kwargs.pop("text")
|
||||
self._text_label.configure(text=self._text)
|
||||
|
||||
if "font" in kwargs:
|
||||
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")
|
||||
self._set_cursor()
|
||||
require_redraw = True
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "hover_color" in kwargs:
|
||||
self._hover_color = kwargs.pop("hover_color")
|
||||
require_redraw = True
|
||||
|
||||
if "text_color" in kwargs:
|
||||
self._text_color = kwargs.pop("text_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_color" in kwargs:
|
||||
self._border_color = kwargs.pop("border_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
require_redraw = True
|
||||
|
||||
if "hover" in kwargs:
|
||||
self._hover = kwargs.pop("hover")
|
||||
|
||||
if "command" in kwargs:
|
||||
self._command = kwargs.pop("command")
|
||||
|
||||
if "textvariable" in kwargs:
|
||||
self._textvariable = kwargs.pop("textvariable")
|
||||
self._text_label.configure(textvariable=self._textvariable)
|
||||
|
||||
if "variable" in kwargs:
|
||||
if self._variable is not None:
|
||||
self._variable.trace_remove("write", self._variable_callback_name)
|
||||
|
||||
self._variable = kwargs.pop("variable")
|
||||
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self._check_state = True if self._variable.get() == self._value else False
|
||||
require_redraw = True
|
||||
|
||||
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_unchecked":
|
||||
return self._border_width_unchecked
|
||||
elif attribute_name == "border_width_checked":
|
||||
return self._border_width_checked
|
||||
elif attribute_name == "radiobutton_width":
|
||||
return self._radiobutton_width
|
||||
elif attribute_name == "radiobutton_height":
|
||||
return self._radiobutton_height
|
||||
|
||||
elif attribute_name == "fg_color":
|
||||
return self._fg_color
|
||||
elif attribute_name == "hover_color":
|
||||
return self._hover_color
|
||||
elif attribute_name == "border_color":
|
||||
return self._border_color
|
||||
elif attribute_name == "text_color":
|
||||
return self._text_color
|
||||
elif attribute_name == "text_color_disabled":
|
||||
return self._text_color_disabled
|
||||
|
||||
elif attribute_name == "text":
|
||||
return self._text
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "textvariable":
|
||||
return self._textvariable
|
||||
elif attribute_name == "variable":
|
||||
return self._variable
|
||||
elif attribute_name == "value":
|
||||
return self._value
|
||||
elif attribute_name == "state":
|
||||
return self._state
|
||||
elif attribute_name == "hover":
|
||||
return self._hover
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def _set_cursor(self):
|
||||
if self._cursor_manipulation_enabled:
|
||||
if self._state == tkinter.DISABLED:
|
||||
if sys.platform == "darwin":
|
||||
self._canvas.configure(cursor="arrow")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="arrow")
|
||||
elif sys.platform.startswith("win"):
|
||||
self._canvas.configure(cursor="arrow")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="arrow")
|
||||
|
||||
elif self._state == tkinter.NORMAL:
|
||||
if sys.platform == "darwin":
|
||||
self._canvas.configure(cursor="pointinghand")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="pointinghand")
|
||||
elif sys.platform.startswith("win"):
|
||||
self._canvas.configure(cursor="hand2")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="hand2")
|
||||
|
||||
def _on_enter(self, event=0):
|
||||
if self._hover is True and self._state == tkinter.NORMAL:
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._hover_color),
|
||||
outline=self._apply_appearance_mode(self._hover_color))
|
||||
|
||||
def _on_leave(self, event=0):
|
||||
if self._check_state is True:
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._border_color),
|
||||
outline=self._apply_appearance_mode(self._border_color))
|
||||
|
||||
def _variable_callback(self, var_name, index, mode):
|
||||
if not self._variable_callback_blocked:
|
||||
if self._variable.get() == self._value:
|
||||
self.select(from_variable_callback=True)
|
||||
else:
|
||||
self.deselect(from_variable_callback=True)
|
||||
|
||||
def invoke(self, event=0):
|
||||
if self._state == tkinter.NORMAL:
|
||||
if self._check_state is False:
|
||||
self._check_state = True
|
||||
self.select()
|
||||
|
||||
if self._command is not None:
|
||||
self._command()
|
||||
|
||||
def select(self, from_variable_callback=False):
|
||||
self._check_state = True
|
||||
self._border_width = self._border_width_checked
|
||||
self._draw()
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(self._value)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def deselect(self, from_variable_callback=False):
|
||||
self._check_state = False
|
||||
self._border_width = self._border_width_unchecked
|
||||
self._draw()
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set("")
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def bind(self, sequence=None, command=None, add=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.bind(sequence, command, add)
|
||||
|
||||
def unbind(self, sequence, funcid=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.unbind(sequence, funcid)
|
||||
|
||||
def focus(self):
|
||||
return self._text_label.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._text_label.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._text_label.focus_force()
|
265
customtkinter/windows/widgets/ctk_scrollbar.py
Normal file
265
customtkinter/windows/widgets/ctk_scrollbar.py
Normal file
@ -0,0 +1,265 @@
|
||||
import sys
|
||||
from typing import Union, Tuple, Callable
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
from .theme.theme_manager import ThemeManager
|
||||
from .core_rendering.draw_engine import DrawEngine
|
||||
from .core_widget_classes.widget_base_class import CTkBaseClass
|
||||
|
||||
|
||||
class CTkScrollbar(CTkBaseClass):
|
||||
"""
|
||||
Scrollbar with rounded corners, configurable spacing.
|
||||
Connect to scrollable widget by passing .set() method and set command attribute.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: Union[int, str] = "default_init",
|
||||
height: Union[int, str] = "default_init",
|
||||
corner_radius: Union[int, str] = "default_theme",
|
||||
border_spacing: Union[int, str] = "default_theme",
|
||||
minimum_pixel_length: int = 20,
|
||||
|
||||
bg_color: Union[str, Tuple[str, str], None] = None,
|
||||
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
|
||||
scrollbar_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
scrollbar_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
hover: bool = True,
|
||||
command: Callable = None,
|
||||
orientation: str = "vertical",
|
||||
**kwargs):
|
||||
|
||||
# set default dimensions according to orientation
|
||||
if width == "default_init":
|
||||
if orientation.lower() == "vertical":
|
||||
width = 16
|
||||
else:
|
||||
width = 200
|
||||
if height == "default_init":
|
||||
if orientation.lower() == "horizontal":
|
||||
height = 16
|
||||
else:
|
||||
height = 200
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# color
|
||||
self._fg_color = ThemeManager.theme["color"]["frame_high"] if fg_color == "default_theme" else fg_color
|
||||
self._scrollbar_color = ThemeManager.theme["color"]["scrollbar_button"] if scrollbar_color == "default_theme" else scrollbar_color
|
||||
self._scrollbar_hover_color = ThemeManager.theme["color"]["scrollbar_button_hover"] if scrollbar_hover_color == "default_theme" else scrollbar_hover_color
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["scrollbar_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
self._border_spacing = ThemeManager.theme["shape"]["scrollbar_border_spacing"] if border_spacing == "default_theme" else border_spacing
|
||||
|
||||
self._hover = hover
|
||||
self._hover_state: bool = False
|
||||
self._command = command
|
||||
self._orientation = orientation
|
||||
self._start_value: float = 0 # 0 to 1
|
||||
self._end_value: float = 1 # 0 to 1
|
||||
self._minimum_pixel_length = minimum_pixel_length
|
||||
|
||||
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.place(x=0, y=0, relwidth=1, relheight=1)
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._canvas.bind("<Enter>", self._on_enter)
|
||||
self._canvas.bind("<Leave>", self._on_leave)
|
||||
self._canvas.tag_bind("border_parts", "<Button-1>", self._clicked)
|
||||
self._canvas.bind("<B1-Motion>", self._clicked)
|
||||
self._canvas.bind("<MouseWheel>", self._mouse_scroll_event)
|
||||
|
||||
self._draw()
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
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(no_color_updates=True)
|
||||
|
||||
def _get_scrollbar_values_for_minimum_pixel_size(self):
|
||||
# correct scrollbar float values if scrollbar is too small
|
||||
if self._orientation == "vertical":
|
||||
scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_height
|
||||
if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0:
|
||||
# calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
|
||||
interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height)
|
||||
corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
|
||||
corrected_start_value = self._start_value - self._start_value * interval_extend_factor
|
||||
return corrected_start_value, corrected_end_value
|
||||
else:
|
||||
return self._start_value, self._end_value
|
||||
|
||||
else:
|
||||
scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_width
|
||||
if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0:
|
||||
# calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length
|
||||
interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width)
|
||||
corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor
|
||||
corrected_start_value = self._start_value - self._start_value * interval_extend_factor
|
||||
return corrected_start_value, corrected_end_value
|
||||
else:
|
||||
return self._start_value, self._end_value
|
||||
|
||||
def _draw(self, no_color_updates=False):
|
||||
corrected_start_value, corrected_end_value = self._get_scrollbar_values_for_minimum_pixel_size()
|
||||
requires_recoloring = self._draw_engine.draw_rounded_scrollbar(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_spacing),
|
||||
corrected_start_value,
|
||||
corrected_end_value,
|
||||
self._orientation)
|
||||
|
||||
if no_color_updates is False or requires_recoloring:
|
||||
if self._hover_state is True:
|
||||
self._canvas.itemconfig("scrollbar_parts",
|
||||
fill=self._apply_appearance_mode(self._scrollbar_hover_color),
|
||||
outline=self._apply_appearance_mode(self._scrollbar_hover_color))
|
||||
else:
|
||||
self._canvas.itemconfig("scrollbar_parts",
|
||||
fill=self._apply_appearance_mode(self._scrollbar_color),
|
||||
outline=self._apply_appearance_mode(self._scrollbar_color))
|
||||
|
||||
if self._fg_color is None:
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._bg_color),
|
||||
outline=self._apply_appearance_mode(self._bg_color))
|
||||
else:
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._fg_color))
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
self._canvas.update_idletasks()
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "scrollbar_color" in kwargs:
|
||||
self._scrollbar_color = kwargs.pop("scrollbar_color")
|
||||
require_redraw = True
|
||||
|
||||
if "scrollbar_hover_color" in kwargs:
|
||||
self._scrollbar_hover_color = kwargs.pop("scrollbar_hover_color")
|
||||
require_redraw = True
|
||||
|
||||
if "hover" in kwargs:
|
||||
self._hover = kwargs.pop("hover")
|
||||
|
||||
if "command" in kwargs:
|
||||
self._command = kwargs.pop("command")
|
||||
|
||||
if "corner_radius" in kwargs:
|
||||
self._corner_radius = kwargs.pop("corner_radius")
|
||||
require_redraw = True
|
||||
|
||||
if "border_spacing" in kwargs:
|
||||
self._border_spacing = kwargs.pop("border_spacing")
|
||||
require_redraw = True
|
||||
|
||||
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_spacing":
|
||||
return self._border_spacing
|
||||
elif attribute_name == "minimum_pixel_length":
|
||||
return self._minimum_pixel_length
|
||||
|
||||
elif attribute_name == "fg_color":
|
||||
return self._fg_color
|
||||
elif attribute_name == "scrollbar_color":
|
||||
return self._scrollbar_color
|
||||
elif attribute_name == "scrollbar_hover_color":
|
||||
return self._scrollbar_hover_color
|
||||
|
||||
elif attribute_name == "hover":
|
||||
return self._hover
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
elif attribute_name == "orientation":
|
||||
return self._orientation
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def _on_enter(self, event=0):
|
||||
if self._hover is True:
|
||||
self._hover_state = True
|
||||
self._canvas.itemconfig("scrollbar_parts",
|
||||
outline=self._apply_appearance_mode(self._scrollbar_hover_color),
|
||||
fill=self._apply_appearance_mode(self._scrollbar_hover_color))
|
||||
|
||||
def _on_leave(self, event=0):
|
||||
self._hover_state = False
|
||||
self._canvas.itemconfig("scrollbar_parts",
|
||||
outline=self._apply_appearance_mode(self._scrollbar_color),
|
||||
fill=self._apply_appearance_mode(self._scrollbar_color))
|
||||
|
||||
def _clicked(self, event):
|
||||
if self._orientation == "vertical":
|
||||
value = ((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing)) / self._widget_scaling
|
||||
else:
|
||||
value = ((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing)) / self._widget_scaling
|
||||
|
||||
current_scrollbar_length = self._end_value - self._start_value
|
||||
value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2)))
|
||||
self._start_value = value - (current_scrollbar_length / 2)
|
||||
self._end_value = value + (current_scrollbar_length / 2)
|
||||
self._draw()
|
||||
|
||||
if self._command is not None:
|
||||
self._command('moveto', self._start_value)
|
||||
|
||||
def _mouse_scroll_event(self, event=None):
|
||||
if self._command is not None:
|
||||
if sys.platform.startswith("win"):
|
||||
self._command('scroll', -int(event.delta/40), 'units')
|
||||
else:
|
||||
self._command('scroll', -event.delta, 'units')
|
||||
|
||||
def set(self, start_value: float, end_value: float):
|
||||
self._start_value = float(start_value)
|
||||
self._end_value = float(end_value)
|
||||
self._draw()
|
||||
|
||||
def get(self):
|
||||
return self._start_value, self._end_value
|
||||
|
||||
def bind(self, sequence=None, command=None, add=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.bind(sequence, command, add)
|
||||
|
||||
def unbind(self, sequence, funcid=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.unbind(sequence, funcid)
|
||||
|
||||
def focus(self):
|
||||
return self._canvas.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._canvas.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._canvas.focus_force()
|
406
customtkinter/windows/widgets/ctk_segmented_button.py
Normal file
406
customtkinter/windows/widgets/ctk_segmented_button.py
Normal file
@ -0,0 +1,406 @@
|
||||
import tkinter
|
||||
from typing import Union, Tuple, List, Dict, Callable
|
||||
|
||||
from .theme.theme_manager import ThemeManager
|
||||
from .ctk_button import CTkButton
|
||||
from .ctk_frame import CTkFrame
|
||||
from .font.ctk_font import CTkFont
|
||||
|
||||
|
||||
class CTkSegmentedButton(CTkFrame):
|
||||
"""
|
||||
Segmented button with corner radius, border width, variable support.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 140,
|
||||
height: int = 28,
|
||||
corner_radius: Union[int, str] = "default_theme",
|
||||
border_width: Union[int, str] = 3,
|
||||
|
||||
bg_color: Union[str, Tuple[str, str], None] = None,
|
||||
fg_color: Union[str, Tuple[str, str], None] = "default_theme",
|
||||
selected_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
selected_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
unselected_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
unselected_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
|
||||
background_corner_colors: Tuple[Union[str, Tuple[str, str]]] = None,
|
||||
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
values: list = None,
|
||||
variable: tkinter.Variable = None,
|
||||
dynamic_resizing: bool = True,
|
||||
command: Callable[[str], None] = None,
|
||||
state: str = "normal",
|
||||
**kwargs):
|
||||
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
self._sb_fg_color = ThemeManager.theme["color"]["segmented_button"] if fg_color == "default_theme" else fg_color
|
||||
|
||||
self._sb_selected_color = ThemeManager.theme["color"]["button"] if selected_color == "default_theme" else selected_color
|
||||
self._sb_selected_hover_color = ThemeManager.theme["color"]["button_hover"] if selected_hover_color == "default_theme" else selected_hover_color
|
||||
|
||||
self._sb_unselected_color = ThemeManager.theme["color"]["segmented_button_unselected"] if unselected_color == "default_theme" else unselected_color
|
||||
self._sb_unselected_hover_color = ThemeManager.theme["color"]["segmented_button_unselected_hover"] if unselected_hover_color == "default_theme" else unselected_hover_color
|
||||
|
||||
self._sb_text_color = ThemeManager.theme["color"]["text_button"] if text_color == "default_theme" else text_color
|
||||
self._sb_text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled
|
||||
|
||||
self._sb_corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
self._sb_border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width
|
||||
|
||||
self._background_corner_colors = background_corner_colors # rendering options for DrawEngine
|
||||
|
||||
self._command: Callable[[str], None] = command
|
||||
self._font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if font == "default_theme" else font
|
||||
self._state = state
|
||||
|
||||
self._buttons_dict: Dict[str, CTkButton] = {} # mapped from value to button object
|
||||
if values is None:
|
||||
self._value_list: List[str] = ["CTkSegmentedButton"]
|
||||
else:
|
||||
self._value_list: List[str] = values # Values ordered like buttons rendered on widget
|
||||
|
||||
self._dynamic_resizing = dynamic_resizing
|
||||
if not self._dynamic_resizing:
|
||||
self.grid_propagate(False)
|
||||
|
||||
self._check_unique_values(self._value_list)
|
||||
self._current_value: str = ""
|
||||
if len(self._value_list) > 0:
|
||||
self._create_buttons_from_values()
|
||||
self._create_button_grid()
|
||||
|
||||
self._variable = variable
|
||||
self._variable_callback_blocked: bool = False
|
||||
self._variable_callback_name: Union[str, None] = None
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self.set(self._variable.get(), from_variable_callback=True)
|
||||
|
||||
super().configure(corner_radius=self._sb_corner_radius, fg_color=None)
|
||||
|
||||
def destroy(self):
|
||||
if self._variable is not None: # remove old callback
|
||||
self._variable.trace_remove("write", self._variable_callback_name)
|
||||
|
||||
super().destroy()
|
||||
|
||||
def _set_dimensions(self, width: int = None, height: int = None):
|
||||
super()._set_dimensions(width, height)
|
||||
|
||||
for button in self._buttons_dict.values():
|
||||
button.configure(height=height)
|
||||
|
||||
def _variable_callback(self, var_name, index, mode):
|
||||
if not self._variable_callback_blocked:
|
||||
self.set(self._variable.get(), from_variable_callback=True)
|
||||
|
||||
def _get_index_by_value(self, value: str):
|
||||
for index, value_from_list in enumerate(self._value_list):
|
||||
if value_from_list == value:
|
||||
return index
|
||||
|
||||
raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
|
||||
|
||||
def _configure_button_corners_for_index(self, index: int):
|
||||
if index == 0 and len(self._value_list) == 1:
|
||||
if self._background_corner_colors is None:
|
||||
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
|
||||
else:
|
||||
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=self._background_corner_colors)
|
||||
|
||||
elif index == 0:
|
||||
if self._background_corner_colors is None:
|
||||
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._sb_fg_color, self._sb_fg_color, self._bg_color))
|
||||
else:
|
||||
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._background_corner_colors[0], self._sb_fg_color, self._sb_fg_color, self._background_corner_colors[3]))
|
||||
|
||||
elif index == len(self._value_list) - 1:
|
||||
if self._background_corner_colors is None:
|
||||
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._bg_color, self._bg_color, self._sb_fg_color))
|
||||
else:
|
||||
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._background_corner_colors[1], self._background_corner_colors[2], self._sb_fg_color))
|
||||
|
||||
else:
|
||||
self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._sb_fg_color, self._sb_fg_color))
|
||||
|
||||
def _unselect_button_by_value(self, value: str):
|
||||
if value in self._buttons_dict:
|
||||
self._buttons_dict[value].configure(fg_color=self._sb_unselected_color,
|
||||
hover_color=self._sb_unselected_hover_color)
|
||||
|
||||
def _select_button_by_value(self, value: str):
|
||||
if self._current_value is not None and self._current_value != "":
|
||||
self._unselect_button_by_value(self._current_value)
|
||||
|
||||
self._current_value = value
|
||||
|
||||
self._buttons_dict[value].configure(fg_color=self._sb_selected_color,
|
||||
hover_color=self._sb_selected_hover_color)
|
||||
|
||||
def _create_button(self, index: int, value: str) -> CTkButton:
|
||||
new_button = CTkButton(self,
|
||||
height=self._current_height,
|
||||
width=0,
|
||||
corner_radius=self._sb_corner_radius,
|
||||
text=value,
|
||||
border_width=self._sb_border_width,
|
||||
border_color=self._sb_fg_color,
|
||||
fg_color=self._sb_unselected_color,
|
||||
hover_color=self._sb_unselected_hover_color,
|
||||
text_color=self._sb_text_color,
|
||||
text_color_disabled=self._sb_text_color_disabled,
|
||||
font=self._font,
|
||||
state=self._state,
|
||||
command=lambda v=value: self.set(v, from_button_callback=True),
|
||||
background_corner_colors=None,
|
||||
round_width_to_even_numbers=False) # DrawEngine rendering option (so that theres no gap between buttons)
|
||||
|
||||
return new_button
|
||||
|
||||
@staticmethod
|
||||
def _check_unique_values(values: List[str]):
|
||||
""" raises exception if values are not unique """
|
||||
if len(values) != len(set(values)):
|
||||
raise ValueError("CTkSegmentedButton values are not unique")
|
||||
|
||||
def _create_button_grid(self):
|
||||
# remove minsize from every grid cell in the first row
|
||||
number_of_columns, _ = self.grid_size()
|
||||
for n in range(number_of_columns):
|
||||
self.grid_columnconfigure(n, weight=1, minsize=0)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
|
||||
for index, value in enumerate(self._value_list):
|
||||
self.grid_columnconfigure(index, weight=1, minsize=self._current_height)
|
||||
self._buttons_dict[value].grid(row=0, column=index, sticky="ew")
|
||||
|
||||
def _create_buttons_from_values(self):
|
||||
assert len(self._buttons_dict) == 0
|
||||
assert len(self._value_list) > 0
|
||||
|
||||
for index, value in enumerate(self._value_list):
|
||||
self._buttons_dict[value] = self._create_button(index, value)
|
||||
self._configure_button_corners_for_index(index)
|
||||
|
||||
def configure(self, **kwargs):
|
||||
if "bg_color" in kwargs:
|
||||
super().configure(bg_color=kwargs.pop("bg_color"))
|
||||
|
||||
if len(self._buttons_dict) > 0:
|
||||
self._configure_button_corners_for_index(0)
|
||||
if len(self._buttons_dict) > 1:
|
||||
max_index = len(self._buttons_dict) - 1
|
||||
self._configure_button_corners_for_index(max_index)
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._sb_fg_color = kwargs.pop("fg_color")
|
||||
for index, button in enumerate(self._buttons_dict.values()):
|
||||
button.configure(border_color=self._sb_fg_color)
|
||||
self._configure_button_corners_for_index(index)
|
||||
|
||||
if "selected_color" in kwargs:
|
||||
self._sb_selected_color = kwargs.pop("selected_color")
|
||||
if self._current_value in self._buttons_dict:
|
||||
self._buttons_dict[self._current_value].configure(fg_color=self._sb_selected_color)
|
||||
|
||||
if "selected_hover_color" in kwargs:
|
||||
self._sb_selected_hover_color = kwargs.pop("selected_hover_color")
|
||||
if self._current_value in self._buttons_dict:
|
||||
self._buttons_dict[self._current_value].configure(hover_color=self._sb_selected_hover_color)
|
||||
|
||||
if "unselected_color" in kwargs:
|
||||
self._sb_unselected_color = kwargs.pop("unselected_color")
|
||||
for value, button in self._buttons_dict.items():
|
||||
if value != self._current_value:
|
||||
button.configure(fg_color=self._sb_unselected_color)
|
||||
|
||||
if "unselected_hover_color" in kwargs:
|
||||
self._sb_unselected_hover_color = kwargs.pop("unselected_hover_color")
|
||||
for value, button in self._buttons_dict.items():
|
||||
if value != self._current_value:
|
||||
button.configure(hover_color=self._sb_unselected_hover_color)
|
||||
|
||||
if "text_color" in kwargs:
|
||||
self._sb_text_color = kwargs.pop("text_color")
|
||||
for button in self._buttons_dict.values():
|
||||
button.configure(text_color=self._sb_text_color)
|
||||
|
||||
if "text_color_disabled" in kwargs:
|
||||
self._sb_text_color_disabled = kwargs.pop("text_color_disabled")
|
||||
for button in self._buttons_dict.values():
|
||||
button.configure(text_color_disabled=self._sb_text_color_disabled)
|
||||
|
||||
if "background_corner_colors" in kwargs:
|
||||
self._background_corner_colors = kwargs.pop("background_corner_colors")
|
||||
for i in range(len(self._buttons_dict)):
|
||||
self._configure_button_corners_for_index(i)
|
||||
|
||||
if "font" in kwargs:
|
||||
self._font = kwargs.pop("font")
|
||||
for button in self._buttons_dict.values():
|
||||
button.configure(font=self._font)
|
||||
|
||||
if "values" in kwargs:
|
||||
for button in self._buttons_dict.values():
|
||||
button.destroy()
|
||||
self._buttons_dict.clear()
|
||||
self._value_list = kwargs.pop("values")
|
||||
|
||||
self._check_unique_values(self._value_list)
|
||||
|
||||
if len(self._value_list) > 0:
|
||||
self._create_buttons_from_values()
|
||||
self._create_button_grid()
|
||||
|
||||
if self._current_value in self._value_list:
|
||||
self._select_button_by_value(self._current_value)
|
||||
|
||||
if "variable" in kwargs:
|
||||
if self._variable is not None: # remove old callback
|
||||
self._variable.trace_remove("write", self._variable_callback_name)
|
||||
|
||||
self._variable = kwargs.pop("variable")
|
||||
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self.set(self._variable.get(), from_variable_callback=True)
|
||||
else:
|
||||
self._variable = None
|
||||
|
||||
if "dynamic_resizing" in kwargs:
|
||||
self._dynamic_resizing = kwargs.pop("dynamic_resizing")
|
||||
if not self._dynamic_resizing:
|
||||
self.grid_propagate(False)
|
||||
else:
|
||||
self.grid_propagate(True)
|
||||
|
||||
if "command" in kwargs:
|
||||
self._command = kwargs.pop("command")
|
||||
|
||||
if "state" in kwargs:
|
||||
self._state = kwargs.pop("state")
|
||||
for button in self._buttons_dict.values():
|
||||
button.configure(state=self._state)
|
||||
|
||||
super().configure(**kwargs)
|
||||
|
||||
def cget(self, attribute_name: str) -> any:
|
||||
if attribute_name == "corner_radius":
|
||||
return self._sb_corner_radius
|
||||
elif attribute_name == "border_width":
|
||||
return self._sb_border_width
|
||||
|
||||
elif attribute_name == "fg_color":
|
||||
return self._sb_fg_color
|
||||
elif attribute_name == "selected_color":
|
||||
return self._sb_selected_color
|
||||
elif attribute_name == "selected_hover_color":
|
||||
return self._sb_selected_hover_color
|
||||
elif attribute_name == "unselected_color":
|
||||
return self._sb_unselected_color
|
||||
elif attribute_name == "unselected_hover_color":
|
||||
return self._sb_unselected_hover_color
|
||||
elif attribute_name == "text_color":
|
||||
return self._sb_text_color
|
||||
elif attribute_name == "text_color_disabled":
|
||||
return self._sb_text_color_disabled
|
||||
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "values":
|
||||
return self._value_list
|
||||
elif attribute_name == "variable":
|
||||
return self._variable
|
||||
elif attribute_name == "dynamic_resizing":
|
||||
return self._dynamic_resizing
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def set(self, value: str, from_variable_callback: bool = False, from_button_callback: bool = False):
|
||||
if value == self._current_value:
|
||||
return
|
||||
elif value in self._buttons_dict:
|
||||
self._select_button_by_value(value)
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(value)
|
||||
self._variable_callback_blocked = False
|
||||
else:
|
||||
if self._current_value in self._buttons_dict:
|
||||
self._unselect_button_by_value(self._current_value)
|
||||
self._current_value = value
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(value)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
if from_button_callback:
|
||||
if self._command is not None:
|
||||
self._command(self._current_value)
|
||||
|
||||
def get(self) -> str:
|
||||
return self._current_value
|
||||
|
||||
def insert(self, index: int, value: str):
|
||||
if value not in self._buttons_dict:
|
||||
self._value_list.insert(index, value)
|
||||
self._buttons_dict[value] = self._create_button(index, value)
|
||||
|
||||
self._configure_button_corners_for_index(index)
|
||||
if index > 0:
|
||||
self._configure_button_corners_for_index(index - 1)
|
||||
if index < len(self._buttons_dict) - 1:
|
||||
self._configure_button_corners_for_index(index + 1)
|
||||
|
||||
self._create_button_grid()
|
||||
|
||||
if value == self._current_value:
|
||||
self._select_button_by_value(self._current_value)
|
||||
else:
|
||||
raise ValueError(f"CTkSegmentedButton can not insert value '{value}', already part of the values")
|
||||
|
||||
def move(self, new_index: int, value: str):
|
||||
if 0 <= new_index < len(self._value_list):
|
||||
if value in self._buttons_dict:
|
||||
self.delete(value)
|
||||
self.insert(new_index, value)
|
||||
else:
|
||||
raise ValueError(f"CTkSegmentedButton has no value named '{value}'")
|
||||
else:
|
||||
raise ValueError(f"CTkSegmentedButton new_index {new_index} not in range of value list with len {len(self._value_list)}")
|
||||
|
||||
def delete(self, value: str):
|
||||
if value in self._buttons_dict:
|
||||
self._buttons_dict[value].destroy()
|
||||
self._buttons_dict.pop(value)
|
||||
index_to_remove = self._get_index_by_value(value)
|
||||
self._value_list.pop(index_to_remove)
|
||||
|
||||
# removed index was outer right element
|
||||
if index_to_remove == len(self._buttons_dict) and len(self._buttons_dict) > 0:
|
||||
self._configure_button_corners_for_index(index_to_remove - 1)
|
||||
|
||||
# removed index was outer left element
|
||||
if index_to_remove == 0 and len(self._buttons_dict) > 0:
|
||||
self._configure_button_corners_for_index(0)
|
||||
|
||||
#if index_to_remove <= len(self._buttons_dict) - 1:
|
||||
# self._configure_button_corners_for_index(index_to_remove)
|
||||
|
||||
self._create_button_grid()
|
||||
else:
|
||||
raise ValueError(f"CTkSegmentedButton does not contain value '{value}'")
|
||||
|
382
customtkinter/windows/widgets/ctk_slider.py
Normal file
382
customtkinter/windows/widgets/ctk_slider.py
Normal file
@ -0,0 +1,382 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Union, Tuple, Callable
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
from .theme.theme_manager import ThemeManager
|
||||
from .core_rendering.draw_engine import DrawEngine
|
||||
from .core_widget_classes.widget_base_class import CTkBaseClass
|
||||
|
||||
|
||||
class CTkSlider(CTkBaseClass):
|
||||
"""
|
||||
Slider with rounded corners, border, number of steps, variable support, vertical orientation.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: Union[int, str] = "default_init",
|
||||
height: Union[int, str] = "default_init",
|
||||
corner_radius: Union[int, str] = "default_theme",
|
||||
button_corner_radius: Union[int, str] = "default_theme",
|
||||
border_width: Union[int, str] = "default_theme",
|
||||
button_length: Union[int, str] = "default_theme",
|
||||
|
||||
bg_color: Union[str, Tuple[str, str], None] = None,
|
||||
fg_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
border_color: Union[str, Tuple[str, str], None] = None,
|
||||
progress_color: Union[str, Tuple[str, str], None] = "default_theme",
|
||||
button_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
button_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
from_: int = 0,
|
||||
to: int = 1,
|
||||
state: str = "normal",
|
||||
number_of_steps: Union[int, None] = None,
|
||||
hover: bool = True,
|
||||
command: Callable[[float], None] = None,
|
||||
variable: tkinter.Variable = None,
|
||||
orientation: str = "horizontal",
|
||||
**kwargs):
|
||||
|
||||
# set default dimensions according to orientation
|
||||
if width == "default_init":
|
||||
if orientation.lower() == "vertical":
|
||||
width = 16
|
||||
else:
|
||||
width = 200
|
||||
if height == "default_init":
|
||||
if orientation.lower() == "vertical":
|
||||
height = 200
|
||||
else:
|
||||
height = 16
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# color
|
||||
self._border_color = border_color
|
||||
self._fg_color = ThemeManager.theme["color"]["slider"] if fg_color == "default_theme" else fg_color
|
||||
self._progress_color = ThemeManager.theme["color"]["slider_progress"] if progress_color == "default_theme" else progress_color
|
||||
self._button_color = ThemeManager.theme["color"]["slider_button"] if button_color == "default_theme" else button_color
|
||||
self._button_hover_color = ThemeManager.theme["color"]["slider_button_hover"] if button_hover_color == "default_theme" else button_hover_color
|
||||
|
||||
# shape
|
||||
self._corner_radius = ThemeManager.theme["shape"]["slider_corner_radius"] if corner_radius == "default_theme" else corner_radius
|
||||
self._button_corner_radius = ThemeManager.theme["shape"]["slider_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius
|
||||
self._border_width = ThemeManager.theme["shape"]["slider_border_width"] if border_width == "default_theme" else border_width
|
||||
self._button_length = ThemeManager.theme["shape"]["slider_button_length"] if button_length == "default_theme" else button_length
|
||||
self._value: float = 0.5 # initial value of slider in percent
|
||||
self._orientation = orientation
|
||||
self._hover_state: bool = False
|
||||
self._hover = hover
|
||||
self._from_ = from_
|
||||
self._to = to
|
||||
self._number_of_steps = number_of_steps
|
||||
self._output_value = self._from_ + (self._value * (self._to - self._from_))
|
||||
|
||||
if self._corner_radius < self._button_corner_radius:
|
||||
self._corner_radius = self._button_corner_radius
|
||||
|
||||
# callback and control variables
|
||||
self._command = command
|
||||
self._variable: tkinter.Variable = variable
|
||||
self._variable_callback_blocked: bool = False
|
||||
self._variable_callback_name: Union[bool, None] = None
|
||||
self._state = state
|
||||
|
||||
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._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe")
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._canvas.bind("<Enter>", self._on_enter)
|
||||
self._canvas.bind("<Leave>", self._on_leave)
|
||||
self._canvas.bind("<Button-1>", self._clicked)
|
||||
self._canvas.bind("<B1-Motion>", self._clicked)
|
||||
|
||||
self._set_cursor()
|
||||
self._draw() # initial draw
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self._variable_callback_blocked = True
|
||||
self.set(self._variable.get(), from_variable_callback=True)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
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 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)
|
||||
|
||||
super().destroy()
|
||||
|
||||
def _set_cursor(self):
|
||||
if self._state == "normal" and self._cursor_manipulation_enabled:
|
||||
if sys.platform == "darwin":
|
||||
self.configure(cursor="pointinghand")
|
||||
elif sys.platform.startswith("win"):
|
||||
self.configure(cursor="hand2")
|
||||
|
||||
elif self._state == "disabled" and self._cursor_manipulation_enabled:
|
||||
if sys.platform == "darwin":
|
||||
self.configure(cursor="arrow")
|
||||
elif sys.platform.startswith("win"):
|
||||
self.configure(cursor="arrow")
|
||||
|
||||
def _draw(self, no_color_updates=False):
|
||||
if self._orientation.lower() == "horizontal":
|
||||
orientation = "w"
|
||||
elif self._orientation.lower() == "vertical":
|
||||
orientation = "s"
|
||||
else:
|
||||
orientation = "w"
|
||||
|
||||
requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(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),
|
||||
self._apply_widget_scaling(self._button_length),
|
||||
self._apply_widget_scaling(self._button_corner_radius),
|
||||
self._value, orientation)
|
||||
|
||||
if no_color_updates is False or requires_recoloring:
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
if self._border_color is None:
|
||||
self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._bg_color),
|
||||
outline=self._apply_appearance_mode(self._bg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._border_color),
|
||||
outline=self._apply_appearance_mode(self._border_color))
|
||||
|
||||
self._canvas.itemconfig("inner_parts", fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
if self._progress_color is None:
|
||||
self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._progress_color),
|
||||
outline=self._apply_appearance_mode(self._progress_color))
|
||||
|
||||
if self._hover_state is True:
|
||||
self._canvas.itemconfig("slider_parts",
|
||||
fill=self._apply_appearance_mode(self._button_hover_color),
|
||||
outline=self._apply_appearance_mode(self._button_hover_color))
|
||||
else:
|
||||
self._canvas.itemconfig("slider_parts",
|
||||
fill=self._apply_appearance_mode(self._button_color),
|
||||
outline=self._apply_appearance_mode(self._button_color))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "state" in kwargs:
|
||||
self._state = kwargs.pop("state")
|
||||
self._set_cursor()
|
||||
require_redraw = True
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "progress_color" in kwargs:
|
||||
self._progress_color = kwargs.pop("progress_color")
|
||||
require_redraw = True
|
||||
|
||||
if "button_color" in kwargs:
|
||||
self._button_color = kwargs.pop("button_color")
|
||||
require_redraw = True
|
||||
|
||||
if "button_hover_color" in kwargs:
|
||||
self._button_hover_color = kwargs.pop("button_hover_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_color" in kwargs:
|
||||
self._border_color = kwargs.pop("border_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
require_redraw = True
|
||||
|
||||
if "from_" in kwargs:
|
||||
self._from_ = kwargs.pop("from_")
|
||||
|
||||
if "to" in kwargs:
|
||||
self._to = kwargs.pop("to")
|
||||
|
||||
if "number_of_steps" in kwargs:
|
||||
self._number_of_steps = kwargs.pop("number_of_steps")
|
||||
|
||||
if "hover" in kwargs:
|
||||
self._hover = kwargs.pop("hover")
|
||||
|
||||
if "command" in kwargs:
|
||||
self._command = kwargs.pop("command")
|
||||
|
||||
if "variable" in kwargs:
|
||||
if self._variable is not None:
|
||||
self._variable.trace_remove("write", self._variable_callback_name)
|
||||
|
||||
self._variable = kwargs.pop("variable")
|
||||
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self.set(self._variable.get(), from_variable_callback=True)
|
||||
else:
|
||||
self._variable = None
|
||||
|
||||
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 == "button_corner_radius":
|
||||
return self._button_corner_radius
|
||||
elif attribute_name == "border_width":
|
||||
return self._border_width
|
||||
elif attribute_name == "button_length":
|
||||
return self._button_length
|
||||
|
||||
elif attribute_name == "fg_color":
|
||||
return self._fg_color
|
||||
elif attribute_name == "border_color":
|
||||
return self._border_color
|
||||
elif attribute_name == "progress_color":
|
||||
return self._progress_color
|
||||
elif attribute_name == "button_color":
|
||||
return self._button_color
|
||||
elif attribute_name == "button_hover_color":
|
||||
return self._button_hover_color
|
||||
|
||||
elif attribute_name == "from_":
|
||||
return self._from_
|
||||
elif attribute_name == "to":
|
||||
return self._to
|
||||
elif attribute_name == "state":
|
||||
return self._state
|
||||
elif attribute_name == "number_of_steps":
|
||||
return self._number_of_steps
|
||||
elif attribute_name == "hover":
|
||||
return self._hover
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
elif attribute_name == "variable":
|
||||
return self._variable
|
||||
elif attribute_name == "orientation":
|
||||
return self._orientation
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def _clicked(self, event=None):
|
||||
if self._state == "normal":
|
||||
if self._orientation.lower() == "horizontal":
|
||||
self._value = (event.x / self._current_width) / self._widget_scaling
|
||||
else:
|
||||
self._value = 1 - (event.y / self._current_height) / self._widget_scaling
|
||||
|
||||
if self._value > 1:
|
||||
self._value = 1
|
||||
if self._value < 0:
|
||||
self._value = 0
|
||||
|
||||
self._output_value = self._round_to_step_size(self._from_ + (self._value * (self._to - self._from_)))
|
||||
self._value = (self._output_value - self._from_) / (self._to - self._from_)
|
||||
|
||||
self._draw(no_color_updates=False)
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
if self._command is not None:
|
||||
self._command(self._output_value)
|
||||
|
||||
def _on_enter(self, event=0):
|
||||
if self._hover is True and self._state == "normal":
|
||||
self._hover_state = True
|
||||
self._canvas.itemconfig("slider_parts",
|
||||
fill=self._apply_appearance_mode(self._button_hover_color),
|
||||
outline=self._apply_appearance_mode(self._button_hover_color))
|
||||
|
||||
def _on_leave(self, event=0):
|
||||
self._hover_state = False
|
||||
self._canvas.itemconfig("slider_parts",
|
||||
fill=self._apply_appearance_mode(self._button_color),
|
||||
outline=self._apply_appearance_mode(self._button_color))
|
||||
|
||||
def _round_to_step_size(self, value) -> float:
|
||||
if self._number_of_steps is not None:
|
||||
step_size = (self._to - self._from_) / self._number_of_steps
|
||||
value = self._to - (round((self._to - value) / step_size) * step_size)
|
||||
return value
|
||||
else:
|
||||
return value
|
||||
|
||||
def get(self) -> float:
|
||||
return self._output_value
|
||||
|
||||
def set(self, output_value, from_variable_callback=False):
|
||||
if self._from_ < self._to:
|
||||
if output_value > self._to:
|
||||
output_value = self._to
|
||||
elif output_value < self._from_:
|
||||
output_value = self._from_
|
||||
else:
|
||||
if output_value < self._to:
|
||||
output_value = self._to
|
||||
elif output_value > self._from_:
|
||||
output_value = self._from_
|
||||
|
||||
self._output_value = self._round_to_step_size(output_value)
|
||||
self._value = (self._output_value - self._from_) / (self._to - self._from_)
|
||||
|
||||
self._draw(no_color_updates=False)
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def _variable_callback(self, var_name, index, mode):
|
||||
if not self._variable_callback_blocked:
|
||||
self.set(self._variable.get(), from_variable_callback=True)
|
||||
|
||||
def bind(self, sequence=None, command=None, add=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.bind(sequence, command, add)
|
||||
|
||||
def unbind(self, sequence, funcid=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.unbind(sequence, funcid)
|
||||
|
||||
def focus(self):
|
||||
return self._canvas.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._canvas.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._canvas.focus_force()
|
451
customtkinter/windows/widgets/ctk_switch.py
Normal file
451
customtkinter/windows/widgets/ctk_switch.py
Normal file
@ -0,0 +1,451 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Union, Tuple, Callable
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
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
|
||||
|
||||
|
||||
class CTkSwitch(CTkBaseClass):
|
||||
"""
|
||||
Switch with rounded corners, border, label, command, variable support.
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 100,
|
||||
height: int = 24,
|
||||
switch_width: int = 36,
|
||||
switch_height: int = 18,
|
||||
corner_radius: Union[int, str] = "default_theme",
|
||||
border_width: Union[int, str] = "default_theme",
|
||||
button_length: Union[int, str] = "default_theme",
|
||||
|
||||
bg_color: Union[str, Tuple[str, str], None] = None,
|
||||
fg_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
border_color: Union[str, Tuple[str, str], None] = None,
|
||||
progress_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
button_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
button_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
text: str = "CTkSwitch",
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
textvariable: tkinter.Variable = None,
|
||||
onvalue: Union[int, str] = 1,
|
||||
offvalue: Union[int, str] = 0,
|
||||
variable: tkinter.Variable = None,
|
||||
hover: bool = True,
|
||||
command: Callable = None,
|
||||
state: str = tkinter.NORMAL,
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# dimensions
|
||||
self._switch_width = switch_width
|
||||
self._switch_height = switch_height
|
||||
|
||||
# color
|
||||
self._border_color = border_color
|
||||
self._fg_color = ThemeManager.theme["color"]["switch"] if fg_color == "default_theme" else fg_color
|
||||
self._progress_color = ThemeManager.theme["color"]["switch_progress"] if progress_color == "default_theme" else progress_color
|
||||
self._button_color = ThemeManager.theme["color"]["switch_button"] if button_color == "default_theme" else button_color
|
||||
self._button_hover_color = ThemeManager.theme["color"]["switch_button_hover"] if button_hover_color == "default_theme" else button_hover_color
|
||||
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
|
||||
|
||||
# text
|
||||
self._text = text
|
||||
self._text_label = None
|
||||
|
||||
# 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
|
||||
# self.button_corner_radius = ThemeManager.theme["shape"]["switch_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius
|
||||
self._border_width = ThemeManager.theme["shape"]["switch_border_width"] if border_width == "default_theme" else border_width
|
||||
self._button_length = ThemeManager.theme["shape"]["switch_button_length"] if button_length == "default_theme" else button_length
|
||||
self._hover_state: bool = False
|
||||
self._check_state: bool = False # True if switch is activated
|
||||
self._hover = hover
|
||||
self._state = state
|
||||
self._onvalue = onvalue
|
||||
self._offvalue = offvalue
|
||||
|
||||
# callback and control variables
|
||||
self._command = command
|
||||
self._variable: tkinter.Variable = variable
|
||||
self._variable_callback_blocked = False
|
||||
self._variable_callback_name = None
|
||||
self._textvariable = textvariable
|
||||
|
||||
# configure grid system (3x1)
|
||||
self.grid_columnconfigure(0, weight=0)
|
||||
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
|
||||
self.grid_columnconfigure(2, weight=1)
|
||||
|
||||
self._bg_canvas = CTkCanvas(master=self,
|
||||
highlightthickness=0,
|
||||
width=self._apply_widget_scaling(self._current_width),
|
||||
height=self._apply_widget_scaling(self._current_height))
|
||||
self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe")
|
||||
|
||||
self._canvas = CTkCanvas(master=self,
|
||||
highlightthickness=0,
|
||||
width=self._apply_widget_scaling(self._switch_width),
|
||||
height=self._apply_widget_scaling(self._switch_height))
|
||||
self._canvas.grid(row=0, column=0, sticky="")
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._canvas.bind("<Enter>", self._on_enter)
|
||||
self._canvas.bind("<Leave>", self._on_leave)
|
||||
self._canvas.bind("<Button-1>", self.toggle)
|
||||
|
||||
self._text_label = tkinter.Label(master=self,
|
||||
bd=0,
|
||||
padx=0,
|
||||
pady=0,
|
||||
text=self._text,
|
||||
justify=tkinter.LEFT,
|
||||
font=self._apply_font_scaling(self._font),
|
||||
textvariable=self._textvariable)
|
||||
self._text_label.grid(row=0, column=2, sticky="w")
|
||||
self._text_label["anchor"] = "w"
|
||||
|
||||
self._text_label.bind("<Enter>", self._on_enter)
|
||||
self._text_label.bind("<Leave>", self._on_leave)
|
||||
self._text_label.bind("<Button-1>", self.toggle)
|
||||
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self.c_heck_state = True if self._variable.get() == self._onvalue else False
|
||||
|
||||
self._draw() # initial draw
|
||||
self._set_cursor()
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6))
|
||||
self._text_label.configure(font=self._apply_font_scaling(self._font))
|
||||
|
||||
self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._switch_width),
|
||||
height=self._apply_widget_scaling(self._switch_height))
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
def _set_dimensions(self, width: int = None, height: int = None):
|
||||
super()._set_dimensions(width, height)
|
||||
|
||||
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):
|
||||
if self._cursor_manipulation_enabled:
|
||||
if self._state == tkinter.DISABLED:
|
||||
if sys.platform == "darwin":
|
||||
self._canvas.configure(cursor="arrow")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="arrow")
|
||||
elif sys.platform.startswith("win"):
|
||||
self._canvas.configure(cursor="arrow")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="arrow")
|
||||
|
||||
elif self._state == tkinter.NORMAL:
|
||||
if sys.platform == "darwin":
|
||||
self._canvas.configure(cursor="pointinghand")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="pointinghand")
|
||||
elif sys.platform.startswith("win"):
|
||||
self._canvas.configure(cursor="hand2")
|
||||
if self._text_label is not None:
|
||||
self._text_label.configure(cursor="hand2")
|
||||
|
||||
def _draw(self, no_color_updates=False):
|
||||
|
||||
if self._check_state is True:
|
||||
requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._switch_width),
|
||||
self._apply_widget_scaling(self._switch_height),
|
||||
self._apply_widget_scaling(self._corner_radius),
|
||||
self._apply_widget_scaling(self._border_width),
|
||||
self._apply_widget_scaling(self._button_length),
|
||||
self._apply_widget_scaling(self._corner_radius),
|
||||
1, "w")
|
||||
else:
|
||||
requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._switch_width),
|
||||
self._apply_widget_scaling(self._switch_height),
|
||||
self._apply_widget_scaling(self._corner_radius),
|
||||
self._apply_widget_scaling(self._border_width),
|
||||
self._apply_widget_scaling(self._button_length),
|
||||
self._apply_widget_scaling(self._corner_radius),
|
||||
0, "w")
|
||||
|
||||
if no_color_updates is False or requires_recoloring:
|
||||
self._bg_canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
if self._border_color is None:
|
||||
self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._bg_color),
|
||||
outline=self._apply_appearance_mode(self._bg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("border_parts", fill=self._apply_appearance_mode(self._border_color),
|
||||
outline=self._apply_appearance_mode(self._border_color))
|
||||
|
||||
self._canvas.itemconfig("inner_parts", fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
if self._progress_color is None:
|
||||
self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("progress_parts", fill=self._apply_appearance_mode(self._progress_color),
|
||||
outline=self._apply_appearance_mode(self._progress_color))
|
||||
|
||||
self._canvas.itemconfig("slider_parts", fill=self._apply_appearance_mode(self._button_color),
|
||||
outline=self._apply_appearance_mode(self._button_color))
|
||||
|
||||
if self._state == tkinter.DISABLED:
|
||||
self._text_label.configure(fg=(self._apply_appearance_mode(self._text_color_disabled)))
|
||||
else:
|
||||
self._text_label.configure(fg=self._apply_appearance_mode(self._text_color))
|
||||
|
||||
self._text_label.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "switch_width" in kwargs:
|
||||
self._switch_width = kwargs.pop("switch_width")
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._switch_width))
|
||||
require_redraw = True
|
||||
|
||||
if "switch_height" in kwargs:
|
||||
self._switch_height = kwargs.pop("switch_height")
|
||||
self._canvas.configure(height=self._apply_widget_scaling(self._switch_height))
|
||||
require_redraw = True
|
||||
|
||||
if "text" in kwargs:
|
||||
self._text = kwargs.pop("text")
|
||||
self._text_label.configure(text=self._text)
|
||||
|
||||
if "font" in kwargs:
|
||||
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")
|
||||
self._set_cursor()
|
||||
require_redraw = True
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
|
||||
if "progress_color" in kwargs:
|
||||
new_progress_color = kwargs.pop("progress_color")
|
||||
if new_progress_color is None:
|
||||
self._progress_color = self._fg_color
|
||||
else:
|
||||
self._progress_color = new_progress_color
|
||||
require_redraw = True
|
||||
|
||||
if "button_color" in kwargs:
|
||||
self._button_color = kwargs.pop("button_color")
|
||||
require_redraw = True
|
||||
|
||||
if "button_hover_color" in kwargs:
|
||||
self._button_hover_color = kwargs.pop("button_hover_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_color" in kwargs:
|
||||
self._border_color = kwargs.pop("border_color")
|
||||
require_redraw = True
|
||||
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
require_redraw = True
|
||||
|
||||
if "hover" in kwargs:
|
||||
self._hover = kwargs.pop("hover")
|
||||
|
||||
if "command" in kwargs:
|
||||
self._command = kwargs.pop("command")
|
||||
|
||||
if "textvariable" in kwargs:
|
||||
self._textvariable = kwargs.pop("textvariable")
|
||||
self._text_label.configure(textvariable=self._textvariable)
|
||||
|
||||
if "variable" in kwargs:
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable.trace_remove("write", self._variable_callback_name)
|
||||
|
||||
self._variable = kwargs.pop("variable")
|
||||
|
||||
if self._variable is not None and self._variable != "":
|
||||
self._variable_callback_name = self._variable.trace_add("write", self._variable_callback)
|
||||
self._check_state = True if self._variable.get() == self._onvalue else False
|
||||
require_redraw = True
|
||||
|
||||
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 == "button_length":
|
||||
return self._button_length
|
||||
elif attribute_name == "switch_width":
|
||||
return self._switch_width
|
||||
elif attribute_name == "switch_height":
|
||||
return self._switch_height
|
||||
|
||||
elif attribute_name == "fg_color":
|
||||
return self._fg_color
|
||||
elif attribute_name == "border_color":
|
||||
return self._border_color
|
||||
elif attribute_name == "progress_color":
|
||||
return self._progress_color
|
||||
elif attribute_name == "button_color":
|
||||
return self._button_color
|
||||
elif attribute_name == "button_hover_color":
|
||||
return self._button_hover_color
|
||||
elif attribute_name == "text_color":
|
||||
return self._text_color
|
||||
elif attribute_name == "text_color_disabled":
|
||||
return self._text_color_disabled
|
||||
|
||||
elif attribute_name == "text":
|
||||
return self._text
|
||||
elif attribute_name == "font":
|
||||
return self._font
|
||||
elif attribute_name == "textvariable":
|
||||
return self._textvariable
|
||||
elif attribute_name == "onvalue":
|
||||
return self._onvalue
|
||||
elif attribute_name == "offvalue":
|
||||
return self._offvalue
|
||||
elif attribute_name == "variable":
|
||||
return self._variable
|
||||
elif attribute_name == "hover":
|
||||
return self._hover
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
elif attribute_name == "state":
|
||||
return self._state
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def toggle(self, event=None):
|
||||
if self._state is not tkinter.DISABLED:
|
||||
if self._check_state is True:
|
||||
self._check_state = False
|
||||
else:
|
||||
self._check_state = True
|
||||
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
if self._variable is not None:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(self._onvalue if self._check_state is True else self._offvalue)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
if self._command is not None:
|
||||
self._command()
|
||||
|
||||
def select(self, from_variable_callback=False):
|
||||
if self._state is not tkinter.DISABLED or from_variable_callback:
|
||||
self._check_state = True
|
||||
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(self._onvalue)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def deselect(self, from_variable_callback=False):
|
||||
if self._state is not tkinter.DISABLED or from_variable_callback:
|
||||
self._check_state = False
|
||||
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
if self._variable is not None and not from_variable_callback:
|
||||
self._variable_callback_blocked = True
|
||||
self._variable.set(self._offvalue)
|
||||
self._variable_callback_blocked = False
|
||||
|
||||
def get(self) -> Union[int, str]:
|
||||
return self._onvalue if self._check_state is True else self._offvalue
|
||||
|
||||
def _on_enter(self, event=0):
|
||||
if self._hover is True and self._state == "normal":
|
||||
self._hover_state = True
|
||||
self._canvas.itemconfig("slider_parts",
|
||||
fill=self._apply_appearance_mode(self._button_hover_color),
|
||||
outline=self._apply_appearance_mode(self._button_hover_color))
|
||||
|
||||
def _on_leave(self, event=0):
|
||||
self._hover_state = False
|
||||
self._canvas.itemconfig("slider_parts",
|
||||
fill=self._apply_appearance_mode(self._button_color),
|
||||
outline=self._apply_appearance_mode(self._button_color))
|
||||
|
||||
def _variable_callback(self, var_name, index, mode):
|
||||
if not self._variable_callback_blocked:
|
||||
if self._variable.get() == self._onvalue:
|
||||
self.select(from_variable_callback=True)
|
||||
elif self._variable.get() == self._offvalue:
|
||||
self.deselect(from_variable_callback=True)
|
||||
|
||||
def bind(self, sequence=None, command=None, add=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.bind(sequence, command, add)
|
||||
|
||||
def unbind(self, sequence, funcid=None):
|
||||
""" called on the tkinter.Canvas """
|
||||
return self._canvas.unbind(sequence, funcid)
|
||||
|
||||
def focus(self):
|
||||
return self._text_label.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._text_label.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._text_label.focus_force()
|
370
customtkinter/windows/widgets/ctk_tabview.py
Normal file
370
customtkinter/windows/widgets/ctk_tabview.py
Normal file
@ -0,0 +1,370 @@
|
||||
from typing import Union, Tuple, Dict, List, Callable
|
||||
|
||||
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
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
from .core_rendering.draw_engine import DrawEngine
|
||||
|
||||
|
||||
class CTkTabview(CTkBaseClass):
|
||||
"""
|
||||
Tabview...
|
||||
For detailed information check out the documentation.
|
||||
"""
|
||||
|
||||
_top_spacing = 10 # px on top of the buttons
|
||||
_top_button_overhang = 8 # px
|
||||
_button_height = 26
|
||||
|
||||
def __init__(self,
|
||||
master: any = None,
|
||||
width: int = 300,
|
||||
height: int = 250,
|
||||
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",
|
||||
|
||||
segmented_button_fg_color: Union[str, Tuple[str, str], None] = "default_theme",
|
||||
segmented_button_selected_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
segmented_button_selected_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
segmented_button_unselected_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
segmented_button_unselected_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
text_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
text_color_disabled: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
command: Callable = None,
|
||||
state: str = "normal",
|
||||
**kwargs):
|
||||
|
||||
# transfer some functionality to CTkFrame
|
||||
super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs)
|
||||
|
||||
# color
|
||||
self._border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color
|
||||
|
||||
# determine fg_color of frame
|
||||
if fg_color == "default_theme":
|
||||
if isinstance(self.master, (CTkFrame, CTkTabview)):
|
||||
if self.master.cget("fg_color") == ThemeManager.theme["color"]["frame_low"]:
|
||||
self._fg_color = ThemeManager.theme["color"]["frame_high"]
|
||||
else:
|
||||
self._fg_color = ThemeManager.theme["color"]["frame_low"]
|
||||
else:
|
||||
self._fg_color = ThemeManager.theme["color"]["frame_low"]
|
||||
else:
|
||||
self._fg_color = fg_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
|
||||
|
||||
self._canvas = CTkCanvas(master=self,
|
||||
bg=self._apply_appearance_mode(self._bg_color),
|
||||
highlightthickness=0,
|
||||
width=self._apply_widget_scaling(self._current_width - self._top_spacing - self._top_button_overhang),
|
||||
height=self._apply_widget_scaling(self._current_height))
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._segmented_button = CTkSegmentedButton(self,
|
||||
values=[],
|
||||
height=self._button_height,
|
||||
fg_color=segmented_button_fg_color,
|
||||
selected_color=segmented_button_selected_color,
|
||||
selected_hover_color=segmented_button_selected_hover_color,
|
||||
unselected_color=segmented_button_unselected_color,
|
||||
unselected_hover_color=segmented_button_unselected_hover_color,
|
||||
text_color=text_color,
|
||||
text_color_disabled=text_color_disabled,
|
||||
corner_radius=corner_radius,
|
||||
border_width=self._apply_widget_scaling(3),
|
||||
command=self._segmented_button_callback,
|
||||
state=state)
|
||||
self._configure_segmented_button_background_corners()
|
||||
self._configure_grid()
|
||||
self._set_grid_canvas()
|
||||
|
||||
self._tab_dict: Dict[str, CTkFrame] = {}
|
||||
self._name_list: List[str] = [] # list of unique tab names in order of tabs
|
||||
self._current_name: str = ""
|
||||
self._command = command
|
||||
|
||||
self._draw()
|
||||
|
||||
def _segmented_button_callback(self, selected_name):
|
||||
self._current_name = selected_name
|
||||
self._grid_forget_all_tabs()
|
||||
self._set_grid_tab_by_name(self._current_name)
|
||||
|
||||
if self._command is not None:
|
||||
self._command()
|
||||
|
||||
def winfo_children(self) -> List[any]:
|
||||
"""
|
||||
winfo_children of CTkTabview without canvas and segmented button widgets,
|
||||
because it's not a child but part of the CTkTabview itself
|
||||
"""
|
||||
|
||||
child_widgets = super().winfo_children()
|
||||
try:
|
||||
child_widgets.remove(self._canvas)
|
||||
child_widgets.remove(self._segmented_button)
|
||||
return child_widgets
|
||||
except ValueError:
|
||||
return child_widgets
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height - self._top_spacing - self._top_button_overhang))
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
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._top_spacing - self._top_button_overhang))
|
||||
self._draw()
|
||||
|
||||
def _configure_segmented_button_background_corners(self):
|
||||
""" needs to be called for changes in fg_color, bg_color """
|
||||
|
||||
if self._fg_color is not None:
|
||||
self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._fg_color, self._fg_color))
|
||||
else:
|
||||
self._segmented_button.configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
|
||||
|
||||
def _configure_tab_background_corners_by_name(self, name: str):
|
||||
""" needs to be called for changes in fg_color, bg_color, border_width """
|
||||
|
||||
# if self._border_width == 0:
|
||||
# if self._fg_color is not None:
|
||||
# self._tab_dict[name].configure(background_corner_colors=(self._fg_color, self._fg_color, self._bg_color, self._bg_color))
|
||||
# else:
|
||||
# self._tab_dict[name].configure(background_corner_colors=(self._bg_color, self._bg_color, self._bg_color, self._bg_color))
|
||||
# else:
|
||||
# self._tab_dict[name].configure(background_corner_colors=None)
|
||||
|
||||
self._tab_dict[name].configure(background_corner_colors=None)
|
||||
|
||||
def _configure_grid(self):
|
||||
""" create 3 x 4 grid system """
|
||||
|
||||
self.grid_rowconfigure(0, weight=0, minsize=self._apply_widget_scaling(self._top_spacing))
|
||||
self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(self._top_button_overhang))
|
||||
self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._button_height - self._top_button_overhang))
|
||||
self.grid_rowconfigure(3, weight=1)
|
||||
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
def _set_grid_canvas(self):
|
||||
self._canvas.grid(row=2, rowspan=2, column=0, columnspan=1, sticky="nsew")
|
||||
|
||||
def _set_grid_segmented_button(self):
|
||||
""" needs to be called for changes in corner_radius """
|
||||
self._segmented_button.grid(row=1, rowspan=2, column=0, columnspan=1, padx=self._apply_widget_scaling(self._corner_radius), sticky="ns")
|
||||
|
||||
def _set_grid_tab_by_name(self, name: str):
|
||||
""" needs to be called for changes in corner_radius, border_width """
|
||||
self._tab_dict[name].grid(row=3, column=0, sticky="nsew",
|
||||
padx=self._apply_widget_scaling(max(self._corner_radius, self._border_width)),
|
||||
pady=self._apply_widget_scaling(max(self._corner_radius, self._border_width)))
|
||||
|
||||
def _grid_forget_all_tabs(self):
|
||||
for frame in self._tab_dict.values():
|
||||
frame.grid_forget()
|
||||
|
||||
def _create_tab(self) -> CTkFrame:
|
||||
new_tab = CTkFrame(self,
|
||||
fg_color=self._fg_color,
|
||||
border_width=0,
|
||||
corner_radius=self._corner_radius)
|
||||
return new_tab
|
||||
|
||||
def _draw(self, no_color_updates: bool = False):
|
||||
if not self._canvas.winfo_exists():
|
||||
return
|
||||
|
||||
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._top_spacing - self._top_button_overhang),
|
||||
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=self._apply_appearance_mode(self._bg_color),
|
||||
outline=self._apply_appearance_mode(self._bg_color))
|
||||
else:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._border_color),
|
||||
outline=self._apply_appearance_mode(self._border_color))
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
if "corner_radius" in kwargs:
|
||||
self._corner_radius = kwargs.pop("corner_radius")
|
||||
require_redraw = True
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
require_redraw = True
|
||||
|
||||
if "fg_color" in kwargs:
|
||||
self._fg_color = kwargs.pop("fg_color")
|
||||
require_redraw = True
|
||||
if "border_color" in kwargs:
|
||||
self._border_color = kwargs.pop("border_color")
|
||||
require_redraw = True
|
||||
if "segmented_button_fg_color" in kwargs:
|
||||
self._segmented_button.configure(fg_color=kwargs.pop("segmented_button_fg_color"))
|
||||
if "segmented_button_selected_color" in kwargs:
|
||||
self._segmented_button.configure(selected_color=kwargs.pop("segmented_button_selected_color"))
|
||||
if "segmented_button_selected_hover_color" in kwargs:
|
||||
self._segmented_button.configure(selected_hover_color=kwargs.pop("segmented_button_selected_hover_color"))
|
||||
if "segmented_button_unselected_color" in kwargs:
|
||||
self._segmented_button.configure(unselected_color=kwargs.pop("segmented_button_unselected_color"))
|
||||
if "segmented_button_unselected_hover_color" in kwargs:
|
||||
self._segmented_button.configure(unselected_hover_color=kwargs.pop("segmented_button_unselected_hover_color"))
|
||||
if "text_color" in kwargs:
|
||||
self._segmented_button.configure(text_color=kwargs.pop("text_color"))
|
||||
if "text_color_disabled" in kwargs:
|
||||
self._segmented_button.configure(text_color_disabled=kwargs.pop("text_color_disabled"))
|
||||
|
||||
if "command" in kwargs:
|
||||
self._command = kwargs.pop("command")
|
||||
if "state" in kwargs:
|
||||
self._segmented_button.configure(state=kwargs.pop("state"))
|
||||
|
||||
super().configure(require_redraw=require_redraw, **kwargs)
|
||||
|
||||
def cget(self, attribute_name: str):
|
||||
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 == "segmented_button_fg_color":
|
||||
return self._segmented_button.cget(attribute_name)
|
||||
elif attribute_name == "segmented_button_selected_color":
|
||||
return self._segmented_button.cget(attribute_name)
|
||||
elif attribute_name == "segmented_button_selected_hover_color":
|
||||
return self._segmented_button.cget(attribute_name)
|
||||
elif attribute_name == "segmented_button_unselected_color":
|
||||
return self._segmented_button.cget(attribute_name)
|
||||
elif attribute_name == "segmented_button_unselected_hover_color":
|
||||
return self._segmented_button.cget(attribute_name)
|
||||
elif attribute_name == "text_color":
|
||||
return self._segmented_button.cget(attribute_name)
|
||||
elif attribute_name == "text_color_disabled":
|
||||
return self._segmented_button.cget(attribute_name)
|
||||
|
||||
elif attribute_name == "command":
|
||||
return self._command
|
||||
elif attribute_name == "state":
|
||||
return self._segmented_button.cget(attribute_name)
|
||||
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def tab(self, name: str) -> CTkFrame:
|
||||
""" returns reference to the tab with given name """
|
||||
|
||||
if name in self._tab_dict:
|
||||
return self._tab_dict[name]
|
||||
else:
|
||||
raise ValueError(f"CTkTabview has no tab named '{name}'")
|
||||
|
||||
def insert(self, index: int, name: str) -> CTkFrame:
|
||||
""" creates new tab with given name at position index """
|
||||
|
||||
if name not in self._tab_dict:
|
||||
# if no tab exists, set grid for segmented button
|
||||
if len(self._tab_dict) == 0:
|
||||
self._set_grid_segmented_button()
|
||||
|
||||
self._name_list.insert(index, name)
|
||||
self._tab_dict[name] = self._create_tab()
|
||||
self._segmented_button.insert(index, name)
|
||||
self._configure_tab_background_corners_by_name(name)
|
||||
|
||||
# if created tab is only tab select this tab
|
||||
if len(self._tab_dict) == 1:
|
||||
self._current_name = name
|
||||
self._segmented_button.set(self._current_name)
|
||||
self._grid_forget_all_tabs()
|
||||
self._set_grid_tab_by_name(self._current_name)
|
||||
|
||||
return self._tab_dict[name]
|
||||
else:
|
||||
raise ValueError(f"CTkTabview already has tab named '{name}'")
|
||||
|
||||
def add(self, name: str) -> CTkFrame:
|
||||
""" appends new tab with given name """
|
||||
return self.insert(len(self._tab_dict), name)
|
||||
|
||||
def move(self, new_index: int, name: str):
|
||||
if 0 <= new_index < len(self._name_list):
|
||||
if name in self._tab_dict:
|
||||
self._segmented_button.move(new_index, name)
|
||||
else:
|
||||
raise ValueError(f"CTkTabview has no name '{name}'")
|
||||
else:
|
||||
raise ValueError(f"CTkTabview new_index {new_index} not in range of name list with len {len(self._name_list)}")
|
||||
|
||||
def delete(self, name: str):
|
||||
""" delete tab by name """
|
||||
|
||||
if name in self._tab_dict:
|
||||
self._name_list.remove(name)
|
||||
self._tab_dict[name].grid_forget()
|
||||
self._tab_dict.pop(name)
|
||||
self._segmented_button.delete(name)
|
||||
|
||||
# set current_name to '' and remove segmented button if no tab is left
|
||||
if len(self._name_list) == 0:
|
||||
self._current_name = ""
|
||||
self._segmented_button.grid_forget()
|
||||
|
||||
# if only one tab left, select this tab
|
||||
elif len(self._name_list) == 1:
|
||||
self._current_name = self._name_list[0]
|
||||
self._segmented_button.set(self._current_name)
|
||||
self._grid_forget_all_tabs()
|
||||
self._set_grid_tab_by_name(self._current_name)
|
||||
|
||||
# more tabs are left
|
||||
else:
|
||||
# if current_name is deleted tab, select first tab at position 0
|
||||
if self._current_name == name:
|
||||
self.set(self._name_list[0])
|
||||
else:
|
||||
raise ValueError(f"CTkTabview has no tab named '{name}'")
|
||||
|
||||
def set(self, name: str):
|
||||
""" select tab by name """
|
||||
|
||||
if name in self._tab_dict:
|
||||
self._current_name = name
|
||||
self._segmented_button.set(name)
|
||||
self._grid_forget_all_tabs()
|
||||
self._set_grid_tab_by_name(name)
|
||||
else:
|
||||
raise ValueError(f"CTkTabview has no tab named '{name}'")
|
||||
|
||||
def get(self) -> str:
|
||||
""" returns name of selected tab, returns empty string if no tab selected """
|
||||
return self._current_name
|
492
customtkinter/windows/widgets/ctk_textbox.py
Normal file
492
customtkinter/windows/widgets/ctk_textbox.py
Normal file
@ -0,0 +1,492 @@
|
||||
import tkinter
|
||||
from typing import Union, Tuple
|
||||
|
||||
from .core_rendering.ctk_canvas import CTkCanvas
|
||||
from .ctk_scrollbar import CTkScrollbar
|
||||
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
|
||||
|
||||
from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty
|
||||
|
||||
|
||||
class CTkTextbox(CTkBaseClass):
|
||||
"""
|
||||
Textbox with x and y scrollbars, rounded corners, and all text features of tkinter.Text widget.
|
||||
Scrollbars only appear when they are needed. Text is wrapped on line end by default,
|
||||
set wrap='none' to disable automatic line wrapping.
|
||||
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)
|
||||
"""
|
||||
|
||||
_scrollbar_update_time = 200 # interval in ms, to check if scrollbars are needed
|
||||
|
||||
# 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,
|
||||
master: any = None,
|
||||
width: int = 200,
|
||||
height: int = 200,
|
||||
corner_radius: Union[int, str] = "default_theme",
|
||||
border_width: Union[int, str] = "default_theme",
|
||||
border_spacing: int = 3,
|
||||
|
||||
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",
|
||||
scrollbar_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
scrollbar_hover_color: Union[str, Tuple[str, str]] = "default_theme",
|
||||
|
||||
font: Union[tuple, CTkFont] = "default_theme",
|
||||
activate_scrollbars: bool = True,
|
||||
**kwargs):
|
||||
|
||||
# transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass
|
||||
super().__init__(master=master, 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
|
||||
self._scrollbar_color = ThemeManager.theme["color"]["scrollbar_button"] if scrollbar_color == "default_theme" else scrollbar_color
|
||||
self._scrollbar_hover_color = ThemeManager.theme["color"]["scrollbar_button_hover"] if scrollbar_hover_color == "default_theme" else scrollbar_hover_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
|
||||
self._border_spacing = border_spacing
|
||||
|
||||
# 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, rowspan=2, columnspan=2, sticky="nsew")
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
self._draw_engine = DrawEngine(self._canvas)
|
||||
|
||||
self._textbox = tkinter.Text(self,
|
||||
fg=self._apply_appearance_mode(self._text_color),
|
||||
width=0,
|
||||
height=0,
|
||||
font=self._apply_font_scaling(self._font),
|
||||
highlightthickness=0,
|
||||
relief="flat",
|
||||
insertbackground=self._apply_appearance_mode(self._text_color),
|
||||
bg=self._apply_appearance_mode(self._fg_color),
|
||||
**pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes))
|
||||
|
||||
check_kwargs_empty(kwargs, raise_error=True)
|
||||
|
||||
# scrollbars
|
||||
self._scrollbars_activated = activate_scrollbars
|
||||
self._hide_x_scrollbar = True
|
||||
self._hide_y_scrollbar = True
|
||||
|
||||
self._y_scrollbar = CTkScrollbar(self,
|
||||
width=8,
|
||||
height=0,
|
||||
border_spacing=0,
|
||||
fg_color=self._fg_color,
|
||||
scrollbar_color=self._scrollbar_color,
|
||||
scrollbar_hover_color=self._scrollbar_hover_color,
|
||||
orientation="vertical",
|
||||
command=self._textbox.yview)
|
||||
self._textbox.configure(yscrollcommand=self._y_scrollbar.set)
|
||||
|
||||
self._x_scrollbar = CTkScrollbar(self,
|
||||
height=8,
|
||||
width=0,
|
||||
border_spacing=0,
|
||||
fg_color=self._fg_color,
|
||||
scrollbar_color=self._scrollbar_color,
|
||||
scrollbar_hover_color=self._scrollbar_hover_color,
|
||||
orientation="horizontal",
|
||||
command=self._textbox.xview)
|
||||
self._textbox.configure(xscrollcommand=self._x_scrollbar.set)
|
||||
|
||||
self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
|
||||
|
||||
self.after(50, self._check_if_scrollbars_needed)
|
||||
self._draw()
|
||||
|
||||
def _create_grid_for_text_and_scrollbars(self, re_grid_textbox=False, re_grid_x_scrollbar=False, re_grid_y_scrollbar=False):
|
||||
|
||||
# configure 2x2 grid
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)))
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)))
|
||||
|
||||
if re_grid_textbox:
|
||||
self._textbox.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew",
|
||||
padx=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0),
|
||||
pady=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0))
|
||||
|
||||
if re_grid_x_scrollbar:
|
||||
if not self._hide_x_scrollbar and self._scrollbars_activated:
|
||||
self._x_scrollbar.grid(row=1, column=0, rowspan=1, columnspan=1, sticky="ewn",
|
||||
pady=(3, self._border_spacing + self._border_width),
|
||||
padx=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling
|
||||
else:
|
||||
self._x_scrollbar.grid_forget()
|
||||
|
||||
if re_grid_y_scrollbar:
|
||||
if not self._hide_y_scrollbar and self._scrollbars_activated:
|
||||
self._y_scrollbar.grid(row=0, column=1, rowspan=1, columnspan=1, sticky="nsw",
|
||||
padx=(3, self._border_spacing + self._border_width),
|
||||
pady=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling
|
||||
else:
|
||||
self._y_scrollbar.grid_forget()
|
||||
|
||||
def _check_if_scrollbars_needed(self, event=None, continue_loop: bool = True):
|
||||
""" Method hides or places the scrollbars if they are needed on key release event of tkinter.text widget """
|
||||
|
||||
if self._scrollbars_activated:
|
||||
if self._textbox.xview() != (0.0, 1.0) and not self._x_scrollbar.winfo_ismapped(): # x scrollbar needed
|
||||
self._hide_x_scrollbar = False
|
||||
self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True)
|
||||
elif self._textbox.xview() == (0.0, 1.0) and self._x_scrollbar.winfo_ismapped(): # x scrollbar not needed
|
||||
self._hide_x_scrollbar = True
|
||||
self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True)
|
||||
|
||||
if self._textbox.yview() != (0.0, 1.0) and not self._y_scrollbar.winfo_ismapped(): # y scrollbar needed
|
||||
self._hide_y_scrollbar = False
|
||||
self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
|
||||
elif self._textbox.yview() == (0.0, 1.0) and self._y_scrollbar.winfo_ismapped(): # y scrollbar not needed
|
||||
self._hide_y_scrollbar = True
|
||||
self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
|
||||
else:
|
||||
self._hide_x_scrollbar = False
|
||||
self._hide_x_scrollbar = False
|
||||
self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True)
|
||||
|
||||
if self._textbox.winfo_exists() and continue_loop is True:
|
||||
self.after(self._scrollbar_update_time, lambda: self._check_if_scrollbars_needed(continue_loop=True))
|
||||
|
||||
def _set_scaling(self, *args, **kwargs):
|
||||
super()._set_scaling(*args, **kwargs)
|
||||
|
||||
self._textbox.configure(font=self._apply_font_scaling(self._font))
|
||||
self._canvas.configure(width=self._apply_widget_scaling(self._desired_width),
|
||||
height=self._apply_widget_scaling(self._desired_height))
|
||||
self._create_grid_for_text_and_scrollbars(re_grid_textbox=False, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
|
||||
self._draw(no_color_updates=True)
|
||||
|
||||
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 _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():
|
||||
return
|
||||
|
||||
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=self._apply_appearance_mode(self._bg_color),
|
||||
outline=self._apply_appearance_mode(self._bg_color))
|
||||
self._textbox.configure(fg=self._apply_appearance_mode(self._text_color),
|
||||
bg=self._apply_appearance_mode(self._bg_color),
|
||||
insertbackground=self._apply_appearance_mode(self._text_color))
|
||||
self._x_scrollbar.configure(fg_color=self._bg_color, scrollbar_color=self._scrollbar_color,
|
||||
scrollbar_hover_color=self._scrollbar_hover_color)
|
||||
self._y_scrollbar.configure(fg_color=self._bg_color, scrollbar_color=self._scrollbar_color,
|
||||
scrollbar_hover_color=self._scrollbar_hover_color)
|
||||
else:
|
||||
self._canvas.itemconfig("inner_parts",
|
||||
fill=self._apply_appearance_mode(self._fg_color),
|
||||
outline=self._apply_appearance_mode(self._fg_color))
|
||||
self._textbox.configure(fg=self._apply_appearance_mode(self._text_color),
|
||||
bg=self._apply_appearance_mode(self._fg_color),
|
||||
insertbackground=self._apply_appearance_mode(self._text_color))
|
||||
self._x_scrollbar.configure(fg_color=self._fg_color, scrollbar_color=self._scrollbar_color,
|
||||
scrollbar_hover_color=self._scrollbar_hover_color)
|
||||
self._y_scrollbar.configure(fg_color=self._fg_color, scrollbar_color=self._scrollbar_color,
|
||||
scrollbar_hover_color=self._scrollbar_hover_color)
|
||||
|
||||
self._canvas.itemconfig("border_parts",
|
||||
fill=self._apply_appearance_mode(self._border_color),
|
||||
outline=self._apply_appearance_mode(self._border_color))
|
||||
self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color))
|
||||
|
||||
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._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
|
||||
require_redraw = True
|
||||
|
||||
if "border_width" in kwargs:
|
||||
self._border_width = kwargs.pop("border_width")
|
||||
self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
|
||||
require_redraw = True
|
||||
|
||||
if "border_spacing" in kwargs:
|
||||
self._border_spacing = kwargs.pop("border_spacing")
|
||||
self._create_grid_for_text_and_scrollbars(re_grid_textbox=True, re_grid_x_scrollbar=True, re_grid_y_scrollbar=True)
|
||||
require_redraw = True
|
||||
|
||||
if "font" in kwargs:
|
||||
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)
|
||||
|
||||
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 == "border_spacing":
|
||||
return self._border_spacing
|
||||
|
||||
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 """
|
||||
|
||||
# if sequence is <KeyRelease>, allow only to add the binding to keep the _textbox_modified_event() being called
|
||||
if sequence == "<KeyRelease>":
|
||||
return self._textbox.bind(sequence, command, add="+")
|
||||
else:
|
||||
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 focus(self):
|
||||
return self._textbox.focus()
|
||||
|
||||
def focus_set(self):
|
||||
return self._textbox.focus_set()
|
||||
|
||||
def focus_force(self):
|
||||
return self._textbox.focus_force()
|
||||
|
||||
def insert(self, index, text, tags=None):
|
||||
self._check_if_scrollbars_needed()
|
||||
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 delete(self, index1, index2=None):
|
||||
return self._textbox.delete(index1, 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):
|
||||
self._check_if_scrollbars_needed()
|
||||
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):
|
||||
self._check_if_scrollbars_needed()
|
||||
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)
|
0
customtkinter/windows/widgets/font/__init__.py
Normal file
0
customtkinter/windows/widgets/font/__init__.py
Normal file
80
customtkinter/windows/widgets/font/ctk_font.py
Normal file
80
customtkinter/windows/widgets/font/ctk_font.py
Normal file
@ -0,0 +1,80 @@
|
||||
from tkinter.font import Font
|
||||
import copy
|
||||
from typing import List, Callable, Tuple
|
||||
|
||||
from ..theme.theme_manager import ThemeManager
|
||||
|
||||
|
||||
class CTkFont(Font):
|
||||
"""
|
||||
Font object with size in pixel, independent of scaling.
|
||||
To get scaled tuple representation use create_scaled_tuple() method.
|
||||
|
||||
family The font family name as a string.
|
||||
size The font height as an integer in pixel.
|
||||
weight 'bold' for boldface, 'normal' for regular weight.
|
||||
slant 'italic' for italic, 'roman' for unslanted.
|
||||
underline 1 for underlined text, 0 for normal.
|
||||
overstrike 1 for overstruck text, 0 for normal.
|
||||
|
||||
Tkinter Font: https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/fonts.html
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
family: str = "default_theme",
|
||||
size: int = "default_theme",
|
||||
weight: str = "normal",
|
||||
slant: str = "roman",
|
||||
underline: bool = False,
|
||||
overstrike: bool = False):
|
||||
|
||||
self._size_configure_callback_list: List[Callable] = []
|
||||
|
||||
self._family = family
|
||||
self._size = ThemeManager.theme["text"]["size"] if size == "default_theme" else size
|
||||
self._tuple_style_string = f"{weight} {slant} {'underline' if underline else ''} {'overstrike' if overstrike else ''}"
|
||||
|
||||
super().__init__(family=ThemeManager.theme["text"]["font"] if family == "default_theme" else family,
|
||||
size=-abs(self._size),
|
||||
weight=weight,
|
||||
slant=slant,
|
||||
underline=underline,
|
||||
overstrike=overstrike)
|
||||
|
||||
def add_size_configure_callback(self, callback: Callable):
|
||||
""" add function, that gets called when font got configured """
|
||||
self._size_configure_callback_list.append(callback)
|
||||
|
||||
def remove_size_configure_callback(self, callback: Callable):
|
||||
""" remove function, that gets called when font got configured """
|
||||
self._size_configure_callback_list.remove(callback)
|
||||
|
||||
def create_scaled_tuple(self, font_scaling: float) -> Tuple[str, int, str]:
|
||||
""" return scaled tuple representation of font in the form (family: str, size: int, style: str)"""
|
||||
return self._family, round(self._size * font_scaling), self._tuple_style_string
|
||||
|
||||
def config(self, *args, **kwargs):
|
||||
raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.")
|
||||
|
||||
def configure(self, **kwargs):
|
||||
if "size" in kwargs:
|
||||
self._size = kwargs.pop("size")
|
||||
super().configure(size=-abs(self._size))
|
||||
|
||||
super().configure(**kwargs)
|
||||
|
||||
# update style string for create_scaled_tuple() method
|
||||
self._tuple_style_string = f"{super().cget('weight')} {super().cget('slant')} {'underline' if super().cget('underline') else ''} {'overstrike' if super().cget('overstrike') else ''}"
|
||||
|
||||
# call all functions registered with add_size_configure_callback()
|
||||
for callback in self._size_configure_callback_list:
|
||||
callback()
|
||||
|
||||
def cget(self, attribute_name: str) -> any:
|
||||
if attribute_name == "size":
|
||||
return self._size
|
||||
else:
|
||||
return super().cget(attribute_name)
|
||||
|
||||
def copy(self) -> "CTkFont":
|
||||
return copy.deepcopy(self)
|
66
customtkinter/windows/widgets/font/font_manager.py
Normal file
66
customtkinter/windows/widgets/font/font_manager.py
Normal file
@ -0,0 +1,66 @@
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
from typing import Union
|
||||
|
||||
|
||||
class FontManager:
|
||||
|
||||
linux_font_path = "~/.fonts/"
|
||||
|
||||
@classmethod
|
||||
def init_font_manager(cls):
|
||||
# Linux
|
||||
if sys.platform.startswith("linux"):
|
||||
try:
|
||||
if not os.path.isdir(os.path.expanduser(cls.linux_font_path)):
|
||||
os.mkdir(os.path.expanduser(cls.linux_font_path))
|
||||
return True
|
||||
except Exception as err:
|
||||
sys.stderr.write("FontManager error: " + str(err) + "\n")
|
||||
return False
|
||||
|
||||
# other platforms
|
||||
else:
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def windows_load_font(cls, font_path: Union[str, bytes], private: bool = True, enumerable: bool = False) -> bool:
|
||||
""" Function taken from: https://stackoverflow.com/questions/11993290/truly-custom-font-in-tkinter/30631309#30631309 """
|
||||
|
||||
from ctypes import windll, byref, create_unicode_buffer, create_string_buffer
|
||||
|
||||
FR_PRIVATE = 0x10
|
||||
FR_NOT_ENUM = 0x20
|
||||
|
||||
if isinstance(font_path, bytes):
|
||||
path_buffer = create_string_buffer(font_path)
|
||||
add_font_resource_ex = windll.gdi32.AddFontResourceExA
|
||||
elif isinstance(font_path, str):
|
||||
path_buffer = create_unicode_buffer(font_path)
|
||||
add_font_resource_ex = windll.gdi32.AddFontResourceExW
|
||||
else:
|
||||
raise TypeError('font_path must be of type bytes or str')
|
||||
|
||||
flags = (FR_PRIVATE if private else 0) | (FR_NOT_ENUM if not enumerable else 0)
|
||||
num_fonts_added = add_font_resource_ex(byref(path_buffer), flags, 0)
|
||||
return bool(min(num_fonts_added, 1))
|
||||
|
||||
@classmethod
|
||||
def load_font(cls, font_path: str) -> bool:
|
||||
# Windows
|
||||
if sys.platform.startswith("win"):
|
||||
return cls.windows_load_font(font_path, private=True, enumerable=False)
|
||||
|
||||
# Linux
|
||||
elif sys.platform.startswith("linux"):
|
||||
try:
|
||||
shutil.copy(font_path, os.path.expanduser(cls.linux_font_path))
|
||||
return True
|
||||
except Exception as err:
|
||||
sys.stderr.write("FontManager error: " + str(err) + "\n")
|
||||
return False
|
||||
|
||||
# macOS and others
|
||||
else:
|
||||
return False
|
0
customtkinter/windows/widgets/image/__init__.py
Normal file
0
customtkinter/windows/widgets/image/__init__.py
Normal file
116
customtkinter/windows/widgets/image/ctk_image.py
Normal file
116
customtkinter/windows/widgets/image/ctk_image.py
Normal file
@ -0,0 +1,116 @@
|
||||
from typing import Tuple, Dict, Callable, List
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class CTkImage:
|
||||
"""
|
||||
Class to store one or two PIl.Image.Image objects and display size independent of scaling:
|
||||
|
||||
light_image: PIL.Image.Image for light mode
|
||||
dark_image: PIL.Image.Image for dark mode
|
||||
size: tuple (<width>, <height>) with display size for both images
|
||||
|
||||
One of the two images can be None and will be replaced by the other image.
|
||||
"""
|
||||
|
||||
_checked_PIL_import = False
|
||||
|
||||
def __init__(self, light_image: Image.Image = None, dark_image: Image.Image = None, size: Tuple[int, int] = None):
|
||||
if not self._checked_PIL_import:
|
||||
self._check_pil_import()
|
||||
|
||||
self._light_image = light_image
|
||||
self._dark_image = dark_image
|
||||
self._check_images()
|
||||
self._size = size
|
||||
|
||||
self._configure_callback_list: List[Callable] = []
|
||||
self._scaled_light_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {}
|
||||
self._scaled_dark_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {}
|
||||
|
||||
@classmethod
|
||||
def _check_pil_import(cls):
|
||||
if "Image" not in dir() or "ImageTk" not in dir():
|
||||
raise ImportError("CTkImage: Couldn't import PIL.Image or PIL.ImageTk. PIL must be installed.")
|
||||
|
||||
def add_configure_callback(self, callback: Callable):
|
||||
""" add function, that gets called when image got configured """
|
||||
self._configure_callback_list.append(callback)
|
||||
|
||||
def remove_configure_callback(self, callback: Callable):
|
||||
""" remove function, that gets called when image got configured """
|
||||
self._configure_callback_list.remove(callback)
|
||||
|
||||
def configure(self, **kwargs):
|
||||
if "light_image" in kwargs:
|
||||
self._light_image = kwargs.pop("light_image")
|
||||
self._scaled_light_photo_images = {}
|
||||
self._check_images()
|
||||
if "dark_image" in kwargs:
|
||||
self._dark_image = kwargs.pop("dark_image")
|
||||
self._scaled_dark_photo_images = {}
|
||||
self._check_images()
|
||||
if "size" in kwargs:
|
||||
self._size = kwargs.pop("size")
|
||||
|
||||
# call all functions registered with add_configure_callback()
|
||||
for callback in self._configure_callback_list:
|
||||
callback()
|
||||
|
||||
def cget(self, attribute_name: str) -> any:
|
||||
if attribute_name == "light_image":
|
||||
return self._light_image
|
||||
if attribute_name == "dark_image":
|
||||
return self._dark_image
|
||||
if attribute_name == "size":
|
||||
return self._size
|
||||
|
||||
def _check_images(self):
|
||||
# check types
|
||||
if self._light_image is not None and not isinstance(self._light_image, Image.Image):
|
||||
raise ValueError(f"CTkImage: light_image must be instance if PIL.Image.Image, not {type(self._light_image)}")
|
||||
if self._dark_image is not None and not isinstance(self._dark_image, Image.Image):
|
||||
raise ValueError(f"CTkImage: dark_image must be instance if PIL.Image.Image, not {type(self._dark_image)}")
|
||||
|
||||
# check values
|
||||
if self._light_image is None and self._dark_image is None:
|
||||
raise ValueError("CTkImage: No image given, light_image is None and dark_image is None.")
|
||||
|
||||
# check sizes
|
||||
if self._light_image is not None and self._dark_image is not None and self._light_image.size != self._dark_image.size:
|
||||
raise ValueError(f"CTkImage: light_image size {self._light_image.size} must be the same as dark_image size {self._dark_image.size}.")
|
||||
|
||||
def _get_scaled_size(self, widget_scaling: float) -> Tuple[int, int]:
|
||||
return round(self._size[0] * widget_scaling), round(self._size[0] * widget_scaling)
|
||||
|
||||
def _get_scaled_light_photo_image(self, scaled_size: Tuple[int, int]) -> ImageTk.PhotoImage:
|
||||
if scaled_size in self._scaled_light_photo_images:
|
||||
return self._scaled_light_photo_images[scaled_size]
|
||||
else:
|
||||
self._scaled_light_photo_images[scaled_size] = ImageTk.PhotoImage(self._light_image.resize(scaled_size))
|
||||
return self._scaled_light_photo_images[scaled_size]
|
||||
|
||||
def _get_scaled_dark_photo_image(self, scaled_size: Tuple[int, int]) -> ImageTk.PhotoImage:
|
||||
if scaled_size in self._scaled_dark_photo_images:
|
||||
return self._scaled_dark_photo_images[scaled_size]
|
||||
else:
|
||||
self._scaled_dark_photo_images[scaled_size] = ImageTk.PhotoImage(self._dark_image.resize(scaled_size))
|
||||
return self._scaled_dark_photo_images[scaled_size]
|
||||
|
||||
def create_scaled_photo_image(self, widget_scaling: float, appearance_mode: int) -> ImageTk.PhotoImage:
|
||||
scaled_size = self._get_scaled_size(widget_scaling)
|
||||
|
||||
if appearance_mode == 0 and self._light_image is not None:
|
||||
return self._get_scaled_light_photo_image(scaled_size)
|
||||
elif appearance_mode == 0 and self._light_image is None:
|
||||
return self._get_scaled_dark_photo_image(scaled_size)
|
||||
|
||||
elif appearance_mode == 1 and self._dark_image is not None:
|
||||
return self._get_scaled_dark_photo_image(scaled_size)
|
||||
elif appearance_mode == 1 and self._dark_image is None:
|
||||
return self._get_scaled_light_photo_image(scaled_size)
|
||||
|
||||
|
0
customtkinter/windows/widgets/scaling/__init__.py
Normal file
0
customtkinter/windows/widgets/scaling/__init__.py
Normal file
133
customtkinter/windows/widgets/scaling/scaling_base_class.py
Normal file
133
customtkinter/windows/widgets/scaling/scaling_base_class.py
Normal 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
|
181
customtkinter/windows/widgets/scaling/scaling_tracker.py
Normal file
181
customtkinter/windows/widgets/scaling/scaling_tracker.py
Normal file
@ -0,0 +1,181 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class ScalingTracker:
|
||||
deactivate_automatic_dpi_awareness = False
|
||||
|
||||
window_widgets_dict = {} # contains window objects as keys with list of widget callbacks as elements
|
||||
window_dpi_scaling_dict = {} # contains window objects as keys and corresponding scaling factors
|
||||
|
||||
widget_scaling = 1 # user values which multiply to detected window scaling factor
|
||||
window_scaling = 1
|
||||
|
||||
update_loop_running = False
|
||||
update_loop_interval = 600 # ms
|
||||
loop_pause_after_new_scaling = 1000 # ms
|
||||
|
||||
@classmethod
|
||||
def get_widget_scaling(cls, widget) -> float:
|
||||
window_root = cls.get_window_root_of_widget(widget)
|
||||
return cls.window_dpi_scaling_dict[window_root] * cls.widget_scaling
|
||||
|
||||
@classmethod
|
||||
def get_window_scaling(cls, window) -> float:
|
||||
window_root = cls.get_window_root_of_widget(window)
|
||||
return cls.window_dpi_scaling_dict[window_root] * cls.window_scaling
|
||||
|
||||
@classmethod
|
||||
def set_widget_scaling(cls, widget_scaling_factor: float):
|
||||
cls.widget_scaling = max(widget_scaling_factor, 0.4)
|
||||
cls.update_scaling_callbacks_all()
|
||||
|
||||
@classmethod
|
||||
def set_window_scaling(cls, window_scaling_factor: float):
|
||||
cls.window_scaling = max(window_scaling_factor, 0.4)
|
||||
cls.update_scaling_callbacks_all()
|
||||
|
||||
@classmethod
|
||||
def get_window_root_of_widget(cls, widget):
|
||||
current_widget = widget
|
||||
|
||||
while isinstance(current_widget, tkinter.Tk) is False and\
|
||||
isinstance(current_widget, tkinter.Toplevel) is False:
|
||||
current_widget = current_widget.master
|
||||
|
||||
return current_widget
|
||||
|
||||
@classmethod
|
||||
def update_scaling_callbacks_all(cls):
|
||||
for window, callback_list in cls.window_widgets_dict.items():
|
||||
for set_scaling_callback in callback_list:
|
||||
if not cls.deactivate_automatic_dpi_awareness:
|
||||
set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling,
|
||||
cls.window_dpi_scaling_dict[window] * cls.window_scaling)
|
||||
else:
|
||||
set_scaling_callback(cls.widget_scaling,
|
||||
cls.window_scaling)
|
||||
|
||||
@classmethod
|
||||
def update_scaling_callbacks_for_window(cls, window):
|
||||
for set_scaling_callback in cls.window_widgets_dict[window]:
|
||||
if not cls.deactivate_automatic_dpi_awareness:
|
||||
set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling,
|
||||
cls.window_dpi_scaling_dict[window] * cls.window_scaling)
|
||||
else:
|
||||
set_scaling_callback(cls.widget_scaling,
|
||||
cls.window_scaling)
|
||||
|
||||
@classmethod
|
||||
def add_widget(cls, widget_callback: Callable, widget):
|
||||
window_root = cls.get_window_root_of_widget(widget)
|
||||
|
||||
if window_root not in cls.window_widgets_dict:
|
||||
cls.window_widgets_dict[window_root] = [widget_callback]
|
||||
else:
|
||||
cls.window_widgets_dict[window_root].append(widget_callback)
|
||||
|
||||
if window_root not in cls.window_dpi_scaling_dict:
|
||||
cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_scaling(window_root)
|
||||
|
||||
if not cls.update_loop_running:
|
||||
window_root.after(100, cls.check_dpi_scaling)
|
||||
cls.update_loop_running = True
|
||||
|
||||
@classmethod
|
||||
def remove_widget(cls, widget_callback, widget):
|
||||
window_root = cls.get_window_root_of_widget(widget)
|
||||
try:
|
||||
cls.window_widgets_dict[window_root].remove(widget_callback)
|
||||
except:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def remove_window(cls, window_callback, window):
|
||||
try:
|
||||
del cls.window_widgets_dict[window]
|
||||
except:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def add_window(cls, window_callback, window):
|
||||
if window not in cls.window_widgets_dict:
|
||||
cls.window_widgets_dict[window] = [window_callback]
|
||||
else:
|
||||
cls.window_widgets_dict[window].append(window_callback)
|
||||
|
||||
if window not in cls.window_dpi_scaling_dict:
|
||||
cls.window_dpi_scaling_dict[window] = cls.get_window_dpi_scaling(window)
|
||||
|
||||
@classmethod
|
||||
def activate_high_dpi_awareness(cls):
|
||||
""" make process DPI aware, customtkinter elements will get scaled automatically,
|
||||
only gets activated when CTk object is created """
|
||||
|
||||
if not cls.deactivate_automatic_dpi_awareness:
|
||||
if sys.platform == "darwin":
|
||||
pass # high DPI scaling works automatically on macOS
|
||||
|
||||
elif sys.platform.startswith("win"):
|
||||
from ctypes import windll
|
||||
windll.shcore.SetProcessDpiAwareness(2)
|
||||
# Microsoft Docs: https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness
|
||||
else:
|
||||
pass # DPI awareness on Linux not implemented
|
||||
|
||||
@classmethod
|
||||
def get_window_dpi_scaling(cls, window) -> float:
|
||||
if not cls.deactivate_automatic_dpi_awareness:
|
||||
if sys.platform == "darwin":
|
||||
return 1 # scaling works automatically on macOS
|
||||
|
||||
elif sys.platform.startswith("win"):
|
||||
from ctypes import windll, pointer, wintypes
|
||||
|
||||
DPI100pc = 96 # DPI 96 is 100% scaling
|
||||
DPI_type = 0 # MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2
|
||||
window_hwnd = wintypes.HWND(window.winfo_id())
|
||||
monitor_handle = windll.user32.MonitorFromWindow(window_hwnd, wintypes.DWORD(2)) # MONITOR_DEFAULTTONEAREST = 2
|
||||
x_dpi, y_dpi = wintypes.UINT(), wintypes.UINT()
|
||||
windll.shcore.GetDpiForMonitor(monitor_handle, DPI_type, pointer(x_dpi), pointer(y_dpi))
|
||||
return (x_dpi.value + y_dpi.value) / (2 * DPI100pc)
|
||||
|
||||
else:
|
||||
return 1 # DPI awareness on Linux not implemented
|
||||
else:
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def check_dpi_scaling(cls):
|
||||
new_scaling_detected = False
|
||||
|
||||
# check for every window if scaling value changed
|
||||
for window in cls.window_widgets_dict:
|
||||
if window.winfo_exists() and not window.state() == "iconic":
|
||||
current_dpi_scaling_value = cls.get_window_dpi_scaling(window)
|
||||
if current_dpi_scaling_value != cls.window_dpi_scaling_dict[window]:
|
||||
cls.window_dpi_scaling_dict[window] = current_dpi_scaling_value
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
window.attributes("-alpha", 0.15)
|
||||
|
||||
cls.update_scaling_callbacks_for_window(window)
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
window.after(200, lambda: window.attributes("-alpha", 1))
|
||||
|
||||
new_scaling_detected = True
|
||||
|
||||
# find an existing tkinter object for the next call of .after()
|
||||
for app in cls.window_widgets_dict.keys():
|
||||
try:
|
||||
if new_scaling_detected:
|
||||
app.after(cls.loop_pause_after_new_scaling, cls.check_dpi_scaling)
|
||||
else:
|
||||
app.after(cls.update_loop_interval, cls.check_dpi_scaling)
|
||||
return
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
cls.update_loop_running = False
|
0
customtkinter/windows/widgets/theme/__init__.py
Normal file
0
customtkinter/windows/widgets/theme/__init__.py
Normal file
27
customtkinter/windows/widgets/theme/theme_manager.py
Normal file
27
customtkinter/windows/widgets/theme/theme_manager.py
Normal file
@ -0,0 +1,27 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
class ThemeManager:
|
||||
|
||||
theme = {} # contains all the theme data
|
||||
built_in_themes = ["blue", "green", "dark-blue", "sweetkind"]
|
||||
|
||||
@classmethod
|
||||
def load_theme(cls, theme_name_or_path: str):
|
||||
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:
|
||||
cls.theme = json.load(f)
|
||||
else:
|
||||
with open(theme_name_or_path, "r") as f:
|
||||
cls.theme = json.load(f)
|
||||
|
||||
if sys.platform == "darwin":
|
||||
cls.theme["text"] = cls.theme["text"]["macOS"]
|
||||
elif sys.platform.startswith("win"):
|
||||
cls.theme["text"] = cls.theme["text"]["Windows"]
|
||||
else:
|
||||
cls.theme["text"] = cls.theme["text"]["Linux"]
|
Reference in New Issue
Block a user