ListChangeListener wasPermutated block
Asked Answered
G

2

6

The JavaDoc for the ListChangeListener provides a template for handling changes. I don't know how to handle permutations, however. For every index, I can find out where the new index of the item is, but I don't know what to do with it. This is a bit of a puzzle that is independent of programming language. An ObservableList can only add(), remove(), set(), and also has an iterator.

If I have an original list [1,2,3], and bind a list[] to it, the bound list[1,2,3] needs to match it. If the original list gets its comparator swapped so that the original list now reads [3,2,1], how do I make the bound list follow along?

/**
 * Binds a source list's elements to a destination list. Any changes made in
 * the source list will reflect in the destination list.
 *
 * @param <SRC> The source list's object type.
 * @param <DEST> The destination list's object type.
 * @param dest The destination list that will be bound to the src list.
 * @param src The source list to watch for changes, and propagate up to the
 * destination list.
 * @param transformer A function that will transform a source list data
 * type, A, into a destination list data type, B.
 */
public static <SRC, DEST> void bindLists(
        ObservableList<DEST> dest, ObservableList<SRC> src, Function<? super SRC, ? extends DEST> transformer) {
    /*Add the initial data into the destination list.*/
    for (SRC a : src) {
        dest.add(transformer.apply(a));
    }
    /*Watch for future data to add to the destination list. Also watch for removal
     of data form the source list to remove its respective item in the destination
     list.*/
    src.addListener((ListChangeListener.Change<? extends SRC> c) -> {
        while (c.next()) {
            if (c.wasPermutated()) {
                /*How do you handle permutations? Do you remove and then add, 
                 or add and then remove, or use set, or use a copy arraylist 
                 and set the right indices? Removing/adding causes concurrent modifications.*/
                for (int oldIndex = c.getFrom(); oldIndex < c.getTo(); oldIndex++) {
                    int newIndex = c.getPermutation(oldIndex);
                    dest.remove(oldIndex);
                    dest.add(newIndex, dest.get(oldIndex));
                }
            } else if (c.wasUpdated()) {

            } else {
                /*Respond to removed data.*/
                for (SRC item : c.getRemoved()) {
                    int from = c.getFrom();
                    dest.remove(from);
                }
                /*Respond to added data.*/
                for (SRC item : c.getAddedSubList()) {
                    int indexAdded = src.indexOf(item);
                    dest.add(indexAdded, transformer.apply(item));
                }
            }
        }
    });
}
Guidance answered 6/9, 2014 at 23:56 Comment(1)
I also would like to know how wasUpdated() even gets triggered. Do the objects of the ObservableList need to implement Observable or something?Guidance
E
6

For the permutation case, I wouldn't bother trying to use add() and remove() to do handle it. This will cause the indexes to shift around and will make things confusing (at least to me).

Conceptually what you get is a range of elements affected, and an array containing some numbers that indicate where each element was moved. I think you understand that much. In your code you have,

    newIndex = getPermutation(oldIndex);

which means that the element was at oldIndex needs to be moved to newIndex. The wrinkle is that if you just make the move directly, you might be overwriting an element that hasn't been moved yet. I think the simplest way to deal with this is to make a copy of the affected subrange and then just step through the permutation array and move elements from the copy into their new positions. The code to do this is:

    int from = c.getFrom();
    int to = c.getTo();
    List<DEST> copy = new ArrayList<>(dest.subList(from, to));
    for (int oldIndex = from; oldIndex < to; oldIndex++) {
        int newIndex = c.getPermutation(oldIndex);
        dest.set(newIndex, copy.get(oldIndex - from));
    }

This is a permutation, so every element ends up somewhere, and none are added or deleted. This implies that you don't have to copy the list range, and that you could move elements one at a time following the chain of moves while using only a single element of temporary space. There might be multiple cycles of chains, so you'd have to detect and handle that too. That sounds pretty complex. I'll leave that for another answerer. :-) For my money, copying the affected range is simple and easy to understand.

The permutation and updated change modes aren't triggered by normal list actions. If you look at javafx.collections.ObservableListBase you can see a protocol that a list implementation can use to build up information about a specific change. If the implementation supplies the right information to nextPermutation or nextUpdate methods, it will trigger these more other change modes. I'm not sure what might trigger them in JavaFX. For example, the Node.toFront() and Node.toBack() methods to change node stacking order potentially could generate permutation changes, but they don't seem to. I don't know of anything that would generate an update change either.

Semantically, I think an update change implies that the elements in that range of the list have changed but that the length of the list stays the same. This is in contrast to the "replaced" change mode, where a range of elements might be replaced with a different number of elements. It could also be that the update change means that the elements themselves haven't been replaced -- that is, the list contents haven't changed -- merely that the elements' internal states have changed.

Emelyemelyne answered 7/9, 2014 at 5:23 Comment(2)
In general I would favor this approach. Just note that if the list you're manipulating is (or could be) a list of child Nodes in a Pane, then this will break as there will be duplicated nodes in the list while you are processing the loop. In this case, you can apply the permutation to the copy with copy.set(newIndex - from, dest.get(oldIndex)); inside the loop. Then when you are done replace the elements of dest with dest.subList(from, to).clear(); and dest.addAll(from, copy);.Eames
@Eames Good point about avoiding duplicate Node entries if dest is in the scene graph.Emelyemelyne
B
2

Note that what you are doing here is implemented in EasyBind. See how to map and bind a list.

Essentially, this is how you implement your bindLists method:

public static <SRC, DEST> void bindLists(
        ObservableList<DEST> dest, ObservableList<SRC> src,
        Function<? super SRC, ? extends DEST> transformer) {

    EasyBind.listBind(dest, EasyBind.map(src, transformer));
}
Bespread answered 10/9, 2014 at 19:0 Comment(5)
I recall deciding not to use your class for some reason a few months ago, but I can't remember what. I suppose at this point I can switch back to using it though. :)Guidance
Also, off topic question, but what is the difference between a Javadoc jar and a source jar? Isn't the EasyBind.jar the source already? Why do I need to attach a source to it?Guidance
To use EasyBind, you only need easybind-<version>.jar. It only contains compiled classes. The source jar only contains the source code. It is useful to attach the source jar in your IDE so that it can show you the Javadoc comments and/or the source code, for example when you are debugging and stepping through the code.Bespread
I see. Thank you! I'm going to have to do this eventually as well (packaging into a .jar and providing source/javadocs) for my client.Guidance
EasyBind is very nice and a must have for my JavaFX project but I find it's super hard to figure out the API. So far, I can only use by code sample I happen acrossDentoid

© 2022 - 2024 — McMap. All rights reserved.