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" )