diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fa87b4..d5e4824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to this project will be documented in this file! The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +ToDo: + - change font attribute in wiki + - add new button attributes to wiki + + - create grayscale theme file + - cursor configuring + - overwrite winfo methods + + +## [5.0.0] - 2022-11-13 +### Added + - Added CTkTextbox with automatic x and y scrollbars, corner_radius, border_width, border_spacing + - Added CTkSegmentedButton + - Added CTkTabview + - Added .cget() method to all widgets and windows + - Added .bind() and .focus() methods to almost all widgets + - Added 'anchor' option to CTkButton to position image and text inside the button + - Added 'anchor' option to CTkOptionMenu and 'justify' option to CTkComboBox + - Added CTkFont class + - Added CTkImage class to replace PIL.ImageTk.PhotoImage, supports scaling and two images for appearance mode, supports configuring + - Added missing configure options for multiple widgets + +### Changed + - Changed value for transparent colors (same as background) from None to 'transparent' + - Changed 'text_font' attribute to 'font' in all widgets, changed 'dropdown_text_font' to 'dropdown_font' + - Changed 'dropdown_color' attribute to 'dropdown_fg_color' for combobox, optionmenu + - Changed 'orient' attribute of CTkProgressBar and CTkSlider to 'orientation' + - Width and height attributes of CTkCheckBox, CTkRadioButton, CTkSwitch now describe the outer dimensions of the whole widget. The button/switch size is described by separate attributes like checkbox_width, checkbox_height + - font attribute must be tuple or CTkFont now, all size values are measured in pixel now + - Changed dictionary key 'window_bg_color' to 'window' in theme files + - CTkInputDialog attributes completely changed + - CTkScrollbar attributes scrollbar_color, scrollbar_hover_color changed to button_color, button_hover_color + +### Removed + - Removed setter and getter functions like set_text in CTkButton + - Removed bg and background attribute from CTk and CTkToplevel, always use fg_color + - Removed Settings class and moved settings to widget and window classes + - removed customtkinter.set_spacing_scaling(), now set_widget_scaling() is used for spacing too + ## [4.6.0] - 2022-09-17 ### Added - CTkProgressBar indeterminate mode, automatic progress loop with .start() and .stop() diff --git a/Readme.md b/Readme.md index fa018ea..f2936b1 100644 --- a/Readme.md +++ b/Readme.md @@ -1,16 +1,21 @@ +

+ + + + +

