#!/usr/bin/python
# vim:expandtab:autoindent:tabstop=4:shiftwidth=4:filetype=python:tw=0
"""
    this is the documentation...
"""

# import arranged alphabetically
import commands
import cStringIO
import getopt
from gettext import gettext as _
import locale
import os
import pygtk
import sys
import threading
import traceback

pygtk.require('2.0')
import gtk, gtk.glade, pango
import gobject
import gnome.ui

# these are replaced by autotools when installed.
__VERSION__="unreleased_version"
PYTHONDIR=os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),".."))
PKGGLADEDIR=os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),"..","glade"))
PKGDATADIR=os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),"..","ft-cli"))
# end build system subs

# import all local modules after this. This allows us to run from build tree
sys.path.insert(0,PYTHONDIR)
sys.path.insert(0,PKGDATADIR)

import firmwaretools.trace_decorator as trace_decorator
import guihelpers
import firmwaretools.repository as repository
import firmwaretools.package

PROGRAM_NAME="Firmware Inventory and Update GUI"

class CommandAlreadyExecuted(Exception): pass
class CommandAlreadyUndone(Exception): pass
class Command(object):
    def __init__(self, object, method, args, kargs):
        self.object = object
        self.method = method
        self.args = args
        self.kargs = kargs
        self.memento = None
        self.executed = False
        self.undone = False

    def execute(self):
        if self.executed:
            raise CommandAlreadyExecuted()
        self.executed=True
        self.undone = False
        self.memento = self.object.getMemento()
        self.method(*self.args, **self.kargs)

    def undo(self):
        if self.undone:
            raise CommandAlreadyUndone()
        self.undone = True
        self.executed = False
        self.object.setMemento(self.memento)

# use only for pin/unpin, it is more efficent as it doesnt save full memento
class UpdateSetPinCommand(Command):
    def execute(self):
        if self.executed:
            raise CommandAlreadyExecuted()
        self.executed=True
        self.undone = False
        self.memento = self.object.getMemento(deviceHint = self.args[0])
        self.method(*self.args, **self.kargs)

