[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

[et-mgmt-tools] [PATCH] virt-manager: Storage aware delete dialog



Hi all,

The attached patch adds a storage aware delete dialog to virt-manager.
When deleting a VM, we are presented with a list of storage attached to
it, with an option to remove individual disks as part of the delete process.

Some screenshots:

http://fedorapeople.org/~crobinso/virt-manager/delete/vmm-delete-2.1.png

The dialog's look when it is launched (we make the user opt in to delete
storage).

http://fedorapeople.org/~crobinso/virt-manager/delete/vmm-delete-2.2.png

Even when the user opts in, we don't select all disks for deletion by
default. We won't select a disk by default if:

It is marked as read-only in the xml
It is marked as shareable in the xml
It is in use by another VM on the same connection

A tooltip to this effect is shown when hovering over the warning icon.

http://fedorapeople.org/~crobinso/virt-manager/delete/vmm-delete-2.4.png

If we think we can't delete certain storage, the check box is marked as
inconsistent. There are multiple reasons we can't delete (no
permissions, storage is an iSCSI volume, storage isn't managed and we
are on a remote connection, etc.) The reasoning is also shown in a tooltip.

If any storage deletion fails, we continue with the process, and just
show all aggregated errors to the user at the end. The VM removal will
be attempted regardless.

That's about it. Questions or comments appreciated.

Thanks,
Cole
# HG changeset patch
# User Cole Robinson <crobinso redhat com>
# Node ID bc56a6f4f2d08a11f8b9101441c5abe6d34d0c75
# Parent  311a239d48af9913ad1a553d15490444155e4f56
Storage aware 'Delete VM' dialog.

diff -r 311a239d48af -r bc56a6f4f2d0 src/virtManager/connection.py
--- a/src/virtManager/connection.py	Thu Mar 05 10:44:26 2009 -0500
+++ b/src/virtManager/connection.py	Thu Mar 05 11:23:54 2009 -0500
@@ -315,6 +315,19 @@
                 return pool
         return None
 
+    def get_pool_by_name(self, name):
+        for p in self.pools.values():
+            if p.get_name() == name:
+                return p
+        return None
+
+    def get_vol_by_path(self, path):
+        for pool in self.pools.values():
+            for vol in pool.get_volumes().values():
+                if vol.get_path() == path:
+                    return vol
+        return None
+
     def open(self):
         if self.state != self.STATE_DISCONNECTED:
             return
