Files
Modus/modules/panel/components/enhanced_system_tray.py
T

413 lines
15 KiB
Python

"""
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.
"""
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
def patched_do_update_properties(self, *_):
# Try default GTK theme first
icon_name = self._item.icon_name
attention_icon_name = self._item.attention_icon_name
if self._item.status == "NeedsAttention" and attention_icon_name:
preferred_icon_name = attention_icon_name
else:
preferred_icon_name = icon_name
# Try to load from default GTK theme
if preferred_icon_name:
try:
default_theme = Gtk.IconTheme.get_default()
if default_theme.has_icon(preferred_icon_name):
pixbuf = default_theme.load_icon(
preferred_icon_name, self._icon_size, Gtk.IconLookupFlags.FORCE_SIZE
)
if pixbuf:
self._image.set_from_pixbuf(pixbuf)
# Set tooltip
tooltip = self._item.tooltip
self.set_tooltip_markup(
tooltip.description or tooltip.title or self._item.title.title()
if self._item.title
else "Unknown"
)
return
except:
pass
# Enhanced fallback handling for file paths
if preferred_icon_name and self._try_load_icon_from_path(preferred_icon_name):
return
# Fallback to original implementation
original_do_update_properties(self, *_)
def _try_load_icon_from_path(self, icon_path):
try:
# Check if it's a file path and handle it directly
if os.path.isabs(icon_path) or "/" in icon_path:
# Try to load as SVG from the original path if it exists
if os.path.exists(icon_path):
if icon_path.lower().endswith(".svg"):
# Load SVG directly
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
icon_path, self._icon_size, self._icon_size
)
if pixbuf:
self._image.set_from_pixbuf(pixbuf)
self._set_tooltip()
return True
else:
# Load other image formats
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
icon_path, self._icon_size, self._icon_size
)
if pixbuf:
self._image.set_from_pixbuf(pixbuf)
self._set_tooltip()
return True
# If it's a file path, try to extract just the filename for theme lookup
filename = os.path.basename(icon_path)
if filename:
# Remove extension for theme lookup
name_without_ext = os.path.splitext(filename)[0]
default_theme = Gtk.IconTheme.get_default()
# Try filename without extension
if default_theme.has_icon(name_without_ext):
pixbuf = default_theme.load_icon(
name_without_ext,
self._icon_size,
Gtk.IconLookupFlags.FORCE_SIZE,
)
if pixbuf:
self._image.set_from_pixbuf(pixbuf)
self._set_tooltip()
return True
# Try full filename
if default_theme.has_icon(filename):
pixbuf = default_theme.load_icon(
filename, self._icon_size, Gtk.IconLookupFlags.FORCE_SIZE
)
if pixbuf:
self._image.set_from_pixbuf(pixbuf)
self._set_tooltip()
return True
# If it looks like a file path but doesn't exist, try common icon locations
if os.path.isabs(icon_path):
common_icon_dirs = [
"/usr/share/icons",
"/usr/share/pixmaps",
"/usr/local/share/icons",
"/usr/local/share/pixmaps",
os.path.expanduser("~/.local/share/icons"),
os.path.expanduser("~/.icons"),
]
filename = os.path.basename(icon_path)
for icon_dir in common_icon_dirs:
potential_path = os.path.join(icon_dir, filename)
if os.path.exists(potential_path):
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
potential_path, self._icon_size, self._icon_size
)
if pixbuf:
self._image.set_from_pixbuf(pixbuf)
self._set_tooltip()
return True
except:
continue
except Exception:
pass
return False
def _set_tooltip(self):
tooltip = self._item.tooltip
self.set_tooltip_markup(
tooltip.description or tooltip.title or self._item.title.title()
if self._item.title
else "Unknown"
)
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
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,
)
def extract_menu_items(menu, tray_item):
"""Recursively extract menu items from a DbusmenuGtk3.Menu"""
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
# 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()
except Exception:
pass
return action
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:
sub_options = extract_menu_items(submenu, tray_item)
if sub_options:
options.extend(sub_options)
except Exception:
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:
pass
def show_native_menu():
try:
tray_item._item.invoke_menu_for_event(None)
except Exception:
pass
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:
# Extract actual menu items
extracted_options = extract_menu_items(menu, tray_item)
if extracted_options:
# 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
# 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
))
else:
# No menu available, just add basic activation
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:
# 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 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
)
# 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:
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()
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
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
SystemTrayItem.do_update_properties = patched_do_update_properties
SystemTrayItem.on_clicked = patched_on_clicked
# Store references to original methods
original_do_update_properties = None
original_on_clicked = None