diff --git a/.gitignore b/.gitignore index 3b44e68a..1af685c6 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,4 @@ cython_debug/ documentation_images/customtkinter_design.afdesign images/ Readme_pypi.md +temp.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d432f85..f5541e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,26 @@ ToDo: - set icon (self.call("wm", "iconphoto", self._w, tkinter.PhotoImage(file="test_images/CustomTkinter_logo_single.png"))) - add option to change label position for checkbox, switch, radiobutton #628 +## [5.3.0] - 2026-01-21 +### Added + - Showroom App, immediately available with the library installation + - Gold theme + - set(), index(), len() methods for those widgets that were suitable to use them + - Attribute to CTkSegmentedButton to make it vertically instead of horizontally + - Attribute to CTkTabview to configure the font for its CTkSegmentedButton + - Possibility to add a border to CTkLabel + - Mouse Wheel detection to CTkSlider and improved it on CTkScrollbar + +### Changed + - CTkButton triggers the command when the Mouse Button is released + - CTkEntry and CTkTextbox lose focus when you click somewhere else + - Clicking the Dropdown button again closes the menu for CTkComboBox and CTkOptionMenu + - Improved/fixed configure() and cget() methods for all widgets + - Improved Tab renaming for CTkTabview + - Improved drag behavior for CTkScrollbar + - Properly managed borders for CTkScrollableFrame + - Fixed a bug that prevented setting a custom Icon for CTkToplevel + - Fixed many bugs related to missing invocations or wrong names ## [5.2.0] - 2022-05-02 ### Added diff --git a/Readme.md b/Readme.md index d42ad15e..d798596c 100644 --- a/Readme.md +++ b/Readme.md @@ -6,27 +6,33 @@

