Gtk4 / listview questions

Hi everyone. I’m implementing ( in python ) a ‘data grid’, to store and edit data from a database. Requirements:

  • Millions of rows
  • Support combo / “dropdown” widgets
  • Support icons ( eg record status, and other custom icons )

I have read that we can now pack regular widgets into a listview. Is this what I want to do in my case? I have existing code ( in perl ) for gtk3 - should I just port the GtkCellRenderer stuff across? Are there some examples( in python would be nice ) of using ( eg ) a DropDown in a ListView?

Yes. my feeling is that GtkLIstView and GtkColumnView is the path to go in Gtk4. And I have seen that even millions of rows may work – I have only a few dozen rows myself. I am doing it in Nim, not Python. For me getting started was not easy, I spent a few days on it, but now I have some basic understanding, and a tiny working example. For Python this one may help you: Reorder rows in a list (GTK4) - #4 by SoongNoonien Other examples in C are Gtk4-tutorial/sec26.md at main · ToshioCP/Gtk4-tutorial · GitHub and maybe GitHub - taozuhong/GtkColumnViewDemo: A GtkColumnView demo app. One problem for a compact layout is the big size of some ordinary widgets, like GtkSpinButton. I am currently avoiding them, using GtkText and GtkCheckButton only. For editing entries, I just had a discussion with the author of the C tutorial, see GtkListView with editable text examples for our books · Issue #33 · ToshioCP/Gtk4-tutorial · GitHub. I am connecting the GtkEntry to the “activate” signal currently, and assign an ID to each visible widget in the “bind” callback so that I can update my intern data when a user changes a widget state. And there are the blog posts of Mr. Lasen like Scalable lists in GTK 4 – GTK Development Blog and Gtk – 4.0: List Widget Overview. Maybe also see GtkColumnView is really not easy - #4 by ebassi :slight_smile:

I’m actually working on this right now. I’m porting Gtk3::Ex::DBI ( perl / gtk3, supports both ‘datasheet’ and ‘form’ style widgets ) to python and gtk4. I’m getting quite close, but I’m now confused about the gtk4-specific stuff - in particular around changes to the liststore and listview. In gtk3, we had to use GtkCellRenderer, but I see that this is now not required, and we can use ‘regular’ widgets. I have been looking for the past hour for a short example of doing this ( in python ), but I haven’t seen any yet - the tutorials I’ve found still use GtkCellRenderer. Anyway, I’m happy to collaborate on this.

Anyway, are there any examples around that pack regular gtk widgets into a listview?

You don’t “pack regular widgets” inside a list view: you set up a factory, and whenever the list view needs to create a row, it will ask the factory to create one; the list view will then ask the factory to bind the data from a row in the list model to the corresponding row in the list or grid.

List views work by “recycling” row widgets, so you don’t have direct access to the rows; rows are created on demand, and bound/unbound to the data in the model whenever it’s needed.

More information is available here:

There are examples in C for list widgets inside gtk4-demo. There are very few Python examples because the Python bindings don’t have the capability of creating GtkExpression objects. This limits the complexity of list views in Python. You can work around it by creating your own widget and object types, and doing direct property bindings, but it takes more code.

Hi @dkasak,

This project is very helpful to get started: GitHub - ToshioCP/Gtk4-tutorial: A gtk4 tutorial for beginners.
You may also have a look at my project as I am using a GtkColumnView and GtkExpression to sort columns: GitHub - vcottineau/pyGpxViewer: A simple gpx file viewer written in python

Regards,
@vcottineau

OK I’ve made reasonable progress - I have the model, column view column, and factory stuff in place, and I can see my data, displayed using label + entry widgets ( at this point ). I know that I need to do some extra work to handle user input - capture things after editing is complete, and update the underlying model. Does anyone have example code for gtk4? I have done all this in gtk3, but I don’t quite see how to make it work. I’ve tried connecting to the “editing-done” signal of ( eg ) the entry widget, but this doesn’t seem to fire.

For GtkEntry or GtkText we generally connect to the activate signal. I think GtkEditableLabel has only the changed signal. I have recently cleaned up my Nim GtkColumnView example a bit, for Python it may look similar:

import gintro/[gtk4, gobject, gio, glib, cairo]
import times

var qt = "GTKLV" & $epochTime()
if g_quark_try_string(qt) != 0:
  qt = "NGIQ" & $epochTime()
let CVquark: int = quark_from_static_string(qt) # caution, do not use name Quark!

const
  LayerNames = ["Ground", "Power", "Signal", "Remark"]

