The tooltip system provides cross-platform, performant tooltips for the tkinter-based GUI. This document covers the architecture changes from commit 247255a and the critical timer cleanup mechanisms.
Problem: Large parameter tables (100+ parameters) created hundreds of tooltip Toplevel windows during initialization, causing:
- High memory overhead (each Tk Toplevel window ~8-10KB minimum)
- Slow application startup time
- Unnecessary GUI objects for non-hovered parameters
Solution: Tooltips are now created on-demand when the user hovers over a widget.
Timeline:
+-----------------+ +-----------------+ +-----------------+
| Tooltip Init | | Mouse Hover | | Show Tooltip |
| (No GUI) | --> | Schedule Show | --> | Create Window |
| Bindings only | | (250ms delay) | | Display |
+-----------------+ +-----------------+ +-----------------+
~0.3ms per item Timer fires ~2-3ms per item
Before: Platform-specific implementations
- macOS: Deferred creation, scheduled show/hide
- Linux/Windows: Pre-created Toplevel, show/hide on demand
After: Unified implementation across all platforms
- All platforms use timer-based scheduling
- Timer-based show/hide prevents flicker when moving through dense UIs
- Identical behavior ensures consistent UX
The tooltip system uses three types of timers:
+--------------------------------------------------------------+
| Timer Types and Lifecycle |
+--------------------------------------------------------------+
| |
| "show" timer: |
| - Scheduled by: schedule_show() |
| - Fires: TOOLTIP_SHOW_DELAY_MS (250ms) |
| - Executes: create_show() |
| - Cleaned by: _cancel_show(), _on_widget_destroy() |
| |
| "hide" timer: |
| - Removed in the current design |
| - Tooltip is destroyed immediately by destroy_hide() |
| - No hide-delay behavior remains |
| |
| "alpha" timer (macOS only): |
| - Scheduled by: create_show() after deiconify |
| - Fires: 50ms after deiconify |
| - Executes: _activate_alpha() - fades in tooltip |
| - Cleaned by: _cancel_timer("alpha"), _on_widget_destroy() |
| |
+--------------------------------------------------------------+
This is the critical safety mechanism that prevents timer leaks:
def _on_widget_destroy(self, event: Optional[tk.Event] = None) -> None:
"""Stop any active timers if the widget is destroyed."""
self._cancel_show() # Cancel "show" timer if pending
self._cancel_timer("alpha") # Cancel "alpha" timer if pending
if self.tooltip:
with contextlib.suppress(tk.TclError):
self.tooltip.destroy()
self.tooltip = NoneWhy this is critical:
- If the widget is destroyed while a timer is pending, Tk will try to fire a callback on a non-existent widget
- This causes
tk.TclError: invalid command name "..." - The handler cleans up ALL timers before widget destruction
Binding registered in __init__:
self.widget.bind("<Destroy>", self._on_widget_destroy, "+")The _cancel_timer() method handles already-fired or stale timers gracefully:
def _cancel_timer(self, name: str) -> None:
"""Safely cancel a timer and remove it."""
timer_id = self.timers.pop(name, None)
if timer_id:
with contextlib.suppress(tk.TclError):
self.widget.after_cancel(timer_id)Edge cases handled:
- Timer ID in dict but Tk already fired it --
tk.TclErrorsuppressed - Widget destroyed before cancellation --
tk.TclErrorsuppressed - Multiple cancellation calls --
timers.pop(name, None)returns None safely - Stale timer IDs from previous interactions -- Tk's
after_cancelsilently ignores
When entering a widget that has a pending show timer:
def schedule_show(self, _event: Optional[tk.Event] = None) -> None:
"""Delay tooltip creation slightly to avoid flicker during pointer movement."""
self._cancel_show() # Cancel any pending show
self.timers["show"] = self.widget.after(TOOLTIP_SHOW_DELAY_MS, self.create_show)Purpose: Prevents flicker when user quickly moves mouse through dense UI elements:
- Mouse leave triggers immediate destroy via
destroy_hide() - Mouse re-enters before tooltip appears
- Previous show timer canceled, new show timer scheduled
- Result: Tooltip appears cleanly without flashing
The create_show() method validates pointer position before creating tooltip:
def create_show(self, _event: Optional[tk.Event] = None) -> None:
"""Create and show the tooltip when the pointer is still over the widget."""
try:
pointed = self.widget.winfo_containing(
self.widget.winfo_pointerx(), self.widget.winfo_pointery()
)
widget_path = str(self.widget)
pointed_path = "" if pointed is None else str(pointed)
if pointed is None or (
pointed_path != widget_path
and not pointed_path.startswith(widget_path + ".")
):
return # Pointer no longer over widget
except tk.TclError:
return # Widget destroyed during timer executionWhy this is necessary:
- Timer fires even if widget is destroyed (TclError caught)
- Pointer may have left widget during 250ms delay
- Prevents creating orphaned tooltip windows
Problem: If create_show() is called multiple times (e.g., multiple Enter events queued)
Solution: Check if tooltip already exists:
if self.tooltip:
Tooltip._active_tooltip = self
return # Avoid redundant tooltip creationProblem: User closes dialog while tooltip timer is pending
Solution: _on_widget_destroy() cancels all timers before widget is destroyed
Problem: User quickly moves mouse through dense parameter table
Solution: Mutual cancellation in schedule_show():
self._cancel_show() # Previous show timer canceled- Before: Each tooltip created a Tk Toplevel window (8-10KB memory, tens of milliseconds per tooltip)
- After: Only Tooltip wrapper objects are created lazily
- Measured result: ~0.21ms per tooltip for lazy initialization
- Old eager baseline: ~12ms per tooltip for up-front Toplevel creation
- Result: Lazy init reduces startup overhead by an order of magnitude for large tables
- Lazy first hover: Measured ~285ms per tooltip with real event-loop and visible windows
- Includes the 250ms
TOOLTIP_SHOW_DELAY_MStimer - Includes actual Tk event handling, window manager work, and painting
- Includes the 250ms
- Eager show: Measured ~0.11ms per tooltip when the window already exists
- Result: Lazy loading shifts cost from startup to first hover
- good when many tooltips are never hovered
- less ideal when every tooltip is visited immediately
- Large parameter tables load much faster
- Memory usage significantly reduced
- Better responsiveness during navigation after first hover
- First tooltip hover includes the 250ms show delay plus creation cost
- Users moving mouse over parameters for the first time see a noticeable, but intentional, delay
For commit review and testing:
- All timers cleaned up on widget destruction
- Stale timer IDs handled gracefully
- Pointer position validated before creating tooltip
- No memory leaks from orphaned Tk objects
- Consistent behavior across macOS, Linux, Windows
- Edge case: widget destroyed during create_show()
- Edge case: multiple Enter/Leave in rapid succession
- Edge case: timer fires after widget destroyed
- Performance benchmark shows expected improvements
- Backward compatibility with existing Tooltip API
- Timer cancellation on widget destruction
- Pointer position validation
- Redundant tooltip prevention
- Cross-platform behavior consistency
- Large parameter table (100+ tooltips) initialization
- Rapid mouse movement through dense parameter table
- Widget destruction with pending timers
- Application shutdown with active tooltips
- Lazy vs eager initialization
- Lazy first-hover creation vs eager show
- Cleanup and destruction
- Real Tk event-loop and visible-window timing
- Check
TOOLTIP_SHOW_DELAY_MSconstant (currently 250ms) - Verify
create_show()isn't returning early due to pointer check - Check browser console for
tk.TclErrorin timer execution
- Check for orphaned tooltip windows with
tooltip.tooltip is not None - Verify
_on_widget_destroy()is being called - Check timer dict for stale entries:
tooltip.timers
- Check
schedule_show()is canceling previous timers - Increase
TOOLTIP_SHOW_DELAY_MSto reduce flicker - Verify pointer validation logic in
create_show()
frontend_tkinter_show.py: Main tooltip implementationbdd_frontend_tkinter_show.py: Comprehensive test suitetest_tooltip_performance_benchmark.py: Performance validation