Sign up

Problems with TreeModel depth > 1

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)

Re: Problems with TreeModel depth > 1

On Fri, 05 Jan 2018 16:12:22 GMT, Luís Marques wrote:

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][1] 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.

I meant GtkTreeIter* vs GtkTreeIter**, obviously.

Re: Problems with TreeModel depth > 1

On 05-01-18 17:14, Luís Marques wrote:

On Fri, 05 Jan 2018 16:12:22 GMT, Luís Marques wrote:

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][1] 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.

I meant GtkTreeIter* vs GtkTreeIter**, obviously.

I've found two issues with gtkd.Implement, and with those fixed things seem to work.

For a7456b5 a null check was missing for the returned object, causing a segfault when null was returned from getPath.

For 143fe42 In the case where an out TreeIter was used the generated code would set to pointer passed to the wrapper function to the adress of the new TreeIter instead of setting the data in the TreeIter.

For the GtkTreeIter* vs GtkTreeIter** the iters are marked with out because they are marked as such in the GObject introspection files.
Most of the time out parameters are indeed defined as GtkTreeIter** but it shouldn't matter much as the out documents that you will get an empty iter passed in that parameter. Tough the generated code should handle that case correctly which it wasn't.

Re: Problems with TreeModel depth > 1

On Fri, 05 Jan 2018 19:30:59 GMT, Mike Wey wrote:

I've found two issues with gtkd.Implement, and with those fixed things seem to work.

For a7456b5 a null check was missing for the returned object, causing a segfault when null was returned from getPath.

For 143fe42 In the case where an out TreeIter was used the generated code would set to pointer passed to the wrapper function to the adress of the new TreeIter instead of setting the data in the TreeIter.

For the GtkTreeIter* vs GtkTreeIter** the iters are marked with out because they are marked as such in the GObject introspection files.
Most of the time out parameters are indeed defined as GtkTreeIter** but it shouldn't matter much as the out documents that you will get an empty iter passed in that parameter. Tough the generated code should handle that case correctly which it wasn't.

I was using version ~master for those fixes. I saw that gtk-d had been update on 2018-Feb-28, so I tried upgrading to that version, 3.7.4. But version 3.7.4 still seems to have these bugs. I get a wrong stamp value in iterHasChild. If I ignore that issue I get a crash.

Re: Problems with TreeModel depth > 1

On 01-03-18 16:13, Luís Marques wrote:

I was using version ~master for those fixes. I saw that gtk-d had been update on 2018-Feb-28, so I tried upgrading to that version, 3.7.4. But version 3.7.4 still seems to have these bugs. I get a wrong stamp value in iterHasChild. If I ignore that issue I get a crash.

I've added those fixes to the 3.7 branch, and released 3.7.5.

Re: Problems with TreeModel depth > 1

On Thu, 1 Mar 2018 19:47:56 +0100, Mike Wey wrote:

On 01-03-18 16:13, Luís Marques wrote:
I've added those fixes to the 3.7 branch, and released 3.7.5.

Thanks!