type
  LayerRow = object
    name: string
    style: string
    group: string
    locked: bool
    visible: bool

var
  layers = newSeq[LayerRow](LayerNames.len)

proc initLayers =
  for i, el in mpairs(layers):
    el.name = LayerNames[i]
    el.style = "default"
    el.group = "G"
    el.visible = true

type
  ColumnViewGObject = ref object of gobject.Object

proc onSelectionChanged(self: SelectionModel; pos: int; nItems: int) =
  echo "onSelectionChanged"
  #echo pos, nItems
  #echo typeof(cast[SingleSelection](self).getModel)
  echo "bbb ", self.getSelection.maximum

proc onLayerNameChanged(w: Text) =
  let row = cast[int](w.getQdata(CVquark))
  layers[row].name = w.text

proc onStyleNameChanged(w: Text) =
  let row = cast[int](w.getQdata(CVquark))
  layers[row].style = w.text

proc onGroupNameChanged(w: Text) =
  let row = cast[int](w.getQdata(CVquark))
  layers[row].group = w.text

proc onLockedChanged(w: CheckButton) =
  let row = cast[int](w.getQdata(CVquark))
  layers[row].locked = w.active
  echo layers

proc onVisibilityChanged(w: CheckButton) =
  let row = cast[int](w.getQdata(CVquark))
  layers[row].visible = w.active

proc draw(d: DrawingArea; cr: cairo.Context; w, h: int) =
  echo "draw", w, " ", h
  cr.setSource(1, 0, 0)
  cr.paint

proc setup0(f: SignalListItemFactory; item: ListItem) =
  let l = newLabel()
  item.setChild(l)

proc setup1(f: SignalListItemFactory; item: ListItem) =
  let l = newText()
  l.connect("activate", onLayerNameChanged)
  item.setChild(l)

proc setup2(f: SignalListItemFactory; item: ListItem) =
  let l = newText()
  l.connect("activate", onStyleNameChanged)
  item.setChild(l)

proc setup3(f: SignalListItemFactory; item: ListItem) =
  let l = newText()
  l.connect("activate", onGroupNameChanged)
  item.setChild(l)

proc setup4(f: SignalListItemFactory; item: ListItem) =
  let l = newCheckButton()
  l.connect("toggled", onLockedChanged)
  item.setChild(l)

proc setup5(f: SignalListItemFactory; item: ListItem) =
  let l = newCheckButton()
  l.connect("toggled", onVisibilityChanged)
  item.setChild(l)

proc setup6(f: SignalListItemFactory; item: ListItem) =
  let l = newDrawingArea()
  l.setContentWidth(4)
  l.setContentHeight(4)
  l.setDrawFunc(draw)
  item.setChild(l)

proc bind0(f: SignalListItemFactory; item: ListItem) =
  let l = Label(item.getChild())
  l.text = $item.getPosition

proc bind1(f: SignalListItemFactory; item: ListItem) =
  let l = Text(item.getChild())
  l.setQdata(CVquark, cast[pointer](item.getPosition))
  l.text = layers[item.getPosition].name

proc bind2(f: SignalListItemFactory; item: ListItem) =
  let l = Text(item.getChild())
  l.setQdata(CVquark, cast[pointer](item.getPosition))
  l.text = layers[item.getPosition].style

proc bind3(f: SignalListItemFactory; item: ListItem) =
  let l = Text(item.getChild())
  l.setQdata(CVquark, cast[pointer](item.getPosition))
  l.text = layers[item.getPosition].group

proc bind4(f: SignalListItemFactory; item: ListItem) =
  let l = CheckButton(item.getChild())
  l.setQdata(CVquark, cast[pointer](item.getPosition))
  l.setActive(layers[item.getPosition].locked)

proc bind5(f: SignalListItemFactory; item: ListItem) =
  let l = CheckButton(item.getChild())
  l.setQdata(CVquark, cast[pointer](item.getPosition))
  l.setActive(layers[item.getPosition].visible)

proc bind6(f: SignalListItemFactory; item: ListItem) =
  let l = DrawingArea(item.getChild())

# e.g. double click on item
proc onColumnViewActivate(cv: ColumnView, pos:int) =
  echo "onColumnViewActivate"

