BARRAUX Arthur
2 years ago
94 changed files with 10180 additions and 12 deletions
@ -0,0 +1,81 @@ |
|||||
|
__version__ = "5.0.5" |
||||
|
|
||||
|
import os |
||||
|
import sys |
||||
|
from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar |
||||
|
from tkinter.constants import * |
||||
|
import tkinter.filedialog as filedialog |
||||
|
|
||||
|
# import manager classes |
||||
|
from .windows.widgets.appearance_mode import AppearanceModeTracker |
||||
|
from .windows.widgets.font import FontManager |
||||
|
from .windows.widgets.scaling import ScalingTracker |
||||
|
from .windows.widgets.theme import ThemeManager |
||||
|
from .windows.widgets.core_rendering import DrawEngine |
||||
|
|
||||
|
# import base widgets |
||||
|
from .windows.widgets.core_rendering import CTkCanvas |
||||
|
from .windows.widgets.core_widget_classes import CTkBaseClass |
||||
|
|
||||
|
# import widgets |
||||
|
from .windows.widgets import CTkButton |
||||
|
from .windows.widgets import CTkCheckBox |
||||
|
from .windows.widgets import CTkComboBox |
||||
|
from .windows.widgets import CTkEntry |
||||
|
from .windows.widgets import CTkFrame |
||||
|
from .windows.widgets import CTkLabel |
||||
|
from .windows.widgets import CTkOptionMenu |
||||
|
from .windows.widgets import CTkProgressBar |
||||
|
from .windows.widgets import CTkRadioButton |
||||
|
from .windows.widgets import CTkScrollbar |
||||
|
from .windows.widgets import CTkSegmentedButton |
||||
|
from .windows.widgets import CTkSlider |
||||
|
from .windows.widgets import CTkSwitch |
||||
|
from .windows.widgets import CTkTabview |
||||
|
from .windows.widgets import CTkTextbox |
||||
|
|
||||
|
# import windows |
||||
|
from .windows import CTk |
||||
|
from .windows import CTkToplevel |
||||
|
from .windows import CTkInputDialog |
||||
|
|
||||
|
# import font classes |
||||
|
from .windows.widgets.font import CTkFont |
||||
|
|
||||
|
# import image classes |
||||
|
from .windows.widgets.image import CTkImage |
||||
|
|
||||
|
_ = Variable, StringVar, IntVar, DoubleVar, BooleanVar, CENTER, filedialog # prevent IDE from removing unused imports |
||||
|
|
||||
|
|
||||
|
def set_appearance_mode(mode_string: str): |
||||
|
""" possible values: light, dark, system """ |
||||
|
AppearanceModeTracker.set_appearance_mode(mode_string) |
||||
|
|
||||
|
|
||||
|
def get_appearance_mode() -> str: |
||||
|
""" get current state of the appearance mode (light or dark) """ |
||||
|
if AppearanceModeTracker.appearance_mode == 0: |
||||
|
return "Light" |
||||
|
elif AppearanceModeTracker.appearance_mode == 1: |
||||
|
return "Dark" |
||||
|
|
||||
|
|
||||
|
def set_default_color_theme(color_string: str): |
||||
|
""" set color theme or load custom theme file by passing the path """ |
||||
|
ThemeManager.load_theme(color_string) |
||||
|
|
||||
|
|
||||
|
def set_widget_scaling(scaling_value: float): |
||||
|
""" set scaling for the widget dimensions """ |
||||
|
ScalingTracker.set_widget_scaling(scaling_value) |
||||
|
|
||||
|
|
||||
|
def set_window_scaling(scaling_value: float): |
||||
|
""" set scaling for window dimensions """ |
||||
|
ScalingTracker.set_window_scaling(scaling_value) |
||||
|
|
||||
|
|
||||
|
def deactivate_automatic_dpi_awareness(): |
||||
|
""" deactivate DPI awareness of current process (windll.shcore.SetProcessDpiAwareness(0)) """ |
||||
|
ScalingTracker.deactivate_automatic_dpi_awareness = True |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1,152 @@ |
|||||
|
{ |
||||
|
"CTk": { |
||||
|
"fg_color": ["gray92", "gray14"] |
||||
|
}, |
||||
|
"CTkToplevel": { |
||||
|
"fg_color": ["gray92", "gray14"] |
||||
|
}, |
||||
|
"CTkFrame": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["gray86", "gray17"], |
||||
|
"top_fg_color": ["gray81", "gray20"], |
||||
|
"border_color": ["gray65", "gray28"] |
||||
|
}, |
||||
|
"CTkButton": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["#3B8ED0", "#1F6AA5"], |
||||
|
"hover_color": ["#36719F", "#144870"], |
||||
|
"border_color": ["#3E454A", "#949A9F"], |
||||
|
"text_color": ["#DCE4EE", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray74", "gray60"] |
||||
|
}, |
||||
|
"CTkLabel": { |
||||
|
"corner_radius": 0, |
||||
|
"fg_color": "transparent", |
||||
|
"text_color": ["gray10", "#DCE4EE"] |
||||
|
}, |
||||
|
"CTkEntry": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 2, |
||||
|
"fg_color": ["#F9F9FA", "#343638"], |
||||
|
"border_color": ["#979DA2", "#565B5E"], |
||||
|
"text_color":["gray10", "#DCE4EE"], |
||||
|
"placeholder_text_color": ["gray52", "gray62"] |
||||
|
}, |
||||
|
"CTkCheckbox": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 3, |
||||
|
"fg_color": ["#3B8ED0", "#1F6AA5"], |
||||
|
"border_color": ["#3E454A", "#949A9F"], |
||||
|
"hover_color": ["#3B8ED0", "#1F6AA5"], |
||||
|
"checkmark_color": ["#DCE4EE", "gray90"], |
||||
|
"text_color": ["gray10", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray60", "gray45"] |
||||
|
}, |
||||
|
"CTkSwitch": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_width": 3, |
||||
|
"button_length": 0, |
||||
|
"fg_Color": ["#939BA2", "#4A4D50"], |
||||
|
"progress_color": ["#3B8ED0", "#1F6AA5"], |
||||
|
"button_color": ["gray36", "#D5D9DE"], |
||||
|
"button_hover_color": ["gray20", "gray100"], |
||||
|
"text_color": ["gray10", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray60", "gray45"] |
||||
|
}, |
||||
|
"CTkRadiobutton": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_width_checked": 6, |
||||
|
"border_width_unchecked": 3, |
||||
|
"fg_color": ["#3B8ED0", "#1F6AA5"], |
||||
|
"border_color": ["#3E454A", "#949A9F"], |
||||
|
"hover_color": ["#36719F", "#144870"], |
||||
|
"text_color": ["gray10", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray60", "gray45"] |
||||
|
}, |
||||
|
"CTkProgressBar": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["#939BA2", "#4A4D50"], |
||||
|
"progress_color": ["#3B8ED0", "#1F6AA5"], |
||||
|
"border_color": ["gray", "gray"] |
||||
|
}, |
||||
|
"CTkSlider": { |
||||
|
"corner_radius": 1000, |
||||
|
"button_corner_radius": 1000, |
||||
|
"border_width": 6, |
||||
|
"button_length": 0, |
||||
|
"fg_color": ["#939BA2", "#4A4D50"], |
||||
|
"progress_color": ["gray40", "#AAB0B5"], |
||||
|
"button_color": ["#3B8ED0", "#1F6AA5"], |
||||
|
"button_hover_color": ["#36719F", "#144870"] |
||||
|
}, |
||||
|
"CTkOptionMenu": { |
||||
|
"corner_radius": 6, |
||||
|
"fg_color": ["#3B8ED0", "#1F6AA5"], |
||||
|
"button_color": ["#36719F", "#144870"], |
||||
|
"button_hover_color": ["#27577D", "#203A4F"], |
||||
|
"text_color": ["#DCE4EE", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray74", "gray60"] |
||||
|
}, |
||||
|
"CTkComboBox": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 2, |
||||
|
"fg_color": ["#F9F9FA", "#343638"], |
||||
|
"border_color": ["#979DA2", "#565B5E"], |
||||
|
"button_color": ["#979DA2", "#565B5E"], |
||||
|
"button_hover_color": ["#6E7174", "#7A848D"], |
||||
|
"text_color": ["gray10", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray50", "gray45"] |
||||
|
}, |
||||
|
"CTkScrollbar": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_spacing": 4, |
||||
|
"fg_color": "transparent", |
||||
|
"button_color": ["gray55", "gray41"], |
||||
|
"button_hover_color": ["gray40", "gray53"] |
||||
|
}, |
||||
|
"CTkSegmentedButton": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 2, |
||||
|
"fg_color": ["#979DA2", "gray29"], |
||||
|
"selected_color": ["#3B8ED0", "#1F6AA5"], |
||||
|
"selected_hover_color": ["#36719F", "#144870"], |
||||
|
"unselected_color": ["#979DA2", "gray29"], |
||||
|
"unselected_hover_color": ["gray70", "gray41"], |
||||
|
"text_color": ["#DCE4EE", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray74", "gray60"] |
||||
|
}, |
||||
|
"CTkTextbox": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["#F9F9FA", "#1D1E1E"], |
||||
|
"border_color": ["#979DA2", "#565B5E"], |
||||
|
"text_color":["gray10", "#DCE4EE"], |
||||
|
"scrollbar_button_color": ["gray55", "gray41"], |
||||
|
"scrollbar_button_hover_color": ["gray40", "gray53"] |
||||
|
}, |
||||
|
"DropdownMenu": { |
||||
|
"fg_color": ["gray90", "gray20"], |
||||
|
"hover_color": ["gray75", "gray28"], |
||||
|
"text_color": ["gray10", "gray90"] |
||||
|
}, |
||||
|
"CTkFont": { |
||||
|
"macOS": { |
||||
|
"family": "SF Display", |
||||
|
"size": 13, |
||||
|
"weight": "normal" |
||||
|
}, |
||||
|
"Windows": { |
||||
|
"family": "Roboto", |
||||
|
"size": 13, |
||||
|
"weight": "normal" |
||||
|
}, |
||||
|
"Linux": { |
||||
|
"family": "Roboto", |
||||
|
"size": 13, |
||||
|
"weight": "normal" |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,152 @@ |
|||||
|
{ |
||||
|
"CTk": { |
||||
|
"fg_color": ["gray95", "gray10"] |
||||
|
}, |
||||
|
"CTkToplevel": { |
||||
|
"fg_color": ["gray95", "gray10"] |
||||
|
}, |
||||
|
"CTkFrame": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["gray90", "gray13"], |
||||
|
"top_fg_color": ["gray85", "gray16"], |
||||
|
"border_color": ["gray65", "gray28"] |
||||
|
}, |
||||
|
"CTkButton": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["#3a7ebf", "#1f538d"], |
||||
|
"hover_color": ["#325882", "#14375e"], |
||||
|
"border_color": ["#3E454A", "#949A9F"], |
||||
|
"text_color": ["#DCE4EE", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray74", "gray60"] |
||||
|
}, |
||||
|
"CTkLabel": { |
||||
|
"corner_radius": 0, |
||||
|
"fg_color": "transparent", |
||||
|
"text_color": ["gray14", "gray84"] |
||||
|
}, |
||||
|
"CTkEntry": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 2, |
||||
|
"fg_color": ["#F9F9FA", "#343638"], |
||||
|
"border_color": ["#979DA2", "#565B5E"], |
||||
|
"text_color": ["gray14", "gray84"], |
||||
|
"placeholder_text_color": ["gray52", "gray62"] |
||||
|
}, |
||||
|
"CTkCheckbox": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 3, |
||||
|
"fg_color": ["#3a7ebf", "#1f538d"], |
||||
|
"border_color": ["#3E454A", "#949A9F"], |
||||
|
"hover_color": ["#325882", "#14375e"], |
||||
|
"checkmark_color": ["#DCE4EE", "gray90"], |
||||
|
"text_color": ["gray14", "gray84"], |
||||
|
"text_color_disabled": ["gray60", "gray45"] |
||||
|
}, |
||||
|
"CTkSwitch": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_width": 3, |
||||
|
"button_length": 0, |
||||
|
"fg_Color": ["#939BA2", "#4A4D50"], |
||||
|
"progress_color": ["#3a7ebf", "#1f538d"], |
||||
|
"button_color": ["gray36", "#D5D9DE"], |
||||
|
"button_hover_color": ["gray20", "gray100"], |
||||
|
"text_color": ["gray14", "gray84"], |
||||
|
"text_color_disabled": ["gray60", "gray45"] |
||||
|
}, |
||||
|
"CTkRadiobutton": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_width_checked": 6, |
||||
|
"border_width_unchecked": 3, |
||||
|
"fg_color": ["#3a7ebf", "#1f538d"], |
||||
|
"border_color": ["#3E454A", "#949A9F"], |
||||
|
"hover_color": ["#325882", "#14375e"], |
||||
|
"text_color": ["gray14", "gray84"], |
||||
|
"text_color_disabled": ["gray60", "gray45"] |
||||
|
}, |
||||
|
"CTkProgressBar": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["#939BA2", "#4A4D50"], |
||||
|
"progress_color": ["#3a7ebf", "#1f538d"], |
||||
|
"border_color": ["gray", "gray"] |
||||
|
}, |
||||
|
"CTkSlider": { |
||||
|
"corner_radius": 1000, |
||||
|
"button_corner_radius": 1000, |
||||
|
"border_width": 6, |
||||
|
"button_length": 0, |
||||
|
"fg_color": ["#939BA2", "#4A4D50"], |
||||
|
"progress_color": ["gray40", "#AAB0B5"], |
||||
|
"button_color": ["#3a7ebf", "#1f538d"], |
||||
|
"button_hover_color": ["#325882", "#14375e"] |
||||
|
}, |
||||
|
"CTkOptionMenu": { |
||||
|
"corner_radius": 6, |
||||
|
"fg_color": ["#3a7ebf", "#1f538d"], |
||||
|
"button_color": ["#325882", "#14375e"], |
||||
|
"button_hover_color": ["#234567", "#1e2c40"], |
||||
|
"text_color": ["#DCE4EE", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray74", "gray60"] |
||||
|
}, |
||||
|
"CTkComboBox": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 2, |
||||
|
"fg_color": ["#F9F9FA", "#343638"], |
||||
|
"border_color": ["#979DA2", "#565B5E"], |
||||
|
"button_color": ["#979DA2", "#565B5E"], |
||||
|
"button_hover_color": ["#6E7174", "#7A848D"], |
||||
|
"text_color": ["gray14", "gray84"], |
||||
|
"text_color_disabled": ["gray50", "gray45"] |
||||
|
}, |
||||
|
"CTkScrollbar": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_spacing": 4, |
||||
|
"fg_color": "transparent", |
||||
|
"button_color": ["gray55", "gray41"], |
||||
|
"button_hover_color": ["gray40", "gray53"] |
||||
|
}, |
||||
|
"CTkSegmentedButton": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 2, |
||||
|
"fg_color": ["#979DA2", "gray29"], |
||||
|
"selected_color": ["#3a7ebf", "#1f538d"], |
||||
|
"selected_hover_color": ["#325882", "#14375e"], |
||||
|
"unselected_color": ["#979DA2", "gray29"], |
||||
|
"unselected_hover_color": ["gray70", "gray41"], |
||||
|
"text_color": ["#DCE4EE", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray74", "gray60"] |
||||
|
}, |
||||
|
"CTkTextbox": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["gray100", "gray20"], |
||||
|
"border_color": ["#979DA2", "#565B5E"], |
||||
|
"text_color": ["gray14", "gray84"], |
||||
|
"scrollbar_button_color": ["gray55", "gray41"], |
||||
|
"scrollbar_button_hover_color": ["gray40", "gray53"] |
||||
|
}, |
||||
|
"DropdownMenu": { |
||||
|
"fg_color": ["gray90", "gray20"], |
||||
|
"hover_color": ["gray75", "gray28"], |
||||
|
"text_color": ["gray14", "gray84"] |
||||
|
}, |
||||
|
"CTkFont": { |
||||
|
"macOS": { |
||||
|
"family": "SF Display", |
||||
|
"size": 13, |
||||
|
"weight": "normal" |
||||
|
}, |
||||
|
"Windows": { |
||||
|
"family": "Roboto", |
||||
|
"size": 13, |
||||
|
"weight": "normal" |
||||
|
}, |
||||
|
"Linux": { |
||||
|
"family": "Roboto", |
||||
|
"size": 13, |
||||
|
"weight": "normal" |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,152 @@ |
|||||
|
{ |
||||
|
"CTk": { |
||||
|
"fg_color": ["gray92", "gray14"] |
||||
|
}, |
||||
|
"CTkToplevel": { |
||||
|
"fg_color": ["gray92", "gray14"] |
||||
|
}, |
||||
|
"CTkFrame": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["gray86", "gray17"], |
||||
|
"top_fg_color": ["gray81", "gray20"], |
||||
|
"border_color": ["gray65", "gray28"] |
||||
|
}, |
||||
|
"CTkButton": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["#2CC985", "#2FA572"], |
||||
|
"hover_color": ["#0C955A", "#106A43"], |
||||
|
"border_color": ["#3E454A", "#949A9F"], |
||||
|
"text_color": ["gray98", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray78", "gray68"] |
||||
|
}, |
||||
|
"CTkLabel": { |
||||
|
"corner_radius": 0, |
||||
|
"fg_color": "transparent", |
||||
|
"text_color": ["gray10", "#DCE4EE"] |
||||
|
}, |
||||
|
"CTkEntry": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 2, |
||||
|
"fg_color": ["#F9F9FA", "#343638"], |
||||
|
"border_color": ["#979DA2", "#565B5E"], |
||||
|
"text_color":["gray10", "#DCE4EE"], |
||||
|
"placeholder_text_color": ["gray52", "gray62"] |
||||
|
}, |
||||
|
"CTkCheckbox": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 3, |
||||
|
"fg_color": ["#2CC985", "#2FA572"], |
||||
|
"border_color": ["#3E454A", "#949A9F"], |
||||
|
"hover_color": ["#0C955A", "#106A43"], |
||||
|
"checkmark_color": ["#DCE4EE", "gray90"], |
||||
|
"text_color": ["gray10", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray60", "gray45"] |
||||
|
}, |
||||
|
"CTkSwitch": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_width": 3, |
||||
|
"button_length": 0, |
||||
|
"fg_Color": ["#939BA2", "#4A4D50"], |
||||
|
"progress_color": ["#2CC985", "#2FA572"], |
||||
|
"button_color": ["gray36", "#D5D9DE"], |
||||
|
"button_hover_color": ["gray20", "gray100"], |
||||
|
"text_color": ["gray10", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray60", "gray45"] |
||||
|
}, |
||||
|
"CTkRadiobutton": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_width_checked": 6, |
||||
|
"border_width_unchecked": 3, |
||||
|
"fg_color": ["#2CC985", "#2FA572"], |
||||
|
"border_color": ["#3E454A", "#949A9F"], |
||||
|
"hover_color":["#0C955A", "#106A43"], |
||||
|
"text_color": ["gray10", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray60", "gray45"] |
||||
|
}, |
||||
|
"CTkProgressBar": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["#939BA2", "#4A4D50"], |
||||
|
"progress_color": ["#2CC985", "#2FA572"], |
||||
|
"border_color": ["gray", "gray"] |
||||
|
}, |
||||
|
"CTkSlider": { |
||||
|
"corner_radius": 1000, |
||||
|
"button_corner_radius": 1000, |
||||
|
"border_width": 6, |
||||
|
"button_length": 0, |
||||
|
"fg_color": ["#939BA2", "#4A4D50"], |
||||
|
"progress_color": ["gray40", "#AAB0B5"], |
||||
|
"button_color": ["#2CC985", "#2FA572"], |
||||
|
"button_hover_color": ["#0C955A", "#106A43"] |
||||
|
}, |
||||
|
"CTkOptionMenu": { |
||||
|
"corner_radius": 6, |
||||
|
"fg_color": ["#2cbe79", "#2FA572"], |
||||
|
"button_color": ["#0C955A", "#106A43"], |
||||
|
"button_hover_color": ["#0b6e3d", "#17472e"], |
||||
|
"text_color": ["gray98", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray78", "gray68"] |
||||
|
}, |
||||
|
"CTkComboBox": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 2, |
||||
|
"fg_color": ["#F9F9FA", "#343638"], |
||||
|
"border_color": ["#979DA2", "#565B5E"], |
||||
|
"button_color": ["#979DA2", "#565B5E"], |
||||
|
"button_hover_color": ["#6E7174", "#7A848D"], |
||||
|
"text_color": ["gray10", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray50", "gray45"] |
||||
|
}, |
||||
|
"CTkScrollbar": { |
||||
|
"corner_radius": 1000, |
||||
|
"border_spacing": 4, |
||||
|
"fg_color": "transparent", |
||||
|
"button_color": ["gray55", "gray41"], |
||||
|
"button_hover_color": ["gray40", "gray53"] |
||||
|
}, |
||||
|
"CTkSegmentedButton": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 2, |
||||
|
"fg_color": ["#979DA2", "gray29"], |
||||
|
"selected_color": ["#2CC985", "#2FA572"], |
||||
|
"selected_hover_color": ["#0C955A", "#106A43"], |
||||
|
"unselected_color": ["#979DA2", "gray29"], |
||||
|
"unselected_hover_color": ["gray70", "gray41"], |
||||
|
"text_color": ["gray98", "#DCE4EE"], |
||||
|
"text_color_disabled": ["gray78", "gray68"] |
||||
|
}, |
||||
|
"CTkTextbox": { |
||||
|
"corner_radius": 6, |
||||
|
"border_width": 0, |
||||
|
"fg_color": ["#F9F9FA", "gray23"], |
||||
|
"border_color": ["#979DA2", "#565B5E"], |
||||
|
"text_color":["gray10", "#DCE4EE"], |
||||
|
"scrollbar_button_color": ["gray55", "gray41"], |
||||
|
"scrollbar_button_hover_color": ["gray40", "gray53"] |
||||
|
}, |
||||
|
"DropdownMenu": { |
||||
|
"fg_color": ["gray90", "gray20"], |
||||
|
"hover_color": ["gray75", "gray28"], |
||||
|
"text_color": ["gray10", "gray90"] |
||||
|
}, |
||||
|
"CTkFont": { |
||||
|
"macOS": { |
||||
|
"family": "SF Display", |
||||
|
"size": 13, |
||||
|
"weight": "normal" |
||||
|
}, |
||||
|
"Windows": { |
||||
|
"family": "Roboto", |
||||
|
"size": 13, |
||||
|
"weight": "normal" |
||||
|
}, |
||||
|
"Linux": { |
||||
|
"family": "Roboto", |
||||
|
"size": 13, |
||||
|
"weight": "normal" |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
from .ctk_tk import CTk |
||||
|
from .ctk_toplevel import CTkToplevel |
||||
|
from .ctk_input_dialog import CTkInputDialog |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,110 @@ |
|||||
|
from typing import Union, Tuple, Optional |
||||
|
|
||||
|
from .widgets import CTkLabel |
||||
|
from .widgets import CTkEntry |
||||
|
from .widgets import CTkButton |
||||
|
from .widgets.theme import ThemeManager |
||||
|
from .ctk_toplevel import CTkToplevel |
||||
|
|
||||
|
|
||||
|
class CTkInputDialog(CTkToplevel): |
||||
|
""" |
||||
|
Dialog with extra window, message, entry widget, cancel and ok button. |
||||
|
For detailed information check out the documentation. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
entry_border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
entry_text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
title: str = "CTkDialog", |
||||
|
text: str = "CTkDialog"): |
||||
|
|
||||
|
super().__init__(fg_color=fg_color) |
||||
|
|
||||
|
self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color) |
||||
|
self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color) |
||||
|
self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) |
||||
|
self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color) |
||||
|
self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color) |
||||
|
self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color) |
||||
|
self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color) |
||||
|
|
||||
|
self._user_input: Union[str, None] = None |
||||
|
self._running: bool = False |
||||
|
self._text = text |
||||
|
|
||||
|
self.title(title) |
||||
|
self.lift() # lift window on top |
||||
|
self.attributes("-topmost", True) # stay on top |
||||
|
self.protocol("WM_DELETE_WINDOW", self._on_closing) |
||||
|
self.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background |
||||
|
self.resizable(False, False) |
||||
|
self.grab_set() # make other windows not clickable |
||||
|
|
||||
|
def _create_widgets(self): |
||||
|
|
||||
|
self.grid_columnconfigure((0, 1), weight=1) |
||||
|
self.rowconfigure(0, weight=1) |
||||
|
|
||||
|
self._label = CTkLabel(master=self, |
||||
|
width=300, |
||||
|
wraplength=300, |
||||
|
fg_color="transparent", |
||||
|
text_color=self._text_color, |
||||
|
text=self._text,) |
||||
|
self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew") |
||||
|
|
||||
|
self._entry = CTkEntry(master=self, |
||||
|
width=230, |
||||
|
fg_color=self._entry_fg_color, |
||||
|
border_color=self._entry_border_color, |
||||
|
text_color=self._entry_text_color) |
||||
|
self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew") |
||||
|
|
||||
|
self._ok_button = CTkButton(master=self, |
||||
|
width=100, |
||||
|
border_width=0, |
||||
|
fg_color=self._button_fg_color, |
||||
|
hover_color=self._button_hover_color, |
||||
|
text_color=self._button_text_color, |
||||
|
text='Ok', |
||||
|
command=self._ok_event) |
||||
|
self._ok_button.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew") |
||||
|
|
||||
|
self._cancel_button = CTkButton(master=self, |
||||
|
width=100, |
||||
|
border_width=0, |
||||
|
fg_color=self._button_fg_color, |
||||
|
hover_color=self._button_hover_color, |
||||
|
text_color=self._button_text_color, |
||||
|
text='Cancel', |
||||
|
command=self._ok_event) |
||||
|
self._cancel_button.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew") |
||||
|
|
||||
|
self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work |
||||
|
self._entry.bind("<Return>", self._ok_event) |
||||
|
|
||||
|
def _ok_event(self, event=None): |
||||
|
self._user_input = self._entry.get() |
||||
|
self.grab_release() |
||||
|
self.destroy() |
||||
|
|
||||
|
def _on_closing(self): |
||||
|
self.grab_release() |
||||
|
self.destroy() |
||||
|
|
||||
|
def _cancel_event(self): |
||||
|
self.grab_release() |
||||
|
self.destroy() |
||||
|
|
||||
|
def get_input(self): |
||||
|
self.master.wait_window(self) |
||||
|
return self._user_input |
@ -0,0 +1,316 @@ |
|||||
|
import tkinter |
||||
|
from distutils.version import StrictVersion as Version |
||||
|
import sys |
||||
|
import os |
||||
|
import platform |
||||
|
import ctypes |
||||
|
from typing import Union, Tuple, Optional |
||||
|
|
||||
|
from .widgets.theme import ThemeManager |
||||
|
from .widgets.scaling import CTkScalingBaseClass |
||||
|
from .widgets.appearance_mode import CTkAppearanceModeBaseClass |
||||
|
|
||||
|
from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty |
||||
|
|
||||
|
|
||||
|
class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): |
||||
|
""" |
||||
|
Main app window with dark titlebar on Windows and macOS. |
||||
|
For detailed information check out the documentation. |
||||
|
""" |
||||
|
|
||||
|
_valid_tk_constructor_arguments: set = {"screenName", "baseName", "className", "useTk", "sync", "use"} |
||||
|
|
||||
|
_valid_tk_configure_arguments: set = {'bd', 'borderwidth', 'class', 'menu', 'relief', 'screen', |
||||
|
'use', 'container', 'cursor', 'height', |
||||
|
'highlightthickness', 'padx', 'pady', 'takefocus', 'visual', 'width'} |
||||
|
|
||||
|
_deactivate_macos_window_header_manipulation: bool = False |
||||
|
_deactivate_windows_window_header_manipulation: bool = False |
||||
|
|
||||
|
def __init__(self, |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
**kwargs): |
||||
|
|
||||
|
self._enable_macos_dark_title_bar() |
||||
|
|
||||
|
# 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) |
||||
|
|
||||
|
self._current_width = 600 # initial window size, independent of scaling |
||||
|
self._current_height = 500 |
||||
|
self._min_width: int = 0 |
||||
|
self._min_height: int = 0 |
||||
|
self._max_width: int = 1_000_000 |
||||
|
self._max_height: int = 1_000_000 |
||||
|
self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs) |
||||
|
|
||||
|
self._fg_color = ThemeManager.theme["CTk"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
|
||||
|
# set bg of tkinter.Tk |
||||
|
super().configure(bg=self._apply_appearance_mode(self._fg_color)) |
||||
|
|
||||
|
# set title |
||||
|
self.title("CTk") |
||||
|
|
||||
|
# indicator variables |
||||
|
self._iconbitmap_method_called = False # indicates if wm_iconbitmap method got called |
||||
|
self._state_before_windows_set_titlebar_color = None |
||||
|
self._window_exists = False # indicates if the window is already shown through update() or mainloop() after init |
||||
|
self._withdraw_called_before_window_exists = False # indicates if withdraw() was called before window is first shown through update() or mainloop() |
||||
|
self._iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop() |
||||
|
self._block_update_dimensions_event = False |
||||
|
|
||||
|
# set CustomTkinter titlebar icon (Windows only) |
||||
|
if sys.platform.startswith("win"): |
||||
|
self.after(200, self._windows_set_titlebar_icon) |
||||
|
|
||||
|
# set titlebar color (Windows only) |
||||
|
if sys.platform.startswith("win"): |
||||
|
self._windows_set_titlebar_color(self._get_appearance_mode()) |
||||
|
|
||||
|
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.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": |
||||
|
self.lift() |
||||
|
|
||||
|
def _update_dimensions_event(self, event=None): |
||||
|
if not self._block_update_dimensions_event: |
||||
|
|
||||
|
detected_width = super().winfo_width() # detect current window size |
||||
|
detected_height = super().winfo_height() |
||||
|
|
||||
|
# detected_width = event.width |
||||
|
# detected_height = event.height |
||||
|
|
||||
|
if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height): |
||||
|
self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event |
||||
|
self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale |
||||
|
|
||||
|
def _set_scaling(self, new_widget_scaling, new_window_scaling): |
||||
|
super()._set_scaling(new_widget_scaling, new_window_scaling) |
||||
|
|
||||
|
# Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. |
||||
|
super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) |
||||
|
super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) |
||||
|
|
||||
|
super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}") |
||||
|
|
||||
|
# set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) |
||||
|
self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) |
||||
|
|
||||
|
def block_update_dimensions_event(self): |
||||
|
self._block_update_dimensions_event = False |
||||
|
|
||||
|
def unblock_update_dimensions_event(self): |
||||
|
self._block_update_dimensions_event = False |
||||
|
|
||||
|
def _set_scaled_min_max(self): |
||||
|
if self._min_width is not None or self._min_height is not None: |
||||
|
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height)) |
||||
|
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 withdraw(self): |
||||
|
if self._window_exists is False: |
||||
|
self._withdraw_called_before_window_exists = True |
||||
|
super().withdraw() |
||||
|
|
||||
|
def iconify(self): |
||||
|
if self._window_exists is False: |
||||
|
self._iconify_called_before_window_exists = True |
||||
|
super().iconify() |
||||
|
|
||||
|
def update(self): |
||||
|
if self._window_exists is False: |
||||
|
self._window_exists = True |
||||
|
|
||||
|
if sys.platform.startswith("win"): |
||||
|
if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists: |
||||
|
# print("window dont exists -> deiconify in update") |
||||
|
self.deiconify() |
||||
|
|
||||
|
super().update() |
||||
|
|
||||
|
def mainloop(self, *args, **kwargs): |
||||
|
if not self._window_exists: |
||||
|
self._window_exists = True |
||||
|
|
||||
|
if sys.platform.startswith("win"): |
||||
|
if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists: |
||||
|
# print("window dont exists -> deiconify in mainloop") |
||||
|
self.deiconify() |
||||
|
|
||||
|
super().mainloop(*args, **kwargs) |
||||
|
|
||||
|
def resizable(self, width: bool = None, height: bool = None): |
||||
|
current_resizable_values = super().resizable(width, height) |
||||
|
self._last_resizable_args = ([], {"width": width, "height": height}) |
||||
|
|
||||
|
if sys.platform.startswith("win"): |
||||
|
self._windows_set_titlebar_color(self._get_appearance_mode()) |
||||
|
|
||||
|
return current_resizable_values |
||||
|
|
||||
|
def minsize(self, width: int = None, height: int = None): |
||||
|
self._min_width = width |
||||
|
self._min_height = height |
||||
|
if self._current_width < width: |
||||
|
self._current_width = width |
||||
|
if self._current_height < height: |
||||
|
self._current_height = height |
||||
|
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height)) |
||||
|
|
||||
|
def maxsize(self, width: int = None, height: int = None): |
||||
|
self._max_width = width |
||||
|
self._max_height = height |
||||
|
if self._current_width > width: |
||||
|
self._current_width = width |
||||
|
if self._current_height > height: |
||||
|
self._current_height = height |
||||
|
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height)) |
||||
|
|
||||
|
def geometry(self, geometry_string: str = None): |
||||
|
if geometry_string is not None: |
||||
|
super().geometry(self._apply_geometry_scaling(geometry_string)) |
||||
|
|
||||
|
# update width and height attributes |
||||
|
width, height, x, y = self._parse_geometry_string(geometry_string) |
||||
|
if width is not None and height is not None: |
||||
|
self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max |
||||
|
self._current_height = max(self._min_height, min(height, self._max_height)) |
||||
|
else: |
||||
|
return self._reverse_geometry_scaling(super().geometry()) |
||||
|
|
||||
|
def configure(self, **kwargs): |
||||
|
if "fg_color" in kwargs: |
||||
|
self._fg_color = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
super().configure(bg=self._apply_appearance_mode(self._fg_color)) |
||||
|
|
||||
|
for child in self.winfo_children(): |
||||
|
try: |
||||
|
child.configure(bg_color=self._fg_color) |
||||
|
except Exception: |
||||
|
pass |
||||
|
|
||||
|
super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_configure_arguments)) |
||||
|
check_kwargs_empty(kwargs) |
||||
|
|
||||
|
def cget(self, attribute_name: str) -> any: |
||||
|
if attribute_name == "fg_color": |
||||
|
return self._fg_color |
||||
|
else: |
||||
|
return super().cget(attribute_name) |
||||
|
|
||||
|
def wm_iconbitmap(self, bitmap=None, default=None): |
||||
|
self._iconbitmap_method_called = True |
||||
|
super().wm_iconbitmap(bitmap, default) |
||||
|
|
||||
|
def _windows_set_titlebar_icon(self): |
||||
|
try: |
||||
|
# if not the user already called iconbitmap method, set icon |
||||
|
if not self._iconbitmap_method_called: |
||||
|
customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
||||
|
self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico")) |
||||
|
except Exception: |
||||
|
pass |
||||
|
|
||||
|
@classmethod |
||||
|
def _enable_macos_dark_title_bar(cls): |
||||
|
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS |
||||
|
if Version(platform.python_version()) < Version("3.10"): |
||||
|
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 |
||||
|
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No") |
||||
|
# This command allows dark-mode for all programs |
||||
|
|
||||
|
@classmethod |
||||
|
def _disable_macos_dark_title_bar(cls): |
||||
|
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS |
||||
|
if Version(platform.python_version()) < Version("3.10"): |
||||
|
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 |
||||
|
os.system("defaults delete -g NSRequiresAquaSystemAppearance") |
||||
|
# This command reverts the dark-mode setting for all programs. |
||||
|
|
||||
|
def _windows_set_titlebar_color(self, color_mode: str): |
||||
|
""" |
||||
|
Set the titlebar color of the window to light or dark theme on Microsoft Windows. |
||||
|
|
||||
|
Credits for this function: |
||||
|
https://stackoverflow.com/questions/23836000/can-i-change-the-title-bar-in-tkinter/70724666#70724666 |
||||
|
|
||||
|
MORE INFO: |
||||
|
https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute |
||||
|
""" |
||||
|
|
||||
|
if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation: |
||||
|
|
||||
|
if self._window_exists: |
||||
|
self._state_before_windows_set_titlebar_color = self.state() |
||||
|
# print("window_exists -> state_before_windows_set_titlebar_color: ", self.state_before_windows_set_titlebar_color) |
||||
|
|
||||
|
if self._state_before_windows_set_titlebar_color != "iconic" or self._state_before_windows_set_titlebar_color != "withdrawn": |
||||
|
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible |
||||
|
else: |
||||
|
# print("window dont exists -> withdraw and update") |
||||
|
super().withdraw() |
||||
|
super().update() |
||||
|
|
||||
|
if color_mode.lower() == "dark": |
||||
|
value = 1 |
||||
|
elif color_mode.lower() == "light": |
||||
|
value = 0 |
||||
|
else: |
||||
|
return |
||||
|
|
||||
|
try: |
||||
|
hwnd = ctypes.windll.user32.GetParent(self.winfo_id()) |
||||
|
DWMWA_USE_IMMERSIVE_DARK_MODE = 20 |
||||
|
DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19 |
||||
|
|
||||
|
# try with DWMWA_USE_IMMERSIVE_DARK_MODE |
||||
|
if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, |
||||
|
ctypes.byref(ctypes.c_int(value)), |
||||
|
ctypes.sizeof(ctypes.c_int(value))) != 0: |
||||
|
|
||||
|
# try with DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20h1 |
||||
|
ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, |
||||
|
ctypes.byref(ctypes.c_int(value)), |
||||
|
ctypes.sizeof(ctypes.c_int(value))) |
||||
|
|
||||
|
except Exception as err: |
||||
|
print(err) |
||||
|
|
||||
|
if self._window_exists: |
||||
|
# print("window_exists -> return to original state: ", self.state_before_windows_set_titlebar_color) |
||||
|
if self._state_before_windows_set_titlebar_color == "normal": |
||||
|
self.deiconify() |
||||
|
elif self._state_before_windows_set_titlebar_color == "iconic": |
||||
|
self.iconify() |
||||
|
elif self._state_before_windows_set_titlebar_color == "zoomed": |
||||
|
self.state("zoomed") |
||||
|
else: |
||||
|
self.state(self._state_before_windows_set_titlebar_color) # other states |
||||
|
else: |
||||
|
pass # wait for update or mainloop to be called |
||||
|
|
||||
|
def _set_appearance_mode(self, mode_string: str): |
||||
|
super()._set_appearance_mode(mode_string) |
||||
|
|
||||
|
if sys.platform.startswith("win"): |
||||
|
self._windows_set_titlebar_color(mode_string) |
||||
|
|
||||
|
super().configure(bg=self._apply_appearance_mode(self._fg_color)) |
@ -0,0 +1,299 @@ |
|||||
|
import tkinter |
||||
|
from distutils.version import StrictVersion as Version |
||||
|
import sys |
||||
|
import os |
||||
|
import platform |
||||
|
import ctypes |
||||
|
from typing import Union, Tuple, Optional |
||||
|
|
||||
|
from .widgets.theme import ThemeManager |
||||
|
from .widgets.scaling import CTkScalingBaseClass |
||||
|
from .widgets.appearance_mode import CTkAppearanceModeBaseClass |
||||
|
|
||||
|
from customtkinter.windows.widgets.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty |
||||
|
|
||||
|
|
||||
|
class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseClass): |
||||
|
""" |
||||
|
Toplevel window with dark titlebar on Windows and macOS. |
||||
|
For detailed information check out the documentation. |
||||
|
""" |
||||
|
|
||||
|
_valid_tk_toplevel_arguments: set = {"bd", "borderwidth", "class", "container", "cursor", "height", |
||||
|
"highlightbackground", "highlightthickness", "menu", "relief", |
||||
|
"screen", "takefocus", "use", "visual", "width"} |
||||
|
|
||||
|
_deactivate_macos_window_header_manipulation: bool = False |
||||
|
_deactivate_windows_window_header_manipulation: bool = False |
||||
|
|
||||
|
def __init__(self, *args, |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
**kwargs): |
||||
|
|
||||
|
self._enable_macos_dark_title_bar() |
||||
|
|
||||
|
# call init methods of super classes |
||||
|
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) |
||||
|
|
||||
|
try: |
||||
|
# Set Windows titlebar icon |
||||
|
if sys.platform.startswith("win"): |
||||
|
customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
||||
|
self.after(200, lambda: self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico"))) |
||||
|
except Exception: |
||||
|
pass |
||||
|
|
||||
|
self._current_width = 200 # initial window size, always without scaling |
||||
|
self._current_height = 200 |
||||
|
self._min_width: int = 0 |
||||
|
self._min_height: int = 0 |
||||
|
self._max_width: int = 1_000_000 |
||||
|
self._max_height: int = 1_000_000 |
||||
|
self._last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs) |
||||
|
|
||||
|
self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(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") |
||||
|
|
||||
|
# indicator variables |
||||
|
self._iconbitmap_method_called = True |
||||
|
self._state_before_windows_set_titlebar_color = None |
||||
|
self._windows_set_titlebar_color_called = False # indicates if windows_set_titlebar_color was called, stays True until revert_withdraw_after_windows_set_titlebar_color is called |
||||
|
self._withdraw_called_after_windows_set_titlebar_color = False # indicates if withdraw() was called after windows_set_titlebar_color |
||||
|
self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color |
||||
|
self._block_update_dimensions_event = False |
||||
|
|
||||
|
# set CustomTkinter titlebar icon (Windows only) |
||||
|
if sys.platform.startswith("win"): |
||||
|
self.after(200, self._windows_set_titlebar_icon) |
||||
|
|
||||
|
# set titlebar color (Windows only) |
||||
|
if sys.platform.startswith("win"): |
||||
|
self._windows_set_titlebar_color(self._get_appearance_mode()) |
||||
|
|
||||
|
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": |
||||
|
self.lift() |
||||
|
|
||||
|
def _update_dimensions_event(self, event=None): |
||||
|
if not self._block_update_dimensions_event: |
||||
|
detected_width = self.winfo_width() # detect current window size |
||||
|
detected_height = self.winfo_height() |
||||
|
|
||||
|
if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height): |
||||
|
self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event |
||||
|
self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale |
||||
|
|
||||
|
def _set_scaling(self, new_widget_scaling, new_window_scaling): |
||||
|
super()._set_scaling(new_widget_scaling, new_window_scaling) |
||||
|
|
||||
|
# Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. |
||||
|
super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) |
||||
|
super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) |
||||
|
|
||||
|
super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}") |
||||
|
|
||||
|
# set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) |
||||
|
self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) |
||||
|
|
||||
|
def block_update_dimensions_event(self): |
||||
|
self._block_update_dimensions_event = False |
||||
|
|
||||
|
def unblock_update_dimensions_event(self): |
||||
|
self._block_update_dimensions_event = False |
||||
|
|
||||
|
def _set_scaled_min_max(self): |
||||
|
if self._min_width is not None or self._min_height is not None: |
||||
|
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height)) |
||||
|
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 geometry(self, geometry_string: str = None): |
||||
|
if geometry_string is not None: |
||||
|
super().geometry(self._apply_geometry_scaling(geometry_string)) |
||||
|
|
||||
|
# update width and height attributes |
||||
|
width, height, x, y = self._parse_geometry_string(geometry_string) |
||||
|
if width is not None and height is not None: |
||||
|
self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max |
||||
|
self._current_height = max(self._min_height, min(height, self._max_height)) |
||||
|
else: |
||||
|
return self._reverse_geometry_scaling(super().geometry()) |
||||
|
|
||||
|
def withdraw(self): |
||||
|
if self._windows_set_titlebar_color_called: |
||||
|
self._withdraw_called_after_windows_set_titlebar_color = True |
||||
|
super().withdraw() |
||||
|
|
||||
|
def iconify(self): |
||||
|
if self._windows_set_titlebar_color_called: |
||||
|
self._iconify_called_after_windows_set_titlebar_color = True |
||||
|
super().iconify() |
||||
|
|
||||
|
def resizable(self, width: bool = None, height: bool = None): |
||||
|
current_resizable_values = super().resizable(width, height) |
||||
|
self._last_resizable_args = ([], {"width": width, "height": height}) |
||||
|
|
||||
|
if sys.platform.startswith("win"): |
||||
|
self.after(10, lambda: self._windows_set_titlebar_color(self._get_appearance_mode())) |
||||
|
|
||||
|
return current_resizable_values |
||||
|
|
||||
|
def minsize(self, width=None, height=None): |
||||
|
self._min_width = width |
||||
|
self._min_height = height |
||||
|
if self._current_width < width: |
||||
|
self._current_width = width |
||||
|
if self._current_height < height: |
||||
|
self._current_height = height |
||||
|
super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height)) |
||||
|
|
||||
|
def maxsize(self, width=None, height=None): |
||||
|
self._max_width = width |
||||
|
self._max_height = height |
||||
|
if self._current_width > width: |
||||
|
self._current_width = width |
||||
|
if self._current_height > height: |
||||
|
self._current_height = height |
||||
|
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height)) |
||||
|
|
||||
|
def configure(self, **kwargs): |
||||
|
if "fg_color" in kwargs: |
||||
|
self._fg_color = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
super().configure(bg=self._apply_appearance_mode(self._fg_color)) |
||||
|
|
||||
|
for child in self.winfo_children(): |
||||
|
try: |
||||
|
child.configure(bg_color=self._fg_color) |
||||
|
except Exception: |
||||
|
pass |
||||
|
|
||||
|
super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments)) |
||||
|
check_kwargs_empty(kwargs) |
||||
|
|
||||
|
def cget(self, attribute_name: str) -> any: |
||||
|
if attribute_name == "fg_color": |
||||
|
return self._fg_color |
||||
|
else: |
||||
|
return super().cget(attribute_name) |
||||
|
|
||||
|
def wm_iconbitmap(self, bitmap=None, default=None): |
||||
|
self._iconbitmap_method_called = True |
||||
|
super().wm_iconbitmap(bitmap, default) |
||||
|
|
||||
|
def _windows_set_titlebar_icon(self): |
||||
|
try: |
||||
|
# if not the user already called iconbitmap method, set icon |
||||
|
if not self._iconbitmap_method_called: |
||||
|
customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
||||
|
self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico")) |
||||
|
except Exception: |
||||
|
pass |
||||
|
|
||||
|
@classmethod |
||||
|
def _enable_macos_dark_title_bar(cls): |
||||
|
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS |
||||
|
if Version(platform.python_version()) < Version("3.10"): |
||||
|
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 |
||||
|
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No") |
||||
|
|
||||
|
@classmethod |
||||
|
def _disable_macos_dark_title_bar(cls): |
||||
|
if sys.platform == "darwin" and not cls._deactivate_macos_window_header_manipulation: # macOS |
||||
|
if Version(platform.python_version()) < Version("3.10"): |
||||
|
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 |
||||
|
os.system("defaults delete -g NSRequiresAquaSystemAppearance") |
||||
|
# This command reverts the dark-mode setting for all programs. |
||||
|
|
||||
|
def _windows_set_titlebar_color(self, color_mode: str): |
||||
|
""" |
||||
|
Set the titlebar color of the window to light or dark theme on Microsoft Windows. |
||||
|
|
||||
|
Credits for this function: |
||||
|
https://stackoverflow.com/questions/23836000/can-i-change-the-title-bar-in-tkinter/70724666#70724666 |
||||
|
|
||||
|
MORE INFO: |
||||
|
https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute |
||||
|
""" |
||||
|
|
||||
|
if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation: |
||||
|
|
||||
|
self._state_before_windows_set_titlebar_color = self.state() |
||||
|
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible |
||||
|
super().update() |
||||
|
|
||||
|
if color_mode.lower() == "dark": |
||||
|
value = 1 |
||||
|
elif color_mode.lower() == "light": |
||||
|
value = 0 |
||||
|
else: |
||||
|
return |
||||
|
|
||||
|
try: |
||||
|
hwnd = ctypes.windll.user32.GetParent(self.winfo_id()) |
||||
|
DWMWA_USE_IMMERSIVE_DARK_MODE = 20 |
||||
|
DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19 |
||||
|
|
||||
|
# try with DWMWA_USE_IMMERSIVE_DARK_MODE |
||||
|
if ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, |
||||
|
ctypes.byref(ctypes.c_int(value)), |
||||
|
ctypes.sizeof(ctypes.c_int(value))) != 0: |
||||
|
# try with DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20h1 |
||||
|
ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, |
||||
|
ctypes.byref(ctypes.c_int(value)), |
||||
|
ctypes.sizeof(ctypes.c_int(value))) |
||||
|
|
||||
|
except Exception as err: |
||||
|
print(err) |
||||
|
|
||||
|
self._windows_set_titlebar_color_called = True |
||||
|
self.after(5, self._revert_withdraw_after_windows_set_titlebar_color) |
||||
|
|
||||
|
def _revert_withdraw_after_windows_set_titlebar_color(self): |
||||
|
""" if in a short time (5ms) after """ |
||||
|
if self._windows_set_titlebar_color_called: |
||||
|
|
||||
|
if self._withdraw_called_after_windows_set_titlebar_color: |
||||
|
pass # leave it withdrawed |
||||
|
elif self._iconify_called_after_windows_set_titlebar_color: |
||||
|
super().iconify() |
||||
|
else: |
||||
|
if self._state_before_windows_set_titlebar_color == "normal": |
||||
|
self.deiconify() |
||||
|
elif self._state_before_windows_set_titlebar_color == "iconic": |
||||
|
self.iconify() |
||||
|
elif self._state_before_windows_set_titlebar_color == "zoomed": |
||||
|
self.state("zoomed") |
||||
|
else: |
||||
|
self.state(self._state_before_windows_set_titlebar_color) # other states |
||||
|
|
||||
|
self._windows_set_titlebar_color_called = False |
||||
|
self._withdraw_called_after_windows_set_titlebar_color = False |
||||
|
self._iconify_called_after_windows_set_titlebar_color = False |
||||
|
|
||||
|
def _set_appearance_mode(self, mode_string): |
||||
|
super()._set_appearance_mode(mode_string) |
||||
|
|
||||
|
if sys.platform.startswith("win"): |
||||
|
self._windows_set_titlebar_color(mode_string) |
||||
|
|
||||
|
super().configure(bg=self._apply_appearance_mode(self._fg_color)) |
@ -0,0 +1,15 @@ |
|||||
|
from .ctk_button import CTkButton |
||||
|
from .ctk_checkbox import CTkCheckBox |
||||
|
from .ctk_combobox import CTkComboBox |
||||
|
from .ctk_entry import CTkEntry |
||||
|
from .ctk_frame import CTkFrame |
||||
|
from .ctk_label import CTkLabel |
||||
|
from .ctk_optionmenu import CTkOptionMenu |
||||
|
from .ctk_progressbar import CTkProgressBar |
||||
|
from .ctk_radiobutton import CTkRadioButton |
||||
|
from .ctk_scrollbar import CTkScrollbar |
||||
|
from .ctk_segmented_button import CTkSegmentedButton |
||||
|
from .ctk_slider import CTkSlider |
||||
|
from .ctk_switch import CTkSwitch |
||||
|
from .ctk_tabview import CTkTabview |
||||
|
from .ctk_textbox import CTkTextbox |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,4 @@ |
|||||
|
from .appearance_mode_base_class import CTkAppearanceModeBaseClass |
||||
|
from .appearance_mode_tracker import AppearanceModeTracker |
||||
|
|
||||
|
AppearanceModeTracker.init_appearance_mode() |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,61 @@ |
|||||
|
from typing import Union, Tuple, List |
||||
|
|
||||
|
from .appearance_mode_tracker import AppearanceModeTracker |
||||
|
|
||||
|
|
||||
|
class CTkAppearanceModeBaseClass: |
||||
|
""" |
||||
|
Super-class that manages the appearance mode. Methods: |
||||
|
|
||||
|
- destroy() must be called when sub-class is destroyed |
||||
|
- _set_appearance_mode() abstractmethod, gets called when appearance mode changes, must be overridden |
||||
|
- _apply_appearance_mode() |
||||
|
|
||||
|
""" |
||||
|
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 _set_appearance_mode(self, mode_string: str): |
||||
|
""" can be overridden but super method must be called at the beginning """ |
||||
|
if mode_string.lower() == "dark": |
||||
|
self.__appearance_mode = 1 |
||||
|
elif mode_string.lower() == "light": |
||||
|
self.__appearance_mode = 0 |
||||
|
|
||||
|
def _get_appearance_mode(self) -> str: |
||||
|
""" get appearance mode as a string, 'light' or 'dark' """ |
||||
|
if self.__appearance_mode == 0: |
||||
|
return "light" |
||||
|
else: |
||||
|
return "dark" |
||||
|
|
||||
|
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 isinstance(color, (tuple, list)): |
||||
|
return color[self.__appearance_mode] |
||||
|
else: |
||||
|
return color |
||||
|
|
||||
|
@staticmethod |
||||
|
def _check_color_type(color: any, transparency: bool = False): |
||||
|
if color is None: |
||||
|
raise ValueError(f"color is None, for transparency set color='transparent'") |
||||
|
elif isinstance(color, (tuple, list)) and (color[0] == "transparent" or color[1] == "transparent"): |
||||
|
raise ValueError(f"transparency is not allowed in tuple color {color}, use 'transparent'") |
||||
|
elif color == "transparent" and transparency is False: |
||||
|
raise ValueError(f"transparency is not allowed for this attribute") |
||||
|
elif isinstance(color, str): |
||||
|
return color |
||||
|
elif isinstance(color, (tuple, list)) and len(color) == 2 and isinstance(color[0], str) and isinstance(color[1], str): |
||||
|
return color |
||||
|
else: |
||||
|
raise ValueError(f"color {color} must be string ('transparent' or 'color-name' or 'hex-color') or tuple of two strings, not {type(color)}") |
@ -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 = 30 # 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" |
@ -0,0 +1,12 @@ |
|||||
|
import sys |
||||
|
|
||||
|
from .ctk_canvas import CTkCanvas |
||||
|
from .draw_engine import DrawEngine |
||||
|
|
||||
|
CTkCanvas.init_font_character_mapping() |
||||
|
|
||||
|
# determine draw method based on current platform |
||||
|
if sys.platform == "darwin": |
||||
|
DrawEngine.preferred_drawing_method = "polygon_shapes" |
||||
|
else: |
||||
|
DrawEngine.preferred_drawing_method = "font_shapes" |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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) |
File diff suppressed because it is too large
@ -0,0 +1,2 @@ |
|||||
|
from .dropdown_menu import DropdownMenu |
||||
|
from .ctk_base_class import CTkBaseClass |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,325 @@ |
|||||
|
import sys |
||||
|
import warnings |
||||
|
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 |
||||
|
|
||||
|
# removed due to circular import |
||||
|
# from ...ctk_tk import CTk |
||||
|
# from ...ctk_toplevel import CTkToplevel |
||||
|
from .... import windows # import windows for isinstance checks |
||||
|
|
||||
|
from ..theme import ThemeManager |
||||
|
from ..font import CTkFont |
||||
|
from ..image import CTkImage |
||||
|
from ..appearance_mode import CTkAppearanceModeBaseClass |
||||
|
from ..scaling import CTkScalingBaseClass |
||||
|
|
||||
|
from ..utility 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, |
||||
|
width: int = 0, |
||||
|
height: int = 0, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
**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 == "transparent" else self._check_color_type(bg_color, transparency=True) |
||||
|
|
||||
|
# 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, tkinter.LabelFrame, ttk.Frame, ttk.LabelFrame, ttk.Notebook)) and not isinstance(self.master, CTkBaseClass): |
||||
|
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): |
||||
|
""" can be overridden but super method must be called """ |
||||
|
if no_color_updates is False: |
||||
|
# Configuring color of tkinter.Frame not necessary at the moment? |
||||
|
# Causes flickering on Windows and Linux for segmented button for some reason! |
||||
|
# super().configure(bg=self._apply_appearance_mode(self._bg_color)) |
||||
|
pass |
||||
|
|
||||
|
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 = self._check_color_type(kwargs.pop("bg_color"), transparency=True) |
||||
|
if new_bg_color == "transparent": |
||||
|
self._bg_color = self._detect_color_of_master() |
||||
|
else: |
||||
|
self._bg_color = self._check_color_type(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.") |
||||
|
|
||||
|
def _check_font_type(self, font: any): |
||||
|
""" check font type when passed to widget """ |
||||
|
if isinstance(font, CTkFont): |
||||
|
return font |
||||
|
|
||||
|
elif type(font) == tuple and len(font) == 1: |
||||
|
warnings.warn(f"{type(self).__name__} 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) <= 6: |
||||
|
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 to 6 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 _check_image_type(self, image: any): |
||||
|
""" check image type when passed to widget """ |
||||
|
if image is None: |
||||
|
return image |
||||
|
elif isinstance(image, CTkImage): |
||||
|
return image |
||||
|
else: |
||||
|
warnings.warn(f"{type(self).__name__} Warning: Given image is not CTkImage but {type(image)}. " + |
||||
|
f"Image can not be scaled on HighDPI displays, use CTkImage instead.\n") |
||||
|
return image |
||||
|
|
||||
|
def _update_dimensions_event(self, event): |
||||
|
# only redraw if dimensions changed (for performance), independent of scaling |
||||
|
if round(self._current_width) != round(self._reverse_widget_scaling(event.width)) or round(self._current_height) != round(self._reverse_widget_scaling(event.height)): |
||||
|
self._current_width = self._reverse_widget_scaling(event.width) # adjust current size according to new size given by event |
||||
|
self._current_height = self._reverse_widget_scaling(event.height) # _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 foreground color of master widget for bg_color and transparent color """ |
||||
|
|
||||
|
if master_widget is None: |
||||
|
master_widget = self.master |
||||
|
|
||||
|
if isinstance(master_widget, (windows.widgets.core_widget_classes.CTkBaseClass, windows.CTk, windows.CTkToplevel)): |
||||
|
if master_widget.cget("fg_color") is not None and master_widget.cget("fg_color") != "transparent": |
||||
|
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): |
||||
|
super()._set_appearance_mode(mode_string) |
||||
|
self._draw() |
||||
|
super().update_idletasks() |
||||
|
|
||||
|
def _set_scaling(self, new_widget_scaling, new_window_scaling): |
||||
|
super()._set_scaling(new_widget_scaling, new_window_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 bind(self, sequence=None, command=None, add=None): |
||||
|
raise NotImplementedError |
||||
|
|
||||
|
def unbind(self, sequence=None, funcid=None): |
||||
|
raise NotImplementedError |
||||
|
|
||||
|
def unbind_all(self, sequence): |
||||
|
raise AttributeError("'unbind_all' is not allowed, because it would delete necessary internal callbacks for all widgets") |
||||
|
|
||||
|
def bind_all(self, sequence=None, func=None, add=None): |
||||
|
raise AttributeError("'bind_all' is not allowed, could result in undefined behavior") |
||||
|
|
||||
|
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() |
@ -0,0 +1,198 @@ |
|||||
|
import tkinter |
||||
|
import sys |
||||
|
from typing import Union, Tuple, Callable, List, Optional |
||||
|
|
||||
|
from ..theme import ThemeManager |
||||
|
from ..font import CTkFont |
||||
|
from ..appearance_mode import CTkAppearanceModeBaseClass |
||||
|
from ..scaling import CTkScalingBaseClass |
||||
|
|
||||
|
|
||||
|
class DropdownMenu(tkinter.Menu, CTkAppearanceModeBaseClass, CTkScalingBaseClass): |
||||
|
def __init__(self, *args, |
||||
|
min_character_width: int = 18, |
||||
|
|
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
command: Union[Callable, None] = None, |
||||
|
values: Optional[List[str]] = None, |
||||
|
**kwargs): |
||||
|
|
||||
|
# call init methods of super classes |
||||
|
tkinter.Menu.__init__(self, *args, **kwargs) |
||||
|
CTkAppearanceModeBaseClass.__init__(self) |
||||
|
CTkScalingBaseClass.__init__(self, scaling_type="widget") |
||||
|
|
||||
|
self._min_character_width = min_character_width |
||||
|
self._fg_color = ThemeManager.theme["DropdownMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
self._hover_color = ThemeManager.theme["DropdownMenu"]["hover_color"] if hover_color is None else self._check_color_type(hover_color) |
||||
|
self._text_color = ThemeManager.theme["DropdownMenu"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
|
||||
|
# font |
||||
|
self._font = CTkFont() if font is None 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=self._apply_appearance_mode(self._hover_color), |
||||
|
borderwidth=self._apply_widget_scaling(4), |
||||
|
activeborderwidth=self._apply_widget_scaling(4), |
||||
|
bg=self._apply_appearance_mode(self._fg_color), |
||||
|
fg=self._apply_appearance_mode(self._text_color), |
||||
|
activeforeground=self._apply_appearance_mode(self._text_color), |
||||
|
font=self._apply_font_scaling(self._font), |
||||
|
cursor="hand2") |
||||
|
|
||||
|
else: |
||||
|
super().configure(tearoff=False, |
||||
|
relief="flat", |
||||
|
activebackground=self._apply_appearance_mode(self._hover_color), |
||||
|
borderwidth=0, |
||||
|
activeborderwidth=0, |
||||
|
bg=self._apply_appearance_mode(self._fg_color), |
||||
|
fg=self._apply_appearance_mode(self._text_color), |
||||
|
activeforeground=self._apply_appearance_mode(self._text_color), |
||||
|
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 = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
super().configure(bg=self._apply_appearance_mode(self._fg_color)) |
||||
|
|
||||
|
if "hover_color" in kwargs: |
||||
|
self._hover_color = self._check_color_type(kwargs.pop("hover_color")) |
||||
|
super().configure(activebackground=self._apply_appearance_mode(self._hover_color)) |
||||
|
|
||||
|
if "text_color" in kwargs: |
||||
|
self._text_color = self._check_color_type(kwargs.pop("text_color")) |
||||
|
super().configure(fg=self._apply_appearance_mode(self._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 "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) |
||||
|
|
||||
|
@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)} for font '{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): |
||||
|
super()._set_scaling(new_widget_scaling, new_window_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 """ |
||||
|
super()._set_appearance_mode(mode_string) |
||||
|
self._configure_menu_for_platforms() |
@ -0,0 +1,589 @@ |
|||||
|
import tkinter |
||||
|
import sys |
||||
|
from typing import Union, Tuple, Callable, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .font import CTkFont |
||||
|
from .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: int = 6 |
||||
|
|
||||
|
def __init__(self, |
||||
|
master: any, |
||||
|
width: int = 140, |
||||
|
height: int = 28, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width: Optional[int] = None, |
||||
|
border_spacing: int = 2, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None, |
||||
|
round_width_to_even_numbers: bool = True, |
||||
|
round_height_to_even_numbers: bool = True, |
||||
|
|
||||
|
text: str = "CTkButton", |
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
textvariable: Union[tkinter.Variable, None] = None, |
||||
|
image: Union[CTkImage, None] = None, |
||||
|
state: str = "normal", |
||||
|
hover: bool = True, |
||||
|
command: Union[Callable[[], None], None] = 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) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius: int = ThemeManager.theme["CTkButton"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._corner_radius = min(self._corner_radius, round(self._current_height / 2)) |
||||
|
self._border_width: int = ThemeManager.theme["CTkButton"]["border_width"] if border_width is None else border_width |
||||
|
self._border_spacing: int = border_spacing |
||||
|
|
||||
|
# color |
||||
|
self._fg_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) |
||||
|
self._hover_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color) |
||||
|
self._border_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["border_color"] if border_color is None else self._check_color_type(border_color) |
||||
|
self._text_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
self._text_color_disabled: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) |
||||
|
|
||||
|
# rendering options |
||||
|
self._background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = background_corner_colors # rendering options for DrawEngine |
||||
|
self._round_width_to_even_numbers: bool = round_width_to_even_numbers # rendering options for DrawEngine |
||||
|
self._round_height_to_even_numbers: bool = round_height_to_even_numbers # rendering options for DrawEngine |
||||
|
|
||||
|
# text, font |
||||
|
self._text = text |
||||
|
self._text_label: Union[tkinter.Label, None] = None |
||||
|
self._textvariable: tkinter.Variable = textvariable |
||||
|
self._font: Union[tuple, CTkFont] = CTkFont() if font is None else self._check_font_type(font) |
||||
|
if isinstance(self._font, CTkFont): |
||||
|
self._font.add_size_configure_callback(self._update_font) |
||||
|
|
||||
|
# image |
||||
|
self._image = self._check_image_type(image) |
||||
|
self._image_label: Union[tkinter.Label, None] = None |
||||
|
if isinstance(self._image, CTkImage): |
||||
|
self._image.add_configure_callback(self._update_image) |
||||
|
|
||||
|
# other |
||||
|
self._state: str = state |
||||
|
self._hover: bool = hover |
||||
|
self._command: Callable = command |
||||
|
self._compound: str = compound |
||||
|
self._anchor: str = anchor |
||||
|
self._click_animation_running: bool = False |
||||
|
|
||||
|
# canvas and draw engine |
||||
|
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 |
||||
|
|
||||
|
# configure cursor and initial draw |
||||
|
self._create_bindings() |
||||
|
self._set_cursor() |
||||
|
self._draw() |
||||
|
|
||||
|
def _create_bindings(self, sequence: Optional[str] = None): |
||||
|
""" set necessary bindings for functionality of widget, will overwrite other bindings """ |
||||
|
|
||||
|
if sequence is None or sequence == "<Enter>": |
||||
|
self._canvas.bind("<Enter>", self._on_enter) |
||||
|
|
||||
|
if self._text_label is not None: |
||||
|
self._text_label.bind("<Enter>", self._on_enter) |
||||
|
if self._image_label is not None: |
||||
|
self._image_label.bind("<Enter>", self._on_enter) |
||||
|
|
||||
|
if sequence is None or sequence == "<Leave>": |
||||
|
self._canvas.bind("<Leave>", self._on_leave) |
||||
|
|
||||
|
if self._text_label is not None: |
||||
|
self._text_label.bind("<Leave>", self._on_leave) |
||||
|
if self._image_label is not None: |
||||
|
self._image_label.bind("<Leave>", self._on_leave) |
||||
|
|
||||
|
if sequence is None or sequence == "<Button-1>": |
||||
|
self._canvas.bind("<Button-1>", self._clicked) |
||||
|
|
||||
|
if self._text_label is not None: |
||||
|
self._text_label.bind("<Button-1>", self._clicked) |
||||
|
if self._image_label is not None: |
||||
|
self._image_label.bind("<Button-1>", self._clicked) |
||||
|
|
||||
|
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._update_image() |
||||
|
|
||||
|
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_appearance_mode(self, mode_string): |
||||
|
super()._set_appearance_mode(mode_string) |
||||
|
self._update_image() |
||||
|
|
||||
|
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 _update_image(self): |
||||
|
if self._image_label is not None: |
||||
|
self._image_label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(), |
||||
|
self._get_appearance_mode())) |
||||
|
|
||||
|
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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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 == "transparent": |
||||
|
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._apply_appearance_mode(self._fg_color) == "transparent": |
||||
|
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._update_image() # set image |
||||
|
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._apply_appearance_mode(self._fg_color) == "transparent": |
||||
|
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)) |
||||
|
|
||||
|
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 = self._check_color_type(kwargs.pop("fg_color"), transparency=True) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "hover_color" in kwargs: |
||||
|
self._hover_color = self._check_color_type(kwargs.pop("hover_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_color" in kwargs: |
||||
|
self._border_color = self._check_color_type(kwargs.pop("border_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color" in kwargs: |
||||
|
self._text_color = self._check_color_type(kwargs.pop("text_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color_disabled" in kwargs: |
||||
|
self._text_color_disabled = self._check_color_type(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) |
||||
|
|
||||
|
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: |
||||
|
if isinstance(self._image, CTkImage): |
||||
|
self._image.remove_configure_callback(self._update_image) |
||||
|
self._image = self._check_image_type(kwargs.pop("image")) |
||||
|
if isinstance(self._image, CTkImage): |
||||
|
self._image.add_configure_callback(self._update_image) |
||||
|
self._update_image() |
||||
|
|
||||
|
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 == "transparent": |
||||
|
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._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) |
||||
|
|
||||
|
if self._command is not None: |
||||
|
self._command() |
||||
|
|
||||
|
def invoke(self): |
||||
|
""" calls command function if button is not disabled """ |
||||
|
if self._state != tkinter.DISABLED: |
||||
|
if self._command is not None: |
||||
|
return self._command() |
||||
|
|
||||
|
def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
|
||||
|
if self._text_label is not None: |
||||
|
self._text_label.bind(sequence, command, add=True) |
||||
|
if self._image_label is not None: |
||||
|
self._image_label.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence: str = None, funcid: str = None): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) |
||||
|
|
||||
|
if self._text_label is not None: |
||||
|
self._text_label.unbind(sequence, None) |
||||
|
if self._image_label is not None: |
||||
|
self._image_label.unbind(sequence, None) |
||||
|
|
||||
|
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence |
||||
|
|
||||
|
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() |
@ -0,0 +1,461 @@ |
|||||
|
import tkinter |
||||
|
import sys |
||||
|
from typing import Union, Tuple, Callable, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .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, |
||||
|
width: int = 100, |
||||
|
height: int = 24, |
||||
|
checkbox_width: int = 24, |
||||
|
checkbox_height: int = 24, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width: Optional[int] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
checkmark_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
text: str = "CTkCheckBox", |
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
textvariable: Union[tkinter.Variable, None] = None, |
||||
|
state: str = tkinter.NORMAL, |
||||
|
hover: bool = True, |
||||
|
command: Union[Callable[[], None], None] = None, |
||||
|
onvalue: Union[int, str] = 1, |
||||
|
offvalue: Union[int, str] = 0, |
||||
|
variable: Union[tkinter.Variable, None] = 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["CTkCheckbox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
self._hover_color = ThemeManager.theme["CTkCheckbox"]["hover_color"] if hover_color is None else self._check_color_type(hover_color) |
||||
|
self._border_color = ThemeManager.theme["CTkCheckbox"]["border_color"] if border_color is None else self._check_color_type(border_color) |
||||
|
self._checkmark_color = ThemeManager.theme["CTkCheckbox"]["checkmark_color"] if checkmark_color is None else self._check_color_type(checkmark_color) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkCheckbox"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_width = ThemeManager.theme["CTkCheckbox"]["border_width"] if border_width is None else border_width |
||||
|
|
||||
|
# text |
||||
|
self._text = text |
||||
|
self._text_label: Union[tkinter.Label, None] = None |
||||
|
self._text_color = ThemeManager.theme["CTkCheckbox"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
self._text_color_disabled = ThemeManager.theme["CTkCheckbox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) |
||||
|
|
||||
|
# font |
||||
|
self._font = CTkFont() if font is None 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._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" |
||||
|
|
||||
|
# 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._create_bindings() |
||||
|
self._set_cursor() |
||||
|
self._draw() |
||||
|
|
||||
|
def _create_bindings(self, sequence: Optional[str] = None): |
||||
|
""" set necessary bindings for functionality of widget, will overwrite other bindings """ |
||||
|
if sequence is None or sequence == "<Enter>": |
||||
|
self._canvas.bind("<Enter>", self._on_enter) |
||||
|
self._text_label.bind("<Enter>", self._on_enter) |
||||
|
if sequence is None or sequence == "<Leave>": |
||||
|
self._canvas.bind("<Leave>", self._on_leave) |
||||
|
self._text_label.bind("<Leave>", self._on_leave) |
||||
|
if sequence is None or sequence == "<Button-1>": |
||||
|
self._canvas.bind("<Button-1>", self.toggle) |
||||
|
self._text_label.bind("<Button-1>", self.toggle) |
||||
|
|
||||
|
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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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 "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 "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 = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "hover_color" in kwargs: |
||||
|
self._hover_color = self._check_color_type(kwargs.pop("hover_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color" in kwargs: |
||||
|
self._text_color = self._check_color_type(kwargs.pop("text_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_color" in kwargs: |
||||
|
self._border_color = self._check_color_type(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: str = None, command: Callable = None, add: Union[str, bool] = True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
self._text_label.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence: str = None, funcid: str = None): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) |
||||
|
self._text_label.unbind(sequence, None) |
||||
|
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence |
||||
|
|
||||
|
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() |
@ -0,0 +1,424 @@ |
|||||
|
import tkinter |
||||
|
import sys |
||||
|
import copy |
||||
|
from typing import Union, Tuple, Callable, List, Optional |
||||
|
|
||||
|
from .core_widget_classes import DropdownMenu |
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .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, |
||||
|
width: int = 140, |
||||
|
height: int = 28, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width: Optional[int] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
dropdown_text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
dropdown_font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
values: Optional[List[str]] = None, |
||||
|
state: str = tkinter.NORMAL, |
||||
|
hover: bool = True, |
||||
|
variable: Union[tkinter.Variable, None] = None, |
||||
|
command: Union[Callable[[str], None], None] = 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) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkComboBox"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_width = ThemeManager.theme["CTkComboBox"]["border_width"] if border_width is None else border_width |
||||
|
|
||||
|
# color |
||||
|
self._fg_color = ThemeManager.theme["CTkComboBox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
self._border_color = ThemeManager.theme["CTkComboBox"]["border_color"] if border_color is None else self._check_color_type(border_color) |
||||
|
self._button_color = ThemeManager.theme["CTkComboBox"]["button_color"] if button_color is None else self._check_color_type(button_color) |
||||
|
self._button_hover_color = ThemeManager.theme["CTkComboBox"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) |
||||
|
self._text_color = ThemeManager.theme["CTkComboBox"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
self._text_color_disabled = ThemeManager.theme["CTkComboBox"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) |
||||
|
|
||||
|
# font |
||||
|
self._font = CTkFont() if font is None 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() |
||||
|
self._create_bindings() |
||||
|
self._draw() # initial draw |
||||
|
|
||||
|
if self._variable is not None: |
||||
|
self._entry.configure(textvariable=self._variable) |
||||
|
|
||||
|
# insert default value |
||||
|
if self._variable is None: |
||||
|
if len(self._values) > 0: |
||||
|
self._entry.insert(0, self._values[0]) |
||||
|
else: |
||||
|
self._entry.insert(0, "CTkComboBox") |
||||
|
|
||||
|
def _create_bindings(self, sequence: Optional[str] = None): |
||||
|
""" set necessary bindings for functionality of widget, will overwrite other bindings """ |
||||
|
if sequence is None: |
||||
|
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) |
||||
|
|
||||
|
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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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._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)) |
||||
|
|
||||
|
self._entry.configure(bg=self._apply_appearance_mode(self._fg_color), |
||||
|
fg=self._apply_appearance_mode(self._text_color), |
||||
|
readonlybackground=self._apply_appearance_mode(self._fg_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 = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_color" in kwargs: |
||||
|
self._border_color = self._check_color_type(kwargs.pop("border_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_color" in kwargs: |
||||
|
self._button_color = self._check_color_type(kwargs.pop("button_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_hover_color" in kwargs: |
||||
|
self._button_hover_color = self._check_color_type(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 = self._check_color_type(kwargs.pop("text_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color_disabled" in kwargs: |
||||
|
self._text_color_disabled = self._check_color_type(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 copy.copy(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=None): |
||||
|
if self._state is not tkinter.DISABLED and len(self._values) > 0: |
||||
|
self._open_dropdown_menu() |
||||
|
|
||||
|
def bind(self, sequence=None, command=None, add=True): |
||||
|
""" called on the tkinter.Entry """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._entry.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence=None, funcid=None): |
||||
|
""" called on the tkinter.Entry """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._entry.unbind(sequence, None) # unbind all callbacks for sequence |
||||
|
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence |
||||
|
|
||||
|
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() |
@ -0,0 +1,384 @@ |
|||||
|
import tkinter |
||||
|
from typing import Union, Tuple, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .font import CTkFont |
||||
|
from .utility 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, |
||||
|
width: int = 140, |
||||
|
height: int = 28, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width: Optional[int] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
placeholder_text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
textvariable: Union[tkinter.Variable, None] = None, |
||||
|
placeholder_text: Union[str, None] = None, |
||||
|
font: Optional[Union[tuple, CTkFont]] = 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) |
||||
|
|
||||
|
# configure grid system (1x1) |
||||
|
self.grid_rowconfigure(0, weight=1) |
||||
|
self.grid_columnconfigure(0, weight=1) |
||||
|
|
||||
|
# color |
||||
|
self._fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) |
||||
|
self._text_color = ThemeManager.theme["CTkEntry"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
self._placeholder_text_color = ThemeManager.theme["CTkEntry"]["placeholder_text_color"] if placeholder_text_color is None else self._check_color_type(placeholder_text_color) |
||||
|
self._border_color = ThemeManager.theme["CTkEntry"]["border_color"] if border_color is None else self._check_color_type(border_color) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkEntry"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_width = ThemeManager.theme["CTkEntry"]["border_width"] if border_width is None 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 is None 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)) |
||||
|
|
||||
|
check_kwargs_empty(kwargs, raise_error=True) |
||||
|
|
||||
|
self._create_grid() |
||||
|
self._activate_placeholder() |
||||
|
self._create_bindings() |
||||
|
self._draw() |
||||
|
|
||||
|
def _create_bindings(self, sequence: Optional[str] = None): |
||||
|
""" set necessary bindings for functionality of widget, will overwrite other bindings """ |
||||
|
if sequence is None or sequence == "<FocusIn>": |
||||
|
self._entry.bind("<FocusIn>", self._entry_focus_in) |
||||
|
if sequence is None or sequence == "<FocusOut>": |
||||
|
self._entry.bind("<FocusOut>", self._entry_focus_out) |
||||
|
|
||||
|
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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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) == "transparent": |
||||
|
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), |
||||
|
disabledbackground=self._apply_appearance_mode(self._bg_color), |
||||
|
readonlybackground=self._apply_appearance_mode(self._bg_color), |
||||
|
highlightcolor=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._entry.configure(bg=self._apply_appearance_mode(self._fg_color), |
||||
|
disabledbackground=self._apply_appearance_mode(self._fg_color), |
||||
|
readonlybackground=self._apply_appearance_mode(self._fg_color), |
||||
|
highlightcolor=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)) |
||||
|
|
||||
|
if self._placeholder_text_active: |
||||
|
self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color), |
||||
|
disabledforeground=self._apply_appearance_mode(self._placeholder_text_color), |
||||
|
insertbackground=self._apply_appearance_mode(self._placeholder_text_color)) |
||||
|
else: |
||||
|
self._entry.config(fg=self._apply_appearance_mode(self._text_color), |
||||
|
disabledforeground=self._apply_appearance_mode(self._text_color), |
||||
|
insertbackground=self._apply_appearance_mode(self._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 = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color" in kwargs: |
||||
|
self._text_color = self._check_color_type(kwargs.pop("text_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "placeholder_text_color" in kwargs: |
||||
|
self._placeholder_text_color = self._check_color_type(kwargs.pop("placeholder_text_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_color" in kwargs: |
||||
|
self._border_color = self._check_color_type(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 "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=True): |
||||
|
""" called on the tkinter.Entry """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._entry.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence=None, funcid=None): |
||||
|
""" called on the tkinter.Entry """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._entry.unbind(sequence, None) # unbind all callbacks for sequence |
||||
|
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence |
||||
|
|
||||
|
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), |
||||
|
disabledforeground=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 and self._entry.cget("state") != "readonly": |
||||
|
self._placeholder_text_active = False |
||||
|
|
||||
|
self._entry.config(fg=self._apply_appearance_mode(self._text_color), |
||||
|
disabledforeground=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) |
@ -0,0 +1,196 @@ |
|||||
|
from typing import Union, Tuple, List, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes 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, |
||||
|
width: int = 200, |
||||
|
height: int = 200, |
||||
|
corner_radius: Optional[Union[int, str]] = None, |
||||
|
border_width: Optional[Union[int, str]] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None, |
||||
|
|
||||
|
overwrite_preferred_drawing_method: Union[str, None] = 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["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color) |
||||
|
|
||||
|
# determine fg_color of frame |
||||
|
if fg_color is None: |
||||
|
if isinstance(self.master, CTkFrame): |
||||
|
if self.master._fg_color == ThemeManager.theme["CTkFrame"]["fg_color"]: |
||||
|
self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"] |
||||
|
else: |
||||
|
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"] |
||||
|
else: |
||||
|
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"] |
||||
|
else: |
||||
|
self._fg_color = self._check_color_type(fg_color, transparency=True) |
||||
|
|
||||
|
self._background_corner_colors = background_corner_colors # rendering options for DrawEngine |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None 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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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 == "transparent": |
||||
|
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 = self._check_color_type(kwargs.pop("fg_color"), transparency=True) |
||||
|
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 "transparent" |
||||
|
if self._fg_color == "transparent": |
||||
|
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 = self._check_color_type(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=True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence=None, funcid=None): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) |
@ -0,0 +1,272 @@ |
|||||
|
import tkinter |
||||
|
from typing import Union, Tuple, Callable, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .font import CTkFont |
||||
|
from .image import CTkImage |
||||
|
from .utility 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 = {"cursor", "justify", "padx", "pady", |
||||
|
"textvariable", "state", "takefocus", "underline"} |
||||
|
|
||||
|
def __init__(self, |
||||
|
master: any, |
||||
|
width: int = 0, |
||||
|
height: int = 28, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
text: str = "CTkLabel", |
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
image: Union[CTkImage, None] = None, |
||||
|
compound: str = "center", |
||||
|
anchor: str = "center", # label anchor: center, n, e, s, w |
||||
|
wraplength: int = 0, |
||||
|
**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["CTkLabel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) |
||||
|
self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkLabel"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
|
||||
|
# text |
||||
|
self._anchor = anchor |
||||
|
self._text = text |
||||
|
self._wraplength = wraplength |
||||
|
|
||||
|
# image |
||||
|
self._image = self._check_image_type(image) |
||||
|
self._compound = compound |
||||
|
if isinstance(self._image, CTkImage): |
||||
|
self._image.add_configure_callback(self._update_image) |
||||
|
|
||||
|
# font |
||||
|
self._font = CTkFont() if font is None 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._label = tkinter.Label(master=self, |
||||
|
highlightthickness=0, |
||||
|
padx=0, |
||||
|
pady=0, |
||||
|
borderwidth=0, |
||||
|
anchor=self._anchor, |
||||
|
compound=self._compound, |
||||
|
wraplength=self._apply_widget_scaling(self._wraplength), |
||||
|
text=self._text, |
||||
|
font=self._apply_font_scaling(self._font)) |
||||
|
self._label.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_label_attributes)) |
||||
|
|
||||
|
check_kwargs_empty(kwargs, raise_error=True) |
||||
|
|
||||
|
self._create_grid() |
||||
|
self._update_image() |
||||
|
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._label.configure(font=self._apply_font_scaling(self._font)) |
||||
|
self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength)) |
||||
|
|
||||
|
self._create_grid() |
||||
|
self._update_image() |
||||
|
self._draw(no_color_updates=True) |
||||
|
|
||||
|
def _set_appearance_mode(self, mode_string): |
||||
|
super()._set_appearance_mode(mode_string) |
||||
|
self._update_image() |
||||
|
|
||||
|
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._create_grid() |
||||
|
self._draw() |
||||
|
|
||||
|
def _update_font(self): |
||||
|
""" pass font to tkinter widgets with applied font scaling and update grid with workaround """ |
||||
|
self._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 _update_image(self): |
||||
|
if isinstance(self._image, CTkImage): |
||||
|
self._label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(), |
||||
|
self._get_appearance_mode())) |
||||
|
elif self._image is not None: |
||||
|
self._label.configure(image=self._image) |
||||
|
|
||||
|
def destroy(self): |
||||
|
if isinstance(self._font, CTkFont): |
||||
|
self._font.remove_size_configure_callback(self._update_font) |
||||
|
super().destroy() |
||||
|
|
||||
|
def _create_grid(self): |
||||
|
""" configure grid system (1x1) """ |
||||
|
|
||||
|
text_label_grid_sticky = self._anchor if self._anchor != "center" else "" |
||||
|
self._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)))) |
||||
|
|
||||
|
def _draw(self, no_color_updates=False): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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) == "transparent": |
||||
|
self._canvas.itemconfig("inner_parts", |
||||
|
fill=self._apply_appearance_mode(self._bg_color), |
||||
|
outline=self._apply_appearance_mode(self._bg_color)) |
||||
|
|
||||
|
self._label.configure(fg=self._apply_appearance_mode(self._text_color), |
||||
|
bg=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._label.configure(fg=self._apply_appearance_mode(self._text_color), |
||||
|
bg=self._apply_appearance_mode(self._fg_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") |
||||
|
self._create_grid() |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "fg_color" in kwargs: |
||||
|
self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color" in kwargs: |
||||
|
self._text_color = self._check_color_type(kwargs.pop("text_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text" in kwargs: |
||||
|
self._text = kwargs.pop("text") |
||||
|
self._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 "image" in kwargs: |
||||
|
if isinstance(self._image, CTkImage): |
||||
|
self._image.remove_configure_callback(self._update_image) |
||||
|
self._image = self._check_image_type(kwargs.pop("image")) |
||||
|
if isinstance(self._image, CTkImage): |
||||
|
self._image.add_configure_callback(self._update_image) |
||||
|
self._update_image() |
||||
|
|
||||
|
if "compound" in kwargs: |
||||
|
self._compound = kwargs.pop("compound") |
||||
|
self._label.configure(compound=self._compound) |
||||
|
|
||||
|
if "anchor" in kwargs: |
||||
|
self._anchor = kwargs.pop("anchor") |
||||
|
self._label.configure(anchor=self._anchor) |
||||
|
self._create_grid() |
||||
|
|
||||
|
if "wraplength" in kwargs: |
||||
|
self._wraplength = kwargs.pop("wraplength") |
||||
|
self._label.configure(wraplength=self._apply_widget_scaling(self._wraplength)) |
||||
|
|
||||
|
self._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 == "image": |
||||
|
return self._image |
||||
|
elif attribute_name == "compound": |
||||
|
return self._compound |
||||
|
elif attribute_name == "anchor": |
||||
|
return self._anchor |
||||
|
elif attribute_name == "wraplength": |
||||
|
return self._wraplength |
||||
|
|
||||
|
elif attribute_name in self._valid_tk_label_attributes: |
||||
|
return self._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 = True): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
self._label.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence: str = None, funcid: Optional[str] = None): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) |
||||
|
self._label.unbind(sequence, None) |
||||
|
|
||||
|
def focus(self): |
||||
|
return self._label.focus() |
||||
|
|
||||
|
def focus_set(self): |
||||
|
return self._label.focus_set() |
||||
|
|
||||
|
def focus_force(self): |
||||
|
return self._label.focus_force() |
@ -0,0 +1,422 @@ |
|||||
|
import tkinter |
||||
|
import copy |
||||
|
import sys |
||||
|
from typing import Union, Tuple, Callable, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .core_widget_classes import DropdownMenu |
||||
|
from .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, |
||||
|
width: int = 140, |
||||
|
height: int = 28, |
||||
|
corner_radius: Optional[Union[int]] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
dropdown_fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
dropdown_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
dropdown_text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
dropdown_font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
values: Optional[list] = None, |
||||
|
variable: Union[tkinter.Variable, None] = None, |
||||
|
state: str = tkinter.NORMAL, |
||||
|
hover: bool = True, |
||||
|
command: Union[Callable[[str], None], 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["CTkOptionMenu"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
self._button_color = ThemeManager.theme["CTkOptionMenu"]["button_color"] if button_color is None else self._check_color_type(button_color) |
||||
|
self._button_hover_color = ThemeManager.theme["CTkOptionMenu"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkOptionMenu"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
|
||||
|
# text and font |
||||
|
self._text_color = ThemeManager.theme["CTkOptionMenu"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
self._text_color_disabled = ThemeManager.theme["CTkOptionMenu"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) |
||||
|
|
||||
|
# font |
||||
|
self._font = CTkFont() if font is None 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) |
||||
|
|
||||
|
if self._cursor_manipulation_enabled: |
||||
|
if sys.platform == "darwin": |
||||
|
self.configure(cursor="pointinghand") |
||||
|
elif sys.platform.startswith("win"): |
||||
|
self.configure(cursor="hand2") |
||||
|
|
||||
|
self._create_grid() |
||||
|
if not self._dynamic_resizing: |
||||
|
self.grid_propagate(0) |
||||
|
|
||||
|
self._create_bindings() |
||||
|
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_bindings(self, sequence: Optional[str] = None): |
||||
|
""" set necessary bindings for functionality of widget, will overwrite other bindings """ |
||||
|
if sequence is None or sequence == "<Enter>": |
||||
|
self._canvas.bind("<Enter>", self._on_enter) |
||||
|
self._text_label.bind("<Enter>", self._on_enter) |
||||
|
if sequence is None or sequence == "<Leave>": |
||||
|
self._canvas.bind("<Leave>", self._on_leave) |
||||
|
self._text_label.bind("<Leave>", self._on_leave) |
||||
|
if sequence is None or sequence == "<Button-1>": |
||||
|
self._canvas.bind("<Button-1>", self._clicked) |
||||
|
self._text_label.bind("<Button-1>", self._clicked) |
||||
|
|
||||
|
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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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 = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_color" in kwargs: |
||||
|
self._button_color = self._check_color_type(kwargs.pop("button_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_hover_color" in kwargs: |
||||
|
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color" in kwargs: |
||||
|
self._text_color = self._check_color_type(kwargs.pop("text_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 "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 copy.copy(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: Union[str, bool] = True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
self._text_label.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence: str = None, funcid: str = None): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) |
||||
|
self._text_label.unbind(sequence, None) |
||||
|
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence |
||||
|
|
||||
|
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() |
@ -0,0 +1,312 @@ |
|||||
|
import tkinter |
||||
|
import math |
||||
|
from typing import Union, Tuple, Optional, Callable |
||||
|
try: |
||||
|
from typing import Literal |
||||
|
except ImportError: |
||||
|
from typing_extensions import Literal |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes 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, |
||||
|
width: Optional[int] = None, |
||||
|
height: Optional[int] = None, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width: Optional[int] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
progress_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
variable: Union[tkinter.Variable, None] = None, |
||||
|
orientation: str = "horizontal", |
||||
|
mode: Literal["determinate", "indeterminate"] = "determinate", |
||||
|
determinate_speed: float = 1, |
||||
|
indeterminate_speed: float = 1, |
||||
|
**kwargs): |
||||
|
|
||||
|
# set default dimensions according to orientation |
||||
|
if width is None: |
||||
|
if orientation.lower() == "vertical": |
||||
|
width = 8 |
||||
|
else: |
||||
|
width = 200 |
||||
|
if height is None: |
||||
|
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["CTkProgressBar"]["border_color"] if border_color is None else self._check_color_type(border_color) |
||||
|
self._fg_color = ThemeManager.theme["CTkProgressBar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
self._progress_color = ThemeManager.theme["CTkProgressBar"]["progress_color"] if progress_color is None else self._check_color_type(progress_color) |
||||
|
|
||||
|
# control variable |
||||
|
self._variable = variable |
||||
|
self._variable_callback_blocked = False |
||||
|
self._variable_callback_name = None |
||||
|
self._loop_after_id = None |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkProgressBar"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_width = ThemeManager.theme["CTkProgressBar"]["border_width"] if border_width is None 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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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 "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 = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_color" in kwargs: |
||||
|
self._border_color = self._check_color_type(kwargs.pop("border_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "progress_color" in kwargs: |
||||
|
self._progress_color = self._check_color_type(kwargs.pop("progress_color")) |
||||
|
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 automatic mode """ |
||||
|
if not self._loop_running: |
||||
|
self._loop_running = True |
||||
|
self._internal_loop() |
||||
|
|
||||
|
def stop(self): |
||||
|
""" stop automatic mode """ |
||||
|
if self._loop_after_id is not None: |
||||
|
self.after_cancel(self._loop_after_id) |
||||
|
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._loop_after_id = self.after(20, self._internal_loop) |
||||
|
else: |
||||
|
self._indeterminate_value += self._indeterminate_speed |
||||
|
self._draw() |
||||
|
self._loop_after_id = self.after(20, self._internal_loop) |
||||
|
|
||||
|
def step(self): |
||||
|
""" increase progress """ |
||||
|
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: str = None, command: Callable = None, add: Union[str, bool] = True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence: str = None, funcid: str = None): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) |
||||
|
|
||||
|
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() |
@ -0,0 +1,430 @@ |
|||||
|
import tkinter |
||||
|
import sys |
||||
|
from typing import Union, Tuple, Callable, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .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, |
||||
|
width: int = 100, |
||||
|
height: int = 22, |
||||
|
radiobutton_width: int = 22, |
||||
|
radiobutton_height: int = 22, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width_unchecked: Optional[int] = None, |
||||
|
border_width_checked: Optional[int] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
text: str = "CTkRadioButton", |
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
textvariable: Union[tkinter.Variable, None] = None, |
||||
|
variable: Union[tkinter.Variable, None] = None, |
||||
|
value: Union[int, str] = 0, |
||||
|
state: str = tkinter.NORMAL, |
||||
|
hover: bool = True, |
||||
|
command: Union[Callable, None] = 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["CTkRadiobutton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
self._hover_color = ThemeManager.theme["CTkRadiobutton"]["hover_color"] if hover_color is None else self._check_color_type(hover_color) |
||||
|
self._border_color = ThemeManager.theme["CTkRadiobutton"]["border_color"] if border_color is None else self._check_color_type(border_color) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkRadiobutton"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_width_unchecked = ThemeManager.theme["CTkRadiobutton"]["border_width_unchecked"] if border_width_unchecked is None else border_width_unchecked |
||||
|
self._border_width_checked = ThemeManager.theme["CTkRadiobutton"]["border_width_checked"] if border_width_checked is None else border_width_checked |
||||
|
|
||||
|
# text |
||||
|
self._text = text |
||||
|
self._text_label: Union[tkinter.Label, None] = None |
||||
|
self._text_color = ThemeManager.theme["CTkRadiobutton"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
self._text_color_disabled = ThemeManager.theme["CTkRadiobutton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) |
||||
|
|
||||
|
# font |
||||
|
self._font = CTkFont() if font is None 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.grid_rowconfigure(0, 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._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" |
||||
|
|
||||
|
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._create_bindings() |
||||
|
self._set_cursor() |
||||
|
self._draw() |
||||
|
|
||||
|
def _create_bindings(self, sequence: Optional[str] = None): |
||||
|
""" set necessary bindings for functionality of widget, will overwrite other bindings """ |
||||
|
if sequence is None or sequence == "<Enter>": |
||||
|
self._canvas.bind("<Enter>", self._on_enter) |
||||
|
self._text_label.bind("<Enter>", self._on_enter) |
||||
|
if sequence is None or sequence == "<Leave>": |
||||
|
self._canvas.bind("<Leave>", self._on_leave) |
||||
|
self._text_label.bind("<Leave>", self._on_leave) |
||||
|
if sequence is None or sequence == "<Button-1>": |
||||
|
self._canvas.bind("<Button-1>", self.invoke) |
||||
|
self._text_label.bind("<Button-1>", self.invoke) |
||||
|
|
||||
|
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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
if self._check_state is True: |
||||
|
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_checked)) |
||||
|
else: |
||||
|
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_unchecked)) |
||||
|
|
||||
|
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 "corner_radius" in kwargs: |
||||
|
self._corner_radius = kwargs.pop("corner_radius") |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_width_unchecked" in kwargs: |
||||
|
self._border_width_unchecked = kwargs.pop("border_width_unchecked") |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_width_checked" in kwargs: |
||||
|
self._border_width_checked = kwargs.pop("border_width_checked") |
||||
|
require_redraw = True |
||||
|
|
||||
|
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 = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "hover_color" in kwargs: |
||||
|
self._hover_color = self._check_color_type(kwargs.pop("hover_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color" in kwargs: |
||||
|
self._text_color = self._check_color_type(kwargs.pop("text_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color_disabled" in kwargs: |
||||
|
self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_color" in kwargs: |
||||
|
self._border_color = self._check_color_type(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: |
||||
|
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._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._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: str = None, command: Callable = None, add: Union[str, bool] = True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
self._text_label.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence: str = None, funcid: str = None): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) |
||||
|
self._text_label.unbind(sequence, None) |
||||
|
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence |
||||
|
|
||||
|
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() |
@ -0,0 +1,281 @@ |
|||||
|
import sys |
||||
|
from typing import Union, Tuple, Callable, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes 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, |
||||
|
width: Optional[Union[int, str]] = None, |
||||
|
height: Optional[Union[int, str]] = None, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_spacing: Optional[int] = None, |
||||
|
minimum_pixel_length: int = 20, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
hover: bool = True, |
||||
|
command: Union[Callable, None] = None, |
||||
|
orientation: str = "vertical", |
||||
|
**kwargs): |
||||
|
|
||||
|
# set default dimensions according to orientation |
||||
|
if width is None: |
||||
|
if orientation.lower() == "vertical": |
||||
|
width = 16 |
||||
|
else: |
||||
|
width = 200 |
||||
|
if height is None: |
||||
|
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["CTkScrollbar"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) |
||||
|
self._button_color = ThemeManager.theme["CTkScrollbar"]["button_color"] if button_color is None else self._check_color_type(button_color) |
||||
|
self._button_hover_color = ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkScrollbar"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_spacing = ThemeManager.theme["CTkScrollbar"]["border_spacing"] if border_spacing is None 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._create_bindings() |
||||
|
self._draw() |
||||
|
|
||||
|
def _create_bindings(self, sequence: Optional[str] = None): |
||||
|
""" set necessary bindings for functionality of widget, will overwrite other bindings """ |
||||
|
if sequence is None: |
||||
|
self._canvas.tag_bind("border_parts", "<Button-1>", self._clicked) |
||||
|
if sequence is None or sequence == "<Enter>": |
||||
|
self._canvas.bind("<Enter>", self._on_enter) |
||||
|
if sequence is None or sequence == "<Leave>": |
||||
|
self._canvas.bind("<Leave>", self._on_leave) |
||||
|
if sequence is None or sequence == "<B1-Motion>": |
||||
|
self._canvas.bind("<B1-Motion>", self._clicked) |
||||
|
if sequence is None or sequence == "<MouseWheel>": |
||||
|
self._canvas.bind("<MouseWheel>", self._mouse_scroll_event) |
||||
|
|
||||
|
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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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._button_hover_color), |
||||
|
outline=self._apply_appearance_mode(self._button_hover_color)) |
||||
|
else: |
||||
|
self._canvas.itemconfig("scrollbar_parts", |
||||
|
fill=self._apply_appearance_mode(self._button_color), |
||||
|
outline=self._apply_appearance_mode(self._button_color)) |
||||
|
|
||||
|
if self._fg_color == "transparent": |
||||
|
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 = self._check_color_type(kwargs.pop("fg_color"), transparency=True) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_color" in kwargs: |
||||
|
self._button_color = self._check_color_type(kwargs.pop("button_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_hover_color" in kwargs: |
||||
|
self._button_hover_color = self._check_color_type(kwargs.pop("button_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._button_color |
||||
|
elif attribute_name == "scrollbar_hover_color": |
||||
|
return self._button_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._button_hover_color), |
||||
|
fill=self._apply_appearance_mode(self._button_hover_color)) |
||||
|
|
||||
|
def _on_leave(self, event=0): |
||||
|
self._hover_state = False |
||||
|
self._canvas.itemconfig("scrollbar_parts", |
||||
|
outline=self._apply_appearance_mode(self._button_color), |
||||
|
fill=self._apply_appearance_mode(self._button_color)) |
||||
|
|
||||
|
def _clicked(self, event): |
||||
|
if self._orientation == "vertical": |
||||
|
value = self._reverse_widget_scaling(((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing))) |
||||
|
else: |
||||
|
value = self._reverse_widget_scaling(((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing))) |
||||
|
|
||||
|
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=True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence=None, funcid=None): |
||||
|
""" called on the tkinter.Canvas, restores internal callbacks """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) # unbind all callbacks for sequence |
||||
|
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence |
||||
|
|
||||
|
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() |
@ -0,0 +1,421 @@ |
|||||
|
import tkinter |
||||
|
import copy |
||||
|
from typing import Union, Tuple, List, Dict, Callable, Optional |
||||
|
try: |
||||
|
from typing import Literal |
||||
|
except ImportError: |
||||
|
from typing_extensions import Literal |
||||
|
|
||||
|
from .theme import ThemeManager |
||||
|
from .font import CTkFont |
||||
|
from .ctk_button import CTkButton |
||||
|
from .ctk_frame import CTkFrame |
||||
|
|
||||
|
|
||||
|
class CTkSegmentedButton(CTkFrame): |
||||
|
""" |
||||
|
Segmented button with corner radius, border width, variable support. |
||||
|
For detailed information check out the documentation. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, |
||||
|
master: any, |
||||
|
width: int = 140, |
||||
|
height: int = 28, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width: int = 3, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
selected_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
unselected_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
unselected_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
background_corner_colors: Union[Tuple[Union[str, Tuple[str, str]]], None] = None, |
||||
|
|
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
values: Optional[list] = None, |
||||
|
variable: Union[tkinter.Variable, None] = None, |
||||
|
dynamic_resizing: bool = True, |
||||
|
command: Union[Callable[[str], None], 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["CTkSegmentedButton"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
|
||||
|
self._sb_selected_color = ThemeManager.theme["CTkSegmentedButton"]["selected_color"] if selected_color is None else self._check_color_type(selected_color) |
||||
|
self._sb_selected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["selected_hover_color"] if selected_hover_color is None else self._check_color_type(selected_hover_color) |
||||
|
|
||||
|
self._sb_unselected_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_color"] if unselected_color is None else self._check_color_type(unselected_color) |
||||
|
self._sb_unselected_hover_color = ThemeManager.theme["CTkSegmentedButton"]["unselected_hover_color"] if unselected_hover_color is None else self._check_color_type(unselected_hover_color) |
||||
|
|
||||
|
self._sb_text_color = ThemeManager.theme["CTkSegmentedButton"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
self._sb_text_color_disabled = ThemeManager.theme["CTkSegmentedButton"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) |
||||
|
|
||||
|
self._sb_corner_radius = ThemeManager.theme["CTkSegmentedButton"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._sb_border_width = ThemeManager.theme["CTkSegmentedButton"]["border_width"] if border_width is None else border_width |
||||
|
|
||||
|
self._background_corner_colors = background_corner_colors # rendering options for DrawEngine |
||||
|
|
||||
|
self._command: Callable[[str], None] = command |
||||
|
self._font = CTkFont() if font is None 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="transparent") |
||||
|
|
||||
|
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, |
||||
|
width=0, |
||||
|
height=self._current_height, |
||||
|
corner_radius=self._sb_corner_radius, |
||||
|
border_width=self._sb_border_width, |
||||
|
fg_color=self._sb_unselected_color, |
||||
|
border_color=self._sb_fg_color, |
||||
|
hover_color=self._sb_unselected_hover_color, |
||||
|
text_color=self._sb_text_color, |
||||
|
text_color_disabled=self._sb_text_color_disabled, |
||||
|
text=value, |
||||
|
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, |
||||
|
round_height_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 = self._check_color_type(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 = self._check_color_type(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 = self._check_color_type(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 = self._check_color_type(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 = self._check_color_type(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 = self._check_color_type(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 = self._check_color_type(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 copy.copy(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: |
||||
|
if value != "": |
||||
|
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 ''") |
||||
|
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}'") |
||||
|
|
||||
|
def bind(self, sequence=None, command=None, add=None): |
||||
|
raise NotImplementedError |
||||
|
|
||||
|
def unbind(self, sequence=None, funcid=None): |
||||
|
raise NotImplementedError |
||||
|
|
@ -0,0 +1,397 @@ |
|||||
|
import tkinter |
||||
|
import sys |
||||
|
from typing import Union, Tuple, Callable, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes 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, |
||||
|
width: Optional[int] = None, |
||||
|
height: Optional[int] = None, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
button_corner_radius: Optional[int] = None, |
||||
|
border_width: Optional[int] = None, |
||||
|
button_length: Optional[int] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
progress_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
from_: int = 0, |
||||
|
to: int = 1, |
||||
|
state: str = "normal", |
||||
|
number_of_steps: Union[int, None] = None, |
||||
|
hover: bool = True, |
||||
|
command: Union[Callable[[float], None], None] = None, |
||||
|
variable: Union[tkinter.Variable, None] = None, |
||||
|
orientation: str = "horizontal", |
||||
|
**kwargs): |
||||
|
|
||||
|
# set default dimensions according to orientation |
||||
|
if width is None: |
||||
|
if orientation.lower() == "vertical": |
||||
|
width = 16 |
||||
|
else: |
||||
|
width = 200 |
||||
|
if height is None: |
||||
|
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 = self._check_color_type(border_color, transparency=True) |
||||
|
self._fg_color = ThemeManager.theme["CTkSlider"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
self._progress_color = ThemeManager.theme["CTkSlider"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True) |
||||
|
self._button_color = ThemeManager.theme["CTkSlider"]["button_color"] if button_color is None else self._check_color_type(button_color) |
||||
|
self._button_hover_color = ThemeManager.theme["CTkSlider"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkSlider"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._button_corner_radius = ThemeManager.theme["CTkSlider"]["button_corner_radius"] if button_corner_radius is None else button_corner_radius |
||||
|
self._border_width = ThemeManager.theme["CTkSlider"]["border_width"] if border_width is None else border_width |
||||
|
self._button_length = ThemeManager.theme["CTkSlider"]["button_length"] if button_length is None 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._create_bindings() |
||||
|
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 _create_bindings(self, sequence: Optional[str] = None): |
||||
|
""" set necessary bindings for functionality of widget, will overwrite other bindings """ |
||||
|
if sequence is None or sequence == "<Enter>": |
||||
|
self._canvas.bind("<Enter>", self._on_enter) |
||||
|
if sequence is None or sequence == "<Leave>": |
||||
|
self._canvas.bind("<Leave>", self._on_leave) |
||||
|
if sequence is None or sequence == "<Button-1>": |
||||
|
self._canvas.bind("<Button-1>", self._clicked) |
||||
|
if sequence is None or sequence == "<B1-Motion>": |
||||
|
self._canvas.bind("<B1-Motion>", self._clicked) |
||||
|
|
||||
|
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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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 == "transparent": |
||||
|
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 == "transparent": |
||||
|
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 = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "progress_color" in kwargs: |
||||
|
self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_color" in kwargs: |
||||
|
self._button_color = self._check_color_type(kwargs.pop("button_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_hover_color" in kwargs: |
||||
|
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_color" in kwargs: |
||||
|
self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True) |
||||
|
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 = self._reverse_widget_scaling(event.x / self._current_width) |
||||
|
else: |
||||
|
self._value = 1 - self._reverse_widget_scaling(event.y / self._current_height) |
||||
|
|
||||
|
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: str = None, command: Callable = None, add: Union[str, bool] = True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence: str = None, funcid: str = None): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) |
||||
|
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence |
||||
|
|
||||
|
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() |
@ -0,0 +1,475 @@ |
|||||
|
import tkinter |
||||
|
import sys |
||||
|
from typing import Union, Tuple, Callable, Optional |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .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, |
||||
|
width: int = 100, |
||||
|
height: int = 24, |
||||
|
switch_width: int = 36, |
||||
|
switch_height: int = 18, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width: Optional[int] = None, |
||||
|
button_length: Optional[int] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
progress_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
text: str = "CTkSwitch", |
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
textvariable: Union[tkinter.Variable, None] = None, |
||||
|
onvalue: Union[int, str] = 1, |
||||
|
offvalue: Union[int, str] = 0, |
||||
|
variable: Union[tkinter.Variable, None] = None, |
||||
|
hover: bool = True, |
||||
|
command: Union[Callable, None] = 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 = self._check_color_type(border_color, transparency=True) |
||||
|
self._fg_color = ThemeManager.theme["CTkSwitch"]["fg_Color"] if fg_color is None else self._check_color_type(fg_color) |
||||
|
self._progress_color = ThemeManager.theme["CTkSwitch"]["progress_color"] if progress_color is None else self._check_color_type(progress_color, transparency=True) |
||||
|
self._button_color = ThemeManager.theme["CTkSwitch"]["button_color"] if button_color is None else self._check_color_type(button_color) |
||||
|
self._button_hover_color = ThemeManager.theme["CTkSwitch"]["button_hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) |
||||
|
self._text_color = ThemeManager.theme["CTkSwitch"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
self._text_color_disabled = ThemeManager.theme["CTkSwitch"]["text_color_disabled"] if text_color_disabled is None else self._check_color_type(text_color_disabled) |
||||
|
|
||||
|
# text |
||||
|
self._text = text |
||||
|
self._text_label = None |
||||
|
|
||||
|
# font |
||||
|
self._font = CTkFont() if font is None 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["CTkSwitch"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_width = ThemeManager.theme["CTkSwitch"]["border_width"] if border_width is None else border_width |
||||
|
self._button_length = ThemeManager.theme["CTkSwitch"]["button_length"] if button_length is None 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 = 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.grid_rowconfigure(0, 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._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" |
||||
|
|
||||
|
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._create_bindings() |
||||
|
self._set_cursor() |
||||
|
self._draw() # initial draw |
||||
|
|
||||
|
def _create_bindings(self, sequence: Optional[str] = None): |
||||
|
""" set necessary bindings for functionality of widget, will overwrite other bindings """ |
||||
|
if sequence is None or sequence == "<Enter>": |
||||
|
self._canvas.bind("<Enter>", self._on_enter) |
||||
|
self._text_label.bind("<Enter>", self._on_enter) |
||||
|
if sequence is None or sequence == "<Leave>": |
||||
|
self._canvas.bind("<Leave>", self._on_leave) |
||||
|
self._text_label.bind("<Leave>", self._on_leave) |
||||
|
if sequence is None or sequence == "<Button-1>": |
||||
|
self._canvas.bind("<Button-1>", self.toggle) |
||||
|
self._text_label.bind("<Button-1>", self.toggle) |
||||
|
|
||||
|
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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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 == "transparent": |
||||
|
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 == "transparent": |
||||
|
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 "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 "button_length" in kwargs: |
||||
|
self._button_length = kwargs.pop("button_length") |
||||
|
require_redraw = True |
||||
|
|
||||
|
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 = self._check_color_type(kwargs.pop("fg_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "progress_color" in kwargs: |
||||
|
self._progress_color = self._check_color_type(kwargs.pop("progress_color"), transparency=True) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_color" in kwargs: |
||||
|
self._button_color = self._check_color_type(kwargs.pop("button_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "button_hover_color" in kwargs: |
||||
|
self._button_hover_color = self._check_color_type(kwargs.pop("button_hover_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "border_color" in kwargs: |
||||
|
self._border_color = self._check_color_type(kwargs.pop("border_color"), transparency=True) |
||||
|
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: str = None, command: Callable = None, add: Union[str, bool] = True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._canvas.bind(sequence, command, add=True) |
||||
|
self._text_label.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence: str = None, funcid: str = None): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._canvas.unbind(sequence, None) |
||||
|
self._text_label.unbind(sequence, None) |
||||
|
self._create_bindings(sequence=sequence) # restore internal callbacks for sequence |
||||
|
|
||||
|
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() |
@ -0,0 +1,370 @@ |
|||||
|
import tkinter |
||||
|
from typing import Union, Tuple, Dict, List, Callable, Optional |
||||
|
|
||||
|
from .theme import ThemeManager |
||||
|
from .ctk_frame import CTkFrame |
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .ctk_segmented_button import CTkSegmentedButton |
||||
|
|
||||
|
|
||||
|
class CTkTabview(CTkBaseClass): |
||||
|
""" |
||||
|
Tabview... |
||||
|
For detailed information check out the documentation. |
||||
|
""" |
||||
|
|
||||
|
_top_spacing: int = 10 # px on top of the buttons |
||||
|
_top_button_overhang: int = 8 # px |
||||
|
_button_height: int = 26 |
||||
|
_segmented_button_border_width: int = 3 |
||||
|
|
||||
|
def __init__(self, |
||||
|
master: any, |
||||
|
width: int = 300, |
||||
|
height: int = 250, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width: Optional[int] = None, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
segmented_button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
segmented_button_selected_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
segmented_button_selected_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
segmented_button_unselected_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
segmented_button_unselected_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
text_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
command: Union[Callable, None] = 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["CTkFrame"]["border_color"] if border_color is None else self._check_color_type(border_color) |
||||
|
|
||||
|
# determine fg_color of frame |
||||
|
if fg_color is None: |
||||
|
if isinstance(self.master, (CTkFrame, CTkTabview)): |
||||
|
if self.master.cget("fg_color") == ThemeManager.theme["CTkFrame"]["fg_color"]: |
||||
|
self._fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"] |
||||
|
else: |
||||
|
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"] |
||||
|
else: |
||||
|
self._fg_color = ThemeManager.theme["CTkFrame"]["fg_color"] |
||||
|
else: |
||||
|
self._fg_color = self._check_color_type(fg_color, transparency=True) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkFrame"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_width = ThemeManager.theme["CTkFrame"]["border_width"] if border_width is None else border_width |
||||
|
|
||||
|
self._canvas = CTkCanvas(master=self, |
||||
|
bg=self._apply_appearance_mode(self._bg_color), |
||||
|
highlightthickness=0, |
||||
|
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_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._segmented_button_border_width, |
||||
|
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._configure_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._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 """ |
||||
|
|
||||
|
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, |
||||
|
height=0, |
||||
|
width=0, |
||||
|
fg_color=self._fg_color, |
||||
|
border_width=0, |
||||
|
corner_radius=self._corner_radius) |
||||
|
return new_tab |
||||
|
|
||||
|
def _draw(self, no_color_updates: bool = False): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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 == "transparent": |
||||
|
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)) |
||||
|
tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self._bg_color)) # configure bg color of tkinter.Frame, cuase canvas does not fill frame |
||||
|
|
||||
|
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 = self._check_color_type(kwargs.pop("fg_color"), transparency=True) |
||||
|
require_redraw = True |
||||
|
if "border_color" in kwargs: |
||||
|
self._border_color = self._check_color_type(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 |
@ -0,0 +1,500 @@ |
|||||
|
import tkinter |
||||
|
from typing import Union, Tuple, Optional, Callable |
||||
|
|
||||
|
from .core_rendering import CTkCanvas |
||||
|
from .ctk_scrollbar import CTkScrollbar |
||||
|
from .theme import ThemeManager |
||||
|
from .core_rendering import DrawEngine |
||||
|
from .core_widget_classes import CTkBaseClass |
||||
|
from .font import CTkFont |
||||
|
from .utility 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, |
||||
|
width: int = 200, |
||||
|
height: int = 200, |
||||
|
corner_radius: Optional[int] = None, |
||||
|
border_width: Optional[int] = None, |
||||
|
border_spacing: int = 3, |
||||
|
|
||||
|
bg_color: Union[str, Tuple[str, str]] = "transparent", |
||||
|
fg_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
border_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
text_color: Optional[Union[str, str]] = None, |
||||
|
scrollbar_button_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
scrollbar_button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, |
||||
|
|
||||
|
font: Optional[Union[tuple, CTkFont]] = None, |
||||
|
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["CTkTextbox"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) |
||||
|
self._border_color = ThemeManager.theme["CTkTextbox"]["border_color"] if border_color is None else self._check_color_type(border_color) |
||||
|
self._text_color = ThemeManager.theme["CTkTextbox"]["text_color"] if text_color is None else self._check_color_type(text_color) |
||||
|
self._scrollbar_button_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_color"] if scrollbar_button_color is None else self._check_color_type(scrollbar_button_color) |
||||
|
self._scrollbar_button_hover_color = ThemeManager.theme["CTkTextbox"]["scrollbar_button_hover_color"] if scrollbar_button_hover_color is None else self._check_color_type(scrollbar_button_hover_color) |
||||
|
|
||||
|
# shape |
||||
|
self._corner_radius = ThemeManager.theme["CTkTextbox"]["corner_radius"] if corner_radius is None else corner_radius |
||||
|
self._border_width = ThemeManager.theme["CTkTextbox"]["border_width"] if border_width is None else border_width |
||||
|
self._border_spacing = border_spacing |
||||
|
|
||||
|
# font |
||||
|
self._font = CTkFont() if font is None 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._desired_width), |
||||
|
height=self._apply_widget_scaling(self._desired_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), |
||||
|
**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, |
||||
|
button_color=self._scrollbar_button_color, |
||||
|
button_hover_color=self._scrollbar_button_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, |
||||
|
button_color=self._scrollbar_button_color, |
||||
|
button_hover_color=self._scrollbar_button_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, None, True) |
||||
|
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 = False): |
||||
|
""" 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=True, 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): |
||||
|
super()._draw(no_color_updates) |
||||
|
|
||||
|
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 == "transparent": |
||||
|
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, button_color=self._scrollbar_button_color, |
||||
|
button_hover_color=self._scrollbar_button_hover_color) |
||||
|
self._y_scrollbar.configure(fg_color=self._bg_color, button_color=self._scrollbar_button_color, |
||||
|
button_hover_color=self._scrollbar_button_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, button_color=self._scrollbar_button_color, |
||||
|
button_hover_color=self._scrollbar_button_hover_color) |
||||
|
self._y_scrollbar.configure(fg_color=self._fg_color, button_color=self._scrollbar_button_color, |
||||
|
button_hover_color=self._scrollbar_button_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 = self._check_color_type(kwargs.pop("fg_color"), transparency=True) |
||||
|
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 = self._check_color_type(kwargs.pop("border_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "text_color" in kwargs: |
||||
|
self._text_color = self._check_color_type(kwargs.pop("text_color")) |
||||
|
require_redraw = True |
||||
|
|
||||
|
if "scrollbar_button_color" in kwargs: |
||||
|
self._scrollbar_button_color = self._check_color_type(kwargs.pop("scrollbar_button_color")) |
||||
|
self._x_scrollbar.configure(button_color=self._scrollbar_button_color) |
||||
|
self._y_scrollbar.configure(button_color=self._scrollbar_button_color) |
||||
|
|
||||
|
if "scrollbar_button_hover_color" in kwargs: |
||||
|
self._scrollbar_button_hover_color = self._check_color_type(kwargs.pop("scrollbar_button_hover_color")) |
||||
|
self._x_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color) |
||||
|
self._y_scrollbar.configure(button_hover_color=self._scrollbar_button_hover_color) |
||||
|
|
||||
|
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: str = None, command: Callable = None, add: Union[str, bool] = True): |
||||
|
""" called on the tkinter.Canvas """ |
||||
|
if not (add == "+" or add is True): |
||||
|
raise ValueError("'add' argument can only be '+' or True to preserve internal callbacks") |
||||
|
self._textbox.bind(sequence, command, add=True) |
||||
|
|
||||
|
def unbind(self, sequence: str = None, funcid: str = None): |
||||
|
""" called on the tkinter.Label and tkinter.Canvas """ |
||||
|
if funcid is not None: |
||||
|
raise ValueError("'funcid' argument can only be None, because there is a bug in" + |
||||
|
" tkinter and its not clear whether the internal callbacks will be unbinded or not") |
||||
|
self._textbox.unbind(sequence, None) |
||||
|
|
||||
|
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): |
||||
|
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,0 +1,24 @@ |
|||||
|
import os |
||||
|
import sys |
||||
|
|
||||
|
from .ctk_font import CTkFont |
||||
|
from .font_manager import FontManager |
||||
|
|
||||
|
# import DrawEngine to set preferred_drawing_method if loading shapes font fails |
||||
|
from ..core_rendering import DrawEngine |
||||
|
|
||||
|
FontManager.init_font_manager() |
||||
|
|
||||
|
# load Roboto fonts (used on Windows/Linux) |
||||
|
customtkinter_directory = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) |
||||
|
FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf")) |
||||
|
FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf")) |
||||
|
|
||||
|
# load font necessary for rendering the widgets (used on Windows/Linux) |
||||
|
if FontManager.load_font(os.path.join(customtkinter_directory, "assets", "fonts", "CustomTkinter_shapes_font.otf")) is False: |
||||
|
# change draw method if font loading failed |
||||
|
if DrawEngine.preferred_drawing_method == "font_shapes": |
||||
|
sys.stderr.write("customtkinter.windows.widgets.font warning: " + |
||||
|
"Preferred drawing method 'font_shapes' can not be used because the font file could not be loaded.\n" + |
||||
|
"Using 'circle_shapes' instead. The rendering quality will be bad!\n") |
||||
|
DrawEngine.preferred_drawing_method = "circle_shapes" |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,91 @@ |
|||||
|
from tkinter.font import Font |
||||
|
import copy |
||||
|
from typing import List, Callable, Tuple, Optional |
||||
|
try: |
||||
|
from typing import Literal |
||||
|
except ImportError: |
||||
|
from typing_extensions import Literal |
||||
|
|
||||
|
from ..theme 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: Optional[str] = None, |
||||
|
size: Optional[int] = None, |
||||
|
weight: Literal["normal", "bold"] = None, |
||||
|
slant: Literal["italic", "roman"] = "roman", |
||||
|
underline: bool = False, |
||||
|
overstrike: bool = False): |
||||
|
|
||||
|
self._size_configure_callback_list: List[Callable] = [] |
||||
|
|
||||
|
self._size = ThemeManager.theme["CTkFont"]["size"] if size is None else size |
||||
|
|
||||
|
super().__init__(family=ThemeManager.theme["CTkFont"]["family"] if family is None else family, |
||||
|
size=-abs(self._size), |
||||
|
weight=ThemeManager.theme["CTkFont"]["weight"] if weight is None else weight, |
||||
|
slant=slant, |
||||
|
underline=underline, |
||||
|
overstrike=overstrike) |
||||
|
|
||||
|
self._family = super().cget("family") |
||||
|
self._tuple_style_string = f"{super().cget('weight')} {slant} {'underline' if underline else ''} {'overstrike' if overstrike else ''}" |
||||
|
|
||||
|
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(-abs(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)) |
||||
|
|
||||
|
if "family" in kwargs: |
||||
|
super().configure(family=kwargs.pop("family")) |
||||
|
self._family = super().cget("family") |
||||
|
|
||||
|
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 |
||||
|
if attribute_name == "family": |
||||
|
return self._family |
||||
|
else: |
||||
|
return super().cget(attribute_name) |
||||
|
|
||||
|
def copy(self) -> "CTkFont": |
||||
|
return copy.deepcopy(self) |
@ -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,0 +1 @@ |
|||||
|
from .ctk_image import CTkImage |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,122 @@ |
|||||
|
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] = (20, 20)): |
||||
|
|
||||
|
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): |
||||
|
try: |
||||
|
_, _ = Image, ImageTk |
||||
|
except NameError: |
||||
|
raise ImportError("PIL.Image and PIL.ImageTk couldn't be imported") |
||||
|
|
||||
|
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[1] * 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: str) -> "ImageTk.PhotoImage": |
||||
|
scaled_size = self._get_scaled_size(widget_scaling) |
||||
|
|
||||
|
if appearance_mode == "light" and self._light_image is not None: |
||||
|
return self._get_scaled_light_photo_image(scaled_size) |
||||
|
elif appearance_mode == "light" and self._light_image is None: |
||||
|
return self._get_scaled_dark_photo_image(scaled_size) |
||||
|
|
||||
|
elif appearance_mode == "dark" and self._dark_image is not None: |
||||
|
return self._get_scaled_dark_photo_image(scaled_size) |
||||
|
elif appearance_mode == "dark" and self._dark_image is None: |
||||
|
return self._get_scaled_light_photo_image(scaled_size) |
||||
|
|
||||
|
|
@ -0,0 +1,7 @@ |
|||||
|
import sys |
||||
|
|
||||
|
from .scaling_base_class import CTkScalingBaseClass |
||||
|
from .scaling_tracker import ScalingTracker |
||||
|
|
||||
|
if sys.platform.startswith("win") and sys.getwindowsversion().build < 9000: # No automatic scaling on Windows < 8.1 |
||||
|
ScalingTracker.deactivate_automatic_dpi_awareness = True |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,159 @@ |
|||||
|
from typing import Union, Tuple |
||||
|
import copy |
||||
|
import re |
||||
|
try: |
||||
|
from typing import Literal |
||||
|
except ImportError: |
||||
|
from typing_extensions import Literal |
||||
|
|
||||
|
from .scaling_tracker import ScalingTracker |
||||
|
from ..font import CTkFont |
||||
|
|
||||
|
|
||||
|
class CTkScalingBaseClass: |
||||
|
""" |
||||
|
Super-class that manages the scaling values and callbacks. |
||||
|
Works for widgets and windows, type must be set in init method with |
||||
|
scaling_type attribute. Methods: |
||||
|
|
||||
|
- _set_scaling() abstractmethod, gets called when scaling changes, must be overridden |
||||
|
- destroy() must be called when sub-class is destroyed |
||||
|
- _apply_widget_scaling() |
||||
|
- _reverse_widget_scaling() |
||||
|
- _apply_window_scaling() |
||||
|
- _reverse_window_scaling() |
||||
|
- _apply_font_scaling() |
||||
|
- _apply_argument_scaling() |
||||
|
- _apply_geometry_scaling() |
||||
|
- _reverse_geometry_scaling() |
||||
|
- _parse_geometry_string() |
||||
|
|
||||
|
""" |
||||
|
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 _set_scaling(self, new_widget_scaling, new_window_scaling): |
||||
|
""" can be overridden, but super method must be called at the beginning """ |
||||
|
self.__widget_scaling = new_widget_scaling |
||||
|
self.__window_scaling = new_window_scaling |
||||
|
|
||||
|
def _get_widget_scaling(self) -> float: |
||||
|
return self.__widget_scaling |
||||
|
|
||||
|
def _get_window_scaling(self) -> float: |
||||
|
return self.__window_scaling |
||||
|
|
||||
|
def _apply_widget_scaling(self, value: Union[int, float]) -> Union[float]: |
||||
|
assert self.__scaling_type == "widget" |
||||
|
return value * self.__widget_scaling |
||||
|
|
||||
|
def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[float]: |
||||
|
assert self.__scaling_type == "widget" |
||||
|
return value / self.__widget_scaling |
||||
|
|
||||
|
def _apply_window_scaling(self, value: Union[int, float]) -> int: |
||||
|
assert self.__scaling_type == "window" |
||||
|
return int(value * self.__window_scaling) |
||||
|
|
||||
|
def _reverse_window_scaling(self, scaled_value: Union[int, float]) -> int: |
||||
|
assert self.__scaling_type == "window" |
||||
|
return int(scaled_value / self.__window_scaling) |
||||
|
|
||||
|
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 3 <= len(font) <= 6: |
||||
|
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) |
||||
|
|
||||
|
# scale padding values |
||||
|
if "pady" in scaled_kwargs: |
||||
|
if isinstance(scaled_kwargs["pady"], (int, float)): |
||||
|
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)): |
||||
|
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"]]) |
||||
|
|
||||
|
# scaled x, y values for place geometry manager |
||||
|
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}" |
@ -0,0 +1,206 @@ |
|||||
|
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 = 100 # ms |
||||
|
loop_pause_after_new_scaling = 1500 # 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"): |
||||
|
import ctypes |
||||
|
|
||||
|
# Values for SetProcessDpiAwareness and SetProcessDpiAwarenessContext: |
||||
|
# internal enum PROCESS_DPI_AWARENESS |
||||
|
# { |
||||
|
# Process_DPI_Unaware = 0, |
||||
|
# Process_System_DPI_Aware = 1, |
||||
|
# Process_Per_Monitor_DPI_Aware = 2 |
||||
|
# } |
||||
|
# |
||||
|
# internal enum DPI_AWARENESS_CONTEXT |
||||
|
# { |
||||
|
# DPI_AWARENESS_CONTEXT_UNAWARE = 16, |
||||
|
# DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = 17, |
||||
|
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = 18, |
||||
|
# DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = 34 |
||||
|
# } |
||||
|
|
||||
|
# ctypes.windll.user32.SetProcessDpiAwarenessContext(34) # Non client area scaling at runtime (titlebar) |
||||
|
# does not work with resizable(False, False), window starts growing on monitor with different scaling (weird tkinter bug...) |
||||
|
# ctypes.windll.user32.EnableNonClientDpiScaling(hwnd) does not work for some reason (tested on Windows 11) |
||||
|
|
||||
|
# It's too bad, that these Windows API methods don't work properly with tkinter. But I tested days with multiple monitor setups, |
||||
|
# and I don't think there is anything left to do. So this is the best option at the moment: |
||||
|
|
||||
|
ctypes.windll.shcore.SetProcessDpiAwareness(2) # Titlebar does not scale at runtime |
||||
|
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) |
||||
|
|
||||
|
window.block_update_dimensions_event() |
||||
|
cls.update_scaling_callbacks_for_window(window) |
||||
|
window.unblock_update_dimensions_event() |
||||
|
|
||||
|
if sys.platform.startswith("win"): |
||||
|
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,0 +1,9 @@ |
|||||
|
from .theme_manager import ThemeManager |
||||
|
|
||||
|
# load default blue theme |
||||
|
try: |
||||
|
ThemeManager.load_theme("blue") |
||||
|
except FileNotFoundError as err: |
||||
|
raise FileNotFoundError(f"{err}\n\nThe .json theme file for CustomTkinter could not be found.\n" + |
||||
|
f"If packaging with pyinstaller was used, have a look at the wiki:\n" + |
||||
|
f"https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging#windows-pyinstaller-auto-py-to-exe") |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,47 @@ |
|||||
|
import sys |
||||
|
import os |
||||
|
import json |
||||
|
from typing import List, Union |
||||
|
|
||||
|
|
||||
|
class ThemeManager: |
||||
|
|
||||
|
theme: dict = {} # contains all the theme data |
||||
|
_built_in_themes: List[str] = ["blue", "green", "dark-blue", "sweetkind"] |
||||
|
_currently_loaded_theme: Union[str, None] = None |
||||
|
|
||||
|
@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) |
||||
|
|
||||
|
# store theme path for saving |
||||
|
cls._currently_loaded_theme = theme_name_or_path |
||||
|
|
||||
|
# filter theme values for platform |
||||
|
for key in cls.theme.keys(): |
||||
|
# check if values for key differ on platforms |
||||
|
if "macOS" in cls.theme[key].keys(): |
||||
|
if sys.platform == "darwin": |
||||
|
cls.theme[key] = cls.theme[key]["macOS"] |
||||
|
elif sys.platform.startswith("win"): |
||||
|
cls.theme[key] = cls.theme[key]["Windows"] |
||||
|
else: |
||||
|
cls.theme[key] = cls.theme[key]["Linux"] |
||||
|
|
||||
|
@classmethod |
||||
|
def save_theme(cls): |
||||
|
if cls._currently_loaded_theme is not None: |
||||
|
if cls._currently_loaded_theme not in cls._built_in_themes: |
||||
|
with open(cls._currently_loaded_theme, "r") as f: |
||||
|
json.dump(cls.theme, f, indent=2) |
||||
|
else: |
||||
|
raise ValueError(f"cannot modify builtin theme '{cls._currently_loaded_theme}'") |
||||
|
else: |
||||
|
raise ValueError(f"cannot save theme, no theme is loaded") |
@ -0,0 +1 @@ |
|||||
|
from .utility_functions import pop_from_dict_by_set, check_kwargs_empty |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,22 @@ |
|||||
|
|
||||
|
def pop_from_dict_by_set(dictionary: dict, valid_keys: set) -> dict: |
||||
|
""" remove and create new dict with key value pairs of dictionary, where key is in valid_keys """ |
||||
|
new_dictionary = {} |
||||
|
|
||||
|
for key in list(dictionary.keys()): |
||||
|
if key in valid_keys: |
||||
|
new_dictionary[key] = dictionary.pop(key) |
||||
|
|
||||
|
return new_dictionary |
||||
|
|
||||
|
|
||||
|
def check_kwargs_empty(kwargs_dict, raise_error=False) -> bool: |
||||
|
""" returns True if kwargs are empty, False otherwise, raises error if not empty """ |
||||
|
|
||||
|
if len(kwargs_dict) > 0: |
||||
|
if raise_error: |
||||
|
raise ValueError(f"{list(kwargs_dict.keys())} are not supported arguments. Look at the documentation for supported arguments.") |
||||
|
else: |
||||
|
return True |
||||
|
else: |
||||
|
return False |
Loading…
Reference in new issue