Skip to content

Conversation

lost-melody
Copy link

@lost-melody lost-melody commented Sep 12, 2024

This PR (partially) fixes #494.

Edit:

This problem is caused by the "empty menus":

  • For a LayoutUpdate signal, class DBusClient only requests a layout update when this._active is true (indicating the menu is currently open), or it is delayed until this._active becomes true (when the menu is opened later).
  • this._active is only set to true when an open-state-changed signal is emitted, which is triggered by opening the popup menu.
  • But an empty menu will never open, so this._active keeps false, so the delayed LayoutUpdate handler never runs.

QQ is an IM app who provides no menu entries before user logged in, and updates it when logged in. But it probably does not emit a LayoutUpdated signal before updating menu so its tray icon menu can never open until appindicator restarts (by disabling the extension or ending gnome session). Here we try updating entries before opening an empty menu. Only primary click and secondary click will trigger an update.

Wondering if there are better solutions...

@AmionSky
Copy link

This also fixes Dropbox's tray icon menu not opening.

Comment on lines 406 to 413
if (this.menu.numMenuItems) {
this.menu.toggle();
} else {
// update menu if there's no entries
this._updateMenu();
// wait for menu update
this._waitForDoubleClick().catch(logError);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure here, shouldn't be:

Suggested change
if (this.menu.numMenuItems) {
this.menu.toggle();
} else {
// update menu if there's no entries
this._updateMenu();
// wait for menu update
this._waitForDoubleClick().catch(logError);
}
// Try to update the menu if there are no entries.
if (!this.menu.numMenuItems)
this._updateMenu();
this.menu.toggle();

Copy link
Author

@lost-melody lost-melody May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you.

I think it should be.

It seems that this.menu.toggle() is called inside this._waitForDoubleClick() so does not need to be called here.

@3v1n0
Copy link
Collaborator

3v1n0 commented May 6, 2025

Mhmh, since _updateMenu is triggered by the menu update, I'm wondering if we can instead react to some signal happening in dbusMenu.js to actually do this...

I can't personally reproduce this with QQ, not sure if that happens after logging in or if I can test it before.

@3v1n0 3v1n0 linked an issue May 6, 2025 that may be closed by this pull request
@plumlis
Copy link

plumlis commented May 7, 2025

The system tray icon in Blueman exhibits a similar persistence issue - once the application launches, the context menu associated with the tray icon becomes static and does not refresh to reflect state changes.

After launching Blueman, performing Bluetooth enable/disable operations through the interface will not update the visual state of the tray icon

The icon state only refreshes after either:

  • User session restart (logout/login cycle)

  • Manual reload of the desktop extension (disabling and re-enabling the extension)

This behavioral pattern suggests a fundamental refresh mechanism limitation in the tray icon implementation. Similar behavior can be expected in analogous scenarios where system tray components require dynamic state updates without full application/desktop environment reloads.

#494 (comment)

@DerryAlex
Copy link

A minimal example to reproduce the issue. The tray should have Logout item after clicking the Login button in the application. However, you could not observe expected behavior until you lock the screen (this somehow updates the tray menu).

import gi
from gi.repository import AyatanaAppIndicator3 as AppIndicator3
from gi.repository import Gtk

class IndicatorApp:
    def __init__(self):
        # Create the main window
        self.window = Gtk.Window(title="Login Example")
        self.window.set_default_size(200, 100)
        self.window.connect("destroy", self.on_quit)
        
        # Create a button
        self.button = Gtk.Button(label="Login")
        self.button.connect("clicked", self.on_login_clicked)
        self.window.add(self.button)
        
        # Create the app indicator
        self.indicator = AppIndicator3.Indicator.new(
            "indicator-example",
            "system-run-symbolic",  # Default icon
            AppIndicator3.IndicatorCategory.APPLICATION_STATUS)
        self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
        
        # Initial empty menu
        self.menu = Gtk.Menu()
        self.indicator.set_menu(self.menu)
        
        # Show the window
        self.window.show_all()
        
        # Track login state
        self.logged_in = False

    def on_login_clicked(self, widget):
        if not self.logged_in:
            # Update button text
            self.button.set_label("Logout")
            
            # Update tray menu
            self.update_tray_menu()
            
            self.logged_in = True
        else:
            # Logout action
            self.button.set_label("Login")
            self.clear_tray_menu()
            self.logged_in = False

    def update_tray_menu(self):
        # Clear existing menu items
        self.clear_tray_menu()
        
        # Create new menu items
        menu = Gtk.Menu()
        
        # Add logout item
        item_logout = Gtk.MenuItem(label="Logout")
        item_logout.connect("activate", self.on_logout_clicked)
        menu.append(item_logout)
        
        # Add separator
        menu.append(Gtk.SeparatorMenuItem())
        
        # Add quit item
        item_quit = Gtk.MenuItem(label="Quit")
        item_quit.connect("activate", self.on_quit)
        menu.append(item_quit)
        
        # Show all menu items
        menu.show_all()
        
        # Set the new menu
        self.indicator.set_menu(menu)

    def clear_tray_menu(self):
        # Create an empty menu
        menu = Gtk.Menu()
        self.indicator.set_menu(menu)

    def on_logout_clicked(self, widget):
        # Reset to login state
        self.button.set_label("Login")
        self.clear_tray_menu()
        self.logged_in = False

    def on_quit(self, widget=None):
        Gtk.main_quit()

if __name__ == "__main__":
    app = IndicatorApp()
    Gtk.main()

@lost-melody
Copy link
Author

lost-melody commented May 7, 2025

Mhmh, since _updateMenu is triggered by the menu update, I'm wondering if we can instead react to some signal happening in dbusMenu.js to actually do this...

Yes I believe that there should be better solutions to this.

I was unable to inspect what exactly happened during the communication between QQ and StatusNotifierHost but with Bustle I can see how it is going now.

I can't personally reproduce this with QQ, not sure if that happens after logging in or if I can test it before.

Yes, this happens after logging in.

It seems that QQ does not have the problem any more, probably because it does not provide an empty menu before login.

Thanks to DerryAlex who provided another minimal reproduce.

@lost-melody lost-melody changed the title update and open menu if there's no entries do LayoutUpdate for menus with no entries May 8, 2025
@mdmower
Copy link

mdmower commented Jun 8, 2025

The code changes in this PR are different since @AmionSky's comment that this fixes the Dropbox tray icon, so I just wanted to chime in and say that the latest revision still works. I applied it in a minimalist way to v43 in Debian 12 (Gnome 43.9):

diff --git a/dbusMenu.js.bak b/dbusMenu.js
index a30502a..f74899e 100644
--- a/dbusMenu.js.bak
+++ b/dbusMenu.js
@@ -470,14 +470,14 @@ var DBusClient = GObject.registerClass({
 
     _onSignal(_sender, signal, params) {
         if (signal === 'LayoutUpdated') {
-            if (!this._active) {
+            if (!this._active && this.getRoot()?.getChildren().length) {
                 this._flagLayoutUpdateRequired = true;
                 return;
             }
 
             this._requestLayoutUpdate();
         } else if (signal === 'ItemsPropertiesUpdated') {
-            if (!this._active) {
+            if (!this._active && this.getRoot()?.getChildren().length) {
                 this._flagItemsUpdateRequired = true;
                 return;
             }

Now, when I boot my system and log in, the Dropbox tray icon works when I click it. I used to have to lock the screen and then unlock it before the icon would work.

@mdmower
Copy link

mdmower commented Aug 16, 2025

@3v1n0 - any chance this could be re-reviewed/merged?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Linuxqq tray-Icons not clickable LinuxQQ cannot show app menu
6 participants