diff --git a/modules/panel/components/enhanced_system_tray.py b/modules/panel/components/enhanced_system_tray.py index 9943e38..ac7e63b 100644 --- a/modules/panel/components/enhanced_system_tray.py +++ b/modules/panel/components/enhanced_system_tray.py @@ -3,23 +3,13 @@ Enhanced System Tray Icon Handling This module provides enhanced icon loading capabilities for system tray items, including fallback mechanisms for file paths and common icon locations. -It also adds dropdown menu functionality for system tray items with: - -- Dynamic menu state tracking (checkboxes, radio buttons) -- Real-time menu refresh on each right-click -- Proper toggle state visualization (✓ for checked, ● for radio selected) -- Enhanced error handling and debugging -- Support for enabled/disabled menu items """ import os from gi.repository import GdkPixbuf, Gtk -from fabric.widgets.label import Label from fabric.system_tray.widgets import SystemTrayItem -from widgets.dropdown import ModusDropdown, dropdown_divider -from widgets.mousecapture import DropDownMouseCapture # FIX: the tooltip should show application names instead of unknown @@ -158,617 +148,18 @@ def _set_tooltip(self): ) -def dropdown_option( - label: str = "", - on_click=None, - submenu_items=None, - has_native_submenu=False, - native_menu_item=None, -): - """Create a dropdown option for system tray items with optional submenu""" - from fabric.widgets.button import Button - from fabric.widgets.centerbox import CenterBox - from fabric.widgets.box import Box - - # If this has native submenu (like NetworkManager WiFi list), create special expandable option - if has_native_submenu and native_menu_item: - from fabric.widgets.revealer import Revealer - - # State for submenu expansion - submenu_expanded = False - - def toggle_native_submenu(_): - nonlocal submenu_expanded - submenu_expanded = not submenu_expanded - submenu_revealer.set_reveal_child(submenu_expanded) - - # Update the arrow indicator - arrow_label = "▼" if submenu_expanded else "▶" - main_button.get_child().get_start_children()[0].set_label( - f"{label} {arrow_label}" - ) - - print( - f"[DEBUG] Native submenu { - 'expanded' if submenu_expanded else 'collapsed' - }" - ) - - def show_native_menu(_): - """Show the native context menu for this item""" - try: - print(f"[DEBUG] Showing native menu for '{label}'") - - # Hide our dropdown first - from widgets.dropdown import dropdowns - - for dropdown in dropdowns: - if dropdown.is_visible() and hasattr( - dropdown, "hide_via_mousecapture" - ): - dropdown.hide_via_mousecapture() - break - - # Try to activate the native menu item - if hasattr(native_menu_item, "activate"): - native_menu_item.activate() - elif hasattr(native_menu_item, "handle_event"): - native_menu_item.handle_event("clicked", None, 0) - - except Exception as e: - print(f"[DEBUG] Failed to show native menu for '{label}': {e}") - - # Create main button with arrow indicator - main_button = Button( - child=CenterBox( - start_children=[ - Label( - label=f"{label} ▶", - h_align="start", - name="dropdown-option-label", - ), - ], - orientation="horizontal", - h_align="fill", - h_expand=True, - v_expand=True, - ), - name="dropdown-option", - h_align="fill", - on_clicked=toggle_native_submenu, - h_expand=True, - v_expand=True, - ) - - # Create submenu with native menu option - native_submenu_items = [ - Button( - child=CenterBox( - start_children=[ - Label( - label=" Show Native Menu", - h_align="start", - name="dropdown-option-label", - ), - ], - orientation="horizontal", - h_align="fill", - h_expand=True, - v_expand=True, - ), - name="dropdown-option", - h_align="fill", - on_clicked=show_native_menu, - h_expand=True, - v_expand=True, - ), - Button( - child=CenterBox( - start_children=[ - Label( - label=" (Hover over tray icon for full menu)", - h_align="start", - name="dropdown-option-label", - ), - ], - orientation="horizontal", - h_align="fill", - h_expand=True, - v_expand=True, - ), - name="dropdown-option", - h_align="fill", - sensitive=False, # Disabled info text - h_expand=True, - v_expand=True, - ), - ] - - # Create revealer for submenu - submenu_revealer = Revealer( - child=Box( - children=native_submenu_items, - orientation="vertical", - name="submenu-items", - ), - reveal_child=False, - transition_type="slide-down", - transition_duration=200, - ) - - # Return container with main button and submenu - return Box( - children=[main_button, submenu_revealer], - orientation="vertical", - name="dropdown-option-with-submenu", - ) - - # If this has submenu items, create an expandable option - elif submenu_items: - from fabric.widgets.revealer import Revealer - - # State for submenu expansion - submenu_expanded = False - - def toggle_submenu(_): - nonlocal submenu_expanded - submenu_expanded = not submenu_expanded - submenu_revealer.set_reveal_child(submenu_expanded) - - # Update the arrow indicator - arrow_label = "▼" if submenu_expanded else "▶" - main_button.get_child().get_start_children()[0].set_label( - f"{label} {arrow_label}" - ) - - print(f"[DEBUG] Submenu {'expanded' if submenu_expanded else 'collapsed'}") - - # Create main button with arrow indicator - main_button = Button( - child=CenterBox( - start_children=[ - Label( - label=f"{label} ▶", - h_align="start", - name="dropdown-option-label", - ), - ], - orientation="horizontal", - h_align="fill", - h_expand=True, - v_expand=True, - ), - name="dropdown-option", - h_align="fill", - on_clicked=toggle_submenu, - h_expand=True, - v_expand=True, - ) - - # Create submenu items with indentation - indented_submenu_items = [] - for item in submenu_items: - # Add indentation to submenu items - if hasattr(item, "get_child") and hasattr( - item.get_child(), "get_start_children" - ): - original_label = item.get_child().get_start_children()[0] - if hasattr(original_label, "get_label"): - original_text = original_label.get_label() - original_label.set_label(f" {original_text}") # Add indentation - indented_submenu_items.append(item) - - # Create revealer for submenu - submenu_revealer = Revealer( - child=Box( - children=indented_submenu_items, - orientation="vertical", - name="submenu-items", - ), - reveal_child=False, - transition_type="slide-down", - transition_duration=200, - ) - - # Return container with main button and submenu - return Box( - children=[main_button, submenu_revealer], - orientation="vertical", - name="dropdown-option-with-submenu", - ) - - # Regular option without submenu - def on_click_handler(_): - if on_click: - on_click() - # Hide dropdown after action - from widgets.dropdown import dropdowns - - for dropdown in dropdowns: - if dropdown.is_visible() and hasattr(dropdown, "hide_via_mousecapture"): - dropdown.hide_via_mousecapture() - break - - return Button( - child=CenterBox( - start_children=[ - Label(label=label, h_align="start", name="dropdown-option-label"), - ], - orientation="horizontal", - h_align="fill", - h_expand=True, - v_expand=True, - ), - name="dropdown-option", - h_align="fill", - on_clicked=on_click_handler, - h_expand=True, - v_expand=True, - ) - - -# Note: Submenu functionality is now handled inline within dropdown_option -# using Revealer widgets for expandable menu sections - - -def extract_menu_items(menu, tray_item): - """Recursively extract menu items from a DbusmenuGtk3.Menu with state tracking""" - options = [] - - try: - if hasattr(menu, "get_children"): - children = menu.get_children() - for child in children: - try: - # Get menu item properties - label = "" - if hasattr(child, "property_get"): - label = child.property_get("label") or "" - elif hasattr(child, "get_label"): - label = child.get_label() or "" - - # Skip empty labels and separators - if not label or label.strip() == "": - if ( - hasattr(child, "property_get") - and child.property_get("type") == "separator" - ): - options.append(dropdown_divider("")) - continue - - # Check for toggle state and other properties - toggle_type = None - toggle_state = None - enabled = True - visible = True - - if hasattr(child, "property_get"): - toggle_type = child.property_get("toggle-type") - toggle_state = child.property_get("toggle-state") - enabled = child.property_get("enabled") - visible = child.property_get("visible") - - # Handle different property formats - if enabled is None: - enabled = True - if visible is None: - visible = True - - # Debug output for complex menu items - if toggle_type or toggle_state is not None: - print( - f"[DEBUG] Menu item '{label}': toggle_type={ - toggle_type - }, toggle_state={toggle_state}, enabled={enabled}" - ) - - # Skip invisible items - if not visible: - continue - - # Format label based on toggle state - display_label = label.replace("_", "") # Remove mnemonics - - if toggle_type == "checkmark" and toggle_state is not None: - # Add checkmark for checked items - if toggle_state == 1 or toggle_state is True: # Checked - display_label = f"✓ {display_label}" - else: # Unchecked - display_label = f" {display_label}" - elif toggle_type == "radio" and toggle_state is not None: - # Add radio button indicator - if toggle_state == 1 or toggle_state is True: # Selected - display_label = f"● {display_label}" - else: # Not selected - display_label = f"○ {display_label}" - - # Create action for this menu item - def create_menu_action(menu_child): - def action(): - try: - if hasattr(menu_child, "handle_event"): - menu_child.handle_event("clicked", None, 0) - elif hasattr(menu_child, "activate"): - menu_child.activate() - else: - print( - f"[DEBUG] No activation method found for menu item: {label}" - ) - except Exception as e: - print( - f"[DEBUG] Failed to activate menu item '{label}': { - e - }" - ) - - return action - - # Handle submenus - submenu_items = None - has_native_submenu = False - - if hasattr(child, "get_submenu"): - submenu = child.get_submenu() - if submenu: - submenu_items = extract_menu_items(submenu, tray_item) - if submenu_items: - print( - f"[DEBUG] Found submenu for '{label}' with { - len(submenu_items) - } items" - ) - - # Check for potential native submenus (common patterns) - if not submenu_items and hasattr(child, "property_get"): - # Check for indicators that this might have a native submenu - item_type = child.property_get("type") or "" - children_display = child.property_get("children-display") or "" - - # Common patterns for items that show native submenus on hover - submenu_indicators = [ - "WiFi Networks" in label, - "Available Networks" in label, - "Connect to" in label, - "Bluetooth" in label - and ("Device" in label or "Connect" in label), - "Audio" in label - and ("Device" in label or "Output" in label), - "VPN" in label and "Connect" in label, - children_display == "submenu", - item_type == "submenu", - # Additional patterns for common system tray items - label.endswith("...") - and ( - "Network" in label - or "Audio" in label - or "Bluetooth" in label - ), - "Select" in label - and ("Device" in label or "Network" in label), - ] - - if any(submenu_indicators): - has_native_submenu = True - print( - f"[DEBUG] Detected potential native submenu for '{label}'" - ) - - # Create dropdown option with proper state indication and submenu - option = dropdown_option( - display_label, - on_click=( - create_menu_action(child) - if not submenu_items and not has_native_submenu - else None - ), - submenu_items=submenu_items, - has_native_submenu=has_native_submenu, - native_menu_item=child if has_native_submenu else None, - ) - - # Disable option if not enabled - if not enabled: - option.set_sensitive(False) - - options.append(option) - - except Exception as e: - print(f"[DEBUG] Error processing menu item: {e}") - continue - - except Exception: - pass - - return options - - -def get_tray_menu_options(tray_item): - """Extract menu options from the system tray item's native menu""" - options = [] - app_name = tray_item._item.title or tray_item._item.identifier or "Unknown App" - - # Create unique functions for this specific tray item to avoid closure issues - def activate_app(): - try: - tray_item._item.activate_for_event(None) - except Exception as e: - print(f"[DEBUG] Failed to activate {app_name}: {e}") - - def show_native_menu(): - try: - tray_item._item.invoke_menu_for_event(None) - except Exception as e: - print(f"[DEBUG] Failed to show native menu for {app_name}: {e}") - - try: - # Try to get the actual menu items from the tray item - menu = None - if hasattr(tray_item._item, "menu"): - menu = tray_item._item.menu - elif hasattr(tray_item._item, "get_menu"): - menu = tray_item._item.get_menu() - - menu_extraction_successful = False - - if menu is not None: - print(f"[DEBUG] Extracting menu for {app_name}, menu type: {type(menu)}") - # Extract actual menu items - extracted_options = extract_menu_items(menu, tray_item) - if extracted_options: - print( - f"[DEBUG] Successfully extracted { - len(extracted_options) - } menu items for {app_name}" - ) - # Add dividers after each option (except the last one) - for i, option in enumerate(extracted_options): - options.append(option) - # Add divider after each option except the last one - if i < len(extracted_options) - 1: - options.append(dropdown_divider("")) - - menu_extraction_successful = True - else: - print(f"[DEBUG] No menu items extracted for {app_name}") - - # Only add fallback options if menu extraction was not successful - if not menu_extraction_successful: - if menu is not None: - # Menu exists but extraction failed, show native menu option - print( - f"[DEBUG] Menu exists but extraction failed for { - app_name - }, adding fallback" - ) - options.append( - dropdown_option(f"Show {app_name} Menu", on_click=show_native_menu) - ) - else: - # No menu available, just add basic activation - print(f"[DEBUG] No menu available for {app_name}, adding basic options") - options.append( - dropdown_option(f"Open {app_name}", on_click=activate_app) - ) - options.append(dropdown_divider("")) - options.append( - dropdown_option("Show Context Menu", on_click=show_native_menu) - ) - - except Exception as e: - print(f"[DEBUG] Exception while extracting menu for {app_name}: {e}") - # Fallback to basic options if anything fails - options = [ - dropdown_option(f"Open {app_name}", on_click=activate_app), - dropdown_divider(""), - dropdown_option("Show Context Menu", on_click=show_native_menu), - ] - - return options - - -def refresh_dropdown_menu(dropdown, tray_item): - """Refresh the dropdown menu with current menu state""" - try: - # Get fresh menu options - fresh_options = get_tray_menu_options(tray_item) - - # Update the dropdown children - if hasattr(dropdown, "child_window") and hasattr( - dropdown.child_window, "dropdown" - ): - dropdown.child_window.dropdown.children = fresh_options - - return True - except Exception: - return False - - -def create_system_tray_dropdown(tray_item, parent=None): - """Create a dropdown menu for a system tray item""" - dropdown_children = get_tray_menu_options(tray_item) - - # Create unique ID using both identifier and object id to ensure uniqueness - unique_id = f"systray-{tray_item._item.identifier}-{id(tray_item)}" - - dropdown = ModusDropdown( - parent=parent, - dropdown_id=unique_id, - dropdown_children=dropdown_children, - ) - - dropdown_menu = DropDownMouseCapture(layer="bottom", child_window=dropdown) - - # Store reference to tray item for refreshing - dropdown_menu._tray_item = tray_item - - # Custom positioning for system tray items - def custom_get_coords_for_widget(widget): - """Custom coordinate calculation for system tray items""" - if not ((toplevel := widget.get_toplevel()) and toplevel.is_toplevel()): - return 0, 0 - - # Get the widget's allocation and position - allocation = widget.get_allocation() - - # Try to get absolute position on screen - try: - # Get widget position relative to toplevel - this should give us the correct coordinates - widget_x, widget_y = widget.translate_coordinates(toplevel, 0, 0) or (0, 0) - - # Position the dropdown below the tray item - # Don't divide by 2 like the original method does - that's causing the positioning issue - return widget_x, widget_y + allocation.height - - except Exception: - # Fallback to basic positioning - return 0, allocation.height - - # Override the get_coords_for_widget method for this specific dropdown window - # Store the original method in case we need it - dropdown._original_get_coords_for_widget = dropdown.get_coords_for_widget - - # Replace with our custom method - def bound_custom_get_coords(widget): - return custom_get_coords_for_widget(widget) - - dropdown.get_coords_for_widget = bound_custom_get_coords - - dropdown_menu.child_window.set_pointing_to(tray_item) - - return dropdown_menu - - -def patched_on_clicked(self, widget, event): - """Enhanced click handler that adds dropdown functionality""" - # Right click shows our custom dropdown instead of native menu - if event.button == 3: - # Always refresh the dropdown to get current menu state - parent = self.get_toplevel() - self._dropdown_capture = create_system_tray_dropdown(self, parent) - - # Toggle the dropdown (positioning is handled by custom positioning function) - self._dropdown_capture.toggle_mousecapture() - return True - - # For left clicks and other buttons, use original behavior - return original_on_clicked(self, widget, event) - - def apply_enhanced_system_tray(): - # Store original methods - global original_do_update_properties, original_on_clicked + # Store original method + global original_do_update_properties original_do_update_properties = SystemTrayItem.do_update_properties - original_on_clicked = SystemTrayItem.on_clicked # Attach helper methods to SystemTrayItem class SystemTrayItem._try_load_icon_from_path = _try_load_icon_from_path SystemTrayItem._set_tooltip = _set_tooltip - # Replace the methods + # Replace the do_update_properties method SystemTrayItem.do_update_properties = patched_do_update_properties - SystemTrayItem.on_clicked = patched_on_clicked -# Store references to original methods +# Store reference to original method original_do_update_properties = None -original_on_clicked = None