Hi everybody,

I have seen the recent discussion in the list about implementing custom
CellRenderers in Python and some guys (Austin Henry) found it difficult
because you cannot (yet) implement a C interface with Python.

I have succesfully used a gtk.Combo inside a Cell in a gtk.TreeView so
I'm going to share my code with the list members just in case anyone
find it useful. Before even reading the code let me warn you about some
important points:

- This code has some nasty hacks and they should be replaced with proper
pygtk code when c interfaces become accesible from Python.

- It's far from being bug free, I just want to share the technique I'm
using for possible feedback.

Having said that, I will describe the two changes I have added to a
regular gtk.TreeView for the curious:

- Custom CellEditors: when the cursor is on a cell I draw a GridEditor
on top of it. A GridEditor is nothing but a gtk.Window with a custom
widget for editing purposes like a gtk.Entry, a gtk.Combo, a gtk.Toggle
or anything that you want. It does not have to implement the
CellEditable interface which is why you couldn't implement a custom
CellEditor.

- Auto editing mode: you don't have to press enter anymore to get into
the edition state like in a regular gtk.TreeView. You just move through
the grid with the tab key, up arrow, down arrow, page up and page down.
Well this is not totally true: when the Grid is realized you are not in
editing mode yet, so just for the first time, you have to press enter.

As you have already read, i call my widget Grid and I hope you find it
usefull. Comments are greatly apreciated

Lorenzo Gil Sanchez
import pygtk
pygtk.require('2.0')
import gtk
import gobject
import pango

class GridCellEditor(gtk.Window):
    def __init__(self, grid, columnIndex):
        gtk.Window.__init__(self)
        self.grid = grid
        self.columnIndex = columnIndex
        self.set_decorated(gtk.FALSE)
        self.editor = self.createEditor()
        self.add(self.editor)
        self.editor.grab_focus()
        self.path = None
        self.connect('delete-event', self.deleteEvent)

    def applyChanges(self):
        iter = self.grid.model.get_iter(self.path)
        self.grid.model.set_value(iter, self.columnIndex, self.getValue())
    
    def createEditor(self):
        raise 'this is an abstract method'

    def deleteEvent(self, widget, event, data=None):
        self.grid.get_toplevel().destroy()

    def focusOutEvent(self, widget, data=None):
        self.hide()
        
    def getValue(self):
        raise 'this is an abstract method'

    def keyPressEvent(self, widget, event, data=None):
        if event.keyval in [gtk.keysyms.Tab, gtk.keysyms.Down, gtk.keysyms.Up,
                            gtk.keysyms.Page_Down, gtk.keysyms.Page_Up]:
            self.applyChanges()
            self.grid.keyPressCallback(widget, event)
            return gtk.TRUE
        return gtk.FALSE
    
    def setValue(self, value):
        raise 'this is an abstract method'
    
    def show(self, x, y, w, h, path):
        self.set_transient_for(self.grid.get_toplevel())
        self.set_destroy_with_parent(gtk.TRUE)
        self.path = path
        iter = self.grid.model.get_iter(self.path)
        value = self.grid.model.get_value(iter, self.columnIndex)
        self.setValue(value)
        self.set_size_request(w, h)
        self.resize(w, h)
        self.show_all()
        self.present()
        self.move(x, y)

class GridCellEditorText(GridCellEditor):
    def __init__(self, grid, columnIndex):
        GridCellEditor.__init__(self, grid, columnIndex)

    def createEditor(self):
        editor = gtk.Entry()
        editor.set_has_frame(gtk.FALSE)
        editor.connect('key-press-event', self.keyPressEvent)
        editor.connect('focus-out-event', self.focusOutEvent)
        return editor

    def getValue(self):
        return self.editor.get_text()
    
    def setValue(self, value):
        if value == None:
            value = ''
        self.editor.set_text(value)

class GridCellEditorToggle(GridCellEditor):
    def __init__(self, grid, columnIndex):
        GridCellEditor.__init__(self, grid, columnIndex)

    def createEditor(self):
        editor = gtk.Alignment(xalign=0.5, yalign=0.5)
        button = gtk.CheckButton()
        button.connect('key-press-event', self.keyPressEvent)
        button.connect('focus-out-event', self.focusOutEvent)
        button.show()
        editor.add(button)
        return editor

    def getValue(self):
        return self.editor.get_child().get_active()

    def setValue(self, value):
        self.editor.get_child().set_active(value)

