diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e6536f..ed71059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Changelog All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -*This document is a WIP.* +## [4.0.0] - 2022-05-22 +### Added + - This changelog file + - Adopted semantic versioning + - Added HighDPI scaling to all widgets and geometry managers (place, pack, grid) + - Restructured CTkSettings and renamed a few manager classes + +### Changed + +### Removed + - A few unnecessary tests diff --git a/customtkinter/widgets/ctk_switch.py b/customtkinter/widgets/ctk_switch.py index a6ce372..648d80b 100644 --- a/customtkinter/widgets/ctk_switch.py +++ b/customtkinter/widgets/ctk_switch.py @@ -13,6 +13,7 @@ class CTkSwitch(CTkBaseClass): text="CTkSwitch", text_font="default_theme", text_color="default_theme", + text_color_disabled="default_theme", bg_color=None, border_color=None, fg_color="default_theme", @@ -30,6 +31,7 @@ class CTkSwitch(CTkBaseClass): offvalue=0, variable=None, textvariable=None, + state=tkinter.NORMAL, **kwargs): # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass @@ -42,6 +44,7 @@ class CTkSwitch(CTkBaseClass): self.button_color = ThemeManager.theme["color"]["switch_button"] if button_color == "default_theme" else button_color self.button_hover_color = ThemeManager.theme["color"]["switch_button_hover"] if button_hover_color == "default_theme" else button_hover_color self.text_color = ThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color + self.text_color_disabled = ThemeManager.theme["color"]["text_button_disabled"] if text_color_disabled == "default_theme" else text_color_disabled # text self.text = text @@ -55,6 +58,7 @@ class CTkSwitch(CTkBaseClass): self.button_length = ThemeManager.theme["shape"]["switch_button_length"] if button_length == "default_theme" else button_length self.hover_state = False self.check_state = False # True if switch is activated + self.state = state self.onvalue = onvalue self.offvalue = offvalue @@ -119,10 +123,16 @@ class CTkSwitch(CTkBaseClass): def set_cursor(self): if Settings.cursor_manipulation_enabled: - if sys.platform == "darwin" and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled: - self.canvas.configure(cursor="hand2") + if self.state == tkinter.DISABLED: + if sys.platform == "darwin" and Settings.cursor_manipulation_enabled: + self.canvas.configure(cursor="arrow") + elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled: + self.canvas.configure(cursor="arrow") + else: + if sys.platform == "darwin" and Settings.cursor_manipulation_enabled: + self.canvas.configure(cursor="pointinghand") + elif sys.platform.startswith("win") and Settings.cursor_manipulation_enabled: + self.canvas.configure(cursor="hand2") def draw(self, no_color_updates=False): @@ -178,7 +188,11 @@ class CTkSwitch(CTkBaseClass): if self.textvariable is not None: self.text_label.configure(textvariable=self.textvariable) - self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode)) + if self.state == tkinter.DISABLED: + self.text_label.configure(fg=(ThemeManager.single_color(self.text_color_disabled, self.appearance_mode))) + else: + self.text_label.configure(fg=ThemeManager.single_color(self.text_color, self.appearance_mode)) + self.text_label.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode)) self.set_text(self.text) @@ -189,54 +203,59 @@ class CTkSwitch(CTkBaseClass): self.text_label.configure(text=self.text) def toggle(self, event=None): - if self.check_state is True: - self.check_state = False - else: - self.check_state = True + 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) + self.draw(no_color_updates=True) - if self.callback_function is not None: - self.callback_function() + if self.callback_function is not None: + self.callback_function() - if self.variable is not None: - self.variable_callback_blocked = True - self.variable.set(self.onvalue if self.check_state is True else self.offvalue) - self.variable_callback_blocked = False + if self.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 select(self, from_variable_callback=False): - self.check_state = True + if self.state is not tkinter.DISABLED or from_variable_callback: + self.check_state = True - self.draw(no_color_updates=True) + self.draw(no_color_updates=True) - if self.callback_function is not None: - self.callback_function() + if self.callback_function is not None: + self.callback_function() - 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 + if self.variable is not None and not from_variable_callback: + self.variable_callback_blocked = True + self.variable.set(self.onvalue) + self.variable_callback_blocked = False def deselect(self, from_variable_callback=False): - self.check_state = False + if self.state is not tkinter.DISABLED or from_variable_callback: + self.check_state = False - self.draw(no_color_updates=True) + self.draw(no_color_updates=True) - if self.callback_function is not None: - self.callback_function() + if self.callback_function is not None: + self.callback_function() - 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 + if self.variable is not None and not from_variable_callback: + self.variable_callback_blocked = True + self.variable.set(self.offvalue) + self.variable_callback_blocked = False def get(self): return self.onvalue if self.check_state is True else self.offvalue def on_enter(self, event=0): self.hover_state = True - self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_hover_color, self.appearance_mode), - outline=ThemeManager.single_color(self.button_hover_color, self.appearance_mode)) + + if self.state is not tkinter.DISABLED: + self.canvas.itemconfig("slider_parts", fill=ThemeManager.single_color(self.button_hover_color, self.appearance_mode), + outline=ThemeManager.single_color(self.button_hover_color, self.appearance_mode)) def on_leave(self, event=0): self.hover_state = False @@ -253,6 +272,12 @@ class CTkSwitch(CTkBaseClass): def configure(self, *args, **kwargs): require_redraw = False # some attribute changes require a call of self.draw() at the end + if "state" in kwargs: + self.state = kwargs["state"] + self.set_cursor() + require_redraw = True + del kwargs["state"] + if "fg_color" in kwargs: self.fg_color = kwargs["fg_color"] require_redraw = True diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index e7e1ecc..bb63641 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -69,7 +69,6 @@ class CTk(tkinter.Tk): if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling): self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event self.current_height = round(detected_height / self.window_scaling) # current_width and current_height are independent of the scale - print("update_dimensions_event:", self.current_width) def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): self.window_scaling = new_window_scaling @@ -78,7 +77,6 @@ class CTk(tkinter.Tk): super().minsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height)) super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height)) super().geometry(f"{self.apply_window_scaling(self.current_width)}x"+f"{self.apply_window_scaling(self.current_height)}") - print("set_scaling:", self.apply_window_scaling(self.current_width), self.max_width, self.min_width) # set new scaled min and max with 400ms delay (otherwise it won't work for some reason) self.after(400, self.set_scaled_min_max) @@ -103,7 +101,6 @@ class CTk(tkinter.Tk): def mainloop(self, *args, **kwargs): if not self.window_exists: - print("deiconify") self.deiconify() self.window_exists = True super().mainloop(*args, **kwargs) @@ -133,7 +130,6 @@ class CTk(tkinter.Tk): super().maxsize(self.apply_window_scaling(self.max_width), self.apply_window_scaling(self.max_height)) def geometry(self, geometry_string): - print("geometry:", geometry_string) super().geometry(self.apply_geometry_scaling(geometry_string)) # update width and height attributes diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index a8954eb..a47c8e2 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -73,7 +73,6 @@ class CTkToplevel(tkinter.Toplevel): super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height)) super().geometry( f"{self.apply_window_scaling(self.current_width)}x" + f"{self.apply_window_scaling(self.current_height)}") - print("set_scaling:", self.apply_window_scaling(self.current_width), self.max_width, self.min_width) # set new scaled min and max with 400ms delay (otherwise it won't work for some reason) self.after(400, self.set_scaled_min_max) diff --git a/documentation_images/Windows_scaling.png b/documentation_images/Windows_scaling.png index 58a7975..cf2737b 100755 Binary files a/documentation_images/Windows_scaling.png and b/documentation_images/Windows_scaling.png differ diff --git a/test/manual_integration_tests/test_askdialog.py b/test/manual_integration_tests/test_askdialog.py index aa4cb2d..c82512c 100644 --- a/test/manual_integration_tests/test_askdialog.py +++ b/test/manual_integration_tests/test_askdialog.py @@ -32,7 +32,6 @@ class App(customtkinter.CTk): height=App.HEIGHT-40, corner_radius=5) self.frame_left.place(relx=0.38, rely=0.5, anchor=tkinter.E) - print(self.frame_left.widget_scaling) self.frame_right = customtkinter.CTkFrame(master=self, width=350, @@ -65,4 +64,4 @@ class App(customtkinter.CTk): if __name__ == "__main__": app = App() - app.start() \ No newline at end of file + app.start() diff --git a/test/manual_integration_tests/test_button_state.py b/test/manual_integration_tests/test_button_state.py deleted file mode 100644 index cf45c62..0000000 --- a/test/manual_integration_tests/test_button_state.py +++ /dev/null @@ -1,33 +0,0 @@ -import tkinter -import customtkinter # <- import the CustomTkinter module - -customtkinter.set_appearance_mode("System") # Other: "Dark", "Light" - -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("400x240") -root_tk.title("CustomTkinter Test") - - -def change_button_2_state(): - if button_2.state == tkinter.NORMAL: - button_2.configure(state=tkinter.DISABLED) - elif button_2.state == tkinter.DISABLED: - button_2.configure(state=tkinter.NORMAL) - - -def button_2_click(): - print("button_2 clicked") - - -frame_1 = customtkinter.CTkFrame(master=root_tk, width=300, height=200, corner_radius=15) -frame_1.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER) - -button_1 = customtkinter.CTkButton(master=frame_1, text="Disable/Enable Button_2", - corner_radius=10, command=change_button_2_state, width=200) -button_1.place(relx=0.5, rely=0.3, anchor=tkinter.CENTER) - -button_2 = customtkinter.CTkButton(master=frame_1, text="Button_2", - corner_radius=10, command=button_2_click) -button_2.place(relx=0.5, rely=0.7, anchor=tkinter.CENTER) - -root_tk.mainloop() diff --git a/test/manual_integration_tests/test_ctk_toplevel.py b/test/manual_integration_tests/test_ctk_toplevel.py index e66ba7a..4031e14 100644 --- a/test/manual_integration_tests/test_ctk_toplevel.py +++ b/test/manual_integration_tests/test_ctk_toplevel.py @@ -15,11 +15,9 @@ class ExampleApp(customtkinter.CTk): window = customtkinter.CTkToplevel(self) window.geometry("400x200") - print(window.master.winfo_class()) - label = customtkinter.CTkLabel(window, text="CTkToplevel window") label.pack(side="top", fill="both", expand=True, padx=40, pady=40) app = ExampleApp() -app.mainloop() \ No newline at end of file +app.mainloop() 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 5845c62..23f61be 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 @@ -25,7 +25,8 @@ def button_function(): def slider_function(value): customtkinter.set_widget_scaling(value * 2) - customtkinter.ScalingTracker.set_window_scaling(value * 2) + customtkinter.set_spacing_scaling(value * 2) + customtkinter.set_window_scaling(value * 2) progressbar_1.set(value) diff --git a/test/manual_integration_tests/test_widget_states.py b/test/manual_integration_tests/test_widget_states.py new file mode 100644 index 0000000..26363f1 --- /dev/null +++ b/test/manual_integration_tests/test_widget_states.py @@ -0,0 +1,31 @@ +import tkinter +import customtkinter + + +root_tk = customtkinter.CTk() +root_tk.geometry("400x240") +root_tk.title("CustomTkinter Test") + + +def change_state(widget): + if widget.state == tkinter.NORMAL: + widget.configure(state=tkinter.DISABLED) + elif widget.state == tkinter.DISABLED: + widget.configure(state=tkinter.NORMAL) + + +def button_2_click(): + print("button_2 clicked") + + +button_1 = customtkinter.CTkButton(master=root_tk, text="button_1", command=button_2_click) +button_1.pack(padx=20, pady=10) +button_2 = customtkinter.CTkButton(master=root_tk, text="Disable/Enable button_1", command=lambda: change_state(button_1)) +button_2.pack(padx=20, pady=10) + +switch_1 = customtkinter.CTkSwitch(master=root_tk, text="switch_1", command=button_2_click) +switch_1.pack(padx=20, pady=10) +switch_2 = customtkinter.CTkSwitch(master=root_tk, text="Disable/Enable switch_1", command=lambda: change_state(switch_1)) +switch_2.pack(padx=20, pady=10) + +root_tk.mainloop() diff --git a/test/unit_tests/test_ctk.py b/test/unit_tests/test_ctk.py index 16ef894..ab0d801 100644 --- a/test/unit_tests/test_ctk.py +++ b/test/unit_tests/test_ctk.py @@ -62,20 +62,15 @@ class TestCTk(): 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 + assert self.root_ctk.window_scaling == 1.5 * customtkinter.ScalingTracker.get_window_dpi_scaling(self.root_ctk) self.root_ctk.maxsize(400, 500) self.root_ctk.geometry("500x500") - 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): diff --git a/test/unit_tests/test_ctk_toplevel.py b/test/unit_tests/test_ctk_toplevel.py index 877e726..2f1c63a 100644 --- a/test/unit_tests/test_ctk_toplevel.py +++ b/test/unit_tests/test_ctk_toplevel.py @@ -64,20 +64,15 @@ class TestCTkToplevel(): 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 + assert self.root_ctk.window_scaling == 1.5 * customtkinter.ScalingTracker.get_window_dpi_scaling(self.root_ctk) 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):