mirror of
https://github.com/virt-manager/virt-manager.git
synced 2024-12-22 13:34:07 +03:00
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:
parent
408b3023bc
commit
4348d4b128
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user