diff -r 311a239d48af -r bc56a6f4f2d0 src/virtManager/delete.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/virtManager/delete.py	Thu Mar 05 11:23:54 2009 -0500
@@ -0,0 +1,384 @@
+#
+# Copyright (C) 2009 Red Hat, Inc.
+# Copyright (C) 2009 Cole Robinson <crobinso redhat com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301 USA.
+#
+
+import gobject
+import gtk.glade
+
+import os, stat
+import traceback
+import logging
+
+import libvirt
+import virtinst
+
+from virtManager.error import vmmErrorDialog
+from virtManager.asyncjob import vmmAsyncJob
+from virtManager.createmeter import vmmCreateMeter
+
+STORAGE_ROW_CONFIRM = 0
+STORAGE_ROW_CANT_DELETE = 1
+STORAGE_ROW_PATH = 2
+STORAGE_ROW_TARGET = 3
+STORAGE_ROW_ICON_SHOW = 4
+STORAGE_ROW_ICON = 5
+STORAGE_ROW_ICON_SIZE = 6
+STORAGE_ROW_TOOLTIP = 7
+
+class vmmDeleteDialog(gobject.GObject):
+    __gsignals__ = {
+        #"vol-created": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, [])
+    }
+
+    def __init__(self, config, vm):
+        self.__gobject_init__()
+        self.window = gtk.glade.XML(config.get_glade_dir() + \
+                                    "/vmm-delete.glade",
+                                    "vmm-delete", domain="virt-manager")
+        self.config = config
+        self.vm = vm
+        self.conn = vm.connection
+
+        self.topwin = self.window.get_widget("vmm-delete")
+        self.err = vmmErrorDialog(self.topwin,
+                                  0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
+                                  _("Unexpected Error"),
+                                  _("An unexpected error occurred"))
+        self.topwin.hide()
+
+        self.window.signal_autoconnect({
+            "on_vmm_delete_delete_event" : self.close,
+            "on_delete_cancel_clicked" : self.close,
+            "on_delete_ok_clicked" : self.finish,
+            "on_delete_remove_storage_toggled" : self.toggle_remove_storage,
+        })
+
+        prepare_storage_list(self.window.get_widget("delete-storage-list"))
+
+    def toggle_remove_storage(self, src):
+        dodel = src.get_active()
+        self.window.get_widget("delete-storage-list").set_sensitive(dodel)
+
+
+    def show(self):
+        self.reset_state()
+        self.topwin.show()
+        self.topwin.present()
+
+    def close(self, ignore1=None, ignore2=None):
+        self.topwin.hide()
+        return 1
+
+    def reset_state(self):
+
+        # Set VM name in title'
+        title_str = ("<span size='x-large'>%s '%s'</span>" %
+                     (_("Delete"), self.vm.get_name()))
+        self.window.get_widget("delete-main-label").set_markup(title_str)
+
+        # Disable storage removal by default
+        self.window.get_widget("delete-remove-storage").set_active(False)
+        self.window.get_widget("delete-remove-storage").toggled()
+
+        populate_storage_list(self.window.get_widget("delete-storage-list"),
+                              self.vm, self.conn)
+
+    def set_vm(self, vm):
+        self.vm = vm
+        self.conn = vm.connection
+        self.reset_state()
+
+    def get_config_format(self):
+        format_combo = self.window.get_widget("vol-format")
+        model = format_combo.get_model()
+        if format_combo.get_active_iter() != None:
+            model = format_combo.get_model()
+            return model.get_value(format_combo.get_active_iter(), 0)
+        return None
+
+    def get_paths_to_delete(self):
+        del_list = self.window.get_widget("delete-storage-list")
+        model = del_list.get_model()
+
+        paths = []
+        if self.window.get_widget("delete-remove-storage").get_active():
+            for row in model:
+                if (not row[STORAGE_ROW_CANT_DELETE] and
+                    row[STORAGE_ROW_CONFIRM]):
+                    paths.append(row[STORAGE_ROW_PATH])
+        return paths
+
+    def finish(self, src):
+        devs = self.get_paths_to_delete()
+
+        self.error_msg = None
+        self.error_details = None
+        self.topwin.set_sensitive(False)
+        self.topwin.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
+
+        title = _("Deleting virtual machine '%s'") % self.vm.get_name()
+        text = title
+        if devs:
+            text = title + _(" and selected storage (this may take a while")
+
+        progWin = vmmAsyncJob(self.config, self._async_delete, [devs],
+                              title=title, text=text)
+        progWin.run()
+
+        self.topwin.set_sensitive(True)
+        self.topwin.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.TOP_LEFT_ARROW))
+        self.close()
+
+        if self.error_msg is not None:
+            self.err.show_err(self.error_msg, self.error_details)
+
+        self.conn.tick(noStatsUpdate=True)
+
+
+    def _async_delete(self, paths, asyncjob):
+        newconn = None
+        storage_errors = []
+
+        try:
+            # Open a seperate connection to install on since this is async
+            logging.debug("Threading off connection to delete vol.")
+            #newconn = vmmConnection(self.config, self.conn.get_uri(),
+            #                        self.conn.is_read_only())
+            #newconn.open()
+            #newconn.connectThreadEvent.wait()
+            newconn = libvirt.open(self.conn.get_uri())
+            meter = vmmCreateMeter(asyncjob)
+
+            for path in paths:
+                try:
+                    logging.debug("Deleting path: %s" % path)
+                    meter.start(text = _("Deleting path '%s'") % path)
+                    self._async_delete_path(newconn, path, meter)
+                except Exception, e:
+                    storage_errors.append((str(e),
+                                          "".join(traceback.format_exc())))
+                meter.end(0)
+
+            logging.debug("Removing VM '%s'" % self.vm.get_name())
+            self.vm.delete()
+
+        except Exception, e:
+            self.error_msg = (_("Error deleting virtual machine '%s': %s") %
+                              (self.vm.get_name(), str(e)))
+            self.error_details = "".join(traceback.format_exc())
+            logging.error(self.error_msg + "\n" + self.error_details)
+
+        storage_errstr = ""
+        for errinfo in storage_errors:
+            storage_errstr += "%s\n%s\n" % (errinfo[0], errinfo[1])
+
+        if not storage_errstr:
+            return
+
+        # We had extra storage errors. If there was another error message,
+        # errors to it. Otherwise, build the main error around them.
+        if self.error_details:
+            self.error_details += "\n\n"
+            self.error_details += _("Additionally, there were errors removing"
+                                    " certain storage devices: \n")
+            self.error_details += storage_errstr
+        else:
+            self.error_msg = _("Errors encountered while removing certain "
+                               "storage devices.")
+            self.error_details = storage_errstr
+
+    def _async_delete_path(self, conn, path, ignore):
+        vol = None
+
+        try:
+            vol = conn.storageVolLookupByPath(path)
+        except:
+            logging.debug("Path '%s' is not managed. Deleting locally." % path)
+
+        if vol:
+            vol.delete(0)
+        else:
+            os.unlink(path)
+
+
+def populate_storage_list(storage_list, vm, conn):
+    model = storage_list.get_model()
+    model.clear()
+
+    for disk in vm.get_disk_devices():
+        vol = None
+
+        target = disk[1]
+        path = disk[3]
+        ro = disk[6]
+        shared = disk[7]
+
+        # There are a few pieces here
+        # 1) Can we even delete the storage? If not, make the checkbox
+        #    inconsistent. self.can_delete decides this for us, and if
+        #    we can't delete, gives us a nice message to show the user
+        #    for that row.
+        #
+        # 2) If we can delete, do we want to delete this storage by
+        #    default? Reasons not to, are if the storage is marked
+        #    readonly or sharable, or is in use by another VM.
+
+        if not path or path == "-":
+            continue
+
+        default = False
+        definfo = None
+        vol = conn.get_vol_by_path(path)
+        can_del, delinfo = can_delete(conn, vol, path)
+
+        if can_del:
+            default, definfo = do_we_default(conn, vm.get_name(), vol,
+                                             path, ro, shared)
+
+        info = None
+        if not can_del:
+            info = delinfo
+        elif not default:
+            info = definfo
+
+        icon = gtk.STOCK_DIALOG_WARNING
+        icon_size = gtk.ICON_SIZE_LARGE_TOOLBAR
+
+        row = [default, not can_del, path, target,
+               bool(info), icon, icon_size, info]
+        model.append(row)
+
+
+def prepare_storage_list(storage_list):
+    # Checkbox, deleteable?, storage path, target (hda), icon stock,
+    # icon size, tooltip
+    model = gtk.ListStore(bool, bool, str, str, bool, str, int, str)
+    storage_list.set_model(model)
+    try:
+        storage_list.set_tooltip_column(STORAGE_ROW_TOOLTIP)
+    except:
+        # FIXME: use tooltip wrapper for this
+        pass
+
+    confirmCol = gtk.TreeViewColumn()
+    pathCol = gtk.TreeViewColumn(_("Storage Path"))
+    targetCol = gtk.TreeViewColumn(_("Target"))
+    infoCol = gtk.TreeViewColumn()
+
+    storage_list.append_column(confirmCol)
+    storage_list.append_column(pathCol)
+    storage_list.append_column(targetCol)
+    storage_list.append_column(infoCol)
+
+    chkbox = gtk.CellRendererToggle()
+    chkbox.connect('toggled', storage_item_toggled, storage_list)
+    confirmCol.pack_start(chkbox, False)
+    confirmCol.add_attribute(chkbox, 'active', STORAGE_ROW_CONFIRM)
+    confirmCol.add_attribute(chkbox, 'inconsistent',
+                             STORAGE_ROW_CANT_DELETE)
+    confirmCol.set_sort_column_id(STORAGE_ROW_CANT_DELETE)
+
+    path_txt = gtk.CellRendererText()
+    pathCol.pack_start(path_txt, True)
+    pathCol.add_attribute(path_txt, 'text', STORAGE_ROW_PATH)
+    pathCol.set_sort_column_id(STORAGE_ROW_PATH)
+
+    target_txt = gtk.CellRendererText()
+    targetCol.pack_start(target_txt, False)
+    targetCol.add_attribute(target_txt, 'text', STORAGE_ROW_TARGET)
+    targetCol.set_sort_column_id(STORAGE_ROW_TARGET)
+
+    info_img = gtk.CellRendererPixbuf()
+    infoCol.pack_start(info_img, False)
+    infoCol.add_attribute(info_img, 'visible', STORAGE_ROW_ICON_SHOW)
+    infoCol.add_attribute(info_img, 'stock-id', STORAGE_ROW_ICON)
+    infoCol.add_attribute(info_img, 'stock-size', STORAGE_ROW_ICON_SIZE)
+    infoCol.set_sort_column_id(STORAGE_ROW_ICON)
+
+def storage_item_toggled(src, index, storage_list):
+    active = src.get_active()
+
+    model = storage_list.get_model()
+    model[index][STORAGE_ROW_CONFIRM] = not active
+
+def can_delete(conn, vol, path):
+    """Is the passed path even deleteable"""
+    ret = True
+    msg = None
+
+    if vol:
+        # Managed storage
+        if (vol.get_pool().get_type() ==
+            virtinst.Storage.StoragePool.TYPE_ISCSI):
+            msg = _("Cannot delete iscsi share.")
+    else:
+        if conn.is_remote():
+            msg = _("Cannot delete unmanaged remote storage.")
+        elif not os.path.exists(path):
+            msg = _("Path does not exist.")
+        elif not os.access(os.path.dirname(path), os.W_OK):
+            msg = _("No write access to parent directory.")
+        elif stat.S_ISBLK(os.stat(path)[stat.ST_MODE]):
+            msg = _("Cannot delete unmanaged block device.")
+
+    if msg:
+        ret = False
+
+    return (ret, msg)
+
+def do_we_default(conn, vm_name, vol, path, ro, shared):
+    """ Returns (do we delete by default?, info string if not)"""
+    info = ""
+
+    def append_str(str1, str2, delim="\n"):
+        if not str2:
+            return str1
+        if str1:
+            str1 += delim
+        str1 += str2
+        return str1
+
+    if ro:
+        info = append_str(info, _("Storage is read-only."))
+    elif not vol and not os.access(path, os.W_OK):
+        info = append_str(info, _("No write access to path."))
+
+    if shared:
+        info = append_str(info, _("Storage is marked as shareable."))
+
+    # Check if disk is actually in use by other VMs. For useful err
+    # reporting, we need more info from VirtualDisk is_conflict_disk
+    try:
+        d = virtinst.VirtualDisk(conn=conn.vmm, path=path,
+                                 readOnly=ro, shareable=shared)
+        names = d.is_conflict_disk(conn.vmm, return_names=True)
+
+        if len(names) > 1:
+            namestr = ""
+            names.remove(vm_name)
+            for name in names:
+                namestr = append_str(namestr, name, delim=", ")
+            info = append_str(info, _("Storage is in use by the following "
+                                      "virtual machines: %s" % namestr))
+    except Exception, e:
+        logging.exception("Failed checking disk conflict: %s" % str(e))
+
+    return (not info, info)
+
+gobject.type_register(vmmDeleteDialog)
diff -r 311a239d48af -r bc56a6f4f2d0 src/virtManager/manager.py
--- a/src/virtManager/manager.py	Thu Mar 05 10:44:26 2009 -0500
+++ b/src/virtManager/manager.py	Thu Mar 05 11:23:54 2009 -0500
@@ -22,7 +22,6 @@
 import gtk
 import gtk.glade
 import logging