class InventoryFirmware:
    GLADE_FILE = '/inventory_firmware_gui.glade'
    def __init__(self, base):
        self.wTree = gtk.glade.XML(PKGGLADEDIR + self.GLADE_FILE)
        self.wTree.signal_autoconnect(self)
        self.main_window = self.wTree.get_widget("MainWindow")
        self.wTree.get_widget("about_dialog").destroy()

        # set up toggles
        self.showUnknown=0
        self.toolbarAllowDowngrade = self.wTree.get_widget("toolbar_allow_downgrade")
        self.toolbarAllowReflash = self.wTree.get_widget("toolbar_allow_reflash")
        self.toolbarShowUnknown = self.wTree.get_widget("toolbar_show_unknown")
        self.menuAllowDowngrade = self.wTree.get_widget("menu_allow_downgrade")
        self.menuAllowReflash = self.wTree.get_widget("menu_allow_reflash")
        self.menuShowUnknown = self.wTree.get_widget("menu_show_unknown")
        self.recursiveCallback=0

        # internal accounting
        self.numDowngradeSelected = 0
        self.numReflashSelected = 0

        # set up command stack, used for undo/redo
        self.undoStack = []
        self.redoStack = []

        # setup tree views
        self._setupInventoryTreeView()
        self._setupBootstrapTreeView()
        self._setupUpdateStatusTreeView()

        # get handle to status bar
        self.statusBar = self.wTree.get_widget("main_window_status_bar")
        ctx = self.statusBar.get_context_id("main")
        self.statusBar.push(ctx, "Ready")

        # disable input in main window until we are finished initializing...
        self.main_window.set_sensitive(0)

        # show main window
        self.main_window.show()

        # set status == collecting inventory
        ctx = self.statusBar.get_context_id("inventory")
        self.statusBar.push(ctx, _("Performing system inventory..."))
        guihelpers.gtkYield() # make sure current GUI is fully displayed

        # special function to make sure GUI updates smoothly while we
        # generate the update set
        def myYield(*args, **kargs):
            # eats all its args...
            guihelpers.gtkYield()

        # the following two lines are equivalent, but runLongProcess() does the
        # action in a background thread so the GUI will update while it works.
        #self.updateSet = base.calculateUpgradeList(cb=(myYield, None))
        self.updateSet = guihelpers.runLongProcessGtk(
            base.calculateUpgradeList, 
            args=(), 
            kargs={'cb':(myYield, None)})

        self._populateInventoryTree()
        self._populateBootstrapTree(base)
        self.inventoryTreeView.expand_all()
        self._refresh()

        # set status == ready
        self.statusBar.pop(ctx)
        self.main_window.set_sensitive(1)

    def _setupBootstrapTreeView(self):
        # create model for bootstrap treeview
        self.bootstrapTreeView = self.wTree.get_widget("bootstrap_treeview")
        self.bootstrapTreeModel= gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
        self.bootstrapTreeView.set_model(self.bootstrapTreeModel)
        self.BOOTSTRAP_COLUMN_BOOTSTRAP_NAME = 0
        self.BOOTSTRAP_COLUMN_DEVICE_NAME = 1
        self.BOOTSTRAP_COLUMN_FW_VER = 2

        # add column headers to the inventory treeview
        self.bootstrapTreeView.set_headers_visible(True)

        # add column: Display name for devices, version select for updates
        renderer=gtk.CellRendererText()
        column=gtk.TreeViewColumn(_("Bootstrap Name"),renderer, text=self.BOOTSTRAP_COLUMN_BOOTSTRAP_NAME)
        column.set_resizable(True)
        self.bootstrapTreeView.append_column(column)

        # add column: Display name for devices, version select for updates
        renderer=gtk.CellRendererText()
        column=gtk.TreeViewColumn(_("Device Name"),renderer, text=self.BOOTSTRAP_COLUMN_DEVICE_NAME)
        column.set_resizable(True)
        self.bootstrapTreeView.append_column(column)

        # add column: Display name for devices, version select for updates
        renderer=gtk.CellRendererText()
        column=gtk.TreeViewColumn(_("Firmware Version"),renderer, text=self.BOOTSTRAP_COLUMN_FW_VER)
        column.set_resizable(True)
        self.bootstrapTreeView.append_column(column)

        # let us select multiple releases
        self.bootstrapTreeView.get_selection().set_mode(gtk.SELECTION_MULTIPLE)

    def _setupUpdateStatusTreeView(self):
        # create model for update status treeview
        self.updateStatusTreeView = self.wTree.get_widget("status_treeview")
        self.updateStatusTreeModel= gtk.TreeStore( object, object, int )
        self.updateStatusTreeView.set_model(self.updateStatusTreeModel)
        self.STATUS_COLUMN_DEVICE = 0
        self.STATUS_COLUMN_PACKAGE = 1
        self.STATUS_COLUMN_SERIAL = 2

        # add column headers to the inventory treeview
        self.updateStatusTreeView.set_headers_visible(True)

        # status, component, status description, log?

        # add column: Display name for devices, version select for updates
        renderer=gtk.CellRendererText()
        column=gtk.TreeViewColumn(_("Status"),renderer)
        column.set_resizable(True)
        column.set_cell_data_func(renderer, self.cell_data_func_us_status)
        self.updateStatusTreeView.append_column(column)

        # add column: Display name for devices, version select for updates
        renderer=gtk.CellRendererText()
        column=gtk.TreeViewColumn(_("Component"),renderer)
        column.set_resizable(True)
        column.set_cell_data_func(renderer, self.cell_data_func_us_component)
        self.updateStatusTreeView.append_column(column)

        # add column: Display name for devices, version select for updates
        renderer=gtk.CellRendererText()
        column=gtk.TreeViewColumn(_("Current Version"),renderer)
        column.set_resizable(True)
        column.set_cell_data_func(renderer, self.cell_data_func_us_cur_version)
        self.updateStatusTreeView.append_column(column)

        # add column: Display name for devices, version select for updates
        renderer=gtk.CellRendererText()
        column=gtk.TreeViewColumn(_("Update Version"),renderer)
        column.set_resizable(True)
        column.set_cell_data_func(renderer, self.cell_data_func_us_update_version)
        self.updateStatusTreeView.append_column(column)

        # add column: Display name for devices, version select for updates
        renderer=gtk.CellRendererText()
        column=gtk.TreeViewColumn(_("Status Description"),renderer)
        column.set_resizable(True)
        column.set_cell_data_func(renderer, self.cell_data_func_us_status_description)
        self.updateStatusTreeView.append_column(column)

        # let us select multiple releases
        self.updateStatusTreeView.get_selection().set_mode(gtk.SELECTION_MULTIPLE)

    def _setupInventoryTreeView(self):
        # create model for inventory treeview
        self.inventoryTreeView = self.wTree.get_widget("inventory_treeview")
        self.inventoryTreeModel= gtk.TreeStore(
                object,                # device or package
                gobject.TYPE_BOOLEAN,  # for device == enable update for device (checkbox),
                                       # for package == update to this package
                gobject.TYPE_INT,      # flags
                gobject.TYPE_INT,      # update serial
                )
        self.inventoryTreeView.set_model(self.inventoryTreeModel)
        self.INVENTORY_COLUMN_DEVICE = 0
        self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE = 1
        self.INVENTORY_COLUMN_FLAGS = 2
        self.INVENTORY_COLUMN_SERIAL = 3
        self.FLAG_REFLASH = 1
        self.FLAG_DOWNGRADE = 2

        # add column headers to the inventory treeview
        self.inventoryTreeView.set_headers_visible(True)

        # select, status, criticality, package name, component, type (bios/firmware/driver), current ver, repo ver

        # add column: Flash yes/no checkbox column
        renderer=gtk.CellRendererToggle()
        renderer.set_property("radio", False)
        renderer.set_property('activatable', True)
        renderer.connect('toggled', self.toggle_device_cb, self.inventoryTreeModel)
        column=gtk.TreeViewColumn(_("Flash"),renderer)
        column.add_attribute(renderer, "active", self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE)
        column.set_cell_data_func(renderer, self.cell_data_func_iv_toggle)
        self.inventoryTreeView.append_column(column)

        # add column: Display name for devices, version select for updates
        renderer=gtk.CellRendererToggle()
        renderer.set_property("radio", True)
        renderer.set_property('activatable', True)
        renderer.connect('toggled', self.toggle_update_cb, self.inventoryTreeModel)
        column=gtk.TreeViewColumn(_("Device Name"),renderer)
        column.add_attribute(renderer, "active", self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE)
        column.set_resizable(True)
        renderer=gtk.CellRendererText()
        column.pack_start(renderer)
        column.set_cell_data_func(renderer, self.cell_data_func_iv_display_name)
        self.inventoryTreeView.append_column(column)
        self.inventoryTreeView.set_expander_column(column)

        # add column: Firmware version
        renderer = gtk.CellRendererText()
        column=gtk.TreeViewColumn(_("Current Version"),renderer)
        column.set_cell_data_func(renderer, self.cell_data_func_iv_version)
        column.set_resizable(True)
        self.inventoryTreeView.append_column(column)

        # let us select multiple releases
        self.inventoryTreeView.get_selection().set_mode(gtk.SELECTION_MULTIPLE)


    # this is a helper function to initially populate the tree model.
    # should only ever be called once.
    def _populateInventoryTree(self):
        self.inventoryTreeModel.clear()
        for device in self.updateSet.iterDevices():
            guihelpers.gtkYield()
            if device.version == "unknown" and not self.showUnknown:
                continue
            flags = 0
            toggle=False
            if self.updateSet.getUpdatePackageForDevice(device) is not None:
                toggle=True
            iter = self.inventoryTreeModel.append(None, [device, toggle, flags, 0])
            for availPkg in self.updateSet.iterAvailableUpdates(device):
                guihelpers.gtkYield()
                flags = 0
                if device.compareVersion(availPkg) == 0:
                    flags = flags | self.FLAG_REFLASH
                if device.compareVersion(availPkg) > 0:
                    flags = flags | self.FLAG_DOWNGRADE
                toggle=False
                if self.updateSet.getUpdatePackageForDevice(device) == availPkg:
                    toggle=True
                self.inventoryTreeModel.append(iter, [availPkg, toggle, flags, 0])

    # this is a helper function to initially populate the tree model.
    # should only ever be called once.
    def _populateBootstrapTree(self, base):
        self.bootstrapTreeModel.clear()
        venId, sysId = base.getSystemId()
        for dev in base.yieldInventory():
            guihelpers.gtkYield()
            self.bootstrapTreeModel.append(None, [dev.name, dev.displayname, dev.version])
            if venId and sysId:
                self.bootstrapTreeModel.append(None, ["%s/system(ven_0x%04x_dev_0x%04x)" % (dev.name, venId, sysId), dev.displayname, dev.version])

    def _populateUpdateStatusTree(self):
        self.updateStatusTreeModel.clear()
        for device, package in self.updateSet.generateInstallationOrder(returnDeviceToo=1):
            self.updateStatusTreeModel.append(None, [device, package, 0])

    # refresh the display when something happens behind the scenes. Should be rarely used.
    def _refresh(self):
        self._refreshUpdateEnableToggles()
        self._refreshAllowToggles()
        self._refreshEnableUndoRedo()

    # only refreshes the toggles and radio buttons to reflect current package set.
    def _refreshUpdateEnableToggles(self):
        for i in range(self.inventoryTreeModel.iter_n_children(None)):
            device_path = self.inventoryTreeModel.get_path(self.inventoryTreeModel.iter_nth_child(None, i))
            device = self.inventoryTreeModel[device_path][self.INVENTORY_COLUMN_DEVICE]
            if self.updateSet.getUpdatePackageForDevice(device) is not None:
                self.inventoryTreeModel[device_path][self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE]=True
            else:
                self.inventoryTreeModel[device_path][self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE]=False
            self._fixupChildren(self.inventoryTreeModel, self.inventoryTreeModel.get_iter(device_path))

    # refreshes allow reflash/downgrade toggles
    def _refreshAllowToggles(self):
        self.recursiveCallback=1
        self.menuAllowDowngrade.set_property("active", self.updateSet.allowDowngrade)
        self.menuAllowReflash.set_property("active", self.updateSet.allowReflash)
        self.menuShowUnknown.set_property("active", self.showUnknown)
        self.toolbarAllowDowngrade.set_active(self.updateSet.allowDowngrade)
        self.toolbarAllowReflash.set_active(self.updateSet.allowReflash)
        self.toolbarShowUnknown.set_active(self.showUnknown)

        if self.numDowngradeSelected:
            self.menuAllowDowngrade.set_sensitive(0)
            self.toolbarAllowDowngrade.set_sensitive(0)
        else:
            self.menuAllowDowngrade.set_sensitive(1)
            self.toolbarAllowDowngrade.set_sensitive(1)

        if self.numReflashSelected:
            self.menuAllowReflash.set_sensitive(0)
            self.toolbarAllowReflash.set_sensitive(0)
        else:
            self.menuAllowReflash.set_sensitive(1)
            self.toolbarAllowReflash.set_sensitive(1)

        self.recursiveCallback=0

    # enables/disables the undo/redo/reset buttons so only valid actions are enabled
    def _refreshEnableUndoRedo(self):
        if self.undoStack:
            self.wTree.get_widget("reset_button").set_sensitive(1)
            self.wTree.get_widget("reset_menu").set_sensitive(1)
            self.wTree.get_widget("undo_button").set_sensitive(1)
            self.wTree.get_widget("undo_menu").set_sensitive(1)
        else:
            self.wTree.get_widget("reset_button").set_sensitive(0)
            self.wTree.get_widget("reset_menu").set_sensitive(0)
            self.wTree.get_widget("undo_button").set_sensitive(0)
            self.wTree.get_widget("undo_menu").set_sensitive(0)

        if self.redoStack:
            self.wTree.get_widget("redo_button").set_sensitive(1)
            self.wTree.get_widget("redo_menu").set_sensitive(1)
        else:
            self.wTree.get_widget("redo_button").set_sensitive(0)
            self.wTree.get_widget("redo_menu").set_sensitive(0)

    def cell_data_func_iv_display_name(self, column, cell, model, iter):
        pyobj = model.get_value(iter,self.INVENTORY_COLUMN_DEVICE)
        renderers = column.get_cell_renderers()
        text = str(pyobj)
        if isinstance(pyobj, firmwaretools.package.Device):
            renderers[0].set_property("visible", False)
            renderers[1].set_property("text", text)
        else:
            flags = model.get_value(iter,self.INVENTORY_COLUMN_FLAGS)
            renderers[0].set_property("visible", True)
            parentIter = model.iter_parent(iter)
            device = model.get_value(parentIter,self.INVENTORY_COLUMN_DEVICE)
            text = str(pyobj.version)

            renderers[1].set_property("text", _("Available Version: %s") % text)
            renderers[0].set_property('activatable', True)

            if not model.get_value(parentIter, self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE):
                renderers[0].set_property('activatable', False)

            if self.updateSet.getSuggestedUpdatePackageForDevice(device) == pyobj:
                # TODO: picture?
                renderers[1].set_property("text", _("Available Version: %s  (suggested)") % text)

            if flags & self.FLAG_REFLASH:
                # TODO: picture?
                renderers[1].set_property("text", _("Available Version: %s  (reflash)") % text)
                if not self.updateSet.allowReflash:
                    renderers[0].set_property('activatable', False)
                    renderers[1].set_property("text", _("Available Version: %s  (reflash disabled per policy)") % text)
                if not pyobj.getCapability("can_reflash"):
                    renderers[0].set_property('activatable', False)
                    renderers[1].set_property("text", _("Available Version: %s  (reflash disabled due to package limitations)") % text)

            if flags & self.FLAG_DOWNGRADE:
                # TODO: picture?
                renderers[1].set_property("text", _("Available Version: %s  (downgrade)") % text)
                if not self.updateSet.allowDowngrade:
                    renderers[0].set_property('activatable', False)
                    renderers[1].set_property("text", _("Available Version: %s  (downgrade disabled per policy)") % text)
                if not pyobj.getCapability("can_downgrade"):
                    renderers[0].set_property('activatable', False)
                    renderers[1].set_property("text", _("Available Version: %s  (downgrade disabled due to package limitations)") % text)


    def cell_data_func_iv_version(self, column, cell, model, iter):
        pyobj = model.get_value(iter,self.INVENTORY_COLUMN_DEVICE)
        if isinstance(pyobj, firmwaretools.package.Device):
            cell.set_property("visible", True)
            cell.set_property("text", pyobj.version)
        else:
            cell.set_property("visible", False)

    def cell_data_func_iv_toggle(self, column, cell, model, iter):
        pyobj = model.get_value(iter,self.INVENTORY_COLUMN_DEVICE)
        if isinstance(pyobj, firmwaretools.package.Device):
            cell.set_property("visible", True)
        else:
            cell.set_property("visible", False)

    def toggle_device_cb(self, renderer, path, model, *args, **kargs):
        model[path][self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE] = not model[path][self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE]
        device = model[path][self.INVENTORY_COLUMN_DEVICE]
        if model[path][self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE]:
            # unpin it and calculated pkg takes over
            self._executeCommand(UpdateSetPinCommand(self.updateSet, self.updateSet.unPinDevice, (device,), {}))
        else:
            # pin it to None to disable update for this device
            self._executeCommand(UpdateSetPinCommand(self.updateSet, self.updateSet.pinUpdatePackage, (device, None), {}))

        self._fixupChildren(model, model.get_iter(path))
        self._refreshAllowToggles()

    def toggle_update_cb(self, renderer, path, model, *args, **kargs):
        # dont re-activate if it is already the active update
        if not model[path][self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE]:
            iter = model.get_iter(path)
            parentIter = model.iter_parent(iter)
            device = model.get_value(parentIter, self.INVENTORY_COLUMN_DEVICE)
            update = model[path][self.INVENTORY_COLUMN_DEVICE]

            self._executeCommand(UpdateSetPinCommand(self.updateSet, self.updateSet.pinUpdatePackage, (device, update), {}))
            self._fixupChildren(model, parentIter)
            self._refreshAllowToggles()

    # this method sets the enable toggle on packages appropriately
    # it also interlocks the allow reflash/downgrade toggles
    def _fixupChildren(self, model, iter):
        for i in range(model.iter_n_children(iter)):
            child_path = model.get_path(model.iter_nth_child(iter, i))
            curValue = model[child_path][self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE]
            if model[child_path][self.INVENTORY_COLUMN_DEVICE] == self.updateSet.getUpdatePackageForDevice(model.get_value(iter,self.INVENTORY_COLUMN_DEVICE)):
                if curValue == False and model[child_path][self.INVENTORY_COLUMN_FLAGS] & self.FLAG_DOWNGRADE:
                    self.numDowngradeSelected = self.numDowngradeSelected + 1
                if curValue == False and model[child_path][self.INVENTORY_COLUMN_FLAGS] & self.FLAG_REFLASH:
                    self.numReflashSelected = self.numReflashSelected + 1
                model[child_path][self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE] = True
            else:
                if curValue == True and model[child_path][self.INVENTORY_COLUMN_FLAGS] & self.FLAG_DOWNGRADE:
                    self.numDowngradeSelected = self.numDowngradeSelected - 1
                if curValue == True and model[child_path][self.INVENTORY_COLUMN_FLAGS] & self.FLAG_REFLASH:
                    self.numReflashSelected = self.numReflashSelected - 1
                model[child_path][self.INVENTORY_COLUMN_DEVICE_ENABLE_UPDATE] = False

            # force update serial so rows get redisplayed
            self.inventoryTreeModel[child_path][self.INVENTORY_COLUMN_SERIAL]= (
                self.inventoryTreeModel[child_path][self.INVENTORY_COLUMN_SERIAL] + 1)

    def on_allow_downgrade_toggled(self, widget, *args, **kargs):
        # guard against executeCommand being called while we are in a refresh
        if not self.recursiveCallback:
            if self.numDowngradeSelected > 0:
                active = 1
            else:
                active = widget.get_active()
            self._executeCommand(Command(self.updateSet, self.updateSet.setAllowDowngrade, (active,), {}))
            self._refreshAllowToggles()
            # force update serial so rows get redisplayed
            for i in range(self.inventoryTreeModel.iter_n_children(None)):
                device_path = self.inventoryTreeModel.get_path(self.inventoryTreeModel.iter_nth_child(None, i))
                self._fixupChildren(self.inventoryTreeModel, self.inventoryTreeModel.get_iter(device_path))

    def on_show_unknown_toggled(self, widget, *args, **kargs):
        if not self.recursiveCallback:
            self.showUnknown = widget.get_active()
            self._refreshAllowToggles()
            self._populateInventoryTree()

    def on_allow_reflash_toggled(self, widget, *args, **kargs):
        # guard against executeCommand being called while we are in a refresh
        if not self.recursiveCallback:
            if self.numReflashSelected > 0:
                active = 1
            else:
                active = widget.get_active()
            self._executeCommand(Command(self.updateSet, self.updateSet.setAllowReflash, (active,), {}))
            self._refreshAllowToggles()
            # force update serial so rows get redisplayed
            for i in range(self.inventoryTreeModel.iter_n_children(None)):
                device_path = self.inventoryTreeModel.get_path(self.inventoryTreeModel.iter_nth_child(None, i))
                self._fixupChildren(self.inventoryTreeModel, self.inventoryTreeModel.get_iter(device_path))

    def on_help_about(self, *args):
        wTree = gtk.glade.XML(PKGGLADEDIR + self.GLADE_FILE, "about_dialog")
        wTree.get_widget("about_dialog").set_property('name',PROGRAM_NAME)
        wTree.get_widget("about_dialog").set_property('version',__VERSION__)
        wTree.get_widget("about_dialog").run() # modal until 'close'
        wTree.get_widget("about_dialog").destroy()

    def _executeCommand(self, command):
        command.execute()
        self.undoStack.append(command)
        self.redoStack = []
        self._refreshEnableUndoRedo()
        if len(self.undoStack) > 20:
            self.undoStack = self.undoStack[-20:]

    def on_undo_activate(self, *args, **kargs):
        if self.undoStack:
            command = self.undoStack.pop()
            command.undo()
            self.redoStack.append(command)

        self._refresh()

    def on_redo_activate(self, *args, **kargs):
        if self.redoStack:
            command = self.redoStack.pop()
            command.execute()
            self.undoStack.append(command)

        self._refresh()

    def on_reset_activate(self, *args, **kargs):
        self.updateSet.reset()
        self.undoStack = []
        self.redoStack = []
        self.updateSet.setAllowReflash(0)
        self.updateSet.setAllowDowngrade(0)
        self._refresh()

    def on_system_inventory_menu_activate(self, *args, **kargs):
        notebook = self.wTree.get_widget("notebook")
        widget = self.wTree.get_widget("inventory_vbox")
        page = notebook.page_num(widget)
        notebook.set_current_page(page)

    def on_bootstrap_inventory_menu_activate(self, *args, **kargs):
        notebook = self.wTree.get_widget("notebook")
        widget = self.wTree.get_widget("bootstrap_scrolledwindow")
        page = notebook.page_num(widget)
        notebook.set_current_page(page)

    def on_file_quit(self, *args):
        self.on_quit_app( allowCancel = 1 )

    def on_quit_app(self, *args, **kargs):
        # check kargs['allowCancel']
        gtk.main_quit()

    def on_update_now_activate(self, *args, **kargs):
        detailsStr = cStringIO.StringIO()
        detailsStr.write( _("Going to update the following devices:\n\n") )
        for device in self.updateSet.iterDevices():
            pkg = self.updateSet.getUpdatePackageForDevice(device)
            if pkg is not None:
                detailsStr.write("\t%s\n" % str(device))
                detailsStr.write(_("\t\tFrom Version: %s\n") % device.version)
                detailsStr.write(_("\t\tTo Version  : %s\n") % pkg.version)
                detailsStr.write("\n")

        dialog = gtk.MessageDialog(parent=None,
                   flags=0,
                   type=gtk.MESSAGE_WARNING,
                   buttons=gtk.BUTTONS_NONE)
        dialog.set_title(_("Update Firmware"))
        dialog.set_markup(_( "<big>Your system will now be updated.</big>\n\nYou will not be able to come back to this page after you continue.\nPress the 'Show Details' button to see which devices are set to be updated."))

        showButton = dialog.add_button(_("Show Details..."), 1)
        dialog.add_button(_("Continue to update page..."), 2)
        dialog.add_button(_("Cancel Update"), 3)

        # Details
        textview = gtk.TextView()
        textview.set_editable(False)
        textview.modify_font(pango.FontDescription("Monospace"))
        sw = gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        sw.add(textview)
        frame = gtk.Frame()
        frame.set_shadow_type(gtk.SHADOW_IN)
        frame.add(sw)
        frame.set_border_width(6)
        dialog.vbox.add(frame)
        textbuffer = textview.get_buffer()
        textbuffer.set_text(detailsStr.getvalue())
        textview.set_size_request(gtk.gdk.screen_width()/2, gtk.gdk.screen_height()/3)

        dialog.details = frame
        dialog.set_position(gtk.WIN_POS_CENTER)
        dialog.set_gravity(gtk.gdk.GRAVITY_CENTER)

        show = False
        while 1:
            resp = dialog.run()
            if resp == 1:
                show = not show
                if show:
                    dialog.details.show_all()
                    showButton.set_label(_("Hide Details"))
                else:
                    dialog.details.hide_all()
                    showButton.set_label(_("Show Details..."))
            elif resp == 2:
                dialog.destroy()
                self._gotoUpdatePage()
                break
            else:
                dialog.destroy()
                break

    def _gotoUpdatePage(self):
        self._populateUpdateStatusTree()
        notebook = self.wTree.get_widget("notebook")
        widget = self.wTree.get_widget("update_status_scrolledwindow")
        page = notebook.page_num(widget)
        notebook.set_current_page(page)
        # disable view menu
        view_menu = self.wTree.get_widget("view_menu")
        view_menu.set_sensitive(0)
        self.menuAllowDowngrade.set_sensitive(0)
        self.menuAllowReflash.set_sensitive(0)
        self.wTree.get_widget("update_now_menu").set_sensitive(0)
        self.redoStack = []
        self.undoStack = []
        self._refreshEnableUndoRedo()
        ctx = self.statusBar.get_context_id("update page")
        self.statusBar.push(ctx, "Click 'Update Now' to begin update...")

    def cell_data_func_us_status(self, column, cell, model, iter):
        pkg = model.get_value(iter,self.STATUS_COLUMN_PACKAGE)
        if pkg.getCapability('accurate_update_percentage'):
            cell.set_property("text", "%s%%" % (pkg.getProgress()*100))
        else:
            if pkg.status == "installing":
                cell.set_property("text", firmwaretools.pycompat.spinner())
            else:
                cell.set_property("text", "")

    def cell_data_func_us_status_description(self, column, cell, model, iter):
        pkg = model.get_value(iter,self.STATUS_COLUMN_PACKAGE)
        cell.set_property("text", pkg.getStatusStr())

    def cell_data_func_us_component(self, column, cell, model, iter):
        device = model.get_value(iter,self.STATUS_COLUMN_DEVICE)
        cell.set_property("text", str(device))

    def cell_data_func_us_cur_version(self, column, cell, model, iter):
        device = model.get_value(iter,self.STATUS_COLUMN_DEVICE)
        cell.set_property("text", device.version)

    def cell_data_func_us_update_version(self, column, cell, model, iter):
        pkg = model.get_value(iter,self.STATUS_COLUMN_PACKAGE)
        cell.set_property("text", pkg.version)

    def on_really_update_now_button_clicked(self, *args, **kargs):
        # disable update button...
        ctx = self.statusBar.get_context_id("update page")
        self.statusBar.pop(ctx)
        self.statusBar.push(ctx, "Performing updates now...")
        self.wTree.get_widget("really_update_now_button").set_sensitive(0)

        success = 1
        for pkg in self.updateSet.generateInstallationOrder():
            stop = self.wTree.get_widget("stop_on_errors").get_active()
            try:
                ret = guihelpers.runLongProcessGtk(pkg.install, waitLoopFunction=self._refreshUpdateStatus)

            except (firmwaretools.package.NoInstaller,), e:
                print "package %s - %s does not have an installer available." % (pkg.name, pkg.version)
                print "skipping this package for now."
            except (Exception,), e:
                success = 0
                print "Installation failed for package: %s - %s" % (pkg.name, pkg.version)
                print "The error message from the low-level command was:"
                print e
                if stop:
                    print
                    print "stop on errors selected. error detected, so I'm stopping."
                    break

        self._refreshUpdateStatus()
        self.statusBar.pop(ctx)
        if success:
            self.statusBar.push(ctx, "All updates successfully completed.")
        else:
            self.statusBar.push(ctx, "Some updates failed.")

    def _refreshUpdateStatus(self):
        # update serial # to force GUI to refresh
        model = self.updateStatusTreeModel
        for i in range(model.iter_n_children(None)):
            path = model.get_path(model.iter_nth_child(None, i))
            model[path][self.STATUS_COLUMN_SERIAL] = model[path][self.STATUS_COLUMN_SERIAL] + 1