proc createLayersWidget: ScrolledWindow =
  initLayers()
  let cv = newColumnView()
  cv.setHexpand
  #cv.setSingleClickActivate(false)
  cv.addCssClass("data-table") # [.column-separators][.rich-list][.navigation-sidebar][.data-table]

  let c0 = newColumnViewColumn()
  c0.title = "#"
  let f0 = newSignalListItemFactory()
  f0.connect("setup", setup0)
  f0.connect("bind", bind0)
  c0.setFactory(f0)
  cv.appendColumn(c0)

  let c1 = newColumnViewColumn()
  c1.title = "Layer"
  let f1 = newSignalListItemFactory()
  f1.connect("setup", setup1)
  f1.connect("bind", bind1)
  c1.setFactory(f1)
  cv.appendColumn(c1)

  let c2 = newColumnViewColumn()
  c2.title = "Style"
  c2.setExpand
  let f2 = newSignalListItemFactory()
  f2.connect("setup", setup2)
  f2.connect("bind", bind2)
  c2.setFactory(f2)
  cv.appendColumn(c2)

  let c3 = newColumnViewColumn()
  c3.title = "Group"
  let f3 = newSignalListItemFactory()
  f3.connect("setup", setup3)
  f3.connect("bind", bind3)
  c3.setFactory(f3)
  cv.appendColumn(c3)

  let c4 = newColumnViewColumn()
  c4.title = "Lock"
  let f4 = newSignalListItemFactory()
  f4.connect("setup", setup4)
  f4.connect("bind", bind4)
  c4.setFactory(f4)
  cv.appendColumn(c4)

  let c5 = newColumnViewColumn()
  c5.title = "Vis."
  let f5 = newSignalListItemFactory()
  f5.connect("setup", setup5)
  f5.connect("bind", bind5)
  c5.setFactory(f5)
  cv.appendColumn(c5)

  let c6 = newColumnViewColumn()
  #c6.title = "Vis."
  let f6 = newSignalListItemFactory()
  f6.connect("setup", setup6)
  f6.connect("bind", bind6)
  c6.setFactory(f6)
  cv.appendColumn(c6)

  cv.connect("activate", onColumnViewActivate)

  let gtype = gObjectGetType()
  var listStore = gio.newListStore(gtype)

  for i in 0 .. LayerNames.high:
    let o = newObjectv(ColumnViewGObject, gtype, 0, nil)
    #o.name = Names[i]#.sample
    #o.age = rand(18 .. 95)
    listStore.append(o)

  let model = cast[SelectionModel](newSingleSelection(cast[ListModel](listStore)))
  model.connect("selection-changed", onSelectionChanged)
  cv.setModel(model)
  result = newScrolledWindow()
  result.setChild(cv)

proc activate(app: Application) =
  let window = newApplicationWindow(app)
  window.title = "GTK4 & Nim"
  window.defaultSize = (200, 200)
  window.setChild(createLayersWidget())
  window.present

proc main =
  let app = newApplication("org.gtk.example")
  connect(app, "activate", activate)
  discard run(app)

main()

Thanks for the response :slight_smile: Unfortunately, it is not clear at all how to handle the edited data from this example. When I connect to the “activate” signal of the entry in my factory, my handler only receives the entry itself. If I get the parent of the entry, it’s a GtkColumnViewCell - and beyond this, I don’t see:

let row = cast[int](w.getQdata(CVquark))

… but I have no idea what getQdata() - or any of the rest of it - is

We have discussed that in some detail lately, and that is why I posted the Nim example here. You should find all this when you read the discussions of the last two or three weeks, or the last issue of the GTK4 tutorial of GtkListView with editable text examples for our books · Issue #33 · ToshioCP/Gtk4-tutorial · GitHub. Of course we do not know for sure if our sulotion is the best one. Short recap: The problem is, that GtkColumnView and GtkListView “recycle” widgets, so widgets are reused to display different data. One solution is, to assign in the bind callback the ordinal number of the data row to the widget. I did it with setData(), which assigns a pointer to a widget – pointer is basically a integer number. I could have subclassed the widgets instead, so give them an additional integer field, and store the row index there. But as it was only one field, I just used setData. Note that SetQData() is an optimized form of SetData, you can use SetData() and avoid the use of a “Quark”. In the case that your Python bindings do not provide SetData(), just do subclassing, that should be working in Python. A very different approach would be property binding, which I have not tested. For GtkStringObject we could bind the string property to the text field of a GtkEntry. But when GtkStringObject can not be used as item of the Model, maybe because we want to use other data types, then we would have to create new GObjects with our own property sets. That may work in Python, but is currently not supported in Nim. I may write some more about all this in the Nim GTK4 book later, maybe in late 2023. Maybe until then I will have learned some more about GtkColumnView, GtkListView and the other one, I think it was called GtkDropDown. You may also see my recent post and the nice comment of someone here: GtkColumnView is really not easy - #4 by ebassi

