List and Tree Widgets

In version 4, GTK introduced new widgets for efficiently displaying table-like data as one-dimensional lists, trees, or two-dimensional arrays.

GtkListView

Example

The code below can be found in "listview.jl" in the "examples" subdirectory.

We start with the widget for displaying one-dimensional lists. Here is a simple example:

using Gtk4

model = GtkStringList(string.(names(Gtk4)))
selmodel = GtkSelectionModel(GtkSingleSelection(GListModel(model)))

function setup_cb(f, list_item)  # here `f` is the item factory
    set_child(list_item, GtkLabel(""))
end

function bind_cb(f, list_item)
    text = list_item[].string
    label = get_child(list_item)
    label.label = text
end

factory = GtkSignalListItemFactory(setup_cb, bind_cb)
list = GtkListView(selmodel, factory)

win = GtkWindow("Listview demo", 250, 800)
sw = GtkScrolledWindow()
win[] = sw
sw[] = list

Let's go through each step. First, we create a model that holds the data we want to display. In this example we display a list of strings (all of the names exported by the Gtk4 module, which was 1053 strings when this was written), and we store them in a GtkStringList. This object implements the interface GListModel, which stores an ordered list of GObjects.

Next, we create a "selection model", which wraps the model we just created and controls how the user can select items in the list. Possible wrappers include GtkNoSelection (no selection allowed), GtkSingleSelection (a single item can be selected), and GtkMultiSelection (multiple items can be selected).

Next, a "factory" is created. The list widget can efficiently display a huge number of items by only populating display widgets for an item when the item becomes visible. The "factory" is what does this. The constructor takes two callback functions: "setup", which creates a suitable widget for displaying an item, and "bind", which sets the widget to display a particular item. The arguments of the callbacks are the factory f (often unused) and a GtkListItem list_item, which is an object that holds pointers to elements of the GListModel (the "item") and the widget (the "child").

In the "setup" callback, we use set_child to set a new GtkLabel as the GtkListItem's child widget. If you need to set up signals for the widget that don't depend on the item, this is where you would typically do that. This callback is called when the ListView widget is created (or resized, presumably), unlike the "bind" callback, which is called multiple times as the user scrolls through the list.

In the "bind" callback, we first fetch the element of the list model that will be displayed, using the getindex method on the list item (by calling list_item[]). Each string in GtkStringList is stored inside a GtkStringObject. The string can be accessed using the "string" object (i.e. obj.string) or using an accessor (i.e. string(obj)). We then fetch the text from its "string" property. We then get the GtkLabel using the get_child function on the list item, and finally we set the text of this GtkLabel.

Finally, we construct the GtkListView using the selection model and the factory and add it to a GtkScrolledWindow and a GtkWindow.

Filtering

Example

The code below can be found in "filteredlistview.jl" in the "examples" subdirectory.

The list above is very long, and it's useful to allow the user to filter it down. An easy way to implement this is to use GtkFilterListModel, which wraps the model and allows it to be filtered before being displayed.

Here is an example:

model = GtkStringList(string.(names(Gtk4)))

entry = GtkSearchEntry()

function match(item)
  return startswith(item.string, entry.text)
end

filter = GtkCustomFilter(match)
filteredModel = GtkFilterListModel(GLib.GListModel(model), filter)
selmodel = GtkSelectionModel(GtkSingleSelection(GListModel(filteredModel)))

We create a GtkCustomFilter using a match callback that returns true for items that we want to display, in our case those that start with the text entered by the user in entry. We construct a GtkFilterListModel using this filter and use it instead of the GListModel in the constructor for GtkSingleSelection.

Finally, we update the filter when the user changes the text by connecting to the "search-changed" signal:

signal_connect(entry, :search_changed) do w
  @idle_add Gtk4.changed(filter, Gtk4.FilterChange_DIFFERENT) 
end

Sorting

Example

The code below can be found in "sortedlistview.jl" in the "examples" subdirectory.

It's also useful to be able to sort the list. This can be done using another model wrapper, GtkSortListModel.

Here is an example that sorts the list in reverse alphabetical order:

model = GtkStringList(string.(names(Gtk4)))