def main():
    try:
        locale.setlocale(locale.LC_ALL, '')
    except locale.Error, e:
        # default to C locale if we get a failure.
        print >> sys.stderr, 'Failed to set locale, defaulting to C'
        locale.setlocale(locale.LC_ALL, 'C')

    try:
        import cli
        import firmwaretools.plugins as plugins
        base = firmwaretools.FtBase()
        parser = cli.FtOptionParser(usage=__doc__, version=__VERSION__)
        opts = parser.firstParse(sys.argv[1:])
        configFiles = opts.configFiles
        base.loggingConfig = configFiles[0]
        opts,args = parser.parse_args(sys.argv[1:])

        pluginTypes = [plugins.TYPE_CORE, ] 
        if not opts.fake_mode:
            pluginTypes.extend([plugins.TYPE_INVENTORY,])
        else:
            pluginTypes.extend([plugins.TYPE_MOCK_CORE, plugins.TYPE_MOCK_INVENTORY])

        base.opts = opts
        base.verbosity = opts.verbosity
        base.trace = opts.trace
        base._getConfig(configFiles, pluginTypes, parser, opts.disabledPlugins)

        #gnome.init(PROGRAM_NAME, version)
        test = InventoryFirmware(base)
        gtk.main()

    except:
        traceback.print_exc()
        sys.exit(2)

    sys.exit(0)

