snapshots: Split out vmmSnapshotNew class

Simplifies code org
This commit is contained in:
Cole Robinson 2019-05-23 20:34:55 -04:00
parent 318e0c0a39
commit f2304664d6
4 changed files with 565 additions and 512 deletions

View File

@ -1,297 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.2 --> <!-- Generated with glade 3.22.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.22"/> <requires lib="gtk+" version="3.22"/>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-new</property>
</object>
<object class="GtkWindow" id="snapshot-new">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Create snapshot</property>
<property name="resizable">False</property>
<property name="type_hint">dialog</property>
<signal name="delete-event" handler="on_snapshot_new_delete_event" swapped="no"/>
<child>
<object class="GtkBox" id="box1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkViewport" id="header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="resize_mode">queue</property>
<child>
<object class="GtkBox" id="hbox77">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">6</property>
<property name="spacing">10</property>
<child>
<object class="GtkImage" id="image4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-new</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="hbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">&lt;span size='large' color='white'&gt;Create snapshot&lt;/span&gt;</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkGrid" id="box4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<property name="column_spacing">6</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Name:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">snapshot-new-name</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="snapshot-new-name">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">●</property>
<signal name="activate" handler="on_snapshot_new_name_activate" swapped="no"/>
<signal name="changed" handler="on_snapshot_new_name_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="label" translatable="yes">_Description:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">snapshot-new-description</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow1">
<property name="width_request">300</property>
<property name="height_request">125</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="snapshot-new-description">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Status:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">3</property>
<child>
<object class="GtkImage" id="snapshot-new-status-icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-cancel</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="snapshot-new-status-text">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label">Shut down</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label6">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="label" translatable="yes">Screenshot:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkImage" id="snapshot-new-screenshot">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="stock">gtk-missing-image</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="buttonbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="snapshot-new-cancel">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_snapshot_new_cancel_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="snapshot-new-ok">
<property name="label" translatable="yes">_Finish</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">image3</property>
<property name="use_underline">True</property>
<signal name="clicked" handler="on_snapshot_new_ok_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<placeholder/>
</child>
</object>
<object class="GtkWindow" id="snapshot-top-window"> <object class="GtkWindow" id="snapshot-top-window">
<property name="width_request">600</property> <property name="width_request">600</property>
<property name="height_request">400</property> <property name="height_request">400</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<child type="titlebar">
<placeholder/>
</child>
<child> <child>
<object class="GtkBox" id="snapshot-top-box"> <object class="GtkBox" id="snapshot-top-box">
<property name="visible">True</property> <property name="visible">True</property>
@ -819,8 +536,5 @@
</child> </child>
</object> </object>
</child> </child>
<child type="titlebar">
<placeholder/>
</child>
</object> </object>
</interface> </interface>

296
ui/snapshotsnew.ui Normal file
View File

