Former-commit-id:a02aeb236c
[formerly9f19e3f712
] [formerlya02aeb236c
[formerly9f19e3f712
] [formerly06a8b51d6d
[formerly 64fa9254b946eae7e61bbc3f513b7c3696c4f54f]]] Former-commit-id:06a8b51d6d
Former-commit-id:8e80217e59
[formerly3360eb6c5f
] Former-commit-id:377dcd10b9
628 lines
19 KiB
Python
Executable file
628 lines
19 KiB
Python
Executable file
"""
|
|
|
|
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):
|
|
"""
|
|
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()
|
|
|