775 lines
29 KiB
Python
775 lines
29 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 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
|
|
|
|
|
|
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,
|
|
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
|
|
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
|