-import traceback
 
 import sparkline
 import libvirt
@@ -30,6 +29,7 @@
 from virtManager.connection import vmmConnection
 from virtManager.asyncjob import vmmAsyncJob
 from virtManager.error import vmmErrorDialog
+from virtManager.delete import vmmDeleteDialog
 from virtManager import util as util
 
 VMLIST_SORT_ID = 1
@@ -118,6 +118,9 @@
                                   _("An unexpected error occurred"))
         self.config = config
         self.engine = engine
+
+        self.delete_dialog = None
+
         self.prepare_vmlist()
 
         self.config.on_vmlist_domain_id_visible_changed(self.toggle_domain_id_visible_widget)
@@ -806,32 +809,31 @@
         conn = self.current_connection()
         vm = self.current_vm()
         if vm is None:
-            # Delete the connection handle
-            if conn is None:
-                return
+            self._do_delete_connection(conn)
+        else:
+            self._do_delete_vm(vm)
 
-            result = self.err.yes_no(_("Are you sure you want to permanently delete the connection %s?") % self.rows[conn.get_uri()][ROW_NAME])
-            if not result:
-                return
-            self.engine.remove_connection(conn.get_uri())
+    def _do_delete_connection(self, conn):
+        if conn is None:
+            return
+
+        result = self.err.yes_no(_("This will remove the connection \"%s\","
+                                   "are you sure?") %
+                                   self.rows[conn.get_uri()][ROW_NAME])
+        if not result:
+            return
+        self.engine.remove_connection(conn.get_uri())
+
+    def _do_delete_vm(self, vm):
+        if vm.is_active():
+            return
+
+        if not self.delete_dialog:
+            self.delete_dialog = vmmDeleteDialog(self.config, vm)
         else:
