I have ran into problems when I changed my custom TreeModel from a flat list to a tree. To debug this issue, I created a model for a tree with 2 items: "0" and "1". "1" is a child of "0", and "0" is a child of the invisible root node. So each level only has one item, and the tree has a depth of 2. My iterators store an integer value indicating if we are pointing to either "0" or "1".
The "0" node shows correctly, but my program misbehaves when I try to expand that node to show the "1".
Where and how the problem manifests depends on how exactly you deal with the first unexpected value. In my case, I get an unexpected stamp (value 0) in iterHasChild
and iterNext
. If I attempt to proceed (e.g. getValue
returns a value even for an iterator with an invalid stamp) the program eventually crashes.
I have checked that I always set my stamps when I create or modify an iterator (either stamp=42
for a valid iterator, or -1
for an invalid one).
One thing that surprised me, and I wonder if it might be related, is an apparent inconsistency between Gtk and GtkD, or between various GtkD methods of TreeModel: All of the methods listed in the Gtk documentation for TreeModel that deal with setting iterators have parameters with type GtkTreeIter*
-- that is, TreeIter
. On the other hand, on the GtkD side, we have getIter
, iterNext
, and iterNChildren
which take a TreeIter
(AKA GtkTreeIter*
) and iterChildren
, iterNthChild
, iterParent
which take an out TreeIter
(AKA GtkTreeIter*
). I wonder why this inconsistency exists, and if it might be related.
Here is my program with several printfs and asserts to show and clarify this issue:
class CustomList : ObjectG, TreeModelIF
{
GType[1] columnTypes;
enum stamp = 42;
mixin ImplementInterface!(GObject, GtkTreeModelIface);
mixin TreeModelT!(GtkTreeModel);
public this()
{
super(getType(), null);
columnTypes[0] = GType.STRING;
}
override GtkTreeModelFlags getFlags()
{
return GtkTreeModelFlags.ITERS_PERSIST;
}
override int getNColumns()
{
return columnTypes.length;
}
override GType getColumnType(int index)
{
if(index >= columnTypes.length || index < 0)
return GType.INVALID;
return columnTypes[index];
}
override int getIter(TreeIter iter, TreePath path)
{
int depth = path.getDepth();
assert(depth > 0);
if(depth > 2)
{
writeln("getIter depth > 2");
iter.stamp = -1;
return false;
}
auto indices = path.getIndices();
auto id = depth-1;
iter.stamp = stamp;
iter.setUserData!int(id);
writefln("getIter depth:%s indices:%s = %s", depth, indices, id);
return true;
}
override TreePath getPath(TreeIter iter)
{
if(iter is null || iter.stamp != stamp)
{
writeln("getPath ", iter is null ? "null iter" :
"invalid stamp " ~ iter.stamp.to!string);
return null;
}
auto id = iter.getUserData!int;
auto path = new TreePath(0.repeat(id+1).array);
auto indices = path.getIndices();
writefln("getPath for id %s is %s", id, indices);
return path;
}
override Value getValue(TreeIter iter, int column, Value value = null)
{
if(value is null)
value = new Value();
value.init(columnTypes[column]);
if(iter is null || iter.stamp != stamp || column >= columnTypes.length)
{
if(iter is null)
writeln("getValue null iter");
else if(iter.stamp != stamp)
writeln("getValue invalid stamp ", iter.stamp);
else
writeln("getValue invalid column");
//return null;
value.setString("ERROR");
return value;
}
assert(iter.userData !is null);
auto id = iter.getUserData!int;
if(id > 1)
{
writeln("getValue invalid id ", id);
//return null;
value.setString("ERROR");
return value;
}
switch(column)
{
case 0:
value.setString(id.to!string);
break;
default:
assert(0);
}
writefln("getValue %s = '%s'", id, value.getString);
return value;
}
override bool iterNext(TreeIter iter)
{
if(iter is null || iter.stamp != stamp)
{
if(iter is null)
writeln("iterNext for null iter = false");
else
{
if(iter.userData is null)
writefln("iterNext for iter with stamp %s and null userData = false", iter.stamp);
else
writefln("iterNext for iter with stamp %s and userData %s = false", iter.getUserData!int);
}
iter.stamp = -1;
return false;
}
assert(iter.userData !is null);
auto id = iter.getUserData!int;
writefln("iterNext %s = false", id);
iter.stamp = -1;
return false;
}
override bool iterChildren(out TreeIter iter, TreeIter parent)
{
iter = new TreeIter();
iter.stamp = stamp;
if(parent is null)
{
iter.setUserData!int(0);
writeln("iterChildren null = 0");
}
else
{
assert(parent.userData !is null);
auto id = parent.getUserData!int;
if(id > 0)
{
writeln("asked for children of id %s", id);
iter.stamp = -1;
return false;
}
iter.setUserData!int(1);
writefln("iterChildren %s = 1", id);
}
return true;
}
override bool iterHasChild(TreeIter iter)
{
if(iter is null)
{
writeln("iterHasChild null = true");
return true;
}
else
{
if(iter.stamp != stamp)
{
if(iter.userData is null)
writefln("iterHasChild with invalid stamp %s and null userData = false", iter.stamp);
else
writefln("iterHasChild with invalid stamp %s and userData %s = false", iter.stamp, iter.getUserData!int);
return false;
}
assert(iter.userData !is null);
auto id = iter.getUserData!int;
writefln("iterHasChild %s = %s", id, id == 0);
return id == 0;
}
}
override int iterNChildren(TreeIter iter)
{
if(iter is null)
{
writeln("iterNChildren null = 1");
return 1;
}
assert(iter.userData !is null);
auto id = iter.getUserData!int;
auto num = id == 0 ? 1 : 0;
writefln("iterNChildren %s = %s", id, num);
return num;
}
override bool iterNthChild(out TreeIter iter, TreeIter parent, int n)
{
iter = new TreeIter();
iter.stamp = stamp;
if(n > 0)
{
writefln("iterNthChild: asked for child %s", n);
iter.stamp = -1;
return false;
}
if(parent is null)
{
iter.setUserData!int(0);
}
else
{
assert(parent.userData !is null);
auto id = parent.getUserData!int;
if(id > 0)
{
writefln("iterNthChild: asked for child %s of id %s", n, id);
iter.stamp = -1;
return false;
}
iter.setUserData!int(1);
}
writefln("iterNthChild %s/%s = %s", parent, n, iter.getUserData!int);
return true;
}
override bool iterParent(out TreeIter iter, TreeIter child)
{
writeln("iterParent");
iter = new TreeIter();
iter.stamp = stamp;
if(child is null)
{
iter.stamp = -1;
return false;
}
assert(child.userData !is null);
auto id = child.getUserData!int;
assert(id == 0 || id == 1);
if(id == 0)
{
iter.stamp = -1;
return false;
}
else
{
iter.setUserData!int(0);
return true;
}
}
}
Here is the output when I run my program and I expand "0" to try to show "1":
getIter depth:1 indices:[0] = 0
iterHasChild 0 = true
iterNext 0 = false
getIter depth:1 indices:[0] = 0
getValue 0 = '0'
getPath for id 0 is [0]
getIter depth:1 indices:[0] = 0
iterNthChild null/0 = 0
getIter depth:1 indices:[0] = 0
getValue 0 = '0'
getPath for id 0 is [0]
getValue 0 = '0'
getPath for id 0 is [0]
getIter depth:1 indices:[0] = 0
getValue 0 = '0'
getPath for id 0 is [0]
getValue 0 = '0'
getPath for id 0 is [0]
getIter depth:1 indices:[0] = 0
getIter depth:1 indices:[0] = 0
iterNthChild null/0 = 0
getIter depth:1 indices:[0] = 0
getValue 0 = '0'
getPath for id 0 is [0]
getValue 0 = '0'
getPath for id 0 is [0]
getIter depth:1 indices:[0] = 0
iterHasChild 0 = true
iterHasChild 0 = true
iterChildren 0 = 1
iterHasChild with invalid stamp 0 and null userData = false
iterNext for iter with stamp 0 and null userData = false
getIter depth:1 indices:[0] = 0
iterChildren 0 = 1
getValue invalid stamp 0
getPath invalid stamp 0
(segfault)