ralpha_compare(item1, item2) = isless(item1.string, item2.string) ? 1 : 0

sorter = GtkCustomSorter(match)
sortedModel = GtkSortListModel(GListModel(model), sorter)

We create a GtkCustomSorter using a compare callback that takes two arguments item1 and item2 and returns -1 if item1 is before item2, 0 if they are equal, and 1 if item1 is after item2. We construct a GtkSortListModel using this filter and use it instead of the GListModel in the constructor for GtkSingleSelection.

GtkColumnView

What if we want to display information in columns? Let's say we want to have one column show the name of the function and another show the number of methods. For this we can use GtkColumnView. It works very similarly to GtkListView, but instead of having one factory for the entire widget, each column has a factory whose setup and bind callbacks populate the widgets used to display the information for that column.

Here is an example:

using Gtk4

win = GtkWindow("ColumnView demo", 450, 800)
sw = GtkScrolledWindow()
push!(win, sw)

model = GtkStringList(string.(names(Gtk4)))

function setup_cb(f, li)
    set_child(li,GtkLabel(""))
end

function bind_cb(f, li)
    text = li[].string
    label = get_child(li)
    label.label = text
end

function bind2_cb(f, li)
    text = li[].string
    label = get_child(li)
    label.label = string(length(methods(eval(Symbol(text)))))
end

list = GtkColumnView(GtkSelectionModel(GtkSingleSelection(GListModel(model))))

factory1 = GtkSignalListItemFactory(setup_cb, bind_cb)
col1 = GtkColumnViewColumn("name", factory1)
push!(list, col1)

factory2 = GtkSignalListItemFactory(setup_cb, bind2_cb)
col2 = GtkColumnViewColumn("methods", factory2)
push!(list, col2)

sw[] = list

Note that filtering and sorting work just the same as with GtkListView since they operate on the model.

Example

An example of using GtkColumnView with filtering and sorting can be found in "columnview.jl" in the "examples" subdirectory.

GtkTreeView

The GtkTreeView was the widget used to display table-like or hierarchical data and trees in version 3 of GTK. It's also present in version 4 but is being deprecated in the C library in favor of the widgets discussed above. It will continue to be supported in Gtk4.jl.

List Store

This widget uses dedicated GtkListStore and GtkTreeStore containers to hold the data.

Lets start with a very simple example: A table with two columns representing the name and age of a person. Each column must have a specific type. We initialize the list store using

ls = GtkListStore(String, Int)

Now we will the store with data

push!(ls,("Peter",20))
push!(ls,("Paul",30))
push!(ls,("Mary",25))

If we want so insert the data at a specific position we can use the insert function

insert!(ls, 2, ("Susanne", 35))

You can use ls like a matrix like container. Calling length and size will give you

julia> length(ls)
4

julia> size(ls)
(4,2)

Specific element can be be accessed using

julia> ls[1,1]
"Peter"
julia> ls[1,1] = "Pete"
"Pete"

Displaying a list

Now we actually want to display our data. To this end we create a tree view object

tv = GtkTreeView(GtkTreeModel(ls))

Then we need specific renderers for each of the columns. Usually you will only need a text renderer, but in our example we want to display the boolean value using a checkbox.

rTxt = GtkCellRendererText()
rTog = GtkCellRendererToggle()

Finally we create for each column a TreeViewColumn object

c1 = GtkTreeViewColumn("Name", rTxt, Dict([("text",0)]))
c2 = GtkTreeViewColumn("Age", rTxt, Dict([("text",1)]))

We need to push these column description objects to the tree view

push!(tv, c1, c2)

Then we can display the tree view widget in a window

win = GtkWindow(tv, "List View")

If you prefer that the columns are resizable by the user call

for c in [c1, c2]
    Gtk4.resizable(c, true)
end

Sorting

We next want to make the tree view sortable

for (i,c) in enumerate([c1,c2])
  Gtk4.sort_column_id(c,i-1)
end

If you now click on one of the column headers, the data will be sorted with respect to the selected column. You can even make the columns reorderable

for (i,c) in enumerate([c1, c2])
    Gtk4.reorderable(c, true)
end

Selection

