Source code for ccu.fancyplots._gui.tooltip
"""A robust tooltip class.
This module provides the :class:`Tooltip` class.
"""
import tkinter as tk
from tkinter import ttk
[docs]
class Tooltip:
"""Create a tooltip for a given widget as the mouse goes on it.
see:
http://stackoverflow.com/questions/3221956/
what-is-the-simplest-way-to-make-tooltips-
in-tkinter/36221216#36221216
http://www.daniweb.com/programming/software-development/
code/484591/a-tooltip-class-for-tkinter
- Originally written by vegaseat on 2014.09.09.
- Modified to include a delay time by Victor Zaccardo on 2016.03.25.
Attributes:
parent: The widget over which the user must hover to activate the
tooltip.
bg: The tooltip background as a hex string. Defaults to `"#FFFFEA"`.
pad: A 4-tuple `(left, top, bottom, right)`, indicating the
padding around the text within the tooltip (in pixels).
text: The text displayed in the tooltip. Defaults to an empty string.
waittime: Time before displaying (in milliseconds). Defaults to `400`.
wraplength: Length before wrapping text (in pixels). Defaults to `250`.
"""
def __init__(
self,
parent: tk.Widget | ttk.Widget,
*,
bg: str = "#000000",
pad: tuple[float, float, float, float] = (5, 3, 5, 3),
text: str = "",
waittime: int = 400,
wraplength: float = 250,
) -> None:
"""Create a tooltip.
Args:
parent: The parent widget over which one must hover to generate
the tooltip.
bg: The tooltip background. Defaults to "#FFFFEA".
pad: The padding to use for the tooltip. Defaults to (5, 3, 5, 3).
text: The text to display on the tooltip. Defaults to "".
waittime: The time (in milliseconds) to wait for a user to hover
before generating the tooltip. Defaults to 400.
wraplength: The character length at which the tooltip message will
wrap. Defaults to 250.
"""
self._id: str | None = None
self._top_level: tk.Toplevel | None = None
self.parent = parent
self.bg = bg
self.pad = pad
self.text = text
self.waittime = waittime
self.wraplength = wraplength
self._bind_keys()
[docs]
def on_enter(self, _: tk.Event | None = None) -> None:
"""Begin counting to display the tooltip."""
self.schedule()
[docs]
def on_leave(self, _: tk.Event | None = None) -> None:
"""Stop counting to display the tooltip."""
self.unschedule()
self.hide()
[docs]
def schedule(self) -> None:
"""Plan to display the tooltip."""
self.unschedule()
self._id = self.parent.after(self.waittime, self.show)
[docs]
def unschedule(self) -> None:
"""Cancel scheduling to show the tooltip."""
id_ = self._id
self._id = None
if id_:
self.parent.after_cancel(id_)
[docs]
def show(self) -> None:
"""Show the tooltip."""
def calculate_tooltip_position(
widget: tk.Widget | ttk.Widget,
label: tk.Label,
*,
offset: tuple[float, float] = (10, 5),
pad: tuple[float, float, float, float] = (5, 3, 5, 3),
) -> tuple[float, float]:
"""Calculate the position of the tooltip based on cursor position.
Args:
widget: The hidden :class:`tk.Toplevel` widget to which the
tooltip belongs.
label: The :class:`tk.Label` widget used to display to tooltip.
offset: A 2-tuple `(offset_x, offset_y)` indicating the
number of pixels by which to offset the tooltip from the
left and top of the cursor, respectively. Defaults to
`(10, 5)`.
pad: 4-tuple `(left, top, bottom, right)` indicating the
number of pixels by which to pad the label from the left,
top, bottom and right, respectively. Defaults to
`(5, 3, 5, 3)`.
Returns:
A 2-tuple `(x, y)`, representing the position of the tooltip.
"""
w = widget
s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight()
width, height = (
pad[0] + label.winfo_reqwidth() + pad[2],
pad[1] + label.winfo_reqheight() + pad[3],
)
mouse_x, mouse_y = w.winfo_pointerxy()
x1, y1 = mouse_x + offset[0], mouse_y + offset[1]
x2, y2 = x1 + width, y1 + height
x_delta = x2 - s_width
x_delta = max(x_delta, 0)
y_delta = y2 - s_height
y_delta = max(y_delta, 0)
# offscreen
if (x_delta, y_delta) != (0, 0):
x1 = mouse_x - offset[0] - width if x_delta else x1
y1 = mouse_y - offset[1] - height if y_delta else y1
# offscreen_again - out on the top; no further checks will be done
y1 = 0 if y1 < 0 else y1
return x1, y1
# Leaves only the label and removes the app window
self._top_level = tk.Toplevel(self.parent)
self._top_level.wm_overrideredirect(True)
frame = ttk.Frame(self._top_level, borderwidth=0)
# This must remain a tk.Label due to rendering issues
# see https://stackoverflow.com/a/41381685
label = tk.Label(
frame,
text=self.text,
justify=tk.LEFT,
background=self.bg,
foreground="#FFFFFF",
relief=tk.SOLID,
borderwidth=0,
wraplength=self.wraplength,
)
label.grid(
padx=(self.pad[0], self.pad[2]),
pady=(self.pad[1], self.pad[3]),
sticky=tk.NSEW,
)
frame.grid()
x, y = calculate_tooltip_position(self.parent, label)
self._top_level.wm_geometry(f"+{x}+{y}")
[docs]
def hide(self) -> None:
"""Hide the tooltip."""
if self._top_level:
self._top_level.destroy()
self._top_level = None
[docs]
def _bind_keys(self) -> None:
"""Configure key bindings."""
self.parent.bind("<Enter>", self.on_enter)
self.parent.bind("<Leave>", self.on_leave)
self.parent.bind("<ButtonPress>", self.on_leave)