-            # Delete the VM itself
+            self.delete_dialog.set_vm(vm)
 
-            if vm.is_active():
-                return
-
-            # are you sure you want to delete this VM?
-            result = self.err.yes_no(_("Are you sure you want to permanently delete the virtual machine %s?") % vm.get_name())
-            if not result:
-                return
-            conn = vm.get_connection()
-            try:
-                vm.delete()
-            except Exception, e:
-                self.err.show_err(_("Error deleting domain: %s" % str(e)),\
-                                  "".join(traceback.format_exc()))
-                return
-            conn.tick(noStatsUpdate=True)
+        self.delete_dialog.show()
 
     def show_about(self, src):
         self.emit("action-show-about")
diff -r 311a239d48af -r bc56a6f4f2d0 src/virtManager/storagevol.py
--- a/src/virtManager/storagevol.py	Thu Mar 05 10:44:26 2009 -0500
+++ b/src/virtManager/storagevol.py	Thu Mar 05 11:23:54 2009 -0500
@@ -42,6 +42,10 @@
     def get_path(self):
         return self.vol.path()
 
+    def get_pool(self):
+        pobj = self.vol.storagePoolLookupByVolume()
+        return self.connection.get_pool_by_name(pobj.name())
+
     def delete(self):
         self.vol.delete(0)
         del(self.vol)
