Liststore / binding help please! I'm SO close

Continuing on with my attempts to dynamically generate a class to handle data from a SQL query ( see How to force a "refresh" of widget values in a ColumnView? ) …

I now thought I had everything in place. I’m generating a class definition, and using importlib.util to load it. I can populate the model and see my data. However I’ve done “something” wrong with the binding part, because when I call a setter method, I instead see the getter method being called. However it looks correct to me. Can someone please take a look and see what I’ve done?

#!/usr/bin/python3

import gi
gi.require_version( "Gtk" , "4.0" )
from gi.repository import Gtk, Gio, Gdk, Pango, GObject, GLib
import json , uuid , importlib.util , sys

class GridWidget( Gtk.Widget ):

    def __init__( self , column_name="oops" , **kwargs ):

        super().__init__( **kwargs )
        self.model_position = -1
        self.column_name = column_name

class GridEntry( Gtk.Entry , GridWidget ):

    def __init__( self , **kwargs ):

        super().__init__( **kwargs )

class GridLabel( Gtk.Label , GridWidget ):

    def __init__( self , **kwargs ):

        super().__init__( **kwargs )

class GridImage( Gtk.Image , GridWidget ):

    def __init__( self , **kwargs ):

        super().__init__( **kwargs )

class Datasheet( Gtk.ScrolledWindow ):

    def __init__( self , column_definitions , data ):

        super().__init__()
        self.set_policy( Gtk.PolicyType.NEVER , Gtk.PolicyType.AUTOMATIC )
        self.setup_columns( column_definitions )
        self.setup_model( column_definitions , data )

    def generate_grid_row_class ( self , column_definitions ):

        """This method generates a GridRow class, based on the columns in the query.
        We need to do this, as the bindings use decorators, and there doesn't appear to be a way
        to dynamically configure decorators"""

        unique_class_name = "GridRow_" + uuid.uuid4().hex[:6].upper()

        class_def = "from gi.repository import Gtk, Gio, Gdk, Pango, GObject, GLib\n\n" \
            "class " + unique_class_name + "( GObject.Object ):\n    __gtype_name__ = '" + unique_class_name + "'\n\n" \
            "    def __init__( self , track , record ):\n" \
            "        \n" \
            "        super().__init__()\n" \
            "        self._track = track\n" \
            "        \n" \
            "        # Unpack record into class attributes\n"

        access_methods = []

        for i in range( 0 , len( column_definitions ) ):
            class_def = class_def + "        self._{0} = record[ {1} ]\n".format( column_definitions[ i ]['name'] , i )
            access_methods.append (
                "    @GObject.Property(type=str)\n" \
                "    def {0}( self ):\n" \
                "        return self._{0}\n" \
                "    \n" \
                "    @{0}.setter\n" \
                "    def set_{0}( self , {0} ):\n" \
                "        if self._{0} != {0}:\n" \
                "            self._{0} = {0}\n" \
                "            self.notify( \"{0}\" )\n    ".format( column_definitions[ i ]['name'] )
            )

        class_def = class_def + "\n" + "\n".join( access_methods )
        print( "Class definition:\n{0}".format( class_def ) )
        tmp_class_path = "/tmp/{0}.py".format( unique_class_name )

        with open( tmp_class_path , "w" ) as class_file:
            class_file.write( class_def )

        # https://stackoverflow.com/questions/67631/how-can-i-import-a-module-dynamically-given-the-full-path
        spec = importlib.util.spec_from_file_location( unique_class_name , tmp_class_path )
        module = importlib.util.module_from_spec( spec )
        sys.modules[ unique_class_name ] = module
        spec.loader.exec_module( module )
        grid_row_class = getattr( module , unique_class_name )

        return grid_row_class

    def setup_columns( self , column_definitions ):
        cv = Gtk.ColumnView( hexpand=True , single_click_activate=False )
        counter = 0
        for d in column_definitions:
            cvc = Gtk.ColumnViewColumn( title = d['name'] )
            f = Gtk.SignalListItemFactory()
            f.connect( "setup" , self.setup , d['type'] , 1 , -1 , d['name'] )
            f.connect( "bind" , self.bind , d['type'] , d['name'] )
            cvc.set_factory( f )
            cv.append_column( cvc )
            counter = counter + 1
        self._column_definitions = column_definitions
        self.cv = cv

    def setup( self , factory , item , type , xalign , chars , name ):
        if type == "label":
            label = GridLabel( xalign=xalign , width_chars=chars , ellipsize=Pango.EllipsizeMode.END , valign=Gtk.Align.FILL , vexpand=True , column_name=name )
            item.set_child( label )
        elif type == "entry":
            entry = GridEntry( xalign=xalign , width_chars=chars , valign=Gtk.Align.FILL , vexpand=True , column_name=name )
            item.set_child( entry )
            entry.connect( "activate" , self.on_entry_activate )
            entry.connect( "state-flags-changed" , self.on_entry_move_focus )
        elif type == "image":
            image = GridImage( column_name=name )
            item.set_child( image )
        else:
            raise Exception( "Unknown type: {0}".format( type ) )

    def on_entry_move_focus( self , entry , flags ):
