text: """
  
  Some gtk specific tools and widgets
  
     * rec2gtk          : put record array in GTK treeview - requires gtk
  
  Example usage
  
      import matplotlib.mlab as mlab
      import mpl_toolkits.gtktools as gtktools
  
      r = mlab.csv2rec('somefile.csv', checkrows=0)
  
      formatd = dict(
          weight = mlab.FormatFloat(2),
          change = mlab.FormatPercent(2),
          cost   = mlab.FormatThousands(2),
          )
  
  
      exceltools.rec2excel(r, 'test.xls', formatd=formatd)
      mlab.rec2csv(r, 'test.csv', formatd=formatd)
  
  
      import gtk
      scroll = gtktools.rec2gtk(r, formatd=formatd)
      win = gtk.Window()
      win.set_size_request(600,800)
      win.add(scroll)
      win.show_all()
      gtk.main()
  
  """
  import copy
  import gtk, gobject
  import numpy as npy
  import matplotlib.cbook as cbook
  import matplotlib.mlab as mlab
  
  
  def error_message(msg, parent=None, title=None):
      """
      create an error message dialog with string msg.  Optionally set
      the parent widget and dialog title
      """
  
      dialog = gtk.MessageDialog(
          parent         = None,
          type           = gtk.MESSAGE_ERROR,
          buttons        = gtk.BUTTONS_OK,
          message_format = msg)
      if parent is not None:
          dialog.set_transient_for(parent)
      if title is not None:
          dialog.set_title(title)
      else:
          dialog.set_title('Error!')
      dialog.show()
      dialog.run()
      dialog.destroy()
      return None
  
  def simple_message(msg, parent=None, title=None):
      """
      create a simple message dialog with string msg.  Optionally set
      the parent widget and dialog title
      """
      dialog = gtk.MessageDialog(
          parent         = None,
          type           = gtk.MESSAGE_INFO,
          buttons        = gtk.BUTTONS_OK,
          message_format = msg)
      if parent is not None:
          dialog.set_transient_for(parent)
      if title is not None:
          dialog.set_title(title)
      dialog.show()
      dialog.run()
      dialog.destroy()
      return None
  
  
  def gtkformat_factory(format, colnum):
      """
      copy the format, perform any overrides, and attach an gtk style attrs
  
  
      xalign = 0.
      cell = None
  
      """
      if format is None: return None
      format = copy.copy(format)
      format.xalign = 0.
      format.cell = None
  
      def negative_red_cell(column, cell, model, thisiter):
          val = model.get_value(thisiter, colnum)
          try: val = float(val)
          except: cell.set_property('foreground', 'black')
          else:
              if val<0:
                  cell.set_property('foreground', 'red')
              else:
                  cell.set_property('foreground', 'black')
  
  
      if isinstance(format, mlab.FormatFloat) or isinstance(format, mlab.FormatInt):
          format.cell = negative_red_cell
          format.xalign = 1.
      elif isinstance(format, mlab.FormatDate):
          format.xalign = 1.
      return format
  
  
  
  class SortedStringsScrolledWindow(gtk.ScrolledWindow):
      """
      A simple treeview/liststore assuming all columns are strings.
      Supports ascending/descending sort by clicking on column header
      """
  
      def __init__(self, colheaders, formatterd=None):
          """
          xalignd if not None, is a dict mapping col header to xalignent (default 1)
  
          formatterd if not None, is a dict mapping col header to a ColumnFormatter
          """
  
  
          gtk.ScrolledWindow.__init__(self)
          self.colheaders = colheaders
          self.seq = None # not initialized with accts
          self.set_shadow_type(gtk.SHADOW_ETCHED_IN)
          self.set_policy(gtk.POLICY_AUTOMATIC,
                          gtk.POLICY_AUTOMATIC)
  
          types = [gobject.TYPE_STRING] * len(colheaders)
          model = self.model = gtk.ListStore(*types)
  
  
          treeview = gtk.TreeView(self.model)
          treeview.show()
          treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
          treeview.set_rules_hint(True)
  
  
          class Clicked:
              def __init__(self, parent, i):
                  self.parent = parent
                  self.i = i
                  self.num = 0
  
              def __call__(self, column):
                  ind = []
                  dsu = []
                  for rownum, thisiter in enumerate(self.parent.iters):
                      val = model.get_value(thisiter, self.i)
                      try: val = float(val.strip().rstrip('%'))
                      except ValueError: pass
                      if npy.isnan(val): val = npy.inf # force nan to sort uniquely
                      dsu.append((val, rownum))
                  dsu.sort()
                  if not self.num%2: dsu.reverse()
  
                  vals, otherind = zip(*dsu)
                  ind.extend(otherind)
  
                  self.parent.model.reorder(ind)
                  newiters = []
                  for i in ind:
                      newiters.append(self.parent.iters[i])
                  self.parent.iters = newiters[:]
                  for i, thisiter in enumerate(self.parent.iters):
                      key = tuple([self.parent.model.get_value(thisiter, j) for j in range(len(colheaders))])
                      self.parent.rownumd[i] = key
  
                  self.num+=1
  
  
          if formatterd is None:
              formatterd = dict()
  
          formatterd = formatterd.copy()
  
          for i, header in enumerate(colheaders):
              renderer = gtk.CellRendererText()
              if header not in formatterd:
                  formatterd[header] = ColumnFormatter()
              formatter = formatterd[header]
  
              column = gtk.TreeViewColumn(header, renderer, text=i)
              renderer.set_property('xalign', formatter.xalign)
              renderer.set_property('editable', True)
              renderer.connect("edited", self.position_edited, i)
              column.connect('clicked', Clicked(self, i))
              column.set_property('clickable', True)
  
              if formatter.cell is not None:
                  column.set_cell_data_func(renderer, formatter.cell)
  
              treeview.append_column(column)
  
  
  
          self.formatterd = formatterd
          self.lastcol = column
          self.add(treeview)
          self.treeview = treeview
          self.clear()
  
      def position_edited(self, renderer, path, newtext, position):
          #print path, position
          self.model[path][position] = newtext
  
      def clear(self):
          self.iterd = dict()
          self.iters = []        # an ordered list of iters
          self.rownumd = dict()  # a map from rownum -> symbol
          self.model.clear()
          self.datad = dict()
  
  
      def flat(self, row):
          seq = []
          for i,val in enumerate(row):
              formatter = self.formatterd.get(self.colheaders[i])
              seq.extend([i,formatter.tostr(val)])
          return seq
  
      def __delete_selected(self, *unused): # untested
  
  
          keyd = dict([(thisiter, key) for key, thisiter in self.iterd.values()])
          for row in self.get_selected():
              key = tuple(row)
              thisiter = self.iterd[key]
              self.model.remove(thisiter)
              del self.datad[key]
              del self.iterd[key]
              self.iters.remove(thisiter)
  
          for i, thisiter in enumerate(self.iters):
              self.rownumd[i] = keyd[thisiter]
  
  
  
      def delete_row(self, row):
          key = tuple(row)
          thisiter = self.iterd[key]
          self.model.remove(thisiter)
  
  
          del self.datad[key]
          del self.iterd[key]
          self.rownumd[len(self.iters)] = key
          self.iters.remove(thisiter)
  
          for rownum, thiskey in self.rownumd.items():
              if thiskey==key: del self.rownumd[rownum]
  
      def add_row(self, row):
          thisiter = self.model.append()
          self.model.set(thisiter, *self.flat(row))
          key = tuple(row)
          self.datad[key] = row
          self.iterd[key] = thisiter
          self.rownumd[len(self.iters)] = key
          self.iters.append(thisiter)
  
      def update_row(self, rownum, newrow):
          key = self.rownumd[rownum]
          thisiter = self.iterd[key]
          newkey = tuple(newrow)
  
          self.rownumd[rownum] = newkey
          del self.datad[key]
          del self.iterd[key]
          self.datad[newkey] = newrow
          self.iterd[newkey] = thisiter
  
  
          self.model.set(thisiter, *self.flat(newrow))
  
      def get_row(self, rownum):
          key = self.rownumd[rownum]
          return self.datad[key]
  
      def get_selected(self):
          selected = []
          def foreach(model, path, iter, selected):
              selected.append(model.get_value(iter, 0))
  
          self.treeview.get_selection().selected_foreach(foreach, selected)
          return selected
  
  
  
  def rec2gtk(r, formatd=None, rownum=0, autowin=True):
      """
      save record array r to excel pyExcelerator worksheet ws
      starting at rownum.  if ws is string like, assume it is a
      filename and save to it
  
      formatd is a dictionary mapping dtype name -> mlab.Format instances
  
      This function creates a SortedStringsScrolledWindow (derived
      from gtk.ScrolledWindow) and returns it.  if autowin is True,
      a gtk.Window is created, attached to the
      SortedStringsScrolledWindow instance, shown and returned.  If
      autowin=False, the caller is responsible for adding the
      SortedStringsScrolledWindow instance to a gtk widget and
      showing it.
      """
  
  
  
      if formatd is None:
          formatd = dict()
  
      formats = []
      for i, name in enumerate(r.dtype.names):
          dt = r.dtype[name]
          format = formatd.get(name)
          if format is None:
              format = mlab.defaultformatd.get(dt.type, mlab.FormatObj())
          #print 'gtk fmt factory', i, name, format, type(format)
          format = gtkformat_factory(format, i)
          formatd[name] = format
  
  
      colheaders = r.dtype.names
      scroll = SortedStringsScrolledWindow(colheaders, formatd)
  
      ind = npy.arange(len(r.dtype.names))
      for row in r:
          scroll.add_row(row)
  
  
      if autowin:
          win = gtk.Window()
          win.set_default_size(800,600)
          #win.set_geometry_hints(scroll)
          win.add(scroll)
          win.show_all()
          scroll.win = win
  
      return scroll
  
  
  class RecListStore(gtk.ListStore):
      """
      A liststore as a model of an editable record array.
  
      attributes:
  
       * r - the record array with the edited values
  
       * formatd - the list of mlab.FormatObj instances, with gtk attachments
  
       * stringd - a dict mapping dtype names to a list of valid strings for the combo drop downs
       
       * callbacks - a matplotlib.cbook.CallbackRegistry.  Connect to the cell_changed with
  
          def mycallback(liststore, rownum, colname, oldval, newval):
             print 'verify: old=%s, new=%s, rec=%s'%(oldval, newval, liststore.r[rownum][colname])
  
          cid = liststore.callbacks.connect('cell_changed', mycallback)
  
          """
      def __init__(self, r, formatd=None, stringd=None):
          """
          r is a numpy record array
  
          formatd is a dict mapping dtype name to mlab.FormatObj instances
  
          stringd, if not None, is a dict mapping dtype names to a list of
          valid strings for a combo drop down editor
          """
          
          if stringd is None:
              stringd = dict()
              
          if formatd is None:
              formatd = mlab.get_formatd(r)
  
          self.stringd = stringd
          self.callbacks = cbook.CallbackRegistry(['cell_changed'])
  
          self.r = r
  
          self.headers = r.dtype.names
          self.formats = [gtkformat_factory(formatd.get(name, mlab.FormatObj()),i)
                          for i,name in enumerate(self.headers)]
  
          # use the gtk attached versions
          self.formatd = formatd = dict(zip(self.headers, self.formats))
          types = []
          for format in self.formats:
              if isinstance(format, mlab.FormatBool):
                  types.append(gobject.TYPE_BOOLEAN)
              else:
                  types.append(gobject.TYPE_STRING)
  
          self.combod = dict()
          if len(stringd):
              types.extend([gobject.TYPE_INT]*len(stringd))
  
              keys = stringd.keys()
              keys.sort()
          
              valid = set(r.dtype.names)
              for ikey, key in enumerate(keys):
                  assert(key in valid)
                  combostore = gtk.ListStore(gobject.TYPE_STRING)
                  for s in stringd[key]:
                      combostore.append([s])
                  self.combod[key] = combostore, len(self.headers)+ikey
  
          
          gtk.ListStore.__init__(self, *types)
  
          for row in r:
              vals = []
              for formatter, val in zip(self.formats, row):
                  if isinstance(formatter, mlab.FormatBool):
                      vals.append(val)
                  else:
                      vals.append(formatter.tostr(val))            
              if len(stringd):
                  # todo, get correct index here?
                  vals.extend([0]*len(stringd))
              self.append(vals)
  
  
      def position_edited(self, renderer, path, newtext, position):
  
          position = int(position)
          format = self.formats[position]
  
          rownum = int(path)
          colname = self.headers[position]
          oldval = self.r[rownum][colname]
          try: newval = format.fromstr(newtext)
          except ValueError:
              msg = cbook.exception_to_str('Error converting "%s"'%newtext)
              error_message(msg, title='Error')
              return
          self.r[rownum][colname] = newval
  
          self[path][position] = format.tostr(newval)
  
  
          self.callbacks.process('cell_changed', self, rownum, colname, oldval, newval)
  
      def position_toggled(self, cellrenderer, path, position):
          position = int(position)
          format = self.formats[position]
  
          newval = not cellrenderer.get_active()
  
          rownum = int(path)
          colname = self.headers[position]
          oldval = self.r[rownum][colname]
          self.r[rownum][colname] = newval
  
          self[path][position] = newval
  
          self.callbacks.process('cell_changed', self, rownum, colname, oldval, newval)
  
  
  
  
  
  class RecTreeView(gtk.TreeView):
      """
      An editable tree view widget for record arrays
      """
      def __init__(self, recliststore, constant=None):
          """
          build a gtk.TreeView to edit a RecListStore
  
          constant, if not None, is a list of dtype names which are not editable
          """
          self.recliststore = recliststore
          
          gtk.TreeView.__init__(self, recliststore)
  
          combostrings = set(recliststore.stringd.keys())
  
          
          if constant is None:
              constant = []
  
          constant = set(constant)
  
          for i, header in enumerate(recliststore.headers):
              formatter = recliststore.formatd[header]
              coltype =  recliststore.get_column_type(i)
  
              if coltype==gobject.TYPE_BOOLEAN:
                  renderer = gtk.CellRendererToggle()
                  if header not in constant:
                      renderer.connect("toggled", recliststore.position_toggled, i)
                      renderer.set_property('activatable', True)
  
              elif header in combostrings:
                   renderer = gtk.CellRendererCombo()
                   renderer.connect("edited", recliststore.position_edited, i)
                   combostore, listind = recliststore.combod[header]
                   renderer.set_property("model", combostore)
                   renderer.set_property('editable', True)             
              else:
                  renderer = gtk.CellRendererText()
                  if header not in constant:
                      renderer.connect("edited", recliststore.position_edited, i)
                      renderer.set_property('editable', True)
  
  
                  if formatter is not None:
                      renderer.set_property('xalign', formatter.xalign)
  
  
  
              tvcol = gtk.TreeViewColumn(header)
              self.append_column(tvcol)
              tvcol.pack_start(renderer, True)
  
              if coltype == gobject.TYPE_STRING:
                  tvcol.add_attribute(renderer, 'text', i)
                  if header in combostrings:
                      combostore, listind = recliststore.combod[header]
                      tvcol.add_attribute(renderer, 'text-column', listind) 
              elif coltype == gobject.TYPE_BOOLEAN:
                  tvcol.add_attribute(renderer, 'active', i)
  
  
              if formatter is not None and formatter.cell is not None:
                  tvcol.set_cell_data_func(renderer, formatter.cell)
  
  
  
  
          self.connect("button-release-event", self.on_selection_changed)
          self.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH)
          self.get_selection().set_mode(gtk.SELECTION_BROWSE)
          self.get_selection().set_select_function(self.on_select)
  
  
      def on_select(self, *args):
          return False
  
      def on_selection_changed(self, *args):
          (path, col) = self.get_cursor()
          ren = col.get_cell_renderers()[0]
          if isinstance(ren, gtk.CellRendererText):
              self.set_cursor_on_cell(path, col, ren, start_editing=True)
  
  def edit_recarray(r, formatd=None, stringd=None, constant=None, autowin=True):
      """
      create a RecListStore and RecTreeView and return them.
  
      If autowin is True, create a gtk.Window, insert the treeview into
      it, and return it (return value will be (liststore, treeview, win)
  
      See RecListStore and RecTreeView for a description of the keyword args
      """
  
      liststore = RecListStore(r, formatd=formatd, stringd=stringd)
      treeview = RecTreeView(liststore, constant=constant)
  
      if autowin:
          win = gtk.Window()
          win.add(treeview)
          win.show_all()
          return liststore, treeview, win
      else:
          return liststore, treeview
          
      
  
  
  if __name__=='__main__':
  
      import datetime
      import gtk
      import numpy as np
      import matplotlib.mlab as mlab
      N = 10
      today = datetime.date.today()
      dates = [today+datetime.timedelta(days=i) for i in range(N)] # datetimes
      weekdays = [d.strftime('%a') for d in dates]                 # strings
      gains = np.random.randn(N)                                   # floats
      prices = np.random.rand(N)*1e7                               # big numbers
      up = gains>0                                                 # bools
      clientid = range(N)                                          # ints
  
      r = np.rec.fromarrays([clientid, dates, weekdays, gains, prices, up],
                            names='clientid,date,weekdays,gains,prices,up')
  
      # some custom formatters
      formatd = mlab.get_formatd(r)
      formatd['date'] = mlab.FormatDate('%Y-%m-%d')
      formatd['prices'] = mlab.FormatMillions(precision=1)
      formatd['gain'] = mlab.FormatPercent(precision=2)
  
      # use a drop down combo for weekdays
      stringd = dict(weekdays=['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])
      constant = ['clientid']   # block editing of this field
  
  
      liststore = RecListStore(r, formatd=formatd, stringd=stringd)
      treeview = RecTreeView(liststore, constant=constant)
  
      def mycallback(liststore, rownum, colname, oldval, newval):
          print 'verify: old=%s, new=%s, rec=%s'%(oldval, newval, liststore.r[rownum][colname])
  
      liststore.callbacks.connect('cell_changed', mycallback)
  
      win = gtk.Window()
      win.set_title('with full customization')
      win.add(treeview)
      win.show_all()
  
      # or you just use the defaults
      r2 = r.copy()
      ls, tv, win2 = edit_recarray(r2)
      win2.set_title('with all defaults')
  
      gtk.main()
      
  