from fabric.hyprland.widgets import HyprlandActiveWindow as ActiveWindow from fabric.utils import FormattedString, exec_shell_command_async, logger from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox 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 from window.settings.main import get_settings_window def has_active_window(): return ( modus_service.current_active_app_name and modus_service.current_active_app_name != "Finder" ) def create_menu_button(label, on_clicked=None): button_kwargs = { "label": label, "name": "global-menu", } if on_clicked is not None: button_kwargs["on_clicked"] = on_clicked button = Button(**button_kwargs) setup_cursor_hover(button, "pointer") return button def create_dropdown_with_capture(dropdown_id, parent, dropdown_children, layer="top"): dropdown = ModusDropdown( dropdown_id=dropdown_id, parent=parent, dropdown_children=dropdown_children, ) return dropdown def manage_button_style_classes(buttons, active_button=None, style_class="active"): for button in buttons: if button: button.remove_style_class(style_class) if active_button: active_button.add_style_class(style_class) def show_about_app(_=None): if not has_active_window(): return app_name = modus_service.current_active_app_name wmclass = getattr(modus_service, "current_active_wm_class", "") about_window = AboutApp(app_name=app_name, wmclass=wmclass) about_window.toggle(None) def on_click_subthread(button, on_clicked, on_click): if on_clicked: on_clicked(button) else: exec_shell_command_async(on_click, lambda *_: None) for dropdown in dropdowns: if dropdown.is_visible(): dropdown.hide() break def dropdown_option( label: str = "", keybind: str = "", on_click='echo "ModusPanelDropdown Action"', on_clicked=None, ): btn = Button( child=CenterBox( start_children=[ Label(label=label, h_align="start", name="dropdown-option-label"), ], end_children=[ Label(label=keybind, h_align="end", name="dropdown-option-keybind") ], orientation="horizontal", h_align="fill", h_expand=True, v_expand=True, ), name="dropdown-option", h_align="fill", on_clicked=lambda button: on_click_subthread(button, on_clicked, on_click), h_expand=True, v_expand=True, ) setup_cursor_hover(btn, "pointer") 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__( dropdown_id="os-menu", parent=parent, dropdown_children=[ dropdown_option( "About this PC", on_clicked=lambda _: get_about_window().toggle() ), dropdown_divider("---------------------"), dropdown_option( "System Settings...", on_clicked=lambda _: get_settings_window().toggle(), ), dropdown_divider("---------------------"), dropdown_option( "Force Quit", "", "hyprctl dispatch 'hl.dsp.window.kill()'" ), dropdown_divider("---------------------"), dropdown_option("Sleep", "", "systemctl suspend"), dropdown_option("Restart", "", "systemctl reboot"), dropdown_option("Shut Down", "", "shutdown now"), dropdown_divider("---------------------"), dropdown_option("Lock Screen", "󰘳 L", "hyprlock"), ], **kwargs, ) class GlobalMenuDropdowns: def __init__(self, parent, menu_box=None): self.parent = parent self._menu_box = menu_box # The GlobalMenu Box widget to update self._imac_button = None # 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.global_title_menu_about = dropdown_option( f"About {modus_service.current_active_app_name}", on_clicked=show_about_app, ) self.global_title_menu_quit = dropdown_option( f"Quit {modus_service.current_active_app_name}", on_click="hyprctl dispatch 'hl.dsp.window.close()'", ) self.global_menu_title = create_dropdown_with_capture( "global-menu-title", parent, [self.global_title_menu_about, self.global_title_menu_quit], ) # Dynamic menu dropdowns (populated from extracted app menus) self._dynamic_dropdowns: list[ModusDropdown] = [] modus_service.connect( "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 = ActiveWindow( formatter=FormattedString( "{ format_window(win_title, win_class) }", format_window=format_window, ), name="global-menu", on_clicked=self._on_title_button_clicked, ) setup_cursor_hover(self.global_menu_button_title, "pointer") self.global_menu_title.set_pointing_to(self.global_menu_button_title) # Start with only title self.all_menu_buttons = [ self.global_menu_button_title, ] self.dropdown_button_map = { "global-menu-title": self.global_menu_button_title, "os-menu": None, } 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 set_imac_button(self, button): self._imac_button = button self.dropdown_button_map["os-menu"] = button def _get_all_buttons(self): buttons = list(self.all_menu_buttons) if self._imac_button and self._imac_button not in buttons: buttons.append(self._imac_button) return buttons def hide_dropdowns(self, *_): manage_button_style_classes(self._get_all_buttons()) def changed_dropdown(self, _, dropdown_id): active_button = self.dropdown_button_map.get(dropdown_id) manage_button_style_classes(self._get_all_buttons(), active_button) 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}") try: self.global_title_menu_about.get_child().get_start_children()[0].set_label( f"About {value}" ) self.global_title_menu_quit.get_child().get_start_children()[0].set_label( f"Quit {value}" ) except Exception: pass # 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.""" 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") # Destroy old buttons to prevent massive GTK memory leaks if hasattr(self, "all_menu_buttons"): for btn in self.all_menu_buttons: if btn not in (self.global_menu_button_title,): try: if btn.get_parent(): btn.get_parent().remove(btn) btn.destroy() except Exception: pass # Clean up any previous dynamic dropdowns for dd in self._dynamic_dropdowns: try: dd.destroy() except Exception: pass self._dynamic_dropdowns.clear() # Filter to visible top-level menu items only top_level = [ item for item in menu_items if hasattr(item, "label") and item.label and getattr(item, "visible", True) ] 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 []: if not getattr(child, "visible", True): continue 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 [] ): if not getattr(child, "visible", True): continue 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 destroy(self): """Clean up all dropdowns and signal connections""" try: modus_service.disconnect_by_func(self._on_active_app_changed) modus_service.disconnect_by_func(self.changed_dropdown) modus_service.disconnect_by_func(self.hide_dropdowns) except Exception as e: logger.error(f"An error occurred: {e}") # 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, "system_dropdown", None), getattr(self, "global_menu_title", None), ] for capture in dropdown_captures: try: if capture and hasattr(capture, "destroy"): capture.destroy() except Exception as e: logger.error(f"An error occurred: {e}") super().destroy() class GlobalMenu(Box): def __init__(self, parent_window=None, **kwargs): if parent_window is None: parent_window = kwargs.pop("parent_window", None) super().__init__( name="globalmenu", orientation="horizontal", spacing=0, **kwargs ) self.dropdown_system = GlobalMenuDropdowns(parent=parent_window, menu_box=self) self.children = self.dropdown_system.all_menu_buttons def set_imac_button(self, button): self.dropdown_system.set_imac_button(button) def show_system_dropdown(self, imac_button): self.dropdown_system.set_imac_button(imac_button) 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""" if hasattr(self, "dropdown_system") and self.dropdown_system: try: self.dropdown_system.destroy() except Exception as e: logger.error(f"An error occurred: {e}") super().destroy()