@ -0,0 +1,296 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-new</property>
</object>
<object class="GtkWindow" id="snapshot-new">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Create snapshot</property>
<property name="type_hint">dialog</property>
<signal name="delete-event" handler="on_snapshot_new_delete_event" swapped="no"/>
<child type="titlebar">
<placeholder/>
</child>
<child>
<object class="GtkBox" id="box1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkViewport" id="header">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="resize_mode">queue</property>
<child>
<object class="GtkBox" id="hbox77">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">6</property>
<property name="spacing">10</property>
<child>
<object class="GtkImage" id="image4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-new</property>
<property name="icon_size">6</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="hbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">&lt;span size='large' color='white'&gt;Create snapshot&lt;/span&gt;</property>
<property name="use_markup">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<child>
<object class="GtkAlignment" id="snapshot-new-box-align">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkGrid" id="snapshot-new-box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<property name="column_spacing">6</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Name:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">snapshot-new-name</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="snapshot-new-name">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">●</property>
<signal name="activate" handler="on_snapshot_new_name_activate" swapped="no"/>
<signal name="changed" handler="on_snapshot_new_name_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="label" translatable="yes">_Description:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">snapshot-new-description</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow1">
<property name="width_request">300</property>
<property name="height_request">125</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="snapshot-new-description">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Status:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">3</property>
<child>
<object class="GtkImage" id="snapshot-new-status-icon">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-cancel</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="snapshot-new-status-text">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label">Shut down</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label6">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="label" translatable="yes">Screenshot:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkImage" id="snapshot-new-screenshot">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="stock">gtk-missing-image</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="buttonbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="snapshot-new-cancel">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_snapshot_new_cancel_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="snapshot-new-ok">
<property name="label" translatable="yes">_Finish</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">image3</property>
<property name="use_underline">True</property>
<signal name="clicked" handler="on_snapshot_new_ok_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@ -258,13 +258,6 @@ class vmmGObject(GObject.GObject):
class vmmGObjectUI(vmmGObject): class vmmGObjectUI(vmmGObject):
@staticmethod
def bind_escape_key_close_helper(topwin, close_cb):
def close_on_escape(src_ignore, event):
if Gdk.keyval_name(event.keyval) == "Escape":
close_cb()
topwin.connect("key-press-event", close_on_escape)
def __init__(self, filename, windowname, builder=None, topwin=None): def __init__(self, filename, windowname, builder=None, topwin=None):
vmmGObject.__init__(self) vmmGObject.__init__(self)
self._external_topwin = bool(topwin) self._external_topwin = bool(topwin)
@ -325,7 +318,10 @@ class vmmGObjectUI(vmmGObject):
return bool(self.topwin and self.topwin.get_visible()) return bool(self.topwin and self.topwin.get_visible())
def bind_escape_key_close(self): def bind_escape_key_close(self):
self.bind_escape_key_close_helper(self.topwin, self.close) def close_on_escape(src_ignore, event):
if Gdk.keyval_name(event.keyval) == "Escape":
self.close()
self.topwin.connect("key-press-event", close_on_escape)
def _set_cursor(self, cursor_type): def _set_cursor(self, cursor_type):
gdk_window = self.topwin.get_window() gdk_window = self.topwin.get_window()

View File