Usually the interesting bit of a list will be the entry being selected. This is done using an additional GtkTreeSelection object that can be retrieved by

selection = Gtk4.selection(tv)

One either have single selection or multiple selections. We toggle this by calling

Gtk4.mode(selection,Gtk4.SelectionMode_MULTIPLE)

For single selection, if we want to know the index of the selected item we can use

julia> ls[selected(selection),1]
"Pete"

For multiple selection, we can get a list of selected rows using selected_rows, which can be used to index the GtkListStore

julia> [ls[x,1] for x in selected_rows(selection)]
3-element Vector{String}:
 "Susanne"
 "Pete"
 "Paul"

Since it can happen that no item has been selected at all, it is a good idea to put this into an if statement

if hasselection(selection)
  # do something with selected(selection)
end

Sometimes you want to invoke an action of an item is selected. This can be done by

signal_connect(selection, "changed") do widget
  if hasselection(selection)
    currentIt = selected(selection)

    # now you can to something with the selected item
    println("Name: ", ls[currentIt,1], " Age: ", ls[currentIt,1])
  end
end

Another useful signal is "row-activated" that will be triggered by a double click of the user.

Filtering

A very useful thing is to apply a filter to a list view such that only a subset of data is shown. We can do this using the GtkTreeModelFilter type. It is as the GtkListStore a GtkTreeModel and therefore we can assign it to a tree view. So the idea is to wrap a GtkListStore in a GtkTreeModelFilter and assign that to the tree view.

Next question is how to decide which row of the list store should be shown and which shouldn't. We will do this by adding an additional column to the list store that is hidden. The column will be of type Bool and a value true indicates that the entry is to be shown while false indicates the opposite. We make the filtering based on this column by a call to Gtk4.visible_column. The full example now looks like this:

using Gtk4

ls = GtkListStore(String, Int, Bool, Bool)
push!(ls,("Peter",20,false,true))
push!(ls,("Paul",30,false,true))
push!(ls,("Mary",25,true,true))
insert!(ls, 2, ("Susanne",35,true,true))

rTxt = GtkCellRendererText()
rTog = GtkCellRendererToggle()

c1 = GtkTreeViewColumn("Name", rTxt, Dict([("text",0)]), sort_column_id=0)
c2 = GtkTreeViewColumn("Age", rTxt, Dict([("text",1)]), sort_column_id=1)

tmFiltered = GtkTreeModelFilter(ls)
Gtk4.visible_column(tmFiltered,3)
tv = GtkTreeView(GtkTreeModel(tmFiltered))
push!(tv, c1, c2)

selection = Gtk4.selection(tv)

signal_connect(selection, "changed") do widget
  if hasselection(selection)
    currentIt = selected(selection)

    println("Name: ", GtkTreeModel(tmFiltered)[currentIt,0],
            " Age: ", GtkTreeModel(tmFiltered)[currentIt,1])
  end
end

ent = GtkEntry()

signal_connect(ent, "changed") do widget
  searchText = get_gtk_property(ent, :text, String)

  for l=1:length(ls)
    showMe = true

    if length(searchText) > 0
      showMe = showMe && occursin(lowercase(searchText), lowercase(ls[l,1]))
    end

    ls[l,4] = showMe
  end
end

vbox = GtkBox(:v)
push!(vbox,ent,tv)

win = GtkWindow(vbox, "List View with Filter")

You can see that we have added a little search bar such that you can see the filtering in action. It is furthermore important to note that we had to replace ls with GtkTreeModel(tmFiltered) in the selection changed callback since the selection will give an iterator that is only valid in the filtered tree model.

Tree Widget

Here is an example of the tree model in action:

using Gtk4

ts = GtkTreeStore(String)
iter1 = push!(ts,("one",))
iter2 = push!(ts,("two",),iter1)
iter3 = push!(ts,("three",),iter2)
tv = GtkTreeView(GtkTreeModel(ts))
r1 = GtkCellRendererText()
c1 = GtkTreeViewColumn("A", r1, Dict([("text",0)]))
push!(tv,c1)
win = GtkWindow(tv, "Tree View")

iter = Gtk4.iter_from_index(ts, [1])
ts[iter,1] = "ONE"