Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
927e975
Updating out deprecated blocks in graddle file
Aug 4, 2025
3cb558a
Updating the publication for 8+ version
Symeon94 Aug 4, 2025
833b137
Including the correct version of JavaFX in the build
Symeon94 Aug 4, 2025
f169edb
Mapped list with index
Symeon94 Aug 4, 2025
f5130a1
Removing item added by mistake
Symeon94 Aug 4, 2025
a927fd6
Adding tests for lazy evaluation and removing getSource() to use the …
Symeon94 Aug 4, 2025
d7d7393
Issue with outdated 'runtime' and missing JavaFX dependencies for rea…
Symeon94 Aug 4, 2025
8ca155a
Issue with outdated 'runtime' and missing JavaFX dependencies for rea…
Symeon94 Aug 4, 2025
6d0bc07
Javadoc fails building due to h2 title being used while parent is h3.…
Symeon94 Aug 4, 2025
120acab
Issue with build due to likely bug in the builder library. Upgrading …
Symeon94 Aug 4, 2025
411f081
merge
Symeon94 Aug 4, 2025
cf251ac
merge
Symeon94 Aug 8, 2025
a756475
merge
Symeon94 Aug 8, 2025
c5ccdad
merge
Symeon94 Aug 8, 2025
810958b
Using a secondary constructor instead of inheritance
Symeon94 Aug 8, 2025
95e7798
Removing generated gradle files
Symeon94 Aug 8, 2025
17e668d
Adding some comments to differentiate between the two methods
Symeon94 Aug 8, 2025
3764deb
Cosmetic cleanup
Symeon94 Aug 8, 2025
01a43d4
fixing issue when you are removing multiple items at the same time
Symeon94 Aug 9, 2025
431275d
Added unit tests
Symeon94 Aug 11, 2025
6fbb8a2
Forgot to remove a print
Symeon94 Aug 11, 2025
9255efa
Reversing to the original method to remove elements
Symeon94 Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ build/
.project
.settings/
.idea/
gradle/
gradlew
gradle.bat
2 changes: 1 addition & 1 deletion reactfx/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ group = 'org.reactfx'

