TreeListStore exception due to wrong item typing #349

Closed
opened 2026-04-15 17:31:23 +02:00 by mrlem · 7 comments

Originally logged on github, relogged here for convenience.

Hi 👋

Here's a minimal test case showcasing the error:

private val strings = listOf(
    "plip",
    "plap",
    "plop",
)
private val listModel = ListStore<StringObject>().apply {
    strings.forEach {
        append(StringObject(it))
    }
}
private val treeModel = TreeListModel<StringObject>(listModel, false, false) { parent ->
    return@TreeListModel null
}

fun main(args: Array<String>) {
    // runs ok
    val listItem = listModel.getItem(0)
    println("list item #0: $listItem")

    // crashes at runtime with a class cast exception:
    //  compile-time type is StringObject, runtime type is TreeListRow
    val treeItem = treeModel.getItem(0)
    println("tree item #0: $treeItem")
}

I'm assuming the issue derives from the fact that TreeListModel<T> extends ListModel<T>, when it should instead derive from ListModel<TreeListRow>. Less handy in terms of generics, but I guess that's what the API allows for now (unless I miss something).

Note: the GTK doc mentions that it extends ListModel, but not the type of the items


Did some more investigation: it turns out this code behaves perfectly when creating a TreeListModel with passthrough parameter true.

So the typing is only wrong in case you use passthrough = false (like I did).

Maybe the TreeListModel.getItem(int) method could just use:

  • super.getItem(int) if passthrough is true
  • getRow(int).getItem() if passthrough is false

What do you think?