class GridCellEditorCombo(GridCellEditor):
    def __init__(self, grid, columnIndex, values):
        self.values = values
        GridCellEditor.__init__(self, grid, columnIndex)

    def buttonPressEvent(self, widget, data=None):
        self.listOpened = gtk.TRUE
    
    def createEditor(self):
        combo = gtk.Combo()
        combo.set_popdown_strings(self.values)
        combo.set_use_arrows(gtk.FALSE)
        combo.entry.connect('key-press-event', self.keyPressEvent)
        combo.entry.connect('focus-out-event', self.focusOutEvent)
        button = combo.get_children()[-1]
        button.connect('pressed', self.buttonPressEvent)
        self.listOpened = gtk.FALSE
        combo.show()
        return combo

    def focusOutEvent(self, widget, data=None):
        if not self.listOpened:
            self.hide()

    def keyPressEvent(self, widget, event, data=None):
        self.listOpened = gtk.FALSE
        return GridCellEditor.keyPressEvent(self, widget, event, data)
        
    def getValue(self):
        return self.editor.entry.get_text()
    
    def setValue(self, value):
        self.editor.entry.set_text(value)

class Grid(gtk.TreeView):
    def __init__(self, model):
        gtk.TreeView.__init__(self, model)

        self.model = model

        self.connect('key-press-event', self.keyPressCallback)
        self.connect('button-release-event', self.buttonReleaseCallback)

        self.set_rules_hint(gtk.TRUE)

    def cursorChangedCallback(self, widget, data=None):
        path, column = self.get_cursor()
        if path != None and column != None:
            self.updateEditor(path, column)

    def buttonReleaseCallback(self, widget, event, data=None):
        if event.button == 1:
            path, column, x, y = self.get_path_at_pos(event.x, event.y)
            self.setCursor(path, column)
    
    def keyPressCallback(self, widget, event, data=None):
        if event.keyval == gtk.keysyms.Tab:
            self.onKeyTab(event)
        elif event.keyval == gtk.keysyms.Down:
            self.onKeyDown(event)
        elif event.keyval == gtk.keysyms.Up:
            self.onKeyUp(event)
        elif event.keyval == gtk.keysyms.Page_Down:
            self.onKeyPageDown(event)
        elif event.keyval == gtk.keysyms.Page_Up:
            self.onKeyPageUp(event)
        elif event.keyval == gtk.keysyms.Return:
            self.cursorChangedCallback(None, None)

        return gtk.TRUE

    def onKeyDown(self, event):
        path, column = self.get_cursor()
        self.moveCursorToNextRow(path, column)

    def onKeyPageDown(self, event):
        path, column = self.get_cursor()
        self.moveCursorToNextPage(path, column)

    def onKeyPageUp(self, event):
        path, column = self.get_cursor()
        self.moveCursorToPrevPage(path, column)
    
    def onKeyUp(self, event):
        path, column = self.get_cursor()
        self.moveCursorToPrevRow(path, column)
        
    def onKeyTab(self, event):
        path, column = self.get_cursor()
        if event.state == gtk.gdk.SHIFT_MASK:
            self.moveCursorToPrevCell(path, column)
        else:
            self.moveCursorToNextCell(path, column)

    def setCursor(self, path, column):
        self.set_cursor(path, column)
        self.updateEditor(path, column)
        
    def updateEditor(self, path, column):
        cell_area = self.get_cell_area(path, column)
        bin = self.get_bin_window()
        x_bin, y_bin = bin.get_origin()
        columns = self.get_columns()
        index = columns.index(column)
        if self.editors[index] != None:
            self.editors[index].show(x_bin + cell_area.x,
                                     y_bin + cell_area.y,
                                     cell_area.width,
                                     cell_area.height,
                                     path)
        
    def moveCursorToNextCell(self, path, column):
        columns = self.get_columns()
        index = columns.index(column) + 1
        if index == len(columns):
            index = 0                
            path = (path[0]+1,)
            try:
                self.model.get_iter(path)
            except:
                return
            
        column = columns[index]
        self.setCursor(path, column)
        
    def moveCursorToNextPage(self, path, column):
        rowHeight = column.cell_get_size()[4]
        gridHeight = self.allocation[3] - rowHeight
        visibleRows = gridHeight / rowHeight
        valid = 0
        path = (path[0]+visibleRows,)
        while not valid:
            try:
                self.model.get_iter(path)
                valid = 1
            except:
                path = (path[0]-1,)

        self.setCursor(path, column)

    def moveCursorToNextRow(self, path, column):
        path = (path[0]+1,)
        try:
            self.model.get_iter(path)
        except:
            path = (path[0]-1,)
        self.setCursor(path, column)
    
    def moveCursorToPrevCell(self, path, column):
        columns = self.get_columns()
        index = columns.index(column) - 1
        if index == -1:
            index = len(columns) - 1
            if path[0] == 0:
                self.get_toplevel().child_focus(gtk.DIR_TAB_BACKWARD)
                return
            else:
                path = (path[0]-1, )
                
        column = columns[index]
        self.setCursor(path, column)

    def moveCursorToPrevPage(self, path, column):
        rowHeight = column.cell_get_size()[4]
        gridHeight = self.allocation[3] - rowHeight
        visibleRows = gridHeight / rowHeight
        path = (max(0,path[0]-visibleRows),)
        self.setCursor(path, column)
        
    def moveCursorToPrevRow(self, path, column):
        if path[0] > 0:
            path = (path[0]-1,)
            self.setCursor(path, column)

    def createColumns(self):
        self.editors = []

        for i in range(self.model.get_n_columns()):
            title = self.model.getColumnTitle(i)
            type = self.model.get_column_type(i)
            if type == gobject.TYPE_BOOLEAN:
                renderer = gtk.CellRendererToggle()
                
                column = gtk.TreeViewColumn(title, renderer, active=i)
                self.editors.append(GridCellEditorToggle(self, i))
            elif type == gobject.TYPE_PYOBJECT:
                renderer = gtk.CellRendererText()
                xalign = 1.0
                renderer.set_property('xalign', xalign)
                column = gtk.TreeViewColumn(title, renderer, text=i)
                self.editors.append(GridCellEditorCombo(self, i, ['value 1','2','3']))
                
            else:
                renderer = gtk.CellRendererText()
                if type in [gobject.TYPE_INT, gobject.TYPE_FLOAT]:
                    xalign = 1.0
                else:
                    xalign = 0.0
                renderer.set_property('xalign', xalign)
                column = gtk.TreeViewColumn(title, renderer, text=i)
                self.editors.append(GridCellEditorText(self, i))
                
            column.set_alignment(0.5)
            column.set_resizable(gtk.TRUE)
            self.append_column(column)

    def setEditors(self, editors):
        self.editors = editors
        