dependencies {
// Test dependencies
testImplementation group: 'junit', name: 'junit', version: '4.12'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.13.4'
testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version: '1.3'
testImplementation group: 'org.junit.contrib', name: 'junit-theories', version: '4.12'
testImplementation group: 'com.pholser', name: 'junit-quickcheck-core', version: '0.4'
Expand Down
19 changes: 19 additions & 0 deletions reactfx/src/main/java/org/reactfx/collection/LiveList.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
Expand All @@ -21,6 +22,7 @@
import org.reactfx.collection.LiveList.QuasiModificationObserver;
import org.reactfx.util.AccumulatorSize;
import org.reactfx.util.Experimental;
import org.reactfx.util.Tuple2;
import org.reactfx.util.WrapperBase;
import org.reactfx.value.Val;

Expand Down Expand Up @@ -179,6 +181,10 @@ default <F> LiveList<F> map(Function<? super E, ? extends F> f) {
return map(this, f);
}

default <F> LiveList<F> map(BiFunction<Integer, ? super E, ? extends F> f) {
return map(this, f);
}

default <F> LiveList<F> mapDynamic(
ObservableValue<? extends Function<? super E, ? extends F>> f) {
return mapDynamic(this, f);
Expand Down Expand Up @@ -291,12 +297,25 @@ static Val<Integer> sizeOf(ObservableList<?> list) {
return Val.create(() -> list.size(), list);
}

/**
* If you wish to use the index of the element in your mapping function, check {@link #map(ObservableList, BiFunction)}.
* @param f a mapping function taking the element from source as input and producing a result
*/
static <E, F> LiveList<F> map(
ObservableList<? extends E> list,
Function<? super E, ? extends F> f) {
return new MappedList<>(list, f);
}

/**
* @param f a mapping function taking the index and the element from source as input and producing a result
*/
static <E, F> LiveList<F> map(
ObservableList<? extends E> list,
BiFunction<Integer, ? super E, ? extends F> f) {
return new MappedList<>(list, f);
}

static <E, F> LiveList<F> mapDynamic(
ObservableList<? extends E> list,
ObservableValue<? extends Function<? super E, ? extends F>> f) {
Expand Down
19 changes: 16 additions & 3 deletions reactfx/src/main/java/org/reactfx/collection/MappedList.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.reactfx.collection;

import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

import javafx.beans.value.ObservableValue;
Expand All @@ -13,18 +14,24 @@
class MappedList<E, F> extends LiveListBase<F>
implements UnmodifiableByDefaultLiveList<F> {
private final ObservableList<? extends E> source;
private final Function<? super E, ? extends F> mapper;
private final BiFunction<Integer, ? super E, ? extends F> mapper;

public MappedList(
ObservableList<? extends E> source,
Function<? super E, ? extends F> mapper) {
this(source, (index, elem) -> mapper.apply(elem));
}

public MappedList(
ObservableList<? extends E> source,
BiFunction<Integer, ? super E, ? extends F> mapper) {
this.source = source;
this.mapper = mapper;
}

@Override
public F get(int index) {
return mapper.apply(source.get(index));
return mapper.apply(index, source.get(index));
}

@Override
Expand All @@ -44,6 +51,12 @@ private void sourceChanged(QuasiListChange<? extends E> change) {
static <E, F> QuasiListChange<F> mappedChangeView(
QuasiListChange<? extends E> change,
Function<? super E, ? extends F> mapper) {
return mappedChangeView(change, (index, elem) -> mapper.apply(elem));
}

static <E, F> QuasiListChange<F> mappedChangeView(
QuasiListChange<? extends E> change,
BiFunction<Integer, ? super E, ? extends F> mapper) {
return new QuasiListChange<F>() {

@Override
Expand All @@ -63,7 +76,7 @@ public int getAddedSize() {

@Override
public List<? extends F> getRemoved() {
return Lists.mappedView(mod.getRemoved(), mapper);
return Lists.mappedView(mod.getRemoved(), elem -> mapper.apply(mod.getFrom(), elem));
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right.

It seems that you need a BiFunction version of Lists.mappedView, and then

Lists.mappedView(mod.getRemoved(), (i, elem) -> mapper.apply(mod.getFrom() + i, elem));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking the time to review.
I actually have tried that before you commented, but the problem is that the index provided by the mappedView is not the index of the removed item, it is the index in a new list.

    public static <E, F> List<F> mappedView(
            List<? extends E> source,
            BiFunction<Integer, ? super E, ? extends F> f) {
        return new AbstractList<F>() {

            @Override
            public F get(int index) {
                return f.apply(index, source.get(index));
            }

            @Override
            public int size() {
                return source.size();
            }
        };
    }

If I have removed items at index 1 and 2, I want the apply to use the index 1 and 2. I guess the i in your comment will be 0 and 1 in that case, which would result in index 1 and 3 ? (mod.getFrom() + i). Or said otherwise, we are mixing two differents unrelated types of index if we do that sum.
So, I'm wondering about the correctness of that change. Maybe you have a scenario in mind where it would fail, if you describe it I can add it to the UT and make sure to fix it.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, i will range from 0 to 1. But then mod.getFrom() will always return 1. So you get 1 and 2 as a result, which is as expected.

Anyway, I haven't run it. A scenario where this should fail is if you remove a range of at least 2 elements.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit puzzled by the logic you are laying forth. I have actually already tested that case if you look at UT testIndexedList which test the removal of index 1 and 2 (where you can see <index>-<len> below).

        // Remove an entry to the list and check changes (note that 3-7 becomes 2-7)
        strings.remove(2);
        assertLinesMatch(Stream.of("0-1", "1-4", "2-7"), lengths.stream());
        assertEquals(Arrays.asList("1-4", "3-7"), added);
        assertEquals(Arrays.asList("1-2", "2-3"), removed);

It is my understanding that mod.getFrom() returns the original index (as has been tested with the UT). I'm not sure where you are getting that mod.getFrom() will always return 1?

If I'm mistaken somewhere, please let me know (as you created the library, you definitly have a better understanding of the overall picture than I do).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could easily be mistaken, as I wrote the library 10+ years ago :)

However, in the code you quoted, you only remove a single element at a time (strings.remove(2)). Try removing a range (e.g. strings.remove(2, 4)).

For the same mod, mod.getFrom() always returns the same number. That number is 1 if the change begins at index 1, as in your example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to parse all the lines, I have already the code to identify the style change in only the visible portion of the code. The problem is that RichTextFX re-render and then applies the style after re-rendering, creating a visible delay between the two (which should be fixed with the index). I'll try to investigate when I have time on your proposal to play directly with the document prior to re-rendering and see if that can help.

(I'm not sure why you are proposing to parse the whole file each time, that is not something I'll do, as we reevaluate the style each time a user press a key, I cannot have a lag of 200ms on each key press).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why you are proposing to parse the whole file each time

I'm not 😉
However, I did think you maintained an up-to-date version of all the styles.

Anyway, it seems to me that you are maintaining the styles somewhere on the side, while I'm saying the style should come through EditableStyledDocument.

If you do not want to maintain styles of the whole document, you should still be able to compute them lazily in EditableStyledDocument#getParagraph(int) (e.g. for scrolling) plus eagerly for each edit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For more precisions, I'm only keeping track of Python open/close group comments in my file. The rest of Python syntax can be computed based on the line content, which I do and only assess on changing the visible lines or on modifying a line. So each time a user changes a line, I use the information I saved about group comments to tell if the line is commented, and if not (or for the part that isn't) I recompute the style.

The styles are in the document, but it is hard for me to maintain that multiline comment based uniquely on the style class in the EditableStyledDocument<Collection<String>, String, Collection<String>>.

And, in any case, I don't think the style calculation is the right conversation here. The only problem is with a delay in the display.

Note that I'm not having delay in the inline style because I create it when opening my file. This creates the slow opening (although acceptable), which could be avoided if there wasn't any delay between rendering and styling. The issue arise as soon as I remove/add a group comment tag which must cause change potentially in the whole file. That's when you see the delay happen on scrolling (changing the visible lines)

Based on what you said in previous post, I thought you wanted me to try to create a custom EditableStyledDocument which would handle the logic of styling Python code. Did I understand correctly?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And, in any case, I don't think the style calculation is the right conversation here. The only problem is with a delay in the display.

Correct me if I am getting this wrong, but the delay is there because the paragraphs were not styled correctly from the start, but only re-styled in reaction to a change in visible paragraphs (which is sent out only after the paragraphs are rendered).

Then all I'm trying to suggest is to get them styled correctly on the first render (not only after the visible notification).

I thought you wanted me to try to create a custom EditableStyledDocument which would handle the logic of styling Python code. Did I understand correctly?

Yes, a custom EditableStyledDocument that would return correctly styled paragraphs on the first shot.

Listening to visible paragraphs might still be useful for edits (i.e. on edit, do not restyle invisible paragraphs), but for scrolling just get the correct style from the get go.

Copy link
Copy Markdown
Contributor Author

@Symeon94 Symeon94 Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for confirming, when I'll get some time on my hand, I'll try to implement that custom document.

You are correct in your first statement (it's just that "not styled correctly" here, is just that they are not yet styled). Small correction, they are styled on 1. visibility change and 2. edition.

}
});
}
Expand Down
127 changes: 106 additions & 21 deletions reactfx/src/test/java/org/reactfx/collection/ListMapTest.java
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
package org.reactfx.collection;

import static org.junit.Assert.*;
import static org.junit.jupiter.api.Assertions.assertLinesMatch;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import org.junit.Test;
import org.junit.jupiter.api.DisplayName;
import org.reactfx.value.Var;

public class ListMapTest {

@Test
public void testGet() {
ObservableList<String> strings = FXCollections.observableArrayList("1", "22", "333");
LiveList<Integer> lengths = LiveList.map(strings, String::length);

assertEquals(Arrays.asList(1, 2, 3), lengths);
}

@Test
public void testChanges() {
ObservableList<String> strings = FXCollections.observableArrayList("1", "22", "333");
LiveList<Integer> lengths = LiveList.map(strings, String::length);
assertEquals(Arrays.asList(1, 2, 3), lengths);

List<Integer> removed = new ArrayList<>();
List<Integer> added = new ArrayList<>();
Expand All @@ -39,34 +35,59 @@ public void testChanges() {
}
});

// Set an item
strings.set(1, "4444");
assertEquals(Arrays.asList(1, 4, 3), lengths);
assertEquals(Collections.singletonList(2), removed);
assertEquals(Collections.singletonList(4), added);

// Add an item
strings.add("7777777");
assertEquals(Arrays.asList(1, 4, 3, 7), lengths);
assertEquals(Collections.singletonList(2), removed);
assertEquals(Arrays.asList(4, 7), added);

assertEquals(Arrays.asList(2), removed);
assertEquals(Arrays.asList(4), added);
// Remove an item
strings.remove(1);
assertEquals(Arrays.asList(1, 3, 7), lengths);
assertEquals(Arrays.asList(2, 4), removed);
assertEquals(Arrays.asList(4, 7), added);
}

@Test
public void testLaziness() {
ObservableList<String> strings = FXCollections.observableArrayList("1", "22", "333");
IntegerProperty evaluations = new SimpleIntegerProperty(0);
LiveList<Integer> lengths = LiveList.map(strings, s -> {
evaluations.set(evaluations.get() + 1);
return s.length();
IntegerProperty evaluationsCounter = new SimpleIntegerProperty(0);
LiveList<Integer> lengths = LiveList.map(strings, elem -> {
evaluationsCounter.set(evaluationsCounter.get() + 1);
return elem.length();
});

lengths.observeChanges(ch -> {});
strings.remove(1);

assertEquals(0, evaluations.get());
assertEquals(0, evaluationsCounter.get());

// Get the first element and the counter has increased
assertEquals(1, lengths.get(0).intValue());
assertEquals(1, evaluationsCounter.get());

// Get the second element, it will evaluate one item
assertEquals(3, lengths.get(1).intValue());
assertEquals(2, evaluationsCounter.get());

// Get again the first, it will reevaluate it
assertEquals(1, lengths.get(0).intValue());
assertEquals(3, evaluationsCounter.get());
}

@Test
public void testLazinessOnChangeAccumulation() {
ObservableList<String> strings = FXCollections.observableArrayList("1", "22", "333");
IntegerProperty evaluations = new SimpleIntegerProperty(0);
LiveList<Integer> lengths = LiveList.map(strings, s -> {
evaluations.set(evaluations.get() + 1);
return s.length();
IntegerProperty evaluationsCounter = new SimpleIntegerProperty(0);
LiveList<Integer> lengths = LiveList.map(strings, elem -> {
evaluationsCounter.set(evaluationsCounter.get() + 1);
return elem.length();
});
SuspendableList<Integer> suspendable = lengths.suspendable();

Expand All @@ -76,7 +97,7 @@ public void testLazinessOnChangeAccumulation() {
strings.set(1, "abcd");
});

assertEquals(0, evaluations.get());
assertEquals(0, evaluationsCounter.get());
}

@Test
Expand All @@ -99,4 +120,68 @@ public void testDynamicMap() {
fn.setValue(s -> s.length() * s.length());
});
}

@Test
public void testIndexedList() {
ObservableList<String> strings = FXCollections.observableArrayList("1", "22", "333");
// Live map receives index,item and returns %d-%d index item
LiveList<String> lengths = LiveList.map(strings, (index, item) -> String.format("%d-%d", index, item.length()));
assertLinesMatch(Stream.of("0-1", "1-2", "2-3"), lengths.stream());

List<String> removed = new ArrayList<>();
List<String> added = new ArrayList<>();
lengths.observeChanges(ch -> {
for(ListModification<? extends String> mod: ch.getModifications()) {
removed.addAll(mod.getRemoved());
added.addAll(mod.getAddedSubList());
}
});

// Set a value in the list and check changes
strings.set(1, "4444");
assertLinesMatch(Stream.of("0-1", "1-4", "2-3"), lengths.stream());
assertEquals(Collections.singletonList("1-4"), added);
assertEquals(Collections.singletonList("1-2"), removed);

// Add an entry to the list and check changes
strings.add("7777777");
assertLinesMatch(Stream.of("0-1", "1-4", "2-3", "3-7"), lengths.stream());
assertEquals(Arrays.asList("1-4", "3-7"), added);
assertEquals(Collections.singletonList("1-2"), removed);

// Remove an entry to the list and check changes (note that 3-7 becomes 2-7)
strings.remove(2);
assertLinesMatch(Stream.of("0-1", "1-4", "2-7"), lengths.stream());
assertEquals(Arrays.asList("1-4", "3-7"), added);
assertEquals(Arrays.asList("1-2", "2-3"), removed);
}

@Test
@DisplayName("testLazyIndexedList")
public void testLazyIndexedList() {
ObservableList<String> strings = FXCollections.observableArrayList("1", "22", "333");
IntegerProperty evaluationsCounter = new SimpleIntegerProperty(0);
LiveList<String> lengths = LiveList.map(strings, (index, elem) -> {
evaluationsCounter.set(evaluationsCounter.get() + 1);
return String.format("%d-%d", index, elem.length());
});

lengths.observeChanges(ch -> {});
strings.remove(1);

assertEquals(0, evaluationsCounter.get());

// Get the first element and the counter has increased
assertEquals("0-1", lengths.get(0));
assertEquals(1, evaluationsCounter.get());

// Get the second element, it will evaluate one item
assertEquals("1-3", lengths.get(1));
assertEquals(2, evaluationsCounter.get());

// Get again the first, it will reevaluate it
assertEquals("0-1", lengths.get(0));
assertEquals(3, evaluationsCounter.get());
}

}