♻️ refactor(panel): simplify dropdown menu creation for system tray

This commit is contained in:
sankalp committed 2025-08-04 09:09:22 +05:45
1 parent 6102b77c52
commit 1ec5d87477
1 file changed
+24 -321
+24 -321
View File
@@ -3,13 +3,7 @@ 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
It also adds dropdown menu functionality for system tray items.
"""
import os
@@ -158,191 +152,11 @@ 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"""
def dropdown_option(label: str = "", on_click=None):
"""Create a dropdown option for system tray items"""
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()
@@ -371,12 +185,8 @@ def dropdown_option(label: str = "", on_click=None, submenu_items=None, has_nati
)
# 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"""
"""Recursively extract menu items from a DbusmenuGtk3.Menu"""
options = []
try:
@@ -397,48 +207,6 @@ def extract_menu_items(menu, tray_item):
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():
@@ -447,65 +215,24 @@ def extract_menu_items(menu, tray_item):
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}")
except Exception:
pass
return action
# Handle submenus
submenu_items = None
has_native_submenu = False
options.append(dropdown_option(
label.replace("_", ""), # Remove mnemonics
on_click=create_menu_action(child)
))
# Handle submenus
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")
sub_options = extract_menu_items(submenu, tray_item)
if sub_options:
options.extend(sub_options)
# 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}")
except Exception:
continue
except Exception:
@@ -523,14 +250,14 @@ def get_tray_menu_options(tray_item):
def activate_app():
try:
tray_item._item.activate_for_event(None)
except Exception as e:
print(f"[DEBUG] Failed to activate {app_name}: {e}")
except Exception:
pass
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}")
except Exception:
pass
try:
# Try to get the actual menu items from the tray item
@@ -543,11 +270,9 @@ def get_tray_menu_options(tray_item):
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)
@@ -556,21 +281,17 @@ 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
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
@@ -581,8 +302,7 @@ def get_tray_menu_options(tray_item):
on_click=show_native_menu
))
except Exception as e:
print(f"[DEBUG] Exception while extracting menu for {app_name}: {e}")
except Exception:
# Fallback to basic options if anything fails
options = [
dropdown_option(
@@ -599,21 +319,6 @@ def get_tray_menu_options(tray_item):
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)
@@ -632,9 +337,6 @@ def create_system_tray_dropdown(tray_item, parent=None):
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"""
@@ -677,9 +379,10 @@ 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)
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)
# Toggle the dropdown (positioning is handled by custom positioning function)
self._dropdown_capture.toggle_mousecapture()