#        print( "entry move focus and i have {0}".format( flags ) )
#        print( flags.FOCUS_WITHIN )
        self.on_entry_activate( entry )

    def on_entry_activate( self , entry ):
        parent = entry.get_parent()
        single_selection = self.cv.get_model()
        position = entry.model_position
#        print( "ss: {0}".format( single_selection[ position ] ) )
        grid_row = single_selection[ position ]
        column_name = entry.column_name
        old_value = getattr( grid_row , column_name )
        new_value = entry.get_text()
        if old_value != new_value:
            # setter = getattr( grid_row , "set_" + column_name )
            # grid_row.set_Some_String( new_value )
            # grid_row.notify( "Some_String" )
            # ???
            getattr( grid_row , "set_" + column_name)( new_value )
            getattr( grid_row , "set_icon"  , "view-refresh" )

    def bind( self , factory , item , type , column_name ):
        widget = item.get_child()
        grid_row = item.get_item()
        if type == "label":            
            grid_row.bind_property( column_name , widget , "label" , GObject.BindingFlags.SYNC_CREATE )
        elif type == "entry":
            grid_row.bind_property( column_name , widget , "text" , GObject.BindingFlags.SYNC_CREATE )
        elif type == "image":
            grid_row.bind_property( column_name , widget , "icon-name" , GObject.BindingFlags.SYNC_CREATE )
        else:
            raise Exception( "Unknown type {0}".format( type ) )
        widget.model_position = item.get_position()

    def setup_model( self , column_definitions , data ):
        grid_row_class = self.generate_grid_row_class( column_definitions )
        glist = Gio.ListStore.new( item_type=grid_row_class )
        track = 0
        for row in data:
            glist.append( grid_row_class( track , row ) )
            track = track + 1
        model=Gtk.SingleSelection( model = glist )
        self.cv.set_model( model )
        self.set_child( self.cv )

    def column_name_to_number( self , column_name ):
        counter = 0
        for d in self._column_definitions:
            if d['name'] == column_name:
                return counter
            counter = counter + 1
        return False

def on_activate(app):
    column_definitions = [
        {
            "name": "ID"
          , "type": "label"
        }
      , {
            "name": "icon"
          , "type": "image"
        }
      , {
            "name": "Some_String"
          , "type": "entry"
        }
      , {
            "name": "Another_thingy"
          , "type": "entry"
        }
    ]
    data = []
    for i  in range( 0 , 1000 ):
        # data.append( [ i , "ok" , "blah", "more" ] )
        data.append( [ i , "ok" , uuid.uuid4().hex[:6].upper(), uuid.uuid4().hex[:6].upper() ] )

    datasheet = Datasheet( column_definitions , data )
    win=Gtk.ApplicationWindow( application=app )
    win.set_child( datasheet )
    win.present()

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

The dynamically generated class from this code looks like:

from gi.repository import Gtk, Gio, Gdk, Pango, GObject, GLib

