diff --git a/client/src/main/kotlin/com/r3corda/client/fxutils/FlattenedList.kt b/client/src/main/kotlin/com/r3corda/client/fxutils/FlattenedList.kt new file mode 100644 index 0000000000..b048c173dc --- /dev/null +++ b/client/src/main/kotlin/com/r3corda/client/fxutils/FlattenedList.kt @@ -0,0 +1,105 @@ +package com.r3corda.client.fxutils + +import javafx.beans.InvalidationListener +import javafx.beans.value.ObservableValue +import javafx.collections.ListChangeListener +import javafx.collections.ObservableList +import javafx.collections.transformation.TransformationList +import java.util.* + +/** + * [FlattenedList] flattens the passed in list of [ObservableValue]s so that changes in individual updates to the values + * are reflected in the exposed list as expected. + */ +class FlattenedList(val sourceList: ObservableList>) : TransformationList>(sourceList) { + + /** + * We maintain an ObservableValue->index map. This is needed because we need the ObservableValue's index in order to + * propagate a change and if the listener closure captures the index at the time of the call to + * [ObservableValue.addListener] it will become incorrect if the indices shift around later. + * + * Note that because of the bookkeeping required for this map, any remove operation and any add operation that + * inserts to the middle of the list will be O(N) as we need to scan the map and shift indices accordingly. + */ + val indexMap = HashMap, Pair>() + init { + sourceList.forEachIndexed { index, observableValue -> + indexMap[observableValue] = Pair(index, createListener(observableValue)) + } + } + + private fun createListener(observableValue: ObservableValue): InvalidationListener { + return InvalidationListener { + val currentIndex = indexMap[observableValue]!!.first + beginChange() + nextAdd(currentIndex, currentIndex + 1) + endChange() + } + } + + override fun sourceChanged(c: ListChangeListener.Change>) { + beginChange() + while (c.next()) { + if (c.wasPermutated()) { + val from = c.from + val to = c.to + val permutation = IntArray(to, { c.getPermutation(it) }) + indexMap.replaceAll { _observableValue, pair -> Pair(permutation[pair.first], pair.second) } + nextPermutation(from, to, permutation) + } else if (c.wasUpdated()) { + throw UnsupportedOperationException("FlattenedList doesn't support Update changes") + } else { + val removed = c.removed + if (removed.size != 0) { + val removeStart = indexMap[removed.first()]!!.first + val removeEnd = indexMap[removed.last()]!!.first + 1 + require(removeStart < removeEnd) + val removeRange = removeEnd - removeStart + val iterator = indexMap.iterator() + for (entry in iterator) { + val (observableValue, pair) = entry + val (index, listener) = pair + if (index >= removeStart) { + if (index < removeEnd) { + observableValue.removeListener(listener) + iterator.remove() + } else { + // Shift indices + entry.setValue(Pair(index - removeRange, listener)) + } + } + } + nextRemove(removeStart, removed.map { it.value }) + } + if (c.wasAdded()) { + val addStart = c.from + val addEnd = c.to + val addRange = addEnd - addStart + // If it was a push to the end we don't need to shift indices + if (addStart != indexMap.size) { + val iterator = indexMap.iterator() + for (entry in iterator) { + val (index, listener) = entry.value + if (index >= addStart) { + // Shift indices + entry.setValue(Pair(index + addRange, listener)) + } + } + } + c.addedSubList.forEachIndexed { sublistIndex, observableValue -> + indexMap[observableValue] = Pair(addStart + sublistIndex, createListener(observableValue)) + } + nextAdd(addStart, addEnd) + } + } + } + endChange() + require(sourceList.size == indexMap.size) + } + + override fun get(index: Int) = sourceList.get(index).value + + override fun getSourceIndex(index: Int) = index + + override val size: Int get() = sourceList.size +} diff --git a/client/src/test/kotlin/com/r3corda/client/fxutils/FlattenedListTest.kt b/client/src/test/kotlin/com/r3corda/client/fxutils/FlattenedListTest.kt new file mode 100644 index 0000000000..8c5cd1b714 --- /dev/null +++ b/client/src/test/kotlin/com/r3corda/client/fxutils/FlattenedListTest.kt @@ -0,0 +1,89 @@ +package com.r3corda.client.fxutils + +import javafx.beans.property.SimpleObjectProperty +import javafx.collections.FXCollections +import org.junit.Before +import org.junit.Test + +class FlattenedListTest { + + var sourceList = FXCollections.observableArrayList(SimpleObjectProperty(1234)) + var flattenedList = FlattenedList(sourceList) + + @Before + fun setup() { + sourceList = FXCollections.observableArrayList(SimpleObjectProperty(1234)) + flattenedList = FlattenedList(sourceList) + } + + @Test + fun addWorks() { + require(flattenedList.size == 1) + require(flattenedList[0] == 1234) + + sourceList.add(SimpleObjectProperty(12)) + require(flattenedList.size == 2) + require(flattenedList[0] == 1234) + require(flattenedList[1] == 12) + + sourceList.add(SimpleObjectProperty(34)) + require(flattenedList.size == 3) + require(flattenedList[0] == 1234) + require(flattenedList[1] == 12) + require(flattenedList[2] == 34) + + sourceList.add(0, SimpleObjectProperty(56)) + require(flattenedList.size == 4) + require(flattenedList[0] == 56) + require(flattenedList[1] == 1234) + require(flattenedList[2] == 12) + require(flattenedList[3] == 34) + + sourceList.addAll(2, listOf(SimpleObjectProperty(78), SimpleObjectProperty(910))) + require(flattenedList.size == 6) + require(flattenedList[0] == 56) + require(flattenedList[1] == 1234) + require(flattenedList[2] == 78) + require(flattenedList[3] == 910) + require(flattenedList[4] == 12) + require(flattenedList[5] == 34) + } + + @Test + fun removeWorks() { + val firstRemoved = sourceList.removeAt(0) + require(firstRemoved.get() == 1234) + require(flattenedList.size == 0) + firstRemoved.set(123) + + sourceList.add(SimpleObjectProperty(12)) + sourceList.add(SimpleObjectProperty(34)) + sourceList.add(SimpleObjectProperty(56)) + require(flattenedList.size == 3) + val secondRemoved = sourceList.removeAt(1) + require(secondRemoved.get() == 34) + require(flattenedList.size == 2) + require(flattenedList[0] == 12) + require(flattenedList[1] == 56) + secondRemoved.set(123) + + sourceList.clear() + require(flattenedList.size == 0) + } + + @Test + fun updatingObservableWorks() { + require(flattenedList[0] == 1234) + sourceList[0].set(4321) + require(flattenedList[0] == 4321) + + sourceList.add(0, SimpleObjectProperty(12)) + sourceList[1].set(8765) + require(flattenedList[0] == 12) + require(flattenedList[1] == 8765) + + sourceList[0].set(34) + require(flattenedList[0] == 34) + require(flattenedList[1] == 8765) + } +}