♻️ refactor(panel): simplify dropdown menu creation for system tray
This commit is contained in:
1 parent
6102b77c52
commit
1ec5d87477
1 file changed
+24
-321
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user