class GridRow_1A00DB( GObject.Object ):
    __gtype_name__ = 'GridRow_1A00DB'

    def __init__( self , track , record ):
        
        super().__init__()
        self._track = track
        
        # Unpack record into class attributes
        self._ID = record[ 0 ]
        self._icon = record[ 1 ]
        self._Some_String = record[ 2 ]
        self._Another_thingy = record[ 3 ]

    @GObject.Property(type=str)
    def ID( self ):
        return self._ID
    
    @ID.setter
    def set_ID( self , ID ):
        if self._ID != ID:
            self._ID = ID
            self.notify( "ID" )
    
    @GObject.Property(type=str)
    def icon( self ):
        return self._icon
    
    @icon.setter
    def set_icon( self , icon ):
        if self._icon != icon:
            self._icon = icon
            self.notify( "icon" )
    
    @GObject.Property(type=str)
    def Some_String( self ):
        return self._Some_String
    
    @Some_String.setter
    def set_Some_String( self , Some_String ):
        if self._Some_String != Some_String:
            self._Some_String = Some_String
            self.notify( "Some_String" )
    
    @GObject.Property(type=str)
    def Another_thingy( self ):
        return self._Another_thingy
    
    @Another_thingy.setter
    def set_Another_thingy( self , Another_thingy ):
        if self._Another_thingy != Another_thingy:
            self._Another_thingy = Another_thingy
            self.notify( "Another_thingy" )
    