diff -r 311a239d48af -r bc56a6f4f2d0 src/vmm-delete.glade
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/vmm-delete.glade	Thu Mar 05 11:23:54 2009 -0500
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--Generated with glade3 3.4.5 on Wed Feb 25 17:45:14 2009 -->
+<glade-interface>
+  <widget class="GtkDialog" id="vmm-delete">
+    <property name="width_request">500</property>
+    <property name="height_request">300</property>
+    <property name="border_width">5</property>
+    <property name="title" translatable="yes">Delete Confirmation</property>
+    <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
+    <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+    <property name="has_separator">False</property>
+    <signal name="delete_event" handler="on_vmm_delete_delete_event"/>
+    <child internal-child="vbox">
+      <widget class="GtkVBox" id="dialog-vbox1">
+        <property name="visible">True</property>
+        <property name="spacing">2</property>
+        <child>
+          <widget class="GtkVBox" id="vbox1">
+            <property name="visible">True</property>
+            <property name="border_width">6</property>
+            <property name="spacing">6</property>
+            <child>
+              <widget class="GtkVBox" id="vbox3">
+                <property name="visible">True</property>
+                <property name="spacing">3</property>
+                <child>
+                  <widget class="GtkVBox" id="vbox2">
+                    <property name="visible">True</property>
+                    <property name="spacing">10</property>
+                    <child>
+                      <widget class="GtkHBox" id="hbox6">
+                        <property name="visible">True</property>
+                        <property name="spacing">3</property>
+                        <child>
+                          <widget class="GtkImage" id="image1">
+                            <property name="visible">True</property>
+                            <property name="xalign">0.059999998658895493</property>
+                            <property name="yalign">0</property>
+                            <property name="stock">gtk-delete</property>
+                          </widget>
+                          <packing>
+                            <property name="expand">False</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <widget class="GtkLabel" id="delete-main-label">
+                            <property name="visible">True</property>
+                            <property name="xalign">0</property>
+                            <property name="yalign">0</property>
+                            <property name="label">&lt;span size='x-large'&gt;Delete 'foo'&lt;/span&gt;</property>
+                            <property name="use_markup">True</property>
+                          </widget>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">False</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </widget>
+                      <packing>
+                        <property name="expand">False</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <widget class="GtkCheckButton" id="delete-remove-storage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="label" translatable="yes">Delete associated storage files</property>
+                        <property name="response_id">0</property>
+                        <property name="draw_indicator">True</property>
+                        <signal name="toggled" handler="on_delete_remove_storage_toggled"/>
+                      </widget>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </widget>
+                  <packing>
+                    <property name="expand">False</property>
+                  </packing>
+                </child>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkScrolledWindow" id="scrolledwindow1">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+                <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+                <property name="shadow_type">GTK_SHADOW_ETCHED_IN</property>
+                <child>
+                  <widget class="GtkTreeView" id="delete-storage-list">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="headers_clickable">True</property>
+                  </widget>
+                </child>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <widget class="GtkHButtonBox" id="dialog-action_area1">
+            <property name="visible">True</property>
+            <property name="layout_style">GTK_BUTTONBOX_END</property>
+            <child>
+              <widget class="GtkButton" id="delete-ok">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="label" translatable="yes">gtk-delete</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">0</property>
+                <signal name="clicked" handler="on_delete_ok_clicked"/>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkButton" id="delete-cancel">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="label" translatable="yes">gtk-cancel</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">0</property>
+                <signal name="clicked" handler="on_delete_cancel_clicked"/>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">GTK_PACK_END</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+</glade-interface>

[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]