From b779c57e61e2e3089dc6bb6989e2e8e450675c70 Mon Sep 17 00:00:00 2001 From: "Crane, Trask" Date: Fri, 2 Jun 2023 20:58:51 -0400 Subject: [PATCH 1/4] [feat] Added in invalidate command Put invalidate command back into accpetable pass variables and works on entry fields. Signed off by Trask Crane (hcrane3@gatech.edu) --- customtkinter/windows/widgets/ctk_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py index db54386..3a3ec0d 100644 --- a/customtkinter/windows/widgets/ctk_entry.py +++ b/customtkinter/windows/widgets/ctk_entry.py @@ -19,7 +19,7 @@ class CTkEntry(CTkBaseClass): # attributes that are passed to and managed by the tkinter entry only: _valid_tk_entry_attributes = {"exportselection", "insertborderwidth", "insertofftime", - "insertontime", "insertwidth", "justify", "selectborderwidth", + "insertontime", "insertwidth", "invalidcommand", "justify", "selectborderwidth", "show", "takefocus", "validate", "validatecommand", "xscrollcommand"} def __init__(self, From 4838b01d6ae1f61029631fe2367b52e1902cbb95 Mon Sep 17 00:00:00 2001 From: "Crane, Trask" Date: Sun, 4 Jun 2023 23:48:43 -0400 Subject: [PATCH 2/4] [feat] Event Generation on Canvas For widgets based on the _canvas, attempting to use event_generate for custom events failed. Typical notation in tkinter would allow such bindings but in customtkinter, operations are performed on hidden widgets. Thus a function override is required to operate as expected. Widgets that are not based on _canvas or where a combination of hidden widgets were not updated. Signed off by Trask Crane (hcrane3@gatech.edu) --- customtkinter/windows/ctk_tk.py | 6 +++--- customtkinter/windows/ctk_toplevel.py | 6 +++--- customtkinter/windows/widgets/ctk_frame.py | 4 ++++ customtkinter/windows/widgets/ctk_label.py | 4 ++++ customtkinter/windows/widgets/ctk_progressbar.py | 4 ++++ customtkinter/windows/widgets/ctk_scrollbar.py | 4 ++++ customtkinter/windows/widgets/ctk_slider.py | 4 ++++ 7 files changed, 26 insertions(+), 6 deletions(-) diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index cc3c940..24df49f 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -107,13 +107,13 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): def _set_scaling(self, new_widget_scaling, new_window_scaling): super()._set_scaling(new_widget_scaling, new_window_scaling) - # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. + # Force new dimensions on window by using abs_min, max, and geometry. Without abs_min, max it won't work. super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}") - # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) + # set new scaled abs_min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) def block_update_dimensions_event(self): @@ -196,7 +196,7 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): # update width and height attributes width, height, x, y = self._parse_geometry_string(geometry_string) if width is not None and height is not None: - self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max + self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between abs_min and max self._current_height = max(self._min_height, min(height, self._max_height)) else: return self._reverse_geometry_scaling(super().geometry()) diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 3a351dc..d232138 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -109,13 +109,13 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl def _set_scaling(self, new_widget_scaling, new_window_scaling): super()._set_scaling(new_widget_scaling, new_window_scaling) - # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. + # Force new dimensions on window by using abs_min, max, and geometry. Without abs_min, max it won't work. super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}") - # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) + # set new scaled abs_min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) def block_update_dimensions_event(self): @@ -137,7 +137,7 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl # update width and height attributes width, height, x, y = self._parse_geometry_string(geometry_string) if width is not None and height is not None: - self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max + self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between abs_min and max self._current_height = max(self._min_height, min(height, self._max_height)) else: return self._reverse_geometry_scaling(super().geometry()) diff --git a/customtkinter/windows/widgets/ctk_frame.py b/customtkinter/windows/widgets/ctk_frame.py index 67bf161..7014713 100644 --- a/customtkinter/windows/widgets/ctk_frame.py +++ b/customtkinter/windows/widgets/ctk_frame.py @@ -194,3 +194,7 @@ class CTkFrame(CTkBaseClass): raise ValueError("'funcid' argument can only be None, because there is a bug in" + " tkinter and its not clear whether the internal callbacks will be unbinded or not") self._canvas.unbind(sequence, None) + + def event_generate(self, sequence, **kw): + """ called on the tkinter.Canvas """ + self._canvas.event_generate(sequence, **kw) diff --git a/customtkinter/windows/widgets/ctk_label.py b/customtkinter/windows/widgets/ctk_label.py index 0eb7831..8cf1db3 100644 --- a/customtkinter/windows/widgets/ctk_label.py +++ b/customtkinter/windows/widgets/ctk_label.py @@ -281,6 +281,10 @@ class CTkLabel(CTkBaseClass): self._canvas.unbind(sequence, None) self._label.unbind(sequence, None) + def event_generate(self, sequence, **kw): + """ called on the tkinter.Canvas """ + self._canvas.event_generate(sequence, **kw) + def focus(self): return self._label.focus() diff --git a/customtkinter/windows/widgets/ctk_progressbar.py b/customtkinter/windows/widgets/ctk_progressbar.py index 084354d..6733850 100644 --- a/customtkinter/windows/widgets/ctk_progressbar.py +++ b/customtkinter/windows/widgets/ctk_progressbar.py @@ -302,6 +302,10 @@ class CTkProgressBar(CTkBaseClass): " tkinter and its not clear whether the internal callbacks will be unbinded or not") self._canvas.unbind(sequence, None) + def event_generate(self, sequence, **kw): + """ called on the tkinter.Canvas """ + self._canvas.event_generate(sequence, **kw) + def focus(self): return self._canvas.focus() diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py index 1038282..8962f24 100644 --- a/customtkinter/windows/widgets/ctk_scrollbar.py +++ b/customtkinter/windows/widgets/ctk_scrollbar.py @@ -271,6 +271,10 @@ class CTkScrollbar(CTkBaseClass): self._canvas.unbind(sequence, None) # unbind all callbacks for sequence self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + def event_generate(self, sequence, **kw): + """ called on the tkinter.Canvas """ + self._canvas.event_generate(sequence, **kw) + def focus(self): return self._canvas.focus() diff --git a/customtkinter/windows/widgets/ctk_slider.py b/customtkinter/windows/widgets/ctk_slider.py index 3016a5c..0df5a85 100644 --- a/customtkinter/windows/widgets/ctk_slider.py +++ b/customtkinter/windows/widgets/ctk_slider.py @@ -387,6 +387,10 @@ class CTkSlider(CTkBaseClass): self._canvas.unbind(sequence, None) self._create_bindings(sequence=sequence) # restore internal callbacks for sequence + def event_generate(self, sequence, **kw): + """ called on the tkinter.Canvas """ + self._canvas.event_generate(sequence, **kw) + def focus(self): return self._canvas.focus() From 3f4fe4f5abcc5e413a0c3fda59b19cdde591d551 Mon Sep 17 00:00:00 2001 From: "Crane, Trask" Date: Mon, 5 Jun 2023 00:03:53 -0400 Subject: [PATCH 3/4] [refactor] Removed Unintended Name Changes Signed off by Trask Crane (hcrane3@gatech.edu) --- customtkinter/windows/ctk_tk.py | 6 +++--- customtkinter/windows/ctk_toplevel.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index 24df49f..cc3c940 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -107,13 +107,13 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): def _set_scaling(self, new_widget_scaling, new_window_scaling): super()._set_scaling(new_widget_scaling, new_window_scaling) - # Force new dimensions on window by using abs_min, max, and geometry. Without abs_min, max it won't work. + # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}") - # set new scaled abs_min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) + # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) def block_update_dimensions_event(self): @@ -196,7 +196,7 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): # update width and height attributes width, height, x, y = self._parse_geometry_string(geometry_string) if width is not None and height is not None: - self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between abs_min and max + self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max self._current_height = max(self._min_height, min(height, self._max_height)) else: return self._reverse_geometry_scaling(super().geometry()) diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index d232138..3a351dc 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -109,13 +109,13 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl def _set_scaling(self, new_widget_scaling, new_window_scaling): super()._set_scaling(new_widget_scaling, new_window_scaling) - # Force new dimensions on window by using abs_min, max, and geometry. Without abs_min, max it won't work. + # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) super().maxsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}") - # set new scaled abs_min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) + # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) def block_update_dimensions_event(self): @@ -137,7 +137,7 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl # update width and height attributes width, height, x, y = self._parse_geometry_string(geometry_string) if width is not None and height is not None: - self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between abs_min and max + self._current_width = max(self._min_width, min(width, self._max_width)) # bound value between min and max self._current_height = max(self._min_height, min(height, self._max_height)) else: return self._reverse_geometry_scaling(super().geometry()) From b8efb9739e707082ec2b590ee3d9e4dfe145a3a5 Mon Sep 17 00:00:00 2001 From: "Crane, Trask" Date: Sun, 11 Jun 2023 00:13:20 -0400 Subject: [PATCH 4/4] [feat] Combobox Validation Allows for validation of the combobox through the hidden entry widget. User is able to validate provided that they *do not delete or insert text*. If the user wishes to do this in validation, they first must ensure that the customtkinter library does not trigger the call repeatidly AND should add a short delay (via self.after). This is because on when `self._entry.insert` is called within customtkinter, it will clear the `validate` property thus causing future validation to fail. Signed off by Trask Crane (hcrane3@gatech.edu) --- customtkinter/windows/widgets/ctk_combobox.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/customtkinter/windows/widgets/ctk_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py index aa95337..3df5483 100644 --- a/customtkinter/windows/widgets/ctk_combobox.py +++ b/customtkinter/windows/widgets/ctk_combobox.py @@ -9,6 +9,7 @@ from .theme import ThemeManager from .core_rendering import DrawEngine from .core_widget_classes import CTkBaseClass from .font import CTkFont +from .utility import pop_from_dict_by_set class CTkComboBox(CTkBaseClass): @@ -17,6 +18,9 @@ class CTkComboBox(CTkBaseClass): For detailed information check out the documentation. """ + # attributes that are passed to and managed by the tkinter entry only: + _valid_tk_entry_attributes = {"invalidcommand", "validate", "validatecommand"} + def __init__(self, master: any, width: int = 140, @@ -100,7 +104,8 @@ class CTkComboBox(CTkBaseClass): bd=0, justify=justify, highlightthickness=0, - font=self._apply_font_scaling(self._font)) + font=self._apply_font_scaling(self._font), + **pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes)) self._create_grid() self._create_bindings() @@ -295,6 +300,7 @@ class CTkComboBox(CTkBaseClass): if "justify" in kwargs: self._entry.configure(justify=kwargs.pop("justify")) + self._entry.configure(**pop_from_dict_by_set(kwargs, self._valid_tk_entry_attributes)) # configure Tkinter.Entry super().configure(require_redraw=require_redraw, **kwargs) def cget(self, attribute_name: str) -> any: @@ -338,6 +344,9 @@ class CTkComboBox(CTkBaseClass): return self._command elif attribute_name == "justify": return self._entry.cget("justify") + + elif attribute_name in self._valid_tk_entry_attributes: + return self._entry.cget(attribute_name) # cget of tkinter.Entry else: return super().cget(attribute_name) @@ -393,6 +402,9 @@ class CTkComboBox(CTkBaseClass): self._entry.delete(0, tkinter.END) self._entry.insert(0, value) + def delete(self, start, end): + self._entry.delete(start, end) + def get(self) -> str: return self._entry.get()