GTK4 ColumnView - Update entire row when changes are made in one cell

61 Views Asked by At

How can I get a GTK4 ColumnView to update the entire row when one cell in the row is changed?

The following simple example creates a ColumnView with two columns for a String and the corresponding ASCII code.

Changing the string in a cell will update the model for both String and ASCII, and changing the ASCII updates the model for both as well.

However, the ColumnView display only updates when I use the mouse to leave the row by clicking into another row.

How do I get the display to update without leaving the row?

import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gio, GObject

class DataObject(GObject.GObject):

    __gtype_name__ = 'DataObject'

    text = GObject.Property(type=str, default=None)
    number = GObject.Property(type=str, default=None)
    
    def __init__(self, text, number):
        super().__init__()
        self.text = text
        self.number = number

def setup_c1(widget, item):
    """Setup the widget to show in the Gtk.Listview"""
    cell = Gtk.EditableLabel()
    item.set_child(cell)
    cell.connect('changed', on_c1_change, item)

def bind_c1(widget, item):
    """bind data from the store object to the widget"""
    #item ListItem object
    label = item.get_child() #Gtk.EditableLabel
    obj = item.get_item() #DataObject
    label.set_text(obj.text)
    label.bind_property("text", obj, "text", GObject.BindingFlags.SYNC_CREATE)
    
def setup_c2(widget, item):
    """Setup the widget to show in the Gtk.Listview"""
    cell = Gtk.EditableLabel()    
    item.set_child(cell)
    cell.connect('changed', on_c2_change, item)

def bind_c2(widget, item):
    """bind data from the store object to the widget"""
    label = item.get_child() #Gtk.EdiableLabel
    obj = item.get_item() #DataObject
    label.set_text(obj.number)
    label.bind_property("text", obj, "number", GObject.BindingFlags.SYNC_CREATE)

def on_c1_change(self, item):
    obj = item.get_item()
    obj.number = str(ord(obj.text))

def on_c2_change(self, item):
    obj = item.get_item()
    if not obj.number == '':
        if int(obj.number) >= 65:
            obj.text = chr(int(obj.number))

def on_activate(app):
    win = Gtk.ApplicationWindow(
        application=app,
        title="GTK 4 ColumnView is Confusing !!!",
        default_height=400,
        default_width=400,
    )
    
    list_view = Gtk.ColumnView()  
    factory_c1 = Gtk.SignalListItemFactory()
    factory_c1.connect("setup", setup_c1)
    factory_c1.connect("bind", bind_c1)
    
    factory_c2 = Gtk.SignalListItemFactory()
    factory_c2.connect("setup", setup_c2)
    factory_c2.connect("bind", bind_c2)    
    
    selection = Gtk.SingleSelection()
    store = Gio.ListStore.new(DataObject)  
    selection.set_model(store)
    list_view.set_model(selection)
    
    column1 = Gtk.ColumnViewColumn.new("String", factory_c1)
    column2 = Gtk.ColumnViewColumn.new("ASCII Code", factory_c2)
    
    list_view.append_column(column1)
    list_view.append_column(column2)

    for i in range(65, 91):
        store.append(DataObject(chr(i), str(i)))
    
    sw = Gtk.ScrolledWindow()
    sw.set_child(list_view)
    win.set_child(sw)
    win.present()

app = Gtk.Application(application_id="org.gtk.Example")
app.connect("activate", on_activate)
app.run(None)
1

There are 1 best solutions below

0
Dodezv On

The problem is that property bindings are, by default, unidirectional (see GObject.Object.bind_property). You set the binding up such that the object will be updated by the label, but not vice versa.

The updating of the label was actually done through binding the item, not through the binding of the properties.

So one solution is to change the binding such that it is bidirectional:

 obj.bind_property( "number", label, "text", GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL)

But this seemingly causes endless loops of [Text of label changed] and [Object property changed] calling each other. So I just changed the direction of the binding

 obj.bind_property( "number", label, "text", GObject.BindingFlags.SYNC_CREATE)

and then started to listen to notify::editable instead of changed for the labels.

Another solution, which I did not try, would be to add two new properties, "set_number" which is then bound like

 obj.bind_property( "set_number", label, "text",  GObject.BindingFlags.DEFAULT)
 label.bind_property( "text", obj, "number", GObject.BindingFlags.SYNC_CREATE)

and "set_text" and the chain of bindings would make updating "set_text" also the label of "text" and then the "text" of the object.

The full updated code:

import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gio, GObject

class DataObject(GObject.GObject):

    __gtype_name__ = 'DataObject'

    text = GObject.Property(type=str, default=None)
    number = GObject.Property(type=str, default=None)
    
    def __init__(self, text, number):
        super().__init__()
        self.text = text
        self.number = number

def setup_c1(widget, item):
    """Setup the widget to show in the Gtk.Listview"""
    cell = Gtk.EditableLabel()
    item.set_child(cell)
    cell.connect('notify::editing', on_c1_change, item)

def bind_c1(widget, item):
    """bind data from the store object to the widget"""
    #item ListItem object
    label = item.get_child() #Gtk.EditableLabel
    obj = item.get_item() #DataObject
    obj.bind_property("text", label , "text", GObject.BindingFlags.SYNC_CREATE)
    
def setup_c2(widget, item):
    """Setup the widget to show in the Gtk.Listview"""
    cell = Gtk.EditableLabel()    
    item.set_child(cell)
    cell.connect('notify::editing', on_c2_change, item)

def bind_c2(widget, item):
    """bind data from the store object to the widget"""
    label = item.get_child() #Gtk.EdiableLabel
    obj = item.get_item() #DataObject
    obj.bind_property( "number", label, "text", GObject.BindingFlags.SYNC_CREATE)

def on_c1_change(self, _pspec, item):
    obj = item.get_item()
    try:
        number = str(ord(self.get_text()))
    except TypeError:
        self.set_text(obj.text)
    else:
        obj.number = number
        obj.text = self.get_text()

def on_c2_change(self, _pspec, item):
    obj = item.get_item()
    try:
        text = self.get_text()
        if int(text) >= 65:
            obj.text = chr(int(text))
            obj.number = text
    except ValueError:
        obj.number = obj.number # Resetting

def on_activate(app):
    win = Gtk.ApplicationWindow(
        application=app,
        title="GTK 4 ColumnView is Confusing !!!",
        default_height=400,
        default_width=400,
    )
    
    list_view = Gtk.ColumnView()  
    factory_c1 = Gtk.SignalListItemFactory()
    factory_c1.connect("setup", setup_c1)
    factory_c1.connect("bind", bind_c1)
    
    factory_c2 = Gtk.SignalListItemFactory()
    factory_c2.connect("setup", setup_c2)
    factory_c2.connect("bind", bind_c2)    
    
    selection = Gtk.SingleSelection()
    store = Gio.ListStore.new(DataObject)  
    selection.set_model(store)
    list_view.set_model(selection)
    
    column1 = Gtk.ColumnViewColumn.new("String", factory_c1)
    column2 = Gtk.ColumnViewColumn.new("ASCII Code", factory_c2)
    
    list_view.append_column(column1)
    list_view.append_column(column2)

    for i in range(65, 91):
        store.append(DataObject(chr(i), str(i)))
    
    sw = Gtk.ScrolledWindow()
    sw.set_child(list_view)
    win.set_child(sw)
    win.present()

app = Gtk.Application(application_id="org.gtk.Example")
app.connect("activate", on_activate)
app.run(None)