diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index 1815c55..348c040 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -1,5 +1,6 @@ __version__ = "3.12" +# import widgets from .widgets.ctk_button import CTkButton from .widgets.ctk_checkbox import CTkCheckBox from .widgets.ctk_entry import CTkEntry @@ -11,47 +12,22 @@ from .widgets.ctk_radiobutton import CTkRadioButton from .widgets.ctk_canvas import CTkCanvas from .widgets.ctk_switch import CTkSwitch +# import windows from .windows.ctk_tk import CTk from .windows.ctk_toplevel import CTkToplevel from .windows.ctk_input_dialog import CTkInputDialog +# import other classes from .ctk_settings import CTkSettings from .appearance_mode_tracker import AppearanceModeTracker -from .theme_manager import CTkThemeManager +from .ctk_theme_manager import CTkThemeManager from .scaling_tracker import ScalingTracker -from distutils.version import StrictVersion as Version -import tkinter import os import sys import shutil -def enable_macos_darkmode(): - if sys.platform == "darwin": # macOS - if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 - os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No") - - sys.stderr.write("WARNING (customtkinter.enable_macos_darkmode): " + - "This command forces macOS dark-mode on all programs. " + - "This can cause bugs on some other programs.\n" + - "Disable it by calling customtkinter.disable_macos_darkmode() at the end of the program.\n") - else: - sys.stderr.write("WARNING (customtkinter.enable_macos_darkmode): " + - "Currently this works only with anaconda python version (Tcl/Tk >= 8.6.9).\n" + - "(python.org Tcl/Tk version is only 8.6.8)\n") - else: - sys.stderr.write("WARNING (customtkinter.enable_macos_darkmode): " + - "System is not macOS, but the following: {}\n".format(sys.platform)) - - -def disable_macos_darkmode(): - if sys.platform == "darwin": # macOS - if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 - os.system("defaults delete -g NSRequiresAquaSystemAppearance") - # This command reverts the dark-mode setting for all programs. - - def set_appearance_mode(mode_string): AppearanceModeTracker.set_appearance_mode(mode_string) diff --git a/customtkinter/assets/themes/dark-blue.json b/customtkinter/assets/themes/dark-blue.json index fb76562..bbe8d87 100644 --- a/customtkinter/assets/themes/dark-blue.json +++ b/customtkinter/assets/themes/dark-blue.json @@ -16,7 +16,7 @@ "text": ["gray12", "gray90"], "text_disabled": ["gray60", "gray50"], "text_button_disabled": ["gray40", "gray74"], - "progressbar": ["#6B6B6B", "gray6"], + "progressbar": ["#6B6B6B", "gray0"], "progressbar_progress": ["#608BD5", "#395E9C"], "progressbar_border": ["gray", "gray"], "slider": ["#6B6B6B", "gray6"], @@ -66,4 +66,4 @@ "switch_button_corner_radius": 1000, "switch_button_length": 0 } -} \ No newline at end of file +} diff --git a/customtkinter/ctk_draw_engine.py b/customtkinter/ctk_draw_engine.py index f12188c..d59e0f3 100644 --- a/customtkinter/ctk_draw_engine.py +++ b/customtkinter/ctk_draw_engine.py @@ -568,6 +568,15 @@ class CTkDrawEngine: slider_x_position - (button_length / 2), height - button_corner_radius) self._canvas.itemconfig("slider_line_1", width=button_corner_radius * 2) + elif orientation == "s": + slider_y_position = corner_radius + (button_length / 2) + (height - 2 * corner_radius - button_length) * (1 - slider_value) + self._canvas.coords("slider_line_1", + button_corner_radius, slider_y_position - (button_length / 2), + button_corner_radius, slider_y_position + (button_length / 2), + width - button_corner_radius, slider_y_position + (button_length / 2), + width - button_corner_radius, slider_y_position - (button_length / 2)) + self._canvas.itemconfig("slider_line_1", + width=button_corner_radius * 2) return requires_recoloring diff --git a/customtkinter/theme_manager.py b/customtkinter/ctk_theme_manager.py similarity index 100% rename from customtkinter/theme_manager.py rename to customtkinter/ctk_theme_manager.py diff --git a/customtkinter/scaling_tracker.py b/customtkinter/scaling_tracker.py index 0e7c285..f79decc 100644 --- a/customtkinter/scaling_tracker.py +++ b/customtkinter/scaling_tracker.py @@ -81,9 +81,24 @@ class ScalingTracker: if window_root not in cls.window_dpi_scaling_dict: cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_scaling(window_root) - if not cls.update_loop_running: - window_root.after(100, cls.check_dpi_scaling) - cls.update_loop_running = True + #if not cls.update_loop_running: + # window_root.after(100, cls.check_dpi_scaling) + # cls.update_loop_running = True + + @classmethod + def remove_widget(cls, widget_callback, widget): + window_root = cls.get_window_root_of_widget(widget) + try: + cls.window_widgets_dict[window_root].remove(widget_callback) + except: + pass + + @classmethod + def remove_window(cls, window_callback, window): + try: + del cls.window_widgets_dict[window] + except: + pass @classmethod def add_window(cls, window_callback, window): @@ -133,6 +148,9 @@ class ScalingTracker: @classmethod def check_dpi_scaling(cls): + # check for every window if scaling value changed + # (not implemented yet) + # find an existing tkinter object for the next call of .after() for root_tk in cls.window_widgets_dict.keys(): try: diff --git a/customtkinter/widgets/ctk_button.py b/customtkinter/widgets/ctk_button.py index 342e333..0b5f141 100644 --- a/customtkinter/widgets/ctk_button.py +++ b/customtkinter/widgets/ctk_button.py @@ -3,7 +3,7 @@ import sys import math from .ctk_canvas import CTkCanvas -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_settings import CTkSettings from ..ctk_draw_engine import CTkDrawEngine from .widget_base_class import CTkBaseClass diff --git a/customtkinter/widgets/ctk_checkbox.py b/customtkinter/widgets/ctk_checkbox.py index b0ae737..4661e71 100644 --- a/customtkinter/widgets/ctk_checkbox.py +++ b/customtkinter/widgets/ctk_checkbox.py @@ -2,7 +2,7 @@ import tkinter import sys from .ctk_canvas import CTkCanvas -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_settings import CTkSettings from ..ctk_draw_engine import CTkDrawEngine from .widget_base_class import CTkBaseClass @@ -325,7 +325,10 @@ class CTkCheckBox(CTkBaseClass): self.variable_callback_blocked = False if self.function is not None: - self.function() + try: + self.function() + except: + pass def deselect(self, from_variable_callback=False): self.check_state = False @@ -337,7 +340,10 @@ class CTkCheckBox(CTkBaseClass): self.variable_callback_blocked = False if self.function is not None: - self.function() + try: + self.function() + except: + pass def get(self): return self.onvalue if self.check_state is True else self.offvalue diff --git a/customtkinter/widgets/ctk_entry.py b/customtkinter/widgets/ctk_entry.py index 6d3b89d..0efa054 100644 --- a/customtkinter/widgets/ctk_entry.py +++ b/customtkinter/widgets/ctk_entry.py @@ -1,7 +1,7 @@ import tkinter from .ctk_canvas import CTkCanvas -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_settings import CTkSettings from ..ctk_draw_engine import CTkDrawEngine from .widget_base_class import CTkBaseClass diff --git a/customtkinter/widgets/ctk_frame.py b/customtkinter/widgets/ctk_frame.py index 8b38a43..5c6a20a 100644 --- a/customtkinter/widgets/ctk_frame.py +++ b/customtkinter/widgets/ctk_frame.py @@ -1,7 +1,7 @@ import tkinter from .ctk_canvas import CTkCanvas -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_settings import CTkSettings from ..ctk_draw_engine import CTkDrawEngine from .widget_base_class import CTkBaseClass diff --git a/customtkinter/widgets/ctk_label.py b/customtkinter/widgets/ctk_label.py index 273112d..002de7c 100644 --- a/customtkinter/widgets/ctk_label.py +++ b/customtkinter/widgets/ctk_label.py @@ -1,7 +1,7 @@ import tkinter from .ctk_canvas import CTkCanvas -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_settings import CTkSettings from ..ctk_draw_engine import CTkDrawEngine from .widget_base_class import CTkBaseClass diff --git a/customtkinter/widgets/ctk_progressbar.py b/customtkinter/widgets/ctk_progressbar.py index 28fd039..1fdb6f3 100644 --- a/customtkinter/widgets/ctk_progressbar.py +++ b/customtkinter/widgets/ctk_progressbar.py @@ -1,7 +1,7 @@ import tkinter from .ctk_canvas import CTkCanvas -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_draw_engine import CTkDrawEngine from ..ctk_settings import CTkSettings from .widget_base_class import CTkBaseClass @@ -17,11 +17,24 @@ class CTkProgressBar(CTkBaseClass): fg_color="default_theme", progress_color="default_theme", corner_radius="default_theme", - width=200, - height=8, + width=None, + height=None, border_width="default_theme", + orient="horizontal", **kwargs): + # set default dimensions according to orientation + if width is None: + if orient.lower() == "vertical": + width = 8 + else: + width = 200 + if height is None: + if orient.lower() == "vertical": + height = 200 + else: + height = 8 + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) @@ -39,6 +52,7 @@ class CTkProgressBar(CTkBaseClass): self.corner_radius = CTkThemeManager.theme["shape"]["progressbar_corner_radius"] if corner_radius == "default_theme" else corner_radius self.border_width = CTkThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width self.value = 0.5 + self.orient = orient self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) @@ -74,11 +88,19 @@ class CTkProgressBar(CTkBaseClass): super().destroy() def draw(self, no_color_updates=False): + if self.orient.lower() == "horizontal": + orientation = "w" + elif self.orient.lower() == "vertical": + orientation = "s" + else: + orientation = "w" + requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.apply_widget_scaling(self.current_width), self.apply_widget_scaling(self.current_height), self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width), - self.value, "w") + self.value, + orientation) if no_color_updates is False or requires_recoloring: self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) diff --git a/customtkinter/widgets/ctk_radiobutton.py b/customtkinter/widgets/ctk_radiobutton.py index 5626803..5aa9b1d 100644 --- a/customtkinter/widgets/ctk_radiobutton.py +++ b/customtkinter/widgets/ctk_radiobutton.py @@ -2,7 +2,7 @@ import tkinter import sys from .ctk_canvas import CTkCanvas -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_settings import CTkSettings from ..ctk_draw_engine import CTkDrawEngine from .widget_base_class import CTkBaseClass diff --git a/customtkinter/widgets/ctk_slider.py b/customtkinter/widgets/ctk_slider.py index 90a64f9..b841d0b 100644 --- a/customtkinter/widgets/ctk_slider.py +++ b/customtkinter/widgets/ctk_slider.py @@ -2,7 +2,7 @@ import tkinter import sys from .ctk_canvas import CTkCanvas -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_settings import CTkSettings from ..ctk_draw_engine import CTkDrawEngine from .widget_base_class import CTkBaseClass @@ -21,16 +21,29 @@ class CTkSlider(CTkBaseClass): from_=0, to=1, number_of_steps=None, - width=160, - height=16, + width=None, + height=None, corner_radius="default_theme", button_corner_radius="default_theme", border_width="default_theme", button_length="default_theme", command=None, variable=None, + orient="horizontal", **kwargs): + # set default dimensions according to orientation + if width is None: + if orient.lower() == "vertical": + width = 16 + else: + width = 200 + if height is None: + if orient.lower() == "vertical": + height = 200 + else: + height = 16 + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) @@ -47,6 +60,7 @@ class CTkSlider(CTkBaseClass): self.border_width = CTkThemeManager.theme["shape"]["slider_border_width"] if border_width == "default_theme" else border_width self.button_length = CTkThemeManager.theme["shape"]["slider_button_length"] if button_length == "default_theme" else button_length self.value = 0.5 # initial value of slider in percent + self.orient = orient self.hover_state = False self.from_ = from_ self.to = to @@ -110,13 +124,20 @@ class CTkSlider(CTkBaseClass): self.configure(cursor="hand2") def draw(self, no_color_updates=False): + if self.orient.lower() == "horizontal": + orientation = "w" + elif self.orient.lower() == "vertical": + orientation = "s" + else: + orientation = "w" + requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self.current_width), self.apply_widget_scaling(self.current_height), self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width), self.apply_widget_scaling(self.button_length), self.apply_widget_scaling(self.button_corner_radius), - self.value, "w") + self.value, orientation) if no_color_updates is False or requires_recoloring: self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) @@ -142,7 +163,10 @@ class CTkSlider(CTkBaseClass): outline=CTkThemeManager.single_color(self.button_color, self.appearance_mode)) def clicked(self, event=None): - self.value = (event.x / self.current_width) / self.widget_scaling + if self.orient.lower() == "horizontal": + self.value = (event.x / self.current_width) / self.widget_scaling + else: + self.value = 1 - (event.y / self.current_height) / self.widget_scaling if self.value > 1: self.value = 1 diff --git a/customtkinter/widgets/ctk_switch.py b/customtkinter/widgets/ctk_switch.py index d08671d..c08da9c 100644 --- a/customtkinter/widgets/ctk_switch.py +++ b/customtkinter/widgets/ctk_switch.py @@ -2,7 +2,7 @@ import tkinter import sys from .ctk_canvas import CTkCanvas -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_settings import CTkSettings from ..ctk_draw_engine import CTkDrawEngine from .widget_base_class import CTkBaseClass diff --git a/customtkinter/widgets/widget_base_class.py b/customtkinter/widgets/widget_base_class.py index 36f7e2b..49baa72 100644 --- a/customtkinter/widgets/widget_base_class.py +++ b/customtkinter/widgets/widget_base_class.py @@ -9,7 +9,7 @@ from ..windows.ctk_tk import CTk from ..windows.ctk_toplevel import CTkToplevel from ..appearance_mode_tracker import AppearanceModeTracker from ..scaling_tracker import ScalingTracker -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager class CTkBaseClass(tkinter.Frame): diff --git a/customtkinter/windows/ctk_input_dialog.py b/customtkinter/windows/ctk_input_dialog.py index e17b27d..9e674b7 100644 --- a/customtkinter/windows/ctk_input_dialog.py +++ b/customtkinter/windows/ctk_input_dialog.py @@ -7,7 +7,7 @@ from ..widgets.ctk_frame import CTkFrame from ..windows.ctk_toplevel import CTkToplevel from ..widgets.ctk_button import CTkButton from ..appearance_mode_tracker import AppearanceModeTracker -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager class CTkInputDialog: diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index b1f3eff..21603f7 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -8,7 +8,7 @@ import re from typing import Union, Tuple from ..appearance_mode_tracker import AppearanceModeTracker -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..scaling_tracker import ScalingTracker from ..ctk_settings import CTkSettings @@ -33,10 +33,10 @@ class CTk(tkinter.Tk): self.current_width = 600 # initial window size, always without scaling self.current_height = 500 - self.min_width: Union[int, None] = None - self.min_height: Union[int, None] = None - self.max_width: Union[int, None] = None - self.max_height: Union[int, None] = None + self.min_width: int = 0 + self.min_height: int = 0 + self.max_width: int = 1_000_000 + self.max_height: int = 1_000_000 self.last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs) self.fg_color = CTkThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color @@ -86,7 +86,7 @@ class CTk(tkinter.Tk): # set scaled min, max sizes and reapply resizable if self.last_resizable_args is not None: - super().resizable(self.last_resizable_args[0], self.last_resizable_args[1]) # args, kwargs + super().resizable(*self.last_resizable_args[0], **self.last_resizable_args[1]) # args, kwargs if self.min_width is not None or self.min_height is not None: super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height)) if self.max_width is not None or self.max_height is not None: @@ -94,6 +94,7 @@ class CTk(tkinter.Tk): def destroy(self): AppearanceModeTracker.remove(self.set_appearance_mode) + ScalingTracker.remove_window(self.set_scaling, self) self.disable_macos_dark_title_bar() super().destroy() @@ -122,11 +123,15 @@ class CTk(tkinter.Tk): def minsize(self, width=None, height=None): self.min_width = width self.min_height = height + if self.current_width < width: self.current_width = width + if self.current_height < height: self.current_height = height super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height)) def maxsize(self, width=None, height=None): self.max_width = width self.max_height = height + if self.current_width > width: self.current_width = width + if self.current_height > height: self.current_height = height super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height)) def geometry(self, geometry_string): @@ -134,7 +139,8 @@ class CTk(tkinter.Tk): # update width and height attributes numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers - self.current_width, self.current_height = numbers[0], numbers[1] + self.current_width = max(self.min_width, min(numbers[0], self.max_width)) # bound value between min and max + self.current_height = max(self.min_height, min(numbers[1], self.max_height)) def apply_geometry_scaling(self, geometry_string): numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 8fdb0a4..e8ee442 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -5,9 +5,10 @@ import os import platform import ctypes import re +from typing import Union, Tuple from ..appearance_mode_tracker import AppearanceModeTracker -from ..theme_manager import CTkThemeManager +from ..ctk_theme_manager import CTkThemeManager from ..ctk_settings import CTkSettings from ..scaling_tracker import ScalingTracker @@ -27,6 +28,11 @@ class CTkToplevel(tkinter.Toplevel): self.current_width = 600 # initial window size, always without scaling self.current_height = 500 + self.min_width: int = 0 + self.min_height: int = 0 + self.max_width: int = 1_000_000 + self.max_height: int = 1_000_000 + self.last_resizable_args: Union[Tuple[list, dict], None] = None # (args, kwargs) self.fg_color = CTkThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color @@ -59,9 +65,25 @@ class CTkToplevel(tkinter.Toplevel): def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): self.window_scaling = new_window_scaling + # reset min, max and resizable constraints for applying scaling + if self.last_resizable_args is not None: + super().resizable(True, True) + if self.min_width is not None or self.min_height is not None: + super().minsize(0, 0) + if self.max_width is not None or self.max_height is not None: + super().maxsize(1_000_000, 1_000_000) + # set new window size by applying scaling to the current window size self.geometry(f"{self.current_width}x{self.current_height}") + # set scaled min, max sizes and reapply resizable + if self.last_resizable_args is not None: + super().resizable(*self.last_resizable_args[0], **self.last_resizable_args[1]) # args, kwargs + if self.min_width is not None or self.min_height is not None: + super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height)) + if self.max_width is not None or self.max_height is not None: + super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height)) + def apply_geometry_scaling(self, geometry_string): numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers @@ -78,7 +100,7 @@ class CTkToplevel(tkinter.Toplevel): def apply_window_scaling(self, value): if isinstance(value, (int, float)): - return value * self.window_scaling + return int(value * self.window_scaling) else: return value @@ -87,15 +109,18 @@ class CTkToplevel(tkinter.Toplevel): # update width and height attributes numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers - self.current_width, self.current_height = numbers[0], numbers[1] + self.current_width = max(self.min_width, min(numbers[0], self.max_width)) # bound value between min and max + self.current_height = max(self.min_height, min(numbers[1], self.max_height)) def destroy(self): AppearanceModeTracker.remove(self.set_appearance_mode) + ScalingTracker.remove_window(self.set_scaling, self) self.disable_macos_dark_title_bar() super().destroy() def resizable(self, *args, **kwargs): super().resizable(*args, **kwargs) + self.last_resizable_args = (args, kwargs) if sys.platform.startswith("win"): if self.appearance_mode == 1: @@ -103,6 +128,20 @@ class CTkToplevel(tkinter.Toplevel): else: self.windows_set_titlebar_color("light") + def minsize(self, width=None, height=None): + self.min_width = width + self.min_height = height + if self.current_width < width: self.current_width = width + if self.current_height < height: self.current_height = height + super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height)) + + def maxsize(self, width=None, height=None): + self.max_width = width + self.max_height = height + if self.current_width > width: self.current_width = width + if self.current_height > height: self.current_height = height + super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height)) + def config(self, *args, **kwargs): self.configure(*args, **kwargs) diff --git a/documentation_images/Ubuntu_dark.png b/documentation_images/Ubuntu_dark.png new file mode 100644 index 0000000..d92be28 Binary files /dev/null and b/documentation_images/Ubuntu_dark.png differ diff --git a/documentation_images/appearance_mode_switch.gif b/documentation_images/appearance_mode_switch.gif deleted file mode 100644 index fcc2101..0000000 Binary files a/documentation_images/appearance_mode_switch.gif and /dev/null differ diff --git a/test/visual_tests/test_all_widgets_with_colors.py b/test/manual_integration_tests/test_all_widgets_with_colors.py similarity index 100% rename from test/visual_tests/test_all_widgets_with_colors.py rename to test/manual_integration_tests/test_all_widgets_with_colors.py diff --git a/test/visual_tests/test_askdialog.py b/test/manual_integration_tests/test_askdialog.py similarity index 100% rename from test/visual_tests/test_askdialog.py rename to test/manual_integration_tests/test_askdialog.py diff --git a/test/visual_tests/test_auto_resizing.py b/test/manual_integration_tests/test_auto_resizing.py similarity index 100% rename from test/visual_tests/test_auto_resizing.py rename to test/manual_integration_tests/test_auto_resizing.py diff --git a/test/visual_tests/test_button_antialiasing.py b/test/manual_integration_tests/test_button_antialiasing.py similarity index 100% rename from test/visual_tests/test_button_antialiasing.py rename to test/manual_integration_tests/test_button_antialiasing.py diff --git a/test/visual_tests/test_button_state.py b/test/manual_integration_tests/test_button_state.py similarity index 100% rename from test/visual_tests/test_button_state.py rename to test/manual_integration_tests/test_button_state.py diff --git a/test/visual_tests/test_ctk_toplevel.py b/test/manual_integration_tests/test_ctk_toplevel.py similarity index 100% rename from test/visual_tests/test_ctk_toplevel.py rename to test/manual_integration_tests/test_ctk_toplevel.py diff --git a/test/visual_tests/test_iconify_destroy.py b/test/manual_integration_tests/test_iconify_destroy.py similarity index 100% rename from test/visual_tests/test_iconify_destroy.py rename to test/manual_integration_tests/test_iconify_destroy.py diff --git a/test/visual_tests/test_images/bell.png b/test/manual_integration_tests/test_images/bell.png similarity index 100% rename from test/visual_tests/test_images/bell.png rename to test/manual_integration_tests/test_images/bell.png diff --git a/test/visual_tests/test_images/bg_gradient.jpg b/test/manual_integration_tests/test_images/bg_gradient.jpg similarity index 100% rename from test/visual_tests/test_images/bg_gradient.jpg rename to test/manual_integration_tests/test_images/bg_gradient.jpg diff --git a/test/visual_tests/test_images/settings.png b/test/manual_integration_tests/test_images/settings.png similarity index 100% rename from test/visual_tests/test_images/settings.png rename to test/manual_integration_tests/test_images/settings.png diff --git a/test/visual_tests/test_new_menu_design.py b/test/manual_integration_tests/test_new_menu_design.py similarity index 100% rename from test/visual_tests/test_new_menu_design.py rename to test/manual_integration_tests/test_new_menu_design.py diff --git a/test/visual_tests/test_scaling/complex_example.py b/test/manual_integration_tests/test_scaling/complex_example.py similarity index 100% rename from test/visual_tests/test_scaling/complex_example.py rename to test/manual_integration_tests/test_scaling/complex_example.py diff --git a/test/visual_tests/test_scaling/example_background_image.py b/test/manual_integration_tests/test_scaling/example_background_image.py similarity index 100% rename from test/visual_tests/test_scaling/example_background_image.py rename to test/manual_integration_tests/test_scaling/example_background_image.py diff --git a/test/visual_tests/test_scaling/simple_example.py b/test/manual_integration_tests/test_scaling/simple_example.py similarity index 100% rename from test/visual_tests/test_scaling/simple_example.py rename to test/manual_integration_tests/test_scaling/simple_example.py diff --git a/test/visual_tests/test_simple_font_circle.py b/test/manual_integration_tests/test_simple_font_circle.py similarity index 100% rename from test/visual_tests/test_simple_font_circle.py rename to test/manual_integration_tests/test_simple_font_circle.py diff --git a/test/visual_tests/test_string_dialog.py b/test/manual_integration_tests/test_string_dialog.py similarity index 100% rename from test/visual_tests/test_string_dialog.py rename to test/manual_integration_tests/test_string_dialog.py diff --git a/test/visual_tests/test_tk_variables.py b/test/manual_integration_tests/test_tk_variables.py similarity index 100% rename from test/visual_tests/test_tk_variables.py rename to test/manual_integration_tests/test_tk_variables.py diff --git a/test/visual_tests/test_ttk_frames.py b/test/manual_integration_tests/test_ttk_frames.py similarity index 100% rename from test/visual_tests/test_ttk_frames.py rename to test/manual_integration_tests/test_ttk_frames.py diff --git a/test/manual_integration_tests/test_vertical_widgets.py b/test/manual_integration_tests/test_vertical_widgets.py new file mode 100644 index 0000000..775419c --- /dev/null +++ b/test/manual_integration_tests/test_vertical_widgets.py @@ -0,0 +1,24 @@ +import customtkinter + +root_tk = customtkinter.CTk() # create CTk window like you do with the Tk window (you can also use normal tkinter.Tk window) +root_tk.geometry("400x650") +root_tk.title("test_vertical_widgets") + +root_tk.grid_columnconfigure(0, weight=1) +root_tk.grid_rowconfigure((0, 1, 2, 3), weight=1) + +progressbar_1 = customtkinter.CTkProgressBar(root_tk, orient="horizontal") +progressbar_1.grid(row=0, column=0, pady=20, padx=20) + +progressbar_2 = customtkinter.CTkProgressBar(root_tk, orient="vertical") +progressbar_2.grid(row=1, column=0, pady=20, padx=20) + +slider_1 = customtkinter.CTkSlider(root_tk, orient="horizontal", command=progressbar_1.set) +slider_1.grid(row=2, column=0, pady=20, padx=20) + +slider_2 = customtkinter.CTkSlider(root_tk, orient="vertical", command=progressbar_2.set, + button_corner_radius=2, button_length=20) +slider_2.grid(row=3, column=0, pady=20, padx=20) + + +root_tk.mainloop() diff --git a/test/unit_tests/__init__.py b/test/unit_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit_tests/test_all.py b/test/unit_tests/test_all.py new file mode 100644 index 0000000..d1874be --- /dev/null +++ b/test/unit_tests/test_all.py @@ -0,0 +1,7 @@ +from test_ctk import TestCTk +from test_ctk_toplevel import TestCTkToplevel +from test_ctk_button import TestCTkButton + +TestCTk().main() +TestCTkToplevel().main() +TestCTkButton().main() diff --git a/test/unit_tests/test_ctk.py b/test/unit_tests/test_ctk.py new file mode 100644 index 0000000..16ef894 --- /dev/null +++ b/test/unit_tests/test_ctk.py @@ -0,0 +1,112 @@ +import time +import customtkinter + + +class TestCTk(): + def __init__(self): + self.root_ctk = customtkinter.CTk() + self.root_ctk.title("TestCTk") + + def clean(self): + self.root_ctk.quit() + self.root_ctk.withdraw() + + def main(self): + self.execute_tests() + self.root_ctk.mainloop() + + def execute_tests(self): + print("\nTestCTk started:") + start_time = 0 + + self.root_ctk.after(start_time, self.test_geometry) + start_time += 100 + + self.root_ctk.after(start_time, self.test_scaling) + start_time += 100 + + self.root_ctk.after(start_time, self.test_configure) + start_time += 100 + + self.root_ctk.after(start_time, self.test_appearance_mode) + start_time += 100 + + self.root_ctk.after(start_time, self.test_iconify) + start_time += 1500 + + self.root_ctk.after(start_time, self.clean) + + def test_geometry(self): + print(" -> test_geometry: ", end="") + self.root_ctk.geometry("100x200+200+300") + assert self.root_ctk.current_width == 100 and self.root_ctk.current_height == 200 + + self.root_ctk.minsize(300, 400) + assert self.root_ctk.current_width == 300 and self.root_ctk.current_height == 400 + assert self.root_ctk.min_width == 300 and self.root_ctk.min_height == 400 + + self.root_ctk.maxsize(400, 500) + self.root_ctk.geometry("600x600") + assert self.root_ctk.current_width == 400 and self.root_ctk.current_height == 500 + assert self.root_ctk.max_width == 400 and self.root_ctk.max_height == 500 + + self.root_ctk.maxsize(1000, 1000) + self.root_ctk.geometry("300x400") + self.root_ctk.resizable(False, False) + self.root_ctk.geometry("500x600") + assert self.root_ctk.current_width == 500 and self.root_ctk.current_height == 600 + print("successful") + + def test_scaling(self): + print(" -> test_scaling: ", end="") + + customtkinter.ScalingTracker.set_window_scaling(1.5) + self.root_ctk.geometry("300x400") + self.root_ctk.update() + assert self.root_ctk.current_width == 300 and self.root_ctk.current_height == 400 + assert round(self.root_ctk.winfo_width()) == 450 and round(self.root_ctk.winfo_height()) == 600 + + self.root_ctk.maxsize(400, 500) + self.root_ctk.geometry("500x500") + self.root_ctk.update() + assert self.root_ctk.current_width == 400 and self.root_ctk.current_height == 500 + assert round(self.root_ctk.winfo_width()) == 600 and round(self.root_ctk.winfo_height()) == 750 + + customtkinter.ScalingTracker.set_window_scaling(1) + self.root_ctk.update() + assert self.root_ctk.current_width == 400 and self.root_ctk.current_height == 500 + assert round(self.root_ctk.winfo_width()) == 400 and round(self.root_ctk.winfo_height()) == 500 + print("successful") + + def test_configure(self): + print(" -> test_configure: ", end="") + self.root_ctk.configure(bg="white") + assert self.root_ctk.fg_color == "white" + + self.root_ctk.configure(background="red") + assert self.root_ctk.fg_color == "red" + assert self.root_ctk.cget("bg") == "red" + + self.root_ctk.config(fg_color=("green", "#FFFFFF")) + assert self.root_ctk.fg_color == ("green", "#FFFFFF") + print("successful") + + def test_appearance_mode(self): + print(" -> test_appearance_mode: ", end="") + customtkinter.set_appearance_mode("light") + self.root_ctk.config(fg_color=("green", "#FFFFFF")) + assert self.root_ctk.cget("bg") == "green" + + customtkinter.set_appearance_mode("dark") + assert self.root_ctk.cget("bg") == "#FFFFFF" + print("successful") + + def test_iconify(self): + print(" -> test_iconify: ", end="") + self.root_ctk.iconify() + self.root_ctk.after(100, self.root_ctk.deiconify) + print("successful") + + +if __name__ == "__main__": + TestCTk().main() diff --git a/test/unit_tests/test_ctk_button.py b/test/unit_tests/test_ctk_button.py new file mode 100644 index 0000000..b126d76 --- /dev/null +++ b/test/unit_tests/test_ctk_button.py @@ -0,0 +1,38 @@ +import time +import customtkinter + + +class TestCTkButton(): + def __init__(self): + self.root_ctk = customtkinter.CTk() + self.ctk_button = customtkinter.CTkButton(self.root_ctk) + self.ctk_button.pack(padx=20, pady=20) + self.root_ctk.title(self.__class__.__name__) + + def clean(self): + self.root_ctk.quit() + self.root_ctk.withdraw() + + def main(self): + self.execute_tests() + self.root_ctk.mainloop() + + def execute_tests(self): + print(f"\n{self.__class__.__name__} started:") + + start_time = 0 + + self.root_ctk.after(start_time, self.test_iconify) + start_time += 1500 + + self.root_ctk.after(start_time, self.clean) + + def test_iconify(self): + print(" -> test_iconify: ", end="") + self.root_ctk.iconify() + self.root_ctk.after(100, self.root_ctk.deiconify) + print("successful") + + +if __name__ == "__main__": + TestCTkButton().main() diff --git a/test/unit_tests/test_ctk_toplevel.py b/test/unit_tests/test_ctk_toplevel.py new file mode 100644 index 0000000..877e726 --- /dev/null +++ b/test/unit_tests/test_ctk_toplevel.py @@ -0,0 +1,114 @@ +import customtkinter + + +class TestCTkToplevel(): + def __init__(self): + self.root_ctk = customtkinter.CTk() + self.root_ctk.title("TestCTkToplevel") + self.ctk_toplevel = customtkinter.CTkToplevel() + self.ctk_toplevel.title("TestCTkToplevel") + + def clean(self): + self.root_ctk.quit() + self.ctk_toplevel.withdraw() + self.root_ctk.withdraw() + + def main(self): + self.execute_tests() + self.root_ctk.mainloop() + + def execute_tests(self): + print("\nTestCTkToplevel started:") + start_time = 0 + + self.root_ctk.after(start_time, self.test_geometry) + start_time += 100 + + self.root_ctk.after(start_time, self.test_scaling) + start_time += 100 + + self.root_ctk.after(start_time, self.test_configure) + start_time += 100 + + self.root_ctk.after(start_time, self.test_appearance_mode) + start_time += 100 + + self.root_ctk.after(start_time, self.test_iconify) + start_time += 1500 + + self.root_ctk.after(start_time, self.clean) + + def test_geometry(self): + print(" -> test_geometry: ", end="") + self.ctk_toplevel.geometry("200x300+200+300") + assert self.ctk_toplevel.current_width == 200 and self.ctk_toplevel.current_height == 300 + + self.ctk_toplevel.minsize(300, 400) + assert self.ctk_toplevel.current_width == 300 and self.ctk_toplevel.current_height == 400 + assert self.ctk_toplevel.min_width == 300 and self.ctk_toplevel.min_height == 400 + + self.ctk_toplevel.maxsize(400, 500) + self.ctk_toplevel.geometry("600x600") + assert self.ctk_toplevel.current_width == 400 and self.ctk_toplevel.current_height == 500 + assert self.ctk_toplevel.max_width == 400 and self.ctk_toplevel.max_height == 500 + + self.ctk_toplevel.maxsize(1000, 1000) + self.ctk_toplevel.geometry("300x400") + self.ctk_toplevel.resizable(False, False) + self.ctk_toplevel.geometry("500x600") + assert self.ctk_toplevel.current_width == 500 and self.ctk_toplevel.current_height == 600 + print("successful") + + def test_scaling(self): + print(" -> test_scaling: ", end="") + + customtkinter.ScalingTracker.set_window_scaling(1.5) + self.ctk_toplevel.geometry("300x400") + self.ctk_toplevel.update() + assert self.ctk_toplevel.current_width == 300 and self.ctk_toplevel.current_height == 400 + assert round(self.ctk_toplevel.winfo_width()) == 450 and round(self.ctk_toplevel.winfo_height()) == 600 + + self.ctk_toplevel.maxsize(400, 500) + self.ctk_toplevel.geometry("500x500") + self.ctk_toplevel.update() + assert self.ctk_toplevel.current_width == 400 and self.ctk_toplevel.current_height == 500 + assert round(self.ctk_toplevel.winfo_width()) == 600 and round(self.ctk_toplevel.winfo_height()) == 750 + + customtkinter.ScalingTracker.set_window_scaling(1) + self.ctk_toplevel.update() + assert self.ctk_toplevel.current_width == 400 and self.ctk_toplevel.current_height == 500 + assert round(self.ctk_toplevel.winfo_width()) == 400 and round(self.ctk_toplevel.winfo_height()) == 500 + print("successful") + + def test_configure(self): + print(" -> test_configure: ", end="") + self.ctk_toplevel.configure(bg="white") + assert self.ctk_toplevel.fg_color == "white" + + self.ctk_toplevel.configure(background="red") + assert self.ctk_toplevel.fg_color == "red" + assert self.ctk_toplevel.cget("bg") == "red" + + self.ctk_toplevel.config(fg_color=("green", "#FFFFFF")) + assert self.ctk_toplevel.fg_color == ("green", "#FFFFFF") + print("successful") + + def test_appearance_mode(self): + print(" -> test_appearance_mode: ", end="") + customtkinter.set_appearance_mode("light") + self.ctk_toplevel.config(fg_color=("green", "#FFFFFF")) + assert self.ctk_toplevel.cget("bg") == "green" + + customtkinter.set_appearance_mode("dark") + assert self.ctk_toplevel.cget("bg") == "#FFFFFF" + print("successful") + + def test_iconify(self): + print(" -> test_iconify: ", end="") + self.ctk_toplevel.iconify() + self.ctk_toplevel.after(100, self.ctk_toplevel.deiconify) + print("successful") + + +if __name__ == "__main__": + TestCTkToplevel()