From b06b3f98b1856e52b6d79151c92ba8789c582a05 Mon Sep 17 00:00:00 2001 From: rob Date: Sun, 4 Jan 2026 13:05:32 -0400 Subject: [PATCH] feat: Custom sticky tooltip for Dictate button with fade-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tooltip appears on hover and stays while mouse is still - Starts 2-second fade when mouse moves or button is clicked - Custom popup window instead of built-in tooltip for better control - Automatically cleaned up when dialog closes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/discussions/ui/gui.py | 133 ++++++++++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 14 deletions(-) diff --git a/src/discussions/ui/gui.py b/src/discussions/ui/gui.py index 6878376..c22ab86 100644 --- a/src/discussions/ui/gui.py +++ b/src/discussions/ui/gui.py @@ -413,6 +413,9 @@ class DiscussionGUI: """Handle mouse down on dictate button - start timing for mode detection.""" import time + # Hide tooltip when button is clicked + self._hide_dictate_tooltip() + current_time = time.time() # Check for double-click (two clicks within 300ms) @@ -481,6 +484,108 @@ class DiscussionGUI: if self._continuous_recorder is None: self._start_dictation() + def _show_dictate_tooltip(self): + """Show the custom dictate button tooltip.""" + if dpg.does_item_exist("dictate_tooltip_window"): + return # Already visible + + # Get button position for tooltip placement + if dpg.does_item_exist("dictate_btn"): + btn_pos = dpg.get_item_pos("dictate_btn") + # Get the parent window position to calculate absolute position + parent_pos = dpg.get_item_pos("comment_dialog") if dpg.does_item_exist("comment_dialog") else (0, 0) + tooltip_x = parent_pos[0] + btn_pos[0] + 110 # Right of button + tooltip_y = parent_pos[1] + btn_pos[1] + 30 + else: + tooltip_x, tooltip_y = 500, 300 + + self._tooltip_visible = True + self._tooltip_fading = False + self._tooltip_alpha = 1.0 + self._tooltip_last_mouse_pos = dpg.get_mouse_pos(local=False) + + # Create tooltip window + with dpg.window( + tag="dictate_tooltip_window", + no_title_bar=True, + no_resize=True, + no_move=True, + no_scrollbar=True, + no_collapse=True, + no_background=False, + pos=[tooltip_x, tooltip_y], + autosize=True + ): + dpg.add_text("Voice Dictation", tag="tt_title", color=(200, 200, 255)) + dpg.add_separator() + dpg.add_text("Double-click: Continuous mode", tag="tt_line1") + dpg.add_text(" Transcribed text appends to end.", tag="tt_line2", color=(150, 150, 150)) + dpg.add_text(" Click again to stop.", tag="tt_line3", color=(150, 150, 150)) + dpg.add_spacer(height=5) + dpg.add_text("Click & hold: Walkie-talkie mode", tag="tt_line4") + dpg.add_text(" Inserts at last edited position.", tag="tt_line5", color=(150, 150, 150)) + dpg.add_text(" Release to stop.", tag="tt_line6", color=(150, 150, 150)) + dpg.add_spacer(height=5) + dpg.add_text("Tip: Type at insertion point first", tag="tt_line7", color=(255, 200, 100)) + dpg.add_text("to set cursor position.", tag="tt_line8", color=(255, 200, 100)) + + def _hide_dictate_tooltip(self): + """Hide the custom dictate button tooltip immediately.""" + if dpg.does_item_exist("dictate_tooltip_window"): + dpg.delete_item("dictate_tooltip_window") + self._tooltip_visible = False + self._tooltip_fading = False + + def _start_tooltip_fade(self): + """Start fading out the tooltip.""" + import time + if self._tooltip_visible and not self._tooltip_fading: + self._tooltip_fading = True + self._tooltip_fade_start_time = time.time() + + def _update_tooltip_fade(self): + """Update tooltip fade animation. Call this from frame callback.""" + import time + + if not self._tooltip_visible: + return + + current_mouse = dpg.get_mouse_pos(local=False) + + # Check if mouse moved significantly + dx = abs(current_mouse[0] - self._tooltip_last_mouse_pos[0]) + dy = abs(current_mouse[1] - self._tooltip_last_mouse_pos[1]) + + if dx > 5 or dy > 5: # Mouse moved + if not self._tooltip_fading: + self._start_tooltip_fade() + self._tooltip_last_mouse_pos = current_mouse + + # Handle fade animation + if self._tooltip_fading: + elapsed = time.time() - self._tooltip_fade_start_time + fade_duration = 2.0 # 2 seconds to fade + + if elapsed >= fade_duration: + self._hide_dictate_tooltip() + else: + # Calculate alpha (1.0 to 0.0 over fade_duration) + self._tooltip_alpha = 1.0 - (elapsed / fade_duration) + # Apply alpha to window + if dpg.does_item_exist("dictate_tooltip_window"): + # DearPyGui doesn't have direct alpha control, so we hide at threshold + if self._tooltip_alpha < 0.1: + self._hide_dictate_tooltip() + + def _on_dictate_hover(self, sender, app_data): + """Handle mouse hovering over dictate button.""" + self._show_dictate_tooltip() + + def _on_dictate_unhover(self): + """Handle mouse leaving dictate button area - start fade.""" + if self._tooltip_visible: + self._start_tooltip_fade() + def _start_dictation(self): """Start continuous recording with chunked transcription.""" # Create recorder with callbacks @@ -696,6 +801,13 @@ class DiscussionGUI: self._last_text_value = "" self._approx_cursor_pos = 0 # Approximate cursor position for insertion + # Custom tooltip state for sticky fade behavior + self._tooltip_visible = False + self._tooltip_last_mouse_pos = (0, 0) + self._tooltip_fade_start_time = 0.0 + self._tooltip_fading = False + self._tooltip_alpha = 1.0 + # Initialize Dear PyGui dpg.create_context() dpg.create_viewport(title="Orchestrated Discussions", width=1400, height=900) @@ -2060,6 +2172,8 @@ class DiscussionGUI: dpg.delete_item(window_tag) if dpg.does_item_exist("dictate_btn_handlers"): dpg.delete_item("dictate_btn_handlers") + # Clean up any existing tooltip + self._hide_dictate_tooltip() # Reset dictation state when dialog opens self._dictation_mode = "idle" self._last_dictate_click_time = 0.0 @@ -2078,25 +2192,13 @@ class DiscussionGUI: tag="dictate_btn", width=100 ) - # Add tooltip with usage instructions - with dpg.tooltip("dictate_btn"): - dpg.add_text("Voice Dictation", color=(200, 200, 255)) - dpg.add_separator() - dpg.add_text("Double-click: Continuous mode") - dpg.add_text(" Transcribed text appends to end.", color=(150, 150, 150)) - dpg.add_text(" Click again to stop.", color=(150, 150, 150)) - dpg.add_spacer(height=5) - dpg.add_text("Click & hold: Walkie-talkie mode") - dpg.add_text(" Inserts at last edited position.", color=(150, 150, 150)) - dpg.add_text(" Release to stop.", color=(150, 150, 150)) - dpg.add_spacer(height=5) - dpg.add_text("Tip: Type at insertion point first", color=(255, 200, 100)) - dpg.add_text("to set cursor position.", color=(255, 200, 100)) # Add item handlers for press-and-hold vs double-click detection + # Also includes hover handler for custom sticky tooltip with dpg.item_handler_registry(tag="dictate_btn_handlers"): dpg.add_item_activated_handler(callback=self._on_dictate_activated) dpg.add_item_deactivated_handler(callback=self._on_dictate_deactivated) dpg.add_item_active_handler(callback=self._on_dictate_active) + dpg.add_item_hover_handler(callback=self._on_dictate_hover) dpg.bind_item_handler_registry("dictate_btn", "dictate_btn_handlers") dpg.add_button( label="Add Artifact", @@ -4144,6 +4246,9 @@ final = json.dumps(parsed)''', # Poll for updates from background threads (output, turn completion) self._poll_background_tasks() + # Update tooltip fade animation + self._update_tooltip_fade() + # Render frame dpg.render_dearpygui_frame()