- + ![PyPI](https://img.shields.io/pypi/v/customtkinter) ![PyPI - Downloads](https://img.shields.io/pypi/dm/customtkinter?color=green&label=downloads) ![Downloads last 6 month](https://static.pepy.tech/personalized-badge/customtkinter?period=total&units=international_system&left_color=grey&right_color=green&left_text=total%20downloads) ![PyPI - License](https://img.shields.io/badge/license-MIT-blue) - -
+![Minimum Python Version](https://shields.io/badge/Minimum_Python-3.7-blue) --- -
+| Current Developers | Contact | +|----------------------------------------------|-| +| Tom Schimansky | contact@customtkinter.tomschimansky.com | +| Federico Spada | www.linkedin.com/in/federicospada13 | + Paypal donation button - -| Massive Thanks to all the People who Donated to help this Project 😇 | -|----------------------------------------------| +
-

+--- + +
+ +

Official website: https://customtkinter.tomschimansky.com/ -

+

+ CustomTkinter is a python UI-library based on Tkinter, which provides new, modern and @@ -88,6 +94,11 @@ can find more example programs and in the [Documentation](https://github.com/Tom you can find further information on the appearance mode, scaling, themes and all widgets. ## More Examples and Showcase +You can run the following code to show a simple App that displays all available widgets: +```python +import customtkinter as ctk +ctk.run_showroom() +``` ### Appearance mode change and scaling change diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index d9144ceb..91642edf 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -1,5 +1,6 @@ -__version__ = "5.2.2" +__version__ = "5.3.0" +from typing import Optional import os import sys from tkinter import Variable, StringVar, IntVar, DoubleVar, BooleanVar @@ -86,3 +87,228 @@ def deactivate_automatic_dpi_awareness(): def set_ctk_parent_class(ctk_parent_class): ctk_tk.CTK_PARENT_CLASS = ctk_parent_class + + +def run_showroom() -> None: + set_appearance_mode("Light") + set_default_color_theme("blue") + + new_instance: bool = True + while new_instance: + app = _Showroom() + app.mainloop() + new_instance = app.new_instance_requested + + +class _Showroom(CTk): + SPACING = 20 + + def __init__(self) -> None: + super().__init__() + + # configure window + self.title("CustomTkinter Showroom") + + self.new_instance_requested: bool = False + + # create sidebar frame with widgets + self.sidebar_frame = CTkFrame(self, width=140, corner_radius=0) + self.logo_label = CTkLabel(self.sidebar_frame, text="CustomTkinter", font=CTkFont(size=20, weight="bold")) + self.theme_label = CTkLabel(self.sidebar_frame, text="Theme:", anchor="w") + self.theme_optionmenu = CTkOptionMenu(self.sidebar_frame, values=ThemeManager._built_in_themes, + command=self._change_theme_event) + self.theme_optionmenu.set(ThemeManager._currently_loaded_theme) + self.appearance_mode_label = CTkLabel(self.sidebar_frame, text="Appearance Mode:", anchor="w") + self.appearance_mode_optionemenu = CTkOptionMenu(self.sidebar_frame, values=["Light", "Dark", "System"], + command=self._change_appearance_mode_event) + self.appearance_mode_optionemenu.set(get_appearance_mode()) + self.scaling_label = CTkLabel(self.sidebar_frame, text="UI Scaling:", anchor="w") + self.scaling_optionmenu = CTkOptionMenu(self.sidebar_frame, values=["80%", "90%", "100%", "110%", "120%"], + command=self._change_scaling_event) + widget_scaling = round(ScalingTracker.widget_scaling*100) + self.scaling_optionmenu.set(f"{widget_scaling}%") + self.drawing_label = CTkLabel(self.sidebar_frame, text="Drawing method:", anchor="w") + self.drawing_optionmenu = CTkOptionMenu(self.sidebar_frame, values=DrawEngine.DRAWING_METHODS, + command=self._change_drawing_event) + self.drawing_optionmenu.set(DrawEngine.preferred_drawing_method) + + self.sidebar_frame.pack(side="left", fill="y") + self.logo_label.pack(side="top", fill="x", padx=5, pady=5) + self.theme_label.pack(side="top", fill="x", padx=20, pady=(20, 5)) + self.theme_optionmenu.pack(side="top", fill="x", padx=20, pady=(0, 10)) + self.appearance_mode_label.pack(side="top", fill="x", padx=20, pady=(20, 5)) + self.appearance_mode_optionemenu.pack(side="top", fill="x", padx=20, pady=(0, 10)) + self.scaling_label.pack(side="top", fill="x", padx=20, pady=(20, 5)) + self.scaling_optionmenu.pack(side="top", fill="x", padx=20, pady=(0, 10)) + self.drawing_label.pack(side="top", fill="x", padx=20, pady=(20, 5)) + self.drawing_optionmenu.pack(side="top", fill="x", padx=20, pady=(0, 10)) + + # create main tabview + self.main_tabview = CTkTabview(self) + + self.main_tabview.pack(side="left", fill="both", expand=True, padx=5, pady=(0, 5)) + + # buttons + self.buttons_frame = self.main_tabview.add("Buttons") + + self.button_1 = CTkButton(self.buttons_frame) + self.button_2 = CTkButton(self.buttons_frame, hover=False, text="No Hover") + self.button_3 = CTkButton(self.buttons_frame, state="disabled", text="disabled") + + self.button_1.pack(padx=20, pady=(self.SPACING, 5)) + self.button_2.pack(padx=20, pady=(0, 5)) + self.button_3.pack(padx=20, pady=(0, 5)) + + # choices + self.choices_frame = self.main_tabview.add("Choices") + self.combobox_1 = CTkComboBox(self.choices_frame, + values=["CTkComboBox", "Value 2", "Value 3", "User can also", "write any text"]) + self.combobox_1.set("CTkComboBox") + self.combobox_2 = CTkComboBox(self.choices_frame, state="readonly", + values=["readonly", "Value 2", "Value 3", "User can only", "choose a value"]) + self.combobox_2.set("readonly") + self.optionmenu = CTkOptionMenu(self.choices_frame, dynamic_resizing=False, + values=["CTkOptionMenu", "Value 2", "Value 3"]) + self.seg_button1 = CTkSegmentedButton(self.choices_frame, values=["CTkSegmentedButton", "Value 2", "Value 3"]) + self.seg_button1.set("CTkSegmentedButton") + self.seg_button2 = CTkSegmentedButton(self.choices_frame, values=["vertical", "Value 2", "Value 3"], orientation="vertical") + self.seg_button2.set("vertical") + + self.combobox_1.pack(padx=20, pady=(self.SPACING, 5)) + self.combobox_2.pack(padx=20, pady=(0, 5)) + self.optionmenu.pack(padx=20, pady=(self.SPACING, 5)) + self.seg_button1.pack(padx=20, pady=(self.SPACING, 5)) + self.seg_button2.pack(padx=20, pady=(0, 5)) + + # text + self.text_frame = self.main_tabview.add("Text") + self.label = CTkLabel(self.text_frame, text="CTkLabel", height=1) + self.entry = CTkEntry(self.text_frame, placeholder_text="CTkEntry") + self.textbox = CTkTextbox(self.text_frame, width=320) + 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.label.pack(padx=20, pady=(self.SPACING, 5)) + self.entry.pack(padx=20, pady=(self.SPACING, 5)) + self.textbox.pack(padx=20, pady=(self.SPACING, 5)) + + # boolean + self.boolean_frame = self.main_tabview.add("Boolean") + self.radio_var = IntVar(value=0) + self.radio_button_1 = CTkRadioButton(self.boolean_frame, variable=self.radio_var, value=0, width=130) + self.radio_button_2 = CTkRadioButton(self.boolean_frame, variable=self.radio_var, value=1, hover=False, text="No Hover", width=130) + self.radio_button_3 = CTkRadioButton(self.boolean_frame, variable=self.radio_var, value=2, state="disabled", text="Disabled", width=130) + self.checkbox_var = BooleanVar(value=True) + self.checkbox_1 = CTkCheckBox(self.boolean_frame, variable=self.checkbox_var, width=130) + self.checkbox_2 = CTkCheckBox(self.boolean_frame, hover=False, text="No Hover", width=130) + self.checkbox_3 = CTkCheckBox(self.boolean_frame, state="disabled", text="Disabled", width=130) + self.switch_var = BooleanVar(value=True) + self.switch_1 = CTkSwitch(self.boolean_frame, variable=self.switch_var, width=130) + self.switch_2 = CTkSwitch(self.boolean_frame, hover=False, text="No Hover", width=130) + self.switch_3 = CTkSwitch(self.boolean_frame, state="disabled", text="Disabled", width=130) + + self.radio_button_1.pack(padx=20, pady=(self.SPACING, 5)) + self.radio_button_2.pack(padx=20, pady=(0, 5)) + self.radio_button_3.pack(padx=20, pady=(0, 5)) + self.checkbox_1.pack(padx=20, pady=(self.SPACING, 5)) + self.checkbox_2.pack(padx=20, pady=(0, 5)) + self.checkbox_3.pack(padx=20, pady=(0, 5)) + self.switch_1.pack(padx=20, pady=(self.SPACING, 5)) + self.switch_2.pack(padx=20, pady=(0, 5)) + self.switch_3.pack(padx=20, pady=(0, 5)) + + # bars + self.bars_frame = self.main_tabview.add("Bars") + self.label_progbar_1 = CTkLabel(self.bars_frame, text="CTkProgressBar - determinate", height=1) + self.progressbar_1 = CTkProgressBar(self.bars_frame, mode="determinate", determinate_speed=0.5) + self.label_progbar_2 = CTkLabel(self.bars_frame, text="CTkProgressBar - indeterminate", height=1) + self.progressbar_2 = CTkProgressBar(self.bars_frame, mode="indeterminate", indeterminate_speed=0.5) + self.label_slider_1 = CTkLabel(self.bars_frame, text="CTkSlider - with steps", height=1) + self.slider_1 = CTkSlider(self.bars_frame, from_=0, to=1, number_of_steps=4) + self.label_slider_2 = CTkLabel(self.bars_frame, text="CTkSlider - continuous", height=1) + self.slider_2 = CTkSlider(self.bars_frame, from_=10, to=100) + self.label_scrollbar_1 = CTkLabel(self.bars_frame, text="CTkScrollbar", height=1) + self.scrollbar_1 = CTkScrollbar(self.bars_frame, orientation="horizontal") + self.scrollbar_1.set(0, 0.3) + + self.label_vertical = CTkLabel(self.bars_frame, text="vertical", height=1) + self.frame_vertical = CTkFrame(self.bars_frame) + self.progressbar_3 = CTkProgressBar(self.frame_vertical, orientation="vertical") + self.slider_3 = CTkSlider(self.frame_vertical, orientation="vertical") + self.scrollbar_2 = CTkScrollbar(self.frame_vertical, orientation="vertical") + self.scrollbar_2.set(0, 0.3) + + self.progressbar_1.start() + self.progressbar_2.start() + self.slider_3.configure(command = self.progressbar_3.set) + + self.label_progbar_1.pack(padx=20, pady=(self.SPACING, 5)) + self.progressbar_1.pack(padx=20, pady=(0, 5)) + self.label_progbar_2.pack(padx=20, pady=(0, 5)) + self.progressbar_2.pack(padx=20, pady=(0, 5)) + self.label_slider_1.pack(padx=20, pady=(self.SPACING, 5)) + self.slider_1.pack(padx=20, pady=(0, 5)) + self.label_slider_2.pack(padx=20, pady=(0, 5)) + self.slider_2.pack(padx=20, pady=(0, 5)) + self.label_scrollbar_1.pack(padx=20, pady=(self.SPACING, 5)) + self.scrollbar_1.pack(padx=20, pady=(0, 5)) + + self.label_vertical.pack(padx=20, pady=(self.SPACING, 5)) + self.frame_vertical.pack(padx=20, pady=(0, 5)) + self.progressbar_3.pack(side="left", padx=20) + self.slider_3.pack(side="left", padx=20) + self.scrollbar_2.pack(side="left", padx=20) + + # frames + self.frames_frame = self.main_tabview.add("Frames") + self.scrollable_frame = CTkScrollableFrame(self.frames_frame, label_text="CTkScrollableFrame", + fg_color=ThemeManager.theme["CTk"]["fg_color"]) + self.tabview = CTkTabview(self.frames_frame, fg_color=ThemeManager.theme["CTk"]["fg_color"]) + tab1 = self.tabview.add("CTkTabview") + tab2 = self.tabview.add("Tab 2") + tab3 = self.tabview.add("Tab 3") + CTkButton(tab1, text="Widget on 1st Tab").pack() + CTkCheckBox(tab2, text="Widget on 2nd Tab").pack() + CTkSwitch(tab3, text="Widget on 3rd Tab").pack() + + for i in range(100): + switch = CTkSwitch(self.scrollable_frame, text=f"CTkSwitch {i+1}") + switch.pack(padx=20, pady=5) + + self.scrollable_frame.pack(side=LEFT, padx=20, pady=(self.SPACING, 5)) + self.tabview.pack(side=LEFT, padx=20, pady=(self.SPACING, 5)) + + # windows + self.windows_frame = self.main_tabview.add("Windows") + self.open_toplevel = CTkButton(self.windows_frame, text="Open CTkToplevel", command=self._open_ctktoplevel_event) + self.open_dialog = CTkButton(self.windows_frame, text="Open CTkInputDialog", command=self._open_input_dialog_event) + + self.open_toplevel.pack(padx=20, pady=(self.SPACING, 5)) + self.open_dialog.pack(padx=20, pady=(self.SPACING, 5)) + + + def _open_ctktoplevel_event(self) -> None: + toplevel = CTkToplevel(self) + toplevel.geometry(f"{500}x{250}") + toplevel.resizable(True, True) + toplevel.title("CTkToplevel") + + def _open_input_dialog_event(self) -> None: + dialog = CTkInputDialog(title="CTkInputDialog", text="Description of requested input") + dialog.get_input() + + def _change_appearance_mode_event(self, new_appearance_mode: str) -> None: + set_appearance_mode(new_appearance_mode) + + def _change_scaling_event(self, new_scaling: str) -> None: + new_scaling_float = int(new_scaling.replace("%", "")) / 100 + set_widget_scaling(new_scaling_float) + + def _change_theme_event(self, new_theme: str) -> None: + set_default_color_theme(new_theme) + self.new_instance_requested = True + self.destroy() + + def _change_drawing_event(self, new_drawing_method: str) -> None: + DrawEngine.preferred_drawing_method = new_drawing_method + self.new_instance_requested = True + self.destroy() diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json index 192c1362..938557d9 100644 --- a/customtkinter/assets/themes/blue.json +++ b/customtkinter/assets/themes/blue.json @@ -23,7 +23,9 @@ }, "CTkLabel": { "corner_radius": 0, + "border_width": 0, "fg_color": "transparent", + "border_color": ["#979DA2", "#565B5E"], "text_color": ["gray10", "#DCE4EE"] }, "CTkEntry": { @@ -31,7 +33,7 @@ "border_width": 2, "fg_color": ["#F9F9FA", "#343638"], "border_color": ["#979DA2", "#565B5E"], - "text_color":["gray10", "#DCE4EE"], + "text_color": ["gray10", "#DCE4EE"], "placeholder_text_color": ["gray52", "gray62"] }, "CTkCheckBox": { @@ -39,7 +41,7 @@ "border_width": 3, "fg_color": ["#3B8ED0", "#1F6AA5"], "border_color": ["#3E454A", "#949A9F"], - "hover_color": ["#3B8ED0", "#1F6AA5"], + "hover_color": ["#36719F", "#144870"], "checkmark_color": ["#DCE4EE", "gray90"], "text_color": ["gray10", "#DCE4EE"], "text_color_disabled": ["gray60", "gray45"] @@ -123,7 +125,7 @@ "border_width": 0, "fg_color": ["#F9F9FA", "#1D1E1E"], "border_color": ["#979DA2", "#565B5E"], - "text_color":["gray10", "#DCE4EE"], + "text_color": ["gray10", "#DCE4EE"], "scrollbar_button_color": ["gray55", "gray41"], "scrollbar_button_hover_color": ["gray40", "gray53"] }, diff --git a/customtkinter/assets/themes/dark-blue.json b/customtkinter/assets/themes/dark-blue.json index 54ff211d..5d1547c2 100644 --- a/customtkinter/assets/themes/dark-blue.json +++ b/customtkinter/assets/themes/dark-blue.json @@ -15,15 +15,17 @@ "CTkButton": { "corner_radius": 6, "border_width": 0, - "fg_color": ["#3a7ebf", "#1f538d"], - "hover_color": ["#325882", "#14375e"], + "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, + "border_width": 0, "fg_color": "transparent", + "border_color": ["#979DA2", "#565B5E"], "text_color": ["gray14", "gray84"] }, "CTkEntry": { @@ -37,9 +39,9 @@ "CTkCheckBox": { "corner_radius": 6, "border_width": 3, - "fg_color": ["#3a7ebf", "#1f538d"], + "fg_color": ["#3A7EBF", "#1F538D"], "border_color": ["#3E454A", "#949A9F"], - "hover_color": ["#325882", "#14375e"], + "hover_color": ["#325882", "#14375E"], "checkmark_color": ["#DCE4EE", "gray90"], "text_color": ["gray14", "gray84"], "text_color_disabled": ["gray60", "gray45"] @@ -49,7 +51,7 @@ "border_width": 3, "button_length": 0, "fg_color": ["#939BA2", "#4A4D50"], - "progress_color": ["#3a7ebf", "#1f538d"], + "progress_color": ["#3A7EBF", "#1F538D"], "button_color": ["gray36", "#D5D9DE"], "button_hover_color": ["gray20", "gray100"], "text_color": ["gray14", "gray84"], @@ -59,9 +61,9 @@ "corner_radius": 1000, "border_width_checked": 6, "border_width_unchecked": 3, - "fg_color": ["#3a7ebf", "#1f538d"], + "fg_color": ["#3A7EBF", "#1F538D"], "border_color": ["#3E454A", "#949A9F"], - "hover_color": ["#325882", "#14375e"], + "hover_color": ["#325882", "#14375E"], "text_color": ["gray14", "gray84"], "text_color_disabled": ["gray60", "gray45"] }, @@ -69,7 +71,7 @@ "corner_radius": 1000, "border_width": 0, "fg_color": ["#939BA2", "#4A4D50"], - "progress_color": ["#3a7ebf", "#1f538d"], + "progress_color": ["#3A7EBF", "#1F538D"], "border_color": ["gray", "gray"] }, "CTkSlider": { @@ -79,14 +81,14 @@ "button_length": 0, "fg_color": ["#939BA2", "#4A4D50"], "progress_color": ["gray40", "#AAB0B5"], - "button_color": ["#3a7ebf", "#1f538d"], - "button_hover_color": ["#325882", "#14375e"] + "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"], + "fg_color": ["#3A7EBF", "#1F538D"], + "button_color": ["#325882", "#14375E"], + "button_hover_color": ["#234567", "#1E2C40"], "text_color": ["#DCE4EE", "#DCE4EE"], "text_color_disabled": ["gray74", "gray60"] }, @@ -111,8 +113,8 @@ "corner_radius": 6, "border_width": 2, "fg_color": ["#979DA2", "gray29"], - "selected_color": ["#3a7ebf", "#1f538d"], - "selected_hover_color": ["#325882", "#14375e"], + "selected_color": ["#3A7EBF", "#1F538D"], + "selected_hover_color": ["#325882", "#14375E"], "unselected_color": ["#979DA2", "gray29"], "unselected_hover_color": ["gray70", "gray41"], "text_color": ["#DCE4EE", "#DCE4EE"], diff --git a/customtkinter/assets/themes/gold.json b/customtkinter/assets/themes/gold.json new file mode 100644 index 00000000..4d9d8ecf --- /dev/null +++ b/customtkinter/assets/themes/gold.json @@ -0,0 +1,157 @@ +{ + "CTk": { + "fg_color": ["gray92", "gray14"] + }, + "CTkToplevel": { + "fg_color": ["gray92", "gray14"] + }, + "CTkFrame": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["gray86", "gray17"], + "top_fg_color": ["gray81", "gray20"], + "border_color": ["gray65", "gray28"] + }, + "CTkButton": { + "corner_radius": 6, + "border_width": 0, + "fg_color": ["#EFB951", "#ECA607"], + "hover_color": ["#DBA220", "#B57F05"], + "border_color": ["#3E454A", "#949A9F"], + "text_color": ["gray98", "#DCE4EE"], + "text_color_disabled": ["gray78", "gray68"] + }, + "CTkLabel": { + "corner_radius": 0, + "border_width": 0, + "fg_color": "transparent", + "border_color": ["#979DA2", "#565B5E"], + "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": ["#EFB951", "#ECA607"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color": ["#DBA220", "#B57F05"], + "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": ["#EFB951", "#ECA607"], + "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": ["#EFB951", "#ECA607"], + "border_color": ["#3E454A", "#949A9F"], + "hover_color": ["#DBA220", "#B57F05"], + "text_color": ["gray10", "#DCE4EE"], + "text_color_disabled": ["gray60", "gray45"] + }, + "CTkProgressBar": { + "corner_radius": 1000, + "border_width": 0, + "fg_color": ["#939BA2", "#4A4D50"], + "progress_color": ["#EFB951", "#ECA607"], + "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": ["#EFB951", "#ECA607"], + "button_hover_color": ["#DBA220", "#B57F05"] + }, + "CTkOptionMenu": { + "corner_radius": 6, + "fg_color": ["#EFB951", "#ECA607"], + "button_color": ["#DBA220", "#B57F05"], + "button_hover_color": ["#BC8416", "#896F1C"], + "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": ["#EFB951", "#ECA607"], + "selected_hover_color": ["#DBA220", "#B57F05"], + "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", "#1D1E1E"], + "border_color": ["#979DA2", "#565B5E"], + "text_color": ["gray10", "#DCE4EE"], + "scrollbar_button_color": ["gray55", "gray41"], + "scrollbar_button_hover_color": ["gray40", "gray53"] + }, + "CTkScrollableFrame": { + "label_fg_color": ["gray78", "gray23"] + }, + "DropdownMenu": { + "fg_color": ["gray90", "gray20"], + "hover_color": ["gray75", "gray28"], + "text_color": ["gray10", "gray90"] + }, + "CTkFont": { + "macOS": { + "family": "SF Display", + "size": 13, + "weight": "normal" + }, + "Windows": { + "family": "Roboto", + "size": 13, + "weight": "normal" + }, + "Linux": { + "family": "Roboto", + "size": 13, + "weight": "normal" + } + } +} diff --git a/customtkinter/assets/themes/green.json b/customtkinter/assets/themes/green.json index 200012f7..174747c7 100644 --- a/customtkinter/assets/themes/green.json +++ b/customtkinter/assets/themes/green.json @@ -23,7 +23,9 @@ }, "CTkLabel": { "corner_radius": 0, + "border_width": 0, "fg_color": "transparent", + "border_color": ["#979DA2", "#565B5E"], "text_color": ["gray10", "#DCE4EE"] }, "CTkEntry": { @@ -31,7 +33,7 @@ "border_width": 2, "fg_color": ["#F9F9FA", "#343638"], "border_color": ["#979DA2", "#565B5E"], - "text_color":["gray10", "#DCE4EE"], + "text_color": ["gray10", "#DCE4EE"], "placeholder_text_color": ["gray52", "gray62"] }, "CTkCheckBox": { @@ -61,7 +63,7 @@ "border_width_unchecked": 3, "fg_color": ["#2CC985", "#2FA572"], "border_color": ["#3E454A", "#949A9F"], - "hover_color":["#0C955A", "#106A43"], + "hover_color": ["#0C955A", "#106A43"], "text_color": ["gray10", "#DCE4EE"], "text_color_disabled": ["gray60", "gray45"] }, @@ -84,9 +86,9 @@ }, "CTkOptionMenu": { "corner_radius": 6, - "fg_color": ["#2cbe79", "#2FA572"], + "fg_color": ["#2CBE79", "#2FA572"], "button_color": ["#0C955A", "#106A43"], - "button_hover_color": ["#0b6e3d", "#17472e"], + "button_hover_color": ["#0B6E3D", "#17472E"], "text_color": ["gray98", "#DCE4EE"], "text_color_disabled": ["gray78", "gray68"] }, @@ -123,7 +125,7 @@ "border_width": 0, "fg_color": ["#F9F9FA", "gray23"], "border_color": ["#979DA2", "#565B5E"], - "text_color":["gray10", "#DCE4EE"], + "text_color": ["gray10", "#DCE4EE"], "scrollbar_button_color": ["gray55", "gray41"], "scrollbar_button_hover_color": ["gray40", "gray53"] }, diff --git a/customtkinter/windows/ctk_input_dialog.py b/customtkinter/windows/ctk_input_dialog.py index 6c4669ae..efae43f6 100644 --- a/customtkinter/windows/ctk_input_dialog.py +++ b/customtkinter/windows/ctk_input_dialog.py @@ -31,7 +31,7 @@ def __init__(self, super().__init__(fg_color=fg_color) self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) - self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color) + self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_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) diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index e137dc3c..b8a313a6 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -79,6 +79,11 @@ def __init__(self, self.bind('', self._update_dimensions_event) self.bind('', self._focus_in_event) + #allows CTkEntry and CTkTextbox to lose focus + def set_focus(event: tkinter.Event): + if hasattr(event.widget, "focus_set"): + event.widget.focus_set() + self.bind_all("", set_focus, add=True) def destroy(self): self._disable_macos_dark_title_bar() @@ -220,6 +225,7 @@ def configure(self, **kwargs): def cget(self, attribute_name: str) -> any: if attribute_name == "fg_color": return self._fg_color + else: return super().cget(attribute_name) diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 9780380b..f87bec00 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -38,14 +38,6 @@ def __init__(self, *args, CTkScalingBaseClass.__init__(self, scaling_type="window") check_kwargs_empty(kwargs, raise_error=True) - try: - # Set Windows titlebar icon - if sys.platform.startswith("win"): - customtkinter_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - self.after(200, lambda: self.iconbitmap(os.path.join(customtkinter_directory, "assets", "icons", "CustomTkinter_icon_Windows.ico"))) - except Exception: - pass - self._current_width = 200 # initial window size, always without scaling self._current_height = 200 self._min_width: int = 0 @@ -63,7 +55,7 @@ def __init__(self, *args, super().title("CTkToplevel") # indicator variables - self._iconbitmap_method_called = True + self._iconbitmap_method_called = False 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 @@ -83,6 +75,11 @@ def __init__(self, *args, self.bind('', self._update_dimensions_event) self.bind('', self._focus_in_event) + #allows CTkEntry and CTkTextbox to lose focus + def set_focus(event: tkinter.Event): + if hasattr(event.widget, "focus_set"): + event.widget.focus_set() + self.bind_all("", set_focus, add=True) def destroy(self): self._disable_macos_dark_title_bar() @@ -196,6 +193,7 @@ def configure(self, **kwargs): def cget(self, attribute_name: str) -> any: if attribute_name == "fg_color": return self._fg_color + else: return super().cget(attribute_name) @@ -203,6 +201,10 @@ def wm_iconbitmap(self, bitmap=None, default=None): self._iconbitmap_method_called = True super().wm_iconbitmap(bitmap, default) + def iconbitmap(self, bitmap=None, default=None): + self._iconbitmap_method_called = True + super().wm_iconbitmap(bitmap, default) + def _windows_set_titlebar_icon(self): try: # if not the user already called iconbitmap method, set icon diff --git a/customtkinter/windows/widgets/core_rendering/ctk_canvas.py b/customtkinter/windows/widgets/core_rendering/ctk_canvas.py index f291e2cb..30e85e55 100644 --- a/customtkinter/windows/widgets/core_rendering/ctk_canvas.py +++ b/customtkinter/windows/widgets/core_rendering/ctk_canvas.py @@ -1,6 +1,6 @@ import tkinter import sys -from typing import Union, Tuple +from typing import Union, Tuple, List class CTkCanvas(tkinter.Canvas): @@ -80,23 +80,25 @@ def create_aa_circle(self, x_pos: int, y_pos: int, radius: int, angle: int = 0, return circle_1 - def coords(self, tag_or_id, *args): + def coords(self, tag_or_id, *args) -> List[float]: if type(tag_or_id) == str and "ctk_aa_circle_font_element" in self.gettags(tag_or_id): coords_id = self.find_withtag(tag_or_id)[0] # take the lowest id for the given tag - super().coords(coords_id, *args[:2]) + coords = super().coords(coords_id, *args[:2]) if len(args) == 3: super().itemconfigure(coords_id, font=("CustomTkinter_shapes_font", -int(args[2]) * 2), text=self._get_char_from_radius(args[2])) elif type(tag_or_id) == int and tag_or_id in self._aa_circle_canvas_ids: - super().coords(tag_or_id, *args[:2]) + coords = super().coords(tag_or_id, *args[:2]) if len(args) == 3: super().itemconfigure(tag_or_id, font=("CustomTkinter_shapes_font", -args[2] * 2), text=self._get_char_from_radius(args[2])) else: - super().coords(tag_or_id, *args) + coords = super().coords(tag_or_id, *args) + + return coords def itemconfig(self, tag_or_id, *args, **kwargs): kwargs_except_outline = kwargs.copy() diff --git a/customtkinter/windows/widgets/core_rendering/draw_engine.py b/customtkinter/windows/widgets/core_rendering/draw_engine.py index 5acea560..894cc6fc 100644 --- a/customtkinter/windows/widgets/core_rendering/draw_engine.py +++ b/customtkinter/windows/widgets/core_rendering/draw_engine.py @@ -26,7 +26,8 @@ class DrawEngine: """ - preferred_drawing_method: str = None # 'polygon_shapes', 'font_shapes', 'circle_shapes' + DRAWING_METHODS: list[str] = ["polygon_shapes", "font_shapes", "circle_shapes"] + preferred_drawing_method: str = None def __init__(self, canvas: CTkCanvas): self._canvas = canvas @@ -651,12 +652,12 @@ def __draw_rounded_rect_with_border_vertical_split_font_shapes(self, width: int, self._canvas.delete("inner_corner_part") # delete inner corner parts if not needed # create canvas inner rectangle parts if not already created - if not self._canvas.find_withtag("inner_rectangle_1"): + if not self._canvas.find_withtag("inner_rectangle_left_1"): self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_left_1", "inner_rectangle_part", "inner_parts_left", "inner_parts", "left_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_1", "inner_rectangle_part", "inner_parts_right", "inner_parts", "right_parts"), width=0) requires_recoloring = True - if not self._canvas.find_withtag("inner_rectangle_2") and inner_corner_radius * 2 < height - (border_width * 2): + if not self._canvas.find_withtag("inner_rectangle_left_2") and inner_corner_radius * 2 < height - (border_width * 2): self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_left_2", "inner_rectangle_part", "inner_parts_left", "inner_parts", "left_parts"), width=0) self._canvas.create_rectangle(0, 0, 0, 0, tags=("inner_rectangle_right_2", "inner_rectangle_part", "inner_parts_right", "inner_parts", "right_parts"), width=0) requires_recoloring = True diff --git a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py index afd9431d..4177c8a7 100644 --- a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py +++ b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py @@ -171,7 +171,7 @@ def _check_font_type(self, font: any): def _check_image_type(self, image: any): """ check image type when passed to widget """ - if image is None: + if image is None or image == "": return image elif isinstance(image, CTkImage): return image diff --git a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py index a6b8186f..7c4648b9 100644 --- a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py +++ b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py @@ -1,4 +1,5 @@ import tkinter +import copy import sys from typing import Union, Tuple, Callable, List, Optional @@ -50,6 +51,7 @@ def destroy(self): # call destroy methods of super classes tkinter.Menu.destroy(self) CTkAppearanceModeBaseClass.destroy(self) + CTkScalingBaseClass.destroy(self) def _update_font(self): """ pass font to tkinter widgets with applied font scaling """ @@ -117,7 +119,17 @@ def open(self, x: Union[int, float], y: Union[int, float]): else: # Linux self.tk_popup(int(x), int(y)) + def close(self): + self.unpost() + + def is_open(self) -> bool: + return bool(self.winfo_viewable()) + def configure(self, **kwargs): + if "min_character_width" in kwargs: + self._min_character_width = kwargs.pop("min_character_width") + self._add_menu_commands() + 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)) @@ -136,7 +148,6 @@ def configure(self, **kwargs): 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: @@ -164,7 +175,7 @@ def cget(self, attribute_name: str) -> any: elif attribute_name == "command": return self._command elif attribute_name == "values": - return self._values + return copy.copy(self._values) else: return super().cget(attribute_name) diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py index e9f78391..578025c6 100644 --- a/customtkinter/windows/widgets/ctk_button.py +++ b/customtkinter/windows/widgets/ctk_button.py @@ -90,6 +90,7 @@ def __init__(self, self._compound: str = compound self._anchor: str = anchor self._click_animation_running: bool = False + self._mouse_inside: bool = False # canvas and draw engine self._canvas = CTkCanvas(master=self, @@ -124,13 +125,13 @@ def _create_bindings(self, sequence: Optional[str] = None): if self._image_label is not None: self._image_label.bind("", self._on_leave) - if sequence is None or sequence == "": - self._canvas.bind("", self._clicked) + if sequence is None or sequence == "": + self._canvas.bind("", self._on_release) if self._text_label is not None: - self._text_label.bind("", self._clicked) + self._text_label.bind("", self._on_release) if self._image_label is not None: - self._image_label.bind("", self._clicked) + self._image_label.bind("", self._on_release) def _set_scaling(self, *args, **kwargs): super()._set_scaling(*args, **kwargs) @@ -224,6 +225,7 @@ def _draw(self, no_color_updates=False): self._text_label = tkinter.Label(master=self, font=self._apply_font_scaling(self._font), text=self._text, + anchor=self._anchor, padx=0, pady=0, borderwidth=1, @@ -232,8 +234,8 @@ def _draw(self, no_color_updates=False): 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._text_label.bind("", self._on_release) + self._text_label.bind("", self._on_release) if no_color_updates is False: # set text_label fg color (text color) @@ -260,14 +262,14 @@ def _draw(self, no_color_updates=False): if self._image is not None: if self._image_label is None: - self._image_label = tkinter.Label(master=self) + self._image_label = tkinter.Label(master=self, anchor=self._anchor) 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) + self._image_label.bind("", self._on_release) + self._image_label.bind("", self._on_release) if no_color_updates is False: # set image_label bg color (background color of label) @@ -314,7 +316,7 @@ def _create_grid(self): 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_columnconfigure(2, weight=0, minsize=0) self.grid_rowconfigure((1, 3), weight=0) self.grid_columnconfigure((1, 3), weight=1) @@ -323,7 +325,7 @@ def _create_grid(self): 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_rowconfigure(2, weight=0, minsize=0) self.grid_columnconfigure((1, 3), weight=0) self.grid_rowconfigure((1, 3), weight=1) @@ -402,7 +404,6 @@ def configure(self, require_redraw=False, **kwargs): 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: @@ -416,7 +417,10 @@ def configure(self, require_redraw=False, **kwargs): 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 self._image_label is not None: + self._update_image() + else: + require_redraw = True if "state" in kwargs: self._state = kwargs.pop("state") @@ -436,6 +440,10 @@ def configure(self, require_redraw=False, **kwargs): if "anchor" in kwargs: self._anchor = kwargs.pop("anchor") + if self._text_label is not None: + self._text_label.configure(anchor=self._anchor) + if self._image_label is not None: + self._image_label.configure(anchor=self._anchor) self._create_grid() require_redraw = True @@ -480,6 +488,7 @@ def cget(self, attribute_name: str) -> any: return self._compound elif attribute_name == "anchor": return self._anchor + else: return super().cget(attribute_name) @@ -498,6 +507,7 @@ def _set_cursor(self): self.configure(cursor="hand2") def _on_enter(self, event=None): + self._mouse_inside = True if self._hover is True and self._state == "normal": if self._hover_color is None: inner_parts_color = self._fg_color @@ -518,6 +528,7 @@ def _on_enter(self, event=None): self._image_label.configure(bg=self._apply_appearance_mode(inner_parts_color)) def _on_leave(self, event=None): + self._mouse_inside = False self._click_animation_running = False if self._fg_color == "transparent": @@ -542,9 +553,8 @@ def _click_animation(self): if self._click_animation_running: self._on_enter() - def _clicked(self, event=None): - if self._state != tkinter.DISABLED: - + def _on_release(self, event=None): + if self._mouse_inside and 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 diff --git a/customtkinter/windows/widgets/ctk_checkbox.py b/customtkinter/windows/widgets/ctk_checkbox.py index 42f04f5e..33cd3a7f 100644 --- a/customtkinter/windows/widgets/ctk_checkbox.py +++ b/customtkinter/windows/widgets/ctk_checkbox.py @@ -221,13 +221,7 @@ def _draw(self, no_color_updates=False): 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 + require_new_state = False if "checkbox_width" in kwargs: self._checkbox_width = kwargs.pop("checkbox_width") @@ -239,22 +233,12 @@ def configure(self, require_redraw=False, **kwargs): 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 "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True - if "state" in kwargs: - self._state = kwargs.pop("state") - self._set_cursor() + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") require_redraw = True if "fg_color" in kwargs: @@ -281,38 +265,63 @@ def configure(self, require_redraw=False, **kwargs): self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) 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 "textvariable" in kwargs: + self._textvariable = kwargs.pop("textvariable") + self._text_label.configure(textvariable=self._textvariable) + + 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 "textvariable" in kwargs: - self._textvariable = kwargs.pop("textvariable") - self._text_label.configure(textvariable=self._textvariable) + if "onvalue" in kwargs: + self._onvalue = kwargs.pop("onvalue") + require_new_state = True + + if "offvalue" in kwargs: + self._offvalue = kwargs.pop("offvalue") + require_new_state = True 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 + require_new_state = True + if require_new_state and self._variable is not None and self._variable != "": + 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": + if attribute_name == "checkbox_width": return self._checkbox_width elif attribute_name == "checkbox_height": return self._checkbox_height + elif 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 @@ -337,12 +346,15 @@ def cget(self, attribute_name: str) -> any: return self._state elif attribute_name == "hover": return self._hover + elif attribute_name == "command": + return self._command 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) @@ -404,41 +416,28 @@ def _variable_callback(self, var_name, index, mode): self.select(from_variable_callback=True) elif self._variable.get() == self._offvalue: self.deselect(from_variable_callback=True) + + def set(self, state: bool, from_variable_callback=False): + self._check_state = state + self._draw() + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(self._onvalue if self._check_state is True else self._offvalue) + self._variable_callback_blocked = False 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 + self.set(not self._check_state) 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 + self.set(True, from_variable_callback) 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 + self.set(False, from_variable_callback) def get(self) -> Union[int, str]: return self._onvalue if self._check_state is True else self._offvalue diff --git a/customtkinter/windows/widgets/ctk_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py index 99495641..823a1c4b 100644 --- a/customtkinter/windows/widgets/ctk_combobox.py +++ b/customtkinter/windows/widgets/ctk_combobox.py @@ -83,6 +83,7 @@ def __init__(self, hover_color=dropdown_hover_color, text_color=dropdown_text_color, font=dropdown_font) + self._close_on_next_click: bool = False # configure grid system (1x1) self.grid_rowconfigure(0, weight=1) @@ -217,6 +218,7 @@ def _draw(self, no_color_updates=False): def _open_dropdown_menu(self): self._dropdown_menu.open(self.winfo_rootx(), self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0)) + self._close_on_next_click = True def configure(self, require_redraw=False, **kwargs): if "corner_radius" in kwargs: @@ -267,7 +269,6 @@ def configure(self, require_redraw=False, **kwargs): 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: @@ -338,10 +339,12 @@ def cget(self, attribute_name: str) -> any: return self._command elif attribute_name == "justify": return self._entry.cget("justify") + else: return super().cget(attribute_name) def _on_enter(self, event=0): + self._close_on_next_click = self._dropdown_menu.is_open() 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") @@ -396,8 +399,19 @@ def set(self, value: str): def get(self) -> str: return self._entry.get() + def index(self, value: Optional[Any] = None) -> int: + """ returns index of selected value, raises ValueError if the value is missing + if the parameter is provided, returns the associated index or raises ValueError if no value is found """ + if value is None: + return self._values.index(self.get()) + else: + return self._values.index(value) + def _clicked(self, event=None): - if self._state is not tkinter.DISABLED and len(self._values) > 0: + if self._close_on_next_click: + self._dropdown_menu.close() + self._close_on_next_click = False + elif self._state is not tkinter.DISABLED and len(self._values) > 0: self._open_dropdown_menu() def bind(self, sequence=None, command=None, add=True): diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py index cdc0220b..f2c9f749 100644 --- a/customtkinter/windows/widgets/ctk_entry.py +++ b/customtkinter/windows/widgets/ctk_entry.py @@ -72,7 +72,7 @@ def __init__(self, if isinstance(self._font, CTkFont): self._font.add_size_configure_callback(self._update_font) - if not (self._textvariable is None or self._textvariable == ""): + if self._textvariable is not None and self._textvariable != "": self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback) self._canvas = CTkCanvas(master=self, @@ -145,6 +145,9 @@ def _update_font(self): self._canvas.grid(column=0, row=0, sticky="nswe") def destroy(self): + if self._textvariable is not None: + self._textvariable.trace_remove("write", self._textvariable_callback_name) + if isinstance(self._font, CTkFont): self._font.remove_size_configure_callback(self._update_font) @@ -192,36 +195,40 @@ def _draw(self, no_color_updates=False): 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")) + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + self._create_grid() require_redraw = True - if "text_color" in kwargs: - self._text_color = self._check_color_type(kwargs.pop("text_color")) + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + self._create_grid() require_redraw = True - if "placeholder_text_color" in kwargs: - self._placeholder_text_color = self._check_color_type(kwargs.pop("placeholder_text_color")) + 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 "border_width" in kwargs: - self._border_width = kwargs.pop("border_width") - self._create_grid() + if "text_color" in kwargs: + self._text_color = self._check_color_type(kwargs.pop("text_color")) require_redraw = True - if "corner_radius" in kwargs: - self._corner_radius = kwargs.pop("corner_radius") - self._create_grid() + if "placeholder_text_color" in kwargs: + self._placeholder_text_color = self._check_color_type(kwargs.pop("placeholder_text_color")) require_redraw = True + if "textvariable" in kwargs: + if self._textvariable is not None and self._textvariable != "": + self._textvariable.trace_remove("write", self._textvariable_callback_name) # remove old variable callback + self._textvariable = kwargs.pop("textvariable") + self._entry.configure(textvariable=self._textvariable) + if self._textvariable is not None and self._textvariable != "": + self._textvariable_callback_name = self._textvariable.trace_add("write", self._textvariable_callback) + if "placeholder_text" in kwargs: self._placeholder_text = kwargs.pop("placeholder_text") if self._placeholder_text_active: @@ -230,19 +237,18 @@ def configure(self, require_redraw=False, **kwargs): 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 "state" in kwargs: + self._state = kwargs.pop("state") + self._entry.configure(state=self._state) + 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 @@ -335,6 +341,10 @@ def insert(self, index, string): return self._entry.insert(index, string) + def set(self, string: str): + self._entry.delete(0, tkinter.END) + self.insert(0, string) + def get(self): if self._placeholder_text_active: return "" diff --git a/customtkinter/windows/widgets/ctk_frame.py b/customtkinter/windows/widgets/ctk_frame.py index 7bddf3c2..c1367adb 100644 --- a/customtkinter/windows/widgets/ctk_frame.py +++ b/customtkinter/windows/widgets/ctk_frame.py @@ -132,6 +132,14 @@ def _draw(self, no_color_updates=False): # self._canvas.tag_lower("border_parts") 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 @@ -156,14 +164,6 @@ def configure(self, require_redraw=False, **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: diff --git a/customtkinter/windows/widgets/ctk_label.py b/customtkinter/windows/widgets/ctk_label.py index 7e59ad76..ca303231 100644 --- a/customtkinter/windows/widgets/ctk_label.py +++ b/customtkinter/windows/widgets/ctk_label.py @@ -27,9 +27,11 @@ def __init__(self, width: int = 0, 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, text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, @@ -46,11 +48,12 @@ def __init__(self, # color self._fg_color = ThemeManager.theme["CTkLabel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color, transparency=True) + self._border_color: Union[str, Tuple[str, str]] = ThemeManager.theme["CTkLabel"]["border_color"] if border_color is None else self._check_color_type(border_color) self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_color) if text_color_disabled is None: if "text_color_disabled" in ThemeManager.theme["CTkLabel"]: - self._text_color_disabled = ThemeManager.theme["CTkLabel"]["text_color"] + self._text_color_disabled = ThemeManager.theme["CTkLabel"]["text_color_disabled"] else: self._text_color_disabled = self._text_color else: @@ -58,6 +61,7 @@ def __init__(self, # shape self._corner_radius = ThemeManager.theme["CTkLabel"]["corner_radius"] if corner_radius is None else corner_radius + self._border_width: int = ThemeManager.theme["CTkLabel"]["border_width"] if border_width is None else border_width # text self._anchor = anchor @@ -161,38 +165,54 @@ 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) + self._apply_widget_scaling(self._border_width)) if no_color_updates is False or requires_recoloring: - if self._apply_appearance_mode(self._fg_color) == "transparent": + + 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 parts + 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)) + outline=self._apply_appearance_mode(self._bg_color), + fill=self._apply_appearance_mode(self._bg_color)) self._label.configure(fg=self._apply_appearance_mode(self._text_color), disabledforeground=self._apply_appearance_mode(self._text_color_disabled), 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)) + outline=self._apply_appearance_mode(self._fg_color), + fill=self._apply_appearance_mode(self._fg_color)) self._label.configure(fg=self._apply_appearance_mode(self._text_color), disabledforeground=self._apply_appearance_mode(self._text_color_disabled), 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 "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"), 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 "text_color" in kwargs: self._text_color = self._check_color_type(kwargs.pop("text_color")) require_redraw = True @@ -240,9 +260,13 @@ def configure(self, require_redraw=False, **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 == "text_color": return self._text_color elif attribute_name == "text_color_disabled": diff --git a/customtkinter/windows/widgets/ctk_optionmenu.py b/customtkinter/windows/widgets/ctk_optionmenu.py index 491027b1..0fbd38bb 100644 --- a/customtkinter/windows/widgets/ctk_optionmenu.py +++ b/customtkinter/windows/widgets/ctk_optionmenu.py @@ -90,6 +90,7 @@ def __init__(self, hover_color=dropdown_hover_color, text_color=dropdown_text_color, font=dropdown_font) + self._close_on_next_click: bool = False # configure grid system (1x1) self.grid_rowconfigure(0, weight=1) @@ -262,7 +263,6 @@ def configure(self, require_redraw=False, **kwargs): 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: @@ -275,15 +275,11 @@ def configure(self, require_redraw=False, **kwargs): 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 "state" in kwargs: self._state = kwargs.pop("state") @@ -353,8 +349,10 @@ def cget(self, attribute_name: str) -> any: def _open_dropdown_menu(self): self._dropdown_menu.open(self.winfo_rootx(), self.winfo_rooty() + self._apply_widget_scaling(self._current_height + 0)) + self._close_on_next_click = True def _on_enter(self, event=0): + self._close_on_next_click = self._dropdown_menu.is_open() 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", @@ -396,8 +394,19 @@ def set(self, value: str): def get(self) -> str: return self._current_value + def index(self, value: Optional[Any] = None) -> int: + """ returns index of selected value, raises ValueError if the value is missing + if the parameter is provided, returns the associated index or raises ValueError if no value is found """ + if value is None: + return self._values.index(self._current_value) + else: + return self._values.index(value) + def _clicked(self, event=0): - if self._state is not tkinter.DISABLED and len(self._values) > 0: + if self._close_on_next_click: + self._dropdown_menu.close() + self._close_on_next_click = False + elif self._state is not tkinter.DISABLED and len(self._values) > 0: self._open_dropdown_menu() def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): diff --git a/customtkinter/windows/widgets/ctk_progressbar.py b/customtkinter/windows/widgets/ctk_progressbar.py index 2d6ce59b..e2df078b 100644 --- a/customtkinter/windows/widgets/ctk_progressbar.py +++ b/customtkinter/windows/widgets/ctk_progressbar.py @@ -181,14 +181,10 @@ def configure(self, require_redraw=False, **kwargs): 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") diff --git a/customtkinter/windows/widgets/ctk_radiobutton.py b/customtkinter/windows/widgets/ctk_radiobutton.py index c07cd1f0..5c4ce845 100644 --- a/customtkinter/windows/widgets/ctk_radiobutton.py +++ b/customtkinter/windows/widgets/ctk_radiobutton.py @@ -206,17 +206,7 @@ def _draw(self, no_color_updates=False): 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 + require_new_state = False if "radiobutton_width" in kwargs: self._radiobutton_width = kwargs.pop("radiobutton_width") @@ -228,22 +218,16 @@ def configure(self, require_redraw=False, **kwargs): 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) + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True - self._update_font() + if "border_width_unchecked" in kwargs: + self._border_width_unchecked = kwargs.pop("border_width_unchecked") + require_redraw = True - if "state" in kwargs: - self._state = kwargs.pop("state") - self._set_cursor() + if "border_width_checked" in kwargs: + self._border_width_checked = kwargs.pop("border_width_checked") require_redraw = True if "fg_color" in kwargs: @@ -254,6 +238,10 @@ def configure(self, require_redraw=False, **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 @@ -262,15 +250,17 @@ def configure(self, require_redraw=False, **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 "text" in kwargs: + self._text = kwargs.pop("text") + self._text_label.configure(text=self._text) - if "command" in kwargs: - self._command = kwargs.pop("command") + 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") @@ -279,27 +269,42 @@ def configure(self, require_redraw=False, **kwargs): 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 + require_new_state = True + + if "value" in kwargs: + self._value = kwargs.pop("value") + require_new_state = 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 require_new_state and self._variable is not None and self._variable != "": + 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": + if attribute_name == "radiobutton_width": + return self._radiobutton_width + elif attribute_name == "radiobutton_height": + return self._radiobutton_height + elif 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 @@ -377,32 +382,28 @@ def _variable_callback(self, var_name, index, mode): else: self.deselect(from_variable_callback=True) + def set(self, state: bool, from_variable_callback=False): + self._check_state = state + self._draw() + + if self._variable is not None and not from_variable_callback: + self._variable_callback_blocked = True + self._variable.set(self._value if self._check_state else "") + self._variable_callback_blocked = False + 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 + self.set(True, from_variable_callback) 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 + self.set(False, from_variable_callback) def bind(self, sequence: str = None, command: Callable = None, add: Union[str, bool] = True): """ called on the tkinter.Canvas """ diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py index b727bba4..d29a97b4 100644 --- a/customtkinter/windows/widgets/ctk_scrollable_frame.py +++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -8,6 +8,8 @@ from .ctk_frame import CTkFrame from .ctk_scrollbar import CTkScrollbar +from .ctk_slider import CTkSlider +from .ctk_textbox import CTkTextbox from .appearance_mode import CTkAppearanceModeBaseClass from .scaling import CTkScalingBaseClass from .core_widget_classes import CTkBaseClass @@ -75,11 +77,17 @@ def __init__(self, self.bind("", lambda e: self._parent_canvas.configure(scrollregion=self._parent_canvas.bbox("all"))) self._parent_canvas.bind("", self._fit_frame_dimensions_to_canvas) - self.bind_all("", self._mouse_wheel_all, add="+") - self.bind_all("", self._keyboard_shift_press_all, add="+") - self.bind_all("", self._keyboard_shift_press_all, add="+") - self.bind_all("", self._keyboard_shift_release_all, add="+") - self.bind_all("", self._keyboard_shift_release_all, add="+") + + if "linux" in sys.platform: + self.bind_all("", self._mouse_wheel_all, add=True) + self.bind_all("", self._mouse_wheel_all, add=True) + else: + self.bind_all("", self._mouse_wheel_all, add=True) + + self.bind_all("", self._keyboard_shift_press_all, add=True) + self.bind_all("", self._keyboard_shift_press_all, add=True) + self.bind_all("", self._keyboard_shift_release_all, add=True) + self.bind_all("", self._keyboard_shift_release_all, add=True) self._create_window_id = self._parent_canvas.create_window(0, 0, window=self, anchor="nw") if self._parent_frame.cget("fg_color") == "transparent": @@ -93,6 +101,7 @@ def __init__(self, def destroy(self): tkinter.Frame.destroy(self) + self._parent_frame.destroy() CTkAppearanceModeBaseClass.destroy(self) CTkScalingBaseClass.destroy(self) @@ -190,6 +199,12 @@ def configure(self, **kwargs): if "scrollbar_button_hover_color" in kwargs: self._scrollbar.configure(button_hover_color=kwargs.pop("scrollbar_button_hover_color")) + if "label_fg_color" in kwargs: + self._label.configure(fg_color=kwargs.pop("label_fg_color")) + + if "label_text_color" in kwargs: + self._label.configure(text_color=kwargs.pop("label_text_color")) + if "label_text" in kwargs: self._label_text = kwargs.pop("label_text") self._label.configure(text=self._label_text) @@ -198,12 +213,6 @@ def configure(self, **kwargs): if "label_font" in kwargs: self._label.configure(font=kwargs.pop("label_font")) - if "label_text_color" in kwargs: - self._label.configure(text_color=kwargs.pop("label_text_color")) - - if "label_fg_color" in kwargs: - self._label.configure(fg_color=kwargs.pop("label_fg_color")) - if "label_anchor" in kwargs: self._label.configure(anchor=kwargs.pop("label_anchor")) @@ -215,23 +224,25 @@ def cget(self, attribute_name: str): elif attribute_name == "height": return self._desired_height - elif attribute_name == "label_text": - return self._label_text - elif attribute_name == "label_font": - return self._label.cget("font") - elif attribute_name == "label_text_color": - return self._label.cget("_text_color") - elif attribute_name == "label_fg_color": - return self._label.cget("fg_color") - elif attribute_name == "label_anchor": - return self._label.cget("anchor") - elif attribute_name.startswith("scrollbar_fg_color"): return self._scrollbar.cget("fg_color") elif attribute_name.startswith("scrollbar_button_color"): return self._scrollbar.cget("button_color") elif attribute_name.startswith("scrollbar_button_hover_color"): return self._scrollbar.cget("button_hover_color") + elif attribute_name == "label_fg_color": + return self._label.cget("fg_color") + elif attribute_name == "label_text_color": + return self._label.cget("_text_color") + + elif attribute_name == "label_text": + return self._label_text + elif attribute_name == "label_font": + return self._label.cget("font") + elif attribute_name == "label_anchor": + return self._label.cget("anchor") + elif attribute_name == "orientation": + return self._orientation else: return self._parent_frame.cget(attribute_name) @@ -247,9 +258,11 @@ def _set_scroll_increments(self): self._parent_canvas.configure(xscrollincrement=1, yscrollincrement=1) elif sys.platform == "darwin": self._parent_canvas.configure(xscrollincrement=4, yscrollincrement=8) + else: + self._parent_canvas.configure(xscrollincrement=30, yscrollincrement=30) def _mouse_wheel_all(self, event): - if self.check_if_master_is_canvas(event.widget): + if self._check_if_valid_scroll(event.widget): if sys.platform.startswith("win"): if self._shift_pressed: if self._parent_canvas.xview() != (0.0, 1.0): @@ -267,10 +280,11 @@ def _mouse_wheel_all(self, event): else: if self._shift_pressed: if self._parent_canvas.xview() != (0.0, 1.0): - self._parent_canvas.xview("scroll", -event.delta, "units") + self._parent_canvas.xview_scroll(-1 if event.num == 4 else 1, "units") else: if self._parent_canvas.yview() != (0.0, 1.0): - self._parent_canvas.yview("scroll", -event.delta, "units") + self._parent_canvas.yview_scroll(-1 if event.num == 4 else 1, "units") + def _keyboard_shift_press_all(self, event): self._shift_pressed = True @@ -278,11 +292,15 @@ def _keyboard_shift_press_all(self, event): def _keyboard_shift_release_all(self, event): self._shift_pressed = False - def check_if_master_is_canvas(self, widget): + def _check_if_valid_scroll(self, widget): if widget == self._parent_canvas: return True + elif isinstance(widget, (CTkScrollbar, CTkSlider, CTkTextbox)): + return False + elif isinstance(widget, CTkScrollableFrame): + return widget._parent_canvas == self._parent_canvas elif widget.master is not None: - return self.check_if_master_is_canvas(widget.master) + return self._check_if_valid_scroll(widget.master) else: return False diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py index 8e962215..7e0a9582 100644 --- a/customtkinter/windows/widgets/ctk_scrollbar.py +++ b/customtkinter/windows/widgets/ctk_scrollbar.py @@ -78,14 +78,21 @@ def _create_bindings(self, sequence: Optional[str] = None): """ set necessary bindings for functionality of widget, will overwrite other bindings """ if sequence is None: self._canvas.tag_bind("border_parts", "", self._clicked) + self._canvas.tag_bind("scrollbar_parts", "", self._clicked_scrollbar) if sequence is None or sequence == "": self._canvas.bind("", self._on_enter) if sequence is None or sequence == "": self._canvas.bind("", self._on_leave) if sequence is None or sequence == "": - self._canvas.bind("", self._clicked) - if sequence is None or sequence == "": - self._canvas.bind("", self._mouse_scroll_event) + self._canvas.bind("", self._on_motion) + if "linux" in sys.platform: + if sequence is None or sequence == "": + self._canvas.bind("", self._mouse_scroll_event) + if sequence is None or sequence == "": + self._canvas.bind("", self._mouse_scroll_event) + else: + if sequence is None or sequence == "": + self._canvas.bind("", self._mouse_scroll_event) def _set_scaling(self, *args, **kwargs): super()._set_scaling(*args, **kwargs) @@ -161,6 +168,14 @@ def _draw(self, no_color_updates=False): self._canvas.update_idletasks() def configure(self, require_redraw=False, **kwargs): + 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 + if "fg_color" in kwargs: self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True) require_redraw = True @@ -179,14 +194,6 @@ def configure(self, require_redraw=False, **kwargs): 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: @@ -199,9 +206,9 @@ def cget(self, attribute_name: str) -> any: elif attribute_name == "fg_color": return self._fg_color - elif attribute_name == "scrollbar_color": + elif attribute_name == "button_color": return self._button_color - elif attribute_name == "scrollbar_hover_color": + elif attribute_name == "button_hover_color": return self._button_hover_color elif attribute_name == "hover": @@ -228,10 +235,22 @@ def _on_leave(self, event=0): fill=self._apply_appearance_mode(self._button_color)) def _clicked(self, event): + self._motion_center_offset = 0 + self._on_motion(event) + + def _clicked_scrollbar(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))) + center = self._start_value + ((self._end_value - self._start_value) * 0.5) + self._motion_center_offset = center - value + + def _on_motion(self, event): + if self._orientation == "vertical": + value = self._reverse_widget_scaling(((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing)))+self._motion_center_offset + else: + value = self._reverse_widget_scaling(((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing)))+self._motion_center_offset current_scrollbar_length = self._end_value - self._start_value value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2))) @@ -245,9 +264,30 @@ def _clicked(self, event): 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') + delta = -int(event.delta/40) + elif sys.platform == "darwin": + delta = -event.delta + else: + delta = -1 if event.num == 4 else 1 + self._command('scroll', delta, 'units') + else: + #empty space is divided in 20 steps + delta = (1 - self._end_value + self._start_value) / 20 + #condition for both Linux and others OS + if event.delta > 0 or event.num == 4: + delta = -delta + + if self._start_value + delta < 0.0: + self._end_value = self._end_value - self._start_value + self._start_value = 0.0 + elif self._end_value + delta > 1.0: + self._start_value = 1 - self._end_value + self._start_value + self._end_value = 1.0 else: - self._command('scroll', -event.delta, 'units') + self._start_value += delta + self._end_value += delta + self._draw() + def set(self, start_value: float, end_value: float): self._start_value = float(start_value) diff --git a/customtkinter/windows/widgets/ctk_segmented_button.py b/customtkinter/windows/widgets/ctk_segmented_button.py index b8de1e79..ed575666 100644 --- a/customtkinter/windows/widgets/ctk_segmented_button.py +++ b/customtkinter/windows/widgets/ctk_segmented_button.py @@ -41,7 +41,8 @@ def __init__(self, variable: Union[tkinter.Variable, None] = None, dynamic_resizing: bool = True, command: Union[Callable[[str], Any], None] = None, - state: str = "normal"): + state: str = "normal", + orientation: Literal["horizontal", "vertical"] = "horizontal"): super().__init__(master=master, bg_color=bg_color, width=width, height=height) @@ -64,6 +65,7 @@ def __init__(self, self._command: Callable[[str], None] = command self._font = CTkFont() if font is None else font self._state = state + self._orientation = orientation self._buttons_dict: Dict[str, CTkButton] = {} # mapped from value to button object if values is None: @@ -123,15 +125,27 @@ def _configure_button_corners_for_index(self, index: int): 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)) + if self._orientation == "vertical": + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._bg_color, self._bg_color, self._sb_fg_color, self._sb_fg_color)) + else: + 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])) + if self._orientation == "vertical": + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._background_corner_colors[0], self._background_corner_colors[1], self._sb_fg_color, self._sb_fg_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)) + if self._orientation == "vertical": + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._bg_color, self._bg_color)) + else: + 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)) + if self._orientation == "vertical": + self._buttons_dict[self._value_list[index]].configure(background_corner_colors=(self._sb_fg_color, self._sb_fg_color, self._background_corner_colors[2], self._background_corner_colors[3])) + 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)) @@ -178,15 +192,25 @@ def _check_unique_values(values: List[str]): 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) + number_of_columns, number_of_rows = self.grid_size() + if self._orientation == "vertical": + # remove minsize from every grid cell in the first column + for n in range(number_of_rows): + self.grid_rowconfigure(n, weight=1, minsize=0) + self.grid_columnconfigure(0, weight=1) + + for index, value in enumerate(self._value_list): + self.grid_rowconfigure(index, weight=1, minsize=self._current_height) + self._buttons_dict[value].grid(row=index, column=0, sticky="nsew") + else: + # remove minsize from every grid cell in the first row + 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="nsew") + 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="nsew") def _create_buttons_from_values(self): assert len(self._buttons_dict) == 0 @@ -216,7 +240,6 @@ 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: @@ -289,14 +312,10 @@ def configure(self, **kwargs): 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") @@ -316,17 +335,11 @@ def configure(self, **kwargs): check_kwargs_empty(kwargs, raise_error=True) def cget(self, attribute_name: str) -> any: - if attribute_name == "width": - return super().cget(attribute_name) - elif attribute_name == "height": - return super().cget(attribute_name) - elif attribute_name == "corner_radius": + if attribute_name == "corner_radius": return self._sb_corner_radius elif attribute_name == "border_width": return self._sb_border_width - elif attribute_name == "bg_color": - return super().cget(attribute_name) elif attribute_name == "fg_color": return self._sb_fg_color elif attribute_name == "selected_color": @@ -341,6 +354,8 @@ def cget(self, attribute_name: str) -> any: return self._sb_text_color elif attribute_name == "text_color_disabled": return self._sb_text_color_disabled + elif attribute_name == "background_corner_colors": + return self._background_corner_colors elif attribute_name == "font": return self._font @@ -352,9 +367,13 @@ def cget(self, attribute_name: str) -> any: return self._dynamic_resizing elif attribute_name == "command": return self._command + elif attribute_name == "state": + return self._state + elif attribute_name == "orientation": + return self._orientation else: - raise ValueError(f"'{attribute_name}' is not a supported argument. Look at the documentation for supported arguments.") + 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: @@ -383,8 +402,13 @@ def set(self, value: str, from_variable_callback: bool = False, from_button_call def get(self) -> str: return self._current_value - def index(self, value: str) -> int: - return self._value_list.index(value) + def index(self, value: Optional[str] = None) -> int: + """ returns index of selected value, raises ValueError if the value is missing + if the parameter is provided, returns the associated index or raises ValueError if no value is found """ + if value is None: + return self._value_list.index(self._current_value) + else: + return self._value_list.index(value) def insert(self, index: int, value: str): if value not in self._buttons_dict: @@ -406,6 +430,10 @@ def insert(self, index: int, value: str): raise ValueError(f"CTkSegmentedButton can not insert value ''") else: raise ValueError(f"CTkSegmentedButton can not insert value '{value}', already part of the values") + + def len(self) -> int: + """ returns the number of defined buttons """ + return len(self._value_list) def move(self, new_index: int, value: str): if 0 <= new_index < len(self._value_list): diff --git a/customtkinter/windows/widgets/ctk_slider.py b/customtkinter/windows/widgets/ctk_slider.py index 7aa03eeb..5e2f8f8b 100644 --- a/customtkinter/windows/widgets/ctk_slider.py +++ b/customtkinter/windows/widgets/ctk_slider.py @@ -34,6 +34,7 @@ def __init__(self, to: int = 1, state: str = "normal", number_of_steps: Union[int, None] = None, + scroll_step: Optional[Union[int, float]] = None, hover: bool = True, command: Union[Callable[[float], Any], None] = None, variable: Union[tkinter.Variable, None] = None, @@ -74,6 +75,7 @@ def __init__(self, self._from_ = from_ self._to = to self._number_of_steps = number_of_steps + self._scroll_step = (1 / (20 if number_of_steps is None else number_of_steps)) if scroll_step is None else scroll_step self._output_value = self._from_ + (self._value * (self._to - self._from_)) if self._corner_radius < self._button_corner_radius: @@ -116,6 +118,14 @@ def _create_bindings(self, sequence: Optional[str] = None): self._canvas.bind("", self._clicked) if sequence is None or sequence == "": self._canvas.bind("", self._clicked) + if "linux" in sys.platform: + if sequence is None or sequence == "": + self._canvas.bind("", self._mouse_scroll_event) + if sequence is None or sequence == "": + self._canvas.bind("", self._mouse_scroll_event) + else: + if sequence is None or sequence == "": + self._canvas.bind("", self._mouse_scroll_event) def _set_scaling(self, *args, **kwargs): super()._set_scaling(*args, **kwargs) @@ -249,6 +259,9 @@ def configure(self, require_redraw=False, **kwargs): if "number_of_steps" in kwargs: self._number_of_steps = kwargs.pop("number_of_steps") + if "scroll_step" in kwargs: + self._scroll_step = kwargs.pop("scroll_step") + if "hover" in kwargs: self._hover = kwargs.pop("hover") @@ -258,14 +271,10 @@ def configure(self, require_redraw=False, **kwargs): 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 "orientation" in kwargs: self._orientation = kwargs.pop("orientation") @@ -302,6 +311,8 @@ def cget(self, attribute_name: str) -> any: return self._state elif attribute_name == "number_of_steps": return self._number_of_steps + elif attribute_name == "scroll_step": + return self._scroll_step elif attribute_name == "hover": return self._hover elif attribute_name == "command": @@ -313,31 +324,39 @@ def cget(self, attribute_name: str) -> any: else: return super().cget(attribute_name) + + def _update_value(self, value: float): + self._value = max(0.0, min(1.0, value)) + + 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 _clicked(self, event=None): if self._state == "normal": if self._orientation.lower() == "horizontal": - self._value = self._reverse_widget_scaling(event.x / self._current_width) + value = self._reverse_widget_scaling(event.x / self._current_width) else: - self._value = 1 - self._reverse_widget_scaling(event.y / self._current_height) + value = 1.0 - self._reverse_widget_scaling(event.y / self._current_height) - if self._value > 1: - self._value = 1 - if self._value < 0: - self._value = 0 + self._update_value(value) - 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 + def _mouse_scroll_event(self, event): + delta = self._scroll_step + #condition for both Linux and others OS + if event.delta < 0 or event.num == 5: + delta = -delta - if self._command is not None: - self._command(self._output_value) + self._update_value(self._value + delta) def _on_enter(self, event=0): if self._hover is True and self._state == "normal": @@ -360,9 +379,6 @@ def _round_to_step_size(self, value) -> float: 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: @@ -385,6 +401,9 @@ def set(self, output_value, from_variable_callback=False): self._variable.set(round(self._output_value) if isinstance(self._variable, tkinter.IntVar) else self._output_value) self._variable_callback_blocked = False + def get(self) -> float: + return self._output_value + def _variable_callback(self, var_name, index, mode): if not self._variable_callback_blocked: self.set(self._variable.get(), from_variable_callback=True) diff --git a/customtkinter/windows/widgets/ctk_switch.py b/customtkinter/windows/widgets/ctk_switch.py index 155c1746..5deeb5d6 100644 --- a/customtkinter/windows/widgets/ctk_switch.py +++ b/customtkinter/windows/widgets/ctk_switch.py @@ -255,17 +255,7 @@ def _draw(self, no_color_updates=False): 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 + require_new_state = False if "switch_width" in kwargs: self._switch_width = kwargs.pop("switch_width") @@ -277,22 +267,16 @@ def configure(self, require_redraw=False, **kwargs): 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) + if "corner_radius" in kwargs: + self._corner_radius = kwargs.pop("corner_radius") + require_redraw = True - self._update_font() + if "border_width" in kwargs: + self._border_width = kwargs.pop("border_width") + require_redraw = True - if "state" in kwargs: - self._state = kwargs.pop("state") - self._set_cursor() + if "button_length" in kwargs: + self._button_length = kwargs.pop("button_length") require_redraw = True if "fg_color" in kwargs: @@ -323,40 +307,65 @@ def configure(self, require_redraw=False, **kwargs): self._text_color_disabled = self._check_color_type(kwargs.pop("text_color_disabled")) require_redraw = True - if "hover" in kwargs: - self._hover = kwargs.pop("hover") + if "text" in kwargs: + self._text = kwargs.pop("text") + self._text_label.configure(text=self._text) - if "command" in kwargs: - self._command = kwargs.pop("command") + 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") self._text_label.configure(textvariable=self._textvariable) + if "onvalue" in kwargs: + self._onvalue = kwargs.pop("onvalue") + require_new_state = True + + if "offvalue" in kwargs: + self._offvalue = kwargs.pop("offvalue") + require_new_state = True + 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 + require_new_state = True + + if "hover" in kwargs: + self._hover = kwargs.pop("hover") + + if "command" in kwargs: + self._command = kwargs.pop("command") + if "state" in kwargs: + self._state = kwargs.pop("state") + self._set_cursor() + require_redraw = True + + if require_new_state and self._variable is not None and self._variable != "": + 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": + if attribute_name == "switch_width": + return self._switch_width + elif attribute_name == "switch_height": + return self._switch_height + elif 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 @@ -394,45 +403,28 @@ def cget(self, attribute_name: str) -> any: else: return super().cget(attribute_name) + + def set(self, state: bool, from_variable_callback=False): + self._check_state = state + self._draw(no_color_updates=True) - 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 and not from_variable_callback: + 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._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 + def toggle(self, event=None): + if self._state == tkinter.NORMAL: + self.set(not self._check_state) 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 + self.set(True, from_variable_callback) 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 + self.set(False, from_variable_callback) def get(self) -> Union[int, str]: return self._onvalue if self._check_state is True else self._offvalue diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py index 3b2ea5bb..4b805601 100644 --- a/customtkinter/windows/widgets/ctk_tabview.py +++ b/customtkinter/windows/widgets/ctk_tabview.py @@ -7,6 +7,7 @@ from .core_rendering import DrawEngine from .core_widget_classes import CTkBaseClass from .ctk_segmented_button import CTkSegmentedButton +from .font import CTkFont class CTkTabview(CTkBaseClass): @@ -36,6 +37,7 @@ def __init__(self, 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, + segmented_button_font: Optional[Union[tuple, CTkFont]] = None, text_color: Optional[Union[str, Tuple[str, str]]] = None, text_color_disabled: Optional[Union[str, Tuple[str, str]]] = None, @@ -75,6 +77,9 @@ def __init__(self, height=self._apply_widget_scaling(self._desired_height - self._outer_spacing - self._outer_button_overhang)) self._draw_engine = DrawEngine(self._canvas) + # segmented_button_font font + self._segmented_button_font = CTkFont() if segmented_button_font is None else segmented_button_font + self._segmented_button = CTkSegmentedButton(self, values=[], height=self._button_height, @@ -88,6 +93,7 @@ def __init__(self, corner_radius=corner_radius, border_width=self._segmented_button_border_width, command=self._segmented_button_callback, + font=self._segmented_button_font, state=state) self._configure_segmented_button_background_corners() self._configure_grid() @@ -253,37 +259,53 @@ def configure(self, require_redraw=False, **kwargs): self._set_grid_canvas() self._configure_segmented_button_background_corners() self._segmented_button.configure(corner_radius=self._corner_radius) + 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) self._configure_segmented_button_background_corners() 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 "segmented_button_font" in kwargs: + self._segmented_button_font = kwargs.pop("segmented_button_font") + self._segmented_button.configure(font=self._segmented_button_font) + 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 "anchor" in kwargs: self._anchor = kwargs.pop("anchor") self._configure_grid() self._set_grid_segmented_button() + if "state" in kwargs: self._segmented_button.configure(state=kwargs.pop("state")) @@ -299,6 +321,7 @@ def cget(self, attribute_name: str): 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": @@ -309,6 +332,9 @@ def cget(self, attribute_name: str): 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 == "segmented_button_font": + return self._segmented_button_font + elif attribute_name == "text_color": return self._segmented_button.cget(attribute_name) elif attribute_name == "text_color_disabled": @@ -359,10 +385,6 @@ def add(self, name: str) -> CTkFrame: """ appends new tab with given name """ return self.insert(len(self._tab_dict), name) - def index(self, name) -> int: - """ get index of tab with given name """ - return self._segmented_button.index(name) - def move(self, new_index: int, name: str): if 0 <= new_index < len(self._name_list): if name in self._tab_dict: @@ -382,18 +404,21 @@ def rename(self, old_name: str, new_name: str): self._segmented_button.insert(old_index, new_name) # name list - self._name_list.remove(old_name) - self._name_list.append(new_name) + self._name_list[self._name_list.index(old_name)] = new_name # tab dictionary self._tab_dict[new_name] = self._tab_dict.pop(old_name) + # update current_name so we don't loose the connection to the frame + if self._current_name == old_name: + self._current_name = new_name + 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[name].destroy() self._tab_dict.pop(name) self._segmented_button.delete(name) @@ -428,6 +453,22 @@ def set(self, name: str): 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 + def get(self, index: Optional[int] = None) -> str: + """ returns name of selected tab, returns empty string if no tab selected.\n + if an index is provided, returns the tab name in that position """ + if index is None: + return self._current_name + else: + return self._name_list[index] + + def index(self, name: Optional[str] = None) -> int: + """ returns index of selected tab, raises ValueError if the tab is missing + if the parameter is provided, returns the associated index or raises ValueError if no tab is found """ + if name is None: + return self._name_list.index(self._current_name) + else: + return self._name_list.index(name) + + def len(self) -> int: + """ returns the number of defined tabs """ + return len(self._name_list) diff --git a/customtkinter/windows/widgets/ctk_textbox.py b/customtkinter/windows/widgets/ctk_textbox.py index 4b3a165f..82905287 100644 --- a/customtkinter/windows/widgets/ctk_textbox.py +++ b/customtkinter/windows/widgets/ctk_textbox.py @@ -251,6 +251,21 @@ def _draw(self, no_color_updates=False): self._canvas.tag_lower("border_parts") def configure(self, require_redraw=False, **kwargs): + 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 "fg_color" in kwargs: self._fg_color = self._check_color_type(kwargs.pop("fg_color"), transparency=True) require_redraw = True @@ -278,28 +293,12 @@ def configure(self, require_redraw=False, **kwargs): 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)) @@ -319,10 +318,18 @@ def cget(self, attribute_name: str) -> any: return self._border_color elif attribute_name == "text_color": return self._text_color + elif attribute_name == "scrollbar_button_color": + return self._scrollbar_button_color + elif attribute_name == "scrollbar_button_hover_color": + return self._scrollbar_button_hover_color elif attribute_name == "font": return self._font + elif attribute_name == "activate_scrollbars": + return self._scrollbars_activated + elif attribute_name in self._valid_tk_text_attributes: + return self._textbox.cget(attribute_name) # cget of tkinter.Text else: return super().cget(attribute_name) diff --git a/customtkinter/windows/widgets/font/ctk_font.py b/customtkinter/windows/widgets/font/ctk_font.py index e0eca0d2..a02ce0a0 100644 --- a/customtkinter/windows/widgets/font/ctk_font.py +++ b/customtkinter/windows/widgets/font/ctk_font.py @@ -65,14 +65,14 @@ 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") + if "size" in kwargs: + self._size = kwargs.pop("size") + super().configure(size=-abs(self._size)) + super().configure(**kwargs) # update style string for create_scaled_tuple() method @@ -85,8 +85,7 @@ def configure(self, **kwargs): 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) diff --git a/customtkinter/windows/widgets/image/ctk_image.py b/customtkinter/windows/widgets/image/ctk_image.py index 0247cdd3..60a05f82 100644 --- a/customtkinter/windows/widgets/image/ctk_image.py +++ b/customtkinter/windows/widgets/image/ctk_image.py @@ -55,10 +55,12 @@ def configure(self, **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") @@ -69,9 +71,9 @@ def configure(self, **kwargs): def cget(self, attribute_name: str) -> any: if attribute_name == "light_image": return self._light_image - if attribute_name == "dark_image": + elif attribute_name == "dark_image": return self._dark_image - if attribute_name == "size": + elif attribute_name == "size": return self._size def _check_images(self): diff --git a/customtkinter/windows/widgets/scaling/scaling_base_class.py b/customtkinter/windows/widgets/scaling/scaling_base_class.py index 0d7b29b3..1be0f558 100644 --- a/customtkinter/windows/widgets/scaling/scaling_base_class.py +++ b/customtkinter/windows/widgets/scaling/scaling_base_class.py @@ -57,13 +57,24 @@ def _get_widget_scaling(self) -> float: def _get_window_scaling(self) -> float: return self.__window_scaling - def _apply_widget_scaling(self, value: Union[int, float]) -> Union[float]: + # Some parts of Tk - notably canvas - are very buggy with floats, because they use locale-dependent parsing + # (and thus might not recognize "." as the decimal point) + # https://wiki.tcl-lang.org/page/locale + # https://github.com/python/cpython/issues/56767 + # Hence, we must ensure any integer value stays that way + def _apply_widget_scaling(self, value: Union[int, float]) -> Union[int, float]: assert self.__scaling_type == "widget" - return value * self.__widget_scaling + if isinstance(value, float): + return value * self.__widget_scaling + else: + return int(value * self.__widget_scaling) - def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[float]: + def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[int, float]: assert self.__scaling_type == "widget" - return value / self.__widget_scaling + if isinstance(value, float): + return value / self.__widget_scaling + else: + return int(value / self.__widget_scaling) def _apply_window_scaling(self, value: Union[int, float]) -> int: assert self.__scaling_type == "window" diff --git a/customtkinter/windows/widgets/theme/theme_manager.py b/customtkinter/windows/widgets/theme/theme_manager.py index cf22858b..3469de96 100644 --- a/customtkinter/windows/widgets/theme/theme_manager.py +++ b/customtkinter/windows/widgets/theme/theme_manager.py @@ -8,7 +8,7 @@ class ThemeManager: theme: dict = {} # contains all the theme data - _built_in_themes: List[str] = ["blue", "green", "dark-blue", "sweetkind"] + _built_in_themes: List[str] = ["blue", "green", "gold", "dark-blue"] _currently_loaded_theme: Union[str, None] = None @classmethod @@ -42,6 +42,11 @@ def load_theme(cls, theme_name_or_path: str): cls.theme["CTkCheckBox"] = cls.theme.pop("CTkCheckbox") if "CTkRadiobutton" in cls.theme.keys(): cls.theme["CTkRadioButton"] = cls.theme.pop("CTkRadiobutton") + if "CTkLabel" in cls.theme.keys(): + if "border_width" not in cls.theme["CTkLabel"].keys(): + cls.theme["CTkLabel"]["border_width"] = 0 + if "border_color" not in cls.theme["CTkLabel"].keys(): + cls.theme["CTkLabel"]["border_color"] = ["black", "white"] @classmethod def save_theme(cls): diff --git a/dev-proces.md b/dev-proces.md new file mode 100644 index 00000000..78a40862 --- /dev/null +++ b/dev-proces.md @@ -0,0 +1,28 @@ +Development on `/develop` branch. +- create Pull Requests following these guidelines: + - 1 feature <=> 1 PR: do not condese in a single Pull Requests different changes + - do not leave commented code: if you need to remove an entire section, just delete it + - do not add comments that refer to how the code was: "changed", "fixed", "it wasn't present" do not provide useful info and must be avoided +- merge external pull requests into `/develop` +- implement features, fix bugs on `/develop` +- update changelog in `CHANGELOG.md` +- test on all platforms for new graphical features + +When ready: Bump version using `tbump`: +``` +tbump 5.2.3 +``` + +Create pull request to merge `/develop` into `/master` branch on Github. +- approval by owner (Tom), merge to `/master` + + +Publish new version to PyPI (Tom): +``` +python -m pip install --upgrade build +rm -r dist +python -m build +python -m twine upload dist/* +``` +Finally: Update documentation for new features. +- upload to website by Tom diff --git a/examples/complex_example.py b/examples/complex_example.py index 072bc646..3ac9cb8c 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -132,7 +132,7 @@ def __init__(self): self.scrollable_frame_switches[0].select() self.scrollable_frame_switches[4].select() self.radio_button_3.configure(state="disabled") - self.appearance_mode_optionemenu.set("Dark") + self.appearance_mode_optionemenu.set("System") self.scaling_optionemenu.set("100%") self.optionmenu_1.set("CTkOptionmenu") self.combobox_1.set("CTkComboBox") diff --git a/pyproject.toml b/pyproject.toml index a2ad10d2..1642e743 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" github_url = "https://github.com/TomSchimansky/CustomTkinter" [tool.tbump.version] -current = "5.2.2" +current = "5.3.0" # Example of a semver regexp. # Make sure this matches current_version before diff --git a/requirements.txt b/requirements.txt index c03edc25..c915da35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -darkdetect~=0.3.1 +darkdetect~=0.7.0 typing-extensions~=4.4.0 packaging setuptools diff --git a/setup.cfg b/setup.cfg index 5f95b7f9..2bc7b963 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = customtkinter -version = 5.2.2 +version = 5.3.0 description = Create modern looking GUIs with Python long_description = A modern and customizable python UI-library based on Tkinter: https://customtkinter.tomschimansky.com long_description_content_type = text/markdown 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 90dcbffb..8a62b751 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 @@ -1,6 +1,8 @@ +import locale import tkinter import customtkinter # <- import the CustomTkinter module +locale.setlocale(locale.LC_NUMERIC, 'de_DE') # to verify that the canvas float argument bug is properly averted customtkinter.ScalingTracker.set_window_scaling(0.5) customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light" 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 3c0d8f7d..498ff5f3 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 @@ -1,6 +1,8 @@ +import locale import tkinter import customtkinter # <- import the CustomTkinter module +locale.setlocale(locale.LC_NUMERIC, 'de_DE') # to verify that the canvas float argument bug is properly averted customtkinter.ScalingTracker.set_window_scaling(0.5) customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light" diff --git a/test/manual_integration_tests/test_scrollable_frame.py b/test/manual_integration_tests/test_scrollable_frame.py index 808a4ca2..3929244f 100644 --- a/test/manual_integration_tests/test_scrollable_frame.py +++ b/test/manual_integration_tests/test_scrollable_frame.py @@ -28,7 +28,7 @@ frame_5 = customtkinter.CTkScrollableFrame(app, orientation="vertical", label_text="CTkScrollableFrame", corner_radius=0) frame_5.grid(row=0, column=2, rowspan=2, sticky="nsew") -for i in range(100): +for i in range(20): customtkinter.CTkCheckBox(frame_1).grid(row=i, padx=10, pady=10) customtkinter.CTkCheckBox(frame_2).grid(row=i, padx=10, pady=10) customtkinter.CTkCheckBox(frame_3).grid(row=0, column=i, padx=10, pady=10) diff --git a/test/manual_integration_tests/test_segmented_button.py b/test/manual_integration_tests/test_segmented_button.py index 79ef7ce4..0c3111d7 100644 --- a/test/manual_integration_tests/test_segmented_button.py +++ b/test/manual_integration_tests/test_segmented_button.py @@ -55,27 +55,35 @@ 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, +seg_6 = customtkinter.CTkSegmentedButton(app, corner_radius=20, values=["value 1", "value 2", "value 3"], background_corner_colors=["red", "orange", "green", "blue"], orientation="vertical") +seg_6.set("value 2") +seg_6.pack(side="left", padx=40) + +seg_7 = customtkinter.CTkSegmentedButton(app, corner_radius=40, values=["value 4", "value 5", "value 6"], orientation="vertical") +seg_7.set("value 6") +seg_7.pack(side="left") + +seg_8_var = customtkinter.StringVar(value="kfasjkfdklaj") +seg_8 = customtkinter.CTkSegmentedButton(app, width=300) +seg_8.pack(padx=20, pady=20) +entry_8 = customtkinter.CTkEntry(app) +entry_8.pack(padx=20, pady=(0, 20)) +button_8 = customtkinter.CTkButton(app, text="set", command=lambda: seg_8.set(entry_8.get())) +button_8.pack(padx=20, pady=(0, 20)) +button_8 = customtkinter.CTkButton(app, text="insert value", command=lambda: seg_8.insert(0, entry_8.get())) +button_8.pack(padx=20, pady=(0, 20)) +label_8 = customtkinter.CTkLabel(app, textvariable=seg_8_var) +label_8.pack(padx=20, pady=(0, 20)) + +seg_8.configure(height=50, variable=seg_8_var) +seg_8.delete("CTkSegmentedButton") + +seg_9 = customtkinter.CTkSegmentedButton(app, values=["disabled seg button", "2", "3"]) +seg_9.pack(padx=20, pady=20) +seg_9.configure(state="disabled") +seg_9.set("2") + +seg_9.configure(height=40, width=400, dynamic_resizing=False, font=("Times", -20)) app.mainloop()