From 366e035395be53d52a1d4513bce8ee8acfd5f128 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Mon, 4 Aug 2025 10:30:03 +0545 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(panel):=20enhance=20system=20t?= =?UTF-8?q?ray=20menus=20with=20dynamic=20updates=20and=20submenu=20suppor?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../panel/components/enhanced_system_tray.py | 478 +++++++++++++++--- 1 file changed, 420 insertions(+), 58 deletions(-) diff --git a/modules/panel/components/enhanced_system_tray.py b/modules/panel/components/enhanced_system_tray.py index 56ba1f8..9943e38 100644 --- a/modules/panel/components/enhanced_system_tray.py +++ b/modules/panel/components/enhanced_system_tray.py @@ -3,7 +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. +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 @@ -152,16 +158,232 @@ def _set_tooltip(self): ) -def dropdown_option(label: str = "", on_click=None): - """Create a dropdown option for system tray items""" +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() @@ -185,54 +407,173 @@ def dropdown_option(label: str = "", on_click=None): ) +# 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""" + """Recursively extract menu items from a DbusmenuGtk3.Menu with state tracking""" options = [] try: - if hasattr(menu, 'get_children'): + if hasattr(menu, "get_children"): children = menu.get_children() for child in children: try: # Get menu item properties label = "" - if hasattr(child, 'property_get'): + if hasattr(child, "property_get"): label = child.property_get("label") or "" - elif hasattr(child, 'get_label'): + 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": + 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'): + if hasattr(menu_child, "handle_event"): menu_child.handle_event("clicked", None, 0) - elif hasattr(menu_child, 'activate'): + elif hasattr(menu_child, "activate"): menu_child.activate() - except Exception: - pass + 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 - options.append(dropdown_option( - label.replace("_", ""), # Remove mnemonics - on_click=create_menu_action(child) - )) - # Handle submenus - if hasattr(child, 'get_submenu'): + submenu_items = None + has_native_submenu = False + + if hasattr(child, "get_submenu"): submenu = child.get_submenu() if submenu: - sub_options = extract_menu_items(submenu, tray_item) - if sub_options: - options.extend(sub_options) + submenu_items = extract_menu_items(submenu, tray_item) + if submenu_items: + print( + f"[DEBUG] Found submenu for '{label}' with { + len(submenu_items) + } items" + ) - except Exception: + # 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: @@ -250,29 +591,35 @@ def get_tray_menu_options(tray_item): def activate_app(): try: tray_item._item.activate_for_event(None) - except Exception: - pass + 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: - pass + 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'): + if hasattr(tray_item._item, "menu"): menu = tray_item._item.menu - elif hasattr(tray_item._item, 'get_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) @@ -281,44 +628,61 @@ def get_tray_menu_options(tray_item): 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 - options.append(dropdown_option( - f"Show {app_name} Menu", - on_click=show_native_menu - )) + 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 - options.append(dropdown_option( - f"Open {app_name}", - on_click=activate_app - )) + 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 - )) + options.append( + dropdown_option("Show Context Menu", on_click=show_native_menu) + ) - except Exception: + 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_option(f"Open {app_name}", on_click=activate_app), dropdown_divider(""), - dropdown_option( - "Show Context Menu", - on_click=show_native_menu - ), + 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) @@ -332,10 +696,10 @@ def create_system_tray_dropdown(tray_item, parent=None): dropdown_children=dropdown_children, ) - dropdown_menu = DropDownMouseCapture( - layer="bottom", - child_window=dropdown - ) + 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): @@ -374,15 +738,13 @@ def create_system_tray_dropdown(tray_item, parent=None): 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: - if not hasattr(self, '_dropdown_capture'): - # Try to find the parent window by traversing up the widget hierarchy - parent = self.get_toplevel() - self._dropdown_capture = create_system_tray_dropdown(self, parent) + # 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()