I am using a GtkColumnView
to render a table with interactive cells. Some columns render a CheckButton
, other columns render an Entry
. For simplicity, let’s assume that it’s all entries.
After learning about the widget API in the docs, the way I’ve setup my ColumnView
is to use the model
property to set a SelectionModel
bound to a ListModel
. (So, roughly speaking, something similar to the following pseudocode)
column_view.set_model(new Gtk.NoSelection(list_model));
This allows me to map every item of my ListModel with a row if I use multiple ColumnViewColumn
instances, with a SignalListItemFactory
to provide callbacks to setup and bind each column so that it renders properly, backed by the item itself from the ListModel.
And this works perfectly fine when interacting with a mouse. Click a cell, change the value or enable the checkbox, and things work. Even more, with some property bindings, updates made to the widget can update the different items of the ListModel.
However, I am having a hard time trying to understand the default tab cycling model of GtkColumnView. My ideal goal is to leverage as much as possible from the default GTK behaviours. I don’t want to do clever things with widget focus if it’s not needed, because the risk of introducing accessibility issues while just trying to provide a good keyboard UX is real (if webdev has taught me one thing over the years…).
With the default behaviour, if the focus is on a widget that is NOT the last one in the column, pressing Tab works as expected, moving focus to the next cell in the row.
However, when the focus is in the last widget of a row, pressing Tab seems to move the focus one cell below, but keeping the same column, instead of going back to the first column of the next row.
I’ve made the following test case in Vala. (Not a language that I know a lot, but I wanted to make a minimum working example where this issue can be replicated.)
int main(string[] args) {
var app = new Gtk.Application("es.danirod.ColumnDemo", GLib.ApplicationFlags.DEFAULT_FLAGS);
app.activate.connect(() => {
var model = new Gtk.StringList({ "monday", "tuesday", "wednesday" });
var column_view = new Gtk.ColumnView(new Gtk.NoSelection(model));
column_view.tab_behavior = Gtk.ListTabBehavior.ITEM;
column_view.append_column(new Gtk.ColumnViewColumn("raw", raw_factory()));
column_view.append_column(new Gtk.ColumnViewColumn("up()", up_factory()));
column_view.append_column(new Gtk.ColumnViewColumn("length", len_factory()));
var win = new Gtk.ApplicationWindow(app);
win.set_child(column_view);
win.present();
});
return app.run(args);
}
// Creates a SignalListItemFactory that simply copies the string in the
// ListItem item as text in a GtkEntry.
Gtk.ListItemFactory raw_factory() {
var raw_factory = new Gtk.SignalListItemFactory();
raw_factory.setup.connect((obj) => {
var item = obj as Gtk.ListItem;
var entry = new Gtk.Entry();
item.set_child(entry);
});
raw_factory.bind.connect((obj) => {
var item = obj as Gtk.ListItem;
var entry = item.child as Gtk.Entry;
var value = item.item as Gtk.StringObject;
entry.set_text(value.get_string());
});
return raw_factory;
}
// Converts the text in the ListItem item to uppercase and puts it in an Entry
Gtk.ListItemFactory up_factory() {
var up_factory = new Gtk.SignalListItemFactory();
up_factory.setup.connect((obj) => {
var item = obj as Gtk.ListItem;
var entry = new Gtk.Entry();
item.set_child(entry);
});
up_factory.bind.connect((obj) => {
var item = obj as Gtk.ListItem;
var entry = item.child as Gtk.Entry;
var value = item.item as Gtk.StringObject;
entry.set_text(value.get_string().up());
});
return up_factory;
}
// A more complex case where I place something not obvious into the Entry
// that also depends on whatever is in the StringObject part of the ListItem.
Gtk.ListItemFactory len_factory() {
var len_factory = new Gtk.SignalListItemFactory();
len_factory.setup.connect((obj) => {
var item = obj as Gtk.ListItem;
var entry = new Gtk.Entry();
item.set_child(entry);
});
len_factory.bind.connect((obj) => {
var item = obj as Gtk.ListItem;
var entry = item.child as Gtk.Entry;
var value = item.item as Gtk.StringObject;
entry.set_text(@"$(value.get_string().length)");
});
return len_factory;
}
Pressing tab a few times when the focus is on the first widget of the first row seems to trigger this behaviour. I can cycle through the widgets of the first row, but after the last widget of the first row, the focus is moved to the last widget of the second row, not the first widget of the first row.
Is this the correct behaviour for this widget? If it is working as expected, what options do I have if I wanted to tweak how the focus works.