+ +
+ ![PyPI](https://img.shields.io/pypi/v/customtkinter) -![PyPI - Downloads](https://img.shields.io/pypi/dm/customtkinter?color=green&label=pip%20downloads) +![PyPI - Downloads](https://img.shields.io/pypi/dm/customtkinter?color=green&label=downloads) +![Downloads](https://static.pepy.tech/personalized-badge/customtkinter?period=total&units=international_system&left_color=grey&right_color=green&left_text=downloads) ![PyPI - License](https://img.shields.io/pypi/l/customtkinter) ![Total lines](https://img.shields.io/tokei/lines/github.com/tomschimansky/customtkinter?color=green&label=total%20lines) -# CustomTkinter UI-Library +
-![](documentation_images/Windows_dark.png) -| _`complex_example.py` on Windows 11 with dark mode and 'dark-blue' theme_ - -![](documentation_images/macOS_light.png) -| _`complex_example.py` on macOS in light mode and standard 'blue' theme_ -### +--- CustomTkinter is a python UI-library based on Tkinter, which provides new, modern and fully customizable widgets. They are created and used like normal Tkinter widgets and @@ -20,6 +25,13 @@ and the window colors either adapt to the system appearance or the manually set (Windows, macOS). With CustomTkinter you'll get a consistent and modern look across all desktop platforms (Windows, macOS, Linux). +![](documentation_images/complex_example_dark_Windows.png) +| _`complex_example.py` on Windows 11 with dark mode and 'blue' theme_ + +![](documentation_images/complex_example_light_macOS.png) +| _`complex_example.py` on macOS in light mode and standard 'blue' theme_ +### + ## Installation Install the module with pip: @@ -38,6 +50,7 @@ The **official** documentation can be found in the Wiki Tab here: ## Example Program To test customtkinter you can try this simple example with only a single button: ```python +import tkinter import customtkinter customtkinter.set_appearance_mode("System") # Modes: system (default), light, dark @@ -51,34 +64,35 @@ def button_function(): # Use CTkButton instead of tkinter Button button = customtkinter.CTkButton(master=app, text="CTkButton", command=button_function) -button.place(relx=0.5, rely=0.5, anchor=customtkinter.CENTER) +button.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER) app.mainloop() ``` -which gives the following (macOS dark mode on): +which results in the following window on macOS: -![](documentation_images/macOS_button_dark.png) + In the [examples folder](https://github.com/TomSchimansky/CustomTkinter/tree/master/examples), you can find more example programs and in the [Documentation](https://github.com/TomSchimansky/CustomTkinter/wiki) -you can find further information on the appearance mode, the themes and all widgets. +you can find further information on the appearance mode, scaling, themes and all widgets. ## More Examples and Showcase -### Appearance mode change +### Appearance mode change and scaling change -On Windows 10/11 you get a dark window header, which changes with set -appearance mode or the system, when you use `customtkinter.CTk()` -to create the window, and it works with all python versions: +CustomTkinter can adapt to the Windows 10/11 light or dark mode: +https://user-images.githubusercontent.com/66446067/204672968-6584f360-4c52-434f-9c16-25761341368b.mp4 -| _`complex_example.py` on Windows 11 with system mode change and standard 'blue' theme_ +| _`complex_example.py` on Windows 11 with system appearance mode change and standard 'blue' theme_ ### -On macOS however you either need python3.10 or higher or the anaconda python -version to get a dark window header at all (Tcl/Tk >= 8.6.9 required). +On macOS you either need python3.10 or higher or the anaconda python +version to get a dark window header (Tcl/Tk >= 8.6.9 required): -| _`complex_example.py` on macOS with system mode change and standard 'blue' theme_ +https://user-images.githubusercontent.com/66446067/204673854-b6cbcfda-d9a1-4425-92a3-5b57d7f2fd6b.mp4 + +| _`complex_example.py` on macOS with system appearance mode change, user-scaling change and standard 'blue' theme_ ### ### Button with images @@ -87,8 +101,8 @@ pass a PhotoImage object to the CTkButton with the ``image`` argument. If you want no text at all you have to set ``text=""`` or you specify how to position the text and image at once with the ``compound`` option: -![](documentation_images/macOS_button_images.png) -| _`example_button_images.py` on macOS_ +![](documentation_images/image_example_dark_Windows.png) +| _`image_example.py` on Windows 11_ ### ### Integration of TkinterMapView widget @@ -96,8 +110,9 @@ In the following example I used a TkinterMapView which integrates well with a CustomTkinter program. It's a tile based map widget which displays OpenStreetMap or other tile based maps: -![](documentation_images/tkintermapview_example.gif) -| _`examples/map_with_customtkinter.py` from TkinterMapView repository on macOS_ +https://user-images.githubusercontent.com/66446067/204675835-1584a8da-5acc-4993-b4a9-e70f06fa14b0.mp4 -You can find the TkinterMapView library and the example program here: +| _`examples/map_with_customtkinter.py` from TkinterMapView repository on Windows 11_ + +You can find the TkinterMapView library and example program here: https://github.com/TomSchimansky/TkinterMapView diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index f760bd6..df5dae0 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -2,73 +2,46 @@ __version__ = "4.6.3" import os import sys +from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar from tkinter.constants import * -from tkinter import StringVar, IntVar, DoubleVar, BooleanVar +import tkinter.filedialog as filedialog # import manager classes -from .settings import Settings -from .appearance_mode_tracker import AppearanceModeTracker -from .theme_manager import ThemeManager -from .scaling_tracker import ScalingTracker -from .font_manager import FontManager -from .draw_engine import DrawEngine - -AppearanceModeTracker.init_appearance_mode() - -# 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") - -FontManager.init_font_manager() - -# determine draw method based on current platform -if sys.platform == "darwin": - DrawEngine.preferred_drawing_method = "polygon_shapes" -else: - DrawEngine.preferred_drawing_method = "font_shapes" - -if sys.platform.startswith("win") and sys.getwindowsversion().build < 9000: # No automatic scaling on Windows < 8.1 - ScalingTracker.deactivate_automatic_dpi_awareness = True - -# load Roboto fonts (used on Windows/Linux) -script_directory = os.path.dirname(os.path.abspath(__file__)) -FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf")) -FontManager.load_font(os.path.join(script_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(script_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.__init__ 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!") - DrawEngine.preferred_drawing_method = "circle_shapes" +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 widgets -from .widgets.widget_base_class import CTkBaseClass -from .widgets.ctk_button import CTkButton -from .widgets.ctk_checkbox import CTkCheckBox -from .widgets.ctk_entry import CTkEntry -from .widgets.ctk_slider import CTkSlider -from .widgets.ctk_frame import CTkFrame -from .widgets.ctk_progressbar import CTkProgressBar -from .widgets.ctk_label import CTkLabel -from .widgets.ctk_radiobutton import CTkRadioButton -from .widgets.ctk_canvas import CTkCanvas -from .widgets.ctk_switch import CTkSwitch -from .widgets.ctk_optionmenu import CTkOptionMenu -from .widgets.ctk_combobox import CTkComboBox -from .widgets.ctk_scrollbar import CTkScrollbar -from .widgets.ctk_textbox import CTkTextbox +from .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.ctk_tk import CTk -from .windows.ctk_toplevel import CTkToplevel -from .windows.ctk_input_dialog import CTkInputDialog +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): @@ -94,11 +67,6 @@ def set_widget_scaling(scaling_value: float): ScalingTracker.set_widget_scaling(scaling_value) -def set_spacing_scaling(scaling_value: float): - """ set scaling for geometry manager calls (place, pack, grid)""" - ScalingTracker.set_spacing_scaling(scaling_value) - - def set_window_scaling(scaling_value: float): """ set scaling for window dimensions """ ScalingTracker.set_window_scaling(scaling_value) diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json index 82de860..bca0b2b 100644 --- a/customtkinter/assets/themes/blue.json +++ b/customtkinter/assets/themes/blue.json @@ -1,79 +1,152 @@ { - "color": { - "window_bg_color": ["#EBEBEC", "#212325"], - "button": ["#3B8ED0", "#1F6AA5"], - "button_hover": ["#36719F", "#144870"], - "button_border": ["#3E454A", "#949A9F"], - "checkbox_border": ["#3E454A", "#949A9F"], - "checkmark": ["white", "gray90"], - "entry": ["#F9F9FA", "#343638"], - "entry_border": ["#979DA2", "#565B5E"], - "entry_placeholder_text": ["gray52", "gray62"], - "frame_border": ["#979DA2", "#1F2122"], - "frame_low": ["#D1D5D8", "#2A2D2E"], - "frame_high": ["#C0C2C5", "#343638"], - "label": [null, null], - "text": ["gray10", "#DCE4EE"], - "text_disabled": ["gray60", "#777B80"], - "text_button_disabled": ["gray40", "gray74"], - "progressbar": ["#939BA2", "#4A4D50"], - "progressbar_progress": ["#3B8ED0", "#1F6AA5"], - "progressbar_border": ["gray", "gray"], - "slider": ["#939BA2", "#4A4D50"], - "slider_progress": ["gray40", "#AAB0B5"], - "slider_button": ["#3B8ED0", "#1F6AA5"], - "slider_button_hover": ["#36719F", "#144870"], - "switch": ["#939BA2", "#4A4D50"], - "switch_progress": ["#3B8ED0", "#1F6AA5"], - "switch_button": ["gray36", "#D5D9DE"], - "switch_button_hover": ["gray20", "gray100"], - "optionmenu_button": ["#36719F", "#144870"], - "optionmenu_button_hover": ["#27577D", "#203A4F"], - "combobox_border": ["#979DA2", "#565B5E"], - "combobox_button_hover": ["#6E7174", "#7A848D"], - "dropdown_color": ["gray90", "gray20"], - "dropdown_hover": ["gray75", "gray28"], - "dropdown_text": ["gray10", "#DCE4EE"], - "scrollbar_button": ["gray55", "gray41"], - "scrollbar_button_hover": ["gray40", "gray53"] + "CTk": { + "fg_color": ["gray92", "gray14"] }, - "text": { + "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", "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": { - "font": "SF Display", - "size": -13 + "family": "SF Display", + "size": 13, + "weight": "normal" }, "Windows": { - "font": "Roboto", - "size": -13 + "family": "Roboto", + "size": 13, + "weight": "normal" }, "Linux": { - "font": "Roboto", - "size": -13 + "family": "Roboto", + "size": 13, + "weight": "normal" } - }, - "shape": { - "button_corner_radius": 6, - "button_border_width": 0, - "checkbox_corner_radius": 6, - "checkbox_border_width": 3, - "radiobutton_corner_radius": 1000, - "radiobutton_border_width_unchecked": 3, - "radiobutton_border_width_checked": 6, - "entry_border_width": 2, - "frame_corner_radius": 6, - "frame_border_width": 0, - "label_corner_radius": 0, - "progressbar_border_width": 0, - "progressbar_corner_radius": 1000, - "slider_border_width": 6, - "slider_corner_radius": 1000, - "slider_button_length": 0, - "slider_button_corner_radius": 1000, - "switch_border_width": 3, - "switch_corner_radius": 1000, - "switch_button_corner_radius": 1000, - "switch_button_length": 0, - "scrollbar_corner_radius": 1000, - "scrollbar_border_spacing": 4 } } diff --git a/customtkinter/assets/themes/dark-blue.json b/customtkinter/assets/themes/dark-blue.json index 0b1a33f..1ecf8ab 100644 --- a/customtkinter/assets/themes/dark-blue.json +++ b/customtkinter/assets/themes/dark-blue.json @@ -1,79 +1,152 @@ { - "color": { - "window_bg_color": ["gray98", "gray10"], - "button": ["#608BD5", "#395E9C"], - "button_hover": ["#A4BDE6", "#748BB3"], - "button_border": ["gray40", "gray70"], - "checkbox_border": ["gray40", "gray60"], - "checkmark": ["white", "gray90"], - "entry": ["white", "gray24"], - "entry_border": ["gray70", "gray32"], - "entry_placeholder_text": ["gray52", "gray62"], - "frame_border": ["#A7C2E0", "#5FB4DD"], - "frame_low": ["gray92", "gray16"], - "frame_high": ["gray86", "gray20"], - "label": [null, null], - "text": ["gray12", "gray90"], - "text_disabled": ["gray60", "gray50"], - "text_button_disabled": ["gray40", "gray74"], - "progressbar": ["#6B6B6B", "gray0"], - "progressbar_progress": ["#608BD5", "#395E9C"], - "progressbar_border": ["gray", "gray"], - "slider": ["#6B6B6B", "gray6"], - "slider_progress": ["gray70", "gray30"], - "slider_button": ["#608BD5", "#395E9C"], - "slider_button_hover": ["#A4BDE6", "#748BB3"], - "switch": ["gray70", "gray35"], - "switch_progress": ["#608BD5", "#395E9C"], - "switch_button": ["gray38", "gray70"], - "switch_button_hover": ["gray30", "gray90"], - "optionmenu_button": ["#36719F", "#144870"], - "optionmenu_button_hover": ["#27577D", "#203A4F"], - "combobox_border": ["gray70", "gray32"], - "combobox_button_hover": ["#6E7174", "#7A848D"], - "dropdown_color": ["gray90", "gray20"], - "dropdown_hover": ["gray75", "gray28"], - "dropdown_text": ["gray10", "#DCE4EE"], - "scrollbar_button": ["gray55", "gray41"], - "scrollbar_button_hover": ["gray40", "gray53"] + "CTk": { + "fg_color": ["gray95", "gray10"] }, - "text": { + "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": { - "font": "SF Display", - "size": -13 + "family": "SF Display", + "size": 13, + "weight": "normal" }, "Windows": { - "font": "Roboto", - "size": -13 + "family": "Roboto", + "size": 13, + "weight": "normal" }, "Linux": { - "font": "Roboto", - "size": -13 + "family": "Roboto", + "size": 13, + "weight": "normal" } - }, - "shape": { - "button_corner_radius": 8, - "button_border_width": 0, - "checkbox_corner_radius": 7, - "checkbox_border_width": 3, - "radiobutton_corner_radius": 1000, - "radiobutton_border_width_unchecked": 3, - "radiobutton_border_width_checked": 6, - "entry_border_width": 2, - "frame_corner_radius": 10, - "frame_border_width": 0, - "label_corner_radius": 0, - "progressbar_border_width": 0, - "progressbar_corner_radius": 1000, - "slider_border_width": 6, - "slider_corner_radius": 8, - "slider_button_length": 0, - "slider_button_corner_radius": 1000, - "switch_border_width": 3, - "switch_corner_radius": 1000, - "switch_button_corner_radius": 1000, - "switch_button_length": 0, - "scrollbar_corner_radius": 1000, - "scrollbar_border_spacing": 4 } } diff --git a/customtkinter/assets/themes/green.json b/customtkinter/assets/themes/green.json index d3e9442..14cd8c6 100644 --- a/customtkinter/assets/themes/green.json +++ b/customtkinter/assets/themes/green.json @@ -1,79 +1,152 @@ { - "color": { - "window_bg_color": ["gray92", "gray12"], - "button": ["#72CF9F", "#11B384"], - "button_hover": ["#0E9670", "#0D8A66"], - "button_border": ["gray40", "gray70"], - "checkbox_border": ["gray40", "gray60"], - "checkmark": ["white", "gray90"], - "entry": ["white", "gray24"], - "entry_border": ["gray70", "gray32"], - "entry_placeholder_text": ["gray52", "gray62"], - "frame_border": ["#A7C2E0", "#5FB4DD"], - "frame_low": ["gray87", "gray18"], - "frame_high": ["gray82", "gray22"], - "label": [null, null], - "text": ["gray20", "gray90"], - "text_disabled": ["gray60", "gray50"], - "text_button_disabled": ["gray40", "gray74"], - "progressbar": ["#6B6B6B", "#222222"], - "progressbar_progress": ["#72CF9F", "#11B384"], - "progressbar_border": ["gray", "gray"], - "slider": ["#6B6B6B", "#222222"], - "slider_progress": ["white", "#555555"], - "slider_button": ["#72CF9F", "#11B384"], - "slider_button_hover": ["#0E9670", "#0D8A66"], - "switch": ["gray70", "gray35"], - "switch_progress": ["#72CF9F", "#11B384"], - "switch_button": ["gray38", "gray70"], - "switch_button_hover": ["gray30", "gray90"], - "optionmenu_button": ["#0E9670", "#0D8A66"], - "optionmenu_button_hover":["gray40", "gray70"], - "combobox_border": ["gray70", "gray32"], - "combobox_button_hover": ["#6E7174", "#7A848D"], - "dropdown_color": ["gray90", "gray20"], - "dropdown_hover": ["gray75", "gray28"], - "dropdown_text": ["gray10", "#DCE4EE"], - "scrollbar_button": ["gray55", "gray41"], - "scrollbar_button_hover": ["gray40", "gray53"] + "CTk": { + "fg_color": ["gray92", "gray14"] }, - "text": { + "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": { - "font": "SF Display", - "size": -13 + "family": "SF Display", + "size": 13, + "weight": "normal" }, "Windows": { - "font": "Roboto", - "size": -13 + "family": "Roboto", + "size": 13, + "weight": "normal" }, "Linux": { - "font": "Roboto", - "size": -13 + "family": "Roboto", + "size": 13, + "weight": "normal" } - }, - "shape": { - "button_corner_radius": 6, - "button_border_width": 0, - "checkbox_corner_radius": 7, - "checkbox_border_width": 3, - "radiobutton_corner_radius": 1000, - "radiobutton_border_width_unchecked": 3, - "radiobutton_border_width_checked": 6, - "entry_border_width": 2, - "frame_corner_radius": 10, - "frame_border_width": 0, - "label_corner_radius": 0, - "progressbar_border_width": 0, - "progressbar_corner_radius": 1000, - "slider_border_width": 6, - "slider_corner_radius": 8, - "slider_button_length": 0, - "slider_button_corner_radius": 1000, - "switch_border_width": 3, - "switch_corner_radius": 1000, - "switch_button_corner_radius": 1000, - "switch_button_length": 0, - "scrollbar_corner_radius": 1000, - "scrollbar_border_spacing": 4 } } diff --git a/customtkinter/assets/themes/sweetkind.json b/customtkinter/assets/themes/sweetkind.json deleted file mode 100644 index 178fec1..0000000 --- a/customtkinter/assets/themes/sweetkind.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "color": { - "window_bg_color": ["#ebf0f5", "#181b28"], - "button": ["#e46bff", "#212435"], - "button_hover": ["#8593d6", "#171926"], - "button_border": ["#525983", "#080b12"], - "checkbox_border": ["#01e9c4", "#01e9c4"], - "checkmark": ["#01e9c4", "#01e9c4"], - "entry": ["#dee2e7", "#212435"], - "entry_border": ["#fa00d0", "#080b12"], - "entry_placeholder_text": ["#cdc8ce", "#cdc8ce"], - "frame_border": ["#525983", "#10121f"], - "frame_low": ["#dee2e7", "#181b28"], - "frame_high": ["#dee2e7", "#1b1e2d"], - "label": [null, null], - "text": ["#0c0e14", "#cdc8ce"], - "text_disabled": ["#5e6062", "#7a8894"], - "text_button_disabled": ["#7a8894", "#7a8894"], - "progressbar": ["#fa00d0", "#fa00d0"], - "progressbar_progress": ["#363844", "#363844"], - "progressbar_border": ["#fa00d0", "#0d101f"], - "slider": ["#fa00d0", "#fa00d0"], - "slider_progress": ["#0d101f", "#0d101f"], - "slider_button": ["#fa00d0", "#fa00d0"], - "slider_button_hover": ["#e46bff", "#fa00d0"], - "switch": ["#7681be", "#1f2233"], - "switch_progress": ["#00e6c3", "#00e6c3"], - "switch_button": ["#525983", "#2e324a"], - "switch_button_hover": ["#fa00d0", "#2e324a"], - "optionmenu_button": ["#525983", "#080b12"], - "optionmenu_button_hover": ["#fa00d0", "#080b12"], - "combobox_border": ["#525983", "#080b12"], - "combobox_button_hover": ["#fa00d0", "#fa00d0"], - "dropdown_color": ["#dee2e7", "#212435"], - "dropdown_hover": ["#fa00d0", "#fa00d0"], - "dropdown_text": ["#0c0e14", "#cdc8ce"], - "scrollbar_button": ["#fa00d0", "#fa00d0"], - "scrollbar_button_hover": ["#9b45ff", "#9b45ff"] - }, - "text": { - "macOS": { - "font": "SF Display", - "size": -13 - }, - "Windows": { - "font": "Roboto", - "size": -13 - }, - "Linux": { - "font": "Roboto", - "size": -13 - } - }, - "shape": { - "button_corner_radius": 8, - "button_border_width": 1, - "checkbox_corner_radius": 7, - "checkbox_border_width": 1, - "radiobutton_corner_radius": 1000, - "radiobutton_border_width_unchecked": 2, - "radiobutton_border_width_checked": 6, - "entry_border_width": 1, - "frame_corner_radius": 10, - "frame_border_width": 1, - "label_corner_radius": 3, - "progressbar_border_width": 2, - "progressbar_corner_radius": 1000, - "slider_border_width": 6, - "slider_corner_radius": 8, - "slider_button_length": 0, - "slider_button_corner_radius": 1000, - "switch_border_width": 3, - "switch_corner_radius": 1000, - "switch_button_corner_radius": 1000, - "switch_button_length": 2, - "scrollbar_corner_radius": 1000, - "scrollbar_border_spacing": 4 - } -} diff --git a/customtkinter/settings.py b/customtkinter/settings.py deleted file mode 100644 index a93f800..0000000 --- a/customtkinter/settings.py +++ /dev/null @@ -1,6 +0,0 @@ - -class Settings: - cursor_manipulation_enabled = True - deactivate_macos_window_header_manipulation = False - deactivate_windows_window_header_manipulation = False - use_dropdown_fallback = True diff --git a/customtkinter/theme_manager.py b/customtkinter/theme_manager.py deleted file mode 100644 index 7c70c8a..0000000 --- a/customtkinter/theme_manager.py +++ /dev/null @@ -1,93 +0,0 @@ -import sys -import os -import json - - -class ThemeManager: - - theme = {} # contains all the theme data - built_in_themes = ["blue", "green", "dark-blue", "sweetkind"] - - @classmethod - def load_theme(cls, theme_name_or_path: str): - script_directory = os.path.dirname(os.path.abspath(__file__)) - - if theme_name_or_path in cls.built_in_themes: - with open(os.path.join(script_directory, "assets", "themes", f"{theme_name_or_path}.json"), "r") as f: - cls.theme = json.load(f) - else: - with open(theme_name_or_path, "r") as f: - cls.theme = json.load(f) - - if sys.platform == "darwin": - cls.theme["text"] = cls.theme["text"]["macOS"] - elif sys.platform.startswith("win"): - cls.theme["text"] = cls.theme["text"]["Windows"] - else: - cls.theme["text"] = cls.theme["text"]["Linux"] - - @staticmethod - def single_color(color, appearance_mode: int) -> 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 then returns - always a single color string """ - - if type(color) == tuple or type(color) == list: - return color[appearance_mode] - else: - return color - - @staticmethod - def rgb2hex(rgb_color: tuple) -> str: - return "#{:02x}{:02x}{:02x}".format(round(rgb_color[0]), round(rgb_color[1]), round(rgb_color[2])) - - @staticmethod - def hex2rgb(hex_color: str) -> tuple: - return tuple(int(hex_color.strip("#")[i:i+2], 16) for i in (0, 2, 4)) - - @classmethod - def linear_blend(cls, color_1: str, color_2: str, blend_factor: float) -> str: - """ Blends two hex colors linear, where blend_factor of 0 - results in color_1 and blend_factor of 1 results in color_2. """ - - if color_1 is None or color_2 is None: - return None - - rgb_1 = cls.hex2rgb(color_1) - rgb_2 = cls.hex2rgb(color_2) - - new_rgb = (rgb_1[0] + (rgb_2[0] - rgb_1[0]) * blend_factor, - rgb_1[1] + (rgb_2[1] - rgb_1[1]) * blend_factor, - rgb_1[2] + (rgb_2[2] - rgb_1[2]) * blend_factor) - - return cls.rgb2hex(new_rgb) - - @classmethod - def get_minimal_darker(cls, color: str) -> str: - if color.startswith("#"): - color_rgb = cls.hex2rgb(color) - if color_rgb[0] > 0: - return cls.rgb2hex((color_rgb[0] - 1, color_rgb[1], color_rgb[2])) - elif color_rgb[1] > 0: - return cls.rgb2hex((color_rgb[0], color_rgb[1] - 1, color_rgb[2])) - elif color_rgb[2] > 0: - return cls.rgb2hex((color_rgb[0], color_rgb[1], color_rgb[2] - 1)) - else: - return cls.rgb2hex((color_rgb[0] + 1, color_rgb[1], color_rgb[2] - 1)) # otherwise slightly lighter - - @classmethod - def multiply_hex_color(cls, hex_color: str, factor: float = 1.0) -> str: - try: - rgb_color = ThemeManager.hex2rgb(hex_color) - dark_rgb_color = (min(255, rgb_color[0] * factor), - min(255, rgb_color[1] * factor), - min(255, rgb_color[2] * factor)) - return ThemeManager.rgb2hex(dark_rgb_color) - except Exception as err: - # sys.stderr.write("ERROR (CTkColorManager): failed to darken the following color: " + str(hex_color) + " " + str(err)) - return hex_color - - @classmethod - def set_main_color(cls, main_color, main_color_hover): - cls.MAIN_COLOR = main_color - cls.MAIN_HOVER_COLOR = main_color_hover diff --git a/customtkinter/widgets/__init__.py b/customtkinter/widgets/__init__.py deleted file mode 100644 index 62acbf5..0000000 --- a/customtkinter/widgets/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .ctk_canvas import CTkCanvas - -CTkCanvas.init_font_character_mapping() diff --git a/customtkinter/widgets/ctk_button.py b/customtkinter/widgets/ctk_button.py deleted file mode 100644 index 09acdc8..0000000 --- a/customtkinter/widgets/ctk_button.py +++ /dev/null @@ -1,377 +0,0 @@ -import tkinter -import sys -from typing import Union, Tuple, Callable - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..settings import Settings -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkButton(CTkBaseClass): - """ button with border, rounded corners, hover effect, image support """ - - def __init__(self, *args, - bg_color: Union[str, Tuple[str, str], None] = None, - fg_color: Union[str, Tuple[str, str], None] = "default_theme", - hover_color: Union[str, Tuple[str, str]] = "default_theme", - border_color: Union[str, Tuple[str, str]] = "default_theme", - text_color: Union[str, Tuple[str, str]] = "default_theme", - text_color_disabled: Union[str, Tuple[str, str]] = "default_theme", - width: int = 140, - height: int = 28, - corner_radius: Union[int, str] = "default_theme", - border_width: Union[int, str] = "default_theme", - text: str = "CTkButton", - textvariable: tkinter.Variable = None, - text_font: any = "default_theme", - image: tkinter.PhotoImage = None, - hover: bool = True, - compound: str = "left", - state: str = "normal", - command: Callable = None, - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color - self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color - self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color - self.border_color = ThemeManager.theme["color"]["button_border"] if border_color == "default_theme" else border_color - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled - - # shape - self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width - - # text, font, image - self.image = image - self.image_label = None - self.text = text - self.text_label = None - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - - # callback and hover functionality - self.command = command - self.textvariable = textvariable - self.state = state - self.hover = hover - self.compound = compound - self.click_animation_running = False - - # configure grid system (2x2) - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - self.grid_rowconfigure(1, weight=1) - self.grid_columnconfigure(1, weight=1) - - # canvas - self.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._desired_width), - height=self.apply_widget_scaling(self._desired_height)) - self.canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew") - self.draw_engine = DrawEngine(self.canvas) - - # canvas event bindings - self.canvas.bind("", self.on_enter) - self.canvas.bind("", self.on_leave) - self.canvas.bind("", self.clicked) - self.canvas.bind("", self.clicked) - self.bind('', self.update_dimensions_event) - - # configure cursor and initial draw - self.set_cursor() - self.draw() - - def set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - if self.text_label is not None: - self.text_label.destroy() - self.text_label = None - if self.image_label is not None: - self.image_label.destroy() - self.image_label = None - - 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: 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 draw(self, no_color_updates=False): - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self.corner_radius), - self.apply_widget_scaling(self.border_width)) - - if no_color_updates is False or requires_recoloring: - - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - # set color for the button border parts (outline) - self.canvas.itemconfig("border_parts", - outline=ThemeManager.single_color(self.border_color, self._appearance_mode), - fill=ThemeManager.single_color(self.border_color, self._appearance_mode)) - - # set color for inner button parts - if self.fg_color is None: - self.canvas.itemconfig("inner_parts", - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode), - fill=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("inner_parts", - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - # 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.text_font), - text=self.text, - textvariable=self.textvariable) - - self.text_label.bind("", self.on_enter) - self.text_label.bind("", self.on_leave) - self.text_label.bind("", self.clicked) - self.text_label.bind("", self.clicked) - - if no_color_updates is False: - # set text_label fg color (text color) - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - if self.state == tkinter.DISABLED: - self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))) - else: - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - if self.fg_color is None: - self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - else: - self.text_label.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - else: - # delete text_label if no text given - if self.text_label is not None: - self.text_label.destroy() - self.text_label = None - - # 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.image_label.bind("", self.on_enter) - self.image_label.bind("", self.on_leave) - self.image_label.bind("", self.clicked) - self.image_label.bind("", self.clicked) - - if no_color_updates is False: - # set image_label bg color (background color of label) - if self.fg_color is None: - self.image_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - else: - self.image_label.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - self.image_label.configure(image=self.image) # set image - - else: - # delete text_label if no text given - if self.image_label is not None: - self.image_label.destroy() - self.image_label = None - - # create grid layout with just an image given - if self.image_label is not None and self.text_label is None: - self.image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="", - pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) # bottom pady with +1 for rounding to even - - # create grid layout with just text given - if self.image_label is None and self.text_label is not None: - self.text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="", - padx=self.apply_widget_scaling(self.corner_radius), - pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) # bottom pady with +1 for rounding to even - - # create grid layout of image and text label in 2x2 grid system with given compound - if self.image_label is not None and self.text_label is not None: - if self.compound == tkinter.LEFT or self.compound == "left": - self.image_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1, - padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2), - pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) - self.text_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1, - padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))), - pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) - elif self.compound == tkinter.TOP or self.compound == "top": - self.image_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1, - padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), - pady=(self.apply_widget_scaling(self.border_width), 2)) - self.text_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1, - padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), - pady=(2, self.apply_widget_scaling(self.border_width))) - elif self.compound == tkinter.RIGHT or self.compound == "right": - self.image_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1, - padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))), - pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) - self.text_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1, - padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2), - pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width) + 1)) - elif self.compound == tkinter.BOTTOM or self.compound == "bottom": - self.image_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1, - padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), - pady=(2, self.apply_widget_scaling(self.border_width))) - self.text_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1, - padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), - pady=(self.apply_widget_scaling(self.border_width), 2)) - - def configure(self, require_redraw=False, **kwargs): - 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 "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - if self.text_label is not None: - self.text_label.configure(font=self.apply_font_scaling(self.text_font)) - - if "state" in kwargs: - self.state = kwargs.pop("state") - self.set_cursor() - require_redraw = True - - if "image" in kwargs: - self.image = kwargs.pop("image") - require_redraw = True - - if "corner_radius" in kwargs: - self.corner_radius = kwargs.pop("corner_radius") - require_redraw = True - - if "compound" in kwargs: - self.compound = kwargs.pop("compound") - require_redraw = True - - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - require_redraw = True - - if "border_color" in kwargs: - self.border_color = kwargs.pop("border_color") - require_redraw = True - - if "hover_color" in kwargs: - self.hover_color = kwargs.pop("hover_color") - require_redraw = True - - if "text_color" in kwargs: - self.text_color = kwargs.pop("text_color") - require_redraw = True - - if "command" in kwargs: - self.command = kwargs.pop("command") - - if "textvariable" in kwargs: - self.textvariable = kwargs.pop("textvariable") - if self.text_label is not None: - self.text_label.configure(textvariable=self.textvariable) - - if "width" in kwargs: - self.set_dimensions(width=kwargs.pop("width")) - - if "height" in kwargs: - self.set_dimensions(height=kwargs.pop("height")) - - super().configure(require_redraw=require_redraw, **kwargs) - - def set_cursor(self): - if Settings.cursor_manipulation_enabled: - if self.state == tkinter.DISABLED: - if sys.platform == "darwin" and self.command is not None and Settings.cursor_manipulation_enabled: - self.configure(cursor="arrow") - elif sys.platform.startswith("win") and self.command is not None and Settings.cursor_manipulation_enabled: - self.configure(cursor="arrow") - - elif self.state == tkinter.NORMAL: - if sys.platform == "darwin" and self.command is not None and Settings.cursor_manipulation_enabled: - self.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and self.command is not None and Settings.cursor_manipulation_enabled: - self.configure(cursor="hand2") - - def set_image(self, image): - """ will be removed in next major """ - self.configure(image=image) - - def set_text(self, text): - """ will be removed in next major """ - self.configure(text=text) - - def on_enter(self, event=None): - if self.hover is True and self.state == tkinter.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=ThemeManager.single_color(inner_parts_color, self._appearance_mode), - fill=ThemeManager.single_color(inner_parts_color, self._appearance_mode)) - - # set text_label bg color to button hover color - if self.text_label is not None: - self.text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode)) - - # set image_label bg color to button hover color - if self.image_label is not None: - self.image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode)) - - def on_leave(self, event=None): - self.click_animation_running = False - - if self.hover is True: - if self.fg_color is None: - inner_parts_color = self.bg_color - else: - inner_parts_color = self.fg_color - - # set color of inner button parts - self.canvas.itemconfig("inner_parts", - outline=ThemeManager.single_color(inner_parts_color, self._appearance_mode), - fill=ThemeManager.single_color(inner_parts_color, self._appearance_mode)) - - # set text_label bg color (label color) - if self.text_label is not None: - self.text_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode)) - - # set image_label bg color (image bg color) - if self.image_label is not None: - self.image_label.configure(bg=ThemeManager.single_color(inner_parts_color, self._appearance_mode)) - - def click_animation(self): - if self.click_animation_running: - self.on_enter() - - def clicked(self, event=None): - if self.command is not None: - if self.state != tkinter.DISABLED: - - # click animation: change color with .on_leave() and back to normal after 100ms with click_animation() - self.on_leave() - self.click_animation_running = True - self.after(100, self.click_animation) - - self.command() diff --git a/customtkinter/widgets/ctk_checkbox.py b/customtkinter/widgets/ctk_checkbox.py deleted file mode 100644 index 38931c0..0000000 --- a/customtkinter/widgets/ctk_checkbox.py +++ /dev/null @@ -1,322 +0,0 @@ -import tkinter -import sys -from typing import Union - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..settings import Settings -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkCheckBox(CTkBaseClass): - """ tkinter custom checkbox with border, rounded corners and hover effect """ - - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - hover_color="default_theme", - border_color="default_theme", - border_width="default_theme", - checkmark_color="default_theme", - width=24, - height=24, - corner_radius="default_theme", - text_font="default_theme", - text_color="default_theme", - text="CTkCheckBox", - text_color_disabled="default_theme", - hover=True, - command=None, - state=tkinter.NORMAL, - onvalue=1, - offvalue=0, - variable=None, - textvariable=None, - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color - self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color - self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color - self.border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color - self.checkmark_color = ThemeManager.theme["color"]["checkmark"] if checkmark_color == "default_theme" else checkmark_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["checkbox_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_width = ThemeManager.theme["shape"]["checkbox_border_width"] if border_width == "default_theme" else border_width - - # text - self.text = text - self.text_label: Union[tkinter.Label, None] = None - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_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, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe") - - 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, padx=0, pady=0, columnspan=1, rowspan=1) - self.draw_engine = DrawEngine(self.canvas) - - self.canvas.bind("", self.on_enter) - self.canvas.bind("", self.on_leave) - self.canvas.bind("", self.toggle) - - self.text_label = tkinter.Label(master=self, - bd=0, - text=self.text, - justify=tkinter.LEFT, - font=self.apply_font_scaling(self.text_font), - textvariable=self.textvariable) - self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w") - self.text_label["anchor"] = "w" - - self.text_label.bind("", self.on_enter) - self.text_label.bind("", self.on_leave) - self.text_label.bind("", self.toggle) - - # register variable callback and set state according to variable - if self.variable is not None and self.variable != "": - self.variable_callback_name = self.variable.trace_add("write", self.variable_callback) - self.check_state = True if variable.get() == self.onvalue else False - - self.draw() # initial draw - self.set_cursor() - - def set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) - self.text_label.configure(font=self.apply_font_scaling(self.text_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._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): - 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 self.check_state is True: - self.draw_engine.draw_checkmark(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self._current_height * 0.58)) - else: - self.canvas.delete("checkmark") - - self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - if self.check_state is True: - self.canvas.itemconfig("inner_parts", - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts", - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - if "create_line" in self.canvas.gettags("checkmark"): - self.canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self.checkmark_color, self._appearance_mode)) - else: - self.canvas.itemconfig("checkmark", fill=ThemeManager.single_color(self.checkmark_color, self._appearance_mode)) - else: - self.canvas.itemconfig("inner_parts", - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode), - fill=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts", - outline=ThemeManager.single_color(self.border_color, self._appearance_mode), - fill=ThemeManager.single_color(self.border_color, self._appearance_mode)) - - if self.state == tkinter.DISABLED: - self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))) - else: - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - def configure(self, require_redraw=False, **kwargs): - if "text" in kwargs: - self.text = kwargs.pop("text") - self.text_label.configure(text=self.text) - - if "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - if self.text_label is not None: - self.text_label.configure(font=self.apply_font_scaling(self.text_font)) - - if "state" in kwargs: - self.state = kwargs.pop("state") - self.set_cursor() - require_redraw = True - - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - require_redraw = True - - if "hover_color" in kwargs: - self.hover_color = kwargs.pop("hover_color") - require_redraw = True - - if "text_color" in kwargs: - self.text_color = kwargs.pop("text_color") - require_redraw = True - - if "border_color" in kwargs: - self.border_color = kwargs.pop("border_color") - require_redraw = True - - if "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 set_cursor(self): - if Settings.cursor_manipulation_enabled: - if self.state == tkinter.DISABLED: - if sys.platform == "darwin" and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="arrow") - if self.text_label is not None: - self.text_label.configure(cursor="arrow") - elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled: - 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" and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="pointinghand") - if self.text_label is not None: - self.text_label.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled: - 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=ThemeManager.single_color(self.hover_color, self._appearance_mode), - outline=ThemeManager.single_color(self.hover_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.hover_color, self._appearance_mode), - outline=ThemeManager.single_color(self.hover_color, self._appearance_mode)) - else: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.hover_color, self._appearance_mode), - outline=ThemeManager.single_color(self.hover_color, self._appearance_mode)) - - def on_leave(self, event=0): - if self.hover is True: - if self.check_state is True: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.bg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.border_color, self._appearance_mode), - outline=ThemeManager.single_color(self.border_color, self._appearance_mode)) - - 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): - return self.onvalue if self.check_state is True else self.offvalue diff --git a/customtkinter/widgets/ctk_combobox.py b/customtkinter/widgets/ctk_combobox.py deleted file mode 100644 index eda3a64..0000000 --- a/customtkinter/widgets/ctk_combobox.py +++ /dev/null @@ -1,297 +0,0 @@ -import tkinter -import sys - -from .dropdown_menu import DropdownMenu -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..settings import Settings -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkComboBox(CTkBaseClass): - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - border_color="default_theme", - button_color="default_theme", - button_hover_color="default_theme", - dropdown_color="default_theme", - dropdown_hover_color="default_theme", - dropdown_text_color="default_theme", - variable=None, - values=None, - command=None, - width=140, - height=28, - corner_radius="default_theme", - border_width="default_theme", - text_font="default_theme", - dropdown_text_font="default_theme", - text_color="default_theme", - text_color_disabled="default_theme", - hover=True, - state=tkinter.NORMAL, - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color variables - self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color - self.border_color = ThemeManager.theme["color"]["combobox_border"] if border_color == "default_theme" else border_color - self.button_color = ThemeManager.theme["color"]["combobox_border"] if button_color == "default_theme" else button_color - self.button_hover_color = ThemeManager.theme["color"]["combobox_button_hover"] if button_hover_color == "default_theme" else button_hover_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width - - # text and font - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - - # callback and hover functionality - self.command = command - self.textvariable = 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_color, - hover_color=dropdown_hover_color, - text_color=dropdown_text_color, - text_font=dropdown_text_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, rowspan=1, columnspan=1, sticky="nsew") - self.draw_engine = DrawEngine(self.canvas) - - self.entry = tkinter.Entry(master=self, - state=self.state, - width=1, - bd=0, - highlightthickness=0, - font=self.apply_font_scaling(self.text_font)) - 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)))) - - # insert default value - if len(self.values) > 0: - self.entry.insert(0, self.values[0]) - else: - self.entry.insert(0, "CTkComboBox") - - self.draw() # initial draw - - # event bindings - self.canvas.tag_bind("right_parts", "", self.on_enter) - self.canvas.tag_bind("dropdown_arrow", "", self.on_enter) - self.canvas.tag_bind("right_parts", "", self.on_leave) - self.canvas.tag_bind("dropdown_arrow", "", self.on_leave) - self.canvas.tag_bind("right_parts", "", self.clicked) - self.canvas.tag_bind("dropdown_arrow", "", self.clicked) - self.bind('', self.update_dimensions_event) - - if self.textvariable is not None: - self.entry.configure(textvariable=self.textvariable) - - def set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - # change entry font size and grid padding - left_section_width = self._current_width - self._current_height - self.entry.configure(font=self.apply_font_scaling(self.text_font)) - 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)))) - - 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: 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 draw(self, no_color_updates=False): - left_section_width = self._current_width - self._current_height - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self.corner_radius), - self.apply_widget_scaling(self.border_width), - self.apply_widget_scaling(left_section_width)) - - requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self.apply_widget_scaling(self._current_width - (self._current_height / 2)), - self.apply_widget_scaling(self._current_height / 2), - self.apply_widget_scaling(self._current_height / 3)) - - if no_color_updates is False or requires_recoloring or requires_recoloring_2: - - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - self.canvas.itemconfig("inner_parts_left", - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts_left", - outline=ThemeManager.single_color(self.border_color, self._appearance_mode), - fill=ThemeManager.single_color(self.border_color, self._appearance_mode)) - self.canvas.itemconfig("inner_parts_right", - outline=ThemeManager.single_color(self.border_color, self._appearance_mode), - fill=ThemeManager.single_color(self.border_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts_right", - outline=ThemeManager.single_color(self.border_color, self._appearance_mode), - fill=ThemeManager.single_color(self.border_color, self._appearance_mode)) - - self.entry.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fg=ThemeManager.single_color(self.text_color, self._appearance_mode), - disabledforeground=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode), - disabledbackground=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - if self.state == tkinter.DISABLED: - self.canvas.itemconfig("dropdown_arrow", - fill=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)) - else: - self.canvas.itemconfig("dropdown_arrow", - fill=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - 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 "state" in kwargs: - self.state = kwargs.pop("state") - self.entry.configure(state=self.state) - require_redraw = True - - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - require_redraw = True - - if "button_color" in kwargs: - self.button_color = kwargs.pop("button_color") - require_redraw = True - - if "button_hover_color" in kwargs: - self.button_hover_color = kwargs.pop("button_hover_color") - require_redraw = True - - if "text_color" in kwargs: - self.text_color = kwargs.pop("text_color") - require_redraw = True - - if "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - self.entry.configure(font=self.apply_font_scaling(self.text_font)) - - if "command" in kwargs: - self.command = kwargs.pop("command") - - if "variable" in kwargs: - self.textvariable = kwargs.pop("variable") - self.entry.configure(textvariable=self.textvariable) - - if "width" in kwargs: - self.set_dimensions(width=kwargs.pop("width")) - - if "height" in kwargs: - self.set_dimensions(height=kwargs.pop("height")) - - if "values" in kwargs: - self.values = kwargs.pop("values") - self.dropdown_menu.configure(values=self.values) - - if "dropdown_color" in kwargs: - self.dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color")) - - if "dropdown_hover_color" in kwargs: - self.dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color")) - - if "dropdown_text_color" in kwargs: - self.dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color")) - - if "dropdown_text_font" in kwargs: - self.dropdown_menu.configure(text_font=kwargs.pop("dropdown_text_font")) - - super().configure(require_redraw=require_redraw, **kwargs) - - 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 Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and len(self.values) > 0 and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="hand2") - - # set color of inner button parts to hover color - self.canvas.itemconfig("inner_parts_right", - outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode), - fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts_right", - outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode), - fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode)) - - def on_leave(self, event=0): - if self.hover is True: - if sys.platform == "darwin" and len(self.values) > 0 and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="arrow") - elif sys.platform.startswith("win") and len(self.values) > 0 and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="arrow") - - # set color of inner button parts - self.canvas.itemconfig("inner_parts_right", - outline=ThemeManager.single_color(self.button_color, self._appearance_mode), - fill=ThemeManager.single_color(self.button_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts_right", - outline=ThemeManager.single_color(self.button_color, self._appearance_mode), - fill=ThemeManager.single_color(self.button_color, self._appearance_mode)) - - def dropdown_callback(self, value: str): - if self.state == "readonly": - self.entry.configure(state="normal") - self.entry.delete(0, tkinter.END) - self.entry.insert(0, value) - self.entry.configure(state="readonly") - else: - self.entry.delete(0, tkinter.END) - self.entry.insert(0, value) - - if self.command is not None: - self.command(value) - - def set(self, value: str): - if self.state == "readonly": - self.entry.configure(state="normal") - self.entry.delete(0, tkinter.END) - self.entry.insert(0, value) - self.entry.configure(state="readonly") - else: - self.entry.delete(0, tkinter.END) - self.entry.insert(0, value) - - def get(self) -> str: - return self.entry.get() - - def clicked(self, event=0): - if self.state is not tkinter.DISABLED and len(self.values) > 0: - self.open_dropdown_menu() diff --git a/customtkinter/widgets/ctk_entry.py b/customtkinter/widgets/ctk_entry.py deleted file mode 100644 index ef6171c..0000000 --- a/customtkinter/widgets/ctk_entry.py +++ /dev/null @@ -1,252 +0,0 @@ -import tkinter - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkEntry(CTkBaseClass): - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - text_color="default_theme", - placeholder_text_color="default_theme", - text_font="default_theme", - placeholder_text=None, - corner_radius="default_theme", - border_width="default_theme", - border_color="default_theme", - width=140, - height=28, - state=tkinter.NORMAL, - textvariable: tkinter.Variable = None, - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - if "master" in kwargs: - super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master")) - else: - super().__init__(*args, bg_color=bg_color, width=width, height=height) - - # configure grid system (1x1) - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - - # color - self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self.placeholder_text_color = ThemeManager.theme["color"]["entry_placeholder_text"] if placeholder_text_color == "default_theme" else placeholder_text_color - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - self.border_color = ThemeManager.theme["color"]["entry_border"] if border_color == "default_theme" else border_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_width = ThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width - - # placeholder text - 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 - - # textvariable - self.textvariable = textvariable - - self.state = state - - self.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._current_width), - height=self.apply_widget_scaling(self._current_height)) - self.canvas.grid(column=0, row=0, sticky="nswe") - self.draw_engine = DrawEngine(self.canvas) - - self.entry = tkinter.Entry(master=self, - bd=0, - width=1, - highlightthickness=0, - font=self.apply_font_scaling(self.text_font), - state=self.state, - textvariable=self.textvariable, - **kwargs) - self.entry.grid(column=0, row=0, sticky="nswe", - padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6), - pady=(self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.border_width + 1))) - - super().bind('', self.update_dimensions_event) - self.entry.bind('', self.entry_focus_out) - self.entry.bind('', self.entry_focus_in) - - self.activate_placeholder() - self.draw() - - def set_scaling(self, *args, **kwargs): - super().set_scaling( *args, **kwargs) - - self.entry.configure(font=self.apply_font_scaling(self.text_font)) - self.entry.grid(column=0, row=0, sticky="we", - padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6)) - - 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): - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - 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 ThemeManager.single_color(self.fg_color, self._appearance_mode) is not None: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - self.entry.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode), - disabledbackground=ThemeManager.single_color(self.fg_color, self._appearance_mode), - highlightcolor=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fg=ThemeManager.single_color(self.text_color, self._appearance_mode), - disabledforeground=ThemeManager.single_color(self.text_color, self._appearance_mode), - insertbackground=ThemeManager.single_color(self.text_color, self._appearance_mode)) - else: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.bg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.entry.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode), - disabledbackground=ThemeManager.single_color(self.bg_color, self._appearance_mode), - highlightcolor=ThemeManager.single_color(self.bg_color, self._appearance_mode), - fg=ThemeManager.single_color(self.text_color, self._appearance_mode), - disabledforeground=ThemeManager.single_color(self.text_color, self._appearance_mode), - insertbackground=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.border_color, self._appearance_mode), - outline=ThemeManager.single_color(self.border_color, self._appearance_mode)) - - if self.placeholder_text_active: - self.entry.config(fg=ThemeManager.single_color(self.placeholder_text_color, self._appearance_mode)) - - def bind(self, *args, **kwargs): - self.entry.bind(*args, **kwargs) - - def configure(self, require_redraw=False, **kwargs): - if "state" in kwargs: - self.state = kwargs.pop("state") - self.entry.configure(state=self.state) - - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - require_redraw = True - - if "text_color" in kwargs: - self.text_color = kwargs.pop("text_color") - require_redraw = True - - if "border_color" in kwargs: - self.border_color = kwargs.pop("border_color") - require_redraw = True - - if "corner_radius" in kwargs: - self.corner_radius = kwargs.pop("corner_radius") - - if self.corner_radius * 2 > self._current_height: - self.corner_radius = self._current_height / 2 - elif self.corner_radius * 2 > self._current_width: - self.corner_radius = self._current_width / 2 - - self.entry.grid(column=0, row=0, sticky="we", padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6)) - require_redraw = True - - if "width" in kwargs: - self.set_dimensions(width=kwargs.pop("width")) - - if "height" in kwargs: - self.set_dimensions(height=kwargs.pop("height")) - - if "placeholder_text" in kwargs: - self.placeholder_text = kwargs.pop("placeholder_text") - if self.placeholder_text_active: - self.entry.delete(0, tkinter.END) - self.entry.insert(0, self.placeholder_text) - else: - self.activate_placeholder() - - if "placeholder_text_color" in kwargs: - self.placeholder_text_color = kwargs.pop("placeholder_text_color") - require_redraw = True - - if "textvariable" in kwargs: - self.textvariable = kwargs.pop("textvariable") - self.entry.configure(textvariable=self.textvariable) - - if "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - self.entry.configure(font=self.apply_font_scaling(self.text_font)) - - if "show" in kwargs: - if self.placeholder_text_active: - self.pre_placeholder_arguments["show"] = kwargs.pop("show") - else: - self.entry.configure(show=kwargs.pop("show")) - - if "bg_color" in kwargs: - super().configure(bg_color=kwargs.pop("bg_color"), require_redraw=require_redraw) - else: - super().configure(require_redraw=require_redraw) - - self.entry.configure(**kwargs) # pass remaining kwargs to entry - - 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=ThemeManager.single_color(self.placeholder_text_color, self._appearance_mode), show="") - self.entry.delete(0, tkinter.END) - self.entry.insert(0, self.placeholder_text) - - def deactivate_placeholder(self): - if self.placeholder_text_active: - self.placeholder_text_active = False - - self.entry.config(fg=ThemeManager.single_color(self.text_color, self._appearance_mode)) - 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() - - def entry_focus_in(self, event=None): - self.deactivate_placeholder() - - def delete(self, *args, **kwargs): - self.entry.delete(*args, **kwargs) - - if self.entry.get() == "": - self.activate_placeholder() - - def insert(self, *args, **kwargs): - self.deactivate_placeholder() - - return self.entry.insert(*args, **kwargs) - - def get(self): - if self.placeholder_text_active: - return "" - else: - return self.entry.get() - - def focus(self): - self.entry.focus() - - def focus_force(self): - self.entry.focus_force() diff --git a/customtkinter/widgets/ctk_frame.py b/customtkinter/widgets/ctk_frame.py deleted file mode 100644 index 2e5ced7..0000000 --- a/customtkinter/widgets/ctk_frame.py +++ /dev/null @@ -1,132 +0,0 @@ -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkFrame(CTkBaseClass): - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - border_color="default_theme", - border_width="default_theme", - corner_radius="default_theme", - width=200, - height=200, - overwrite_preferred_drawing_method: str = None, - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color - self.border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color - - # determine fg_color of frame - if fg_color == "default_theme": - if isinstance(self.master, CTkFrame): - if self.master.fg_color == ThemeManager.theme["color"]["frame_low"]: - self.fg_color = ThemeManager.theme["color"]["frame_high"] - else: - self.fg_color = ThemeManager.theme["color"]["frame_low"] - else: - self.fg_color = ThemeManager.theme["color"]["frame_low"] - else: - self.fg_color = fg_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width - - self.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._current_width), - height=self.apply_widget_scaling(self._current_height)) - self.canvas.place(x=0, y=0, relwidth=1, relheight=1) - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.draw_engine = DrawEngine(self.canvas) - self._overwrite_preferred_drawing_method = overwrite_preferred_drawing_method - - self.bind('', self.update_dimensions_event) - - self.draw() - - def winfo_children(self): - """ 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): - - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self.corner_radius), - self.apply_widget_scaling(self.border_width), - overwrite_preferred_drawing_method=self._overwrite_preferred_drawing_method) - - if no_color_updates is False or requires_recoloring: - if self.fg_color is None: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.bg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.border_color, self._appearance_mode), - outline=ThemeManager.single_color(self.border_color, self._appearance_mode)) - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - self.canvas.tag_lower("inner_parts") - self.canvas.tag_lower("border_parts") - - def configure(self, require_redraw=False, **kwargs): - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - require_redraw = True - - # check if CTk widgets are children of the frame and change their bg_color to new frame fg_color - for child in self.winfo_children(): - if isinstance(child, CTkBaseClass): - child.configure(bg_color=self.fg_color) - - if "border_color" in kwargs: - self.border_color = kwargs.pop("border_color") - 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 - - if "width" in kwargs: - self.set_dimensions(width=kwargs.pop("width")) - - if "height" in kwargs: - self.set_dimensions(height=kwargs.pop("height")) - - super().configure(require_redraw=require_redraw, **kwargs) diff --git a/customtkinter/widgets/ctk_label.py b/customtkinter/widgets/ctk_label.py deleted file mode 100644 index cac1154..0000000 --- a/customtkinter/widgets/ctk_label.py +++ /dev/null @@ -1,159 +0,0 @@ -import sys -import tkinter - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkLabel(CTkBaseClass): - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - text_color="default_theme", - corner_radius="default_theme", - width=140, - height=28, - text="CTkLabel", - text_font="default_theme", - anchor="center", # label anchor: center, n, e, s, w - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - if "master" in kwargs: - super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master")) - else: - super().__init__(*args, bg_color=bg_color, width=width, height=height) - - # color - self.fg_color = ThemeManager.theme["color"]["label"] if fg_color == "default_theme" else fg_color - if self.fg_color is None: - self.fg_color = self.bg_color - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["label_corner_radius"] if corner_radius == "default_theme" else corner_radius - - # text - self.anchor = anchor - self.text = text - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - - # configure grid system (1x1) - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - - self.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._desired_width), - height=self.apply_widget_scaling(self._desired_height)) - self.canvas.grid(row=0, column=0, sticky="nswe") - self.draw_engine = DrawEngine(self.canvas) - - self.text_label = tkinter.Label(master=self, - highlightthickness=0, - bd=0, - anchor=self.anchor, - text=self.text, - font=self.apply_font_scaling(self.text_font), - **kwargs) - text_label_grid_sticky = self.anchor if self.anchor != "center" else "" - self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius), - sticky=text_label_grid_sticky) - - self.bind('', self.update_dimensions_event) - self.draw() - - def set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height)) - self.text_label.configure(font=self.apply_font_scaling(self.text_font)) - text_label_grid_sticky = self.anchor if self.anchor != "center" else "" - self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius), - sticky=text_label_grid_sticky) - - self.draw() - - def set_dimensions(self, width=None, height=None): - super().set_dimensions(width, height) - - self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), - height=self.apply_widget_scaling(self._desired_height)) - self.draw() - - def draw(self, no_color_updates=False): - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self.corner_radius), - 0) - - if no_color_updates is False or requires_recoloring: - if ThemeManager.single_color(self.fg_color, self._appearance_mode) is not None: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode), - bg=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.bg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode), - bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - def config(self, **kwargs): - sys.stderr.write("Warning: Use .configure() instead of .config()") - self.configure(**kwargs) - - def configure(self, require_redraw=False, **kwargs): - if "anchor" in kwargs: - self.anchor = kwargs.pop("anchor") - text_label_grid_sticky = self.anchor if self.anchor != "center" else "" - self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius), - sticky=text_label_grid_sticky) - - if "text" in kwargs: - self.text = kwargs["text"] - self.text_label.configure(text=self.text) - del kwargs["text"] - - if "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - self.text_label.configure(font=self.apply_font_scaling(self.text_font)) - - if "fg_color" in kwargs: - self.fg_color = kwargs["fg_color"] - require_redraw = True - del kwargs["fg_color"] - - if "text_color" in kwargs: - self.text_color = kwargs["text_color"] - require_redraw = True - del kwargs["text_color"] - - if "width" in kwargs: - self.set_dimensions(width=kwargs["width"]) - del kwargs["width"] - - if "height" in kwargs: - self.set_dimensions(height=kwargs["height"]) - del kwargs["height"] - - if "bg_color" in kwargs: - super().configure(bg_color=kwargs.pop("bg_color"), require_redraw=require_redraw) - else: - super().configure(require_redraw=require_redraw) - - self.text_label.configure(**kwargs) # pass remaining kwargs to label - - def set_text(self, text): - """ Will be removed in the next major release """ - - self.text = text - self.text_label.configure(text=self.text) diff --git a/customtkinter/widgets/ctk_optionmenu.py b/customtkinter/widgets/ctk_optionmenu.py deleted file mode 100644 index cc8601f..0000000 --- a/customtkinter/widgets/ctk_optionmenu.py +++ /dev/null @@ -1,311 +0,0 @@ -import tkinter -import sys - -from .dropdown_menu import DropdownMenu - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..settings import Settings -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkOptionMenu(CTkBaseClass): - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - button_color="default_theme", - button_hover_color="default_theme", - text_color="default_theme", - text_color_disabled="default_theme", - dropdown_color="default_theme", - dropdown_hover_color="default_theme", - dropdown_text_color="default_theme", - variable=None, - values=None, - command=None, - width=140, - height=28, - corner_radius="default_theme", - text_font="default_theme", - dropdown_text_font="default_theme", - hover=True, - state=tkinter.NORMAL, - dynamic_resizing=True, - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color variables - self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color - self.button_color = ThemeManager.theme["color"]["optionmenu_button"] if button_color == "default_theme" else button_color - self.button_hover_color = ThemeManager.theme["color"]["optionmenu_button_hover"] if button_hover_color == "default_theme" else button_hover_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius - - # text and font - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - self.dropdown_text_font = dropdown_text_font - - # callback and hover functionality - self.command = command - self.variable = variable - self.variable_callback_blocked = False - self.variable_callback_name = 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_color, - hover_color=dropdown_hover_color, - text_color=dropdown_text_color, - text_font=dropdown_text_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, rowspan=1, columnspan=1, sticky="nsew") - self.draw_engine = DrawEngine(self.canvas) - - left_section_width = self._current_width - self._current_height - self.text_label = tkinter.Label(master=self, - font=self.apply_font_scaling(self.text_font), - anchor="w", - text=self.current_value) - self.text_label.grid(row=0, column=0, sticky="w", - 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)))) - - if not self.dynamic_resizing: - self.grid_propagate(0) - - if Settings.cursor_manipulation_enabled: - if sys.platform == "darwin": - self.configure(cursor="pointinghand") - elif sys.platform.startswith("win"): - self.configure(cursor="hand2") - - # event bindings - self.canvas.bind("", self.on_enter) - self.canvas.bind("", self.on_leave) - self.canvas.bind("", self.clicked) - self.canvas.bind("", self.clicked) - - self.text_label.bind("", self.on_enter) - self.text_label.bind("", self.on_leave) - self.text_label.bind("", self.clicked) - self.text_label.bind("", self.clicked) - - self.bind('', self.update_dimensions_event) - - 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 set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - # change label text size and grid padding - left_section_width = self._current_width - self._current_height - self.text_label.configure(font=self.apply_font_scaling(self.text_font)) - self.text_label.grid(row=0, column=0, sticky="w", - 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)))) - - 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: 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 draw(self, no_color_updates=False): - left_section_width = self._current_width - self._current_height - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self.corner_radius), - 0, - self.apply_widget_scaling(left_section_width)) - - requires_recoloring_2 = self.draw_engine.draw_dropdown_arrow(self.apply_widget_scaling(self._current_width - (self._current_height / 2)), - self.apply_widget_scaling(self._current_height / 2), - self.apply_widget_scaling(self._current_height / 3)) - - if no_color_updates is False or requires_recoloring or requires_recoloring_2: - - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - self.canvas.itemconfig("inner_parts_left", - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - self.canvas.itemconfig("inner_parts_right", - outline=ThemeManager.single_color(self.button_color, self._appearance_mode), - fill=ThemeManager.single_color(self.button_color, self._appearance_mode)) - - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - if self.state == tkinter.DISABLED: - self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))) - self.canvas.itemconfig("dropdown_arrow", - fill=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)) - else: - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode)) - self.canvas.itemconfig("dropdown_arrow", - fill=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - self.text_label.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - self.canvas.update_idletasks() - - 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 "state" in kwargs: - self.state = kwargs.pop("state") - require_redraw = True - - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - require_redraw = True - - if "button_color" in kwargs: - self.button_color = kwargs.pop("button_color") - require_redraw = True - - if "button_hover_color" in kwargs: - self.button_hover_color = kwargs.pop("button_hover_color") - require_redraw = True - - if "text_color" in kwargs: - self.text_color = kwargs.pop("text_color") - require_redraw = True - - if "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - self.text_label.configure(font=self.apply_font_scaling(self.text_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 "width" in kwargs: - self.set_dimensions(width=kwargs.pop("width")) - - if "height" in kwargs: - self.set_dimensions(height=kwargs.pop("height")) - - if "values" in kwargs: - self.values = kwargs.pop("values") - self.dropdown_menu.configure(values=self.values) - - if "dropdown_color" in kwargs: - self.dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color")) - - if "dropdown_hover_color" in kwargs: - self.dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color")) - - if "dropdown_text_color" in kwargs: - self.dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color")) - - if "dropdown_text_font" in kwargs: - self.dropdown_text_font = kwargs.pop("dropdown_text_font") - self.dropdown_menu.configure(text_font=self.dropdown_text_font) - - 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) - - super().configure(require_redraw=require_redraw, **kwargs) - - 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=ThemeManager.single_color(self.button_hover_color, self._appearance_mode), - fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode)) - - def on_leave(self, event=0): - if self.hover is True: - # set color of inner button parts - self.canvas.itemconfig("inner_parts_right", - outline=ThemeManager.single_color(self.button_color, self._appearance_mode), - fill=ThemeManager.single_color(self.button_color, self._appearance_mode)) - - 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() diff --git a/customtkinter/widgets/ctk_progressbar.py b/customtkinter/widgets/ctk_progressbar.py deleted file mode 100644 index 1c9494f..0000000 --- a/customtkinter/widgets/ctk_progressbar.py +++ /dev/null @@ -1,257 +0,0 @@ -import tkinter -import math - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkProgressBar(CTkBaseClass): - """ tkinter custom progressbar, values from 0 to 1 """ - - def __init__(self, *args, - variable=None, - bg_color=None, - border_color="default_theme", - fg_color="default_theme", - progress_color="default_theme", - corner_radius="default_theme", - width=None, - height=None, - border_width="default_theme", - orient="horizontal", - mode="determinate", - determinate_speed=1, - indeterminate_speed=1, - **kwargs): - - # set default dimensions according to orientation - if width is None: - if orient.lower() == "vertical": - width = 8 - else: - width = 200 - if height is None: - if orient.lower() == "vertical": - height = 200 - else: - height = 8 - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color - self.border_color = ThemeManager.theme["color"]["progressbar_border"] if border_color == "default_theme" else border_color - self.fg_color = ThemeManager.theme["color"]["progressbar"] if fg_color == "default_theme" else fg_color - self.progress_color = ThemeManager.theme["color"]["progressbar_progress"] if progress_color == "default_theme" else progress_color - - # control variable - self.variable = variable - self.variable_callback_blocked = False - self.variable_callback_name = None - - # shape - self.corner_radius = ThemeManager.theme["shape"]["progressbar_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_width = ThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width - self.determinate_value = 0.5 # range 0-1 - self.determinate_speed = determinate_speed # range 0-1 - self.indeterminate_value = 0 # range 0-inf - self.indeterminate_width = 0.4 # range 0-1 - self.indeterminate_speed = indeterminate_speed # range 0-1 to travel in 50ms - self.loop_running = False - self.orient = orient - 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) - - # Each time an item is resized due to pack position mode, the binding Configure is called on the widget - self.bind('', self.update_dimensions_event) - - 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() - - def set_dimensions(self, width=None, height=None): - super().set_dimensions(width, height) - - self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), - height=self.apply_widget_scaling(self._desired_height)) - self.draw() - - def destroy(self): - if self.variable is not None: - self.variable.trace_remove("write", self.variable_callback_name) - - super().destroy() - - def draw(self, no_color_updates=False): - if self.orient.lower() == "horizontal": - orientation = "w" - elif self.orient.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=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.border_color, self._appearance_mode), - outline=ThemeManager.single_color(self.border_color, self._appearance_mode)) - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - self.canvas.itemconfig("progress_parts", - fill=ThemeManager.single_color(self.progress_color, self._appearance_mode), - outline=ThemeManager.single_color(self.progress_color, self._appearance_mode)) - - def configure(self, require_redraw=False, **kwargs): - if "fg_color" in kwargs: - self.fg_color = kwargs["fg_color"] - del kwargs["fg_color"] - require_redraw = True - - if "border_color" in kwargs: - self.border_color = kwargs["border_color"] - del kwargs["border_color"] - require_redraw = True - - if "progress_color" in kwargs: - self.progress_color = kwargs["progress_color"] - del kwargs["progress_color"] - require_redraw = True - - if "border_width" in kwargs: - self.border_width = kwargs["border_width"] - del kwargs["border_width"] - require_redraw = True - - if "variable" in kwargs: - if self.variable is not None: - self.variable.trace_remove("write", self.variable_callback_name) - - self.variable = kwargs["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 - - del kwargs["variable"] - - 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") - - if "width" in kwargs: - self.set_dimensions(width=kwargs["width"]) - del kwargs["width"] - - if "height" in kwargs: - self.set_dimensions(height=kwargs["height"]) - del kwargs["height"] - - super().configure(require_redraw=require_redraw, **kwargs) - - 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): - """ get determinate value """ - return self.determinate_value - - def start(self): - """ start indeterminate mode """ - if not self.loop_running: - self.loop_running = True - self.internal_loop() - - def stop(self): - """ stop indeterminate mode """ - self.loop_running = False - - def internal_loop(self): - if self.loop_running: - if self.mode == "determinate": - self.determinate_value += self.determinate_speed / 50 - if self.determinate_value > 1: - self.determinate_value -= 1 - self.draw() - self.after(20, self.internal_loop) - else: - self.indeterminate_value += self.indeterminate_speed - self.draw() - self.after(20, self.internal_loop) - - def step(self): - if self.mode == "determinate": - self.determinate_value += self.determinate_speed / 50 - if self.determinate_value > 1: - self.determinate_value -= 1 - self.draw() - else: - self.indeterminate_value += self.indeterminate_speed - self.draw() diff --git a/customtkinter/widgets/ctk_radiobutton.py b/customtkinter/widgets/ctk_radiobutton.py deleted file mode 100644 index 6082af1..0000000 --- a/customtkinter/widgets/ctk_radiobutton.py +++ /dev/null @@ -1,281 +0,0 @@ -import tkinter -import sys -from typing import Union - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..settings import Settings -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkRadioButton(CTkBaseClass): - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - hover_color="default_theme", - border_color="default_theme", - border_width_unchecked="default_theme", - border_width_checked="default_theme", - width=22, - height=22, - corner_radius="default_theme", - text_font="default_theme", - text_color="default_theme", - text="CTkRadioButton", - text_color_disabled="default_theme", - hover=True, - command=None, - state=tkinter.NORMAL, - value=0, - variable=None, - textvariable=None, - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color - self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color - self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color - self.border_color = ThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["radiobutton_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_width_unchecked = ThemeManager.theme["shape"]["radiobutton_border_width_unchecked"] if border_width_unchecked == "default_theme" else border_width_unchecked - self.border_width_checked = ThemeManager.theme["shape"]["radiobutton_border_width_checked"] if border_width_checked == "default_theme" else border_width_checked - self.border_width = self.border_width_unchecked - - # text - self.text = text - self.text_label: Union[tkinter.Label, None] = None - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - - # callback and control variables - self.command = command - self.state = state - self.hover = hover - self.check_state = False - self.value = value - self.variable: tkinter.Variable = variable - self.variable_callback_blocked = False - self.textvariable = textvariable - self.variable_callback_name = None - - # configure grid system (3x1) - self.grid_columnconfigure(0, weight=0) - self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) - self.grid_columnconfigure(2, weight=1) - - self.bg_canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._current_width), - height=self.apply_widget_scaling(self._current_height)) - self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe") - - self.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._current_width), - height=self.apply_widget_scaling(self._current_height)) - self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1) - self.draw_engine = DrawEngine(self.canvas) - - self.canvas.bind("", self.on_enter) - self.canvas.bind("", self.on_leave) - self.canvas.bind("", self.invoke) - - self.text_label = tkinter.Label(master=self, - bd=0, - text=self.text, - justify=tkinter.LEFT, - font=self.apply_font_scaling(self.text_font), - textvariable=self.textvariable) - self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w") - self.text_label["anchor"] = "w" - - self.text_label.bind("", self.on_enter) - self.text_label.bind("", self.on_leave) - self.text_label.bind("", self.invoke) - - if self.variable is not None: - self.variable_callback_name = self.variable.trace_add("write", self.variable_callback) - self.check_state = True if self.variable.get() == self.value else False - - self.draw() # initial draw - self.set_cursor() - - def set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) - self.text_label.configure(font=self.apply_font_scaling(self.text_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._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): - 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)) - - self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - if self.check_state is False: - self.canvas.itemconfig("border_parts", - outline=ThemeManager.single_color(self.border_color, self._appearance_mode), - fill=ThemeManager.single_color(self.border_color, self._appearance_mode)) - else: - self.canvas.itemconfig("border_parts", - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - self.canvas.itemconfig("inner_parts", - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode), - fill=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - if self.state == tkinter.DISABLED: - self.text_label.configure(fg=ThemeManager.single_color(self.text_color_disabled, self._appearance_mode)) - else: - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - def configure(self, require_redraw=False, **kwargs): - if "text" in kwargs: - self.text = kwargs.pop("text") - self.text_label.configure(text=self.text) - - if "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - self.text_label.configure(font=self.apply_font_scaling(self.text_font)) - - if "state" in kwargs: - self.state = kwargs.pop("state") - self.set_cursor() - require_redraw = True - - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - require_redraw = True - - if "hover_color" in kwargs: - self.hover_color = kwargs.pop("hover_color") - require_redraw = True - - if "text_color" in kwargs: - self.text_color = kwargs.pop("text_color") - require_redraw = True - - if "border_color" in kwargs: - self.border_color = kwargs.pop("border_color") - require_redraw = True - - if "border_width" in kwargs: - self.border_width = kwargs.pop("border_width") - require_redraw = True - - if "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 set_cursor(self): - if Settings.cursor_manipulation_enabled: - if self.state == tkinter.DISABLED: - if sys.platform == "darwin" and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="arrow") - if self.text_label is not None: - self.text_label.configure(cursor="arrow") - elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled: - 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" and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="pointinghand") - if self.text_label is not None: - self.text_label.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled: - 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=ThemeManager.single_color(self.hover_color, self._appearance_mode), - outline=ThemeManager.single_color(self.hover_color, self._appearance_mode)) - - def on_leave(self, event=0): - if self.hover is True: - if self.check_state is True: - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.border_color, self._appearance_mode), - outline=ThemeManager.single_color(self.border_color, self._appearance_mode)) - - def variable_callback(self, var_name, index, mode): - if not self.variable_callback_blocked: - if self.variable.get() == self.value: - self.select(from_variable_callback=True) - else: - self.deselect(from_variable_callback=True) - - def invoke(self, event=0): - if self.state == tkinter.NORMAL: - if self.check_state is False: - self.check_state = True - self.select() - - if self.command is not None: - self.command() - - def select(self, from_variable_callback=False): - self.check_state = True - self.border_width = self.border_width_checked - self.draw() - - if self.variable is not None and not from_variable_callback: - self.variable_callback_blocked = True - self.variable.set(self.value) - self.variable_callback_blocked = False - - def deselect(self, from_variable_callback=False): - self.check_state = False - self.border_width = self.border_width_unchecked - self.draw() - - if self.variable is not None and not from_variable_callback: - self.variable_callback_blocked = True - self.variable.set("") - self.variable_callback_blocked = False diff --git a/customtkinter/widgets/ctk_scrollbar.py b/customtkinter/widgets/ctk_scrollbar.py deleted file mode 100644 index 6d60fac..0000000 --- a/customtkinter/widgets/ctk_scrollbar.py +++ /dev/null @@ -1,225 +0,0 @@ -import sys - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkScrollbar(CTkBaseClass): - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - scrollbar_color="default_theme", - scrollbar_hover_color="default_theme", - border_spacing="default_theme", - corner_radius="default_theme", - width=None, - height=None, - minimum_pixel_length=20, - orientation="vertical", - command=None, - hover=True, - **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__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color - self.fg_color = ThemeManager.theme["color"]["frame_high"] if fg_color == "default_theme" else fg_color - self.scrollbar_color = ThemeManager.theme["color"]["scrollbar_button"] if scrollbar_color == "default_theme" else scrollbar_color - self.scrollbar_hover_color = ThemeManager.theme["color"]["scrollbar_button_hover"] if scrollbar_hover_color == "default_theme" else scrollbar_hover_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["scrollbar_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_spacing = ThemeManager.theme["shape"]["scrollbar_border_spacing"] if border_spacing == "default_theme" else border_spacing - - self.hover = hover - self.hover_state = False - self.command = command - self.orientation = orientation - self.start_value: float = 0 # 0 to 1 - self.end_value: float = 1 # 0 to 1 - self.minimum_pixel_length = minimum_pixel_length - - self.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._current_width), - height=self.apply_widget_scaling(self._current_height)) - self.canvas.place(x=0, y=0, relwidth=1, relheight=1) - self.draw_engine = DrawEngine(self.canvas) - - self.canvas.bind("", self.on_enter) - self.canvas.bind("", self.on_leave) - self.canvas.tag_bind("border_parts", "", self.clicked) - self.canvas.bind("", self.clicked) - self.canvas.bind("", self.mouse_scroll_event) - self.bind('', self.update_dimensions_event) - - self.draw() - - def set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height)) - self.draw(no_color_updates=True) - - def set_dimensions(self, width=None, height=None): - super().set_dimensions(width, height) - - self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), - height=self.apply_widget_scaling(self._desired_height)) - self.draw(no_color_updates=True) - - def get_scrollbar_values_for_minimum_pixel_size(self): - # correct scrollbar float values if scrollbar is too small - if self.orientation == "vertical": - scrollbar_pixel_length = (self.end_value - self.start_value) * self._current_height - if scrollbar_pixel_length < self.minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0: - # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length - interval_extend_factor = (-scrollbar_pixel_length + self.minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height) - corrected_end_value = self.end_value + (1 - self.end_value) * interval_extend_factor - corrected_start_value = self.start_value - self.start_value * interval_extend_factor - return corrected_start_value, corrected_end_value - else: - return self.start_value, self.end_value - - else: - scrollbar_pixel_length = (self.end_value - self.start_value) * self._current_width - if scrollbar_pixel_length < self.minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0: - # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length - interval_extend_factor = (-scrollbar_pixel_length + self.minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width) - corrected_end_value = self.end_value + (1 - self.end_value) * interval_extend_factor - corrected_start_value = self.start_value - self.start_value * interval_extend_factor - return corrected_start_value, corrected_end_value - else: - return self.start_value, self.end_value - - def draw(self, no_color_updates=False): - corrected_start_value, corrected_end_value = self.get_scrollbar_values_for_minimum_pixel_size() - requires_recoloring = self.draw_engine.draw_rounded_scrollbar(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self.corner_radius), - self.apply_widget_scaling(self.border_spacing), - corrected_start_value, - corrected_end_value, - self.orientation) - - if no_color_updates is False or requires_recoloring: - if self.hover_state is True: - self.canvas.itemconfig("scrollbar_parts", - fill=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode), - outline=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode)) - else: - self.canvas.itemconfig("scrollbar_parts", - fill=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode), - outline=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode)) - - if self.fg_color is None: - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.bg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - else: - self.canvas.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - self.canvas.update_idletasks() - - 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 configure(self, require_redraw=False, **kwargs): - if "fg_color" in kwargs: - self.fg_color = kwargs["fg_color"] - require_redraw = True - del kwargs["fg_color"] - - if "scrollbar_color" in kwargs: - self.scrollbar_color = kwargs["scrollbar_color"] - require_redraw = True - del kwargs["scrollbar_color"] - - if "scrollbar_hover_color" in kwargs: - self.scrollbar_hover_color = kwargs["scrollbar_hover_color"] - require_redraw = True - del kwargs["scrollbar_hover_color"] - - if "command" in kwargs: - self.command = kwargs["command"] - del kwargs["command"] - - if "corner_radius" in kwargs: - self.corner_radius = kwargs["corner_radius"] - require_redraw = True - del kwargs["corner_radius"] - - if "border_spacing" in kwargs: - self.border_spacing = kwargs["border_spacing"] - require_redraw = True - del kwargs["border_spacing"] - - if "width" in kwargs: - self.set_dimensions(width=kwargs["width"]) - del kwargs["width"] - - if "height" in kwargs: - self.set_dimensions(height=kwargs["height"]) - del kwargs["height"] - - super().configure(require_redraw=require_redraw, **kwargs) - - def on_enter(self, event=0): - if self.hover is True: - self.hover_state = True - self.canvas.itemconfig("scrollbar_parts", - outline=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode), - fill=ThemeManager.single_color(self.scrollbar_hover_color, self._appearance_mode)) - - def on_leave(self, event=0): - self.hover_state = False - self.canvas.itemconfig("scrollbar_parts", - outline=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode), - fill=ThemeManager.single_color(self.scrollbar_color, self._appearance_mode)) - - def clicked(self, event): - if self.orientation == "vertical": - value = ((event.y - self.border_spacing) / (self._current_height - 2 * self.border_spacing)) / self._widget_scaling - else: - value = ((event.x - self.border_spacing) / (self._current_width - 2 * self.border_spacing)) / self._widget_scaling - - current_scrollbar_length = self.end_value - self.start_value - value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2))) - self.start_value = value - (current_scrollbar_length / 2) - self.end_value = value + (current_scrollbar_length / 2) - self.draw() - - if self.command is not None: - self.command('moveto', self.start_value) - - def mouse_scroll_event(self, event=None): - if self.command is not None: - if sys.platform.startswith("win"): - self.command('scroll', -int(event.delta/40), 'units') - else: - self.command('scroll', -event.delta, 'units') - diff --git a/customtkinter/widgets/ctk_slider.py b/customtkinter/widgets/ctk_slider.py deleted file mode 100644 index 5978f13..0000000 --- a/customtkinter/widgets/ctk_slider.py +++ /dev/null @@ -1,339 +0,0 @@ -import tkinter -import sys - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..settings import Settings -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkSlider(CTkBaseClass): - """ tkinter custom slider""" - - def __init__(self, *args, - bg_color=None, - border_color=None, - fg_color="default_theme", - progress_color="default_theme", - button_color="default_theme", - button_hover_color="default_theme", - from_=0, - to=1, - number_of_steps=None, - width=None, - height=None, - corner_radius="default_theme", - button_corner_radius="default_theme", - border_width="default_theme", - button_length="default_theme", - command=None, - variable=None, - orient="horizontal", - state="normal", - **kwargs): - - # set default dimensions according to orientation - if width is None: - if orient.lower() == "vertical": - width = 16 - else: - width = 200 - if height is None: - if orient.lower() == "vertical": - height = 200 - else: - height = 16 - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color - self.border_color = border_color - self.fg_color = ThemeManager.theme["color"]["slider"] if fg_color == "default_theme" else fg_color - self.progress_color = ThemeManager.theme["color"]["slider_progress"] if progress_color == "default_theme" else progress_color - self.button_color = ThemeManager.theme["color"]["slider_button"] if button_color == "default_theme" else button_color - self.button_hover_color = ThemeManager.theme["color"]["slider_button_hover"] if button_hover_color == "default_theme" else button_hover_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["slider_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.button_corner_radius = ThemeManager.theme["shape"]["slider_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius - self.border_width = ThemeManager.theme["shape"]["slider_border_width"] if border_width == "default_theme" else border_width - self.button_length = ThemeManager.theme["shape"]["slider_button_length"] if button_length == "default_theme" else button_length - self.value = 0.5 # initial value of slider in percent - self.orientation = orient - self.hover_state = False - 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 = False - self.variable_callback_name = None - self.state = state - - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - - self.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._desired_width), - height=self.apply_widget_scaling(self._desired_height)) - self.canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe") - self.draw_engine = DrawEngine(self.canvas) - - self.canvas.bind("", self.on_enter) - self.canvas.bind("", self.on_leave) - self.canvas.bind("", self.clicked) - self.canvas.bind("", self.clicked) - - # Each time an item is resized due to pack position mode, the binding Configure is called on the widget - self.bind('', self.update_dimensions_event) - - self.set_cursor() - self.draw() # initial draw - - if self.variable is not None: - self.variable_callback_name = self.variable.trace_add("write", self.variable_callback) - self.variable_callback_blocked = True - self.set(self.variable.get(), from_variable_callback=True) - self.variable_callback_blocked = False - - def set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height)) - self.draw() - - 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 Settings.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 Settings.cursor_manipulation_enabled: - if sys.platform == "darwin": - self.configure(cursor="arrow") - elif sys.platform.startswith("win"): - self.configure(cursor="arrow") - - def draw(self, no_color_updates=False): - if self.orientation.lower() == "horizontal": - orientation = "w" - elif self.orientation.lower() == "vertical": - orientation = "s" - else: - orientation = "w" - - requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self.corner_radius), - self.apply_widget_scaling(self.border_width), - self.apply_widget_scaling(self.button_length), - self.apply_widget_scaling(self.button_corner_radius), - self.value, orientation) - - if no_color_updates is False or requires_recoloring: - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - if self.border_color is None: - self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.bg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.border_color, self._appearance_mode), - outline=ThemeManager.single_color(self.border_color, self._appearance_mode)) - - self.canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - if self.progress_color is None: - self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.progress_color, self._appearance_mode), - outline=ThemeManager.single_color(self.progress_color, self._appearance_mode)) - - if self.hover_state is True: - self.canvas.itemconfig("slider_parts", - fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode), - outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode)) - else: - self.canvas.itemconfig("slider_parts", - fill=ThemeManager.single_color(self.button_color, self._appearance_mode), - outline=ThemeManager.single_color(self.button_color, self._appearance_mode)) - - def clicked(self, event=None): - if self.state == "normal": - if self.orientation.lower() == "horizontal": - self.value = (event.x / self._current_width) / self._widget_scaling - else: - self.value = 1 - (event.y / self._current_height) / self._widget_scaling - - if self.value > 1: - self.value = 1 - if self.value < 0: - self.value = 0 - - self.output_value = self.round_to_step_size(self.from_ + (self.value * (self.to - self.from_))) - self.value = (self.output_value - self.from_) / (self.to - self.from_) - - self.draw(no_color_updates=False) - - if self.variable is not None: - self.variable_callback_blocked = True - self.variable.set(round(self.output_value) if isinstance(self.variable, tkinter.IntVar) else self.output_value) - self.variable_callback_blocked = False - - if self.command is not None: - self.command(self.output_value) - - def on_enter(self, event=0): - if self.state == "normal": - self.hover_state = True - self.canvas.itemconfig("slider_parts", - fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode), - outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode)) - - def on_leave(self, event=0): - self.hover_state = False - self.canvas.itemconfig("slider_parts", - fill=ThemeManager.single_color(self.button_color, self._appearance_mode), - outline=ThemeManager.single_color(self.button_color, self._appearance_mode)) - - def round_to_step_size(self, value): - 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): - 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 configure(self, require_redraw=False, **kwargs): - if "state" in kwargs: - self.state = kwargs["state"] - self.set_cursor() - require_redraw = True - del kwargs["state"] - - if "fg_color" in kwargs: - self.fg_color = kwargs["fg_color"] - require_redraw = True - del kwargs["fg_color"] - - if "progress_color" in kwargs: - if kwargs["progress_color"] is None: - self.progress_color = self.fg_color - else: - self.progress_color = kwargs["progress_color"] - require_redraw = True - del kwargs["progress_color"] - - if "button_color" in kwargs: - self.button_color = kwargs["button_color"] - require_redraw = True - del kwargs["button_color"] - - if "button_hover_color" in kwargs: - self.button_hover_color = kwargs["button_hover_color"] - require_redraw = True - del kwargs["button_hover_color"] - - if "border_color" in kwargs: - self.border_color = kwargs["border_color"] - require_redraw = True - del kwargs["border_color"] - - if "border_width" in kwargs: - self.border_width = kwargs["border_width"] - require_redraw = True - del kwargs["border_width"] - - if "from_" in kwargs: - self.from_ = kwargs["from_"] - del kwargs["from_"] - - if "to" in kwargs: - self.to = kwargs["to"] - del kwargs["to"] - - if "number_of_steps" in kwargs: - self.number_of_steps = kwargs["number_of_steps"] - del kwargs["number_of_steps"] - - if "command" in kwargs: - self.command = kwargs["command"] - del kwargs["command"] - - if "variable" in kwargs: - if self.variable is not None: - self.variable.trace_remove("write", self.variable_callback_name) - - self.variable = kwargs["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 - - del kwargs["variable"] - - if "width" in kwargs: - self.set_dimensions(width=kwargs["width"]) - del kwargs["width"] - - if "height" in kwargs: - self.set_dimensions(height=kwargs["height"]) - del kwargs["height"] - - super().configure(require_redraw=require_redraw, **kwargs) diff --git a/customtkinter/widgets/ctk_switch.py b/customtkinter/widgets/ctk_switch.py deleted file mode 100644 index 1f58518..0000000 --- a/customtkinter/widgets/ctk_switch.py +++ /dev/null @@ -1,324 +0,0 @@ -import tkinter -import sys - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..settings import Settings -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkSwitch(CTkBaseClass): - def __init__(self, *args, - text="CTkSwitch", - text_font="default_theme", - text_color="default_theme", - text_color_disabled="default_theme", - bg_color=None, - border_color=None, - fg_color="default_theme", - progress_color="default_theme", - button_color="default_theme", - button_hover_color="default_theme", - width=36, - height=18, - corner_radius="default_theme", - # button_corner_radius="default_theme", - border_width="default_theme", - button_length="default_theme", - command=None, - onvalue=1, - offvalue=0, - variable=None, - textvariable=None, - state=tkinter.NORMAL, - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - - # color - self.border_color = border_color - self.fg_color = ThemeManager.theme["color"]["switch"] if fg_color == "default_theme" else fg_color - self.progress_color = ThemeManager.theme["color"]["switch_progress"] if progress_color == "default_theme" else progress_color - self.button_color = ThemeManager.theme["color"]["switch_button"] if button_color == "default_theme" else button_color - self.button_hover_color = ThemeManager.theme["color"]["switch_button_hover"] if button_hover_color == "default_theme" else button_hover_color - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self.text_color_disabled = ThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled - - # text - self.text = text - self.text_label = None - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - - # shape - self.corner_radius = ThemeManager.theme["shape"]["switch_corner_radius"] if corner_radius == "default_theme" else corner_radius - # self.button_corner_radius = ThemeManager.theme["shape"]["switch_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius - self.border_width = ThemeManager.theme["shape"]["switch_border_width"] if border_width == "default_theme" else border_width - self.button_length = ThemeManager.theme["shape"]["switch_button_length"] if button_length == "default_theme" else button_length - self.hover_state = False - self.check_state = False # True if switch is activated - self.state = state - self.onvalue = onvalue - self.offvalue = offvalue - - # callback and control variables - self.command = command - self.variable: tkinter.Variable = variable - self.variable_callback_blocked = False - self.variable_callback_name = None - self.textvariable = textvariable - - # configure grid system (3x1) - self.grid_columnconfigure(0, weight=1) - self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) - self.grid_columnconfigure(2, weight=0) - - 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, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe") - - self.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._current_width), - height=self.apply_widget_scaling(self._current_height)) - self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, sticky="nswe") - self.draw_engine = DrawEngine(self.canvas) - - self.canvas.bind("", self.on_enter) - self.canvas.bind("", self.on_leave) - self.canvas.bind("", self.toggle) - - self.text_label = tkinter.Label(master=self, - bd=0, - text=self.text, - justify=tkinter.LEFT, - font=self.apply_font_scaling(self.text_font), - textvariable=self.textvariable) - self.text_label.grid(row=0, column=2, padx=0, pady=0, sticky="w") - self.text_label["anchor"] = "w" - - self.text_label.bind("", self.on_enter) - self.text_label.bind("", self.on_leave) - self.text_label.bind("", self.toggle) - - if self.variable is not None and self.variable != "": - self.variable_callback_name = self.variable.trace_add("write", self.variable_callback) - self.check_state = True if self.variable.get() == self.onvalue else False - - self.draw() # initial draw - self.set_cursor() - - def set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) - self.text_label.configure(font=self.apply_font_scaling(self.text_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._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 Settings.cursor_manipulation_enabled: - if self.state == tkinter.DISABLED: - if sys.platform == "darwin" and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="arrow") - if self.text_label is not None: - self.text_label.configure(cursor="arrow") - elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled: - 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" and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="pointinghand") - if self.text_label is not None: - self.text_label.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="hand2") - if self.text_label is not None: - self.text_label.configure(cursor="hand2") - - def draw(self, no_color_updates=False): - - if self.check_state is True: - requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self._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.corner_radius), - 1, "w") - else: - 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.corner_radius), - 0, "w") - - if no_color_updates is False or requires_recoloring: - self.bg_canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - if self.border_color is None: - self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.bg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("border_parts", fill=ThemeManager.single_color(self.border_color, self._appearance_mode), - outline=ThemeManager.single_color(self.border_color, self._appearance_mode)) - - self.canvas.itemconfig("inner_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - if self.progress_color is None: - self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("progress_parts", fill=ThemeManager.single_color(self.progress_color, self._appearance_mode), - outline=ThemeManager.single_color(self.progress_color, self._appearance_mode)) - - self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self._appearance_mode), - outline=ThemeManager.single_color(self.button_color, self._appearance_mode)) - - if self.state == tkinter.DISABLED: - self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self._appearance_mode))) - else: - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - 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): - return self.onvalue if self.check_state is True else self.offvalue - - def on_enter(self, event=0): - self.hover_state = True - - if self.state is not tkinter.DISABLED: - self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_hover_color, self._appearance_mode), - outline=ThemeManager.single_color(self.button_hover_color, self._appearance_mode)) - - def on_leave(self, event=0): - self.hover_state = False - self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_color, self._appearance_mode), - outline=ThemeManager.single_color(self.button_color, self._appearance_mode)) - - 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 configure(self, require_redraw=False, **kwargs): - if "text" in kwargs: - self.text = kwargs.pop("text") - self.text_label.configure(text=self.text) - - if "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - self.text_label.configure(font=self.apply_font_scaling(self.text_font)) - - if "state" in kwargs: - self.state = kwargs.pop("state") - self.set_cursor() - require_redraw = True - - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - require_redraw = True - - if "progress_color" in kwargs: - new_progress_color = kwargs.pop("progress_color") - if new_progress_color is None: - self.progress_color = self.fg_color - else: - self.progress_color = new_progress_color - require_redraw = True - - if "button_color" in kwargs: - self.button_color = kwargs.pop("button_color") - require_redraw = True - - if "button_hover_color" in kwargs: - self.button_hover_color = kwargs.pop("button_hover_color") - require_redraw = True - - if "border_color" in kwargs: - self.border_color = kwargs.pop("border_color") - require_redraw = True - - if "border_width" in kwargs: - self.border_width = kwargs.pop("border_width") - require_redraw = True - - if "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) diff --git a/customtkinter/widgets/ctk_textbox.py b/customtkinter/widgets/ctk_textbox.py deleted file mode 100644 index bad0d35..0000000 --- a/customtkinter/widgets/ctk_textbox.py +++ /dev/null @@ -1,176 +0,0 @@ -import tkinter - -from .ctk_canvas import CTkCanvas -from ..theme_manager import ThemeManager -from ..draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass - - -class CTkTextbox(CTkBaseClass): - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - border_color="default_theme", - border_width="default_theme", - corner_radius="default_theme", - text_font="default_theme", - text_color="default_theme", - width=200, - height=200, - **kwargs): - - # transfer basic functionality (bg_color, size, _appearance_mode, scaling) to CTkBaseClass - if "master" in kwargs: - super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs.pop("master")) - else: - super().__init__(*args, bg_color=bg_color, width=width, height=height) - - # color - self.fg_color = ThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color - self.border_color = ThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - - # shape - self.corner_radius = ThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_width = ThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width - - # text - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - - # configure 1x1 grid - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - - self.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.apply_widget_scaling(self._current_width), - height=self.apply_widget_scaling(self._current_height)) - self.canvas.grid(row=0, column=0, padx=0, pady=0, rowspan=1, columnspan=1, sticky="nsew") - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - self.draw_engine = DrawEngine(self.canvas) - - for arg in ["highlightthickness", "fg", "bg", "font", "width", "height"]: - kwargs.pop(arg, None) - self.textbox = tkinter.Text(self, - fg=ThemeManager.single_color(self.text_color, self._appearance_mode), - width=0, - height=0, - font=self.text_font, - highlightthickness=0, - relief="flat", - insertbackground=ThemeManager.single_color(("black", "white"), self._appearance_mode), - bg=ThemeManager.single_color(self.fg_color, self._appearance_mode), - **kwargs) - self.textbox.grid(row=0, column=0, padx=self.corner_radius, pady=self.corner_radius, rowspan=1, columnspan=1, sticky="nsew") - - self.bind('', self.update_dimensions_event) - self.draw() - - def set_scaling(self, *args, **kwargs): - super().set_scaling(*args, **kwargs) - - self.textbox.configure(font=self.apply_font_scaling(self.text_font)) - self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), height=self.apply_widget_scaling(self._desired_height)) - self.draw() - - def set_dimensions(self, width=None, height=None): - super().set_dimensions(width, height) - - self.canvas.configure(width=self.apply_widget_scaling(self._desired_width), - height=self.apply_widget_scaling(self._desired_height)) - self.draw() - - def draw(self, no_color_updates=False): - - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self._current_width), - self.apply_widget_scaling(self._current_height), - self.apply_widget_scaling(self.corner_radius), - self.apply_widget_scaling(self.border_width)) - - if no_color_updates is False or requires_recoloring: - if self.fg_color is None: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.bg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - else: - self.canvas.itemconfig("inner_parts", - fill=ThemeManager.single_color(self.fg_color, self._appearance_mode), - outline=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - self.canvas.itemconfig("border_parts", - fill=ThemeManager.single_color(self.border_color, self._appearance_mode), - outline=ThemeManager.single_color(self.border_color, self._appearance_mode)) - self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - self.textbox.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode), - bg=ThemeManager.single_color(self.fg_color, self._appearance_mode), - insertbackground=ThemeManager.single_color(("black", "white"), self._appearance_mode)) - - self.canvas.tag_lower("inner_parts") - self.canvas.tag_lower("border_parts") - - def yview(self, *args): - return self.textbox.yview(*args) - - def xview(self, *args): - return self.textbox.xview(*args) - - def insert(self, *args, **kwargs): - return self.textbox.insert(*args, **kwargs) - - def focus(self): - return self.textbox.focus() - - def tag_add(self, *args, **kwargs): - return self.textbox.tag_add(*args, **kwargs) - - def tag_config(self, *args, **kwargs): - return self.textbox.tag_config(*args, **kwargs) - - def tag_configure(self, *args, **kwargs): - return self.textbox.tag_configure(*args, **kwargs) - - def tag_remove(self, *args, **kwargs): - return self.textbox.tag_remove(*args, **kwargs) - - def configure(self, require_redraw=False, **kwargs): - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - require_redraw = True - - # check if CTk widgets are children of the frame and change their bg_color to new frame fg_color - for child in self.winfo_children(): - if isinstance(child, CTkBaseClass): - child.configure(bg_color=self.fg_color) - - if "border_color" in kwargs: - self.border_color = kwargs.pop("border_color") - 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 - - if "width" in kwargs: - self.set_dimensions(width=kwargs.pop("width")) - - if "height" in kwargs: - self.set_dimensions(height=kwargs.pop("height")) - - if "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - self.textbox.configure(font=self.apply_font_scaling(self.text_font)) - - if "font" in kwargs: - raise ValueError("No attribute named font. Use text_font instead of font for CTk widgets") - - if "bg_color" in kwargs: - super().configure(bg_color=kwargs.pop("bg_color"), require_redraw=require_redraw) - else: - super().configure(require_redraw=require_redraw) - - self.textbox.configure(**kwargs) diff --git a/customtkinter/widgets/dropdown_menu.py b/customtkinter/widgets/dropdown_menu.py deleted file mode 100644 index a553c47..0000000 --- a/customtkinter/widgets/dropdown_menu.py +++ /dev/null @@ -1,170 +0,0 @@ -import tkinter -import sys -import copy -import re -from typing import Union - -from ..theme_manager import ThemeManager -from ..appearance_mode_tracker import AppearanceModeTracker -from ..scaling_tracker import ScalingTracker - - -class DropdownMenu(tkinter.Menu): - def __init__(self, *args, - min_character_width=18, - fg_color="default_theme", - hover_color="default_theme", - text_color="default_theme", - text_font="default_theme", - command=None, - values=None, - **kwargs): - super().__init__(*args, **kwargs) - - ScalingTracker.add_widget(self.set_scaling, self) - self._widget_scaling = ScalingTracker.get_widget_scaling(self) - self._spacing_scaling = ScalingTracker.get_spacing_scaling(self) - - AppearanceModeTracker.add(self.set_appearance_mode, self) - self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" - - self.min_character_width = min_character_width - self.fg_color = ThemeManager.theme["color"]["dropdown_color"] if fg_color == "default_theme" else fg_color - self.hover_color = ThemeManager.theme["color"]["dropdown_hover"] if hover_color == "default_theme" else hover_color - self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self.text_font = (ThemeManager.theme["text"]["font"], ThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - - self.configure_menu_for_platforms() - - self.values = values - self.command = command - - self.add_menu_commands() - - def configure_menu_for_platforms(self): - """ apply platform specific appearance attributes """ - - if sys.platform == "darwin": - self.configure(tearoff=False, - font=self.apply_font_scaling(self.text_font)) - - elif sys.platform.startswith("win"): - self.configure(tearoff=False, - relief="flat", - activebackground=ThemeManager.single_color(self.hover_color, self._appearance_mode), - borderwidth=0, - activeborderwidth=self.apply_widget_scaling(4), - bg=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fg=ThemeManager.single_color(self.text_color, self._appearance_mode), - activeforeground=ThemeManager.single_color(self.text_color, self._appearance_mode), - font=self.apply_font_scaling(self.text_font), - cursor="hand2") - - else: - self.configure(tearoff=False, - relief="flat", - activebackground=ThemeManager.single_color(self.hover_color, self._appearance_mode), - borderwidth=0, - activeborderwidth=0, - bg=ThemeManager.single_color(self.fg_color, self._appearance_mode), - fg=ThemeManager.single_color(self.text_color, self._appearance_mode), - activeforeground=ThemeManager.single_color(self.text_color, self._appearance_mode), - font=self.apply_font_scaling(self.text_font)) - - def add_menu_commands(self): - 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 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 button_callback(self, value): - if self.command is not None: - self.command(value) - - def configure(self, **kwargs): - if "values" in kwargs: - self.values = kwargs.pop("values") - self.delete(0, "end") # delete all old commands - self.add_menu_commands() - - if "fg_color" in kwargs: - self.fg_color = kwargs.pop("fg_color") - self.configure(bg=ThemeManager.single_color(self.fg_color, self._appearance_mode)) - - if "hover_color" in kwargs: - self.hover_color = kwargs.pop("hover_color") - self.configure(activebackground=ThemeManager.single_color(self.hover_color, self._appearance_mode)) - - if "text_color" in kwargs: - self.text_color = kwargs.pop("text_color") - self.configure(fg=ThemeManager.single_color(self.text_color, self._appearance_mode)) - - if "text_font" in kwargs: - self.text_font = kwargs.pop("text_font") - self.configure(font=self.apply_font_scaling(self.text_font)) - - super().configure(**kwargs) - - def apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]: - if isinstance(value, (int, float)): - return value * self._widget_scaling - else: - return value - - def apply_font_scaling(self, font): - if type(font) == tuple or type(font) == list: - font_list = list(font) - for i in range(len(font_list)): - if (type(font_list[i]) == int or type(font_list[i]) == float) and font_list[i] < 0: - font_list[i] = int(font_list[i] * self._widget_scaling) - return tuple(font_list) - - elif type(font) == str: - for negative_number in re.findall(r" -\d* ", font): - font = font.replace(negative_number, f" {int(int(negative_number) * self._widget_scaling)} ") - return font - - elif isinstance(font, tkinter.font.Font): - new_font_object = copy.copy(font) - if font.cget("size") < 0: - new_font_object.config(size=int(font.cget("size") * self._widget_scaling)) - return new_font_object - - else: - return font - - def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): - self._widget_scaling = new_widget_scaling - self._spacing_scaling = new_spacing_scaling - - self.configure(font=self.apply_font_scaling(self.text_font)) - - if sys.platform.startswith("win"): - self.configure(activeborderwidth=self.apply_widget_scaling(4)) - - def set_appearance_mode(self, mode_string): - """ colors won't update on appearance mode change when dropdown is open, because it's not necessary """ - - if mode_string.lower() == "dark": - self._appearance_mode = 1 - elif mode_string.lower() == "light": - self._appearance_mode = 0 - - self.configure_menu_for_platforms() diff --git a/customtkinter/widgets/widget_base_class.py b/customtkinter/widgets/widget_base_class.py deleted file mode 100644 index 303a7d7..0000000 --- a/customtkinter/widgets/widget_base_class.py +++ /dev/null @@ -1,234 +0,0 @@ -import tkinter -import tkinter.ttk as ttk -import copy -import re -from typing import Callable, Union - -try: - from typing import TypedDict -except ImportError: - from typing_extensions import TypedDict - -from ..windows.ctk_tk import CTk -from ..windows.ctk_toplevel import CTkToplevel -from ..appearance_mode_tracker import AppearanceModeTracker -from ..scaling_tracker import ScalingTracker -from ..theme_manager import ThemeManager - - -class CTkBaseClass(tkinter.Frame): - """ 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 """ - - def __init__(self, - *args, - bg_color: Union[str, tuple] = None, - width: int, - height: int, - **kwargs): - - super().__init__(*args, width=width, height=height, **kwargs) # set desired size of underlying tkinter.Frame - - # dimensions - self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget - self._current_height = height # _current_width and _current_height are independent of the scale - self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height - self._desired_height = height - - # scaling - ScalingTracker.add_widget(self.set_scaling, self) # add callback for automatic scaling changes - self._widget_scaling = ScalingTracker.get_widget_scaling(self) - self._spacing_scaling = ScalingTracker.get_spacing_scaling(self) - - 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 - - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.set_appearance_mode, self) - self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" - - # background color - self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color - - super().configure(bg=ThemeManager.single_color(self.bg_color, self._appearance_mode)) - - # overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well - if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame)) and not isinstance(self.master, (CTkBaseClass, CTk, CTkToplevel)): - master_old_configure = self.master.config - - def new_configure(*args, **kwargs): - if "bg" in kwargs: - self.configure(bg_color=kwargs["bg"]) - elif "background" in kwargs: - self.configure(bg_color=kwargs["background"]) - - # args[0] is dict when attribute gets changed by widget[] 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): - AppearanceModeTracker.remove(self.set_appearance_mode) - super().destroy() - - def place(self, **kwargs): - self._last_geometry_manager_call = {"function": super().place, "kwargs": kwargs} - super().place(**self.apply_argument_scaling(kwargs)) - - def pack(self, **kwargs): - self._last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs} - super().pack(**self.apply_argument_scaling(kwargs)) - - def grid(self, **kwargs): - self._last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs} - super().grid(**self.apply_argument_scaling(kwargs)) - - def apply_argument_scaling(self, kwargs: dict) -> dict: - scaled_kwargs = copy.copy(kwargs) - - if "pady" in scaled_kwargs: - if isinstance(scaled_kwargs["pady"], (int, float, str)): - scaled_kwargs["pady"] = self.apply_spacing_scaling(scaled_kwargs["pady"]) - elif isinstance(scaled_kwargs["pady"], tuple): - scaled_kwargs["pady"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["pady"]]) - if "padx" in kwargs: - if isinstance(scaled_kwargs["padx"], (int, float, str)): - scaled_kwargs["padx"] = self.apply_spacing_scaling(scaled_kwargs["padx"]) - elif isinstance(scaled_kwargs["padx"], tuple): - scaled_kwargs["padx"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["padx"]]) - - if "x" in scaled_kwargs: - scaled_kwargs["x"] = self.apply_spacing_scaling(scaled_kwargs["x"]) - if "y" in scaled_kwargs: - scaled_kwargs["y"] = self.apply_spacing_scaling(scaled_kwargs["y"]) - - return scaled_kwargs - - def configure(self, require_redraw=False, **kwargs): - """ basic configure with bg_color support, to be overridden """ - - if "bg_color" in kwargs: - new_bg_color = kwargs.pop("bg_color") - if new_bg_color is None: - self.bg_color = self.detect_color_of_master() - else: - self.bg_color = new_bg_color - require_redraw = True - - super().configure(**kwargs) - - if require_redraw: - self.draw() - - def update_dimensions_event(self, event): - # only redraw if dimensions changed (for performance), independent of scaling - if round(self._current_width) != round(event.width / self._widget_scaling) or round(self._current_height) != round(event.height / self._widget_scaling): - self._current_width = (event.width / self._widget_scaling) # adjust current size according to new size given by event - self._current_height = (event.height / self._widget_scaling) # _current_width and _current_height are independent of the scale - - self.draw(no_color_updates=True) # faster drawing without color changes - - def detect_color_of_master(self, master_widget=None): - """ detect color of self.master widget to set correct bg_color """ - - if master_widget is None: - master_widget = self.master - - if isinstance(master_widget, (CTkBaseClass, CTk, CTkToplevel)) and hasattr(master_widget, "fg_color"): - if master_widget.fg_color is not None: - return master_widget.fg_color - - # if fg_color of master is None, try to retrieve fg_color from master of master - elif hasattr(master_widget.master, "master"): - return self.detect_color_of_master(master_widget.master) - - elif isinstance(master_widget, (ttk.Frame, ttk.LabelFrame, ttk.Notebook, ttk.Label)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(master_widget.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return master_widget.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - - def set_appearance_mode(self, mode_string): - if mode_string.lower() == "dark": - self._appearance_mode = 1 - elif mode_string.lower() == "light": - self._appearance_mode = 0 - - self.draw() - - def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): - self._widget_scaling = new_widget_scaling - self._spacing_scaling = new_spacing_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 apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]: - if isinstance(value, (int, float)): - return value * self._widget_scaling - else: - return value - - def apply_spacing_scaling(self, value: Union[int, float, str]) -> Union[float, str]: - if isinstance(value, (int, float)): - return value * self._spacing_scaling - else: - return value - - def apply_font_scaling(self, font): - if type(font) == tuple or type(font) == list: - font_list = list(font) - for i in range(len(font_list)): - if (type(font_list[i]) == int or type(font_list[i]) == float) and font_list[i] < 0: - font_list[i] = int(font_list[i] * self._widget_scaling) - return tuple(font_list) - - elif type(font) == str: - for negative_number in re.findall(r" -\d* ", font): - font = font.replace(negative_number, f" {int(int(negative_number) * self._widget_scaling)} ") - return font - - elif isinstance(font, tkinter.font.Font): - new_font_object = copy.copy(font) - if font.cget("size") < 0: - new_font_object.config(size=int(font.cget("size") * self._widget_scaling)) - return new_font_object - - else: - return font - - def draw(self, no_color_updates: bool = False): - """ abstract of draw method to be overridden """ - pass diff --git a/customtkinter/windows/__init__.py b/customtkinter/windows/__init__.py index e69de29..ca681b7 100644 --- a/customtkinter/windows/__init__.py +++ b/customtkinter/windows/__init__.py @@ -0,0 +1,3 @@ +from .ctk_tk import CTk +from .ctk_toplevel import CTkToplevel +from .ctk_input_dialog import CTkInputDialog diff --git a/customtkinter/windows/ctk_input_dialog.py b/customtkinter/windows/ctk_input_dialog.py index d0c204f..fc37527 100644 --- a/customtkinter/windows/ctk_input_dialog.py +++ b/customtkinter/windows/ctk_input_dialog.py @@ -1,119 +1,110 @@ -import tkinter -import time +from typing import Union, Tuple, Optional -from ..widgets.ctk_label import CTkLabel -from ..widgets.ctk_entry import CTkEntry -from ..widgets.ctk_frame import CTkFrame -from ..windows.ctk_toplevel import CTkToplevel -from ..widgets.ctk_button import CTkButton -from ..appearance_mode_tracker import AppearanceModeTracker -from ..theme_manager import ThemeManager +from .widgets import CTkLabel +from .widgets import CTkEntry +from .widgets import CTkButton +from .widgets.theme import ThemeManager +from .ctk_toplevel import CTkToplevel -class CTkInputDialog: +class CTkInputDialog(CTkToplevel): + """ + Dialog with extra window, message, entry widget, cancel and ok button. + For detailed information check out the documentation. + """ + def __init__(self, - master=None, - title="CTkDialog", - text="CTkDialog", - fg_color="default_theme", - hover_color="default_theme", - border_color="default_theme"): + 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, - self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" - self.master = master + title: str = "CTkDialog", + text: str = "CTkDialog"): - self.user_input = None - self.running = False + super().__init__(fg_color=fg_color) - self.height = len(text.split("\n"))*20 + 150 + 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.text = text - self.window_bg_color = ThemeManager.theme["color"]["window_bg_color"] - self.fg_color = ThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color - self.hover_color = ThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color - self.border_color = ThemeManager.theme["color"]["button_hover"] if border_color == "default_theme" else border_color + self._user_input: Union[str, None] = None + self._running: bool = False + self._text = text - self.top = CTkToplevel() - self.top.geometry(f"{280}x{self.height}") - self.top.minsize(280, self.height) - self.top.maxsize(280, self.height) - self.top.title(title) - self.top.lift() - self.top.focus_force() - self.top.grab_set() + 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 - self.top.protocol("WM_DELETE_WINDOW", self.on_closing) + def _create_widgets(self): - self.top.after(10, self.create_widgets) # create widgets with slight delay, to avoid white flickering of background + self.grid_columnconfigure((0, 1), weight=1) + self.rowconfigure(0, weight=1) - def create_widgets(self): - self.label_frame = CTkFrame(master=self.top, - corner_radius=0, - fg_color=self.window_bg_color, - width=300, - height=self.height-100) - self.label_frame.place(relx=0.5, rely=0, anchor=tkinter.N) + 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.button_and_entry_frame = CTkFrame(master=self.top, - corner_radius=0, - fg_color=self.window_bg_color, - width=300, - height=100) - self.button_and_entry_frame.place(relx=0.5, rely=1, anchor=tkinter.S) + 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.myLabel = CTkLabel(master=self.label_frame, - text=self.text, - width=300, - fg_color=None, - height=self.height-100) - self.myLabel.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER) + 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.entry = CTkEntry(master=self.button_and_entry_frame, - width=230) - self.entry.place(relx=0.5, rely=0.15, anchor=tkinter.CENTER) + 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.ok_button = CTkButton(master=self.button_and_entry_frame, - text='Ok', - width=100, - command=self.ok_event, - fg_color=self.fg_color, - hover_color=self.hover_color, - border_color=self.border_color) - self.ok_button.place(relx=0.28, rely=0.65, anchor=tkinter.CENTER) + self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work + self._entry.bind("", self._ok_event) - self.cancel_button = CTkButton(master=self.button_and_entry_frame, - text='Cancel', - width=100, - command=self.cancel_event, - fg_color=self.fg_color, - hover_color=self.hover_color, - border_color=self.border_color) - self.cancel_button.place(relx=0.72, rely=0.65, anchor=tkinter.CENTER) + def _ok_event(self, event=None): + self._user_input = self._entry.get() + self.grab_release() + self.destroy() - self.entry.entry.focus_force() - self.entry.bind("", self.ok_event) + def _on_closing(self): + self.grab_release() + self.destroy() - def ok_event(self, event=None): - self.user_input = self.entry.get() - self.running = False - - def on_closing(self): - self.running = False - - def cancel_event(self): - self.running = False + def _cancel_event(self): + self.grab_release() + self.destroy() def get_input(self): - self.running = True - - while self.running: - try: - self.top.update() - except Exception: - return self.user_input - finally: - time.sleep(0.01) - - time.sleep(0.05) - self.top.destroy() - return self.user_input + self.master.wait_window(self) + return self._user_input diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index 7c3b4b7..2371997 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -4,276 +4,230 @@ import sys import os import platform import ctypes -import re -from typing import Union, Tuple +from typing import Union, Tuple, Optional -from ..appearance_mode_tracker import AppearanceModeTracker -from ..theme_manager import ThemeManager -from ..scaling_tracker import ScalingTracker -from ..settings import Settings +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): - def __init__(self, *args, - fg_color="default_theme", +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): - ScalingTracker.activate_high_dpi_awareness() # make process DPI aware - self.enable_macos_dark_title_bar() + self._enable_macos_dark_title_bar() - super().__init__(*args, **kwargs) + # call init methods of super classes + tkinter.Tk.__init__(self, **pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments)) + CTkAppearanceModeBaseClass.__init__(self) + CTkScalingBaseClass.__init__(self, scaling_type="window") + check_kwargs_empty(kwargs, raise_error=True) - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.set_appearance_mode, self) - self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + 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) - # add set_scaling method to callback list of ScalingTracker for automatic scaling changes - ScalingTracker.add_widget(self.set_scaling, self) - self.window_scaling = ScalingTracker.get_window_scaling(self) + self._fg_color = ThemeManager.theme["CTk"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) - self.current_width = 600 # initial window size, always without 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) + # set bg of tkinter.Tk + super().configure(bg=self._apply_appearance_mode(self._fg_color)) - self.fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color + # set title and initial geometry + self.title("CTk") + # self.geometry(f"{self._current_width}x{self._current_height}") - if "bg" in kwargs: - self.fg_color = kwargs["bg"] - del kwargs["bg"] - elif "background" in kwargs: - self.fg_color = kwargs["background"] - del kwargs["background"] - - super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode)) - super().title("CTk") - self.geometry(f"{self.current_width}x{self.current_height}") - - 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._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() if sys.platform.startswith("win"): - if self.appearance_mode == 1: - self.windows_set_titlebar_color("dark") - else: - self.windows_set_titlebar_color("light") + self._windows_set_titlebar_color(self._get_appearance_mode()) - self.bind('', self.update_dimensions_event) + self.bind('', self._update_dimensions_event) + self.bind('', self._focus_in_event) - self.block_update_dimensions_event = False - - 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 != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling): - self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event - self.current_height = round(detected_height / self.window_scaling) # _current_width and _current_height are independent of the scale - - def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): - self.window_scaling = new_window_scaling - - # block update_dimensions_event to prevent current_width and current_height to get updated - self.block_update_dimensions_event = True - - # force new dimensions on window by using min, max, and geometry - 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"+f"{self.apply_window_scaling(self.current_height)}") - - # set new scaled min and max with 400ms delay (otherwise it won't work for some reason) - self.after(400, self.set_scaled_min_max) - - # release the blocking of update_dimensions_event after a small amount of time (slight delay is necessary) - def set_block_update_dimensions_event_false(): - self.block_update_dimensions_event = False - self.after(100, lambda: set_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)) + self._block_update_dimensions_event = False def destroy(self): - AppearanceModeTracker.remove(self.set_appearance_mode) - ScalingTracker.remove_window(self.set_scaling, self) - self.disable_macos_dark_title_bar() - super().destroy() + 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 + 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 + 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 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: + 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 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: + 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, *args, **kwargs): - super().resizable(*args, **kwargs) - self.last_resizable_args = (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"): - if self.appearance_mode == 1: - self.windows_set_titlebar_color("dark") - else: - self.windows_set_titlebar_color("light") + self._windows_set_titlebar_color(self._get_appearance_mode()) - 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)) + return current_resizable_values - 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 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)) + super().geometry(self._apply_geometry_scaling(geometry_string)) # update width and height attributes - width, height, x, y = self.parse_geometry_string(geometry_string) + 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)) + 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()) + return self._reverse_geometry_scaling(super().geometry()) - @staticmethod - def parse_geometry_string(geometry_string: str) -> tuple: - # index: 1 2 3 4 5 6 - # regex group structure: ('x', '', '', '+-+-', '-', '-') - result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string) - - width = int(result.group(2)) if result.group(2) is not None else None - height = int(result.group(3)) if result.group(3) is not None else None - x = int(result.group(5)) if result.group(5) is not None else None - y = int(result.group(6)) if result.group(6) is not None else None - - return width, height, x, y - - def apply_geometry_scaling(self, geometry_string: str) -> str: - width, height, x, y = self.parse_geometry_string(geometry_string) - - if x is None and y is None: # no and 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 and in geometry_string - return f"+{x}+{y}" - - else: - return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}+{x}+{y}" - - def reverse_geometry_scaling(self, scaled_geometry_string: str) -> str: - width, height, x, y = self.parse_geometry_string(scaled_geometry_string) - - if x is None and y is None: # no and 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 and in geometry_string - return f"+{x}+{y}" - - else: - return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}+{x}+{y}" - - def apply_window_scaling(self, value): - if isinstance(value, (int, float)): - return int(value * self.window_scaling) - else: - return value - - def config(self, *args, **kwargs): - self.configure(*args, **kwargs) - - def configure(self, *args, **kwargs): - bg_changed = False - - if "bg" in kwargs: - self.fg_color = kwargs["bg"] - bg_changed = True - kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - elif "background" in kwargs: - self.fg_color = kwargs["background"] - bg_changed = True - kwargs["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - elif "fg_color" in kwargs: - self.fg_color = kwargs["fg_color"] - kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - del kwargs["fg_color"] - bg_changed = True - - elif len(args) > 0 and type(args[0]) == dict: - if "bg" in args[0]: - self.fg_color=args[0]["bg"] - bg_changed = True - args[0]["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - elif "background" in args[0]: - self.fg_color=args[0]["background"] - bg_changed = True - args[0]["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - - if bg_changed: - from ..widgets.widget_base_class import CTkBaseClass + 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(): - if isinstance(child, CTkBaseClass): - child.configure(bg_color=self.fg_color) + try: + child.configure(bg_color=self._fg_color) + except Exception: + pass - super().configure(*args, **kwargs) + super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_configure_arguments)) + check_kwargs_empty(kwargs) - @staticmethod - def enable_macos_dark_title_bar(): - if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS + def cget(self, attribute_name: str) -> any: + if attribute_name == "fg_color": + return self._fg_color + else: + return super().cget(attribute_name) + + @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 - @staticmethod - def disable_macos_dark_title_bar(): - if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS + @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): + def _windows_set_titlebar_color(self, color_mode: str): """ Set the titlebar color of the window to light or dark theme on Microsoft Windows. @@ -284,13 +238,13 @@ class CTk(tkinter.Tk): https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute """ - if sys.platform.startswith("win") and not Settings.deactivate_windows_window_header_manipulation: + 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() + 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": + 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") @@ -322,29 +276,23 @@ class CTk(tkinter.Tk): except Exception as err: print(err) - if self.window_exists: + 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": + if self._state_before_windows_set_titlebar_color == "normal": self.deiconify() - elif self.state_before_windows_set_titlebar_color == "iconic": + elif self._state_before_windows_set_titlebar_color == "iconic": self.iconify() - elif self.state_before_windows_set_titlebar_color == "zoomed": + 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.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): - if mode_string.lower() == "dark": - self.appearance_mode = 1 - elif mode_string.lower() == "light": - self.appearance_mode = 0 + def _set_appearance_mode(self, mode_string: str): + super()._set_appearance_mode(mode_string) if sys.platform.startswith("win"): - if self.appearance_mode == 1: - self.windows_set_titlebar_color("dark") - else: - self.windows_set_titlebar_color("light") + self._windows_set_titlebar_color(mode_string) - super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode)) + super().configure(bg=self._apply_appearance_mode(self._fg_color)) diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index e960560..4e43839 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -4,239 +4,200 @@ import sys import os import platform import ctypes -import re -from typing import Union, Tuple +from typing import Union, Tuple, Optional -from ..appearance_mode_tracker import AppearanceModeTracker -from ..theme_manager import ThemeManager -from ..settings import Settings -from ..scaling_tracker import ScalingTracker +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): +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="default_theme", + fg_color: Optional[Union[str, Tuple[str, str]]] = None, **kwargs): - self.enable_macos_dark_title_bar() - super().__init__(*args, **kwargs) - self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + self._enable_macos_dark_title_bar() - # add set_scaling method to callback list of ScalingTracker for automatic scaling changes - ScalingTracker.add_widget(self.set_scaling, self) - self.window_scaling = ScalingTracker.get_window_scaling(self) + # 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) - 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._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["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color + self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) - if "bg" in kwargs: - self.fg_color = kwargs["bg"] - del kwargs["bg"] - elif "background" in kwargs: - self.fg_color = kwargs["background"] - del kwargs["background"] + # set bg color of tkinter.Toplevel + super().configure(bg=self._apply_appearance_mode(self._fg_color)) - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.set_appearance_mode, self) - super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode)) + # set title of tkinter.Toplevel super().title("CTkToplevel") - 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._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 if sys.platform.startswith("win"): - if self.appearance_mode == 1: - self.windows_set_titlebar_color("dark") - else: - self.windows_set_titlebar_color("light") + self._windows_set_titlebar_color(self._get_appearance_mode()) - self.bind('', self.update_dimensions_event) + self.bind('', self._update_dimensions_event) + self.bind('', self._focus_in_event) - def update_dimensions_event(self, event=None): - detected_width = self.winfo_width() # detect current window size - detected_height = self.winfo_height() + self._block_update_dimensions_event = False - if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling): - self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event - self.current_height = round(detected_height / self.window_scaling) # _current_width and _current_height are independent of the scale + def destroy(self): + self._disable_macos_dark_title_bar() - def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): - self.window_scaling = new_window_scaling + # call destroy methods of super classes + tkinter.Toplevel.destroy(self) + CTkAppearanceModeBaseClass.destroy(self) + CTkScalingBaseClass.destroy(self) - # force new dimensions on window by using min, max, and geometry - 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" + f"{self.apply_window_scaling(self.current_height)}") + 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() - # set new scaled min and max with 400ms delay (otherwise it won't work for some reason) - self.after(400, self.set_scaled_min_max) + 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() - 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)) + 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)) + super().geometry(self._apply_geometry_scaling(geometry_string)) # update width and height attributes - width, height, x, y = self.parse_geometry_string(geometry_string) + 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)) + 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()) - - @staticmethod - def parse_geometry_string(geometry_string: str) -> tuple: - # index: 1 2 3 4 5 6 - # regex group structure: ('x', '', '', '+-+-', '-', '-') - result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string) - - width = int(result.group(2)) if result.group(2) is not None else None - height = int(result.group(3)) if result.group(3) is not None else None - x = int(result.group(5)) if result.group(5) is not None else None - y = int(result.group(6)) if result.group(6) is not None else None - - return width, height, x, y - - def apply_geometry_scaling(self, geometry_string: str) -> str: - width, height, x, y = self.parse_geometry_string(geometry_string) - - if x is None and y is None: # no and 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 and in geometry_string - return f"+{x}+{y}" - - else: - return f"{round(width * self.window_scaling)}x{round(height * self.window_scaling)}+{x}+{y}" - - def reverse_geometry_scaling(self, scaled_geometry_string: str) -> str: - width, height, x, y = self.parse_geometry_string(scaled_geometry_string) - - if x is None and y is None: # no and 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 and in geometry_string - return f"+{x}+{y}" - - else: - return f"{round(width / self.window_scaling)}x{round(height / self.window_scaling)}+{x}+{y}" - - def apply_window_scaling(self, value): - if isinstance(value, (int, float)): - return int(value * self.window_scaling) - else: - return value - - def destroy(self): - AppearanceModeTracker.remove(self.set_appearance_mode) - ScalingTracker.remove_window(self.set_scaling, self) - self.disable_macos_dark_title_bar() - super().destroy() + 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 + 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 + if self._windows_set_titlebar_color_called: + self._iconify_called_after_windows_set_titlebar_color = True super().iconify() - def resizable(self, *args, **kwargs): - super().resizable(*args, **kwargs) - self.last_resizable_args = (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"): - if self.appearance_mode == 1: - self.after(10, lambda: self.windows_set_titlebar_color("dark")) - else: - self.after(10, lambda: self.windows_set_titlebar_color("light")) + 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)) + 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)) + 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 config(self, *args, **kwargs): - self.configure(*args, **kwargs) - - def configure(self, *args, **kwargs): - bg_changed = False - - if "bg" in kwargs: - self.fg_color = kwargs["bg"] - bg_changed = True - kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - elif "background" in kwargs: - self.fg_color = kwargs["background"] - bg_changed = True - kwargs["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - elif "fg_color" in kwargs: - self.fg_color = kwargs["fg_color"] - kwargs["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - del kwargs["fg_color"] - bg_changed = True - - elif len(args) > 0 and type(args[0]) == dict: - if "bg" in args[0]: - self.fg_color=args[0]["bg"] - bg_changed = True - args[0]["bg"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - elif "background" in args[0]: - self.fg_color=args[0]["background"] - bg_changed = True - args[0]["background"] = ThemeManager.single_color(self.fg_color, self.appearance_mode) - - if bg_changed: - from ..widgets.widget_base_class import CTkBaseClass + 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(): - if isinstance(child, CTkBaseClass): - child.configure(bg_color=self.fg_color) + try: + child.configure(bg_color=self._fg_color) + except Exception: + pass - super().configure(*args, **kwargs) + super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments)) + check_kwargs_empty(kwargs) - @staticmethod - def enable_macos_dark_title_bar(): - if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS + def cget(self, attribute_name: str) -> any: + if attribute_name == "fg_color": + return self._fg_color + else: + return super().cget(attribute_name) + + @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") - @staticmethod - def disable_macos_dark_title_bar(): - if sys.platform == "darwin" and not Settings.deactivate_macos_window_header_manipulation: # macOS + @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): + def _windows_set_titlebar_color(self, color_mode: str): """ Set the titlebar color of the window to light or dark theme on Microsoft Windows. @@ -247,9 +208,9 @@ class CTkToplevel(tkinter.Toplevel): https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute """ - if sys.platform.startswith("win") and not Settings.deactivate_windows_window_header_manipulation: + if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation: - self.state_before_windows_set_titlebar_color = self.state() + 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() @@ -277,41 +238,35 @@ class CTkToplevel(tkinter.Toplevel): except Exception as err: print(err) - self.windows_set_titlebar_color_called = True - self.after(5, self.revert_withdraw_after_windows_set_titlebar_color) + 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): + 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._windows_set_titlebar_color_called: - if self.withdraw_called_after_windows_set_titlebar_color: + if self._withdraw_called_after_windows_set_titlebar_color: pass # leave it withdrawed - elif self.iconify_called_after_windows_set_titlebar_color: + elif self._iconify_called_after_windows_set_titlebar_color: super().iconify() else: - if self.state_before_windows_set_titlebar_color == "normal": + if self._state_before_windows_set_titlebar_color == "normal": self.deiconify() - elif self.state_before_windows_set_titlebar_color == "iconic": + elif self._state_before_windows_set_titlebar_color == "iconic": self.iconify() - elif self.state_before_windows_set_titlebar_color == "zoomed": + 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.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 + 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): - if mode_string.lower() == "dark": - self.appearance_mode = 1 - elif mode_string.lower() == "light": - self.appearance_mode = 0 + def _set_appearance_mode(self, mode_string): + super()._set_appearance_mode(mode_string) if sys.platform.startswith("win"): - if self.appearance_mode == 1: - self.windows_set_titlebar_color("dark") - else: - self.windows_set_titlebar_color("light") + self._windows_set_titlebar_color(mode_string) - super().configure(bg=ThemeManager.single_color(self.fg_color, self.appearance_mode)) + super().configure(bg=self._apply_appearance_mode(self._fg_color)) diff --git a/customtkinter/windows/widgets/__init__.py b/customtkinter/windows/widgets/__init__.py new file mode 100644 index 0000000..2e21484 --- /dev/null +++ b/customtkinter/windows/widgets/__init__.py @@ -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 diff --git a/customtkinter/windows/widgets/appearance_mode/__init__.py b/customtkinter/windows/widgets/appearance_mode/__init__.py new file mode 100644 index 0000000..e979ca8 --- /dev/null +++ b/customtkinter/windows/widgets/appearance_mode/__init__.py @@ -0,0 +1,4 @@ +from .appearance_mode_base_class import CTkAppearanceModeBaseClass +from .appearance_mode_tracker import AppearanceModeTracker + +AppearanceModeTracker.init_appearance_mode() diff --git a/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py new file mode 100644 index 0000000..0d19147 --- /dev/null +++ b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py @@ -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)}") diff --git a/customtkinter/appearance_mode_tracker.py b/customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py similarity index 98% rename from customtkinter/appearance_mode_tracker.py rename to customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py index 210b8bf..4958b93 100644 --- a/customtkinter/appearance_mode_tracker.py +++ b/customtkinter/windows/widgets/appearance_mode/appearance_mode_tracker.py @@ -21,7 +21,7 @@ class AppearanceModeTracker: callback_list = [] app_list = [] update_loop_running = False - update_loop_interval = 500 # milliseconds + update_loop_interval = 30 # milliseconds appearance_mode_set_by = "system" appearance_mode = 0 # Light (standard) diff --git a/customtkinter/windows/widgets/core_rendering/__init__.py b/customtkinter/windows/widgets/core_rendering/__init__.py new file mode 100644 index 0000000..ccadbc7 --- /dev/null +++ b/customtkinter/windows/widgets/core_rendering/__init__.py @@ -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" diff --git a/customtkinter/widgets/ctk_canvas.py b/customtkinter/windows/widgets/core_rendering/ctk_canvas.py similarity index 68% rename from customtkinter/widgets/ctk_canvas.py rename to customtkinter/windows/widgets/core_rendering/ctk_canvas.py index c0754b1..f291e2c 100644 --- a/customtkinter/widgets/ctk_canvas.py +++ b/customtkinter/windows/widgets/core_rendering/ctk_canvas.py @@ -4,11 +4,32 @@ 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() + self._aa_circle_canvas_ids = set() @classmethod def init_font_character_mapping(cls): @@ -43,7 +64,7 @@ class CTkCanvas(tkinter.Canvas): else: cls.radius_to_char_fine = radius_to_char_fine_windows_10 - def get_char_from_radius(self, radius: int) -> str: + def _get_char_from_radius(self, radius: int) -> str: if radius >= 20: return "A" else: @@ -52,10 +73,10 @@ class CTkCanvas(tkinter.Canvas): 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, + 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) + self._aa_circle_canvas_ids.add(circle_1) return circle_1 @@ -66,13 +87,13 @@ class CTkCanvas(tkinter.Canvas): 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])) + 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: + 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])) + 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) @@ -83,14 +104,14 @@ class CTkCanvas(tkinter.Canvas): del kwargs_except_outline["outline"] if type(tag_or_id) == int: - if tag_or_id in self.aa_circle_canvas_ids: + 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: + if configure_id in self._aa_circle_canvas_ids: super().itemconfigure(configure_id, *args, **kwargs_except_outline) else: super().itemconfigure(configure_id, *args, **kwargs) diff --git a/customtkinter/draw_engine.py b/customtkinter/windows/widgets/core_rendering/draw_engine.py similarity index 95% rename from customtkinter/draw_engine.py rename to customtkinter/windows/widgets/core_rendering/draw_engine.py index eafaeb2..5acea56 100644 --- a/customtkinter/draw_engine.py +++ b/customtkinter/windows/widgets/core_rendering/draw_engine.py @@ -5,7 +5,7 @@ import tkinter from typing import Union, TYPE_CHECKING if TYPE_CHECKING: - from .widgets.ctk_canvas import CTkCanvas + from ..core_rendering import CTkCanvas class DrawEngine: @@ -30,6 +30,12 @@ class DrawEngine: def __init__(self, canvas: CTkCanvas): self._canvas = canvas + self._round_width_to_even_numbers: bool = True + self._round_height_to_even_numbers: bool = True + + def set_round_to_even_numbers(self, round_width_to_even_numbers: bool = True, round_height_to_even_numbers: bool = True): + self._round_width_to_even_numbers: bool = round_width_to_even_numbers + self._round_height_to_even_numbers: bool = round_height_to_even_numbers def __calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]: # optimize for drawing with polygon shapes @@ -55,6 +61,38 @@ class DrawEngine: else: return user_corner_radius + def draw_background_corners(self, width: Union[float, int], height: Union[float, int], ): + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 + + requires_recoloring = False + + if not self._canvas.find_withtag("background_corner_top_left"): + self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_top_left"), width=0) + requires_recoloring = True + if not self._canvas.find_withtag("background_corner_top_right"): + self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_top_right"), width=0) + requires_recoloring = True + if not self._canvas.find_withtag("background_corner_bottom_right"): + self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_bottom_right"), width=0) + requires_recoloring = True + if not self._canvas.find_withtag("background_corner_bottom_left"): + self._canvas.create_rectangle((0, 0, 0, 0), tags=("background_parts", "background_corner_bottom_left"), width=0) + requires_recoloring = True + + mid_width, mid_height = round(width / 2), round(height / 2) + self._canvas.coords("background_corner_top_left", (0, 0, mid_width, mid_height)) + self._canvas.coords("background_corner_top_right", (mid_width, 0, width, mid_height)) + self._canvas.coords("background_corner_bottom_right", (mid_width, mid_height, width, height)) + self._canvas.coords("background_corner_bottom_left", (0, mid_height, mid_width, height)) + + if requires_recoloring: # new parts were added -> manage z-order + self._canvas.tag_lower("background_parts") + + return requires_recoloring + def draw_rounded_rect_with_border(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], border_width: Union[float, int], overwrite_preferred_drawing_method: str = None) -> bool: """ Draws a rounded rectangle with a corner_radius and border_width on the canvas. The border elements have a 'border_parts' tag, @@ -62,11 +100,13 @@ class DrawEngine: returns bool if recoloring is necessary """ - width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only - height = math.floor(height / 2) * 2 + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 corner_radius = round(corner_radius) - if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger + if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too large corner_radius = min(width / 2, height / 2) border_width = round(border_width) @@ -139,6 +179,7 @@ class DrawEngine: if requires_recoloring: # new parts were added -> manage z-order self._canvas.tag_lower("inner_parts") self._canvas.tag_lower("border_parts") + self._canvas.tag_lower("background_parts") return requires_recoloring @@ -277,6 +318,7 @@ class DrawEngine: if requires_recoloring: # new parts were added -> manage z-order self._canvas.tag_lower("inner_parts") self._canvas.tag_lower("border_parts") + self._canvas.tag_lower("background_parts") return requires_recoloring @@ -364,8 +406,10 @@ class DrawEngine: returns bool if recoloring is necessary """ left_section_width = round(left_section_width) - width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only - height = math.floor(height / 2) * 2 + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round (floor) _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 corner_radius = round(corner_radius) if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger @@ -478,6 +522,7 @@ class DrawEngine: if requires_recoloring: # new parts were added -> manage z-order self._canvas.tag_lower("inner_parts") self._canvas.tag_lower("border_parts") + self._canvas.tag_lower("background_parts") return requires_recoloring @@ -641,6 +686,7 @@ class DrawEngine: if requires_recoloring: # new parts were added -> manage z-order self._canvas.tag_lower("inner_parts") self._canvas.tag_lower("border_parts") + self._canvas.tag_lower("background_parts") return requires_recoloring @@ -652,8 +698,10 @@ class DrawEngine: returns bool if recoloring is necessary """ - width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only - height = math.floor(height / 2) * 2 + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger corner_radius = min(width / 2, height / 2) @@ -824,8 +872,10 @@ class DrawEngine: border_width: Union[float, int], button_length: Union[float, int], button_corner_radius: Union[float, int], slider_value: float, orientation: str) -> bool: - width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only - height = math.floor(height / 2) * 2 + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger corner_radius = min(width / 2, height / 2) @@ -980,8 +1030,11 @@ class DrawEngine: def draw_rounded_scrollbar(self, width: Union[float, int], height: Union[float, int], corner_radius: Union[float, int], border_spacing: Union[float, int], start_value: float, end_value: float, orientation: str) -> bool: - width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only - height = math.floor(height / 2) * 2 + + if self._round_width_to_even_numbers: + width = math.floor(width / 2) * 2 # round _current_width and _current_height and restrict them to even values only + if self._round_height_to_even_numbers: + height = math.floor(height / 2) * 2 if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger corner_radius = min(width / 2, height / 2) @@ -1176,6 +1229,7 @@ class DrawEngine: self._canvas.tag_raise("dropdown_arrow") requires_recoloring = True + self._canvas.itemconfigure("dropdown_arrow", font=("CustomTkinter_shapes_font", -size)) self._canvas.coords("dropdown_arrow", x_position, y_position) return requires_recoloring diff --git a/customtkinter/windows/widgets/core_widget_classes/__init__.py b/customtkinter/windows/widgets/core_widget_classes/__init__.py new file mode 100644 index 0000000..75e2d84 --- /dev/null +++ b/customtkinter/windows/widgets/core_widget_classes/__init__.py @@ -0,0 +1,2 @@ +from .dropdown_menu import DropdownMenu +from .ctk_base_class import CTkBaseClass diff --git a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py new file mode 100644 index 0000000..491a253 --- /dev/null +++ b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py @@ -0,0 +1,312 @@ +import sys +import tkinter +import tkinter.ttk as ttk +from typing import Union, Callable, Tuple + +try: + from typing import TypedDict +except ImportError: + from typing_extensions import TypedDict + +# 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('', 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[] 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: + sys.stderr.write(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) <= 3: + return font + + else: + raise ValueError(f"Wrong font type {type(font)}\n" + + f"For consistency, Customtkinter requires the font argument to be a tuple of len 2 or 3 or an instance of CTkFont.\n" + + f"\nUsage example:\n" + + f"font=customtkinter.CTkFont(family='', size=)\n" + + f"font=('', )\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: + sys.stderr.write(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 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() diff --git a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py new file mode 100644 index 0000000..a6b8186 --- /dev/null +++ b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py @@ -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='', size=)\n" + + f"font=('', )\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() diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py new file mode 100644 index 0000000..595e703 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_button.py @@ -0,0 +1,554 @@ +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[tkinter.PhotoImage, 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 + + # canvas event bindings + self._canvas.bind("", self._on_enter) + self._canvas.bind("", self._on_leave) + self._canvas.bind("", self._clicked) + self._canvas.bind("", self._clicked) + + # configure cursor and initial draw + self._set_cursor() + self._draw() + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._create_grid() + + if self._text_label is not None: + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + self._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("", self._on_enter) + self._text_label.bind("", self._on_leave) + self._text_label.bind("", self._clicked) + self._text_label.bind("", 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("", self._on_enter) + self._image_label.bind("", self._on_leave) + self._image_label.bind("", self._clicked) + self._image_label.bind("", 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) + require_redraw = True + + if "state" in kwargs: + self._state = kwargs.pop("state") + self._set_cursor() + require_redraw = True + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "compound" in kwargs: + self._compound = kwargs.pop("compound") + require_redraw = True + + if "anchor" in kwargs: + self._anchor = kwargs.pop("anchor") + require_redraw = True + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + elif attribute_name == "border_width": + return self._border_width + elif attribute_name == "border_spacing": + return self._border_spacing + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "hover_color": + return self._hover_color + elif attribute_name == "border_color": + return self._border_color + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "text_color_disabled": + return self._text_color_disabled + elif attribute_name == "background_corner_colors": + return self._background_corner_colors + + elif attribute_name == "text": + return self._text + elif attribute_name == "font": + return self._font + elif attribute_name == "textvariable": + return self._textvariable + elif attribute_name == "image": + return self._image + elif attribute_name == "state": + return self._state + elif attribute_name == "hover": + return self._hover + elif attribute_name == "command": + return self._command + elif attribute_name == "compound": + return self._compound + elif attribute_name == "anchor": + return self._anchor + else: + return super().cget(attribute_name) + + def _set_cursor(self): + if self._cursor_manipulation_enabled: + if self._state == tkinter.DISABLED: + if sys.platform == "darwin" and self._command is not None: + self.configure(cursor="arrow") + elif sys.platform.startswith("win") and self._command is not None: + self.configure(cursor="arrow") + + elif self._state == tkinter.NORMAL: + if sys.platform == "darwin" and self._command is not None: + self.configure(cursor="pointinghand") + elif sys.platform.startswith("win") and self._command is not None: + self.configure(cursor="hand2") + + def _on_enter(self, event=None): + if self._hover is True and self._state == "normal": + if self._hover_color is None: + inner_parts_color = self._fg_color + else: + inner_parts_color = self._hover_color + + # set color of inner button parts to hover color + self._canvas.itemconfig("inner_parts", + outline=self._apply_appearance_mode(inner_parts_color), + fill=self._apply_appearance_mode(inner_parts_color)) + + # set text_label bg color to button hover color + if self._text_label is not None: + self._text_label.configure(bg=self._apply_appearance_mode(inner_parts_color)) + + # set image_label bg color to button hover color + if self._image_label is not None: + self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color)) + + def _on_leave(self, event=None): + self._click_animation_running = False + + if self._fg_color == "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: str = None) -> str: + """ called on the tkinter.Label and tkinter.Canvas """ + canvas_bind_return = self._canvas.bind(sequence, command, add) + label_bind_return = self._text_label.bind(sequence, command, add) + return canvas_bind_return + " + " + label_bind_return + + def unbind(self, sequence: str, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + canvas_bind_return, label_bind_return = funcid.split(" + ") + self._canvas.unbind(sequence, canvas_bind_return) + self._text_label.unbind(sequence, label_bind_return) + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/customtkinter/windows/widgets/ctk_checkbox.py b/customtkinter/windows/widgets/ctk_checkbox.py new file mode 100644 index 0000000..fb8a7e2 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_checkbox.py @@ -0,0 +1,448 @@ +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._canvas.bind("", self._on_enter) + self._canvas.bind("", self._on_leave) + self._canvas.bind("", self.toggle) + + self._text_label = tkinter.Label(master=self, + bd=0, + padx=0, + pady=0, + text=self._text, + justify=tkinter.LEFT, + font=self._apply_font_scaling(self._font), + textvariable=self._textvariable) + self._text_label.grid(row=0, column=2, sticky="w") + self._text_label["anchor"] = "w" + + self._text_label.bind("", self._on_enter) + self._text_label.bind("", self._on_leave) + self._text_label.bind("", self.toggle) + + # register variable callback and set state according to variable + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._check_state = True if self._variable.get() == self._onvalue else False + + self._draw() # initial draw + self._set_cursor() + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6)) + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + self._canvas.delete("checkmark") + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.configure(width=self._apply_widget_scaling(self._checkbox_width), + height=self._apply_widget_scaling(self._checkbox_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + if self._text_label is not None: + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._bg_canvas.grid_forget() + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + def destroy(self): + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + 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=None, command=None, add=None): + """ called on the tkinter.Canvas """ + return self._canvas.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Canvas """ + return self._canvas.unbind(sequence, funcid) + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/customtkinter/windows/widgets/ctk_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py new file mode 100644 index 0000000..070919a --- /dev/null +++ b/customtkinter/windows/widgets/ctk_combobox.py @@ -0,0 +1,413 @@ +import tkinter +import sys +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() + + # insert default value + if len(self._values) > 0: + self._entry.insert(0, self._values[0]) + else: + self._entry.insert(0, "CTkComboBox") + + self._draw() # initial draw + + # event bindings + self._canvas.tag_bind("right_parts", "", self._on_enter) + self._canvas.tag_bind("dropdown_arrow", "", self._on_enter) + self._canvas.tag_bind("right_parts", "", self._on_leave) + self._canvas.tag_bind("dropdown_arrow", "", self._on_leave) + self._canvas.tag_bind("right_parts", "", self._clicked) + self._canvas.tag_bind("dropdown_arrow", "", self._clicked) + + if self._variable is not None: + self._entry.configure(textvariable=self._variable) + + def _create_grid(self): + self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew") + + left_section_width = self._current_width - self._current_height + self._entry.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="ew", + padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)), + max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3))), + pady=self._apply_widget_scaling(self._border_width)) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + # change entry font size and grid padding + self._entry.configure(font=self._apply_font_scaling(self._font)) + self._create_grid() + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._entry.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew") + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + 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), + 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 self._values + elif attribute_name == "state": + return self._state + elif attribute_name == "hover": + return self._hover + elif attribute_name == "variable": + return self._variable + elif attribute_name == "command": + return self._command + elif attribute_name == "justify": + return self._entry.cget("justify") + else: + return super().cget(attribute_name) + + def _on_enter(self, event=0): + if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0: + if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled: + self._canvas.configure(cursor="pointinghand") + elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled: + self._canvas.configure(cursor="hand2") + + # set color of inner button parts to hover color + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_hover_color), + fill=self._apply_appearance_mode(self._button_hover_color)) + self._canvas.itemconfig("border_parts_right", + outline=self._apply_appearance_mode(self._button_hover_color), + fill=self._apply_appearance_mode(self._button_hover_color)) + + def _on_leave(self, event=0): + if sys.platform == "darwin" and len(self._values) > 0 and self._cursor_manipulation_enabled: + self._canvas.configure(cursor="arrow") + elif sys.platform.startswith("win") and len(self._values) > 0 and self._cursor_manipulation_enabled: + self._canvas.configure(cursor="arrow") + + # set color of inner button parts + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + self._canvas.itemconfig("border_parts_right", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + + def _dropdown_callback(self, value: str): + if self._state == "readonly": + self._entry.configure(state="normal") + self._entry.delete(0, tkinter.END) + self._entry.insert(0, value) + self._entry.configure(state="readonly") + else: + self._entry.delete(0, tkinter.END) + self._entry.insert(0, value) + + if self._command is not None: + self._command(value) + + def set(self, value: str): + if self._state == "readonly": + self._entry.configure(state="normal") + self._entry.delete(0, tkinter.END) + self._entry.insert(0, value) + self._entry.configure(state="readonly") + else: + self._entry.delete(0, tkinter.END) + self._entry.insert(0, value) + + def get(self) -> str: + return self._entry.get() + + def _clicked(self, event=0): + if self._state is not tkinter.DISABLED and len(self._values) > 0: + self._open_dropdown_menu() + + def bind(self, sequence=None, command=None, add=None): + """ called on the tkinter.Entry """ + return self._entry.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Entry """ + return self._entry.unbind(sequence, funcid) + + def focus(self): + return self._entry.focus() + + def focus_set(self): + return self._entry.focus_set() + + def focus_force(self): + return self._entry.focus_force() diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py new file mode 100644 index 0000000..ac1edb6 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_entry.py @@ -0,0 +1,372 @@ +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)) + + self._create_grid() + + check_kwargs_empty(kwargs, raise_error=True) + + self._entry.bind('', self._entry_focus_out) + self._entry.bind('', self._entry_focus_in) + + self._activate_placeholder() + self._draw() + + def _create_grid(self): + self._canvas.grid(column=0, row=0, sticky="nswe") + + if self._corner_radius >= self._minimum_x_padding: + self._entry.grid(column=0, row=0, sticky="nswe", + padx=min(self._apply_widget_scaling(self._corner_radius), round(self._apply_widget_scaling(self._current_height/2))), + pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1))) + else: + self._entry.grid(column=0, row=0, sticky="nswe", + padx=self._apply_widget_scaling(self._minimum_x_padding), + pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width + 1))) + + def _textvariable_callback(self, var_name, index, mode): + if self._textvariable.get() == "": + self._activate_placeholder() + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._entry.configure(font=self._apply_font_scaling(self._font)) + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) + self._create_grid() + self._draw(no_color_updates=True) + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._entry.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(column=0, row=0, sticky="nswe") + + def destroy(self): + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + 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), + 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), + 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=None): + """ called on the tkinter.Entry """ + return self._entry.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Entry """ + return self._entry.unbind(sequence, funcid) + + def _activate_placeholder(self): + if self._entry.get() == "" and self._placeholder_text is not None and (self._textvariable is None or self._textvariable == ""): + self._placeholder_text_active = True + + self._pre_placeholder_arguments = {"show": self._entry.cget("show")} + self._entry.config(fg=self._apply_appearance_mode(self._placeholder_text_color), + 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: + 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) diff --git a/customtkinter/windows/widgets/ctk_frame.py b/customtkinter/windows/widgets/ctk_frame.py new file mode 100644 index 0000000..baa9fc3 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_frame.py @@ -0,0 +1,191 @@ +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=None): + """ called on the tkinter.Canvas """ + return self._canvas.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Canvas """ + return self._canvas.unbind(sequence, funcid) diff --git a/customtkinter/windows/widgets/ctk_label.py b/customtkinter/windows/widgets/ctk_label.py new file mode 100644 index 0000000..293f923 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_label.py @@ -0,0 +1,269 @@ +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[tkinter.PhotoImage, 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 = None) -> str: + """ called on the tkinter.Label and tkinter.Canvas """ + canvas_bind_return = self._canvas.bind(sequence, command, add) + label_bind_return = self._label.bind(sequence, command, add) + return canvas_bind_return + " + " + label_bind_return + + def unbind(self, sequence: str, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + canvas_bind_return, label_bind_return = funcid.split(" + ") + self._canvas.unbind(sequence, canvas_bind_return) + self._label.unbind(sequence, label_bind_return) + + 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() diff --git a/customtkinter/windows/widgets/ctk_optionmenu.py b/customtkinter/windows/widgets/ctk_optionmenu.py new file mode 100644 index 0000000..e6229fe --- /dev/null +++ b/customtkinter/windows/widgets/ctk_optionmenu.py @@ -0,0 +1,415 @@ +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 .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) + self._create_grid() + + if not self._dynamic_resizing: + self.grid_propagate(0) + + if self._cursor_manipulation_enabled: + if sys.platform == "darwin": + self.configure(cursor="pointinghand") + elif sys.platform.startswith("win"): + self.configure(cursor="hand2") + + # event bindings + self._canvas.bind("", self._on_enter) + self._canvas.bind("", self._on_leave) + self._canvas.bind("", self._clicked) + self._canvas.bind("", self._clicked) + + self._text_label.bind("", self._on_enter) + self._text_label.bind("", self._on_leave) + self._text_label.bind("", self._clicked) + self._text_label.bind("", self._clicked) + + self._draw() # initial draw + + if self._variable is not None: + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._current_value = self._variable.get() + self._text_label.configure(text=self._current_value) + + def _create_grid(self): + self._canvas.grid(row=0, column=0, sticky="nsew") + + left_section_width = self._current_width - self._current_height + self._text_label.grid(row=0, column=0, sticky="ew", + padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(3)), + max(self._apply_widget_scaling(self._current_width - left_section_width + 3), self._apply_widget_scaling(3)))) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + # change label font size and grid padding + self._text_label.configure(font=self._apply_font_scaling(self._font)) + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._create_grid() + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._canvas.grid_forget() + self._canvas.grid(row=0, column=0, sticky="nsew") + + def destroy(self): + if self._variable is not None: # remove old callback + self._variable.trace_remove("write", self._variable_callback_name) + + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + 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_color" in kwargs: + self._dropdown_menu.configure(fg_color=kwargs.pop("dropdown_color")) + + if "dropdown_hover_color" in kwargs: + self._dropdown_menu.configure(hover_color=kwargs.pop("dropdown_hover_color")) + + if "dropdown_text_color" in kwargs: + self._dropdown_menu.configure(text_color=kwargs.pop("dropdown_text_color")) + + if "font" in kwargs: + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + self._font = self._check_font_type(kwargs.pop("font")) + if isinstance(self._font, CTkFont): + self._font.add_size_configure_callback(self._update_font) + + self._update_font() + + if "command" in kwargs: + self._command = kwargs.pop("command") + + if "variable" in kwargs: + if self._variable is not None: # remove old callback + self._variable.trace_remove("write", self._variable_callback_name) + + self._variable = kwargs.pop("variable") + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._current_value = self._variable.get() + self._text_label.configure(text=self._current_value) + else: + self._variable = None + + if "values" in kwargs: + self._values = kwargs.pop("values") + self._dropdown_menu.configure(values=self._values) + + if "dropdown_font" in kwargs: + self._dropdown_menu.configure(font=kwargs.pop("dropdown_font")) + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "state" in kwargs: + self._state = kwargs.pop("state") + require_redraw = True + + if "dynamic_resizing" in kwargs: + self._dynamic_resizing = kwargs.pop("dynamic_resizing") + if not self._dynamic_resizing: + self.grid_propagate(0) + else: + self.grid_propagate(1) + + if "anchor" in kwargs: + self._text_label.configure(anchor=kwargs.pop("anchor")) + + super().configure(require_redraw=require_redraw, **kwargs) + + def cget(self, attribute_name: str) -> any: + if attribute_name == "corner_radius": + return self._corner_radius + + elif attribute_name == "fg_color": + return self._fg_color + elif attribute_name == "button_color": + return self._button_color + elif attribute_name == "button_hover_color": + return self._button_hover_color + elif attribute_name == "text_color": + return self._text_color + elif attribute_name == "text_color_disabled": + return self._text_color_disabled + elif attribute_name == "dropdown_fg_color": + return self._dropdown_menu.cget("fg_color") + elif attribute_name == "dropdown_hover_color": + return self._dropdown_menu.cget("hover_color") + elif attribute_name == "dropdown_text_color": + return self._dropdown_menu.cget("text_color") + + elif attribute_name == "font": + return self._font + elif attribute_name == "dropdown_font": + return self._dropdown_menu.cget("font") + elif attribute_name == "values": + return self._values + elif attribute_name == "variable": + return self._variable + elif attribute_name == "state": + return self._state + elif attribute_name == "hover": + return self._hover + elif attribute_name == "command": + return self._command + elif attribute_name == "dynamic_resizing": + return self._dynamic_resizing + elif attribute_name == "anchor": + return self._text_label.cget("anchor") + + else: + return super().cget(attribute_name) + + def _open_dropdown_menu(self): + self._dropdown_menu.open(self.winfo_rootx(), + self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0)) + + def _on_enter(self, event=0): + if self._hover is True and self._state == tkinter.NORMAL and len(self._values) > 0: + # set color of inner button parts to hover color + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_hover_color), + fill=self._apply_appearance_mode(self._button_hover_color)) + + def _on_leave(self, event=0): + # set color of inner button parts + self._canvas.itemconfig("inner_parts_right", + outline=self._apply_appearance_mode(self._button_color), + fill=self._apply_appearance_mode(self._button_color)) + + def _variable_callback(self, var_name, index, mode): + if not self._variable_callback_blocked: + self._current_value = self._variable.get() + self._text_label.configure(text=self._current_value) + + def _dropdown_callback(self, value: str): + self._current_value = value + self._text_label.configure(text=self._current_value) + + if self._variable is not None: + self._variable_callback_blocked = True + self._variable.set(self._current_value) + self._variable_callback_blocked = False + + if self._command is not None: + self._command(self._current_value) + + def set(self, value: str): + self._current_value = value + self._text_label.configure(text=self._current_value) + + if self._variable is not None: + self._variable_callback_blocked = True + self._variable.set(self._current_value) + self._variable_callback_blocked = False + + def get(self) -> str: + return self._current_value + + def _clicked(self, event=0): + if self._state is not tkinter.DISABLED and len(self._values) > 0: + self._open_dropdown_menu() + + def bind(self, sequence: str = None, command: Callable = None, add: str = None) -> str: + """ called on the tkinter.Label and tkinter.Canvas """ + canvas_bind_return = self._canvas.bind(sequence, command, add) + label_bind_return = self._text_label.bind(sequence, command, add) + return canvas_bind_return + " + " + label_bind_return + + def unbind(self, sequence: str, funcid: str = None): + """ called on the tkinter.Label and tkinter.Canvas """ + canvas_bind_return, label_bind_return = funcid.split(" + ") + self._canvas.unbind(sequence, canvas_bind_return) + self._text_label.unbind(sequence, label_bind_return) + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/customtkinter/windows/widgets/ctk_progressbar.py b/customtkinter/windows/widgets/ctk_progressbar.py new file mode 100644 index 0000000..2859ff4 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_progressbar.py @@ -0,0 +1,299 @@ +import tkinter +import math +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 + + +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: str = "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 + + # 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 indeterminate mode """ + if not self._loop_running: + self._loop_running = True + self._internal_loop() + + def stop(self): + """ stop indeterminate mode """ + self._loop_running = False + + def _internal_loop(self): + if self._loop_running: + if self._mode == "determinate": + self._determinate_value += self._determinate_speed / 50 + if self._determinate_value > 1: + self._determinate_value -= 1 + self._draw() + self.after(20, self._internal_loop) + else: + self._indeterminate_value += self._indeterminate_speed + self._draw() + self.after(20, self._internal_loop) + + def step(self): + if self._mode == "determinate": + self._determinate_value += self._determinate_speed / 50 + if self._determinate_value > 1: + self._determinate_value -= 1 + self._draw() + else: + self._indeterminate_value += self._indeterminate_speed + self._draw() + + def bind(self, sequence=None, command=None, add=None): + """ called on the tkinter.Canvas """ + return self._canvas.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Canvas """ + return self._canvas.unbind(sequence, funcid) + + def focus(self): + return self._canvas.focus() + + def focus_set(self): + return self._canvas.focus_set() + + def focus_force(self): + return self._canvas.focus_force() diff --git a/customtkinter/windows/widgets/ctk_radiobutton.py b/customtkinter/windows/widgets/ctk_radiobutton.py new file mode 100644 index 0000000..9ba3a29 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_radiobutton.py @@ -0,0 +1,416 @@ +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._bg_canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._current_width), + height=self._apply_widget_scaling(self._current_height)) + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._radiobutton_width), + height=self._apply_widget_scaling(self._radiobutton_height)) + self._canvas.grid(row=0, column=0) + self._draw_engine = DrawEngine(self._canvas) + + self._canvas.bind("", self._on_enter) + self._canvas.bind("", self._on_leave) + self._canvas.bind("", self.invoke) + + self._text_label = tkinter.Label(master=self, + bd=0, + padx=0, + pady=0, + text=self._text, + justify=tkinter.LEFT, + font=self._apply_font_scaling(self._font), + textvariable=self._textvariable) + self._text_label.grid(row=0, column=2, sticky="w") + self._text_label["anchor"] = "w" + + self._text_label.bind("", self._on_enter) + self._text_label.bind("", self._on_leave) + self._text_label.bind("", self.invoke) + + if self._variable is not None: + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._check_state = True if self._variable.get() == self._value else False + + self._draw() # initial draw + self._set_cursor() + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6)) + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.configure(width=self._apply_widget_scaling(self._radiobutton_width), + height=self._apply_widget_scaling(self._radiobutton_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._bg_canvas.grid_forget() + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + def destroy(self): + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _draw(self, no_color_updates=False): + 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=None, command=None, add=None): + """ called on the tkinter.Canvas """ + return self._canvas.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Canvas """ + return self._canvas.unbind(sequence, funcid) + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py new file mode 100644 index 0000000..99134ff --- /dev/null +++ b/customtkinter/windows/widgets/ctk_scrollbar.py @@ -0,0 +1,267 @@ +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"]["scrollbar_color"] if button_color is None else self._check_color_type(button_color) + self._button_hover_color = ThemeManager.theme["CTkScrollbar"]["scrollbar_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._canvas.bind("", self._on_enter) + self._canvas.bind("", self._on_leave) + self._canvas.tag_bind("border_parts", "", self._clicked) + self._canvas.bind("", self._clicked) + self._canvas.bind("", self._mouse_scroll_event) + + self._draw() + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _get_scrollbar_values_for_minimum_pixel_size(self): + # correct scrollbar float values if scrollbar is too small + if self._orientation == "vertical": + scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_height + if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_height != 0: + # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length + interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_height) + corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor + corrected_start_value = self._start_value - self._start_value * interval_extend_factor + return corrected_start_value, corrected_end_value + else: + return self._start_value, self._end_value + + else: + scrollbar_pixel_length = (self._end_value - self._start_value) * self._current_width + if scrollbar_pixel_length < self._minimum_pixel_length and -scrollbar_pixel_length + self._current_width != 0: + # calculate how much to increase the float interval values so that the scrollbar width is self.minimum_pixel_length + interval_extend_factor = (-scrollbar_pixel_length + self._minimum_pixel_length) / (-scrollbar_pixel_length + self._current_width) + corrected_end_value = self._end_value + (1 - self._end_value) * interval_extend_factor + corrected_start_value = self._start_value - self._start_value * interval_extend_factor + return corrected_start_value, corrected_end_value + else: + return self._start_value, self._end_value + + def _draw(self, no_color_updates=False): + 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=None): + """ called on the tkinter.Canvas """ + return self._canvas.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Canvas """ + return self._canvas.unbind(sequence, funcid) + + def focus(self): + return self._canvas.focus() + + def focus_set(self): + return self._canvas.focus_set() + + def focus_force(self): + return self._canvas.focus_force() diff --git a/customtkinter/windows/widgets/ctk_segmented_button.py b/customtkinter/windows/widgets/ctk_segmented_button.py new file mode 100644 index 0000000..05b57c9 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_segmented_button.py @@ -0,0 +1,410 @@ +import tkinter +from typing import Union, Tuple, List, Dict, Callable, Optional, 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 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}'") + diff --git a/customtkinter/windows/widgets/ctk_slider.py b/customtkinter/windows/widgets/ctk_slider.py new file mode 100644 index 0000000..a88e956 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_slider.py @@ -0,0 +1,384 @@ +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._canvas.bind("", self._on_enter) + self._canvas.bind("", self._on_leave) + self._canvas.bind("", self._clicked) + self._canvas.bind("", self._clicked) + + self._set_cursor() + self._draw() # initial draw + + if self._variable is not None: + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self._variable_callback_blocked = True + self.set(self._variable.get(), from_variable_callback=True) + self._variable_callback_blocked = False + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width=None, height=None): + super()._set_dimensions(width, height) + + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._draw() + + def destroy(self): + # remove variable_callback from variable callbacks if variable exists + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + super().destroy() + + def _set_cursor(self): + if self._state == "normal" and self._cursor_manipulation_enabled: + if sys.platform == "darwin": + self.configure(cursor="pointinghand") + elif sys.platform.startswith("win"): + self.configure(cursor="hand2") + + elif self._state == "disabled" and self._cursor_manipulation_enabled: + if sys.platform == "darwin": + self.configure(cursor="arrow") + elif sys.platform.startswith("win"): + self.configure(cursor="arrow") + + def _draw(self, no_color_updates=False): + 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=None, command=None, add=None): + """ called on the tkinter.Canvas """ + return self._canvas.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Canvas """ + return self._canvas.unbind(sequence, funcid) + + def focus(self): + return self._canvas.focus() + + def focus_set(self): + return self._canvas.focus_set() + + def focus_force(self): + return self._canvas.focus_force() diff --git a/customtkinter/windows/widgets/ctk_switch.py b/customtkinter/windows/widgets/ctk_switch.py new file mode 100644 index 0000000..a986a65 --- /dev/null +++ b/customtkinter/windows/widgets/ctk_switch.py @@ -0,0 +1,455 @@ +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._bg_canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._current_width), + height=self._apply_widget_scaling(self._current_height)) + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + self._canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self._apply_widget_scaling(self._switch_width), + height=self._apply_widget_scaling(self._switch_height)) + self._canvas.grid(row=0, column=0, sticky="") + self._draw_engine = DrawEngine(self._canvas) + + self._canvas.bind("", self._on_enter) + self._canvas.bind("", self._on_leave) + self._canvas.bind("", self.toggle) + + self._text_label = tkinter.Label(master=self, + bd=0, + padx=0, + pady=0, + text=self._text, + justify=tkinter.LEFT, + font=self._apply_font_scaling(self._font), + textvariable=self._textvariable) + self._text_label.grid(row=0, column=2, sticky="w") + self._text_label["anchor"] = "w" + + self._text_label.bind("", self._on_enter) + self._text_label.bind("", self._on_leave) + self._text_label.bind("", self.toggle) + + if self._variable is not None and self._variable != "": + self._variable_callback_name = self._variable.trace_add("write", self._variable_callback) + self.c_heck_state = True if self._variable.get() == self._onvalue else False + + self._draw() # initial draw + self._set_cursor() + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(6)) + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._canvas.configure(width=self._apply_widget_scaling(self._switch_width), + height=self._apply_widget_scaling(self._switch_height)) + self._draw(no_color_updates=True) + + def _set_dimensions(self, width: int = None, height: int = None): + super()._set_dimensions(width, height) + + self._bg_canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + + def _update_font(self): + """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ + self._text_label.configure(font=self._apply_font_scaling(self._font)) + + # Workaround to force grid to be resized when text changes size. + # Otherwise grid will lag and only resizes if other mouse action occurs. + self._bg_canvas.grid_forget() + self._bg_canvas.grid(row=0, column=0, columnspan=3, sticky="nswe") + + def destroy(self): + # remove variable_callback from variable callbacks if variable exists + if self._variable is not None: + self._variable.trace_remove("write", self._variable_callback_name) + + if isinstance(self._font, CTkFont): + self._font.remove_size_configure_callback(self._update_font) + + super().destroy() + + def _set_cursor(self): + if self._cursor_manipulation_enabled: + if self._state == tkinter.DISABLED: + if sys.platform == "darwin": + self._canvas.configure(cursor="arrow") + if self._text_label is not None: + self._text_label.configure(cursor="arrow") + elif sys.platform.startswith("win"): + self._canvas.configure(cursor="arrow") + if self._text_label is not None: + self._text_label.configure(cursor="arrow") + + elif self._state == tkinter.NORMAL: + if sys.platform == "darwin": + self._canvas.configure(cursor="pointinghand") + if self._text_label is not None: + self._text_label.configure(cursor="pointinghand") + elif sys.platform.startswith("win"): + self._canvas.configure(cursor="hand2") + if self._text_label is not None: + self._text_label.configure(cursor="hand2") + + def _draw(self, no_color_updates=False): + 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=None, command=None, add=None): + """ called on the tkinter.Canvas """ + return self._canvas.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Canvas """ + return self._canvas.unbind(sequence, funcid) + + def focus(self): + return self._text_label.focus() + + def focus_set(self): + return self._text_label.focus_set() + + def focus_force(self): + return self._text_label.focus_force() diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py new file mode 100644 index 0000000..75bde4b --- /dev/null +++ b/customtkinter/windows/widgets/ctk_tabview.py @@ -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 diff --git a/customtkinter/windows/widgets/ctk_textbox.py b/customtkinter/windows/widgets/ctk_textbox.py new file mode 100644 index 0000000..2ba985a --- /dev/null +++ b/customtkinter/windows/widgets/ctk_textbox.py @@ -0,0 +1,502 @@ +import tkinter +from typing import Union, Tuple, Optional + +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), + bg=self._apply_appearance_mode(self._fg_color), + **pop_from_dict_by_set(kwargs, self._valid_tk_text_attributes)) + + check_kwargs_empty(kwargs, raise_error=True) + + # scrollbars + self._scrollbars_activated = activate_scrollbars + self._hide_x_scrollbar = True + self._hide_y_scrollbar = True + + self._y_scrollbar = CTkScrollbar(self, + width=8, + height=0, + border_spacing=0, + fg_color=self._fg_color, + 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) + self._draw() + + def _create_grid_for_text_and_scrollbars(self, re_grid_textbox=False, re_grid_x_scrollbar=False, re_grid_y_scrollbar=False): + + # configure 2x2 grid + self.grid_rowconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing))) + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=0, minsize=self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing))) + + if re_grid_textbox: + self._textbox.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nsew", + padx=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0), + pady=(self._apply_widget_scaling(max(self._corner_radius, self._border_width + self._border_spacing)), 0)) + + if re_grid_x_scrollbar: + if not self._hide_x_scrollbar and self._scrollbars_activated: + self._x_scrollbar.grid(row=1, column=0, rowspan=1, columnspan=1, sticky="ewn", + pady=(3, self._border_spacing + self._border_width), + padx=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling + else: + self._x_scrollbar.grid_forget() + + if re_grid_y_scrollbar: + if not self._hide_y_scrollbar and self._scrollbars_activated: + self._y_scrollbar.grid(row=0, column=1, rowspan=1, columnspan=1, sticky="nsw", + padx=(3, self._border_spacing + self._border_width), + pady=(max(self._corner_radius, self._border_width + self._border_spacing), 0)) # scrollbar grid method without scaling + else: + self._y_scrollbar.grid_forget() + + def _check_if_scrollbars_needed(self, event=None, continue_loop: bool = True): + """ Method hides or places the scrollbars if they are needed on key release event of tkinter.text widget """ + + if self._scrollbars_activated: + if self._textbox.xview() != (0.0, 1.0) and not self._x_scrollbar.winfo_ismapped(): # x scrollbar needed + self._hide_x_scrollbar = False + self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True) + elif self._textbox.xview() == (0.0, 1.0) and self._x_scrollbar.winfo_ismapped(): # x scrollbar not needed + self._hide_x_scrollbar = True + self._create_grid_for_text_and_scrollbars(re_grid_x_scrollbar=True) + + if self._textbox.yview() != (0.0, 1.0) and not self._y_scrollbar.winfo_ismapped(): # y scrollbar needed + self._hide_y_scrollbar = False + self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True) + elif self._textbox.yview() == (0.0, 1.0) and self._y_scrollbar.winfo_ismapped(): # y scrollbar not needed + self._hide_y_scrollbar = True + self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True) + else: + self._hide_x_scrollbar = False + self._hide_x_scrollbar = False + self._create_grid_for_text_and_scrollbars(re_grid_y_scrollbar=True) + + if self._textbox.winfo_exists() and continue_loop is True: + self.after(self._scrollbar_update_time, lambda: self._check_if_scrollbars_needed(continue_loop=True)) + + def _set_scaling(self, *args, **kwargs): + super()._set_scaling(*args, **kwargs) + + self._textbox.configure(font=self._apply_font_scaling(self._font)) + self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), + height=self._apply_widget_scaling(self._desired_height)) + self._create_grid_for_text_and_scrollbars(re_grid_textbox=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, scrollbar_color=self._scrollbar_button_color, + scrollbar_hover_color=self._scrollbar_button_hover_color) + self._y_scrollbar.configure(fg_color=self._bg_color, scrollbar_color=self._scrollbar_button_color, + scrollbar_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=None, command=None, add=None): + """ called on the tkinter.Text """ + + # if sequence is , allow only to add the binding to keep the _textbox_modified_event() being called + if sequence == "": + return self._textbox.bind(sequence, command, add="+") + else: + return self._textbox.bind(sequence, command, add) + + def unbind(self, sequence, funcid=None): + """ called on the tkinter.Text """ + return self._textbox.unbind(sequence, funcid) + + def focus(self): + return self._textbox.focus() + + def focus_set(self): + return self._textbox.focus_set() + + def focus_force(self): + return self._textbox.focus_force() + + def insert(self, index, text, tags=None): + self._check_if_scrollbars_needed() + return self._textbox.insert(index, text, tags) + + def get(self, index1, index2=None): + return self._textbox.get(index1, index2) + + def bbox(self, index): + return self._textbox.bbox(index) + + def compare(self, index, op, index2): + return self._textbox.compare(index, op, index2) + + def delete(self, index1, index2=None): + return self._textbox.delete(index1, index2) + + def dlineinfo(self, index): + return self._textbox.dlineinfo(index) + + def edit_modified(self, arg=None): + return self._textbox.edit_modified(arg) + + def edit_redo(self): + self._check_if_scrollbars_needed() + return self._textbox.edit_redo() + + def edit_reset(self): + return self._textbox.edit_reset() + + def edit_separator(self): + return self._textbox.edit_separator() + + def edit_undo(self): + self._check_if_scrollbars_needed() + return self._textbox.edit_undo() + + def image_create(self, index, **kwargs): + raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") + + def image_cget(self, index, option): + raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") + + def image_configure(self, index): + raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") + + def image_names(self): + raise AttributeError("embedding images is forbidden, because would be incompatible with scaling") + + def index(self, i): + return self._textbox.index(i) + + def mark_gravity(self, mark, gravity=None): + return self._textbox.mark_gravity(mark, gravity) + + def mark_names(self): + return self._textbox.mark_names() + + def mark_next(self, index): + return self._textbox.mark_next(index) + + def mark_previous(self, index): + return self._textbox.mark_previous(index) + + def mark_set(self, mark, index): + return self._textbox.mark_set(mark, index) + + def mark_unset(self, mark): + return self._textbox.mark_unset(mark) + + def scan_dragto(self, x, y): + return self._textbox.scan_dragto(x, y) + + def scan_mark(self, x, y): + return self._textbox.scan_mark(x, y) + + def search(self, pattern, index, *args, **kwargs): + return self._textbox.search(pattern, index, *args, **kwargs) + + def see(self, index): + return self._textbox.see(index) + + def tag_add(self, tagName, index1, index2=None): + return self._textbox.tag_add(tagName, index1, index2) + + def tag_bind(self, tagName, sequence, func, add=None): + return self._textbox.tag_bind(tagName, sequence, func, add) + + def tag_cget(self, tagName, option): + return self._textbox.tag_cget(tagName, option) + + def tag_config(self, tagName, **kwargs): + if "font" in kwargs: + raise AttributeError("'font' option forbidden, because would be incompatible with scaling") + return self._textbox.tag_config(tagName, **kwargs) + + def tag_delete(self, *tagName): + return self._textbox.tag_delete(*tagName) + + def tag_lower(self, tagName, belowThis=None): + return self._textbox.tag_lower(tagName, belowThis) + + def tag_names(self, index=None): + return self._textbox.tag_names(index) + + def tag_nextrange(self, tagName, index1, index2=None): + return self._textbox.tag_nextrange(tagName, index1, index2) + + def tag_prevrange(self, tagName, index1, index2=None): + return self._textbox.tag_prevrange(tagName, index1, index2) + + def tag_raise(self, tagName, aboveThis=None): + return self._textbox.tag_raise(tagName, aboveThis) + + def tag_ranges(self, tagName): + return self._textbox.tag_ranges(tagName) + + def tag_remove(self, tagName, index1, index2=None): + return self._textbox.tag_remove(tagName, index1, index2) + + def tag_unbind(self, tagName, sequence, funcid=None): + return self._textbox.tag_unbind(tagName, sequence, funcid) + + def window_cget(self, index, option): + raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") + + def window_configure(self, index, option): + raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") + + def window_create(self, index, **kwargs): + raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") + + def window_names(self): + raise AttributeError("embedding widgets is forbidden, would probably cause all kinds of problems ;)") + + def xview(self, *args): + return self._textbox.xview(*args) + + def xview_moveto(self, fraction): + return self._textbox.xview_moveto(fraction) + + def xview_scroll(self, n, what): + return self._textbox.xview_scroll(n, what) + + def yview(self, *args): + return self._textbox.yview(*args) + + def yview_moveto(self, fraction): + return self._textbox.yview_moveto(fraction) + + def yview_scroll(self, n, what): + return self._textbox.yview_scroll(n, what) diff --git a/customtkinter/windows/widgets/font/__init__.py b/customtkinter/windows/widgets/font/__init__.py new file mode 100644 index 0000000..64a49f1 --- /dev/null +++ b/customtkinter/windows/widgets/font/__init__.py @@ -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" diff --git a/customtkinter/windows/widgets/font/ctk_font.py b/customtkinter/windows/widgets/font/ctk_font.py new file mode 100644 index 0000000..69fb3f7 --- /dev/null +++ b/customtkinter/windows/widgets/font/ctk_font.py @@ -0,0 +1,88 @@ +from tkinter.font import Font +import copy +from typing import List, Callable, Tuple, Optional, 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) diff --git a/customtkinter/font_manager.py b/customtkinter/windows/widgets/font/font_manager.py similarity index 98% rename from customtkinter/font_manager.py rename to customtkinter/windows/widgets/font/font_manager.py index 91dfd04..b3ef369 100644 --- a/customtkinter/font_manager.py +++ b/customtkinter/windows/widgets/font/font_manager.py @@ -44,7 +44,7 @@ class FontManager: 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(num_fonts_added) + return bool(min(num_fonts_added, 1)) @classmethod def load_font(cls, font_path: str) -> bool: diff --git a/customtkinter/windows/widgets/image/__init__.py b/customtkinter/windows/widgets/image/__init__.py new file mode 100644 index 0000000..b712c89 --- /dev/null +++ b/customtkinter/windows/widgets/image/__init__.py @@ -0,0 +1 @@ +from .ctk_image import CTkImage diff --git a/customtkinter/windows/widgets/image/ctk_image.py b/customtkinter/windows/widgets/image/ctk_image.py new file mode 100644 index 0000000..e197b56 --- /dev/null +++ b/customtkinter/windows/widgets/image/ctk_image.py @@ -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 (, ) 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) + + diff --git a/customtkinter/windows/widgets/scaling/__init__.py b/customtkinter/windows/widgets/scaling/__init__.py new file mode 100644 index 0000000..8fc0db8 --- /dev/null +++ b/customtkinter/windows/widgets/scaling/__init__.py @@ -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 diff --git a/customtkinter/windows/widgets/scaling/scaling_base_class.py b/customtkinter/windows/widgets/scaling/scaling_base_class.py new file mode 100644 index 0000000..74838bc --- /dev/null +++ b/customtkinter/windows/widgets/scaling/scaling_base_class.py @@ -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 len(font) == 3: + return font[0], -abs(round(font[1] * self.__widget_scaling)), font[2] + else: + raise ValueError(f"Can not scale font {font}. font needs to be tuple of len 1, 2 or 3") + + elif isinstance(font, CTkFont): + return font.create_scaled_tuple(self.__widget_scaling) + else: + raise ValueError(f"Can not scale font '{font}' of type {type(font)}. font needs to be tuple or instance of CTkFont") + + def _apply_argument_scaling(self, kwargs: dict) -> dict: + assert self.__scaling_type == "widget" + + scaled_kwargs = copy.copy(kwargs) + + # 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: ('x', '', '', '+-+-', '-', '-') + 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 and 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 and 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 and 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 and in geometry_string + return f"+{x}+{y}" + + else: + return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}+{x}+{y}" diff --git a/customtkinter/scaling_tracker.py b/customtkinter/windows/widgets/scaling/scaling_tracker.py similarity index 73% rename from customtkinter/scaling_tracker.py rename to customtkinter/windows/widgets/scaling/scaling_tracker.py index 2e9983a..d3627c2 100644 --- a/customtkinter/scaling_tracker.py +++ b/customtkinter/windows/widgets/scaling/scaling_tracker.py @@ -11,21 +11,16 @@ class ScalingTracker: widget_scaling = 1 # user values which multiply to detected window scaling factor window_scaling = 1 - spacing_scaling = 1 update_loop_running = False - update_loop_interval = 150 # milliseconds + 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_spacing_scaling(cls, widget) -> float: - window_root = cls.get_window_root_of_widget(widget) - return cls.window_dpi_scaling_dict[window_root] * cls.spacing_scaling - @classmethod def get_window_scaling(cls, window) -> float: window_root = cls.get_window_root_of_widget(window) @@ -36,11 +31,6 @@ class ScalingTracker: cls.widget_scaling = max(widget_scaling_factor, 0.4) cls.update_scaling_callbacks_all() - @classmethod - def set_spacing_scaling(cls, spacing_scaling_factor: float): - cls.spacing_scaling = max(spacing_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) @@ -62,11 +52,9 @@ class ScalingTracker: 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.spacing_scaling, cls.window_dpi_scaling_dict[window] * cls.window_scaling) else: set_scaling_callback(cls.widget_scaling, - cls.spacing_scaling, cls.window_scaling) @classmethod @@ -74,11 +62,9 @@ class ScalingTracker: 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.spacing_scaling, cls.window_dpi_scaling_dict[window] * cls.window_scaling) else: set_scaling_callback(cls.widget_scaling, - cls.spacing_scaling, cls.window_scaling) @classmethod @@ -132,9 +118,32 @@ class ScalingTracker: pass # high DPI scaling works automatically on macOS elif sys.platform.startswith("win"): - from ctypes import windll - windll.shcore.SetProcessDpiAwareness(2) - # Microsoft Docs: https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness + 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 @@ -162,18 +171,34 @@ class ScalingTracker: @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(): + 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: - app.after(cls.update_loop_interval, cls.check_dpi_scaling) + 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 diff --git a/customtkinter/windows/widgets/theme/__init__.py b/customtkinter/windows/widgets/theme/__init__.py new file mode 100644 index 0000000..bd7395a --- /dev/null +++ b/customtkinter/windows/widgets/theme/__init__.py @@ -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") diff --git a/customtkinter/windows/widgets/theme/theme_manager.py b/customtkinter/windows/widgets/theme/theme_manager.py new file mode 100644 index 0000000..e04b679 --- /dev/null +++ b/customtkinter/windows/widgets/theme/theme_manager.py @@ -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") diff --git a/customtkinter/windows/widgets/utility/__init__.py b/customtkinter/windows/widgets/utility/__init__.py new file mode 100644 index 0000000..c4b6fe8 --- /dev/null +++ b/customtkinter/windows/widgets/utility/__init__.py @@ -0,0 +1 @@ +from .utility_functions import pop_from_dict_by_set, check_kwargs_empty diff --git a/customtkinter/windows/widgets/utility/utility_functions.py b/customtkinter/windows/widgets/utility/utility_functions.py new file mode 100644 index 0000000..a9968bb --- /dev/null +++ b/customtkinter/windows/widgets/utility/utility_functions.py @@ -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 diff --git a/documentation_images/CustomTkinter_logo_dark.png b/documentation_images/CustomTkinter_logo_dark.png new file mode 100644 index 0000000..8bd2744 Binary files /dev/null and b/documentation_images/CustomTkinter_logo_dark.png differ diff --git a/documentation_images/CustomTkinter_logo_light.png b/documentation_images/CustomTkinter_logo_light.png new file mode 100644 index 0000000..5ec1cfa Binary files /dev/null and b/documentation_images/CustomTkinter_logo_light.png differ diff --git a/documentation_images/Ubuntu_dark.png b/documentation_images/Ubuntu_dark.png deleted file mode 100644 index d92be28..0000000 Binary files a/documentation_images/Ubuntu_dark.png and /dev/null differ diff --git a/documentation_images/Windows_button_images_dark.png b/documentation_images/Windows_button_images_dark.png deleted file mode 100644 index 795c24a..0000000 Binary files a/documentation_images/Windows_button_images_dark.png and /dev/null differ diff --git a/documentation_images/Windows_button_images_light.png b/documentation_images/Windows_button_images_light.png deleted file mode 100644 index d44e33b..0000000 Binary files a/documentation_images/Windows_button_images_light.png and /dev/null differ diff --git a/documentation_images/Windows_dark.png b/documentation_images/Windows_dark.png deleted file mode 100644 index 9d284ad..0000000 Binary files a/documentation_images/Windows_dark.png and /dev/null differ diff --git a/documentation_images/Windows_manual_mode_change.gif b/documentation_images/Windows_manual_mode_change.gif deleted file mode 100644 index 86aa5ab..0000000 Binary files a/documentation_images/Windows_manual_mode_change.gif and /dev/null differ diff --git a/documentation_images/Windows_scaling.png b/documentation_images/Windows_scaling.png deleted file mode 100755 index cf2737b..0000000 Binary files a/documentation_images/Windows_scaling.png and /dev/null differ diff --git a/documentation_images/Windows_simple_example_dark.png b/documentation_images/Windows_simple_example_dark.png deleted file mode 100644 index 913374c..0000000 Binary files a/documentation_images/Windows_simple_example_dark.png and /dev/null differ diff --git a/documentation_images/Windows_system_mode_change.gif b/documentation_images/Windows_system_mode_change.gif deleted file mode 100644 index 9a355bb..0000000 Binary files a/documentation_images/Windows_system_mode_change.gif and /dev/null differ diff --git a/documentation_images/Winodws_light.png b/documentation_images/Winodws_light.png deleted file mode 100644 index fbeeab5..0000000 Binary files a/documentation_images/Winodws_light.png and /dev/null differ diff --git a/documentation_images/complex_example_dark_Windows.png b/documentation_images/complex_example_dark_Windows.png new file mode 100644 index 0000000..c15703f Binary files /dev/null and b/documentation_images/complex_example_dark_Windows.png differ diff --git a/documentation_images/complex_example_light_macOS.png b/documentation_images/complex_example_light_macOS.png new file mode 100644 index 0000000..80a7c0c Binary files /dev/null and b/documentation_images/complex_example_light_macOS.png differ diff --git a/documentation_images/image_example_dark_Windows.png b/documentation_images/image_example_dark_Windows.png new file mode 100644 index 0000000..46aaa83 Binary files /dev/null and b/documentation_images/image_example_dark_Windows.png differ diff --git a/documentation_images/macOS_button_dark.png b/documentation_images/macOS_button_dark.png deleted file mode 100644 index feb1d1c..0000000 Binary files a/documentation_images/macOS_button_dark.png and /dev/null differ diff --git a/documentation_images/macOS_button_images.png b/documentation_images/macOS_button_images.png deleted file mode 100644 index e187ca8..0000000 Binary files a/documentation_images/macOS_button_images.png and /dev/null differ diff --git a/documentation_images/macOS_dark.png b/documentation_images/macOS_dark.png deleted file mode 100644 index 767f849..0000000 Binary files a/documentation_images/macOS_dark.png and /dev/null differ diff --git a/documentation_images/macOS_light.png b/documentation_images/macOS_light.png deleted file mode 100644 index 1948a24..0000000 Binary files a/documentation_images/macOS_light.png and /dev/null differ diff --git a/documentation_images/macOS_system_mode_change.gif b/documentation_images/macOS_system_mode_change.gif deleted file mode 100644 index 416838a..0000000 Binary files a/documentation_images/macOS_system_mode_change.gif and /dev/null differ diff --git a/documentation_images/single_button_macOS.png b/documentation_images/single_button_macOS.png new file mode 100644 index 0000000..cbb2eb4 Binary files /dev/null and b/documentation_images/single_button_macOS.png differ diff --git a/documentation_images/tkinter_customtkinter_comparison.jpg b/documentation_images/tkinter_customtkinter_comparison.jpg deleted file mode 100644 index 53ba517..0000000 Binary files a/documentation_images/tkinter_customtkinter_comparison.jpg and /dev/null differ diff --git a/documentation_images/tkintermapview_example.gif b/documentation_images/tkintermapview_example.gif deleted file mode 100644 index d177319..0000000 Binary files a/documentation_images/tkintermapview_example.gif and /dev/null differ diff --git a/examples/complex_example.py b/examples/complex_example.py index 199741a..978a706 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -7,184 +7,148 @@ customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "gre class App(customtkinter.CTk): - - WIDTH = 780 - HEIGHT = 520 - def __init__(self): super().__init__() + # configure window self.title("CustomTkinter complex_example.py") - self.geometry(f"{App.WIDTH}x{App.HEIGHT}") - self.protocol("WM_DELETE_WINDOW", self.on_closing) # call .on_closing() when app gets closed + self.geometry(f"{1100}x{580}") - # ============ create two frames ============ - - # configure grid layout (2x1) + # configure grid layout (4x4) self.grid_columnconfigure(1, weight=1) - self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure((2, 3), weight=0) + self.grid_rowconfigure((0, 1, 2), weight=1) - self.frame_left = customtkinter.CTkFrame(master=self, - width=180, - corner_radius=0) - self.frame_left.grid(row=0, column=0, sticky="nswe") + # create sidebar frame with widgets + self.sidebar_frame = customtkinter.CTkFrame(self, width=140, corner_radius=0) + self.sidebar_frame.grid(row=0, column=0, rowspan=4, sticky="nsew") + self.sidebar_frame.grid_rowconfigure(4, weight=1) + self.logo_label = customtkinter.CTkLabel(self.sidebar_frame, text="CustomTkinter", font=customtkinter.CTkFont(size=20, weight="bold")) + self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10)) + self.sidebar_button_1 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_event) + self.sidebar_button_1.grid(row=1, column=0, padx=20, pady=10) + self.sidebar_button_2 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_event) + self.sidebar_button_2.grid(row=2, column=0, padx=20, pady=10) + self.sidebar_button_3 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_event) + self.sidebar_button_3.grid(row=3, column=0, padx=20, pady=10) + self.appearance_mode_label = customtkinter.CTkLabel(self.sidebar_frame, text="Appearance Mode:", anchor="w") + self.appearance_mode_label.grid(row=5, column=0, padx=20, pady=(10, 0)) + self.appearance_mode_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["Light", "Dark", "System"], + command=self.change_appearance_mode_event) + self.appearance_mode_optionemenu.grid(row=6, column=0, padx=20, pady=(10, 10)) + self.scaling_label = customtkinter.CTkLabel(self.sidebar_frame, text="UI Scaling:", anchor="w") + self.scaling_label.grid(row=7, column=0, padx=20, pady=(10, 0)) + self.scaling_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["80%", "90%", "100%", "110%", "120%"], + command=self.change_scaling_event) + self.scaling_optionemenu.grid(row=8, column=0, padx=20, pady=(10, 20)) - self.frame_right = customtkinter.CTkFrame(master=self) - self.frame_right.grid(row=0, column=1, sticky="nswe", padx=20, pady=20) + # create main entry and button + self.entry = customtkinter.CTkEntry(self, placeholder_text="CTkEntry") + self.entry.grid(row=3, column=1, columnspan=2, padx=(20, 0), pady=(20, 20), sticky="nsew") - # ============ frame_left ============ + self.main_button_1 = customtkinter.CTkButton(master=self, fg_color="transparent", border_width=2, text_color=("gray10", "#DCE4EE")) + self.main_button_1.grid(row=3, column=3, padx=(20, 20), pady=(20, 20), sticky="nsew") - # configure grid layout (1x11) - self.frame_left.grid_rowconfigure(0, minsize=10) # empty row with minsize as spacing - self.frame_left.grid_rowconfigure(5, weight=1) # empty row as spacing - self.frame_left.grid_rowconfigure(8, minsize=20) # empty row with minsize as spacing - self.frame_left.grid_rowconfigure(11, minsize=10) # empty row with minsize as spacing + # create textbox + self.textbox = customtkinter.CTkTextbox(self, width=250) + self.textbox.grid(row=0, column=1, padx=(20, 0), pady=(20, 0), sticky="nsew") - self.label_1 = customtkinter.CTkLabel(master=self.frame_left, - text="CustomTkinter", - text_font=("Roboto Medium", -16)) # font name and size in px - self.label_1.grid(row=1, column=0, pady=10, padx=10) + # create tabview + self.tabview = customtkinter.CTkTabview(self, width=250) + self.tabview.grid(row=0, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew") + self.tabview.add("CTkTabview") + self.tabview.add("Tab 2") + self.tabview.add("Tab 3") + self.tabview.tab("CTkTabview").grid_columnconfigure(0, weight=1) # configure grid of individual tabs + self.tabview.tab("Tab 2").grid_columnconfigure(0, weight=1) - self.button_1 = customtkinter.CTkButton(master=self.frame_left, - text="CTkButton", - command=self.button_event) - self.button_1.grid(row=2, column=0, pady=10, padx=20) - - self.button_2 = customtkinter.CTkButton(master=self.frame_left, - text="CTkButton", - command=self.button_event) - self.button_2.grid(row=3, column=0, pady=10, padx=20) - - self.button_3 = customtkinter.CTkButton(master=self.frame_left, - text="CTkButton", - command=self.button_event) - self.button_3.grid(row=4, column=0, pady=10, padx=20) - - self.label_mode = customtkinter.CTkLabel(master=self.frame_left, text="Appearance Mode:") - self.label_mode.grid(row=9, column=0, pady=0, padx=20, sticky="w") - - self.optionmenu_1 = customtkinter.CTkOptionMenu(master=self.frame_left, - values=["Light", "Dark", "System"], - command=self.change_appearance_mode) - self.optionmenu_1.grid(row=10, column=0, pady=10, padx=20, sticky="w") - - # ============ frame_right ============ - - # configure grid layout (3x7) - self.frame_right.rowconfigure((0, 1, 2, 3), weight=1) - self.frame_right.rowconfigure(7, weight=10) - self.frame_right.columnconfigure((0, 1), weight=1) - self.frame_right.columnconfigure(2, weight=0) - - self.frame_info = customtkinter.CTkFrame(master=self.frame_right) - self.frame_info.grid(row=0, column=0, columnspan=2, rowspan=4, pady=20, padx=20, sticky="nsew") - - # ============ frame_info ============ - - # configure grid layout (1x1) - self.frame_info.rowconfigure(0, weight=1) - self.frame_info.columnconfigure(0, weight=1) - - self.label_info_1 = customtkinter.CTkLabel(master=self.frame_info, - text="CTkLabel: Lorem ipsum dolor sit,\n" + - "amet consetetur sadipscing elitr,\n" + - "sed diam nonumy eirmod tempor" , - height=100, - corner_radius=6, # <- custom corner radius - fg_color=("white", "gray38"), # <- custom tuple-color - justify=tkinter.LEFT) - self.label_info_1.grid(column=0, row=0, sticky="nwe", padx=15, pady=15) - - self.progressbar = customtkinter.CTkProgressBar(master=self.frame_info) - self.progressbar.grid(row=1, column=0, sticky="ew", padx=15, pady=15) - - # ============ frame_right ============ + self.optionmenu_1 = customtkinter.CTkOptionMenu(self.tabview.tab("CTkTabview"), dynamic_resizing=False, + values=["Value 1", "Value 2", "Value Long Long Long"]) + self.optionmenu_1.grid(row=0, column=0, padx=20, pady=(20, 10)) + self.combobox_1 = customtkinter.CTkComboBox(self.tabview.tab("CTkTabview"), + values=["Value 1", "Value 2", "Value Long....."]) + self.combobox_1.grid(row=1, column=0, padx=20, pady=(10, 10)) + self.string_input_button = customtkinter.CTkButton(self.tabview.tab("CTkTabview"), text="Open CTkInputDialog", + command=self.open_input_dialog_event) + self.string_input_button.grid(row=2, column=0, padx=20, pady=(10, 10)) + self.label_tab_2 = customtkinter.CTkLabel(self.tabview.tab("Tab 2"), text="CTkLabel on Tab 2") + self.label_tab_2.grid(row=0, column=0, padx=20, pady=20) + # create radiobutton frame + self.radiobutton_frame = customtkinter.CTkFrame(self) + self.radiobutton_frame.grid(row=0, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew") self.radio_var = tkinter.IntVar(value=0) - - self.label_radio_group = customtkinter.CTkLabel(master=self.frame_right, - text="CTkRadioButton Group:") - self.label_radio_group.grid(row=0, column=2, columnspan=1, pady=20, padx=10, sticky="") - - self.radio_button_1 = customtkinter.CTkRadioButton(master=self.frame_right, - variable=self.radio_var, - value=0) + self.label_radio_group = customtkinter.CTkLabel(master=self.radiobutton_frame, text="CTkRadioButton Group:") + self.label_radio_group.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="") + self.radio_button_1 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=0) self.radio_button_1.grid(row=1, column=2, pady=10, padx=20, sticky="n") - - self.radio_button_2 = customtkinter.CTkRadioButton(master=self.frame_right, - variable=self.radio_var, - value=1) + self.radio_button_2 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=1) self.radio_button_2.grid(row=2, column=2, pady=10, padx=20, sticky="n") - - self.radio_button_3 = customtkinter.CTkRadioButton(master=self.frame_right, - variable=self.radio_var, - value=2) + self.radio_button_3 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=2) self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="n") - self.slider_1 = customtkinter.CTkSlider(master=self.frame_right, - from_=0, - to=1, - number_of_steps=3, - command=self.progressbar.set) - self.slider_1.grid(row=4, column=0, columnspan=2, pady=10, padx=20, sticky="we") + # create checkbox and switch frame + self.checkbox_slider_frame = customtkinter.CTkFrame(self) + self.checkbox_slider_frame.grid(row=1, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew") + self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_1.grid(row=1, column=0, pady=(20, 10), padx=20, sticky="n") + self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_2.grid(row=2, column=0, pady=10, padx=20, sticky="n") + self.switch_1 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame, command=lambda: print("switch 1 toggle")) + self.switch_1.grid(row=3, column=0, pady=10, padx=20, sticky="n") + self.switch_2 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame) + self.switch_2.grid(row=4, column=0, pady=(10, 20), padx=20, sticky="n") - self.slider_2 = customtkinter.CTkSlider(master=self.frame_right, - command=self.progressbar.set) - self.slider_2.grid(row=5, column=0, columnspan=2, pady=10, padx=20, sticky="we") - - self.switch_1 = customtkinter.CTkSwitch(master=self.frame_right, - text="CTkSwitch") - self.switch_1.grid(row=4, column=2, columnspan=1, pady=10, padx=20, sticky="we") - - self.switch_2 = customtkinter.CTkSwitch(master=self.frame_right, - text="CTkSwitch") - self.switch_2.grid(row=5, column=2, columnspan=1, pady=10, padx=20, sticky="we") - - self.combobox_1 = customtkinter.CTkComboBox(master=self.frame_right, - values=["Value 1", "Value 2"]) - self.combobox_1.grid(row=6, column=2, columnspan=1, pady=10, padx=20, sticky="we") - - self.check_box_1 = customtkinter.CTkCheckBox(master=self.frame_right, - text="CTkCheckBox") - self.check_box_1.grid(row=6, column=0, pady=10, padx=20, sticky="w") - - self.check_box_2 = customtkinter.CTkCheckBox(master=self.frame_right, - text="CTkCheckBox") - self.check_box_2.grid(row=6, column=1, pady=10, padx=20, sticky="w") - - self.entry = customtkinter.CTkEntry(master=self.frame_right, - width=120, - placeholder_text="CTkEntry") - self.entry.grid(row=8, column=0, columnspan=2, pady=20, padx=20, sticky="we") - - self.button_5 = customtkinter.CTkButton(master=self.frame_right, - text="CTkButton", - border_width=2, # <- custom border_width - fg_color=None, # <- no fg_color - command=self.button_event) - self.button_5.grid(row=8, column=2, columnspan=1, pady=20, padx=20, sticky="we") + # create slider and progressbar frame + self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color="transparent") + self.slider_progressbar_frame.grid(row=1, column=1, columnspan=2, padx=(20, 0), pady=(20, 0), sticky="nsew") + self.slider_progressbar_frame.grid_columnconfigure(0, weight=1) + self.slider_progressbar_frame.grid_rowconfigure(4, weight=1) + self.seg_button_1 = customtkinter.CTkSegmentedButton(self.slider_progressbar_frame) + self.seg_button_1.grid(row=0, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + self.progressbar_1 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) + self.progressbar_1.grid(row=1, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + self.progressbar_2 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) + self.progressbar_2.grid(row=2, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + self.slider_1 = customtkinter.CTkSlider(self.slider_progressbar_frame, from_=0, to=1, number_of_steps=4) + self.slider_1.grid(row=3, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + self.slider_2 = customtkinter.CTkSlider(self.slider_progressbar_frame, orientation="vertical") + self.slider_2.grid(row=0, column=1, rowspan=5, padx=(10, 10), pady=(10, 10), sticky="ns") + self.progressbar_3 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orientation="vertical") + self.progressbar_3.grid(row=0, column=2, rowspan=5, padx=(10, 20), pady=(10, 10), sticky="ns") # set default values - self.optionmenu_1.set("Dark") - self.button_3.configure(state="disabled", text="Disabled CTkButton") - self.combobox_1.set("CTkCombobox") - self.radio_button_1.select() - self.slider_1.set(0.2) - self.slider_2.set(0.7) - self.progressbar.set(0.5) - self.switch_2.select() - self.radio_button_3.configure(state=tkinter.DISABLED) - self.check_box_1.configure(state=tkinter.DISABLED, text="CheckBox disabled") - self.check_box_2.select() + self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton") + self.checkbox_2.configure(state="disabled") + self.switch_2.configure(state="disabled") + self.checkbox_1.select() + self.switch_1.select() + self.radio_button_3.configure(state="disabled") + self.appearance_mode_optionemenu.set("Dark") + self.scaling_optionemenu.set("100%") + self.optionmenu_1.set("CTkOptionmenu") + self.combobox_1.set("CTkComboBox") + self.slider_1.configure(command=self.progressbar_2.set) + self.slider_2.configure(command=self.progressbar_3.set) + self.progressbar_1.configure(mode="indeterminnate") + self.progressbar_1.start() + self.textbox.insert("0.0", "CTkTextbox\n\n" + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + self.seg_button_1.configure(values=["CTkSegmentedButton", "Value 2", "Value 3"]) + self.seg_button_1.set("Value 2") - def button_event(self): - print("Button pressed") + def open_input_dialog_event(self): + dialog = customtkinter.CTkInputDialog(text="Type in a number:", title="CTkInputDialog") + print("CTkInputDialog:", dialog.get_input()) - def change_appearance_mode(self, new_appearance_mode): + def change_appearance_mode_event(self, new_appearance_mode: str): customtkinter.set_appearance_mode(new_appearance_mode) - def on_closing(self, event=0): - self.destroy() + def change_scaling_event(self, new_scaling: str): + new_scaling_float = int(new_scaling.replace("%", "")) / 100 + customtkinter.set_widget_scaling(new_scaling_float) + + def sidebar_button_event(self): + print("sidebar_button click") if __name__ == "__main__": diff --git a/examples/example_background_image.py b/examples/example_background_image.py index 5771843..6a95841 100644 --- a/examples/example_background_image.py +++ b/examples/example_background_image.py @@ -1,69 +1,61 @@ -import tkinter -import tkinter.messagebox import customtkinter -from PIL import Image, ImageTk +from PIL import Image import os -customtkinter.set_appearance_mode("Dark") # Modes: "System" (standard), "Dark", "Light" -customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" - -PATH = os.path.dirname(os.path.realpath(__file__)) +customtkinter.set_appearance_mode("dark") class App(customtkinter.CTk): - - APP_NAME = "CustomTkinter example_background_image.py" - WIDTH = 900 - HEIGHT = 600 + width = 900 + height = 600 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.title(App.APP_NAME) - self.geometry(f"{App.WIDTH}x{App.HEIGHT}") - self.minsize(App.WIDTH, App.HEIGHT) - self.maxsize(App.WIDTH, App.HEIGHT) + self.title("CustomTkinter example_background_image.py") + self.geometry(f"{self.width}x{self.height}") self.resizable(False, False) - self.protocol("WM_DELETE_WINDOW", self.on_closing) + # load and create background image + current_path = os.path.dirname(os.path.realpath(__file__)) + self.bg_image = customtkinter.CTkImage(Image.open(current_path + "/test_images/bg_gradient.jpg"), + size=(self.width, self.height)) + self.bg_image_label = customtkinter.CTkLabel(self, image=self.bg_image) + self.bg_image_label.grid(row=0, column=0) - # load image with PIL and convert to PhotoImage - image = Image.open(PATH + "/test_images/bg_gradient.jpg").resize((self.WIDTH, self.HEIGHT)) - self.bg_image = ImageTk.PhotoImage(image) + # create login frame + self.login_frame = customtkinter.CTkFrame(self, corner_radius=0) + self.login_frame.grid(row=0, column=0, sticky="ns") + self.login_label = customtkinter.CTkLabel(self.login_frame, text="CustomTkinter\nLogin Page", + font=customtkinter.CTkFont(size=20, weight="bold")) + self.login_label.grid(row=0, column=0, padx=30, pady=(150, 15)) + self.username_entry = customtkinter.CTkEntry(self.login_frame, width=200, placeholder_text="username") + self.username_entry.grid(row=1, column=0, padx=30, pady=(15, 15)) + self.password_entry = customtkinter.CTkEntry(self.login_frame, width=200, show="*", placeholder_text="password") + self.password_entry.grid(row=2, column=0, padx=30, pady=(0, 15)) + self.login_button = customtkinter.CTkButton(self.login_frame, text="Login", command=self.login_event, width=200) + self.login_button.grid(row=3, column=0, padx=30, pady=(15, 15)) - self.image_label = tkinter.Label(master=self, image=self.bg_image) - self.image_label.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER) + # create main frame + self.main_frame = customtkinter.CTkFrame(self, corner_radius=0) + self.main_frame.grid_columnconfigure(0, weight=1) + self.main_label = customtkinter.CTkLabel(self.main_frame, text="CustomTkinter\nMain Page", + font=customtkinter.CTkFont(size=20, weight="bold")) + self.main_label.grid(row=0, column=0, padx=30, pady=(30, 15)) + self.back_button = customtkinter.CTkButton(self.main_frame, text="Back", command=self.back_event, width=200) + self.back_button.grid(row=1, column=0, padx=30, pady=(15, 15)) - self.frame = customtkinter.CTkFrame(master=self, - width=300, - height=App.HEIGHT, - corner_radius=0) - self.frame.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER) + def login_event(self): + print("Login pressed - username:", self.username_entry.get(), "password:", self.password_entry.get()) - self.label_1 = customtkinter.CTkLabel(master=self.frame, width=200, height=60, - fg_color=("gray70", "gray25"), text="CustomTkinter\ninterface example") - self.label_1.place(relx=0.5, rely=0.3, anchor=tkinter.CENTER) + self.login_frame.grid_forget() # remove login frame + self.main_frame.grid(row=0, column=0, sticky="nsew", padx=100) # show main frame - self.entry_1 = customtkinter.CTkEntry(master=self.frame, corner_radius=6, width=200, placeholder_text="username") - self.entry_1.place(relx=0.5, rely=0.52, anchor=tkinter.CENTER) - - self.entry_2 = customtkinter.CTkEntry(master=self.frame, corner_radius=6, width=200, show="*", placeholder_text="password") - self.entry_2.place(relx=0.5, rely=0.6, anchor=tkinter.CENTER) - - self.button_2 = customtkinter.CTkButton(master=self.frame, text="Login", - corner_radius=6, command=self.button_event, width=200) - self.button_2.place(relx=0.5, rely=0.7, anchor=tkinter.CENTER) - - def button_event(self): - print("Login pressed - username:", self.entry_1.get(), "password:", self.entry_2.get()) - - def on_closing(self, event=0): - self.destroy() - - def start(self): - self.mainloop() + def back_event(self): + self.main_frame.grid_forget() # remove main frame + self.login_frame.grid(row=0, column=0, sticky="ns") # show login frame if __name__ == "__main__": app = App() - app.start() + app.mainloop() diff --git a/examples/example_button_images.py b/examples/example_button_images.py deleted file mode 100644 index 0e65f09..0000000 --- a/examples/example_button_images.py +++ /dev/null @@ -1,67 +0,0 @@ -import customtkinter -from PIL import Image, ImageTk -import os - -PATH = os.path.dirname(os.path.realpath(__file__)) - -customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light" -customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" - - -class App(customtkinter.CTk): - def __init__(self): - super().__init__() - self.geometry("450x260") - self.title("CustomTkinter example_button_images.py") - - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1, minsize=200) - - self.frame_1 = customtkinter.CTkFrame(master=self, width=250, height=240, corner_radius=15) - self.frame_1.grid(row=0, column=0, padx=20, pady=20, sticky="nsew") - self.frame_1.grid_columnconfigure(0, weight=1) - self.frame_1.grid_columnconfigure(1, weight=1) - - self.settings_image = self.load_image("/test_images/settings.png", 20) - self.bell_image = self.load_image("/test_images/bell.png", 20) - self.add_folder_image = self.load_image("/test_images/add-folder.png", 20) - self.add_list_image = self.load_image("/test_images/add-folder.png", 20) - self.add_user_image = self.load_image("/test_images/add-user.png", 20) - self.chat_image = self.load_image("/test_images/chat.png", 20) - self.home_image = self.load_image("/test_images/home.png", 20) - - self.button_1 = customtkinter.CTkButton(master=self.frame_1, image=self.add_folder_image, text="Add Folder", height=32, - compound="right", command=self.button_function) - self.button_1.grid(row=1, column=0, columnspan=2, padx=20, pady=(20, 10), sticky="ew") - - self.button_2 = customtkinter.CTkButton(master=self.frame_1, image=self.add_list_image, text="Add Item", height=32, - compound="right", fg_color="#D35B58", hover_color="#C77C78", - command=self.button_function) - self.button_2.grid(row=2, column=0, columnspan=2, padx=20, pady=10, sticky="ew") - - self.button_3 = customtkinter.CTkButton(master=self.frame_1, image=self.chat_image, text="", width=40, height=40, - corner_radius=10, fg_color="gray40", hover_color="gray25", - command=self.button_function) - self.button_3.grid(row=3, column=0, columnspan=1, padx=20, pady=10, sticky="w") - - self.button_4 = customtkinter.CTkButton(master=self.frame_1, image=self.home_image, text="", width=40, height=40, - corner_radius=10, fg_color="gray40", hover_color="gray25", - command=self.button_function) - self.button_4.grid(row=3, column=1, columnspan=1, padx=20, pady=10, sticky="e") - - self.button_5 = customtkinter.CTkButton(master=self, image=self.add_user_image, text="Add User", width=130, height=60, border_width=2, - corner_radius=10, compound="bottom", border_color="#D35B58", fg_color=("gray84", "gray25"), - hover_color="#C77C78", command=self.button_function) - self.button_5.grid(row=0, column=1, padx=20, pady=20) - - def load_image(self, path, image_size): - """ load rectangular image with path relative to PATH """ - return ImageTk.PhotoImage(Image.open(PATH + path).resize((image_size, image_size))) - - def button_function(self): - print("button pressed") - - -if __name__ == "__main__": - app = App() - app.mainloop() diff --git a/examples/image_example.py b/examples/image_example.py new file mode 100644 index 0000000..c4064b8 --- /dev/null +++ b/examples/image_example.py @@ -0,0 +1,118 @@ +import customtkinter +import os +from PIL import Image + + +class App(customtkinter.CTk): + def __init__(self): + super().__init__() + + self.title("image_example.py") + self.geometry("700x450") + + # set grid layout 1x2 + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + + # load images with light and dark mode image + image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_images") + self.logo_image = customtkinter.CTkImage(Image.open(os.path.join(image_path, "CustomTkinter_logo_single.png")), size=(26, 26)) + self.large_test_image = customtkinter.CTkImage(Image.open(os.path.join(image_path, "large_test_image.png")), size=(500, 150)) + self.image_icon_image = customtkinter.CTkImage(Image.open(os.path.join(image_path, "image_icon_light.png")), size=(20, 20)) + self.home_image = customtkinter.CTkImage(light_image=Image.open(os.path.join(image_path, "home_dark.png")), + dark_image=Image.open(os.path.join(image_path, "home_light.png")), size=(20, 20)) + self.chat_image = customtkinter.CTkImage(light_image=Image.open(os.path.join(image_path, "chat_dark.png")), + dark_image=Image.open(os.path.join(image_path, "chat_light.png")), size=(20, 20)) + self.add_user_image = customtkinter.CTkImage(light_image=Image.open(os.path.join(image_path, "add_user_dark.png")), + dark_image=Image.open(os.path.join(image_path, "add_user_light.png")), size=(20, 20)) + + # create navigation frame + self.navigation_frame = customtkinter.CTkFrame(self, corner_radius=0) + self.navigation_frame.grid(row=0, column=0, sticky="nsew") + self.navigation_frame.grid_rowconfigure(4, weight=1) + + self.navigation_frame_label = customtkinter.CTkLabel(self.navigation_frame, text=" Image Example", image=self.logo_image, + compound="left", font=customtkinter.CTkFont(size=15, weight="bold")) + self.navigation_frame_label.grid(row=0, column=0, padx=20, pady=20) + + self.home_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Home", + fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"), + image=self.home_image, anchor="w", command=self.home_button_event) + self.home_button.grid(row=1, column=0, sticky="ew") + + self.frame_2_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Frame 2", + fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"), + image=self.chat_image, anchor="w", command=self.frame_2_button_event) + self.frame_2_button.grid(row=2, column=0, sticky="ew") + + self.frame_3_button = customtkinter.CTkButton(self.navigation_frame, corner_radius=0, height=40, border_spacing=10, text="Frame 3", + fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"), + image=self.add_user_image, anchor="w", command=self.frame_3_button_event) + self.frame_3_button.grid(row=3, column=0, sticky="ew") + + self.appearance_mode_menu = customtkinter.CTkOptionMenu(self.navigation_frame, values=["Light", "Dark", "System"], + command=self.change_appearance_mode_event) + self.appearance_mode_menu.grid(row=6, column=0, padx=20, pady=20, sticky="s") + + # create home frame + self.home_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent") + self.home_frame.grid_columnconfigure(0, weight=1) + + self.home_frame_large_image_label = customtkinter.CTkLabel(self.home_frame, text="", image=self.large_test_image) + self.home_frame_large_image_label.grid(row=0, column=0, padx=20, pady=10) + + self.home_frame_button_1 = customtkinter.CTkButton(self.home_frame, text="", image=self.image_icon_image) + self.home_frame_button_1.grid(row=1, column=0, padx=20, pady=10) + self.home_frame_button_2 = customtkinter.CTkButton(self.home_frame, text="CTkButton", image=self.image_icon_image, compound="right") + self.home_frame_button_2.grid(row=2, column=0, padx=20, pady=10) + self.home_frame_button_3 = customtkinter.CTkButton(self.home_frame, text="CTkButton", image=self.image_icon_image, compound="top") + self.home_frame_button_3.grid(row=3, column=0, padx=20, pady=10) + self.home_frame_button_4 = customtkinter.CTkButton(self.home_frame, text="CTkButton", image=self.image_icon_image, compound="bottom", anchor="w") + self.home_frame_button_4.grid(row=4, column=0, padx=20, pady=10) + + # create second frame + self.second_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent") + + # create third frame + self.third_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent") + + # select default frame + self.select_frame_by_name("home") + + def select_frame_by_name(self, name): + # set button color for selected button + self.home_button.configure(fg_color=("gray75", "gray25") if name == "home" else "transparent") + self.frame_2_button.configure(fg_color=("gray75", "gray25") if name == "frame_2" else "transparent") + self.frame_3_button.configure(fg_color=("gray75", "gray25") if name == "frame_3" else "transparent") + + # show selected frame + if name == "home": + self.home_frame.grid(row=0, column=1, sticky="nsew") + else: + self.home_frame.grid_forget() + if name == "frame_2": + self.second_frame.grid(row=0, column=1, sticky="nsew") + else: + self.second_frame.grid_forget() + if name == "frame_3": + self.third_frame.grid(row=0, column=1, sticky="nsew") + else: + self.third_frame.grid_forget() + + def home_button_event(self): + self.select_frame_by_name("home") + + def frame_2_button_event(self): + self.select_frame_by_name("frame_2") + + def frame_3_button_event(self): + self.select_frame_by_name("frame_3") + + def change_appearance_mode_event(self, new_appearance_mode): + customtkinter.set_appearance_mode(new_appearance_mode) + + +if __name__ == "__main__": + app = App() + app.mainloop() + diff --git a/examples/simple_example.py b/examples/simple_example.py index 370b4f4..26d900f 100644 --- a/examples/simple_example.py +++ b/examples/simple_example.py @@ -5,7 +5,7 @@ customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" app = customtkinter.CTk() -app.geometry("400x580") +app.geometry("400x780") app.title("CustomTkinter simple_example.py") @@ -21,41 +21,53 @@ frame_1 = customtkinter.CTkFrame(master=app) frame_1.pack(pady=20, padx=60, fill="both", expand=True) label_1 = customtkinter.CTkLabel(master=frame_1, justify=tkinter.LEFT) -label_1.pack(pady=12, padx=10) +label_1.pack(pady=10, padx=10) progressbar_1 = customtkinter.CTkProgressBar(master=frame_1) -progressbar_1.pack(pady=12, padx=10) +progressbar_1.pack(pady=10, padx=10) button_1 = customtkinter.CTkButton(master=frame_1, command=button_callback) -button_1.pack(pady=12, padx=10) +button_1.pack(pady=10, padx=10) slider_1 = customtkinter.CTkSlider(master=frame_1, command=slider_callback, from_=0, to=1) -slider_1.pack(pady=12, padx=10) +slider_1.pack(pady=10, padx=10) slider_1.set(0.5) entry_1 = customtkinter.CTkEntry(master=frame_1, placeholder_text="CTkEntry") -entry_1.pack(pady=12, padx=10) +entry_1.pack(pady=10, padx=10) optionmenu_1 = customtkinter.CTkOptionMenu(frame_1, values=["Option 1", "Option 2", "Option 42 long long long..."]) -optionmenu_1.pack(pady=12, padx=10) +optionmenu_1.pack(pady=10, padx=10) optionmenu_1.set("CTkOptionMenu") combobox_1 = customtkinter.CTkComboBox(frame_1, values=["Option 1", "Option 2", "Option 42 long long long..."]) -combobox_1.pack(pady=12, padx=10) +combobox_1.pack(pady=10, padx=10) optionmenu_1.set("CTkComboBox") checkbox_1 = customtkinter.CTkCheckBox(master=frame_1) -checkbox_1.pack(pady=12, padx=10) +checkbox_1.pack(pady=10, padx=10) radiobutton_var = tkinter.IntVar(value=1) radiobutton_1 = customtkinter.CTkRadioButton(master=frame_1, variable=radiobutton_var, value=1) -radiobutton_1.pack(pady=12, padx=10) +radiobutton_1.pack(pady=10, padx=10) radiobutton_2 = customtkinter.CTkRadioButton(master=frame_1, variable=radiobutton_var, value=2) -radiobutton_2.pack(pady=12, padx=10) +radiobutton_2.pack(pady=10, padx=10) switch_1 = customtkinter.CTkSwitch(master=frame_1) -switch_1.pack(pady=12, padx=10) +switch_1.pack(pady=10, padx=10) + +text_1 = customtkinter.CTkTextbox(master=frame_1, width=200, height=70) +text_1.pack(pady=10, padx=10) +text_1.insert("0.0", "CTkTextbox\n\n\n\n") + +segmented_button_1 = customtkinter.CTkSegmentedButton(master=frame_1, values=["CTkSegmentedButton", "Value 2"]) +segmented_button_1.pack(pady=10, padx=10) + +tabview_1 = customtkinter.CTkTabview(master=frame_1, width=200, height=70) +tabview_1.pack(pady=10, padx=10) +tabview_1.add("CTkTabview") +tabview_1.add("Tab 2") app.mainloop() diff --git a/examples/test_images/CustomTkinter_logo_single.png b/examples/test_images/CustomTkinter_logo_single.png new file mode 100644 index 0000000..7c22940 Binary files /dev/null and b/examples/test_images/CustomTkinter_logo_single.png differ diff --git a/examples/test_images/add-list.png b/examples/test_images/add-list.png deleted file mode 100644 index 193532f..0000000 Binary files a/examples/test_images/add-list.png and /dev/null differ diff --git a/examples/test_images/add_user_dark.png b/examples/test_images/add_user_dark.png new file mode 100644 index 0000000..937d770 Binary files /dev/null and b/examples/test_images/add_user_dark.png differ diff --git a/examples/test_images/add-user.png b/examples/test_images/add_user_light.png similarity index 100% rename from examples/test_images/add-user.png rename to examples/test_images/add_user_light.png diff --git a/examples/test_images/bell.png b/examples/test_images/bell.png deleted file mode 100644 index df3838f..0000000 Binary files a/examples/test_images/bell.png and /dev/null differ diff --git a/examples/test_images/chat_dark.png b/examples/test_images/chat_dark.png new file mode 100644 index 0000000..8ddbc57 Binary files /dev/null and b/examples/test_images/chat_dark.png differ diff --git a/examples/test_images/chat.png b/examples/test_images/chat_light.png similarity index 100% rename from examples/test_images/chat.png rename to examples/test_images/chat_light.png diff --git a/examples/test_images/home_dark.png b/examples/test_images/home_dark.png new file mode 100644 index 0000000..ff9f200 Binary files /dev/null and b/examples/test_images/home_dark.png differ diff --git a/examples/test_images/home.png b/examples/test_images/home_light.png similarity index 100% rename from examples/test_images/home.png rename to examples/test_images/home_light.png diff --git a/examples/test_images/image_icon_light.png b/examples/test_images/image_icon_light.png new file mode 100644 index 0000000..3f32d0f Binary files /dev/null and b/examples/test_images/image_icon_light.png differ diff --git a/examples/test_images/large_test_image.png b/examples/test_images/large_test_image.png new file mode 100644 index 0000000..cfe5df1 Binary files /dev/null and b/examples/test_images/large_test_image.png differ diff --git a/examples/test_images/settings.png b/examples/test_images/settings.png deleted file mode 100644 index 36e5d79..0000000 Binary files a/examples/test_images/settings.png and /dev/null differ diff --git a/requirements.txt b/requirements.txt index e2ec11a..9c2eaa9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -darkdetect~=0.3.1 \ No newline at end of file +darkdetect~=0.3.1 +typing-extensions~=4.4.0 diff --git a/setup.cfg b/setup.cfg index ec181cc..52f85aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ name = customtkinter version = 4.6.3 description = Create modern looking GUIs with Python -long_description = CustomTkinter UI-Library\n\n[](https://github.com/TomSchimansky/CustomTkinter/blob/master/documentation_images/Windows_dark.png)\n\nMore Information: https://github.com/TomSchimansky/CustomTkinter +long_description = file: Readme.md long_description_content_type = text/markdown url = https://github.com/TomSchimansky/CustomTkinter author = Tom Schimansky @@ -17,8 +17,16 @@ classifiers = python_requires = >=3.7 packages = customtkinter - customtkinter.widgets + customtkinter.utility customtkinter.windows + customtkinter.windows.widgets + customtkinter.windows.widgets.appearance_mode + customtkinter.windows.widgets.core_rendering + customtkinter.windows.widgets.core_widget_classes + customtkinter.windows.widgets.font + customtkinter.windows.widgets.image + customtkinter.windows.widgets.scaling + customtkinter.windows.widgets.theme install_requires = darkdetect typing_extensions; python_version<="3.7" diff --git a/test/manual_integration_tests/complex_example_new.py b/test/manual_integration_tests/complex_example_new.py deleted file mode 100644 index 1c9d869..0000000 --- a/test/manual_integration_tests/complex_example_new.py +++ /dev/null @@ -1,149 +0,0 @@ -import tkinter -import tkinter.messagebox -import customtkinter - -customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light" -customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" - - -class App(customtkinter.CTk): - - def __init__(self): - super().__init__() - - self.title("CustomTkinter complex_example.py") - self.geometry(f"{920}x{500}") - self.protocol("WM_DELETE_WINDOW", self.on_closing) # call .on_closing() when app gets closed - - # configure grid layout (4x4) - self.grid_columnconfigure(1, weight=1) - self.grid_columnconfigure((2, 3), weight=0, minsize=200) - self.grid_rowconfigure((0, 1, 2), weight=1) - - # create sidebar frame with widgets - self.sidebar_frame = customtkinter.CTkFrame(self, width=140) - self.sidebar_frame.grid(row=0, column=0, rowspan=4, sticky="nsew") - self.sidebar_frame.grid_rowconfigure(4, weight=1) - self.logo_label = customtkinter.CTkLabel(self.sidebar_frame, text="CustomTkinter", text_font=("Roboto", -16)) - self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10)) - self.sidebar_button_1 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_callback) - self.sidebar_button_1.grid(row=1, column=0, padx=20, pady=10) - self.sidebar_button_2 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_callback) - self.sidebar_button_2.grid(row=2, column=0, padx=20, pady=10) - self.sidebar_button_3 = customtkinter.CTkButton(self.sidebar_frame, command=self.sidebar_button_callback) - self.sidebar_button_3.grid(row=3, column=0, padx=20, pady=10) - self.appearance_mode_label = customtkinter.CTkLabel(self.sidebar_frame, text="Appearance Mode:", anchor="w") - self.appearance_mode_label.grid(row=5, column=0, padx=20, pady=(10, 0)) - self.appearance_mode_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["Light", "Dark", "System"], - command=self.change_appearance_mode) - self.appearance_mode_optionemenu.grid(row=6, column=0, padx=20, pady=(10, 10)) - self.scaling_label = customtkinter.CTkLabel(self.sidebar_frame, text="UI Scaling:", anchor="w") - self.scaling_label.grid(row=7, column=0, padx=20, pady=(10, 0)) - self.scaling_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["80%", "90%", "100%", "110%", "120%"], - command=self.change_scaling) - self.scaling_optionemenu.grid(row=8, column=0, padx=20, pady=(10, 20)) - - # create main entry and button - self.entry = customtkinter.CTkEntry(self, placeholder_text="CTkEntry") - self.entry.grid(row=3, column=1, columnspan=2, padx=(20, 10), pady=(10, 20), sticky="nsew") - - self.main_button_1 = customtkinter.CTkButton(self, fg_color=None, border_width=2) - self.main_button_1.grid(row=3, column=3, padx=(10, 20), pady=(10, 20), sticky="nsew") - - self.textbox = customtkinter.CTkTextbox(self) - self.textbox.grid(row=0, column=1, padx=(20, 10), pady=(20, 10), sticky="nsew") - - # create radiobutton frame - self.radiobutton_frame = customtkinter.CTkFrame(self) - self.radiobutton_frame.grid(row=0, column=3, padx=(10, 20), pady=(20, 10), sticky="nsew") - self.radio_var = tkinter.IntVar(value=0) - self.label_radio_group = customtkinter.CTkLabel(master=self.radiobutton_frame, text="CTkRadioButton Group:") - self.label_radio_group.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="") - self.radio_button_1 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=0) - self.radio_button_1.grid(row=1, column=2, pady=10, padx=20, sticky="n") - self.radio_button_2 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=1) - self.radio_button_2.grid(row=2, column=2, pady=10, padx=20, sticky="n") - self.radio_button_3 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=2) - self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="n") - - # create optionemnu and combobox frame - self.optionemnu_combobox_frame = customtkinter.CTkFrame(self) - self.optionemnu_combobox_frame.grid(row=0, column=2, padx=(10, 10), pady=(20, 10), sticky="nsew") - self.optionmenu_1 = customtkinter.CTkOptionMenu(self.optionemnu_combobox_frame, - dynamic_resizing=False, - values=["Value 1", "Value 2", "Value Long Long Long"]) - self.optionmenu_1.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="ew") - self.combobox_1 = customtkinter.CTkComboBox(self.optionemnu_combobox_frame, - values=["Value 1", "Value 2", "Value Long....."]) - self.combobox_1.grid(row=1, column=0, padx=20, pady=(10, 10), sticky="ew") - self.string_input_button = customtkinter.CTkButton(self.optionemnu_combobox_frame, text="Open CTkInputDialog", - command=self.open_input_dialog) - self.string_input_button.grid(row=2, column=0, padx=20, pady=(10, 10), sticky="ew") - - # create checkbox and switch frame - self.checkbox_slider_frame = customtkinter.CTkFrame(self) - self.checkbox_slider_frame.grid(row=1, column=3, padx=(10, 20), pady=(10, 10), sticky="nsew") - self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) - self.checkbox_1.grid(row=1, column=0, pady=(20, 10), padx=20, sticky="n") - self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) - self.checkbox_2.grid(row=2, column=0, pady=10, padx=20, sticky="n") - self.switch_1 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame, command=lambda: print("switch 1 toggle")) - self.switch_1.grid(row=3, column=0, pady=10, padx=20, sticky="n") - self.switch_2 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame) - self.switch_2.grid(row=4, column=0, pady=(10, 20), padx=20, sticky="n") - - # create slider and progressbar frame - self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color=None) - self.slider_progressbar_frame.grid(row=1, column=1, columnspan=2, padx=(20, 10), pady=(10, 10), sticky="nsew") - self.slider_progressbar_frame.grid_columnconfigure(0, weight=1) - self.slider_progressbar_frame.grid_rowconfigure(3, weight=1) - self.progressbar_1 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) - self.progressbar_1.grid(row=0, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - self.progressbar_2 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) - self.progressbar_2.grid(row=1, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - self.slider_1 = customtkinter.CTkSlider(self.slider_progressbar_frame, from_=0, to=1, number_of_steps=4) - self.slider_1.grid(row=2, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - self.slider_2 = customtkinter.CTkSlider(self.slider_progressbar_frame, orient="vertical") - self.slider_2.grid(row=0, column=1, rowspan=4, padx=(10, 10), pady=(10, 10), sticky="ns") - self.progressbar_3 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orient="vertical") - self.progressbar_3.grid(row=0, column=2, rowspan=4, padx=(10, 20), pady=(10, 10), sticky="ns") - - # set default values - self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton") - self.checkbox_2.configure(state="disabled") - self.switch_2.configure(state="disabled") - self.checkbox_1.select() - self.switch_1.select() - self.radio_button_3.configure(state="disabled") - self.appearance_mode_optionemenu.set("Dark") - self.scaling_optionemenu.set("100%") - self.optionmenu_1.set("CTkOptionmenu") - self.combobox_1.set("CTkComboBox") - self.textbox.insert("1.0", "CTkTextbox\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.") - self.slider_1.configure(command=self.progressbar_2.set) - self.slider_2.configure(command=self.progressbar_3.set) - self.progressbar_1.configure(mode="indeterminnate") - self.progressbar_1.start() - - def open_input_dialog(self): - dialog = customtkinter.CTkInputDialog(master=None, text="Type in a number:", title="CTkInputDialog") - print("CTkInputDialog:", dialog.get_input()) - - def change_appearance_mode(self, new_appearance_mode: str): - customtkinter.set_appearance_mode(new_appearance_mode) - - def change_scaling(self, new_scaling: str): - new_scaling_float = int(new_scaling.replace("%", "")) / 100 - customtkinter.set_spacing_scaling(new_scaling_float) - customtkinter.set_widget_scaling(new_scaling_float) - - def sidebar_button_callback(self): - print("sidebar_button click") - - def on_closing(self, event=0): - self.destroy() - - -if __name__ == "__main__": - app = App() - app.mainloop() diff --git a/examples/simple_example_standard_tkinter.py b/test/manual_integration_tests/simple_example_standard_tkinter.py similarity index 99% rename from examples/simple_example_standard_tkinter.py rename to test/manual_integration_tests/simple_example_standard_tkinter.py index a5078e1..c39ef3e 100644 --- a/examples/simple_example_standard_tkinter.py +++ b/test/manual_integration_tests/simple_example_standard_tkinter.py @@ -5,7 +5,6 @@ app = tkinter.Tk() app.geometry("400x350") app.title("simple_example_standard_tkinter.py") - def button_function(): print("button pressed") diff --git a/test/manual_integration_tests/test_all_widgets_with_colors.py b/test/manual_integration_tests/test_all_widgets_with_colors.py index 1bc58b6..2f35a27 100644 --- a/test/manual_integration_tests/test_all_widgets_with_colors.py +++ b/test/manual_integration_tests/test_all_widgets_with_colors.py @@ -2,7 +2,6 @@ import tkinter import customtkinter customtkinter.set_appearance_mode("System") # Other: "Dark", "Light" -customtkinter.set_default_color_theme("green") # Themes: "blue" (standard), "green", "dark-blue" class TestApp(customtkinter.CTk): @@ -20,13 +19,13 @@ class TestApp(customtkinter.CTk): """ gets called by self.slider_1 """ if value == 0: - self.label_1.set_text("mode: Light") + self.label_1.configure(text="mode: Light") customtkinter.set_appearance_mode("Light") elif value == 1: - self.label_1.set_text("mode: Dark") + self.label_1.configure(text="mode: Dark") customtkinter.set_appearance_mode("Dark") else: - self.label_1.set_text("mode: System") + self.label_1.configure(text="mode: System") customtkinter.set_appearance_mode("System") def create_widgets_on_tk(self): @@ -74,7 +73,7 @@ class TestApp(customtkinter.CTk): self.progress_bar_2 = customtkinter.CTkProgressBar(master=self.ctk_frame) self.progress_bar_2.place(relx=0.5, y=y + 320, anchor=tkinter.CENTER) - self.slider_2 = customtkinter.CTkSlider(master=self.ctk_frame, command=lambda v: self.label_2.set_text(str(round(v, 5)))) + self.slider_2 = customtkinter.CTkSlider(master=self.ctk_frame, command=lambda v: self.label_2.configure(text=str(round(v, 5)))) self.slider_2.place(relx=0.5, y=y + 400, anchor=tkinter.CENTER) self.check_box_2 = customtkinter.CTkCheckBox(master=self.ctk_frame) @@ -102,7 +101,7 @@ class TestApp(customtkinter.CTk): self.ctk_frame_customized.configure(fg_color=("#F4F4FA", "#1E2742")) self.label_3 = customtkinter.CTkLabel(master=self.ctk_frame_customized, text="customized", corner_radius=60, - text_font=("times", 16)) + font=("times", 16)) self.label_3.place(relx=0.5, y=y, anchor=tkinter.CENTER) self.label_3.configure(fg_color=("#F4F4FA", "#333D5E"), text_color=("#373E57", "#7992C1")) @@ -110,13 +109,13 @@ class TestApp(customtkinter.CTk): self.frame_3.place(relx=0.5, y=y + 80, anchor=tkinter.CENTER) self.frame_3.configure(fg_color=("#EBECF3", "#4B577E")) - self.button_3 = customtkinter.CTkButton(master=self.ctk_frame_customized, command=lambda: x, border_width=3, - corner_radius=20, text_font=("times", 16)) + self.button_3 = customtkinter.CTkButton(master=self.ctk_frame_customized, command=lambda: None, border_width=3, + corner_radius=20, font=("times", 16)) self.button_3.place(relx=0.5, y=y + 160, anchor=tkinter.CENTER) self.button_3.configure(border_color=("#4F90F8", "#6FADF9"), hover_color=("#3A65E8", "#4376EE")) - self.button_3.configure(fg_color=None) + self.button_3.configure(fg_color="transparent") - self.entry_3 = customtkinter.CTkEntry(master=self.ctk_frame_customized, text_font=("times", 16)) + self.entry_3 = customtkinter.CTkEntry(master=self.ctk_frame_customized, font=("times", 16)) self.entry_3.place(relx=0.5, y=y + 240, anchor=tkinter.CENTER) self.entry_3.configure(fg_color=("gray60", "gray5"), corner_radius=20) self.entry_3.insert(0, "1234567890") @@ -131,7 +130,7 @@ class TestApp(customtkinter.CTk): self.slider_3.configure(button_color="#8AE0C3", fg_color=("#EBECF3", "#4B577E"), progress_color=("gray30", "gray10")) self.slider_3.configure(from_=0, to=1) - self.check_box_3 = customtkinter.CTkCheckBox(master=self.ctk_frame_customized, corner_radius=50, text_font=("times", 16)) + self.check_box_3 = customtkinter.CTkCheckBox(master=self.ctk_frame_customized, corner_radius=50, font=("times", 16)) self.check_box_3.place(relx=0.5, y=y + 480, anchor=tkinter.CENTER) self.check_box_3.configure(border_color="#8AE0C3") @@ -152,7 +151,7 @@ class TestApp(customtkinter.CTk): self.button_4 = customtkinter.CTkButton(master=self.tk_frame_customized, command=lambda: x, border_width=3) self.button_4.place(relx=0.5, y=y + 160, anchor=tkinter.CENTER) self.button_4.configure(border_color=("#4F90F8", "#6FADF9"), hover_color=("#3A65E8", "#4376EE")) - self.button_4.configure(fg_color=None) + self.button_4.configure(fg_color="transparent") self.entry_4 = customtkinter.CTkEntry(master=self.tk_frame_customized) self.entry_4.place(relx=0.5, y=y + 240, anchor=tkinter.CENTER) @@ -176,4 +175,4 @@ class TestApp(customtkinter.CTk): if __name__ == "__main__": test_app = TestApp() - test_app.mainloop() \ No newline at end of file + test_app.mainloop() diff --git a/test/manual_integration_tests/test_askdialog.py b/test/manual_integration_tests/test_askdialog.py deleted file mode 100644 index 5ab7924..0000000 --- a/test/manual_integration_tests/test_askdialog.py +++ /dev/null @@ -1,65 +0,0 @@ -import tkinter -import tkinter.messagebox -from tkinter import filedialog as fd -import customtkinter - - -class App(customtkinter.CTk): - - customtkinter.set_appearance_mode("dark") - APP_NAME = "Bulk Barcode Generator" - WIDTH = 600 - HEIGHT = 450 - - MAIN_COLOR = "#5ea886" - MAIN_COLOR_DARK = "#2D5862" - MAIN_HOVER = "#05f4b7" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.title(App.APP_NAME) - self.geometry(str(App.WIDTH) + "x" + str(App.HEIGHT)) - self.minsize(App.WIDTH, App.HEIGHT) - - self.protocol("WM_DELETE_WINDOW", self.on_closing) - # ============ create two CTkFrames ============ - - self.frame_left = customtkinter.CTkFrame(master=self, - width=220, - height=App.HEIGHT-40, - corner_radius=5) - self.frame_left.place(relx=0.38, rely=0.5, anchor=tkinter.E) - - self.frame_right = customtkinter.CTkFrame(master=self, - width=350, - height=App.HEIGHT-40, - corner_radius=5) - self.frame_right.place(relx=0.40, rely=0.5, anchor=tkinter.W) - -# # ============ frame_right ============ - - self.button_output = customtkinter.CTkButton(master=self.frame_right, border_color=App.MAIN_COLOR, - fg_color=None, hover_color=App.MAIN_HOVER, - height=28, text="Output Folder", command=self.button_outputFunc, - border_width=3, corner_radius=10, text_font=('Calibri',12)) - self.button_output.place(relx=0.05, rely=0.06, anchor=tkinter.NW) - self.entry_output = customtkinter.CTkEntry(master=self.frame_right, width=320, height=38, corner_radius=5) - self.entry_output.place(relx=0.05, rely=0.18, anchor=tkinter.NW) - - def button_outputFunc(self): - self.entry_output.delete(0, 'end') - filename = fd.askdirectory() - self.entry_output.insert(0,str(filename)) - pass - - def on_closing(self, event=0): - self.destroy() - - def start(self): - self.mainloop() - - -if __name__ == "__main__": - app = App() - app.start() diff --git a/test/manual_integration_tests/test_ctk_toplevel.py b/test/manual_integration_tests/test_ctk_toplevel.py index 88e8f1c..32bd490 100644 --- a/test/manual_integration_tests/test_ctk_toplevel.py +++ b/test/manual_integration_tests/test_ctk_toplevel.py @@ -8,6 +8,7 @@ class ToplevelWindow(customtkinter.CTkToplevel): super().__init__(*args, **kwargs) self.protocol("WM_DELETE_WINDOW", self.closing) self.geometry("500x300") + self.resizable(False, False) self.closing_event = closing_event self.label = customtkinter.CTkLabel(self, text="ToplevelWindow") @@ -26,6 +27,7 @@ class App(customtkinter.CTk): def __init__(self): super().__init__() self.geometry("500x400") + self.resizable(False, False) self.button_1 = customtkinter.CTkButton(self, text="Open CTkToplevel", command=self.open_toplevel) self.button_1.pack(side="top", padx=40, pady=40) diff --git a/test/manual_integration_tests/test_filedialog.py b/test/manual_integration_tests/test_filedialog.py new file mode 100644 index 0000000..4615da8 --- /dev/null +++ b/test/manual_integration_tests/test_filedialog.py @@ -0,0 +1,31 @@ +import tkinter.messagebox +import customtkinter + +customtkinter.set_appearance_mode("dark") + + +class App(customtkinter.CTk): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.title("test filedialog") + + self.button_1 = customtkinter.CTkButton(master=self, text="askopenfile", command=lambda: print(customtkinter.filedialog.askopenfile())) + self.button_1.pack(pady=10) + self.button_2 = customtkinter.CTkButton(master=self, text="askopenfiles", command=lambda: print(customtkinter.filedialog.askopenfiles())) + self.button_2.pack(pady=10) + self.button_3 = customtkinter.CTkButton(master=self, text="askdirectory", command=lambda: print(customtkinter.filedialog.askdirectory())) + self.button_3.pack(pady=10) + self.button_4 = customtkinter.CTkButton(master=self, text="asksaveasfile", command=lambda: print(customtkinter.filedialog.asksaveasfile())) + self.button_4.pack(pady=10) + self.button_5 = customtkinter.CTkButton(master=self, text="askopenfilename", command=lambda: print(customtkinter.filedialog.askopenfilename())) + self.button_5.pack(pady=10) + self.button_6 = customtkinter.CTkButton(master=self, text="askopenfilenames", command=lambda: print(customtkinter.filedialog.askopenfilenames())) + self.button_6.pack(pady=10) + self.button_7 = customtkinter.CTkButton(master=self, text="asksaveasfilename", command=lambda: print(customtkinter.filedialog.asksaveasfilename())) + self.button_7.pack(pady=10) + + +if __name__ == "__main__": + app = App() + app.mainloop() diff --git a/test/manual_integration_tests/test_font.py b/test/manual_integration_tests/test_font.py new file mode 100644 index 0000000..6dc4fba --- /dev/null +++ b/test/manual_integration_tests/test_font.py @@ -0,0 +1,79 @@ +import customtkinter + + +app = customtkinter.CTk() +app.geometry("1200x1000") +app.grid_rowconfigure(0, weight=1) +app.grid_columnconfigure((0, 1), weight=1) + +frame_1 = customtkinter.CTkFrame(app) +frame_1.grid(row=0, column=0, sticky="nsew", padx=10, pady=10) +frame_2 = customtkinter.CTkFrame(app) +frame_2.grid(row=0, column=1, sticky="nsew", padx=10, pady=10) + +def set_scaling(scaling): + customtkinter.set_widget_scaling(scaling) + +scaling_button = customtkinter.CTkSegmentedButton(frame_1, values=[0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.5, 2.0], command=set_scaling) +scaling_button.pack(pady=(2, 10)) + +b = customtkinter.CTkButton(frame_1, text="single name", font=("Times", )) +b.pack(pady=2) +b = customtkinter.CTkButton(frame_1, text="name with size", font=("Times", 18)) +b.pack(pady=2) +b = customtkinter.CTkButton(frame_1, text="name with negative size", font=("Times", -18)) +b.pack(pady=2) +b = customtkinter.CTkButton(frame_1, text="extra keywords", font=("Times", -18, "bold italic underline overstrike")) +b.pack(pady=2) + +b = customtkinter.CTkButton(frame_1, text="object default") +b.pack(pady=(10, 2)) +b = customtkinter.CTkButton(frame_1, text="object single name", font=customtkinter.CTkFont("Times")) +b.pack(pady=2) +b = customtkinter.CTkButton(frame_1, text="object with name and size", font=customtkinter.CTkFont("Times", 18)) +b.pack(pady=2) +b = customtkinter.CTkButton(frame_1, text="object with name and negative size", font=customtkinter.CTkFont("Times", -18)) +b.pack(pady=2) +b = customtkinter.CTkButton(frame_1, text="object with extra keywords", + font=customtkinter.CTkFont("Times", -18, weight="bold", slant="italic", underline=True, overstrike=True)) +b.pack(pady=2) + +b1 = customtkinter.CTkButton(frame_1, text="object default modified") +b1.pack(pady=(10, 2)) +b1.cget("font").configure(size=9) +print("test_font.py:", b1.cget("font").cget("size"), b1.cget("font").cget("family")) + +b2 = customtkinter.CTkButton(frame_1, text="object default overridden") +b2.pack(pady=10) +b2.configure(font=customtkinter.CTkFont(family="Times")) + +label_font = customtkinter.CTkFont(size=5) +for i in range(30): + l = customtkinter.CTkLabel(frame_2, font=label_font, height=0) + l.grid(row=i, column=0, pady=1) + b = customtkinter.CTkButton(frame_2, font=label_font, height=5) + b.grid(row=i, column=1, pady=1) + c = customtkinter.CTkCheckBox(frame_2, font=label_font) + c.grid(row=i, column=2, pady=1) + c = customtkinter.CTkComboBox(frame_2, font=label_font, dropdown_font=label_font, height=15) + c.grid(row=i, column=3, pady=1) + e = customtkinter.CTkEntry(frame_2, font=label_font, height=15, placeholder_text="testtest") + e.grid(row=i, column=4, pady=1) + o = customtkinter.CTkOptionMenu(frame_2, font=label_font, height=15, width=50) + o.grid(row=i, column=5, pady=1) + r = customtkinter.CTkRadioButton(frame_2, font=label_font, height=15, width=50) + r.grid(row=i, column=6, pady=1) + s = customtkinter.CTkSwitch(frame_2, font=label_font, height=15, width=50) + s.grid(row=i, column=7, pady=1) +frame_2.grid_columnconfigure((0, 1, 2, 3, 4), weight=1) + +def change_font(): + import time + t1 = time.perf_counter() + label_font.configure(size=10, overstrike=True) + t2 = time.perf_counter() + print("change_font:", (t2-t1)*1000, "ms") + +app.after(3000, change_font) +app.after(6000, lambda: label_font.configure(size=8, overstrike=False)) +app.mainloop() diff --git a/test/manual_integration_tests/test_images.py b/test/manual_integration_tests/test_images.py new file mode 100644 index 0000000..e1f9545 --- /dev/null +++ b/test/manual_integration_tests/test_images.py @@ -0,0 +1,43 @@ +import customtkinter +from PIL import Image, ImageTk +import os + +# load images +file_path = os.path.dirname(os.path.realpath(__file__)) +image_1 = customtkinter.CTkImage(light_image=Image.open(file_path + "/test_images/add_folder_dark.png"), + dark_image=Image.open(file_path + "/test_images/add_folder_light.png"), + size=(30, 30)) +image_1.configure(dark_image=Image.open(file_path + "/test_images/add_folder_light.png")) +image_2 = customtkinter.CTkImage(light_image=Image.open(file_path + "/test_images/bg_gradient.jpg"), + size=(30, 50)) + +app = customtkinter.CTk() +app.geometry("500x900") + +mode_switch = customtkinter.CTkSwitch(app, text="darkmode", + command=lambda: customtkinter.set_appearance_mode("dark" if mode_switch.get() == 1 else "light")) +mode_switch.pack(padx=20, pady=20) + +scaling_button = customtkinter.CTkSegmentedButton(app, values=[0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0], + command=lambda v: customtkinter.set_widget_scaling(v)) +scaling_button.pack(padx=20, pady=20) + +button_1 = customtkinter.CTkButton(app, image=image_1) +button_1.pack(padx=20, pady=20) + +button_1 = customtkinter.CTkButton(app, image=image_1, anchor="nw", compound="right", height=50, corner_radius=4) +button_1.pack(padx=20, pady=20) + +label_1 = customtkinter.CTkLabel(app, text="", image=image_2, compound="right", fg_color="green", width=0) +label_1.pack(padx=20, pady=20) +label_1.configure(image=image_1) + +label_2 = customtkinter.CTkLabel(app, text="text", image=image_2, compound="right", fg_color="red", width=0, corner_radius=10) +label_2.pack(padx=20, pady=20) + +label_3 = customtkinter.CTkLabel(app, image=ImageTk.PhotoImage(Image.open(file_path + "/test_images/bg_gradient.jpg").resize((300, 100))), + text="", compound="right", fg_color="green", width=0) +label_3.pack(padx=20, pady=20) + +app.mainloop() + diff --git a/test/manual_integration_tests/test_images/add_folder_dark.png b/test/manual_integration_tests/test_images/add_folder_dark.png new file mode 100644 index 0000000..dcf4f2d Binary files /dev/null and b/test/manual_integration_tests/test_images/add_folder_dark.png differ diff --git a/examples/test_images/add-folder.png b/test/manual_integration_tests/test_images/add_folder_light.png similarity index 100% rename from examples/test_images/add-folder.png rename to test/manual_integration_tests/test_images/add_folder_light.png diff --git a/test/manual_integration_tests/test_optionmenu_combobox.py b/test/manual_integration_tests/test_optionmenu_combobox.py index 3c75360..351743e 100644 --- a/test/manual_integration_tests/test_optionmenu_combobox.py +++ b/test/manual_integration_tests/test_optionmenu_combobox.py @@ -34,7 +34,6 @@ combobox_1 = customtkinter.CTkComboBox(app, variable=variable, values=countries, combobox_1.pack(pady=20, padx=10) def set_new_scaling(scaling): - customtkinter.set_spacing_scaling(scaling) customtkinter.set_window_scaling(scaling) customtkinter.set_widget_scaling(scaling) diff --git a/test/manual_integration_tests/test_scaling/test_scaling_simple_place.py b/test/manual_integration_tests/test_scaling/test_scaling_simple_place.py index bf7f7ba..90dcbff 100644 --- a/test/manual_integration_tests/test_scaling/test_scaling_simple_place.py +++ b/test/manual_integration_tests/test_scaling/test_scaling_simple_place.py @@ -22,7 +22,6 @@ def button_function(): def slider_function(value): customtkinter.set_widget_scaling(value * 2) - customtkinter.set_spacing_scaling(value * 2) customtkinter.set_window_scaling(value * 2) progressbar_1.set(value) diff --git a/test/manual_integration_tests/test_scaling/test_scaling_toplevel_pack.py b/test/manual_integration_tests/test_scaling/test_scaling_toplevel_pack.py index 8f08484..3c0d8f7 100644 --- a/test/manual_integration_tests/test_scaling/test_scaling_toplevel_pack.py +++ b/test/manual_integration_tests/test_scaling/test_scaling_toplevel_pack.py @@ -20,12 +20,11 @@ top_tk.geometry("500x500") def button_function(): app.geometry(f"{200}x{200}") - print("Button click", label_1.text_label.cget("text")) + print("Button click", label_1.cget("text")) def slider_function(value): customtkinter.set_widget_scaling(value * 2) - customtkinter.set_spacing_scaling(value * 2) customtkinter.set_window_scaling(value * 2) progressbar_1.set(value) diff --git a/test/manual_integration_tests/test_scrollbar.py b/test/manual_integration_tests/test_scrollbar.py deleted file mode 100644 index d579911..0000000 --- a/test/manual_integration_tests/test_scrollbar.py +++ /dev/null @@ -1,54 +0,0 @@ -import tkinter -import customtkinter - -# test with scaling -# customtkinter.set_widget_scaling(2) -# customtkinter.set_window_scaling(2) -# customtkinter.set_spacing_scaling(2) - -customtkinter.set_appearance_mode("dark") - -app = customtkinter.CTk() -app.title("test_scrollbar.py") -app.grid_rowconfigure(0, weight=1) -app.grid_columnconfigure((0, 2), weight=1) - -tk_textbox = tkinter.Text(app, highlightthickness=0, padx=5, pady=5) -tk_textbox.grid(row=0, column=0, sticky="nsew") -ctk_textbox_scrollbar = customtkinter.CTkScrollbar(app, command=tk_textbox.yview) -ctk_textbox_scrollbar.grid(row=0, column=1, padx=0, sticky="ns") -tk_textbox.configure(yscrollcommand=ctk_textbox_scrollbar.set) - -frame_1 = customtkinter.CTkFrame(app) -frame_1.grid(row=0, column=2, padx=10, pady=10, sticky="nsew") -frame_1.grid_rowconfigure((0, 1), weight=1) -frame_1.grid_columnconfigure((0, ), weight=1) -tk_textbox_1 = tkinter.Text(frame_1, highlightthickness=0, padx=5, pady=5) -tk_textbox_1.grid(row=0, column=0, sticky="nsew", padx=(5, 0), pady=5) -ctk_textbox_scrollbar_1 = customtkinter.CTkScrollbar(frame_1, command=tk_textbox_1.yview) -ctk_textbox_scrollbar_1.grid(row=0, column=1, sticky="ns", padx=(0, 5), pady=5) -tk_textbox_1.configure(yscrollcommand=ctk_textbox_scrollbar_1.set) -ctk_textbox_scrollbar_1.configure(scrollbar_color="red", scrollbar_hover_color="darkred", - border_spacing=0, width=12, fg_color="green", corner_radius=4) - -frame_2 = customtkinter.CTkFrame(frame_1) -frame_2.grid(row=1, column=0, columnspan=2, padx=20, pady=20, sticky="nsew") -frame_2.grid_rowconfigure((0, ), weight=1) -frame_2.grid_columnconfigure((0, ), weight=1) -tk_textbox_2 = tkinter.Text(frame_2, highlightthickness=0, padx=5, pady=5, wrap="none") -tk_textbox_2.grid(row=0, column=0, sticky="nsew", padx=(5, 0), pady=5) -ctk_textbox_scrollbar_2 = customtkinter.CTkScrollbar(frame_2, command=tk_textbox_2.yview) -ctk_textbox_scrollbar_2.grid(row=0, column=1, sticky="ns", padx=(0, 5), pady=5) -ctk_textbox_scrollbar_2_horizontal = customtkinter.CTkScrollbar(frame_2, command=tk_textbox_2.xview, orientation="horizontal") -ctk_textbox_scrollbar_2_horizontal.grid(row=1, column=0, sticky="ew", padx=(5, 0), pady=(0, 5)) -tk_textbox_2.configure(yscrollcommand=ctk_textbox_scrollbar_2.set, xscrollcommand=ctk_textbox_scrollbar_2_horizontal.set) - -tk_textbox.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"])) -tk_textbox_1.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"])) -tk_textbox_2.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"])) - -tk_textbox.insert("insert", "\n".join([str(i) for i in range(100)])) -tk_textbox_1.insert("insert", "\n".join([str(i) for i in range(1000)])) -tk_textbox_2.insert("insert", "\n".join([str(i) + " - "*30 for i in range(10000)])) - -app.mainloop() diff --git a/test/manual_integration_tests/test_segmented_button.py b/test/manual_integration_tests/test_segmented_button.py new file mode 100644 index 0000000..79ef7ce --- /dev/null +++ b/test/manual_integration_tests/test_segmented_button.py @@ -0,0 +1,81 @@ +import customtkinter + + +app = customtkinter.CTk() +app.geometry("600x950") + +switch_1 = customtkinter.CTkSwitch(app, text="darkmode", command=lambda: customtkinter.set_appearance_mode("dark" if switch_1.get() == 1 else "light")) +switch_1.pack(padx=20, pady=20) + +seg_1 = customtkinter.CTkSegmentedButton(app, values=[]) +seg_1.configure(values=["value 1", "Value 2", "Value 42", "Value 123", "longlonglong"]) +seg_1.pack(padx=20, pady=20) + +frame_1 = customtkinter.CTkFrame(app, height=100) +frame_1.pack(padx=20, pady=20, fill="x") + +seg_2_var = customtkinter.StringVar(value="value 1") + +seg_2 = customtkinter.CTkSegmentedButton(frame_1, values=["value 1", "Value 2", "Value 42"], variable=seg_2_var) +seg_2.configure(values=[]) +seg_2.configure(values=["value 1", "Value 2", "Value 42"]) +seg_2.pack(padx=20, pady=10) +seg_2.insert(0, "insert at 0") +seg_2.insert(1, "insert at 1") + +label_seg_2 = customtkinter.CTkLabel(frame_1, textvariable=seg_2_var) +label_seg_2.pack(padx=20, pady=10) + +frame_1_1 = customtkinter.CTkFrame(frame_1, height=100) +frame_1_1.pack(padx=20, pady=10, fill="x") + +switch_2 = customtkinter.CTkSwitch(frame_1_1, text="change fg", command=lambda: frame_1_1.configure(fg_color="red" if switch_2.get() == 1 else "green")) +switch_2.pack(padx=20, pady=20) + +seg_3 = customtkinter.CTkSegmentedButton(frame_1_1, values=["value 1", "Value 2", "Value 42"]) +seg_3.pack(padx=20, pady=10) + +seg_4 = customtkinter.CTkSegmentedButton(app) +seg_4.pack(padx=20, pady=20) + +seg_5_var = customtkinter.StringVar(value="kfasjkfdklaj") +seg_5 = customtkinter.CTkSegmentedButton(app, corner_radius=1000, border_width=0, unselected_color="green", + variable=seg_5_var) +seg_5.pack(padx=20, pady=20) +seg_5.configure(values=["1", "2", "3", "4"]) +seg_5.insert(0, "insert begin") +seg_5.insert(len(seg_5.cget("values")), "insert 1") +seg_5.insert(len(seg_5.cget("values")), "insert 2") +seg_5.insert(len(seg_5.cget("values")), "insert 3") +seg_5.configure(fg_color="green") + +seg_5.set("insert 2") +seg_5.delete("insert 2") + +label_seg_5 = customtkinter.CTkLabel(app, textvariable=seg_5_var) +label_seg_5.pack(padx=20, pady=20) + +seg_6_var = customtkinter.StringVar(value="kfasjkfdklaj") +seg_6 = customtkinter.CTkSegmentedButton(app, width=300) +seg_6.pack(padx=20, pady=20) +entry_6 = customtkinter.CTkEntry(app) +entry_6.pack(padx=20, pady=(0, 20)) +button_6 = customtkinter.CTkButton(app, text="set", command=lambda: seg_6.set(entry_6.get())) +button_6.pack(padx=20, pady=(0, 20)) +button_6 = customtkinter.CTkButton(app, text="insert value", command=lambda: seg_6.insert(0, entry_6.get())) +button_6.pack(padx=20, pady=(0, 20)) +label_6 = customtkinter.CTkLabel(app, textvariable=seg_6_var) +label_6.pack(padx=20, pady=(0, 20)) + +seg_6.configure(height=50, variable=seg_6_var) +seg_6.delete("CTkSegmentedButton") + +seg_7 = customtkinter.CTkSegmentedButton(app, values=["disabled seg button", "2", "3"]) +seg_7.pack(padx=20, pady=20) +seg_7.configure(state="disabled") +seg_7.set("2") + +seg_7.configure(height=40, width=400, + dynamic_resizing=False, font=("Times", -20)) + +app.mainloop() diff --git a/test/manual_integration_tests/test_states.py b/test/manual_integration_tests/test_states.py index 188aba8..e7c5daf 100644 --- a/test/manual_integration_tests/test_states.py +++ b/test/manual_integration_tests/test_states.py @@ -8,9 +8,9 @@ app.title("CustomTkinter Test") def change_state(widget): - if widget.state == tkinter.NORMAL: + if widget.cget("state") == tkinter.NORMAL: widget.configure(state=tkinter.DISABLED) - elif widget.state == tkinter.DISABLED: + elif widget.cget("state") == tkinter.DISABLED: widget.configure(state=tkinter.NORMAL) diff --git a/test/manual_integration_tests/test_string_dialog.py b/test/manual_integration_tests/test_string_dialog.py index 76b2526..8d33f48 100644 --- a/test/manual_integration_tests/test_string_dialog.py +++ b/test/manual_integration_tests/test_string_dialog.py @@ -2,6 +2,8 @@ import customtkinter customtkinter.set_appearance_mode("dark") customtkinter.set_default_color_theme("blue") +customtkinter.set_window_scaling(0.8) +customtkinter.set_widget_scaling(0.8) app = customtkinter.CTk() app.geometry("400x300") @@ -15,14 +17,21 @@ def change_mode(): customtkinter.set_appearance_mode("dark") -def button_click_event(): - dialog = customtkinter.CTkInputDialog(master=None, text="Type in a number:", title="Test") +def button_1_click_event(): + dialog = customtkinter.CTkInputDialog(text="Type in a number:", title="Test") print("Number:", dialog.get_input()) -button = customtkinter.CTkButton(app, text="Open Dialog", command=button_click_event) -button.place(relx=0.5, rely=0.5, anchor=customtkinter.CENTER) +def button_2_click_event(): + dialog = customtkinter.CTkInputDialog(text="long text "*100, title="Test") + print("Number:", dialog.get_input()) + + +button_1 = customtkinter.CTkButton(app, text="Open Dialog", command=button_1_click_event) +button_1.pack(pady=20) +button_2 = customtkinter.CTkButton(app, text="Open Dialog", command=button_2_click_event) +button_2.pack(pady=20) c1 = customtkinter.CTkCheckBox(app, text="dark mode", command=change_mode) -c1.place(relx=0.5, rely=0.8, anchor=customtkinter.CENTER) +c1.pack(pady=20) app.mainloop() diff --git a/test/manual_integration_tests/test_tabview.py b/test/manual_integration_tests/test_tabview.py new file mode 100644 index 0000000..73f7651 --- /dev/null +++ b/test/manual_integration_tests/test_tabview.py @@ -0,0 +1,34 @@ +import customtkinter + +app = customtkinter.CTk() +app.geometry("800x900") + +tabview_1 = customtkinter.CTkTabview(app) +tabview_1.pack(padx=20, pady=20) + +tab_1 = tabview_1.add("tab 1") +tabview_1.insert(0, "tab 2") + +tabview_1.add("tab 42") +tabview_1.set("tab 42") +tabview_1.delete("tab 42") +tabview_1.insert(0, "tab 42") +tabview_1.delete("tab 42") +tabview_1.insert(1, "tab 42") +tabview_1.delete("tab 42") + +tabview_1.move(0, "tab 2") + +b2 = customtkinter.CTkButton(master=tabview_1.tab("tab 2"), text="button tab 2") +b2.pack() + +# tabview_1.tab("tab 2").configure(fg_color="red") +tabview_1.configure(state="normal") +# tabview_1.delete("tab 1") + +for i in range(10): + for j in range(30): + button = customtkinter.CTkButton(tabview_1.tab("tab 1"), height=10, width=30, font=customtkinter.CTkFont(size=8)) + button.grid(row=j, column=i, padx=2, pady=2) + +app.mainloop() diff --git a/test/manual_integration_tests/test_textbox.py b/test/manual_integration_tests/test_textbox.py index f9a5bd0..0a874bc 100644 --- a/test/manual_integration_tests/test_textbox.py +++ b/test/manual_integration_tests/test_textbox.py @@ -1,54 +1,102 @@ -import tkinter import customtkinter -# test with scaling -# customtkinter.set_widget_scaling(2) -# customtkinter.set_window_scaling(2) -# customtkinter.set_spacing_scaling(2) +#customtkinter.set_widget_scaling(0.9) +#customtkinter.set_window_scaling(0.9) customtkinter.set_appearance_mode("dark") app = customtkinter.CTk() app.title("test_scrollbar.py") +app.geometry("800x1200") app.grid_rowconfigure(0, weight=1) -app.grid_columnconfigure((0, 2), weight=1) +app.grid_columnconfigure((0, 1, 2, 3, 4), weight=1) -tk_textbox = customtkinter.CTkTextbox(app, highlightthickness=0, padx=5, pady=5) -tk_textbox.grid(row=0, column=0, sticky="nsew") -ctk_textbox_scrollbar = customtkinter.CTkScrollbar(app, command=tk_textbox.yview) -ctk_textbox_scrollbar.grid(row=0, column=1, padx=0, sticky="ns") -tk_textbox.configure(yscrollcommand=ctk_textbox_scrollbar.set) +textbox_1 = customtkinter.CTkTextbox(app, fg_color=None, corner_radius=0, border_spacing=0) +textbox_1.grid(row=0, column=0, sticky="nsew") +textbox_1.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) -frame_1 = customtkinter.CTkFrame(app) -frame_1.grid(row=0, column=2, padx=10, pady=10, sticky="nsew") -frame_1.grid_rowconfigure((0, 1), weight=1) -frame_1.grid_columnconfigure((0, ), weight=1) -tk_textbox_1 = customtkinter.CTkTextbox(frame_1, highlightthickness=0, padx=5, pady=5) -tk_textbox_1.grid(row=0, column=0, sticky="nsew", padx=(5, 0), pady=5) -ctk_textbox_scrollbar_1 = customtkinter.CTkScrollbar(frame_1, command=tk_textbox_1.yview) -ctk_textbox_scrollbar_1.grid(row=0, column=1, sticky="ns", padx=(0, 5), pady=5) -tk_textbox_1.configure(yscrollcommand=ctk_textbox_scrollbar_1.set) -ctk_textbox_scrollbar_1.configure(scrollbar_color="red", scrollbar_hover_color="darkred", - border_spacing=0, width=12, fg_color="green", corner_radius=4) +frame_1 = customtkinter.CTkFrame(app, corner_radius=0) +frame_1.grid(row=0, column=1, sticky="nsew") +frame_1.grid_rowconfigure((0, 1, 2, 3, 4), weight=1) +frame_1.grid_columnconfigure(0, weight=1) -frame_2 = customtkinter.CTkFrame(frame_1) -frame_2.grid(row=1, column=0, columnspan=2, padx=20, pady=20, sticky="nsew") -frame_2.grid_rowconfigure((0, ), weight=1) -frame_2.grid_columnconfigure((0, ), weight=1) -tk_textbox_2 = customtkinter.CTkTextbox(frame_2, highlightthickness=0, padx=5, pady=5, wrap="none") -tk_textbox_2.grid(row=0, column=0, sticky="nsew", padx=(5, 0), pady=5) -ctk_textbox_scrollbar_2 = customtkinter.CTkScrollbar(frame_2, command=tk_textbox_2.yview) -ctk_textbox_scrollbar_2.grid(row=0, column=1, sticky="ns", padx=(0, 5), pady=5) -ctk_textbox_scrollbar_2_horizontal = customtkinter.CTkScrollbar(frame_2, command=tk_textbox_2.xview, orientation="horizontal") -ctk_textbox_scrollbar_2_horizontal.grid(row=1, column=0, sticky="ew", padx=(5, 0), pady=(0, 5)) -tk_textbox_2.configure(yscrollcommand=ctk_textbox_scrollbar_2.set, xscrollcommand=ctk_textbox_scrollbar_2_horizontal.set) +textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none") +textbox_2.grid(row=0, column=0, sticky="nsew", padx=20, pady=20) +textbox_2.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) -tk_textbox.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"])) -tk_textbox_1.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"])) -tk_textbox_2.configure(font=(customtkinter.ThemeManager.theme["text"]["font"], customtkinter.ThemeManager.theme["text"]["size"])) +textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none", corner_radius=30) +textbox_2.grid(row=1, column=0, sticky="nsew", padx=20, pady=20) +textbox_2.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) -tk_textbox.insert("insert", "\n".join([str(i) for i in range(100)])) -tk_textbox_1.insert("insert", "\n".join([str(i) for i in range(1000)])) -tk_textbox_2.insert("insert", "\n".join([str(i) + " - "*30 for i in range(10000)])) +textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none", corner_radius=0, border_width=30) +textbox_2.grid(row=2, column=0, sticky="nsew", padx=20, pady=20) +textbox_2.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) +textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none", corner_radius=60, border_width=15) + #fg_color="blue", scrollbar_color="yellow", text_color="red") +textbox_2.grid(row=3, column=0, sticky="nsew", padx=20, pady=20) +textbox_2.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_2 = customtkinter.CTkTextbox(frame_1, wrap="none", corner_radius=0, border_width=0) +textbox_2.grid(row=4, column=0, sticky="nsew", padx=20, pady=20) +textbox_2.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +frame_2 = customtkinter.CTkFrame(app, corner_radius=0, fg_color=None) +frame_2.grid(row=0, column=2, sticky="nsew") +frame_2.grid_rowconfigure((0, 1, 2, 3, 4), weight=1) +frame_2.grid_columnconfigure(0, weight=1) + +textbox_3 = customtkinter.CTkTextbox(frame_2) +textbox_3.grid(row=0, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_3 = customtkinter.CTkTextbox(frame_2, corner_radius=30) +textbox_3.grid(row=1, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_3 = customtkinter.CTkTextbox(frame_2, corner_radius=0, border_width=30) +textbox_3.grid(row=2, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_3 = customtkinter.CTkTextbox(frame_2, corner_radius=60, border_width=15) +textbox_3.grid(row=3, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_3 = customtkinter.CTkTextbox(frame_2, corner_radius=0, border_width=0, border_spacing=20) +textbox_3.grid(row=4, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +frame_3 = customtkinter.CTkFrame(app, corner_radius=0, fg_color=None) +frame_3.grid(row=0, column=3, sticky="nsew") +frame_3.grid_rowconfigure((0, 1, 2, 3, 4), weight=1) +frame_3.grid_columnconfigure(0, weight=1) + +textbox_3 = customtkinter.CTkTextbox(frame_3, activate_scrollbars=False) +textbox_3.grid(row=0, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_3 = customtkinter.CTkTextbox(frame_3, corner_radius=10, border_width=2, activate_scrollbars=False) +textbox_3.grid(row=1, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_3 = customtkinter.CTkTextbox(frame_3, corner_radius=0, border_width=2, activate_scrollbars=False) +textbox_3.grid(row=2, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_3 = customtkinter.CTkTextbox(frame_3, corner_radius=0, border_width=2, activate_scrollbars=False) +textbox_3.grid(row=3, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_3 = customtkinter.CTkTextbox(frame_3, corner_radius=0, border_width=0, activate_scrollbars=False, border_spacing=10) +textbox_3.grid(row=4, column=0, sticky="nsew", padx=20, pady=20) +textbox_3.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) + +textbox_4 = customtkinter.CTkTextbox(app, fg_color=None, corner_radius=0) +textbox_4.grid(row=0, column=4, sticky="nsew") +textbox_4.insert("0.0", "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.\n\n" * 20) +scrollbar4 = customtkinter.CTkScrollbar(app, command=textbox_4.yview) +scrollbar4.grid(row=0, column=5, sticky="nsew") +textbox_4.configure(yscrollcommand=scrollbar4.set) + +# app.after(3000, lambda: customtkinter.set_appearance_mode("light")) app.mainloop() diff --git a/test/manual_integration_tests/test_theme_colors.py b/test/manual_integration_tests/test_theme_colors.py new file mode 100644 index 0000000..e1fe20c --- /dev/null +++ b/test/manual_integration_tests/test_theme_colors.py @@ -0,0 +1,85 @@ +import tkinter +import customtkinter + +customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light" +customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" + +app = customtkinter.CTk() +app.geometry("1100x900") +app.title("CustomTkinter simple_example.py") + + +def create_all_widgets(master, state="normal"): + label_1 = customtkinter.CTkLabel(master=master, justify=tkinter.LEFT) + label_1.pack(pady=10, padx=10) + + progressbar_1 = customtkinter.CTkProgressBar(master=master) + progressbar_1.pack(pady=10, padx=10) + + button_1 = customtkinter.CTkButton(master=master, state=state, border_width=0) + button_1.pack(pady=10, padx=10) + + slider_1 = customtkinter.CTkSlider(master=master, from_=0, to=1, state=state) + slider_1.pack(pady=10, padx=10) + slider_1.set(0.5) + + entry_1 = customtkinter.CTkEntry(master=master, placeholder_text="CTkEntry", state=state) + entry_1.pack(pady=10, padx=10) + + optionmenu_1 = customtkinter.CTkOptionMenu(master, values=["Option 1", "Option 2", "Option 42 long long long..."], state=state) + optionmenu_1.pack(pady=10, padx=10) + optionmenu_1.set("CTkOptionMenu") + + combobox_1 = customtkinter.CTkComboBox(master, values=["Option 1", "Option 2", "Option 42 long long long..."], state=state) + combobox_1.pack(pady=10, padx=10) + optionmenu_1.set("CTkComboBox") + + checkbox_1 = customtkinter.CTkCheckBox(master=master, state=state) + checkbox_1.pack(pady=10, padx=10) + + radiobutton_var = tkinter.IntVar(value=1) + + radiobutton_1 = customtkinter.CTkRadioButton(master=master, variable=radiobutton_var, value=1, state=state) + radiobutton_1.pack(pady=10, padx=10) + + radiobutton_2 = customtkinter.CTkRadioButton(master=master, variable=radiobutton_var, value=2, state=state) + radiobutton_2.pack(pady=10, padx=10) + + switch_1 = customtkinter.CTkSwitch(master=master, state=state) + switch_1.pack(pady=10, padx=10) + + text_1 = customtkinter.CTkTextbox(master=master, width=200, height=70, state=state) + text_1.pack(pady=10, padx=10) + text_1.insert("0.0", "CTkTextbox\n\n\n\n") + + segmented_button_1 = customtkinter.CTkSegmentedButton(master=master, values=["CTkSegmentedButton", "Value 2"], state=state) + segmented_button_1.pack(pady=10, padx=10) + + tabview_1 = customtkinter.CTkTabview(master=master, width=200, height=100, state=state, border_width=2) + tabview_1.pack(pady=10, padx=10) + tabview_1.add("CTkTabview") + tabview_1.add("Tab 2") + + +frame_0 = customtkinter.CTkFrame(master=app, fg_color="transparent") +frame_0.grid(row=0, column=0, padx=10, pady=10) +create_all_widgets(frame_0, state="disabled") + +frame_1 = customtkinter.CTkFrame(master=app, fg_color="transparent") +frame_1.grid(row=0, column=1, padx=10, pady=10) +create_all_widgets(frame_1) + +frame_2 = customtkinter.CTkFrame(master=app) +frame_2.grid(row=0, column=2, padx=10, pady=10) +create_all_widgets(frame_2) + +frame_3 = customtkinter.CTkFrame(master=app) +frame_3.grid(row=0, column=3, padx=10, pady=10) +frame_4 = customtkinter.CTkFrame(master=frame_3) +frame_4.grid(row=0, column=0, padx=25, pady=25) +create_all_widgets(frame_4) + +appearance_mode_button = customtkinter.CTkSegmentedButton(app, values=["light", "dark"], command=lambda v: customtkinter.set_appearance_mode(v)) +appearance_mode_button.grid(row=1, column=0, columnspan=3, padx=25, pady=25) + +app.mainloop() diff --git a/test/manual_integration_tests/test_vertical_widgets.py b/test/manual_integration_tests/test_vertical_widgets.py index 3b2feab..92bd06d 100644 --- a/test/manual_integration_tests/test_vertical_widgets.py +++ b/test/manual_integration_tests/test_vertical_widgets.py @@ -7,17 +7,17 @@ app.title("test_vertical_widgets") app.grid_columnconfigure(0, weight=1) app.grid_rowconfigure((0, 1, 2, 3), weight=1) -progressbar_1 = customtkinter.CTkProgressBar(app, orient="horizontal") +progressbar_1 = customtkinter.CTkProgressBar(app, orientation="horizontal") progressbar_1.grid(row=0, column=0, pady=20, padx=20) -progressbar_2 = customtkinter.CTkProgressBar(app, orient="vertical") +progressbar_2 = customtkinter.CTkProgressBar(app, orientation="vertical") progressbar_2.grid(row=1, column=0, pady=20, padx=20) -slider_1 = customtkinter.CTkSlider(app, orient="horizontal", command=progressbar_1.set, +slider_1 = customtkinter.CTkSlider(app, orientation="horizontal", command=progressbar_1.set, button_corner_radius=3, button_length=20) slider_1.grid(row=2, column=0, pady=20, padx=20) -slider_2 = customtkinter.CTkSlider(app, orient="vertical", command=progressbar_2.set, +slider_2 = customtkinter.CTkSlider(app, orientation="vertical", command=progressbar_2.set, button_corner_radius=3, button_length=20) slider_2.grid(row=3, column=0, pady=20, padx=20) diff --git a/test/unit_tests/test_ctk.py b/test/unit_tests/test_ctk.py index ab0d801..bb95d29 100644 --- a/test/unit_tests/test_ctk.py +++ b/test/unit_tests/test_ctk.py @@ -62,28 +62,28 @@ class TestCTk(): customtkinter.ScalingTracker.set_window_scaling(1.5) self.root_ctk.geometry("300x400") - assert self.root_ctk.current_width == 300 and self.root_ctk.current_height == 400 + assert self.root_ctk._current_width == 300 and self.root_ctk._current_height == 400 assert self.root_ctk.window_scaling == 1.5 * customtkinter.ScalingTracker.get_window_dpi_scaling(self.root_ctk) self.root_ctk.maxsize(400, 500) self.root_ctk.geometry("500x500") - assert self.root_ctk.current_width == 400 and self.root_ctk.current_height == 500 + assert self.root_ctk._current_width == 400 and self.root_ctk._current_height == 500 customtkinter.ScalingTracker.set_window_scaling(1) - assert self.root_ctk.current_width == 400 and self.root_ctk.current_height == 500 + assert self.root_ctk._current_width == 400 and self.root_ctk._current_height == 500 print("successful") def test_configure(self): print(" -> test_configure: ", end="") self.root_ctk.configure(bg="white") - assert self.root_ctk.fg_color == "white" + assert self.root_ctk.cget("fg_color") == "white" self.root_ctk.configure(background="red") - assert self.root_ctk.fg_color == "red" + assert self.root_ctk.cget("fg_color") == "red" assert self.root_ctk.cget("bg") == "red" self.root_ctk.config(fg_color=("green", "#FFFFFF")) - assert self.root_ctk.fg_color == ("green", "#FFFFFF") + assert self.root_ctk.cget("fg_color") == ("green", "#FFFFFF") print("successful") def test_appearance_mode(self): diff --git a/test/unit_tests/test_ctk_entry.py b/test/unit_tests/test_ctk_entry.py new file mode 100644 index 0000000..5c14c9a --- /dev/null +++ b/test/unit_tests/test_ctk_entry.py @@ -0,0 +1,55 @@ +import customtkinter +import time + +app = customtkinter.CTk() + +entry_1 = customtkinter.CTkEntry(app, width=100, height=25) +entry_1.pack(padx=20, pady=20) +entry_2 = customtkinter.CTkEntry(app, width=100, height=25) +entry_2.pack(padx=20, pady=20) + +txt_var = customtkinter.StringVar(value="test") + +entry_1.configure(width=300, + height=35, + corner_radius=1000, + border_width=4, + bg_color="green", + fg_color=("red", "yellow"), + border_color="blue", + text_color=("brown", "green"), + placeholder_text_color="blue", + textvariable=txt_var, + placeholder_text="new_placholder", + font=("Times New Roman", -8, "bold"), + state="normal", + insertborderwidth=5, + insertwidth=10, + justify="right", + show="+") + +assert entry_1.cget("width") == 300 +assert entry_1.cget("height") == 35 +assert entry_1.cget("corner_radius") == 1000 +assert entry_1.cget("border_width") == 4 +assert entry_1.cget("bg_color") == "green" +assert entry_1.cget("fg_color") == ("red", "yellow") +assert entry_1.cget("border_color") == "blue" +assert entry_1.cget("text_color") == ("brown", "green") +assert entry_1.cget("placeholder_text_color") == "blue" +assert entry_1.cget("textvariable") == txt_var +assert entry_1.cget("placeholder_text") == "new_placholder" +assert entry_1.cget("font") == ("Times New Roman", -8, "bold") +assert entry_1.cget("state") == "normal" +assert entry_1.cget("insertborderwidth") == 5 +assert entry_1.cget("insertwidth") == 10 +assert entry_1.cget("justify") == "right" +# assert entry_1.cget("show") == "+" # somehow does not work, maybe a tkinter bug? + +def test_textvariable(): + txt_var.set("test_2") + print(entry_1.get()) + assert entry_1.get() == "test_2" + +app.after(500, test_textvariable) +app.mainloop()