Originally logged on [github](https://github.com/jwharm/java-gi/issues/336), relogged here for convenience. Hi 👋 Here's a minimal test case showcasing the error: ```kotlin private val strings = listOf( "plip", "plap", "plop", ) private val listModel = ListStore<StringObject>().apply { strings.forEach { append(StringObject(it)) } } private val treeModel = TreeListModel<StringObject>(listModel, false, false) { parent -> return@TreeListModel null } fun main(args: Array<String>) { // runs ok val listItem = listModel.getItem(0) println("list item #0: $listItem") // crashes at runtime with a class cast exception: // compile-time type is StringObject, runtime type is TreeListRow val treeItem = treeModel.getItem(0) println("tree item #0: $treeItem") } ``` I'm assuming the issue derives from the fact that `TreeListModel<T>` extends `ListModel<T>`, when it should instead derive from `ListModel<TreeListRow>`. Less handy in terms of generics, but I guess that's what the API allows for now (unless I miss something). Note: the [GTK doc](https://docs.gtk.org/gtk4/class.TreeListModel.html) mentions that it extends ListModel, but not the type of the items --- Did some more investigation: it turns out this code behaves perfectly when creating a TreeListModel with passthrough parameter `true`. So the typing is only wrong in case you use `passthrough = false` (like I did). Maybe the `TreeListModel.getItem(int)` method could just use: * `super.getItem(int)` if `passthrough` is `true` * `getRow(int).getItem()` if `passthrough` is `false` What do you think?
Owner

Thanks for re-logging this here 🙂

The docs of the passthrough property says: "If FALSE, the GListModel functions for this object return custom GtkTreeListRow objects. If TRUE, the values of the child models are pass through unmodified."

So I think it's supposed to return a TreeListRow, and it's not correct to declare the model as TreeListModel<StringObject>.

Thanks for re-logging this here 🙂 The [docs](https://docs.gtk.org/gtk4/property.TreeListModel.passthrough.html) of the `passthrough` property says: "If FALSE, the GListModel functions for this object return custom GtkTreeListRow objects. If TRUE, the values of the child models are pass through unmodified." So I think it's supposed to return a TreeListRow, and it's not correct to declare the model as `TreeListModel<StringObject>`.
Author

Ok, indeed this is a misusage, that makes sense, thanks.
So I guess there's nothing to fix, aside maybe from offering more guidance in the exception?

Ok, indeed this is a misusage, that makes sense, thanks. So I guess there's nothing to fix, aside maybe from offering more guidance in the exception?
Owner

Hmm not sure how to do that… Suggestions?

Hmm not sure how to do that… Suggestions?
Author

Adding generation of a TreeListModel getItem method like:

@Override                                                                                                                                                                                                            
public T getItem(int position) {                                                                                                                                                                                     
    if (!getPassthrough()) {                                                                                                                                                                                         
        throw new IllegalStateException(                                                                                                                                                                             
            "When passthrough=false, TreeListModel returns TreeListRow objects. "                                                                                                                                    
            + "Use getRow(position) to access items. "                                                                                                                                                               
            + "Alternatively, ensure passthrough=true if you expect T directly.");                                                                                                                                   
    }                                                                                                                                                                                                                
    return super.getItem(position);                                                                                                                                                                                  
}   

What do you think? or UnsupportedOperationException maybe?
Because indeed, the usage of getItem doesn't make sense in the context of a non-passthrough model.

Adding generation of a TreeListModel getItem method like: ```java @Override public T getItem(int position) { if (!getPassthrough()) { throw new IllegalStateException( "When passthrough=false, TreeListModel returns TreeListRow objects. " + "Use getRow(position) to access items. " + "Alternatively, ensure passthrough=true if you expect T directly."); } return super.getItem(position); } ``` What do you think? or UnsupportedOperationException maybe? Because indeed, the usage of getItem doesn't make sense in the context of a non-passthrough model.
Owner

I'm keeping this open as an enhancement.
Currently it's really difficult in Java-GI to override functions with a custom implementation, and I have ideas to improve that, so it becomes easier to generate these kind of checks.

I'm keeping this open as an enhancement. Currently it's really difficult in Java-GI to override functions with a custom implementation, and I have ideas to improve that, so it becomes easier to generate these kind of checks.
Owner

@mrlem I studied the documentation of TreeListModel a bit more, and I don't think the proposed check is correct. Creating a TreeListModel<TreeListRow> with passtrough=false and then retrieving TreeListRows with getItem() is a valid usecase. (If it wasn't a valid usecase, the passtrough property would be useless and would not be part of the API.)

I tried to build a check that ensures passtrough=false when the list model is not a TreeListModel<TreeListRow>, but haven't been succesful. Java's type erasure makes it impossible to determine inside getItem() whether the returned type is correct. For example, this doesn't do anything:

@Override
public T getItem(int position) {
    try {
        return ListModel.super.getItem(position);
    } catch (ClassCastException err) {
        throw new ClassCastException(err.getMessage() + " Ensure the 'passtrough' property is set correctly.");
    }
}

This doesn't do anything because getItem() actually doesn't throw anything. The ClassCastException occurs at the call site, in your own code. Inside the getItem() method, the generic type is erased and no ClassCastException occurs. (Even an explicit cast like (T) super.getItem() didn't throw.)

The only thing I can think of, is to add Javadoc warnings to TreeListModel's constructor, to clarify the danger of setting passtrough=false. Would that have prevented the issue for you, or do you expect that most people will not read the javadoc?

@mrlem I studied the documentation of TreeListModel a bit more, and I don't think the proposed check is correct. Creating a `TreeListModel<TreeListRow>` with `passtrough=false` and then retrieving TreeListRows with `getItem()` is a valid usecase. (If it wasn't a valid usecase, the `passtrough` property would be useless and would not be part of the API.) I tried to build a check that ensures `passtrough=false` when the list model is not a `TreeListModel<TreeListRow>`, but haven't been succesful. Java's type erasure makes it impossible to determine inside `getItem()` whether the returned type is correct. For example, this doesn't do anything: ```java @Override public T getItem(int position) { try { return ListModel.super.getItem(position); } catch (ClassCastException err) { throw new ClassCastException(err.getMessage() + " Ensure the 'passtrough' property is set correctly."); } } ``` This doesn't do anything because `getItem()` actually doesn't throw anything. The ClassCastException occurs at the call site, in your own code. Inside the `getItem()` method, the generic type is erased and no ClassCastException occurs. (Even an explicit cast like `(T) super.getItem()` didn't throw.) The only thing I can think of, is to add Javadoc warnings to TreeListModel's constructor, to clarify the danger of setting `passtrough=false`. Would that have prevented the issue for you, or do you expect that most people will not read the javadoc?
Author

I see your point. Maybe we could add a javadoc warning in the constructor as you mentionned, and add one in getItem() too? (as it's the likely place it will crash: a @throws clause?)

I see your point. Maybe we could add a javadoc warning in the constructor as you mentionned, and add one in getItem() too? (as it's the likely place it will crash: a @throws clause?)
jwharm 2026-05-15 21:03:15 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
java-gi/java-gi#349
No description provided.