virt-manager/virtManager/vmwindow.py
Cole Robinson e7c936f16e vmwindow: Customize window should not be 'deletable'
This forces the user to use the 'Cancel' button, which gives a warning
about the install being aborted

Resolves: https://github.com/virt-manager/virt-manager/issues/416

Signed-off-by: Cole Robinson <crobinso@redhat.com>
2022-08-01 09:44:48 -04:00

710 lines
26 KiB
Python

# Copyright (C) 2006-2008, 2013, 2014 Red Hat, Inc.
# Copyright (C) 2006 Daniel P. Berrange <berrange@redhat.com>
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
from gi.repository import Gdk
from gi.repository import Gtk
from virtinst import log
from . import vmmenu
from .baseclass import vmmGObjectUI
from .engine import vmmEngine
from .details.console import vmmConsolePages
from .details.details import vmmDetails
from .details.snapshots import vmmSnapshotPage
# Main tab pages
(DETAILS_PAGE_DETAILS,
DETAILS_PAGE_CONSOLE,
DETAILS_PAGE_SNAPSHOTS) = range(3)
class vmmVMWindow(vmmGObjectUI):
__gsignals__ = {
"customize-finished": (vmmGObjectUI.RUN_FIRST, None, [object]),
"closed": (vmmGObjectUI.RUN_FIRST, None, []),
}
@classmethod
def get_instance(cls, parentobj, vm):
try:
# Maintain one dialog per VM
key = "%s+%s" % (vm.conn.get_uri(), vm.get_uuid())
if cls._instances is None:
cls._instances = {}
if key not in cls._instances:
cls._instances[key] = vmmVMWindow(vm)
return cls._instances[key]
except Exception as e: # pragma: no cover
if not parentobj:
raise
parentobj.err.show_err(
_("Error launching details: %s") % str(e))
def __init__(self, vm, parent=None):
vmmGObjectUI.__init__(self, "vmwindow.ui", "vmm-vmwindow")
self.vm = vm
self.is_customize_dialog = False
if parent:
# Details window is being abused as a 'configure before install'
# dialog, set things as appropriate
self.is_customize_dialog = True
self.topwin.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.topwin.set_transient_for(parent)
self.topwin.set_deletable(False)
self.widget("toolbar-box").show()
self.widget("customize-toolbar").show()
self.widget("details-toolbar").hide()
self.widget("details-menubar").hide()
pages = self.widget("details-pages")
pages.set_current_page(DETAILS_PAGE_DETAILS)
else:
self.conn.connect("vm-removed", self._vm_removed_cb)
self.ignoreDetails = False
self._console = vmmConsolePages(self.vm, self.builder, self.topwin)
self.widget("console-placeholder").add(self._console.top_box)
self._console.connect("page-changed", self._console_page_changed_cb)
self._console.connect("leave-fullscreen",
self._console_leave_fullscreen_cb)
self._console.connect("change-title",
self._console_change_title_cb)
self._snapshots = vmmSnapshotPage(self.vm, self.builder, self.topwin)
self.widget("snapshot-placeholder").add(self._snapshots.top_box)
self._details = vmmDetails(self.vm, self.builder, self.topwin,
self.is_customize_dialog)
self.widget("details-placeholder").add(self._details.top_box)
# Set default window size
w, h = self.vm.get_details_window_size()
if w <= 0 or h <= 0:
self._set_initial_window_size()
else:
self.topwin.set_default_size(w, h)
self._window_size = None
self._shutdownmenu = None
self._vmmenu = None
self.init_menus()
self.builder.connect_signals({
"on_close_details_clicked": self.close,
"on_details_menu_close_activate": self.close,
"on_vmm_details_delete_event": self._window_delete_event,
"on_vmm_details_configure_event": self.window_resized,
"on_details_menu_quit_activate": self.exit_app,
"on_control_vm_details_toggled": self.details_console_changed,
"on_control_vm_console_toggled": self.details_console_changed,
"on_control_snapshots_toggled": self.details_console_changed,
"on_control_run_clicked": self.control_vm_run,
"on_control_shutdown_clicked": self.control_vm_shutdown,
"on_control_pause_toggled": self.control_vm_pause,
"on_control_fullscreen_toggled": self.control_fullscreen,
"on_details_customize_finish_clicked": self.customize_finish,
"on_details_cancel_customize_clicked": self._customize_cancel_clicked,
"on_details_menu_virtual_manager_activate": self.control_vm_menu,
"on_details_menu_screenshot_activate": self.control_vm_screenshot,
"on_details_menu_usb_redirection": self.control_vm_usb_redirection,
"on_details_menu_view_toolbar_activate": self.toggle_toolbar,
"on_details_menu_view_manager_activate": self.view_manager,
"on_details_menu_view_details_toggled": self.details_console_changed,
"on_details_menu_view_console_toggled": self.details_console_changed,
"on_details_menu_view_snapshots_toggled": self.details_console_changed,
"on_details_pages_switch_page": self._details_page_switch_cb,
"on_details_menu_view_fullscreen_activate": self._fullscreen_changed_cb,
"on_details_menu_view_size_to_vm_activate": self._size_to_vm_cb,
"on_details_menu_view_scale_always_toggled": self._scaling_ui_changed_cb,
"on_details_menu_view_scale_fullscreen_toggled": self._scaling_ui_changed_cb,
"on_details_menu_view_scale_never_toggled": self._scaling_ui_changed_cb,
"on_details_menu_view_resizeguest_toggled": self._resizeguest_ui_changed_cb,
"on_details_menu_view_autoconnect_activate": self._autoconnect_ui_changed_cb,
})
# Deliberately keep all this after signal connection
self.vm.connect("state-changed", self._vm_state_changed_cb)
self.vm.connect("resources-sampled", self._resources_sampled_cb)
self._sync_console_page_menu_state()
self._console_refresh_scaling_from_settings()
self.add_gsettings_handle(
self.vm.on_console_scaling_changed(
self._console_refresh_scaling_from_settings))
self._console_refresh_resizeguest_from_settings()
self.add_gsettings_handle(
self.vm.on_console_resizeguest_changed(
self._console_refresh_resizeguest_from_settings))
self._console_refresh_autoconnect_from_settings()
self.add_gsettings_handle(
self.vm.on_console_autoconnect_changed(
self._console_refresh_autoconnect_from_settings))
self._refresh_vm_state()
self.activate_default_page()
@property
def conn(self):
return self.vm.conn
def _cleanup(self):
self._console.cleanup()
self._console = None
self._snapshots.cleanup()
self._snapshots = None
self._details.cleanup()
self._details = None
self._shutdownmenu.destroy()
self._shutdownmenu = None
self._vmmenu.destroy()
self._vmmenu = None
if self._window_size:
self.vm.set_details_window_size(*self._window_size)
self.conn.disconnect_by_obj(self)
self.vm = None
def show(self):
log.debug("Showing VM details: %s", self.vm)
vis = self.is_visible()
self.topwin.present()
if vis:
return
vmmEngine.get_instance().increment_window_counter()
self._refresh_vm_state()
def customize_finish(self, src):
ignore = src
if self._details.vmwindow_has_unapplied_changes():
return
self.emit("customize-finished", self.vm)
def _set_initial_window_size(self):
"""
We want the window size for new windows to be 1024x768 viewer
size, plus whatever it takes to fit the toolbar+menubar, etc.
To achieve this, we force the display box to the desired size
with set_size_request, wait for the window to report it has
been resized, and then unset the hardcoded size request so
the user can manually resize the window however they want.
"""
w = 1024
h = 768
hid = []
def win_cb(src, event):
self.widget("details-pages").set_size_request(-1, -1)
self.topwin.disconnect(hid[0])
self.widget("details-pages").set_size_request(w, h)
hid.append(self.topwin.connect("configure-event", win_cb))
def _vm_removed_cb(self, _conn, vm):
if self.vm == vm:
self.cleanup()
def _customize_cancel(self):
log.debug("Asking to cancel customization")
result = self.err.yes_no(
_("This will abort the installation. Are you sure?"))
if not result:
log.debug("Customize cancel aborted")
return
log.debug("Canceling customization")
return self._close()
def _customize_cancel_clicked(self, src):
ignore = src
return self._customize_cancel()
def _window_delete_event(self, ignore1=None, ignore2=None):
return self.close()
def close(self, ignore1=None, ignore2=None):
if self.is_visible():
log.debug("Closing VM details: %s", self.vm)
return self._close()
def _close(self):
fs = self.widget("details-menu-view-fullscreen")
if fs.get_active():
fs.set_active(False) # pragma: no cover
if not self.is_visible():
return
self.topwin.hide()
self._console.vmwindow_close()
self._details.vmwindow_close()
self.emit("closed")
vmmEngine.get_instance().decrement_window_counter()
return 1
##########################
# Initialization helpers #
##########################
def init_menus(self):
# Virtual Machine menu
self._shutdownmenu = vmmenu.VMShutdownMenu(self, lambda: self.vm)
self.widget("control-shutdown").set_menu(self._shutdownmenu)
self.widget("control-shutdown").set_icon_name("system-shutdown")
topmenu = self.widget("details-vm-menu")
submenu = topmenu.get_submenu()
self._vmmenu = vmmenu.VMActionMenu(
self, lambda: self.vm, show_open=False)
for child in submenu.get_children():
submenu.remove(child)
self._vmmenu.add(child)
topmenu.set_submenu(self._vmmenu)
topmenu.show_all()
self.widget("details-pages").set_show_tabs(False)
self.widget("details-menu-view-toolbar").set_active(
self.config.get_details_show_toolbar())
# Keycombo menu (ctrl+alt+del etc.)
self.widget("details-menu-send-key").set_submenu(
self._console.vmwindow_get_keycombo_menu())
# Serial list menu
self.widget("details-menu-view-console-list").set_submenu(
self._console.vmwindow_get_console_list_menu())
##########################
# Window state listeners #
##########################
def window_resized(self, ignore, ignore2):
if not self.is_visible():
return # pragma: no cover
self._window_size = self.topwin.get_size()
def control_fullscreen(self, src):
menu = self.widget("details-menu-view-fullscreen")
if src.get_active() != menu.get_active():
menu.set_active(src.get_active())
def toggle_toolbar(self, src):
if self.is_customize_dialog:
return
active = src.get_active()
self.config.set_details_show_toolbar(active)
fsactive = self.widget("details-menu-view-fullscreen").get_active()
self.widget("toolbar-box").set_visible(active and not fsactive)
def details_console_changed(self, src):
if self.ignoreDetails:
return
if not src.get_active():
return
is_details = (src == self.widget("control-vm-details") or
src == self.widget("details-menu-view-details"))
is_snapshot = (src == self.widget("control-snapshots") or
src == self.widget("details-menu-view-snapshots"))
pages = self.widget("details-pages")
if pages.get_current_page() == DETAILS_PAGE_DETAILS:
if self._details.vmwindow_has_unapplied_changes():
self._sync_toolbar_page_buttons(pages.get_current_page())
return
if is_details:
pages.set_current_page(DETAILS_PAGE_DETAILS)
elif is_snapshot:
pages.set_current_page(DETAILS_PAGE_SNAPSHOTS)
else:
pages.set_current_page(DETAILS_PAGE_CONSOLE)
def _sync_toolbar_page_buttons(self, newpage):
details = self.widget("control-vm-details")
details_menu = self.widget("details-menu-view-details")
console = self.widget("control-vm-console")
console_menu = self.widget("details-menu-view-console")
snapshot = self.widget("control-snapshots")
snapshot_menu = self.widget("details-menu-view-snapshots")
is_details = newpage == DETAILS_PAGE_DETAILS
is_snapshot = newpage == DETAILS_PAGE_SNAPSHOTS
is_console = not is_details and not is_snapshot
try:
self.ignoreDetails = True
details.set_active(is_details)
details_menu.set_active(is_details)
snapshot.set_active(is_snapshot)
snapshot_menu.set_active(is_snapshot)
console.set_active(is_console)
console_menu.set_active(is_console)
finally:
self.ignoreDetails = False
def _details_page_switch_cb(self, notebook, pagewidget, newpage):
for i in range(notebook.get_n_pages()):
w = notebook.get_nth_page(i)
w.set_visible(i == newpage)
self._refresh_current_page(newpage)
self._sync_toolbar_page_buttons(newpage)
self._sync_console_page_menu_state()
def change_run_text(self, can_restore):
if can_restore:
text = _("_Restore")
else:
text = _("_Run")
strip_text = text.replace("_", "")
self.widget("details-vm-menu").get_submenu().change_run_text(text)
self.widget("control-run").set_label(strip_text)
def _refresh_title(self):
title = (_("%(vm-name)s on %(connection-name)s") % {
"vm-name": self.vm.get_name_or_title(),
"connection-name": self.vm.conn.get_pretty_desc(),
})
grabmsg = self._console.vmwindow_get_title_message()
if grabmsg:
title = grabmsg + " " + title
self.topwin.set_title(title)
def _refresh_vm_state(self):
vm = self.vm
self._refresh_title()
self.widget("details-menu-view-toolbar").set_active(
self.config.get_details_show_toolbar())
self.toggle_toolbar(self.widget("details-menu-view-toolbar"))
run = vm.is_runable()
stop = vm.is_stoppable()
paused = vm.is_paused()
if vm.managedsave_supported:
self.change_run_text(vm.has_managed_save())
self.widget("control-run").set_sensitive(run)
self.widget("control-shutdown").set_sensitive(stop)
self.widget("control-shutdown").get_menu().update_widget_states(vm)
self.widget("control-pause").set_sensitive(stop)
if paused:
pauseTooltip = _("Resume the virtual machine")
else:
pauseTooltip = _("Pause the virtual machine")
self.widget("control-pause").set_tooltip_text(pauseTooltip)
self.widget("details-vm-menu").get_submenu().update_widget_states(vm)
self.set_pause_state(paused)
errmsg = self.vm.snapshots_supported()
cansnap = not bool(errmsg)
self.widget("control-snapshots").set_sensitive(cansnap)
self.widget("details-menu-view-snapshots").set_sensitive(cansnap)
tooltip = _("Manage VM snapshots")
if not cansnap:
tooltip += "\n" + errmsg
self.widget("control-snapshots").set_tooltip_text(tooltip)
self._refresh_current_page()
#############################
# External action listeners #
#############################
def view_manager(self, _src):
from .manager import vmmManager
vmmManager.get_instance(self).show()
def exit_app(self, _src):
vmmEngine.get_instance().exit_app()
def activate_default_console_page(self):
self._console.vmwindow_activate_default_console_page()
# activate_* are called from engine.py via CLI options
def activate_default_page(self):
if self.is_customize_dialog:
return
pages = self.widget("details-pages")
pages.set_current_page(DETAILS_PAGE_CONSOLE)
self.activate_default_console_page()
def activate_console_page(self):
pages = self.widget("details-pages")
pages.set_current_page(DETAILS_PAGE_CONSOLE)
def activate_performance_page(self):
self.widget("details-pages").set_current_page(DETAILS_PAGE_DETAILS)
self._details.vmwindow_activate_performance_page()
def activate_config_page(self):
self.widget("details-pages").set_current_page(DETAILS_PAGE_DETAILS)
def set_pause_state(self, state):
src = self.widget("control-pause")
try:
src.handler_block_by_func(self.control_vm_pause)
src.set_active(state)
finally:
src.handler_unblock_by_func(self.control_vm_pause)
def control_vm_pause(self, src):
do_pause = src.get_active()
# Set button state back to original value: just let the status
# update function fix things for us
self.set_pause_state(not do_pause)
if do_pause:
vmmenu.VMActionUI.suspend(self, self.vm)
else:
vmmenu.VMActionUI.resume(self, self.vm)
def control_vm_menu(self, src_ignore):
can_usb = bool(self.vm.has_spicevmc_type_redirdev() and
self._console.vmwindow_viewer_has_usb_redirection())
self.widget("details-menu-usb-redirection").set_sensitive(can_usb)
def control_vm_run(self, src_ignore):
if self._details.vmwindow_has_unapplied_changes():
return
vmmenu.VMActionUI.run(self, self.vm)
def control_vm_shutdown(self, src_ignore):
vmmenu.VMActionUI.shutdown(self, self.vm)
def control_vm_screenshot(self, src):
ignore = src
try:
return self._take_screenshot()
except Exception as e: # pragma: no cover
self.err.show_err(_("Error taking screenshot: %s") % str(e))
def control_vm_usb_redirection(self, src):
ignore = src
spice_usbdev_dialog = self.err
spice_usbdev_widget = self._console.vmwindow_viewer_get_usb_widget()
if not spice_usbdev_widget: # pragma: no cover
self.err.show_err(_("Error initializing spice USB device widget"))
return
spice_usbdev_widget.show()
spice_usbdev_dialog.show_info(_("Select USB devices for redirection"),
widget=spice_usbdev_widget,
buttons=Gtk.ButtonsType.CLOSE)
def _take_screenshot(self):
image = self._console.vmwindow_viewer_get_pixbuf()
metadata = {
'tEXt::Hypervisor URI': self.vm.conn.get_uri(),
'tEXt::Domain Name': self.vm.get_name(),
'tEXt::Domain UUID': self.vm.get_uuid(),
'tEXt::Generator App': self.config.get_appname(),
'tEXt::Generator Version': self.config.get_appversion(),
}
ret = image.save_to_bufferv(
'png', list(metadata.keys()), list(metadata.values())
)
# On Fedora 19, ret is (bool, str)
# Someday the bindings might be fixed to just return the str, try
# and future proof it a bit
if isinstance(ret, tuple) and len(ret) >= 2:
ret = ret[1]
# F24 rawhide, ret[1] is a named tuple with a 'buffer' element...
if hasattr(ret, "buffer"):
ret = ret.buffer # pragma: no cover
import datetime
now = str(datetime.datetime.now()).split(".")[0].replace(" ", "_")
default = "Screenshot_%s_%s.png" % (self.vm.get_name(), now)
path = self.err.browse_local(
self.vm.conn, _("Save Virtual Machine Screenshot"),
_type=("png", _("PNG files")),
dialog_type=Gtk.FileChooserAction.SAVE,
browse_reason=self.config.CONFIG_DIR_SCREENSHOT,
default_name=default)
if not path: # pragma: no cover
log.debug("No screenshot path given, skipping save.")
return
filename = path
if not filename.endswith(".png"):
filename += ".png" # pragma: no cover
open(filename, "wb").write(ret)
########################
# Details page refresh #
########################
def _refresh_resources(self):
details = self.widget("details-pages")
page = details.get_current_page()
if page == DETAILS_PAGE_DETAILS:
self._details.vmwindow_resources_refreshed()
def _refresh_current_page(self, newpage=None):
newpage = newpage or self.widget("details-pages").get_current_page()
is_details = newpage == DETAILS_PAGE_DETAILS
self._details.vmwindow_refresh_vm_state(is_details)
if newpage == DETAILS_PAGE_CONSOLE:
self._console.vmwindow_refresh_vm_state()
elif newpage == DETAILS_PAGE_SNAPSHOTS:
self._snapshots.vmwindow_refresh_vm_state()
#########################
# Console page handling #
#########################
def _sync_console_page_menu_state(self):
if not self.vm:
# This is triggered via cleanup + idle_add, so vm might
# disappear and spam the logs
return # pragma: no cover
paused = self.vm.is_paused()
is_viewer = self._console.vmwindow_get_viewer_is_visible()
can_usb = self._console.vmwindow_get_can_usb_redirect()
self.widget("details-menu-vm-screenshot").set_sensitive(is_viewer)
self.widget("details-menu-usb-redirection").set_sensitive(can_usb)
keycombo_menu = self._console.vmwindow_get_keycombo_menu()
can_sendkey = (is_viewer and not paused)
for c in keycombo_menu.get_children():
c.set_sensitive(can_sendkey)
self._console_refresh_can_fullscreen()
self._console_refresh_resizeguest_from_settings()
def _console_refresh_can_fullscreen(self):
allow_fullscreen = self._console.vmwindow_get_viewer_is_visible()
self.widget("control-fullscreen").set_sensitive(allow_fullscreen)
self.widget("details-menu-view-fullscreen").set_sensitive(
allow_fullscreen)
def _console_refresh_scaling_from_settings(self):
scale_type = self.vm.get_console_scaling()
self.widget("details-menu-view-scale-always").set_active(
scale_type == self.config.CONSOLE_SCALE_ALWAYS)
self.widget("details-menu-view-scale-never").set_active(
scale_type == self.config.CONSOLE_SCALE_NEVER)
self.widget("details-menu-view-scale-fullscreen").set_active(
scale_type == self.config.CONSOLE_SCALE_FULLSCREEN)
self._console.vmwindow_sync_scaling_with_display()
def _scaling_ui_changed_cb(self, src):
# Called from details.py
if not src.get_active():
return
scale_type = 0
if src == self.widget("details-menu-view-scale-always"):
scale_type = self.config.CONSOLE_SCALE_ALWAYS
elif src == self.widget("details-menu-view-scale-fullscreen"):
scale_type = self.config.CONSOLE_SCALE_FULLSCREEN
elif src == self.widget("details-menu-view-scale-never"):
scale_type = self.config.CONSOLE_SCALE_NEVER
self.vm.set_console_scaling(scale_type)
def _fullscreen_changed_cb(self, src):
do_fullscreen = src.get_active()
self.widget("control-fullscreen").set_active(do_fullscreen)
self._console.vmwindow_set_fullscreen(do_fullscreen)
self.widget("details-menubar").set_visible(not do_fullscreen)
show_toolbar = not do_fullscreen
if not self.widget("details-menu-view-toolbar").get_active():
show_toolbar = False # pragma: no cover
self.widget("toolbar-box").set_visible(show_toolbar)
def _resizeguest_ui_changed_cb(self, src):
if not src.get_sensitive():
return # pragma: no cover
val = int(self.widget("details-menu-view-resizeguest").get_active())
self.vm.set_console_resizeguest(val)
self._console.vmwindow_sync_resizeguest_with_display()
def _console_refresh_resizeguest_from_settings(self):
tooltip = self._console.vmwindow_get_resizeguest_tooltip()
val = self.vm.get_console_resizeguest()
widget = self.widget("details-menu-view-resizeguest")
widget.set_tooltip_text(tooltip)
widget.set_sensitive(not bool(tooltip))
if not tooltip:
self.widget("details-menu-view-resizeguest").set_active(bool(val))
self._console.vmwindow_sync_resizeguest_with_display()
def _autoconnect_ui_changed_cb(self, src):
val = int(self.widget("details-menu-view-autoconnect").get_active())
self.vm.set_console_autoconnect(val)
def _console_refresh_autoconnect_from_settings(self):
val = self.vm.get_console_autoconnect()
self.widget("details-menu-view-autoconnect").set_active(val)
def _size_to_vm_cb(self, src):
self._console.vmwindow_set_size_to_vm()
def _console_leave_fullscreen_cb(self, src):
# This will trigger de-fullscreening in a roundabout way
self.widget("control-fullscreen").set_active(False)
def _console_change_title_cb(self, src):
self._refresh_title()
def _vm_state_changed_cb(self, src):
if self.is_visible():
self._refresh_vm_state()
def _resources_sampled_cb(self, src):
if self.is_visible():
self._refresh_resources()
def _console_page_changed_cb(self, src):
self._sync_console_page_menu_state()