T7x-Workshop-Downloader/CTkToolTip/ctk_tooltip.py

232 lines
7.9 KiB
Python

"""
CTkToolTip Widget
version: 0.8
"""
import time
import sys
import customtkinter
import threading
from tkinter import Toplevel, Frame
class CTkToolTip(Toplevel):
"""
Creates a ToolTip (pop-up) widget for customtkinter.
"""
def __init__(
self,
widget: any = None,
message: str = None,
delay: float = 0.2,
follow: bool = True,
x_offset: int = +20,
y_offset: int = +10,
bg_color: str = None,
corner_radius: int = 10,
border_width: int = 0,
border_color: str = None,
alpha: float = 0.95,
padding: tuple = (10, 2),
topmost: bool = False,
is_noti: bool = False,
noti_event: any = None,
noti_dur: float = 3.0,
**message_kwargs):
super().__init__()
self.widget = widget
if not is_noti:
self.withdraw()
if topmost:
self.attributes('-topmost', 'true')
# Disable ToolTip's title bar
self.overrideredirect(True)
if sys.platform.startswith("win"):
self.transparent_color = self.widget._apply_appearance_mode(
customtkinter.ThemeManager.theme["CTkToplevel"]["fg_color"])
self.attributes("-transparentcolor", self.transparent_color)
self.transient()
elif sys.platform.startswith("darwin"):
self.transparent_color = 'systemTransparent'
self.attributes("-transparent", True)
self.transient(self.master)
else:
self.transparent_color = '#000001'
corner_radius = 0
self.transient()
self.resizable(width=True, height=True)
# Make the background transparent
self.config(background=self.transparent_color)
# StringVar instance for msg string
self.messageVar = customtkinter.StringVar()
self.message = message
self.messageVar.set(self.message)
self.noti_dur = noti_dur
self.is_noti = is_noti
self.delay = delay
self.follow = follow
self.x_offset = x_offset
self.y_offset = y_offset
self.corner_radius = corner_radius
self.alpha = alpha
self.border_width = border_width
self.padding = padding
self.bg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if bg_color is None else bg_color
self.border_color = border_color
self.disable = False
# visibility status of the ToolTip inside|outside|visible
self.status = "outside"
self.last_moved = 0
self.attributes('-alpha', self.alpha)
if sys.platform.startswith("win"):
if self.widget._apply_appearance_mode(self.bg_color) == self.transparent_color:
self.transparent_color = "#000001"
self.config(background=self.transparent_color)
self.attributes("-transparentcolor", self.transparent_color)
# Add the message widget inside the tooltip
self.transparent_frame = Frame(self, bg=self.transparent_color)
self.transparent_frame.pack(padx=0, pady=0, fill="both", expand=True)
self.frame = customtkinter.CTkFrame(self.transparent_frame, bg_color=self.transparent_color,
corner_radius=self.corner_radius,
border_width=self.border_width, fg_color=self.bg_color,
border_color=self.border_color)
self.frame.pack(padx=0, pady=0, fill="both", expand=True)
self.message_label = customtkinter.CTkLabel(self.frame, textvariable=self.messageVar, **message_kwargs)
self.message_label.pack(fill="both", padx=self.padding[0] + self.border_width,
pady=self.padding[1] + self.border_width, expand=True)
if self.widget.winfo_name() != "tk":
if self.frame.cget("fg_color") == self.widget.cget("bg_color"):
if not bg_color:
self._top_fg_color = self.frame._apply_appearance_mode(
customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"])
if self._top_fg_color != self.transparent_color:
self.frame.configure(fg_color=self._top_fg_color)
# Add bindings to the widget without overriding the existing ones
if self.is_noti:
if noti_event:
self.on_enter(noti_event)
else:
self.widget.bind("<Button-1>", self.on_enter, add="+")
else:
self.widget.bind("<Enter>", self.on_enter, add="+")
self.widget.bind("<Leave>", self.on_leave, add="+")
self.widget.bind("<Motion>", self.on_enter, add="+")
self.widget.bind("<B1-Motion>", self.on_enter, add="+")
self.widget.bind("<Destroy>", lambda _: self.hide(), add="+")
def show(self) -> None:
"""
Enable the widget.
"""
self.disable = False
def on_enter(self, event) -> None:
"""
Processes motion within the widget including entering and moving.
"""
if self.disable:
return
self.last_moved = time.time()
# Set the status as inside for the very first time
if self.status == "outside":
self.status = "inside"
# If the follow flag is not set, motion within the widget will make the ToolTip dissapear
if not self.follow:
self.status = "inside"
self.withdraw()
# Calculate available space on the right side of the widget relative to the screen
root_width = self.winfo_screenwidth()
widget_x = event.x_root
space_on_right = root_width - widget_x
# Calculate the width of the tooltip's text based on the length of the message string
text_width = self.message_label.winfo_reqwidth()
# Calculate the offset based on available space and text width to avoid going off-screen on the right side
offset_x = self.x_offset
if space_on_right < text_width + 20: # Adjust the threshold as needed
offset_x = -text_width - 20 # Negative offset when space is limited on the right side
# Offsets the ToolTip using the coordinates od an event as an origin
self.geometry(f"+{event.x_root + offset_x}+{event.y_root + self.y_offset}")
# Time is in integer: milliseconds
self.after(int(self.delay * 1000), self._show)
if self.is_noti:
threading.Timer(self.noti_dur, self.destroy).start()
def on_leave(self, event=None) -> None:
"""
Hides the ToolTip temporarily.
"""
if self.disable: return
self.status = "outside"
self.withdraw()
def _show(self) -> None:
"""
Displays the ToolTip.
"""
if not self.widget.winfo_exists():
self.hide()
self.destroy()
if self.status == "inside" and time.time() - self.last_moved >= self.delay:
self.status = "visible"
self.deiconify()
def hide(self) -> None:
"""
Disable the widget from appearing.
"""
if not self.winfo_exists():
return
self.withdraw()
self.disable = True
def is_disabled(self) -> None:
"""
Return the window state
"""
return self.disable
def get(self) -> None:
"""
Returns the text on the tooltip.
"""
return self.messageVar.get()
def configure(self, message: str = None, delay: float = None, bg_color: str = None, **kwargs):
"""
Set new message or configure the label parameters.
"""
if delay: self.delay = delay
if bg_color: self.frame.configure(fg_color=bg_color)
self.messageVar.set(message)
self.message_label.configure(**kwargs)