From 36b1b2ae7fdde74ecbbad05536aa985e4f068761 Mon Sep 17 00:00:00 2001 From: Supercam19 <79729769+supercam19@users.noreply.github.com> Date: Sun, 22 Jan 2023 13:13:34 -0500 Subject: [PATCH 1/8] Add CTkTooltip --- customtkinter/__init__.py | 1 + customtkinter/windows/__init__.py | 1 + customtkinter/windows/ctk_tooltip.py | 116 +++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 customtkinter/windows/ctk_tooltip.py diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index 58be3cbe..5723a90c 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -38,6 +38,7 @@ from .windows import CTk from .windows import CTkToplevel from .windows import CTkInputDialog +from .windows import CTkTooltip # import font classes from .windows.widgets.font import CTkFont diff --git a/customtkinter/windows/__init__.py b/customtkinter/windows/__init__.py index ca681b72..544c05ae 100644 --- a/customtkinter/windows/__init__.py +++ b/customtkinter/windows/__init__.py @@ -1,3 +1,4 @@ from .ctk_tk import CTk from .ctk_toplevel import CTkToplevel from .ctk_input_dialog import CTkInputDialog +from .ctk_tooltip import CTkTooltip diff --git a/customtkinter/windows/ctk_tooltip.py b/customtkinter/windows/ctk_tooltip.py new file mode 100644 index 00000000..d6d57d49 --- /dev/null +++ b/customtkinter/windows/ctk_tooltip.py @@ -0,0 +1,116 @@ +from .widgets.theme.theme_manager import ThemeManager +from .ctk_toplevel import CTkToplevel +from .widgets.ctk_label import CTkLabel +from .widgets.appearance_mode.appearance_mode_tracker import AppearanceModeTracker +from typing import Union, Tuple + + +class CTkTooltip(CTkToplevel): + # Mouse hover tooltips that can be attached to widgets + def __init__(self, master, + text: str = 'CTk Tooltip', + delay: int = 500, + wrap_length: int = -1, + bg_color: Union[str, Tuple[str, str]] = "transparent", + fg_color: Union[str, Tuple[str, str]] = "default", + mouse_offset: Tuple[int, int] = (1, 1), + **kwargs): + self.wait_time = delay # milliseconds + self.wrap_length = wrap_length + self.master = master + self.text = text + self.mouse_offset = mouse_offset + self.master.bind("", self._schedule, add="+") + self.master.bind("", self._leave, add="+") + self.master.bind("", self._leave, add="+") + self._id = None + self.kwargs = kwargs + self._visible = False + + # determine colors + self.fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"] if fg_color == "default" else fg_color + if bg_color == "transparent": + if bg_color.startswith('#'): + color_list = [int(fg_color[i:i + 2], 16) for i in range(1, len(fg_color), 2)] + if not any(color == 255 for color in color_list): + for i in range(len(color_list)): + color_list[i] += 1 + else: + for i in range(len(color_list)): + color_list[i] -= 1 + + self.bg_color = "#" + ''.join(['{:02x}'.format(x) for x in color_list]) + else: + self.__appearance_mode = AppearanceModeTracker.get_mode() + if self.__appearance_mode == 0: + self.bg_color = 'gray86' if self.fg_color != 'gray86' else 'gray84' + else: + self.bg_color = 'gray17' if self.fg_color != 'gray17' else 'gray15' + else: + self.bg_color = bg_color + + def _leave(self, event=None): + self._unschedule() + if self._visible: self.hide() + self._visible = False + + def _schedule(self, event=None): + self._id = self.master.after(self.wait_time, self.show) + + def _unschedule(self): + # Unschedule scheduled popups + id = self._id + self._id = None + if id: + self.master.after_cancel(id) + + def show(self, event=None): + # Get the position the tooltip needs to appear at + super().__init__(self.master, **self.kwargs) + self._visible = True + x = y = 0 + x, y, cx, cy = self.master.bbox("insert") + # Has to be offset from mouse position, otherwise it will appear and disappear instantly because it left the parent widget + x += self.master.winfo_pointerx() + self.mouse_offset[0] + y += self.master.winfo_pointery() + self.mouse_offset[1] + self.wm_attributes("-toolwindow", True) + self.wm_overrideredirect(True) + self.wm_geometry(f'+{x}+{y}') + self.wm_attributes('-transparentcolor', self.bg_color) + super().configure(bg_color=self.bg_color) + label = CTkLabel(self, text=self.text, corner_radius=10, bg_color=self.bg_color, fg_color=self.fg_color, width=1, wraplength=self.wrap_length) + label.pack() + + def hide(self): + self._unschedule() + self.withdraw() + + def configure(self, **kwargs): + require_redraw = False + if "fg_color" in kwargs: + self.fg_color = kwargs["fg_color"] + require_redraw = True + del kwargs["fg_color"] + if "bg_color" in kwargs: + self.bg_color = kwargs["bg_color"] + require_redraw = True + del kwargs["bg_color"] + if "text" in kwargs: + self.text = kwargs["text"] + require_redraw = True + del kwargs["text"] + if "delay" in kwargs: + self.wait_time = kwargs["delay"] + del kwargs["delay"] + if "wrap_length" in kwargs: + self.wrap_length = kwargs["wrap_length"] + require_redraw = True + del kwargs["wrap_length"] + if "mouse_offset" in kwargs: + self.mouse_offset = kwargs["mouse_offset"] + require_redraw = True + del kwargs["mouse_offset"] + super().configure(**kwargs) + if require_redraw: + self.hide() + self.show() From 52e1407aec151ac3b00b7906ea57863804d599a8 Mon Sep 17 00:00:00 2001 From: Supercam19 <79729769+supercam19@users.noreply.github.com> Date: Sun, 22 Jan 2023 13:28:07 -0500 Subject: [PATCH 2/8] Pass kwargs into label instead of toplevel --- customtkinter/windows/ctk_tooltip.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/customtkinter/windows/ctk_tooltip.py b/customtkinter/windows/ctk_tooltip.py index d6d57d49..94b176d2 100644 --- a/customtkinter/windows/ctk_tooltip.py +++ b/customtkinter/windows/ctk_tooltip.py @@ -15,8 +15,8 @@ def __init__(self, master, fg_color: Union[str, Tuple[str, str]] = "default", mouse_offset: Tuple[int, int] = (1, 1), **kwargs): - self.wait_time = delay # milliseconds - self.wrap_length = wrap_length + self.wait_time = delay # milliseconds until tooltip appears + self.wrap_length = wrap_length # wrap length of the tooltip text self.master = master self.text = text self.mouse_offset = mouse_offset @@ -66,7 +66,7 @@ def _unschedule(self): def show(self, event=None): # Get the position the tooltip needs to appear at - super().__init__(self.master, **self.kwargs) + super().__init__(self.master) self._visible = True x = y = 0 x, y, cx, cy = self.master.bbox("insert") @@ -78,7 +78,7 @@ def show(self, event=None): self.wm_geometry(f'+{x}+{y}') self.wm_attributes('-transparentcolor', self.bg_color) super().configure(bg_color=self.bg_color) - label = CTkLabel(self, text=self.text, corner_radius=10, bg_color=self.bg_color, fg_color=self.fg_color, width=1, wraplength=self.wrap_length) + label = CTkLabel(self, text=self.text, corner_radius=10, bg_color=self.bg_color, fg_color=self.fg_color, width=1, wraplength=self.wrap_length, **self.kwargs) label.pack() def hide(self): From d0622f63426c06e1666e68d058d4ae51c53b0838 Mon Sep 17 00:00:00 2001 From: Supercam19 <79729769+supercam19@users.noreply.github.com> Date: Sun, 22 Jan 2023 13:31:21 -0500 Subject: [PATCH 3/8] Add a few comments --- customtkinter/windows/ctk_tooltip.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/customtkinter/windows/ctk_tooltip.py b/customtkinter/windows/ctk_tooltip.py index 94b176d2..7cd3479a 100644 --- a/customtkinter/windows/ctk_tooltip.py +++ b/customtkinter/windows/ctk_tooltip.py @@ -17,9 +17,9 @@ def __init__(self, master, **kwargs): self.wait_time = delay # milliseconds until tooltip appears self.wrap_length = wrap_length # wrap length of the tooltip text - self.master = master - self.text = text - self.mouse_offset = mouse_offset + self.master = master # parent widget + self.text = text # text to display + self.mouse_offset = mouse_offset # offset from mouse position (x, y) self.master.bind("", self._schedule, add="+") self.master.bind("", self._leave, add="+") self.master.bind("", self._leave, add="+") @@ -86,6 +86,7 @@ def hide(self): self.withdraw() def configure(self, **kwargs): + # Change attributes of the tooltip, and redraw if necessary require_redraw = False if "fg_color" in kwargs: self.fg_color = kwargs["fg_color"] From 820c4ae82d471f4ea62a578ae6e1f6fcf4bddc35 Mon Sep 17 00:00:00 2001 From: Supercam19 <79729769+supercam19@users.noreply.github.com> Date: Fri, 27 Jan 2023 17:29:31 -0500 Subject: [PATCH 4/8] Fix MacOS error; color changes Fixed errors using MacOS trying to create a tooltip --- customtkinter/windows/ctk_tooltip.py | 42 +++++++++++++++++++++------- examples/simple_example.py | 1 + 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/customtkinter/windows/ctk_tooltip.py b/customtkinter/windows/ctk_tooltip.py index 7cd3479a..1ca8f7c8 100644 --- a/customtkinter/windows/ctk_tooltip.py +++ b/customtkinter/windows/ctk_tooltip.py @@ -3,6 +3,7 @@ from .widgets.ctk_label import CTkLabel from .widgets.appearance_mode.appearance_mode_tracker import AppearanceModeTracker from typing import Union, Tuple +import sys class CTkTooltip(CTkToplevel): @@ -21,17 +22,22 @@ def __init__(self, master, self.text = text # text to display self.mouse_offset = mouse_offset # offset from mouse position (x, y) self.master.bind("", self._schedule, add="+") - self.master.bind("", self._leave, add="+") + self.master.bind("", lambda e: self.master.after(5, self._leave), add="+") self.master.bind("", self._leave, add="+") self._id = None self.kwargs = kwargs self._visible = False + self._is_hovering_tooltip = False # determine colors - self.fg_color = ThemeManager.theme["CTkFrame"]["top_fg_color"] if fg_color == "default" else fg_color + self.__appearance_mode = AppearanceModeTracker.get_mode() + if fg_color == "default": + self.fg_color = '#CBCBCB' if self.__appearance_mode == 0 else '#545454' + else: + self.fg_color = fg_color if bg_color == "transparent": if bg_color.startswith('#'): - color_list = [int(fg_color[i:i + 2], 16) for i in range(1, len(fg_color), 2)] + color_list = [int(self.fg_color[i:i + 2], 16) for i in range(1, len(self.fg_color), 2)] if not any(color == 255 for color in color_list): for i in range(len(color_list)): color_list[i] += 1 @@ -41,7 +47,6 @@ def __init__(self, master, self.bg_color = "#" + ''.join(['{:02x}'.format(x) for x in color_list]) else: - self.__appearance_mode = AppearanceModeTracker.get_mode() if self.__appearance_mode == 0: self.bg_color = 'gray86' if self.fg_color != 'gray86' else 'gray84' else: @@ -50,9 +55,10 @@ def __init__(self, master, self.bg_color = bg_color def _leave(self, event=None): - self._unschedule() - if self._visible: self.hide() - self._visible = False + if not self._is_hovering_tooltip: + self._unschedule() + if self._visible: self.hide() + self._visible = False def _schedule(self, event=None): self._id = self.master.after(self.wait_time, self.show) @@ -64,6 +70,15 @@ def _unschedule(self): if id: self.master.after_cancel(id) + def _tt_enter(self, event=None): + if not self._is_hovering_tooltip: + self._is_hovering_tooltip = True + + def _tt_leave(self, event=None): + if self._is_hovering_tooltip: + self._is_hovering_tooltip = False + self._leave() + def show(self, event=None): # Get the position the tooltip needs to appear at super().__init__(self.master) @@ -73,13 +88,20 @@ def show(self, event=None): # Has to be offset from mouse position, otherwise it will appear and disappear instantly because it left the parent widget x += self.master.winfo_pointerx() + self.mouse_offset[0] y += self.master.winfo_pointery() + self.mouse_offset[1] - self.wm_attributes("-toolwindow", True) + if sys.platform.startswith("win"): + self.wm_attributes('-transparentcolor', self.bg_color) # rounds corners + self.wm_attributes("-toolwindow", True) # removes icon from taskbar + super().configure(bg_color=self.bg_color) + elif sys.platform == 'darwin': + self.wm_attributes('-transparent', True) + super().configure(bg_color='systemTransparent') self.wm_overrideredirect(True) self.wm_geometry(f'+{x}+{y}') - self.wm_attributes('-transparentcolor', self.bg_color) - super().configure(bg_color=self.bg_color) label = CTkLabel(self, text=self.text, corner_radius=10, bg_color=self.bg_color, fg_color=self.fg_color, width=1, wraplength=self.wrap_length, **self.kwargs) label.pack() + label.bind("", self._tt_enter, add="+") + label.bind("", self._tt_leave, add="+") + if sys.platform == 'darwin': label.configure(bg_color='systemTransparent') def hide(self): self._unschedule() diff --git a/examples/simple_example.py b/examples/simple_example.py index 6999cc08..f858d7fd 100644 --- a/examples/simple_example.py +++ b/examples/simple_example.py @@ -26,6 +26,7 @@ def slider_callback(value): button_1 = customtkinter.CTkButton(master=frame_1, command=button_callback) button_1.pack(pady=10, padx=10) +tooltip_1 = customtkinter.CTkTooltip(master=button_1) slider_1 = customtkinter.CTkSlider(master=frame_1, command=slider_callback, from_=0, to=1) slider_1.pack(pady=10, padx=10) From 6cff304acfa6b6c4fb0619b1d7823b492cc7fb51 Mon Sep 17 00:00:00 2001 From: Supercam19 <79729769+supercam19@users.noreply.github.com> Date: Sun, 29 Jan 2023 01:31:37 -0500 Subject: [PATCH 5/8] Fix issues caused by slow machines --- customtkinter/windows/ctk_tooltip.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/customtkinter/windows/ctk_tooltip.py b/customtkinter/windows/ctk_tooltip.py index 1ca8f7c8..1df7b27b 100644 --- a/customtkinter/windows/ctk_tooltip.py +++ b/customtkinter/windows/ctk_tooltip.py @@ -28,6 +28,7 @@ def __init__(self, master, self.kwargs = kwargs self._visible = False self._is_hovering_tooltip = False + self._bg_color_is_default = True if bg_color == "transparent" else False # used on linux because rounded corners doesnt seem to be possible usually # determine colors self.__appearance_mode = AppearanceModeTracker.get_mode() @@ -82,6 +83,7 @@ def _tt_leave(self, event=None): def show(self, event=None): # Get the position the tooltip needs to appear at super().__init__(self.master) + super().withdraw() # hide and reshow window once all code is ran to fix issues due to slower machines (??) self._visible = True x = y = 0 x, y, cx, cy = self.master.bbox("insert") @@ -89,12 +91,15 @@ def show(self, event=None): x += self.master.winfo_pointerx() + self.mouse_offset[0] y += self.master.winfo_pointery() + self.mouse_offset[1] if sys.platform.startswith("win"): - self.wm_attributes('-transparentcolor', self.bg_color) # rounds corners + self.wm_attributes('-transparentcolor', self.bg_color) # used for rounded corners self.wm_attributes("-toolwindow", True) # removes icon from taskbar super().configure(bg_color=self.bg_color) elif sys.platform == 'darwin': - self.wm_attributes('-transparent', True) + self.wm_attributes('-transparent', True) # used for rounded corners super().configure(bg_color='systemTransparent') + elif sys.platform.startswith("linux"): + if self._bg_color_is_default: + self.bg_color = self.fg_color # create square edge tooltips self.wm_overrideredirect(True) self.wm_geometry(f'+{x}+{y}') label = CTkLabel(self, text=self.text, corner_radius=10, bg_color=self.bg_color, fg_color=self.fg_color, width=1, wraplength=self.wrap_length, **self.kwargs) @@ -102,6 +107,7 @@ def show(self, event=None): label.bind("", self._tt_enter, add="+") label.bind("", self._tt_leave, add="+") if sys.platform == 'darwin': label.configure(bg_color='systemTransparent') + super().deiconify() def hide(self): self._unschedule() From db1a563801938dd06c28c2c202505c08d69170d4 Mon Sep 17 00:00:00 2001 From: Supercam19 <79729769+supercam19@users.noreply.github.com> Date: Tue, 21 Feb 2023 22:18:50 -0500 Subject: [PATCH 6/8] Tooltip improvements - Fix .configure() not working - Fix tooltips sometimes not disappearing - Add is_visible() method to return visibility status --- customtkinter/windows/ctk_tooltip.py | 52 ++++++++++++---------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/customtkinter/windows/ctk_tooltip.py b/customtkinter/windows/ctk_tooltip.py index 1df7b27b..58631395 100644 --- a/customtkinter/windows/ctk_tooltip.py +++ b/customtkinter/windows/ctk_tooltip.py @@ -22,8 +22,10 @@ def __init__(self, master, self.text = text # text to display self.mouse_offset = mouse_offset # offset from mouse position (x, y) self.master.bind("", self._schedule, add="+") - self.master.bind("", lambda e: self.master.after(5, self._leave), add="+") + self.master.bind("", self._leave) self.master.bind("", self._leave, add="+") + label = self.master.winfo_children()[0] + label.bind("", self._schedule, add="+") self._id = None self.kwargs = kwargs self._visible = False @@ -55,13 +57,15 @@ def __init__(self, master, else: self.bg_color = bg_color + def _leave(self, event=None): - if not self._is_hovering_tooltip: - self._unschedule() - if self._visible: self.hide() - self._visible = False + self._unschedule() + print("leave") + if self._visible: self.hide() def _schedule(self, event=None): + print("schedule") + self._unschedule() self._id = self.master.after(self.wait_time, self.show) def _unschedule(self): @@ -71,16 +75,8 @@ def _unschedule(self): if id: self.master.after_cancel(id) - def _tt_enter(self, event=None): - if not self._is_hovering_tooltip: - self._is_hovering_tooltip = True - - def _tt_leave(self, event=None): - if self._is_hovering_tooltip: - self._is_hovering_tooltip = False - self._leave() - def show(self, event=None): + print("show") # Get the position the tooltip needs to appear at super().__init__(self.master) super().withdraw() # hide and reshow window once all code is ran to fix issues due to slower machines (??) @@ -104,42 +100,40 @@ def show(self, event=None): self.wm_geometry(f'+{x}+{y}') label = CTkLabel(self, text=self.text, corner_radius=10, bg_color=self.bg_color, fg_color=self.fg_color, width=1, wraplength=self.wrap_length, **self.kwargs) label.pack() - label.bind("", self._tt_enter, add="+") - label.bind("", self._tt_leave, add="+") if sys.platform == 'darwin': label.configure(bg_color='systemTransparent') + label.bind("", self._leave, add="+") super().deiconify() def hide(self): + print("hide") self._unschedule() self.withdraw() + self._visible = False def configure(self, **kwargs): # Change attributes of the tooltip, and redraw if necessary require_redraw = False if "fg_color" in kwargs: - self.fg_color = kwargs["fg_color"] + self.fg_color = kwargs.pop("fg_color") require_redraw = True - del kwargs["fg_color"] if "bg_color" in kwargs: - self.bg_color = kwargs["bg_color"] + self.bg_color = kwargs.pop("bg_color") require_redraw = True - del kwargs["bg_color"] if "text" in kwargs: - self.text = kwargs["text"] + self.text = kwargs.pop("text") require_redraw = True - del kwargs["text"] if "delay" in kwargs: - self.wait_time = kwargs["delay"] - del kwargs["delay"] + self.wait_time = kwargs.pop("delay") if "wrap_length" in kwargs: - self.wrap_length = kwargs["wrap_length"] + self.wrap_length = kwargs.pop("wrap_length") require_redraw = True - del kwargs["wrap_length"] if "mouse_offset" in kwargs: - self.mouse_offset = kwargs["mouse_offset"] + self.mouse_offset = kwargs.pop("mouse_offset") require_redraw = True - del kwargs["mouse_offset"] - super().configure(**kwargs) + self.kwargs = kwargs if require_redraw: self.hide() self.show() + + def is_visible(self): + return self._visible From 4a35787e905fa87fa2cd43b34969bae2a4c50d35 Mon Sep 17 00:00:00 2001 From: Supercam19 <79729769+supercam19@users.noreply.github.com> Date: Tue, 21 Feb 2023 22:25:14 -0500 Subject: [PATCH 7/8] Remove debug --- customtkinter/windows/ctk_tooltip.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/customtkinter/windows/ctk_tooltip.py b/customtkinter/windows/ctk_tooltip.py index 58631395..7d852e43 100644 --- a/customtkinter/windows/ctk_tooltip.py +++ b/customtkinter/windows/ctk_tooltip.py @@ -60,11 +60,9 @@ def __init__(self, master, def _leave(self, event=None): self._unschedule() - print("leave") if self._visible: self.hide() def _schedule(self, event=None): - print("schedule") self._unschedule() self._id = self.master.after(self.wait_time, self.show) @@ -76,7 +74,6 @@ def _unschedule(self): self.master.after_cancel(id) def show(self, event=None): - print("show") # Get the position the tooltip needs to appear at super().__init__(self.master) super().withdraw() # hide and reshow window once all code is ran to fix issues due to slower machines (??) @@ -105,7 +102,6 @@ def show(self, event=None): super().deiconify() def hide(self): - print("hide") self._unschedule() self.withdraw() self._visible = False From 456863aa239b1262dde21d4127e38e3d19a2f9f2 Mon Sep 17 00:00:00 2001 From: Supercam19 <79729769+supercam19@users.noreply.github.com> Date: Tue, 21 Feb 2023 23:04:23 -0500 Subject: [PATCH 8/8] Fix multiple tooltips sometimes appearing at once --- customtkinter/windows/ctk_tooltip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/customtkinter/windows/ctk_tooltip.py b/customtkinter/windows/ctk_tooltip.py index 7d852e43..606a2a08 100644 --- a/customtkinter/windows/ctk_tooltip.py +++ b/customtkinter/windows/ctk_tooltip.py @@ -75,6 +75,7 @@ def _unschedule(self): def show(self, event=None): # Get the position the tooltip needs to appear at + if self._visible: return super().__init__(self.master) super().withdraw() # hide and reshow window once all code is ran to fix issues due to slower machines (??) self._visible = True