Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tray icon] dynamic menus not resized correctly #1149

Open
coldfix opened this issue Jul 2, 2021 · 1 comment
Open

[tray icon] dynamic menus not resized correctly #1149

coldfix opened this issue Jul 2, 2021 · 1 comment

Comments

@coldfix
Copy link

coldfix commented Jul 2, 2021

Hi,

Menus with dynamically added/removed entries are not resized correctly before showing. This can result in too much unused space or invisible entries (below the bottom of the menu).

I have again created the following small examples recreating the issue to make sure that it isn't a problem with one particular KSNI implementation:

Right-click the tray icon several times and it will rotate between One/Two/Three menu items, but the geometry always lacks one behind, i.e. either shows too many or one to few entries.

Using Qt/QSystemTrayIcon (this example also demonstrates that the menu geometry is updated correctly in a window menu):

from PyQt5.QtWidgets import (
    QAction, QApplication, QMainWindow, QMenu, QSystemTrayIcon)

import itertools
import signal
import sys


def main():
    app = QApplication(sys.argv)

    window = QMainWindow()
    winmenu = TrayMenu(window.menuBar().addMenu('&File'))
    window.show()

    style = app.style()
    trayicon = QSystemTrayIcon(
        style.standardIcon(style.SP_MessageBoxCritical))
    traymenu = TrayMenu()
    trayicon.setContextMenu(traymenu.menu)
    trayicon.show()
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    app.exec_()


class TrayMenu:

    def __init__(self, menu=None):
        self.menu = menu or QMenu()
        self.menu.aboutToShow.connect(self.update)
        # Show different number of menuitems each time for demo purposes:
        self.counter = itertools.count()
        # Needed to prevent garbage collection of actions:
        self.actions = []
        # Populate menu initially, so libdbusmenu does not ignore the
        # 'about-to-show':
        self.update()

    def update(self):
        for action in self.actions:
            self.menu.removeAction(action)
        self.actions.clear()

        num_items = next(self.counter) % 3
        print("num_items={}".format(num_items))

        for i in range(num_items):
            action = QAction('Item {}'.format(i + 1))
            self.menu.addAction(action)
            self.actions.append(action)

        quit = QAction('&Quit')
        quit.triggered.connect(QApplication.quit)
        self.menu.addAction(quit)
        self.actions.append(quit)


if __name__ == '__main__':
    main()

And here using ``Gtk/AppIndicator3`:

from gi import require_version
require_version('Gtk', '3.0')
require_version('AppIndicator3', '0.1')

import itertools
import signal
from gi.repository import Gtk
from gi.repository import AppIndicator3


def main():
    indicator = AppIndicator3.Indicator.new(
        'checkbox-test-app', 'dialog-error',
        AppIndicator3.IndicatorCategory.OTHER)
    indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
    traymenu = TrayMenu()
    traymenu.set_indicator(indicator)

    signal.signal(signal.SIGINT, signal.SIG_DFL)
    Gtk.main()


class TrayMenu:

    def __init__(self, menu=None):
        self.counter = itertools.count()
        self.menu = Gtk.Menu()
        # Populate menu initially, so libdbusmenu does not ignore the
        # 'about-to-show':
        self.update()

    def update(self):
        for item in self.menu.get_children():
            self.menu.remove(item)
        num_items = next(self.counter) % 3
        print("num_items={}".format(num_items))
        for i in range(num_items):
            self.menu.append(Gtk.MenuItem(label='Item {}'.format(i)))
        quit = Gtk.MenuItem(label='Quit')
        quit.connect('activate', lambda _: Gtk.main_quit())
        self.menu.append(quit)
        self.menu.show_all()

    def set_indicator(self, indicator):
        self.indicator = indicator
        indicator.set_menu(self.menu)
        # Get notified before menu is shown, see:
        # https://bugs.launchpad.net/screenlets/+bug/522152/comments/15
        dbusmenuserver = indicator.get_property('dbus-menu-server')
        dbusmenuitem = dbusmenuserver.get_property('root-node')
        dbusmenuitem.connect('about-to-show', self.update)


if __name__ == '__main__':
    main()

And here is yet another variant that shows that the geometry of tray submenus seems to be updated correctly:

from gi import require_version
require_version('Gtk', '3.0')
require_version('AppIndicator3', '0.1')

import itertools
import signal
from gi.repository import Gtk
from gi.repository import AppIndicator3


def main():
    indicator = AppIndicator3.Indicator.new(
        'checkbox-test-app', 'dialog-error',
        AppIndicator3.IndicatorCategory.OTHER)
    indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE)
    traymenu = TrayMenu()
    traymenu.set_indicator(indicator)

    signal.signal(signal.SIGINT, signal.SIG_DFL)
    Gtk.main()


class TrayMenu:

    def __init__(self, menu=None):
        self.counter = itertools.count()
        self.submenu = Gtk.Menu()
        submenu = Gtk.MenuItem(label='Items')
        submenu.set_submenu(self.submenu)
        quit = Gtk.MenuItem(label='Quit')
        quit.connect('activate', lambda _: Gtk.main_quit())
        self.menu = Gtk.Menu()
        self.menu.append(submenu)
        self.menu.append(quit)
        self.menu.show_all()
        # Populate menu initially, so libdbusmenu does not ignore the
        # 'about-to-show':
        # self.update()

    def update(self):
        for item in self.submenu.get_children():
            self.submenu.remove(item)
        num_items = next(self.counter) % 3 + 1
        print("num_items={}".format(num_items))
        for i in range(num_items):
            self.submenu.append(Gtk.CheckMenuItem(label='Item {}'.format(i)))
        self.submenu.show_all()

    def set_indicator(self, indicator):
        self.indicator = indicator
        indicator.set_menu(self.menu)
        # Get notified before menu is shown, see:
        # https://bugs.launchpad.net/screenlets/+bug/522152/comments/15
        dbusmenuserver = indicator.get_property('dbus-menu-server')
        dbusmenuitem = dbusmenuserver.get_property('root-node')
        dbusmenuitem.connect('about-to-show', self.update)


if __name__ == '__main__':
    main()

again, not sure whether this is a wayland, waybar or sway problem.

(Issue from coldfix/udiskie#199)

Best, Thomas

@alebastr
Copy link
Contributor

alebastr commented Jul 6, 2021

This one is more interesting.
We pass all tray menu handling to the libdbusmenu library which is pretty much dead upstream. Apparently it does not adjust the window size/position if the entries were updated when the menu is visible (AboutToShow is sent right before realizing the menu with the current data and the response will be handled way after that).
I already crossed out any weird gtk and layer-shell interactions by providing a regular xdg surface implementation for the bar. Didn't affect the issue at all.

Need to check if it's possible to work around that without modifying and vendoring the library code. If anyone wants to look at some seasoned and unmaintained C code before I find time for that, feel free 😃

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

No branches or pull requests

2 participants