feat(globalmenu): got a prototype of working globalmenu for some apps

This commit is contained in:
sankalp committed 2026-06-22 15:58:10 +05:45
1 parent dd9cd58106
commit 2a15d71c33
10 files changed
+930 -123

No files matched your search

+198 -39
View File
@@ -7,6 +7,7 @@ from fabric.widgets.label import Label
from shared.dialogs.about import AboutApp, get_about_window
from shared.window.dropdown import ModusDropdown, dropdown_divider, dropdowns
from services.globalmenu import get_global_menu_service
from utils.app_name_resolver import format_window
from utils.roam import modus_service
from utils.utils import setup_cursor_hover
@@ -45,7 +46,7 @@ def create_dropdown_with_capture(dropdown_id, parent, dropdown_children, layer="
def manage_button_style_classes(buttons, active_button=None, style_class="active"):
for button in buttons:
if button: # Check if button exists (some might be None)
if button:
button.remove_style_class(style_class)
if active_button:
@@ -103,6 +104,32 @@ def dropdown_option(
return btn
def _make_menu_item_click(item_id):
"""Create a click handler for a menu item."""
def handler(_, __=None):
global_menu_svc = get_global_menu_service()
if global_menu_svc:
global_menu_svc.click_item(item_id)
return handler
def _menu_item_to_dropdown(item):
"""Convert a DBusMenuItem to a dropdown_option widget."""
if item.type == "separator":
return dropdown_divider("---------------------")
keybind = item.shortcut if item.shortcut else ""
return dropdown_option(
item.label if item.label else "",
keybind,
on_click=None,
on_clicked=_make_menu_item_click(item.id),
)
class SystemDropdown(ModusDropdown):
def __init__(self, parent, **kwargs):
super().__init__(
@@ -131,17 +158,15 @@ class SystemDropdown(ModusDropdown):
class GlobalMenuDropdowns:
def __init__(self, parent):
def __init__(self, parent, menu_box=None):
self.parent = parent
self._menu_box = menu_box # The GlobalMenu Box widget to update
# Get the global menu service
self._global_menu_svc = get_global_menu_service()
self._has_extracted_menu = False
self.system_dropdown = SystemDropdown(parent=parent)
self.menu_button_dropdown = self.system_dropdown
self.menu_button = Button(
label="Modus",
name="global-menu",
on_clicked=lambda _: self.menu_button_dropdown.toggle(),
)
self.menu_button_dropdown.set_pointing_to(self.menu_button)
self.global_title_menu_about = dropdown_option(
f"About {modus_service.current_active_app_name}",
@@ -153,6 +178,10 @@ class GlobalMenuDropdowns:
[self.global_title_menu_about],
)
# Dynamic menu dropdowns (populated from extracted app menus)
self._dynamic_dropdowns: list[ModusDropdown] = []
# Fallback static Hyprland menu
self.global_menu_view = create_dropdown_with_capture(
"global-menu-view",
parent,
@@ -237,6 +266,8 @@ class GlobalMenuDropdowns:
"current-active-app-name-changed", self._on_active_app_changed
)
# Connect to global menu service signals (Moved to the bottom of __init__)
self.global_menu_button_title = Button(
child=ActiveWindow(
formatter=FormattedString(
@@ -249,11 +280,8 @@ class GlobalMenuDropdowns:
)
self.global_menu_title.set_pointing_to(self.global_menu_button_title)
# File, Edit and Go buttons are placeholders - no dropdowns implemented yet
self.global_menu_button_file = create_menu_button("File")
self.global_menu_button_edit = create_menu_button("Edit")
self.global_menu_button_go = create_menu_button("Go")
# Fallback buttons (shown when no app menu is extracted)
self.global_menu_button_view = create_menu_button(
"View", lambda _: self.global_menu_view.toggle()
)
@@ -267,24 +295,14 @@ class GlobalMenuDropdowns:
)
self.global_menu_help.set_pointing_to(self.global_menu_button_help)
# Start with only title
self.all_menu_buttons = [
self.menu_button,
self.global_menu_button_title,
self.global_menu_button_file,
self.global_menu_button_edit,
self.global_menu_button_view,
self.global_menu_button_go,
self.global_menu_button_window,
self.global_menu_button_help,
]
self.dropdown_button_map = {
"os-menu": self.menu_button,
"global-menu-title": self.global_menu_button_title,
"global-menu-file": self.global_menu_button_file,
"global-menu-edit": self.global_menu_button_edit,
"global-menu-view": self.global_menu_button_view,
"global-menu-go": self.global_menu_button_go,
"global-menu-window": self.global_menu_button_window,
"global-menu-help": self.global_menu_button_help,
}
@@ -292,13 +310,148 @@ class GlobalMenuDropdowns:
modus_service.connect("current-dropdown-changed", self.changed_dropdown)
modus_service.connect("dropdowns-hide-changed", self.hide_dropdowns)
# Connect to global menu service signals
if self._global_menu_svc:
self._global_menu_svc.connect("menu-changed", self._on_menu_changed)
def _on_title_button_clicked(self, _):
if has_active_window():
self.global_menu_title.toggle()
def _on_active_app_changed(self, _, value):
logger.info(f"[GlobalMenu] Active app changed: {value}")
self.global_title_menu_about.set_property("label", f"About {value}")
# Notify the global menu service about the active window change
if self._global_menu_svc:
wm_class = getattr(modus_service, "current_active_wm_class", "")
logger.info(
f"[GlobalMenu] Calling update_active_window('{value}', '{wm_class}')"
)
self._global_menu_svc.update_active_window(value, wm_class)
def _on_menu_changed(self, _, menu_items: list):
"""Handle menu changes from the GlobalMenuService."""
logger.info(
f"[GlobalMenu] _on_menu_changed called with {len(menu_items)} items"
)
self._rebuild_dynamic_menus(menu_items)
def _rebuild_dynamic_menus(self, menu_items: list):
"""Rebuild the menu bar buttons based on extracted menu items.
If the app has a menu bar → show extracted menus (File, Edit, etc.)
If not → show fallback Hyprland window controls (View, Window, Help)
"""
logger.info(f"[GlobalMenu] _rebuild_dynamic_menus: {len(menu_items)} items")
# Clean up any previous dynamic dropdowns
for dd in self._dynamic_dropdowns:
try:
dd.destroy()
except Exception:
pass
self._dynamic_dropdowns.clear()
# Filter to top-level menu items only
top_level = [
item for item in menu_items if hasattr(item, "label") and item.label
]
logger.info(f"[GlobalMenu] top_level after filter: {len(top_level)} items")
if not top_level:
# No extracted menu → show only title button
self._has_extracted_menu = False
self.all_menu_buttons = [
self.global_menu_button_title,
]
else:
# Has extracted menu → build dynamic buttons
self._has_extracted_menu = True
new_buttons = [
self.global_menu_button_title,
]
for item in top_level:
is_action = not item.has_submenu and not item.children
if is_action:
# Top-level item without children (e.g., VLC Play/Stop)
btn = create_menu_button(
item.label,
_make_menu_item_click(item.id),
)
new_buttons.append(btn)
else:
# Build initial children for this dropdown
dropdown_children = []
for child in item.children if item.children else []:
dropdown_children.append(_menu_item_to_dropdown(child))
if not dropdown_children:
dropdown_children = [
dropdown_option(f"No items in {item.label}")
]
dropdown_id = f"global-menu-{item.label.lower()}"
dropdown = create_dropdown_with_capture(
dropdown_id, self.parent, dropdown_children
)
btn = create_menu_button(
item.label,
lambda _, dd=dropdown: dd.toggle(),
)
dropdown.set_pointing_to(btn)
# Dynamically fetch submenu on hover
def make_on_enter(menu_item, dd):
def on_enter(widget, event):
if not menu_item.has_submenu:
return False
svc = get_global_menu_service()
if svc:
svc.about_to_show(menu_item.id)
new_menu = svc.refresh_menu_sync()
if new_menu:
# find the updated item
new_item = next(
(i for i in new_menu if i.id == menu_item.id),
None,
)
if new_item:
children = []
for child in (
new_item.children
if new_item.children
else []
):
children.append(
_menu_item_to_dropdown(child)
)
if not children:
children = [
dropdown_option(
f"No items in {new_item.label}"
)
]
dd.dropdown.children = children
return False
return on_enter
btn.connect("enter-notify-event", make_on_enter(item, dropdown))
self._dynamic_dropdowns.append(dropdown)
self.dropdown_button_map[dropdown_id] = btn
new_buttons.append(btn)
self.all_menu_buttons = new_buttons
# Update the panel (the actual GTK Box widget)
if self._menu_box is not None:
self._menu_box.children = self.all_menu_buttons
def hide_dropdowns(self, *_):
manage_button_style_classes(self.all_menu_buttons)
@@ -315,9 +468,24 @@ class GlobalMenuDropdowns:
except Exception as e:
logger.error(f"An error occurred: {e}")
# Destroy all dropdown captures
# Disconnect global menu service
if self._global_menu_svc:
try:
self._global_menu_svc.disconnect_by_func(self._on_menu_changed)
except Exception:
pass
# Destroy dynamic dropdowns
for dd in self._dynamic_dropdowns:
try:
dd.destroy()
except Exception as e:
logger.error(f"An error occurred: {e}")
self._dynamic_dropdowns.clear()
# Destroy static dropdown captures
dropdown_captures = [
getattr(self, "menu_button_dropdown", None),
getattr(self, "system_dropdown", None),
getattr(self, "global_menu_title", None),
getattr(self, "global_menu_view", None),
getattr(self, "global_menu_window", None),
@@ -342,22 +510,13 @@ class GlobalMenu(Box):
name="globalmenu", orientation="horizontal", spacing=0, **kwargs
)
self.dropdown_system = GlobalMenuDropdowns(parent=parent_window)
self.dropdown_system = GlobalMenuDropdowns(parent=parent_window, menu_box=self)
self.children = [
self.dropdown_system.global_menu_button_title,
self.dropdown_system.global_menu_button_file,
self.dropdown_system.global_menu_button_edit,
self.dropdown_system.global_menu_button_view,
self.dropdown_system.global_menu_button_go,
self.dropdown_system.global_menu_button_window,
self.dropdown_system.global_menu_button_help,
]
self.children = self.dropdown_system.all_menu_buttons
def show_system_dropdown(self, imac_button):
self.dropdown_system.menu_button_dropdown.set_pointing_to(imac_button)
mouse_capture = self.dropdown_system.menu_button_dropdown
mouse_capture.toggle()
self.dropdown_system.system_dropdown.set_pointing_to(imac_button)
self.dropdown_system.system_dropdown.toggle()
def destroy(self):
"""Clean up the global menu and its dropdowns"""