diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index 1bcb834..cc3c940 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -64,6 +64,9 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): self._iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop() self._block_update_dimensions_event = False + # save focus before calling withdraw + self.focused_widget_before_widthdraw = None + # set CustomTkinter titlebar icon (Windows only) if sys.platform.startswith("win"): self.after(200, self._windows_set_titlebar_icon) @@ -137,24 +140,26 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): def update(self): if self._window_exists is False: - self._window_exists = True - if sys.platform.startswith("win"): if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists: # print("window dont exists -> deiconify in update") self.deiconify() + self._window_exists = True + super().update() def mainloop(self, *args, **kwargs): if not self._window_exists: - self._window_exists = True - if sys.platform.startswith("win"): + self._windows_set_titlebar_color(self._get_appearance_mode()) + if not self._withdraw_called_before_window_exists and not self._iconify_called_before_window_exists: # print("window dont exists -> deiconify in mainloop") self.deiconify() + self._window_exists = True + super().mainloop(*args, **kwargs) def resizable(self, width: bool = None, height: bool = None): @@ -267,9 +272,11 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): # print("window_exists -> state_before_windows_set_titlebar_color: ", self.state_before_windows_set_titlebar_color) if self._state_before_windows_set_titlebar_color != "iconic" or self._state_before_windows_set_titlebar_color != "withdrawn": + self.focused_widget_before_widthdraw = self.focus_get() super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible else: # print("window dont exists -> withdraw and update") + self.focused_widget_before_widthdraw = self.focus_get() super().withdraw() super().update() @@ -298,7 +305,7 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): except Exception as err: print(err) - if self._window_exists: + if self._window_exists or True: # print("window_exists -> return to original state: ", self.state_before_windows_set_titlebar_color) if self._state_before_windows_set_titlebar_color == "normal": self.deiconify() @@ -311,6 +318,10 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): else: pass # wait for update or mainloop to be called + if self.focused_widget_before_widthdraw is not None: + self.after(1, self.focused_widget_before_widthdraw.focus) + self.focused_widget_before_widthdraw = None + def _set_appearance_mode(self, mode_string: str): super()._set_appearance_mode(mode_string) diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 59b43f6..a4eef57 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -70,6 +70,9 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color self._block_update_dimensions_event = False + # save focus before calling withdraw + self.focused_widget_before_widthdraw = None + # set CustomTkinter titlebar icon (Windows only) if sys.platform.startswith("win"): self.after(200, self._windows_set_titlebar_icon) @@ -238,6 +241,7 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl if sys.platform.startswith("win") and not self._deactivate_windows_window_header_manipulation: self._state_before_windows_set_titlebar_color = self.state() + self.focused_widget_before_widthdraw = self.focus_get() super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible super().update() @@ -268,6 +272,10 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl self._windows_set_titlebar_color_called = True self.after(5, self._revert_withdraw_after_windows_set_titlebar_color) + if self.focused_widget_before_widthdraw is not None: + self.after(10, self.focused_widget_before_widthdraw.focus) + self.focused_widget_before_widthdraw = None + def _revert_withdraw_after_windows_set_titlebar_color(self): """ if in a short time (5ms) after """ if self._windows_set_titlebar_color_called: diff --git a/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py index 0d19147..b7f757a 100644 --- a/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py +++ b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py @@ -9,7 +9,7 @@ class CTkAppearanceModeBaseClass: - destroy() must be called when sub-class is destroyed - _set_appearance_mode() abstractmethod, gets called when appearance mode changes, must be overridden - - _apply_appearance_mode() + - _apply_appearance_mode() to convert tuple color """ def __init__(self): 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 10518b6..5f119bf 100644 --- a/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py +++ b/customtkinter/windows/widgets/core_widget_classes/ctk_base_class.py @@ -9,9 +9,6 @@ try: except ImportError: from typing_extensions import TypedDict -# removed due to circular import -# from ...ctk_tk import CTk -# from ...ctk_toplevel import CTkToplevel from .... import windows # import windows for isinstance checks from ..theme import ThemeManager @@ -74,7 +71,7 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas super().bind('', self._update_dimensions_event) # overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well - if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame, tkinter.LabelFrame, ttk.Frame, ttk.LabelFrame, ttk.Notebook)) and not isinstance(self.master, CTkBaseClass): + if isinstance(self.master, (tkinter.Tk, tkinter.Toplevel, tkinter.Frame, tkinter.LabelFrame, ttk.Frame, ttk.LabelFrame, ttk.Notebook)) and not isinstance(self.master, (CTkBaseClass, CTkAppearanceModeBaseClass)): master_old_configure = self.master.config def new_configure(*args, **kwargs): diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py index 6a2560b..db54386 100644 --- a/customtkinter/windows/widgets/ctk_entry.py +++ b/customtkinter/windows/widgets/ctk_entry.py @@ -133,7 +133,7 @@ class CTkEntry(CTkBaseClass): self._canvas.configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) - self._draw() + self._draw(no_color_updates=True) def _update_font(self): """ pass font to tkinter widgets with applied font scaling and update grid with workaround """ @@ -153,14 +153,14 @@ class CTkEntry(CTkBaseClass): def _draw(self, no_color_updates=False): super()._draw(no_color_updates) - self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) - requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), self._apply_widget_scaling(self._current_height), self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)) if requires_recoloring or no_color_updates is False: + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) + if self._apply_appearance_mode(self._fg_color) == "transparent": self._canvas.itemconfig("inner_parts", fill=self._apply_appearance_mode(self._bg_color), @@ -342,13 +342,13 @@ class CTkEntry(CTkBaseClass): return self._entry.get() def focus(self): - return self._entry.focus() + self._entry.focus() def focus_set(self): - return self._entry.focus_set() + self._entry.focus_set() def focus_force(self): - return self._entry.focus_force() + self._entry.focus_force() def index(self, index): return self._entry.index(index) diff --git a/customtkinter/windows/widgets/ctk_scrollable_frame.py b/customtkinter/windows/widgets/ctk_scrollable_frame.py index 57b6929..1017e2c 100644 --- a/customtkinter/windows/widgets/ctk_scrollable_frame.py +++ b/customtkinter/windows/widgets/ctk_scrollable_frame.py @@ -1,15 +1,18 @@ -from typing import Union, Tuple, List, Optional +from typing import Union, Tuple, Optional +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal import tkinter +import sys from .ctk_frame import CTkFrame from .ctk_scrollbar import CTkScrollbar +from .appearance_mode import CTkAppearanceModeBaseClass +from .core_widget_classes import CTkBaseClass -class CTkScrollableFrame(tkinter.Frame): - - _xscrollincrement = 4 # horizontal scrolling speed - _yscrollincrement = 8 # vertical scrolling speed - +class CTkScrollableFrame(tkinter.Frame, CTkAppearanceModeBaseClass): def __init__(self, master: any, width: int = 200, @@ -21,58 +24,120 @@ class CTkScrollableFrame(tkinter.Frame): fg_color: Optional[Union[str, Tuple[str, str]]] = None, border_color: Optional[Union[str, Tuple[str, str]]] = None, - activate_x_scrollbars: bool = False, - activate_y_scrollbars: bool = True): + orientation: Literal["vertical", "horizontal"] = "vertical"): - self._activate_x_scrollbars = activate_x_scrollbars - self._activate_y_scrollbars = activate_y_scrollbars + self._orientation = orientation - self.parent_frame = CTkFrame(master=master, width=width, height=height, corner_radius=corner_radius, border_width=border_width) - self.parent_canvas = tkinter.Canvas(master=self.parent_frame, yscrollincrement=self._yscrollincrement, xscrollincrement=self._xscrollincrement) - if self._activate_x_scrollbars: - self.x_scrollbar = CTkScrollbar(master=self.parent_frame, orientation="horizontal", command=self.parent_canvas.xview) - self.parent_canvas.configure(xscrollcommand=self.x_scrollbar.set) - if self._activate_y_scrollbars: - self.y_scrollbar = CTkScrollbar(master=self.parent_frame, orientation="vertical", command=self.parent_canvas.yview) - self.parent_canvas.configure(yscrollcommand=self.y_scrollbar.set) + self.parent_frame = CTkFrame(master=master, width=width, height=height, corner_radius=corner_radius, + border_width=border_width, bg_color=bg_color, fg_color=fg_color, border_color=border_color) + self.parent_frame.grid_propagate(0) + self.parent_canvas = tkinter.Canvas(master=self.parent_frame, highlightthickness=0, width=0, height=0) + self._set_scroll_increments() + + if self._orientation == "horizontal": + self.scrollbar = CTkScrollbar(master=self.parent_frame, orientation="horizontal", command=self.parent_canvas.xview) + self.parent_canvas.configure(xscrollcommand=self.scrollbar.set) + elif self._orientation == "vertical": + self.scrollbar = CTkScrollbar(master=self.parent_frame, orientation="vertical", command=self.parent_canvas.yview) + self.parent_canvas.configure(yscrollcommand=self.scrollbar.set) self._create_grid() - super().__init__(master=self.parent_canvas, width=0) + tkinter.Frame.__init__(self, master=self.parent_canvas, highlightthickness=0) + CTkAppearanceModeBaseClass.__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) self.bind_all("", self._keyboard_shift_press_all) self.bind_all("", self._keyboard_shift_press_all) self.bind_all("", self._keyboard_shift_release_all) self.bind_all("", self._keyboard_shift_release_all) - self.parent_canvas.bind("", self._parent_canvas_configure) self._create_window_id = self.parent_canvas.create_window(0, 0, window=self, anchor="nw") + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color"))) + self._shift_pressed = False - self.mouse_over_widget = False + + def destroy(self): + tkinter.Frame.destroy(self) + CTkAppearanceModeBaseClass.destroy(self) + + def _set_appearance_mode(self, mode_string): + super()._set_appearance_mode(mode_string) + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color"))) + + def configure(self, **kwargs): + if "fg_color" in kwargs: + self.parent_frame.configure(fg_color=kwargs.pop("fg_color")) + tkinter.Frame.configure(self, bg=self._apply_appearance_mode(self.parent_frame.cget("fg_color"))) + + for child in self.winfo_children(): + if isinstance(child, CTkBaseClass): + child.configure(bg_color=self.parent_frame.cget("fg_color")) + + if "corner_radius" in kwargs: + self.parent_frame.configure(corner_radius=kwargs.pop("corner_radius")) + self._create_grid() + + if "border_width" in kwargs: + self.parent_frame.configure(border_width=kwargs.pop("border_width")) + self._create_grid() + + self.parent_frame.configure(**kwargs) + + def _fit_frame_dimensions_to_canvas(self, event): + if self._orientation == "horizontal": + self.parent_canvas.itemconfigure(self._create_window_id, height=self.parent_canvas.winfo_height()) + elif self._orientation == "vertical": + self.parent_canvas.itemconfigure(self._create_window_id, width=self.parent_canvas.winfo_width()) + + def _set_scroll_increments(self): + if sys.platform.startswith("win"): + self.parent_canvas.configure(xscrollincrement=1, yscrollincrement=1) + elif sys.platform == "darwin": + self.parent_canvas.configure(xscrollincrement=4, yscrollincrement=8) def _create_grid(self): + border_spacing = self.parent_frame.cget("corner_radius") + self.parent_frame.cget("border_width") self.parent_frame.grid_columnconfigure(0, weight=1) self.parent_frame.grid_rowconfigure(0, weight=1) - self.parent_canvas.grid(row=0, column=0, sticky="nsew") - if self._activate_x_scrollbars: + if self._orientation == "horizontal": self.parent_frame.grid_rowconfigure(1, weight=0) - self.x_scrollbar.grid(row=1, column=0, sticky="nsew") - if self._activate_y_scrollbars: + self.parent_canvas.grid(row=0, column=0, sticky="nsew", + padx=border_spacing, pady=(border_spacing, 0)) + self.scrollbar.grid(row=1, column=0, sticky="nsew", + padx=border_spacing) + elif self._orientation == "vertical": self.parent_frame.grid_columnconfigure(1, weight=0) - self.y_scrollbar.grid(row=0, column=1, sticky="nsew") - - def _parent_canvas_configure(self, event): - #self.parent_canvas.itemconfigure(self._create_window_id, width=event.width, height=event.height) - pass + self.parent_canvas.grid(row=0, column=0, sticky="nsew", + pady=border_spacing, padx=(border_spacing, 0)) + self.scrollbar.grid(row=0, column=1, sticky="nsew", + pady=border_spacing) def _mouse_wheel_all(self, event): if self.check_if_master_is_canvas(event.widget): - if self._shift_pressed: - self.parent_canvas.xview("scroll", -event.delta, "units") + if sys.platform.startswith("win"): + if self._shift_pressed: + if self.parent_canvas.xview() != (0.0, 1.0): + self.parent_canvas.xview("scroll", -int(event.delta/6), "units") + else: + if self.parent_canvas.yview() != (0.0, 1.0): + self.parent_canvas.yview("scroll", -int(event.delta/6), "units") + elif sys.platform == "darwin": + if self._shift_pressed: + if self.parent_canvas.xview() != (0.0, 1.0): + self.parent_canvas.xview("scroll", -event.delta, "units") + else: + if self.parent_canvas.yview() != (0.0, 1.0): + self.parent_canvas.yview("scroll", -event.delta, "units") else: - self.parent_canvas.yview("scroll", -event.delta, "units") + if self._shift_pressed: + if self.parent_canvas.xview() != (0.0, 1.0): + self.parent_canvas.xview("scroll", -event.delta, "units") + else: + if self.parent_canvas.yview() != (0.0, 1.0): + self.parent_canvas.yview("scroll", -event.delta, "units") def _keyboard_shift_press_all(self, event): self._shift_pressed = True @@ -96,3 +161,27 @@ class CTkScrollableFrame(tkinter.Frame): def grid(self, **kwargs): self.parent_frame.grid(**kwargs) + + def pack_forget(self): + self.parent_frame.pack_forget() + + def place_forget(self, **kwargs): + self.parent_frame.place_forget() + + def grid_forget(self, **kwargs): + self.parent_frame.grid_forget() + + def grid_remove(self, **kwargs): + self.parent_frame.grid_remove() + + def grid_propagate(self, **kwargs): + self.parent_frame.grid_propagate() + + def grid_info(self, **kwargs): + self.parent_frame.grid_info() + + def lift(self, aboveThis=None): + self.parent_frame.lift(aboveThis) + + def lower(self, belowThis=None): + self.parent_frame.lower(belowThis)