[SOLVED]Strange behavior of lemur's ListBox

I have faced rather strange behavior of com.simsilica.lemur.ListBox class.

Here is small app which adds ListBox select and binds simple logic to clicking on select options (just write line to System.out).

package com.example.bug;

import com.jme3.app.SimpleApplication;
import com.jme3.math.Vector3f;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.ListBox;
import com.simsilica.lemur.event.ConsumingMouseListener;
import com.simsilica.lemur.event.MouseEventControl;
import com.simsilica.lemur.style.BaseStyles;
import org.lwjgl.opengl.Display;

import java.util.ArrayList;
import java.util.List;

public class ErrorApp extends  SimpleApplication {

    public static void main(String[] args){
        ErrorApp app = new ErrorApp();
        app.start();
    }



    @Override
    public void simpleInitApp() {
        List<Item> items = new ArrayList<>();

        items.add(new Item("Item1"));
        items.add(new Item("Item2"));
        items.add(new Item("Item3"));
        items.add(new Item("Item4"));
        items.add(new Item("Item5"));

        /*
        items.add(new Item("q"));
        items.add(new Item("qq"));
        items.add(new Item("qqq"));
        items.add(new Item("qqqq"));
        items.add(new Item("qqqqq"));
         */

        GuiGlobals.initialize(this);
        BaseStyles.loadGlassStyle();
        GuiGlobals.getInstance().getStyles().setDefaultStyle(BaseStyles.GLASS);

        Container itemContainer = new Container();
        MouseEventControl.addListenersToSpatial(itemContainer, ConsumingMouseListener.INSTANCE);
        itemContainer.setPreferredSize(new Vector3f(Display.getWidth()*0.2f, Display.getHeight()*0.8f, 0));

        ListBox<Item> itemList = itemContainer.addChild(new ListBox<>());
        for(Item i: items){
            itemList.getModel().add(i);
        }
        itemList.setVisibleItems(15);

        itemList.setCellRenderer((value, selected, existing) -> new Label(value.getName()));
        itemList.addClickCommands(source -> System.out.println("######## " + source.getSelectionModel().getSelection()));

        itemContainer.setLocalTranslation(0, Display.getHeight(), 0);

        getGuiNode().attachChild(itemContainer);

    }


    private class Item{
        String name;
        public Item (String name){
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String toString(){
            return  name;
        }
    }

}

This one works as expected.

But if we change list of options from:

        items.add(new Item("Item1"));
        items.add(new Item("Item2"));
        items.add(new Item("Item3"));
        items.add(new Item("Item4"));
        items.add(new Item("Item5"));

to

        items.add(new Item("q"));
        items.add(new Item("qq"));
        items.add(new Item("qqq"));
        items.add(new Item("qqqq"));
        items.add(new Item("qqqqq"));

only clicking on the last element ("qqqqq’) will trigger writing to System.out.

So I have two questions:

  • What is going on here?
  • What is the correct way to implement such logic?

P.S. probably it is somehow related to adding CellRender - at least when I comment out line:

        itemList.setCellRenderer((value, selected, existing) -> new Label(value.getName()));

app works as expected.

Your cell renderer is ignoring the “existing” parameter and creating a new Label every time (so lots of extra Label creation). So all of those new Labels don’t get the proper listeners registered, I guess. (That might be a bug but if you fix your cell renderer then there would be no issue.)

If there is an existing value then set the text on that value else create a new label. (Note also that the default cell renderer uses buttons… but labels should work, too. If you reach a point where parts of your items are clickable and parts aren’t then it could be button vs label related.)

Edit: also looking at your Item class… you shouldn’t need a custom cell renderer anyway. toString() is what the default implementation uses.

Thanks.

Looks like changing renderer to

itemList.setCellRenderer((value, selected, existing) -> existing==null ? new Label(value.getName()): existing);

solves the issue.

P.S. Actually I do need a custom renderer - simply have not included it into the sample for simplicity sake. In what I am planning to implement, simple string is not enough. But thanks for the tip anyway.

If there is an existing value then you will need to change the text else when you add/remove items from the list (or even when scrolling) the labels will be showing the wrong values.

In the end, you might be better off just using one of the default renderers and just provide the toString() implementation. Unless you plan to customize the GUI element used.

new DefaultValueRenderer( (value) -> value.getName() ); 

or whatever.

Ah, I see.
Just have not reached the point when I need to add\remove items yet.
I will play around with this a bit later and hope it will work fine.
Thanks again.

It will also fail if you scroll.

If a list has X visible items then X labels/buttons are created and passed to the renderer to reuse them. So if you scroll down one item then all of the ‘existing’ labels will need their values changed.