@ -29,6 +29,30 @@ mimemap = {
} }
def _make_screenshot_pixbuf(mime, sdata):
loader = GdkPixbuf.PixbufLoader.new_with_mime_type(mime)
loader.write(sdata)
pixbuf = loader.get_pixbuf()
loader.close()
maxsize = 450
def _scale(big, small, maxsize):
if big <= maxsize:
return big, small
factor = float(maxsize) / float(big)
return maxsize, int(factor * float(small))
width = pixbuf.get_width()
height = pixbuf.get_height()
if width > height:
width, height = _scale(width, height, maxsize)
else:
height, width = _scale(height, width, maxsize)
return pixbuf.scale_simple(width, height,
GdkPixbuf.InterpType.BILINEAR)
def _mime_to_ext(val, reverse=False): def _mime_to_ext(val, reverse=False):
for m, e in mimemap.items(): for m, e in mimemap.items():
if val == m and not reverse: if val == m and not reverse:
@ -40,6 +64,226 @@ def _mime_to_ext(val, reverse=False):
reverse and "mime" or "extension") reverse and "mime" or "extension")
class vmmSnapshotNew(vmmGObjectUI):
__gsignals__ = {
"snapshot-created": (vmmGObjectUI.RUN_FIRST, None, [str]),
}
def __init__(self, vm):
vmmGObjectUI.__init__(self, "snapshotsnew.ui", "snapshot-new")
self.vm = vm
self._init_ui()
self.builder.connect_signals({
"on_snapshot_new_delete_event": self.close,
"on_snapshot_new_cancel_clicked": self.close,
"on_snapshot_new_name_changed": self._name_changed_cb,
"on_snapshot_new_name_activate": self._ok_clicked_cb,
"on_snapshot_new_ok_clicked": self._ok_clicked_cb,
})
self.bind_escape_key_close()
#######################
# Standard UI methods #
#######################
def show(self, parent):
logging.debug("Showing new snapshot wizard")
self._reset_state()
self.topwin.set_transient_for(parent)
self.topwin.present()
def close(self, ignore1=None, ignore2=None):
logging.debug("Closing new snapshot wizard")
self.topwin.hide()
return 1
def _cleanup(self):
self.vm = None
###########
# UI init #
###########
def _init_ui(self):
blue = Gdk.color_parse("#0072A8")
self.widget("header").modify_bg(Gtk.StateType.NORMAL, blue)
buf = Gtk.TextBuffer()
self.widget("snapshot-new-description").set_buffer(buf)
def _reset_state(self):
collidelist = [s.get_xmlobj().name for s in self.vm.list_snapshots()]
default_name = DomainSnapshot.find_free_name(
self.vm.get_backend(), collidelist)
self.widget("snapshot-new-name").set_text(default_name)
self.widget("snapshot-new-name").emit("changed")
self.widget("snapshot-new-description").get_buffer().set_text("")
self.widget("snapshot-new-ok").grab_focus()
self.widget("snapshot-new-status-text").set_text(self.vm.run_status())
self.widget("snapshot-new-status-icon").set_from_icon_name(
self.vm.run_status_icon_name(), Gtk.IconSize.BUTTON)
sn = self._get_screenshot()
uiutil.set_grid_row_visible(
self.widget("snapshot-new-screenshot"), bool(sn))
if sn:
self.widget("snapshot-new-screenshot").set_from_pixbuf(sn)
self.widget("snapshot-new-name").grab_focus()
###################
# Create handling #
###################
def _take_screenshot(self):
stream = None
try:
stream = self.vm.conn.get_backend().newStream(0)
screen = 0
flags = 0
mime = self.vm.get_backend().screenshot(stream, screen, flags)
ret = io.BytesIO()
def _write_cb(_stream, data, userdata):
ignore = stream
ignore = userdata
ret.write(data)
stream.recvAll(_write_cb, None)
return mime, ret.getvalue()
finally:
try:
if stream:
stream.finish()
except Exception:
pass
def _get_screenshot(self):
if not self.vm.is_active():
logging.debug("Skipping screenshot since VM is not active")
return
if not self.vm.xmlobj.devices.graphics:
logging.debug("Skipping screenshot since VM has no graphics")
return
try:
# Perform two screenshots, because qemu + qxl has a bug where
# screenshot generally only shows the data from the previous
# screenshot request:
# https://bugs.launchpad.net/qemu/+bug/1314293
self._take_screenshot()
mime, sdata = self._take_screenshot()
except Exception:
logging.exception("Error taking screenshot")
return
ext = _mime_to_ext(mime)
if not ext:
return
newpix = _make_screenshot_pixbuf(mime, sdata)
setattr(newpix, "vmm_mimetype", mime)
setattr(newpix, "vmm_sndata", sdata)
return newpix
def _new_finish_cb(self, error, details, newname):
self.reset_finish_cursor()
if error is not None:
error = _("Error creating snapshot: %s") % error
self.err.show_err(error, details=details)
return
self.emit("snapshot-created", newname)
def _validate_new_snapshot(self):
name = self.widget("snapshot-new-name").get_text()
desc = self.widget("snapshot-new-description"
).get_buffer().get_property("text")
try:
newsnap = DomainSnapshot(self.vm.conn.get_backend())
newsnap.name = name
newsnap.description = desc or None
newsnap.validate()
newsnap.get_xml()
return newsnap
except Exception as e:
return self.err.val_err(_("Error validating snapshot: %s") % e)
def _get_screenshot_data_for_save(self):
snwidget = self.widget("snapshot-new-screenshot")
if not snwidget.is_visible():
return None, None
sn = snwidget.get_pixbuf()
if not sn:
return None, None
mime = getattr(sn, "vmm_mimetype", None)
sndata = getattr(sn, "vmm_sndata", None)
return mime, sndata
def _do_create_snapshot(self, asyncjob, xml, name, mime, sndata):
ignore = asyncjob
self.vm.create_snapshot(xml)
try:
cachedir = self.vm.get_cache_dir()
basesn = os.path.join(cachedir, "snap-screenshot-%s" % name)
# Remove any pre-existing screenshots so we don't show stale data
for ext in list(mimemap.values()):
p = basesn + "." + ext
if os.path.exists(basesn + "." + ext):
os.unlink(p)
if not mime or not sndata:
return
filename = basesn + "." + _mime_to_ext(mime)
logging.debug("Writing screenshot to %s", filename)
open(filename, "wb").write(sndata)
except Exception:
logging.exception("Error saving screenshot")
def _create_new_snapshot(self):
snap = self._validate_new_snapshot()
if not snap:
return
xml = snap.get_xml()
name = snap.name
mime, sndata = self._get_screenshot_data_for_save()
self.close()
self.set_finish_cursor()
progWin = vmmAsyncJob(
self._do_create_snapshot, [xml, name, mime, sndata],
self._new_finish_cb, [name],
_("Creating snapshot"),
_("Creating virtual machine snapshot"),
self.topwin)
progWin.run()
################
# UI listeners #
################
def _name_changed_cb(self, src):
self.widget("snapshot-new-ok").set_sensitive(bool(src.get_text()))
def _ok_clicked_cb(self, src):
return self._create_new_snapshot()
class vmmSnapshotPage(vmmGObjectUI): class vmmSnapshotPage(vmmGObjectUI):
def __init__(self, vm, builder, topwin): def __init__(self, vm, builder, topwin):
vmmGObjectUI.__init__(self, "snapshots.ui", vmmGObjectUI.__init__(self, "snapshots.ui",
@ -49,15 +293,11 @@ class vmmSnapshotPage(vmmGObjectUI):
self._initial_populate = False self._initial_populate = False
self._unapplied_changes = False self._unapplied_changes = False
self._snapshot_new = None
self._snapmenu = None self._snapmenu = None
self._init_ui() self._init_ui()
self._snapshot_new = self.widget("snapshot-new")
self._snapshot_new.set_transient_for(self.topwin)
self.bind_escape_key_close_helper(self._snapshot_new,
self._snapshot_new_close)
self.builder.connect_signals({ self.builder.connect_signals({
"on_snapshot_add_clicked": self._on_add_clicked, "on_snapshot_add_clicked": self._on_add_clicked,
"on_snapshot_delete_clicked": self._on_delete_clicked, "on_snapshot_delete_clicked": self._on_delete_clicked,
@ -67,13 +307,6 @@ class vmmSnapshotPage(vmmGObjectUI):
"on_snapshot_list_button_press_event": self._popup_snapshot_menu, "on_snapshot_list_button_press_event": self._popup_snapshot_menu,
"on_snapshot_refresh_clicked": self._on_refresh_clicked, "on_snapshot_refresh_clicked": self._on_refresh_clicked,
"on_snapshot_list_row_activated": self._on_start_clicked, "on_snapshot_list_row_activated": self._on_start_clicked,
# 'Create' dialog
"on_snapshot_new_delete_event": self._snapshot_new_close,
"on_snapshot_new_ok_clicked": self._on_new_ok_clicked,
"on_snapshot_new_cancel_clicked": self._snapshot_new_close,
"on_snapshot_new_name_changed": self._snapshot_new_name_changed,
"on_snapshot_new_name_activate": self._on_new_ok_clicked,
}) })
self.top_box = self.widget("snapshot-top-box") self.top_box = self.widget("snapshot-top-box")
@ -83,31 +316,27 @@ class vmmSnapshotPage(vmmGObjectUI):
selection.set_mode(Gtk.SelectionMode.MULTIPLE) selection.set_mode(Gtk.SelectionMode.MULTIPLE)
selection.set_select_function(self._confirm_changes, None) selection.set_select_function(self._confirm_changes, None)
############## ##############
# Init stuff # # Init stuff #
############## ##############
def _cleanup(self): def _cleanup(self):
self.vm = None self.vm = None
self._snapshot_new.destroy()
self._snapshot_new = None
self._snapmenu = None self._snapmenu = None
if self._snapshot_new:
self._snapshot_new.cleanup()
self._snapshot_new = None
def _init_ui(self): def _init_ui(self):
# pylint: disable=redefined-variable-type # pylint: disable=redefined-variable-type
blue = Gdk.color_parse("#0072A8")
self.widget("header").modify_bg(Gtk.StateType.NORMAL, blue)
self.widget("snapshot-notebook").set_show_tabs(False) self.widget("snapshot-notebook").set_show_tabs(False)
buf = Gtk.TextBuffer() buf = Gtk.TextBuffer()
buf.connect("changed", self._description_changed) buf.connect("changed", self._description_changed)
self.widget("snapshot-description").set_buffer(buf) self.widget("snapshot-description").set_buffer(buf)
buf = Gtk.TextBuffer()
self.widget("snapshot-new-description").set_buffer(buf)
# [name, row label, tooltip, icon name, sortname, current] # [name, row label, tooltip, icon name, sortname, current]
model = Gtk.ListStore(str, str, str, str, str, bool) model = Gtk.ListStore(str, str, str, str, str, bool)
model.set_sort_column_id(4, Gtk.SortType.ASCENDING) model.set_sort_column_id(4, Gtk.SortType.ASCENDING)
@ -254,29 +483,6 @@ class vmmSnapshotPage(vmmGObjectUI):
self._initial_populate = True self._initial_populate = True
def _make_screenshot_pixbuf(self, mime, sdata):
loader = GdkPixbuf.PixbufLoader.new_with_mime_type(mime)
loader.write(sdata)
pixbuf = loader.get_pixbuf()
loader.close()
maxsize = 450
def _scale(big, small, maxsize):
if big <= maxsize:
return big, small
factor = float(maxsize) / float(big)
return maxsize, int(factor * float(small))
width = pixbuf.get_width()
height = pixbuf.get_height()
if width > height:
width, height = _scale(width, height, maxsize)
else:
height, width = _scale(height, width, maxsize)
return pixbuf.scale_simple(width, height,
GdkPixbuf.InterpType.BILINEAR)
def _read_screenshot_file(self, name): def _read_screenshot_file(self, name):
if not name: if not name:
return return
@ -291,7 +497,7 @@ class vmmSnapshotPage(vmmGObjectUI):
mime = _mime_to_ext(os.path.splitext(filename)[1][1:], reverse=True) mime = _mime_to_ext(os.path.splitext(filename)[1][1:], reverse=True)
if not mime: if not mime:
return return
return self._make_screenshot_pixbuf(mime, open(filename, "rb").read()) return _make_screenshot_pixbuf(mime, open(filename, "rb").read())
def _set_snapshot_state(self, snap=None): def _set_snapshot_state(self, snap=None):
self.widget("snapshot-notebook").set_current_page(0) self.widget("snapshot-notebook").set_current_page(0)
@ -363,164 +569,6 @@ class vmmSnapshotPage(vmmGObjectUI):
return True return True
##################
# 'New' handling #
##################
def _take_screenshot(self):
stream = None
try:
stream = self.vm.conn.get_backend().newStream(0)
screen = 0
flags = 0
mime = self.vm.get_backend().screenshot(stream, screen, flags)
ret = io.BytesIO()
def _write_cb(_stream, data, userdata):
ignore = stream
ignore = userdata
ret.write(data)
stream.recvAll(_write_cb, None)
return mime, ret.getvalue()
finally:
try:
if stream:
stream.finish()
except Exception:
pass
def _get_screenshot(self):
if not self.vm.is_active():
logging.debug("Skipping screenshot since VM is not active")
return
if not self.vm.xmlobj.devices.graphics:
logging.debug("Skipping screenshot since VM has no graphics")
return
try:
# Perform two screenshots, because qemu + qxl has a bug where
# screenshot generally only shows the data from the previous
# screenshot request:
# https://bugs.launchpad.net/qemu/+bug/1314293
self._take_screenshot()
mime, sdata = self._take_screenshot()
except Exception:
logging.exception("Error taking screenshot")
return
ext = _mime_to_ext(mime)
if not ext:
return
newpix = self._make_screenshot_pixbuf(mime, sdata)
setattr(newpix, "vmm_mimetype", mime)
setattr(newpix, "vmm_sndata", sdata)
return newpix
def _reset_new_state(self):
collidelist = [s.get_xmlobj().name for s in self.vm.list_snapshots()]
default_name = DomainSnapshot.find_free_name(
self.vm.get_backend(), collidelist)
self.widget("snapshot-new-name").set_text(default_name)
self.widget("snapshot-new-name").emit("changed")
self.widget("snapshot-new-description").get_buffer().set_text("")
self.widget("snapshot-new-ok").grab_focus()
self.widget("snapshot-new-status-text").set_text(self.vm.run_status())
self.widget("snapshot-new-status-icon").set_from_icon_name(
self.vm.run_status_icon_name(), Gtk.IconSize.BUTTON)
sn = self._get_screenshot()
uiutil.set_grid_row_visible(
self.widget("snapshot-new-screenshot"), bool(sn))
if sn:
self.widget("snapshot-new-screenshot").set_from_pixbuf(sn)
def _snapshot_new_name_changed(self, src):
self.widget("snapshot-new-ok").set_sensitive(bool(src.get_text()))
def _new_finish_cb(self, error, details, newname):
self.reset_finish_cursor()
if error is not None:
error = _("Error creating snapshot: %s") % error
self.err.show_err(error, details=details)
return
self._refresh_snapshots(newname)
def _validate_new_snapshot(self):
name = self.widget("snapshot-new-name").get_text()
desc = self.widget("snapshot-new-description"
).get_buffer().get_property("text")
try:
newsnap = DomainSnapshot(self.vm.conn.get_backend())
newsnap.name = name
newsnap.description = desc or None
newsnap.validate()
newsnap.get_xml()
return newsnap
except Exception as e:
return self.err.val_err(_("Error validating snapshot: %s") % e)
def _get_screenshot_data_for_save(self):
snwidget = self.widget("snapshot-new-screenshot")
if not snwidget.is_visible():
return None, None
sn = snwidget.get_pixbuf()
if not sn:
return None, None
mime = getattr(sn, "vmm_mimetype", None)
sndata = getattr(sn, "vmm_sndata", None)
return mime, sndata
def _do_create_snapshot(self, asyncjob, xml, name, mime, sndata):
ignore = asyncjob
self.vm.create_snapshot(xml)
try:
cachedir = self.vm.get_cache_dir()
basesn = os.path.join(cachedir, "snap-screenshot-%s" % name)
# Remove any pre-existing screenshots so we don't show stale data
for ext in list(mimemap.values()):
p = basesn + "." + ext
if os.path.exists(basesn + "." + ext):
os.unlink(p)
if not mime or not sndata:
return
filename = basesn + "." + _mime_to_ext(mime)
logging.debug("Writing screenshot to %s", filename)
open(filename, "wb").write(sndata)
except Exception:
logging.exception("Error saving screenshot")
def _create_new_snapshot(self):
snap = self._validate_new_snapshot()
if not snap:
return
xml = snap.get_xml()
name = snap.name
mime, sndata = self._get_screenshot_data_for_save()
self._snapshot_new_close()
self.set_finish_cursor()
progWin = vmmAsyncJob(
self._do_create_snapshot, [xml, name, mime, sndata],
self._new_finish_cb, [name],
_("Creating snapshot"),
_("Creating virtual machine snapshot"),
self.topwin)
progWin.run()
def _apply(self): def _apply(self):
snaps = self._get_selected_snapshots() snaps = self._get_selected_snapshots()
if not snaps or len(snaps) > 1: if not snaps or len(snaps) > 1:
@ -554,10 +602,9 @@ class vmmSnapshotPage(vmmGObjectUI):
return return
self._snapmenu.popup_at_pointer(event) self._snapmenu.popup_at_pointer(event)
def _snapshot_new_close(self, *args, **kwargs): def close(self, ignore1=None, ignore2=None):
ignore = args if self._snapshot_new:
ignore = kwargs self._snapshot_new.close()
self._snapshot_new.hide()
return 1 return 1
def _description_changed(self, ignore): def _description_changed(self, ignore):
@ -574,15 +621,15 @@ class vmmSnapshotPage(vmmGObjectUI):
self._apply() self._apply()
self._refresh_snapshots() self._refresh_snapshots()
def _on_new_ok_clicked(self, ignore): def _snapshot_created_cb(self, src, newname):
return self._create_new_snapshot() self._refresh_snapshots(newname)
def _on_add_clicked(self, ignore): def _on_add_clicked(self, ignore):
if self._snapshot_new.is_visible(): if not self._snapshot_new:
return self._snapshot_new = vmmSnapshotNew(self.vm)
self._reset_new_state() self._snapshot_new.connect("snapshot-created",
self._snapshot_new.show() self._snapshot_created_cb)
self.widget("snapshot-new-name").grab_focus() self._snapshot_new.show(self.topwin)
def _on_refresh_clicked(self, ignore): def _on_refresh_clicked(self, ignore):
self._refresh_snapshots() self._refresh_snapshots()