I think there should be no need to use g_object_set_data() or g_object_set_qdata() in a Python application.

In C objects have a fixed struct so you can’t just add and associate arbitrary bits of data with it. Thus, GObject offers an internal dictionary that can be accessed with g_object_get_data() and g_object_set_data() (and the q variants), more or less doing what getattr() and setattr() do in Python.

Hi all. Thank you for ongoing assistance! I now have a proof-of-concept that can store changes back into the model. However this is only working via the entry’s “activate” signal, which happens when a user hits enter after entering some text. It’s common to use the tab key or mouse to switch focus out, without hitting enter. I used to connect to the “focus-out” signal of widgets to catch this, but there is no focus-out signal for most widgets in gtk4. There is a “move-focus” signal, but this doesn’t appear to fire.

<edit - I’m progressing slightly further still>

I see there is the “state-flags-changed” signal, which fires quite a lot. It receives a Gtk.StateFlags object. How can I reliably filter out other state changes, so I only get the state changes that I’m interested in? Some example states I’m seeing:

<flags GTK_STATE_FLAG_DIR_LTR | GTK_STATE_FLAG_FOCUS_WITHIN of type Gtk.StateFlags>
<flags GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_DIR_LTR | GTK_STATE_FLAG_FOCUS_WITHIN of type Gtk.StateFlags>
<flags GTK_STATE_FLAG_DIR_LTR | GTK_STATE_FLAG_FOCUS_WITHIN of type Gtk.StateFlags>

… etc

Sorry, can not help you with this problem, or with your other new question. Actually I am concerned with similar problems, but I can not spent a lot time on it currently. One possible solution is using the “changed” signal, which should fire always when a text of a widget is modified, or connection to the ::notify::text signals, see Gtk.Editable::changed. But that would give an update for every keystroke, which is not really desired, and it may not catch TAB or ESC at all. For some of my apps, including the SDT tool, I would like to have an activate signal, which is fired for ENTER, RETURN, ESC, TAB and maybe for some more. I played with that stuff for a few hours, also with GtkText and GtkEditableLabel, but gave up for now. I played with the various state flags as well but was confused. I am not sure if I already tried the editing-done signal, Gtk.CellEditable, maybe thre is some hope. People writing in pure C may copy all the widgets code and modify it, for some other programming languages that may be possible as well, but for Nim I would like to avoid that. Your app seems to be similar to the Gnumeric tool, I think 12 years ago they used GTK, but maybe the wrote their widgets from scratch.

When you should have some good success, it would be nice if you could write a blog post or something similar about that, as it would be helpful for people like me. And I may add a section about that topic to the Nim Gtk book then.

[EDIT]

Google pointed me to this one, pygtk - A GtkEntry signal emitting at focus change - Ask Ubuntu, mentioning the focus-out-event. But I guess it is still GTK2, and I have currently no time to test it.

[EDIT 2]

I forgot to mention the EventControllers, see Gtk.EventController.
I have used them only for a GtkDrawingArea, but maybe we can add them to a GtkEntry as well?

Hi @dkasak,

Not sure that it will help you but:

I am using button widget instead of entry widget. I guess that it’s working the same way.
In the factory callback, I pass the Gtk.ListItem object to the widget callback:

    @Gtk.Template.Callback()
    def _factory_setup_actions(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem) -> None:
        buttons = [
            {"icon": "mark-location-symbolic", "callback": self._on_button_view_clicked},
            # ...
        ]
        for button in buttons:
            action_button = Gtk.Button().new_from_icon_name(button["icon"])
            action_button.connect("clicked", button["callback"], list_item)
            # ...

In the button callback, I set the active row based on the Gtk.ListItem object and then I am able to work on the selected item object properties:

    def _on_button_view_clicked(self, button: Gtk.Button, list_item: Gtk.ListItem) -> None:
        selected_item = self._get_selected_item(list_item)

To set the active row:

    def _get_selected_item(self, list_item: Gtk.ListItem) -> Gtk.ListItem:
        position = list_item.get_position()
        self._single_selection.set_selected(position)
        return self._single_selection.get_selected_item()

By the way, you can get the Item property directly instead of converting the column name to a number:

    def _factory_bind(self, factory: Gtk.SignalListItemFactory, list_item: Gtk.ListItem, property_name: str) -> None:
        label = list_item.get_child()
        data = list_item.get_item()
        value = data.get_property(property_name)
        label.set_text(value)

Regards,
@vcottineau