systray: Don't rebuild the menu on every change

This works fine for the StatusIcon backend, but for the appindicator
backend which communicates every menu changes over dbus this is
super slow. Instead update the menu in place when objects change
state or disappear

https://bugzilla.redhat.com/show_bug.cgi?id=1779362

Signed-off-by: Cole Robinson <crobinso@redhat.com>
This commit is contained in:
Cole Robinson 2020-08-20 16:19:26 -04:00
parent 408b3023bc
commit 4348d4b128

View File

@ -91,6 +91,9 @@ class _Systray(object):
class _SystrayIndicator(_Systray):
"""
UI backend for appindicator
"""
def __init__(self):
self._icon = AppIndicator3.Indicator.new(
"virt-manager", "virt-manager",
@ -119,6 +122,9 @@ class _SystrayIndicator(_Systray):
class _SystrayStatusIcon(_Systray):
"""
UI backend for Gtk StatusIcon
"""
def __init__(self):
self._icon = Gtk.StatusIcon()
self._icon.set_property("icon-name", "virt-manager")
@ -147,7 +153,208 @@ class _SystrayStatusIcon(_Systray):
self._icon.set_visible(False)
def _cmp(a, b):
return ((a > b) - (a < b))
class _TrayMainMenu(vmmGObject):
"""
Helper class for maintaining the conn + VM menu list and updating
it in place
"""
def __init__(self):
vmmGObject.__init__(self)
self.topwin = None # Need this for error callbacks from VMActionMenu
self._menu = self._build_menu()
def _cleanup(self):
self._menu.destroy()
self._menu = None
###########
# UI init #
###########
def _build_menu(self):
"""
Build the top level conn list menu when clicking the icon
"""
menu = Gtk.Menu()
menu.add(Gtk.SeparatorMenuItem())
exit_item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_QUIT, None)
exit_item.connect("activate", self._exit_app_cb)
menu.add(exit_item)
menu.show_all()
return menu
######################
# UI update routines #
######################
# Helpers for stashing identifying data in the menu item objects
def _get_lookupkey(self, child):
return getattr(child, "_vmlookupkey", None)
def _set_lookupkey(self, child, val):
return setattr(child, "_vmlookupkey", val)
def _get_sortkey(self, child):
return getattr(child, "_vmsortkey", None)
def _set_sortkey(self, child, val):
return setattr(child, "_vmsortkey", val)
def _set_vm_state(self, menu_item, vm):
label = menu_item.get_child()
label.set_text(vm.get_name_or_title())
vm_action_menu = menu_item.get_submenu()
vm_action_menu.update_widget_states(vm)
def _build_vm_menuitem(self, vm):
"""
Build a menu item representing a single VM
"""
menu_item = Gtk.ImageMenuItem.new_with_label("FOO")
menu_item.set_use_underline(False)
vm_action_menu = vmmenu.VMActionMenu(self, lambda: vm)
menu_item.set_submenu(vm_action_menu)
self._set_lookupkey(menu_item, vm)
self._set_sortkey(menu_item, vm.get_name_or_title())
self._set_vm_state(menu_item, vm)
menu_item.show_all()
return menu_item
def _set_conn_state(self, menu_item, conn):
label = menu_item.get_child()
if conn.is_active():
label = menu_item.get_child()
markup = "<b>%s</b>" % xmlutil.xml_escape(conn.get_pretty_desc())
label.set_markup(markup)
else:
label.set_text(conn.get_pretty_desc())
connect_item = self._find_lookupkey(menu_item.get_submenu(), 1)
disconnect_item = self._find_lookupkey(menu_item.get_submenu(), 2)
connect_item.set_visible(conn.is_active())
disconnect_item.set_visible(not conn.is_active())
def _build_conn_menuitem(self, conn):
"""
Build a menu item representing a single connection, and populate
all its VMs as items in a sub menu
"""
menu_item = Gtk.MenuItem.new_with_label("FOO")
self._set_lookupkey(menu_item, conn.get_uri())
# Group active conns first
# Sort by pretty desc within those categories
sortkey = str(int(bool(not conn.is_active())))
sortkey += conn.get_pretty_desc().lower()
self._set_sortkey(menu_item, sortkey)
menu = Gtk.Menu()
menu_item.set_submenu(menu)
menu.add(Gtk.SeparatorMenuItem())
citem1 = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_DISCONNECT, None)
citem1.connect("activate", _conn_disconnect_cb, conn.get_uri())
self._set_lookupkey(citem1, 1)
menu.add(citem1)
citem2 = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_CONNECT, None)
citem2.connect("activate", _conn_connect_cb, conn.get_uri())
self._set_lookupkey(citem2, 2)
menu.add(citem2)
menu_item.show_all()
self._set_conn_state(menu_item, conn)
return menu_item
def _find_lookupkey(self, parent, key):
for child in parent.get_children():
if self._get_lookupkey(child) == key:
return child
def _find_conn_menuitem(self, uri):
return self._find_lookupkey(self._menu, uri)
def _find_vm_menuitem(self, uri, vm):
connmenu = self._find_conn_menuitem(uri)
return self._find_lookupkey(connmenu.get_submenu(), vm)
################
# UI listeners #
################
def _exit_app_cb(self, src):
from .engine import vmmEngine
vmmEngine.get_instance().exit_app()
##############
# Public API #
##############
def get_menu(self):
return self._menu
def conn_add(self, conn):
connmenu = self._build_conn_menuitem(conn)
sortkey = self._get_sortkey(connmenu)
idx = 0
for idx, child in enumerate(list(self._menu.get_children())):
checksort = self._get_sortkey(child)
if checksort is None or checksort > sortkey:
break
self._menu.insert(connmenu, idx)
def conn_remove(self, uri):
connmenu = self._find_conn_menuitem(uri)
if connmenu:
self._menu.remove(connmenu)
connmenu.destroy()
def conn_change(self, conn):
connmenu = self._find_conn_menuitem(conn.get_uri())
self._set_conn_state(connmenu, conn)
def vm_add(self, vm):
connmenu = self._find_conn_menuitem(vm.conn.get_uri())
menu_item = self._build_vm_menuitem(vm)
sortkey = self._get_sortkey(menu_item)
vmsubmenu = connmenu.get_submenu()
idx = 0
for idx, child in enumerate(list(vmsubmenu.get_children())):
checksort = self._get_sortkey(child)
if checksort is None or checksort > sortkey:
break
vmsubmenu.insert(menu_item, idx)
def vm_remove(self, vm):
conn = vm.conn
connmenu = self._find_conn_menuitem(conn.get_uri())
vmitem = self._find_vm_menuitem(conn.get_uri(), vm)
connmenu.get_submenu().remove(vmitem)
vmitem.destroy()
def vm_change(self, vm):
vmitem = self._find_vm_menuitem(vm.conn.get_uri(), vm)
self._set_vm_state(vmitem, vm)
class vmmSystray(vmmGObject):
"""
API class representing a systray icon. May use StatusIcon or appindicator
backends
"""
@classmethod
def get_instance(cls):
if not cls._instance:
@ -166,15 +373,9 @@ class vmmSystray(vmmGObject):
def __init__(self):
vmmGObject.__init__(self)
self._cleanup_on_app_close()
self.topwin = None # Need this for error callbacks from VMActionMenu
self._systray = None
connmanager = vmmConnectionManager.get_instance()
connmanager.connect("conn-added", self._conn_added_cb)
connmanager.connect("conn-removed", self._rebuild_menu)
for conn in connmanager.conns.values():
self._conn_added_cb(connmanager, conn)
self._mainmenu = None
self.add_gsettings_handle(
self.config.on_view_system_tray_changed(
@ -188,19 +389,33 @@ class vmmSystray(vmmGObject):
def _cleanup(self):
self._hide_systray()
self._systray = None
self._mainmenu.cleanup()
self._mainmenu = None
###########################
# Initialization routines #
###########################
def _init_mainmenu(self):
if self._mainmenu:
return
self._mainmenu = _TrayMainMenu()
connmanager = vmmConnectionManager.get_instance()
connmanager.connect("conn-added", self._conn_added_cb)
connmanager.connect("conn-removed", self._conn_removed_cb)
for conn in connmanager.conns.values():
self._conn_added_cb(connmanager, conn)
def _show_systray(self):
if not self._systray:
if _USING_APPINDICATOR:
self._systray = _SystrayIndicator()
else:
self._systray = _SystrayStatusIcon()
self._rebuild_menu(force=True)
self._init_mainmenu()
self._systray.set_menu(self._mainmenu.get_menu())
self._systray.show()
def _hide_systray(self):
@ -222,98 +437,30 @@ class vmmSystray(vmmGObject):
self._show_systray_changed_cb()
#################
# Menu building #
#################
def _build_vm_menuitem(self, vm):
menu_item = Gtk.ImageMenuItem.new_with_label(vm.get_name_or_title())
menu_item.set_use_underline(False)
vm_action_menu = vmmenu.VMActionMenu(self, lambda: vm)
vm_action_menu.update_widget_states(vm)
menu_item.set_submenu(vm_action_menu)
return menu_item
def _build_conn_menuitem(self, conn):
menu_item = Gtk.MenuItem.new_with_label(conn.get_pretty_desc())
if conn.is_active():
label = menu_item.get_child()
markup = "<b>%s</b>" % xmlutil.xml_escape(conn.get_pretty_desc())
label.set_markup(markup)
menu = Gtk.Menu()
vms = conn.list_vms()
vms.sort(key=lambda v: v.get_name_or_title())
for vm in vms:
menu.add(self._build_vm_menuitem(vm))
if not vms:
vmitem = Gtk.MenuItem.new_with_label(_("No virtual machines"))
vmitem.set_sensitive(False)
menu.add(vmitem)
menu.add(Gtk.SeparatorMenuItem())
if conn.is_active():
citem = Gtk.ImageMenuItem.new_from_stock(
Gtk.STOCK_DISCONNECT, None)
citem.connect("activate", _conn_disconnect_cb, conn.get_uri())
else:
citem = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_CONNECT, None)
citem.connect("activate", _conn_connect_cb, conn.get_uri())
menu.add(citem)
menu_item.set_submenu(menu)
return menu_item
def _build_menu(self):
connmanager = vmmConnectionManager.get_instance()
conns = list(connmanager.conns.values())
menu = Gtk.Menu()
conns.sort(key=lambda c: c.get_pretty_desc().lower())
conns.sort(key=lambda c: not c.is_active())
for conn in conns:
connmenu = self._build_conn_menuitem(conn)
menu.add(connmenu)
menu.add(Gtk.SeparatorMenuItem())
exit_item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_QUIT, None)
exit_item.connect("activate", self._exit_app_cb)
menu.add(exit_item)
menu.show_all()
return menu
def _rebuild_menu(self, *args, **kwargs):
ignore = args
ignore = kwargs
if "force" not in kwargs and not self.is_embedded():
return
if vmmConnectionManager.get_instance() is True:
# In app cleanup, don't do anything
return
# Yeah, this is kinda nutty, we rebuild the whole menu widget
# on any conn or VM state change. We kinda need to do this
# for appindicator, because we communicate with the remote
# UI via changing the menu widget, unlike statusicon which
# we could delay until 'show' time. This is likely slow as
# dirt for a virt-manager instance with a lot of connections
# and VMs...
menu = self._build_menu()
self._systray.set_menu(menu)
################
# UI listeners #
################
def _conn_added_cb(self, src, conn):
conn.connect("vm-added", self._vm_added_cb)
conn.connect("vm-removed", self._rebuild_menu)
conn.connect("state-changed", self._rebuild_menu)
self._rebuild_menu()
conn.connect("vm-removed", self._vm_removed_cb)
conn.connect("state-changed", self._conn_state_changed_cb)
self._mainmenu.conn_add(conn)
for vm in conn.list_vms():
self._vm_added_cb(conn, vm)
def _conn_removed_cb(self, src, conn):
self._mainmenu.conn_remove(conn)
def _conn_state_changed_cb(self, conn):
self._mainmenu.conn_change(conn)
def _vm_added_cb(self, conn, vm):
vm.connect("state-changed", self._rebuild_menu)
self._rebuild_menu()
vm.connect("state-changed", self._vm_state_changed_cb)
self._mainmenu.vm_add(vm)
def _exit_app_cb(self, src):
from .engine import vmmEngine
vmmEngine.get_instance().exit_app()
def _vm_removed_cb(self, conn, vm):
self._mainmenu.vm_remove(vm)
def _vm_state_changed_cb(self, vm):
self._mainmenu.vm_change(vm)