diff --git a/README.md b/README.md index c675ebc..6eecd50 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ service to run. `mate-hud.py` tries to get the menu of the currently focused window, lists possible actions and asks the user which one to run. `mate-hud.py`, binds itself to the `Alt_L` keyboard shortcut by default (can be changed in the settings GUI). +When no compatible global menu applet is detected, `mate-hud` now asks +the registrar to quit through its DBus `org.freedesktop.Application.Quit` +method and only falls back to terminating the process if that fails. ### Settings diff --git a/usr/lib/mate-hud/mate-hud b/usr/lib/mate-hud/mate-hud index 5374c7d..11aeefa 100755 --- a/usr/lib/mate-hud/mate-hud +++ b/usr/lib/mate-hud/mate-hud @@ -151,8 +151,6 @@ def kill_process(name): pass def terminate_appmenu_registrar(): - # TODO: - # - Use Dbus Quit method. appmenu_loaded = False if process_running('mate-panel'): applets = get_list( 'org.mate.panel', '/org/mate/panel/general/', 'object-id-list') @@ -208,7 +206,13 @@ def terminate_appmenu_registrar(): break if process_running('appmenu-registrar') and not appmenu_loaded: - kill_process('appmenu-registrar') + try: + bus = dbus.SessionBus() + registrar = bus.get_object('com.canonical.AppMenu.Registrar', '/com/canonical/AppMenu/Registrar') + dbus.Interface(registrar, dbus_interface='org.freedesktop.Application').Quit() + except dbus.exceptions.DBusException as exc: + logging.info('Failed to quit appmenu-registrar over dbus: %s', exc) + kill_process('appmenu-registrar') def get_running_panels(): panels = [] @@ -573,10 +577,22 @@ def try_appmenu_interface(window_id): logging.debug('Unable to access dbusmenu items.') return False + # Chromium-based browsers expose a buggy dbusmenu implementation that + # blocks AboutToShow; treat them specially to avoid locking the HUD. + current_win_name = (STORE.current_win_name or '').lower() + dbusmenu_bus_name = str(dbusmenu_bus).lower() + chromium_tokens = ('chrom', 'brave', 'vivaldi', 'ungoogled', 'microsoft-edge', 'msedge') + chromium_like = any(token in current_win_name for token in chromium_tokens) or \ + any(token in dbusmenu_bus_name for token in chromium_tokens) + dbusmenu_call_timeout = 1.0 if chromium_like else None + if chromium_like: + logging.debug('Detected Chromium-like menu source; applying defensive DBus timeouts.') + # --- Valid menu, so init rofi process to capture keypresses. init_rofi() - dbusmenu_root_item = dbusmenu_object_iface.GetLayout(0, 0, ["label", "children-display"]) + root_layout_kwargs = {'timeout': dbusmenu_call_timeout} if dbusmenu_call_timeout else {} + dbusmenu_root_item = dbusmenu_object_iface.GetLayout(0, 0, ["label", "children-display"], **root_layout_kwargs) dbusmenu_item_dict = dict() #For excluding items which have no action @@ -587,16 +603,43 @@ def try_appmenu_interface(window_id): item_id = item[0] item_props = item[1] - # expand if necessary + # expand if necessary, but tolerate implementations that fail to answer if 'children-display' in item_props: - dbusmenu_object_iface.AboutToShow(item_id) - dbusmenu_object_iface.Event(item_id, "opened", "not used", dbus.UInt32(time.time())) #fix firefox + opened_timestamp = dbus.UInt32(int(time.time())) + if chromium_like: + try: + dbusmenu_object_iface.Event(item_id, "opened", "not used", opened_timestamp) + except dbus.exceptions.DBusException as exc: + logging.debug('Pre-AboutToShow opened event failed for item %s: %s', item_id, exc) + try: + if dbusmenu_call_timeout: + dbusmenu_object_iface.AboutToShow(item_id, timeout=dbusmenu_call_timeout) + else: + dbusmenu_object_iface.AboutToShow(item_id) + except dbus.exceptions.DBusException as exc: + logging.debug('AboutToShow failed for item %s: %s', item_id, exc) + try: + # Some apps (e.g. Firefox) need an opened event to populate children + dbusmenu_object_iface.Event(item_id, "opened", "not used", opened_timestamp) + except dbus.exceptions.DBusException as exc: + logging.debug('Event("opened") failed for item %s: %s', item_id, exc) + + layout_kwargs = {'timeout': dbusmenu_call_timeout} if dbusmenu_call_timeout else {} try: - item = dbusmenu_object_iface.GetLayout(item_id, 1, ["label", "children-display"])[1] - except: + layout = dbusmenu_object_iface.GetLayout(item_id, 1, ["label", "children-display"], **layout_kwargs) + item = layout[1] + except (dbus.exceptions.DBusException, IndexError) as exc: + logging.debug('GetLayout failed for item %s: %s', item_id, exc) + return + except TypeError as exc: + logging.debug('GetLayout returned unexpected payload for item %s: %s', item_id, exc) return - item_children = item[2] + try: + item_children = item[2] + except (IndexError, TypeError): + logging.debug('Layout missing children for item %s', item_id) + item_children = [] if 'label' in item_props: new_path = path + " > " + item_props['label'] @@ -614,6 +657,26 @@ def try_appmenu_interface(window_id): expanse_all_menu_with_dbus(child, False, new_path) expanse_all_menu_with_dbus(dbusmenu_root_item[1], True, "") + if not dbusmenu_item_dict: + logging.debug('AppMenu interface returned no actionable menu items; falling back to alternative interfaces.') + if STORE.rofi_process: + try: + STORE.rofi_process.stdin.close() + except Exception: + pass + try: + STORE.rofi_process.terminate() + except ProcessLookupError: + pass + try: + STORE.rofi_process.wait(timeout=0.2) + except (subprocess.TimeoutExpired, AttributeError): + try: + STORE.rofi_process.kill() + except ProcessLookupError: + pass + STORE.rofi_process = None + return False menu_result = get_menu() # --- Use dmenu result @@ -629,7 +692,7 @@ def try_appmenu_interface(window_id): dbusmenu_level1_items = dbusmenu_object_iface.GetLayout(0, 1, ["label"])[1] for item in dbusmenu_level1_items[2]: item_id = item[0] - dbusmenu_object_iface.Event(item_id, "closed", "not used", dbus.UInt32(time.time())) + dbusmenu_object_iface.Event(item_id, "closed", "not used", dbus.UInt32(int(time.time()))) return True