def _info(type, value, tb):
    # exception dialog code from: Gustavo J A M Carneiro <gjc at inescporto.pt>
    # http://www.daa.com.au/pipermail/pygtk/attachments/20030828/2d304204/gtkexcepthook.py
    # license: "The license is whatever you want."
    # http://www.daa.com.au/pipermail/pygtk/2003-August/005777.html
    # Bugfixes by Michael Brown <michael_e_brown at dell.com>
    dialog = gtk.MessageDialog(parent=None,
                   flags=0,
                   type=gtk.MESSAGE_WARNING,
                   buttons=gtk.BUTTONS_NONE,
                   message_format=_(
    "<big><b>A programming error has been detected during the execution of this program.</b></big>"
    "\n\nIt probably isn't fatal, but should be reported to the developers nonetheless."))
    dialog.set_title(_("Bug Detected"))
    dialog.set_property("has-separator", False)
    dialog.vbox.get_children()[0].get_children()[1].set_property("use-markup", True)

    dialog.add_button(_("Show Details"), 1)
    dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)

    # Details
    textview = gtk.TextView()
    textview.set_editable(False)
    textview.modify_font(pango.FontDescription("Monospace"))
    sw = gtk.ScrolledWindow()
    sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
    sw.add(textview)
    frame = gtk.Frame()
    frame.set_shadow_type(gtk.SHADOW_IN)
    frame.add(sw)
    frame.set_border_width(6)
    dialog.vbox.add(frame)
    textbuffer = textview.get_buffer()
    trace = cStringIO.StringIO()
    traceback.print_exception(type, value, tb, None, trace)
    textbuffer.set_text(trace.getvalue())
    textview.set_size_request(gtk.gdk.screen_width()/2, gtk.gdk.screen_height()/3)

    dialog.details = frame
    dialog.set_position(gtk.WIN_POS_CENTER)
    dialog.set_gravity(gtk.gdk.GRAVITY_CENTER)

    while 1:
        resp = dialog.run()
        if resp == 1:
            dialog.details.show_all()
            dialog.action_area.get_children()[1].set_sensitive(0)
            continue
        else:
            dialog.destroy()
            break

if __name__ == "__main__":
    sys.excepthook = _info
    main()