def test():
    win = gtk.Window(gtk.WINDOW_TOPLEVEL)
    win.connect('destroy', lambda w: gtk.main_quit())

    model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_BOOLEAN,
                          gobject.TYPE_STRING)
    for i in range(10):
        iter = model.append(('foo', gtk.FALSE, 'option%d'%(i%3)))

    grid = Grid(model)
    
    editors = [GridCellEditorText(grid, 0),
               GridCellEditorToggle(grid, 1),
               GridCellEditorCombo(grid, 2, ['option 0','option 1','option 2'])
               ]

    grid.setEditors(editors)
    
    renderer = gtk.CellRendererText()
    column = gtk.TreeViewColumn('Column 1', renderer, text=0)
    column.set_resizable(gtk.TRUE)
    grid.append_column(column)

    renderer = gtk.CellRendererToggle()
    column = gtk.TreeViewColumn('Column 2', renderer, active=1)
    column.set_resizable(gtk.TRUE)
    grid.append_column(column)

    renderer = gtk.CellRendererText()
    column = gtk.TreeViewColumn('Column 3', renderer, text=2)
    column.set_resizable(gtk.TRUE)
    grid.append_column(column)

    grid.show()

    win.add(grid)
    win.show()
    gtk.main()

if __name__ == '__main__':
    test()
_______________________________________________
pygtk mailing list   [EMAIL PROTECTED]
http://www.daa.com.au/mailman/listinfo/pygtk
Read the PyGTK FAQ: http://www.async.com.br/faq/pygtk/

Reply via email to