No responses ;( OK I’ve stripped out the dynamic generation of the GridRow class, to make the example simpler. Whenever I call a setter ( eg set_Some_String ), I instead see the getter ( eg Some_String ) called. I’ve followed the example at How to force a "refresh" of widget values in a ColumnView? - #4 by ebassi by @ebassi … any ideas?

#!/usr/bin/python3

import gi
gi.require_version( "Gtk" , "4.0" )
from gi.repository import Gtk, Gio, Gdk, Pango, GObject, GLib
import json , uuid , importlib.util , sys

class GridRow_B20B2F( GObject.Object ):
    __gtype_name__ = 'GridRow_B20B2F'

    def __init__( self , track , record ):
        
        super().__init__()
        self._track = track
        
        # Unpack record into class attributes
        self._ID = record[ 0 ]
        self._icon = record[ 1 ]
        self._Some_String = record[ 2 ]
        self._Another_thingy = record[ 3 ]

    @GObject.Property(type=str)
    def ID( self ):
        return self._ID
    
    @ID.setter
    def set_ID( self , ID ):
        if self._ID != ID:
            self._ID = ID
            self.notify( "ID" )
    
    @GObject.Property(type=str)
    def icon( self ):
        return self._icon
    
    @icon.setter
    def set_icon( self , icon ):
        if self._icon != icon:
            self._icon = icon
            self.notify( "icon" )
    
    @GObject.Property(type=str)
    def Some_String( self ):
        return self._Some_String
    
    @Some_String.setter
    def set_Some_String( self , Some_String ):
        if self._Some_String != Some_String:
            self._Some_String = Some_String
            self.notify( "Some_String" )
    
    @GObject.Property(type=str)
    def Another_thingy( self ):
        return self._Another_thingy
    
    @Another_thingy.setter
    def set_Another_thingy( self , Another_thingy ):
        if self._Another_thingy != Another_thingy:
            self._Another_thingy = Another_thingy
            self.notify( "Another_thingy" )
    
class GridWidget( Gtk.Widget ):

    def __init__( self , column_name="oops" , **kwargs ):

        super().__init__( **kwargs )
        self.model_position = -1
        self.column_name = column_name

class GridEntry( Gtk.Entry , GridWidget ):

    def __init__( self , **kwargs ):

        super().__init__( **kwargs )

class GridLabel( Gtk.Label , GridWidget ):

    def __init__( self , **kwargs ):

        super().__init__( **kwargs )

class GridImage( Gtk.Image , GridWidget ):

    def __init__( self , **kwargs ):

        super().__init__( **kwargs )

class Datasheet( Gtk.ScrolledWindow ):

    def __init__( self , column_definitions , data ):

        super().__init__()
        self.set_policy( Gtk.PolicyType.NEVER , Gtk.PolicyType.AUTOMATIC )
        self.setup_columns( column_definitions )
        self.setup_model( column_definitions , data )
    
    def setup_columns( self , column_definitions ):
        cv = Gtk.ColumnView( hexpand=True , single_click_activate=False )
        counter = 0
        for d in column_definitions:
            cvc = Gtk.ColumnViewColumn( title = d['name'] )
            f = Gtk.SignalListItemFactory()
            f.connect( "setup" , self.setup , d['type'] , 1 , -1 , d['name'] )
            f.connect( "bind" , self.bind , d['type'] , d['name'] )
            cvc.set_factory( f )
            cv.append_column( cvc )
            counter = counter + 1
        self._column_definitions = column_definitions
        self.cv = cv

    def setup( self , factory , item , type , xalign , chars , name ):
        if type == "label":
            label = GridLabel( xalign=xalign , width_chars=chars , ellipsize=Pango.EllipsizeMode.END , valign=Gtk.Align.FILL , vexpand=True , column_name=name )
            item.set_child( label )
        elif type == "entry":
            entry = GridEntry( xalign=xalign , width_chars=chars , valign=Gtk.Align.FILL , vexpand=True , column_name=name )
            item.set_child( entry )
            entry.connect( "activate" , self.on_entry_activate )
            entry.connect( "state-flags-changed" , self.on_entry_move_focus )
        elif type == "image":
            image = GridImage( column_name=name )
            item.set_child( image )
        else:
            raise Exception( "Unknown type: {0}".format( type ) )

    def on_entry_move_focus( self , entry , flags ):
#        print( "entry move focus and i have {0}".format( flags ) )
#        print( flags.FOCUS_WITHIN )
        self.on_entry_activate( entry )

    def on_entry_activate( self , entry ):
        parent = entry.get_parent()
        single_selection = self.cv.get_model()
        position = entry.model_position
#        print( "ss: {0}".format( single_selection[ position ] ) )
        grid_row = single_selection[ position ]
        column_name = entry.column_name
        old_value = getattr( grid_row , column_name )
        new_value = entry.get_text()
        if old_value != new_value:
            # setter = getattr( grid_row , "set_" + column_name )
            # grid_row.set_Some_String( new_value )
            # grid_row.notify( "Some_String" )
            # ???
            getattr( grid_row , "set_" + column_name)( new_value )
            getattr( grid_row , "set_icon"  , "view-refresh" )

    def bind( self , factory , item , type , column_name ):
        widget = item.get_child()
        grid_row = item.get_item()
        if type == "label":            
            grid_row.bind_property( column_name , widget , "label" , GObject.BindingFlags.SYNC_CREATE )
        elif type == "entry":
            grid_row.bind_property( column_name , widget , "text" , GObject.BindingFlags.SYNC_CREATE )
        elif type == "image":
            grid_row.bind_property( column_name , widget , "icon-name" , GObject.BindingFlags.SYNC_CREATE )
        else:
            raise Exception( "Unknown type {0}".format( type ) )
        widget.model_position = item.get_position()

    def setup_model( self , column_definitions , data ):
        glist = Gio.ListStore.new( item_type=GridRow_B20B2F )
        track = 0
        for row in data:
            glist.append( GridRow_B20B2F( track , row ) )
            track = track + 1
        model=Gtk.SingleSelection( model = glist )
        self.cv.set_model( model )
        self.set_child( self.cv )

    def column_name_to_number( self , column_name ):
        counter = 0
        for d in self._column_definitions:
            if d['name'] == column_name:
                return counter
            counter = counter + 1
        return False

def on_activate(app):
    column_definitions = [
        {
            "name": "ID"
          , "type": "label"
        }
      , {
            "name": "icon"
          , "type": "image"
        }
      , {
            "name": "Some_String"
          , "type": "entry"
        }
      , {
            "name": "Another_thingy"
          , "type": "entry"
        }
    ]
    data = []
    for i  in range( 0 , 1000 ):
        # data.append( [ i , "ok" , "blah", "more" ] )
        data.append( [ i , "ok" , uuid.uuid4().hex[:6].upper(), uuid.uuid4().hex[:6].upper() ] )

    datasheet = Datasheet( column_definitions , data )
    win=Gtk.ApplicationWindow( application=app )
    win.set_child( datasheet )
    win.present()

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

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.