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)