diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/collections/IterableMapChange.java b/modules/javafx.base/src/main/java/com/sun/javafx/collections/IterableMapChange.java new file mode 100644 index 00000000000..283a415629d --- /dev/null +++ b/modules/javafx.base/src/main/java/com/sun/javafx/collections/IterableMapChange.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.collections; + +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableMap; +import java.util.ArrayList; +import java.util.List; + +/** + * Base class for map changes that support bulk change iteration. + * + * @param the key type + * @param the value type + */ +public sealed abstract class IterableMapChange extends MapChangeListener.Change { + + private IterableMapChange(ObservableMap map) { + super(map); + } + + /** + * Returns {@code this} object instance if there is another change to report, or {@code null} if there + * are no more changes. If this method returns another change, the implementation must configure this + * object instance to represent the next change. + *

+ * Note that this narrows down the {@link MapChangeListener.Change#next()} specification, which does + * not mandate that the same object instance is returned on each call. + * + * @return this instance, representing the next change, or {@code null} if there are no more changes + */ + @Override + public abstract MapChangeListener.Change next(); + + /** + * Resets this {@code IterableMapChange} instance to the first change. + */ + public abstract void reset(); + + @Override + public final String toString() { + StringBuilder builder = new StringBuilder(); + + if (wasAdded()) { + if (wasRemoved()) { + builder.append(getValueRemoved()).append(" replaced by ").append(getValueAdded()); + } else { + builder.append(getValueAdded()).append(" added"); + } + } else { + builder.append(getValueRemoved()).append(" removed"); + } + + return builder.append(" at key ").append(getKey()).toString(); + } + + public final static class Remove extends IterableMapChange { + + private record Entry(K key, V value) {} + + private final List> changes; + private int index; + + public Remove(ObservableMap map) { + super(map); + changes = new ArrayList<>(); + } + + public Remove(ObservableMap map, int initialCapacity) { + super(map); + changes = new ArrayList<>(initialCapacity); + } + + @Override + public MapChangeListener.Change next() { + if (index < changes.size() - 1) { + ++index; + return this; + } + + return null; + } + + @Override + public void reset() { + index = 0; + } + + @Override + public boolean wasAdded() { + return false; + } + + @Override + public boolean wasRemoved() { + return true; + } + + @Override + public K getKey() { + return changes.get(index).key; + } + + @Override + public V getValueAdded() { + return null; + } + + @Override + public V getValueRemoved() { + return changes.get(index).value; + } + + public void nextRemoved(K key, V value) { + changes.add(new Entry<>(key, value)); + } + } + + public final static class Generic extends IterableMapChange { + + private static final Object NO_VALUE = new Object(); + + private record Entry(K key, V newValue, V oldValue) { + boolean wasAdded() { + return newValue != NO_VALUE; + } + + boolean wasRemoved() { + return oldValue != NO_VALUE; + } + } + + private final List> changes; + private int index; + + public Generic(ObservableMap map) { + super(map); + changes = new ArrayList<>(); + } + + @Override + public MapChangeListener.Change next() { + if (index < changes.size() - 1) { + ++index; + return this; + } + + return null; + } + + @Override + public void reset() { + index = 0; + } + + @Override + public boolean wasAdded() { + return changes.get(index).wasAdded(); + } + + @Override + public boolean wasRemoved() { + return changes.get(index).wasRemoved(); + } + + @Override + public K getKey() { + return changes.get(index).key; + } + + @Override + public V getValueAdded() { + var change = changes.get(index); + return change.wasAdded() ? change.newValue : null; + } + + @Override + public V getValueRemoved() { + var change = changes.get(index); + return change.wasRemoved() ? change.oldValue : null; + } + + public void nextAdded(K key, V value) { + @SuppressWarnings("unchecked") + var entry = new Entry<>(key, value, (V)NO_VALUE); + changes.add(entry); + } + + public void nextRemoved(K key, V value) { + @SuppressWarnings("unchecked") + var entry = new Entry<>(key, (V)NO_VALUE, value); + changes.add(entry); + } + + public void nextReplaced(K key, V oldValue, V newValue) { + changes.add(new Entry<>(key, newValue, oldValue)); + } + } +} diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/collections/IterableSetChange.java b/modules/javafx.base/src/main/java/com/sun/javafx/collections/IterableSetChange.java new file mode 100644 index 00000000000..f2db6ef87a6 --- /dev/null +++ b/modules/javafx.base/src/main/java/com/sun/javafx/collections/IterableSetChange.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.collections; + +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; +import java.util.List; + +/** + * Base class for set changes that support bulk change iteration. + * + * @param the element type + */ +public sealed abstract class IterableSetChange extends SetChangeListener.Change { + + private IterableSetChange(ObservableSet set) { + super(set); + } + + /** + * Returns {@code this} object instance if there is another change to report, or {@code null} if there + * are no more changes. If this method returns another change, the implementation must configure this + * object instance to represent the next change. + *

+ * Note that this narrows down the {@link SetChangeListener.Change#next()} specification, which does + * not mandate that the same object instance is returned on each call. + * + * @return this instance, representing the next change, or {@code null} if there are no more changes + */ + @Override + public abstract SetChangeListener.Change next(); + + /** + * Resets this {@code IterableSetChange} instance to the first change. + */ + public abstract void reset(); + + public static final class Add extends IterableSetChange { + + private final List elements; + private int index; + + public Add(ObservableSet set, List elements) { + super(set); + this.elements = elements; + } + + @Override + public SetChangeListener.Change next() { + if (index < elements.size() - 1) { + ++index; + return this; + } + + return null; + } + + @Override + public void reset() { + index = 0; + } + + @Override + public boolean wasAdded() { + return true; + } + + @Override + public boolean wasRemoved() { + return false; + } + + @Override + public E getElementAdded() { + return elements.get(index); + } + + @Override + public E getElementRemoved() { + return null; + } + + @Override + public String toString() { + return "added " + elements.get(index); + } + } + + public static final class Remove extends IterableSetChange { + + private final List elements; + private int index; + + public Remove(ObservableSet set, List elements) { + super(set); + this.elements = elements; + } + + @Override + public SetChangeListener.Change next() { + if (index < elements.size() - 1) { + ++index; + return this; + } + + return null; + } + + @Override + public void reset() { + index = 0; + } + + @Override + public boolean wasAdded() { + return false; + } + + @Override + public boolean wasRemoved() { + return true; + } + + @Override + public E getElementAdded() { + return null; + } + + @Override + public E getElementRemoved() { + return elements.get(index); + } + + @Override + public String toString() { + return "removed " + elements.get(index); + } + } +} diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/collections/MapAdapterChange.java b/modules/javafx.base/src/main/java/com/sun/javafx/collections/MapAdapterChange.java index c6d73f3c123..e317d0f2612 100644 --- a/modules/javafx.base/src/main/java/com/sun/javafx/collections/MapAdapterChange.java +++ b/modules/javafx.base/src/main/java/com/sun/javafx/collections/MapAdapterChange.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,8 +29,9 @@ import javafx.collections.MapChangeListener.Change; import javafx.collections.ObservableMap; -public class MapAdapterChange extends MapChangeListener.Change { - private final Change change; +public final class MapAdapterChange extends MapChangeListener.Change { + + private Change change; public MapAdapterChange(ObservableMap map, Change change) { super(map); @@ -62,9 +63,19 @@ public V getValueRemoved() { return change.getValueRemoved(); } + @Override + public Change next() { + Change nextChange = change.next(); + if (nextChange != null) { + change = nextChange; + return this; + } + + return null; + } + @Override public String toString() { return change.toString(); } - } diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/collections/MapListenerHelper.java b/modules/javafx.base/src/main/java/com/sun/javafx/collections/MapListenerHelper.java index ab8238fe517..5558cf04394 100644 --- a/modules/javafx.base/src/main/java/com/sun/javafx/collections/MapListenerHelper.java +++ b/modules/javafx.base/src/main/java/com/sun/javafx/collections/MapListenerHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -157,12 +157,30 @@ protected MapListenerHelper removeListener(MapChangeListener change) { + if (change instanceof IterableMapChange iterableChange) { + fireMapChangeEvent(iterableChange); + } else { + fireMapChangeEvent(change); + } + } + + private void fireMapChangeEvent(MapChangeListener.Change change) { try { listener.onChanged(change); } catch (Exception e) { Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); } } + + private void fireMapChangeEvent(IterableMapChange change) { + do { + try { + listener.onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } while (change.next() != null); + } } private static class Generic extends MapListenerHelper { @@ -320,17 +338,43 @@ protected void fireValueChangedEvent(MapChangeListener.Change iterableChange) { + fireMapChangeEvent(iterableChange, curChangeList, curChangeSize); + } else { + fireMapChangeEvent(change, curChangeList, curChangeSize); + } + } finally { + locked = false; + } + } + + private static void fireMapChangeEvent(MapChangeListener.Change change, + MapChangeListener[] curChangeList, + int curChangeSize) { + for (int i = 0; i < curChangeSize; i++) { + try { + curChangeList[i].onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static void fireMapChangeEvent(IterableMapChange change, + MapChangeListener[] curChangeList, + int curChangeSize) { + for (int i = 0; i < curChangeSize; i++) { + do { try { curChangeList[i].onChanged(change); } catch (Exception e) { Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); } - } - } finally { - locked = false; + } while (change.next() != null); + + change.reset(); } } } - } diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/collections/ObservableMapWrapper.java b/modules/javafx.base/src/main/java/com/sun/javafx/collections/ObservableMapWrapper.java index 3539488bc3d..30225584cdc 100644 --- a/modules/javafx.base/src/main/java/com/sun/javafx/collections/ObservableMapWrapper.java +++ b/modules/javafx.base/src/main/java/com/sun/javafx/collections/ObservableMapWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,9 +30,12 @@ import javafx.collections.ObservableMap; import java.util.Collection; +import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; /** * A Map wrapper class that implements observability. @@ -50,67 +53,6 @@ public ObservableMapWrapper(Map map) { this.backingMap = map; } - private class SimpleChange extends MapChangeListener.Change { - - private final K key; - private final V old; - private final V added; - private final boolean wasAdded; - private final boolean wasRemoved; - - public SimpleChange(K key, V old, V added, boolean wasAdded, boolean wasRemoved) { - super(ObservableMapWrapper.this); - assert(wasAdded || wasRemoved); - this.key = key; - this.old = old; - this.added = added; - this.wasAdded = wasAdded; - this.wasRemoved = wasRemoved; - } - - @Override - public boolean wasAdded() { - return wasAdded; - } - - @Override - public boolean wasRemoved() { - return wasRemoved; - } - - @Override - public K getKey() { - return key; - } - - @Override - public V getValueAdded() { - return added; - } - - @Override - public V getValueRemoved() { - return old; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - if (wasAdded) { - if (wasRemoved) { - builder.append(old).append(" replaced by ").append(added); - } else { - builder.append(added).append(" added"); - } - } else { - builder.append(old).append(" removed"); - } - builder.append(" at key ").append(key); - return builder.toString(); - } - - } - protected void callObservers(MapChangeListener.Change change) { MapListenerHelper.fireValueChangedEvent(listenerHelper, change); } @@ -188,22 +130,138 @@ public V remove(Object key) { @Override public void putAll(Map m) { - for (Map.Entry e : m.entrySet()) { - put(e.getKey(), e.getValue()); + int size = m.size(); + + if (size == 1) { + var entry = m.entrySet().iterator().next(); + put(entry.getKey(), entry.getValue()); + } else if (size > 1) { + var change = new IterableMapChange.Generic<>(this); + + for (Map.Entry e : m.entrySet()) { + K key = e.getKey(); + V newValue = e.getValue(); + + if (!backingMap.containsKey(key)) { + change.nextAdded(key, newValue); + } else { + V oldValue = backingMap.get(key); + + if (!Objects.equals(oldValue, newValue)) { + change.nextReplaced(key, oldValue, newValue); + } + } + } + + backingMap.putAll(m); + callObservers(change); } } + @Override + public void replaceAll(BiFunction function) { + Objects.requireNonNull(function); + MapChangeListener.Change change = null; + + for (Map.Entry entry : backingMap.entrySet()) { + K key; + V oldValue; + + try { + key = entry.getKey(); + oldValue = entry.getValue(); + } catch (IllegalStateException ex) { + // This usually means the entry is no longer in the map. + throw new ConcurrentModificationException(ex); + } + + // IllegalStateException thrown from function is not a ConcurrentModificationException. + V newValue = function.apply(key, oldValue); + + try { + if (!Objects.equals(oldValue, newValue)) { + entry.setValue(newValue); + + if (change instanceof SimpleChange) { + var bulkChange = new IterableMapChange.Generic<>(ObservableMapWrapper.this); + bulkChange.nextReplaced(change.getKey(), change.getValueRemoved(), change.getValueAdded()); + bulkChange.nextReplaced(key, oldValue, newValue); + change = bulkChange; + } else if (change instanceof IterableMapChange.Generic bulkChange) { + bulkChange.nextReplaced(key, oldValue, newValue); + } else { + change = new SimpleChange(key, oldValue, newValue, true, true); + } + } + } catch (IllegalStateException ex) { + // This usually means the entry is no longer in the map. + throw new ConcurrentModificationException(ex); + } + } + + if (change == null) { + return; + } + + callObservers(change); + } + @Override public void clear() { - for (Iterator> i = backingMap.entrySet().iterator(); i.hasNext(); ) { - Entry e = i.next(); - K key = e.getKey(); - V val = e.getValue(); - i.remove(); + int size = backingMap.size(); + + if (size == 1) { + Iterator> it = backingMap.entrySet().iterator(); + Entry entry = it.next(); + K key = entry.getKey(); + V val = entry.getValue(); + it.remove(); callObservers(new SimpleChange(key, val, null, false, true)); + } else if (size > 1) { + var change = new IterableMapChange.Remove<>(this, size); + + for (Map.Entry e : backingMap.entrySet()) { + change.nextRemoved(e.getKey(), e.getValue()); + } + + backingMap.clear(); + callObservers(change); } } + private boolean removeRetain(Collection c, ContainsPredicate p, boolean remove) { + MapChangeListener.Change change = null; + + for (Iterator> it = backingMap.entrySet().iterator(); it.hasNext();) { + Entry e = it.next(); + + if (remove == p.contains(c, e)) { + K key = e.getKey(); + V value = e.getValue(); + + if (change instanceof SimpleChange) { + var bulkChange = new IterableMapChange.Remove<>(ObservableMapWrapper.this); + bulkChange.nextRemoved(change.getKey(), change.getValueRemoved()); + bulkChange.nextRemoved(key, value); + change = bulkChange; + } else if (change instanceof IterableMapChange.Remove bulkChange) { + bulkChange.nextRemoved(key, value); + } else { + change = new SimpleChange(key, value, null, false, true); + } + + it.remove(); + } + } + + if (change == null) { + return false; + } + + callObservers(change); + return true; + } + @Override public Set keySet() { if (keySet == null) { @@ -243,7 +301,7 @@ public int hashCode() { return backingMap.hashCode(); } - private class ObservableKeySet implements Set{ + private class ObservableKeySet implements Set, ContainsPredicate { @Override public int size() { @@ -331,22 +389,7 @@ public boolean retainAll(Collection c) { return false; } - return removeRetain(c, false); - } - - private boolean removeRetain(Collection c, boolean remove) { - boolean removed = false; - for (Iterator> i = backingMap.entrySet().iterator(); i.hasNext();) { - Entry e = i.next(); - if (remove == c.contains(e.getKey())) { - removed = true; - K key = e.getKey(); - V value = e.getValue(); - i.remove(); - callObservers(new SimpleChange(key, value, null, false, true)); - } - } - return removed; + return removeRetain(c, this, false); } @Override @@ -356,7 +399,7 @@ public boolean removeAll(Collection c) { return false; } - return removeRetain(c, true); + return removeRetain(c, this, true); } @Override @@ -379,9 +422,13 @@ public int hashCode() { return backingMap.keySet().hashCode(); } + @Override + public boolean contains(Collection c, Entry e) { + return c.contains(e.getKey()); + } } - private class ObservableValues implements Collection { + private class ObservableValues implements Collection, ContainsPredicate { @Override public int size() { @@ -470,22 +517,7 @@ public boolean removeAll(Collection c) { return false; } - return removeRetain(c, true); - } - - private boolean removeRetain(Collection c, boolean remove) { - boolean removed = false; - for (Iterator> i = backingMap.entrySet().iterator(); i.hasNext();) { - Entry e = i.next(); - if (remove == c.contains(e.getValue())) { - removed = true; - K key = e.getKey(); - V value = e.getValue(); - i.remove(); - callObservers(new SimpleChange(key, value, null, false, true)); - } - } - return removed; + return removeRetain(c, this, true); } @Override @@ -500,7 +532,7 @@ public boolean retainAll(Collection c) { return false; } - return removeRetain(c, false); + return removeRetain(c, this, false); } @Override @@ -523,9 +555,10 @@ public int hashCode() { return backingMap.values().hashCode(); } - - - + @Override + public boolean contains(Collection c, Entry e) { + return c.contains(e.getValue()); + } } private class ObservableEntry implements Entry { @@ -549,7 +582,11 @@ public V getValue() { @Override public V setValue(V value) { V oldValue = backingEntry.setValue(value); - callObservers(new SimpleChange(getKey(), oldValue, value, true, true)); + + if (!Objects.equals(oldValue, value)) { + callObservers(new SimpleChange(getKey(), oldValue, value, true, true)); + } + return oldValue; } @@ -686,22 +723,7 @@ public boolean retainAll(Collection c) { return false; } - return removeRetain(c, false); - } - - private boolean removeRetain(Collection c, boolean remove) { - boolean removed = false; - for (Iterator> i = backingMap.entrySet().iterator(); i.hasNext();) { - Entry e = i.next(); - if (remove == c.contains(e)) { - removed = true; - K key = e.getKey(); - V value = e.getValue(); - i.remove(); - callObservers(new SimpleChange(key, value, null, false, true)); - } - } - return removed; + return removeRetain(c, Collection::contains, false); } @Override @@ -711,7 +733,7 @@ public boolean removeAll(Collection c) { return false; } - return removeRetain(c, true); + return removeRetain(c, Collection::contains, true); } @Override @@ -736,4 +758,72 @@ public int hashCode() { } + private static String changeToString(MapChangeListener.Change change) { + StringBuilder builder = new StringBuilder(); + + if (change.wasAdded()) { + if (change.wasRemoved()) { + builder.append(change.getValueRemoved()).append(" replaced by ").append(change.getValueAdded()); + } else { + builder.append(change.getValueAdded()).append(" added"); + } + } else { + builder.append(change.getValueRemoved()).append(" removed"); + } + + return builder.append(" at key ").append(change.getKey()).toString(); + } + + private class SimpleChange extends MapChangeListener.Change { + + private final K key; + private final V old; + private final V added; + private final boolean wasAdded; + private final boolean wasRemoved; + + public SimpleChange(K key, V old, V added, boolean wasAdded, boolean wasRemoved) { + super(ObservableMapWrapper.this); + assert(wasAdded || wasRemoved); + this.key = key; + this.old = old; + this.added = added; + this.wasAdded = wasAdded; + this.wasRemoved = wasRemoved; + } + + @Override + public boolean wasAdded() { + return wasAdded; + } + + @Override + public boolean wasRemoved() { + return wasRemoved; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValueAdded() { + return added; + } + + @Override + public V getValueRemoved() { + return old; + } + + @Override + public String toString() { + return changeToString(this); + } + } + + private interface ContainsPredicate { + boolean contains(Collection c, Entry e); + } } diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/collections/ObservableSetWrapper.java b/modules/javafx.base/src/main/java/com/sun/javafx/collections/ObservableSetWrapper.java index 100ec0a2439..e556751924d 100644 --- a/modules/javafx.base/src/main/java/com/sun/javafx/collections/ObservableSetWrapper.java +++ b/modules/javafx.base/src/main/java/com/sun/javafx/collections/ObservableSetWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,9 +28,11 @@ import javafx.beans.InvalidationListener; import javafx.collections.ObservableSet; import javafx.collections.SetChangeListener; - +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.Set; /** @@ -85,7 +87,6 @@ public E getElementRemoved() { public String toString() { return "added " + added; } - } private class SimpleRemoveChange extends SetChangeListener.Change { @@ -121,7 +122,6 @@ public E getElementRemoved() { public String toString() { return "removed " + removed; } - } private void callObservers(SetChangeListener.Change change) { @@ -312,12 +312,36 @@ public boolean containsAll(Collection c) { * @return true if this set changed as a result of the call */ @Override - public boolean addAll(Collection c) { - boolean ret = false; + public boolean addAll(Collection c) { + E addedElement = null; + List addedList = null; + for (E element : c) { - ret |= add(element); + if (backingSet.add(element)) { + if (addedList != null) { + addedList.add(element); + } else if (addedElement != null) { + addedList = new ArrayList<>(c.size()); + addedList.add(addedElement); + addedList.add(element); + addedElement = null; + } else { + addedElement = element; + } + } } - return ret; + + if (addedElement != null) { + callObservers(new SimpleAddChange(addedElement)); + return true; + } + + if (addedList != null) { + callObservers(new IterableSetChange.Add<>(this, addedList)); + return true; + } + + return false; } /** @@ -363,16 +387,38 @@ public boolean removeAll(Collection c) { } private boolean removeRetain(Collection c, boolean remove) { - boolean removed = false; + E removedElement = null; + List removedList = null; + for (Iterator i = backingSet.iterator(); i.hasNext();) { E element = i.next(); if (remove == c.contains(element)) { - removed = true; + if (removedList != null) { + removedList.add(element); + } else if (removedElement != null) { + removedList = new ArrayList<>(c.size()); + removedList.add(removedElement); + removedList.add(element); + removedElement = null; + } else { + removedElement = element; + } + i.remove(); - callObservers(new SimpleRemoveChange(element)); } } - return removed; + + if (removedElement != null) { + callObservers(new SimpleRemoveChange(removedElement)); + return true; + } + + if (removedList != null) { + callObservers(new IterableSetChange.Remove<>(this, removedList)); + return true; + } + + return false; } /** @@ -382,10 +428,19 @@ private boolean removeRetain(Collection c, boolean remove) { */ @Override public void clear() { - for (Iterator i = backingSet.iterator(); i.hasNext(); ) { - E element = i.next(); - i.remove(); + int size = backingSet.size(); + + if (size == 1) { + Iterator it = backingSet.iterator(); + E element = it.next(); + it.remove(); callObservers(new SimpleRemoveChange(element)); + } else if (size > 1) { + @SuppressWarnings("unchecked") + E[] removed = (E[])new Object[size]; + backingSet.toArray(removed); + backingSet.clear(); + callObservers(new IterableSetChange.Remove<>(this, Arrays.asList(removed))); } } diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/collections/SetAdapterChange.java b/modules/javafx.base/src/main/java/com/sun/javafx/collections/SetAdapterChange.java index 6d09aa64e32..d6e2524f9eb 100644 --- a/modules/javafx.base/src/main/java/com/sun/javafx/collections/SetAdapterChange.java +++ b/modules/javafx.base/src/main/java/com/sun/javafx/collections/SetAdapterChange.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,8 +29,9 @@ import javafx.collections.SetChangeListener; import javafx.collections.SetChangeListener.Change; -public class SetAdapterChange extends SetChangeListener.Change { - private final Change change; +public final class SetAdapterChange extends SetChangeListener.Change { + + private Change change; public SetAdapterChange(ObservableSet set, Change change) { super(set); @@ -62,4 +63,14 @@ public E getElementRemoved() { return change.getElementRemoved(); } + @Override + public Change next() { + Change nextChange = change.next(); + if (nextChange != null) { + change = nextChange; + return this; + } + + return null; + } } diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/collections/SetListenerHelper.java b/modules/javafx.base/src/main/java/com/sun/javafx/collections/SetListenerHelper.java index 3e082e0dd3f..1c8bf8c90ef 100644 --- a/modules/javafx.base/src/main/java/com/sun/javafx/collections/SetListenerHelper.java +++ b/modules/javafx.base/src/main/java/com/sun/javafx/collections/SetListenerHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -157,12 +157,30 @@ protected SetListenerHelper removeListener(SetChangeListener liste @Override protected void fireValueChangedEvent(SetChangeListener.Change change) { + if (change instanceof IterableSetChange iterableChange) { + fireSetChangedEvent(iterableChange); + } else { + fireSetChangedEvent(change); + } + } + + private void fireSetChangedEvent(SetChangeListener.Change change) { try { listener.onChanged(change); } catch (Exception e) { Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); } } + + private void fireSetChangedEvent(IterableSetChange change) { + do { + try { + listener.onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } while (change.next() != null); + } } private static class Generic extends SetListenerHelper { @@ -320,17 +338,43 @@ protected void fireValueChangedEvent(SetChangeListener.Change chang Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); } } - for (int i = 0; i < curChangeSize; i++) { + + if (change instanceof IterableSetChange iterableChange) { + fireSetChangeEvent(iterableChange, curChangeList, curChangeSize); + } else { + fireSetChangeEvent(change, curChangeList, curChangeSize); + } + } finally { + locked = false; + } + } + + private static void fireSetChangeEvent(SetChangeListener.Change change, + SetChangeListener[] curChangeList, + int curChangeSize) { + for (int i = 0; i < curChangeSize; i++) { + try { + curChangeList[i].onChanged(change); + } catch (Exception e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } + } + } + + private static void fireSetChangeEvent(IterableSetChange change, + SetChangeListener[] curChangeList, + int curChangeSize) { + for (int i = 0; i < curChangeSize; i++) { + do { try { curChangeList[i].onChanged(change); } catch (Exception e) { Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); } - } - } finally { - locked = false; + } while (change.next() != null); + + change.reset(); } } } - } diff --git a/modules/javafx.base/src/main/java/javafx/collections/MapChangeListener.java b/modules/javafx.base/src/main/java/javafx/collections/MapChangeListener.java index 7e34fc0c8a2..7a77f5f8d16 100644 --- a/modules/javafx.base/src/main/java/javafx/collections/MapChangeListener.java +++ b/modules/javafx.base/src/main/java/javafx/collections/MapChangeListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -99,6 +99,34 @@ public ObservableMap getMap() { */ public abstract V getValueRemoved(); + /** + * Gets the next change in a series of changes. + *

+ * Repeatedly calling this method allows a listener to fetch all subsequent changes of a bulk + * map modification that would otherwise be reported as repeated invocations of the listener. + * If the listener only fetches some of the pending changes, the rest of the changes will be + * reported with subsequent listener invocations. + *

+ * After this method has been called, the current {@code Change} instance is no longer valid and + * calling any method on it may result in undefined behavior. Callers must not make any assumptions + * about the identity of the {@code Change} instance returned by this method; even if the returned + * instance is the same as the current instance, it must be treated as a distinct change. + *

+ * Since this method must not be called before inspecting the first change, listeners will + * usually use a do-while loop to iterate over multiple changes: + *

{@code
+         *     map.addListener((MapChangeListener) change -> {
+         *         do {
+         *             // Inspect the change
+         *             // ...
+         *         } while ((change = change.next()) != null);
+         *     });
+         * }
+ * + * @return the next change, or {@code null} if there are no more changes + * @since 26 + */ + public Change next() { return null; } } /** diff --git a/modules/javafx.base/src/main/java/javafx/collections/SetChangeListener.java b/modules/javafx.base/src/main/java/javafx/collections/SetChangeListener.java index 4800f86b8d1..651febc7a62 100644 --- a/modules/javafx.base/src/main/java/javafx/collections/SetChangeListener.java +++ b/modules/javafx.base/src/main/java/javafx/collections/SetChangeListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -86,6 +86,34 @@ public ObservableSet getSet() { */ public abstract E getElementRemoved(); + /** + * Gets the next change in a series of changes. + *

+ * Repeatedly calling this method allows a listener to fetch all subsequent changes of a bulk + * set modification that would otherwise be reported as repeated invocations of the listener. + * If the listener only fetches some of the pending changes, the rest of the changes will be + * reported with subsequent listener invocations. + *

+ * After this method has been called, the current {@code Change} instance is no longer valid and + * calling any method on it may result in undefined behavior. Callers must not make any assumptions + * about the identity of the {@code Change} instance returned by this method; even if the returned + * instance is the same as the current instance, it must be treated as a distinct change. + *

+ * Since this method must not be called before inspecting the first change, listeners will + * usually use a do-while loop to iterate over multiple changes: + *

{@code
+         *     set.addListener((SetChangeListener) change -> {
+         *         do {
+         *             // Inspect the change
+         *             // ...
+         *         } while ((change = change.next()) != null);
+         *     });
+         * }
+ * + * @return the next change, or {@code null} if there are no more changes + * @since 26 + */ + public Change next() { return null; } } /** diff --git a/modules/javafx.base/src/test/java/test/com/sun/javafx/collections/ObservableMapWrapperTest.java b/modules/javafx.base/src/test/java/test/com/sun/javafx/collections/ObservableMapWrapperTest.java index eb62b6e874b..a52f9d838ca 100644 --- a/modules/javafx.base/src/test/java/test/com/sun/javafx/collections/ObservableMapWrapperTest.java +++ b/modules/javafx.base/src/test/java/test/com/sun/javafx/collections/ObservableMapWrapperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,12 +26,16 @@ package test.com.sun.javafx.collections; import com.sun.javafx.collections.ObservableMapWrapper; +import javafx.collections.MapChangeListener; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.AbstractSet; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -39,8 +43,101 @@ public class ObservableMapWrapperTest { + @Test + public void partialChangeIterationCausesSubsequentListenerInvocation() { + var trace = new ArrayList(); + var invocations = new int[1]; + var map = new ObservableMapWrapper(new HashMap<>()); + + // This listener only processes 2 changes in each invocation. + map.addListener((MapChangeListener) change -> { + invocations[0]++; + trace.add(change.toString()); + + change = change.next(); + trace.add(change.toString()); + }); + + map.putAll(new LinkedHashMap<>() {{ + put("k1", "a"); + put("k2", "b"); + put("k3", "c"); + put("k4", "d"); + put("k5", "e"); + put("k6", "f"); + }}); + + assertEquals(3, invocations[0]); + assertEquals( + List.of( + "a added at key k1", + "b added at key k2", + "c added at key k3", + "d added at key k4", + "e added at key k5", + "f added at key k6"), + trace); + } + + @Nested + class ClearTest { + @Test + public void singleEntry() { + var map = new TestObservableMapWrapper(Map.of("k1", "a")); + map.clear(); + map.assertTraceEquals("a removed at key k1"); + } + + @Test + public void multipleEntries() { + var map = new TestObservableMapWrapper(Map.of("k1", "a", "k2", "b", "k3", "c")); + map.clear(); + map.assertTraceEquals( + "a removed at key k1", + "b removed at key k2", + "c removed at key k3"); + } + } + + @Nested + class PutAllTest { + @Test + public void singleEntry() { + var map = new TestObservableMapWrapper(Map.of("k1", "a", "k2", "b", "k3", "c")); + map.putAll(Map.of("k1", "d")); + map.assertTraceEquals("a replaced by d at key k1"); + } + + @Test + public void multipleEntries() { + var map = new TestObservableMapWrapper(Map.of("k1", "a", "k2", "b", "k3", "c")); + map.putAll(Map.of("k1", "d", "k2", "e", "k4", "f")); + map.assertTraceEquals( + "a replaced by d at key k1", + "b replaced by e at key k2", + "f added at key k4"); + } + } + @Nested class RemoveAllTest { + @Test + public void singleEntry() { + var map = new TestObservableMapWrapper(Map.of("k1", "a", "k2", "b", "k3", "c")); + map.keySet().removeAll(List.of("k1")); + map.assertTraceEquals("a removed at key k1"); + } + + @Test + public void multipleEntries() { + var map = new TestObservableMapWrapper(Map.of("k1", "a", "k2", "b", "k3", "c")); + map.keySet().removeAll(List.of("k1", "k2", "k3")); + map.assertTraceEquals( + "a removed at key k1", + "b removed at key k2", + "c removed at key k3"); + } + @Test public void testEntrySetNullArgumentThrowsNPE() { var emptyMap = new ObservableMapWrapper<>(new HashMap<>()); @@ -97,6 +194,20 @@ public void testRemoveAllEntriesWithEmptyCollectionArgumentWorksCorrectly() { @Nested class RetainAllTest { + @Test + public void singleEntry() { + var map = new TestObservableMapWrapper(Map.of("k1", "a", "k2", "b", "k3", "c")); + map.keySet().retainAll(List.of("k1")); + map.assertTraceEquals("b removed at key k2", "c removed at key k3"); + } + + @Test + public void multipleEntries() { + var map = new TestObservableMapWrapper(Map.of("k1", "a", "k2", "b", "k3", "c")); + map.keySet().retainAll(List.of("k1", "k3")); + map.assertTraceEquals("b removed at key k2"); + } + @Test public void testEntrySetNullArgumentThrowsNPE() { var map1 = new ObservableMapWrapper<>(new HashMap<>()); @@ -151,6 +262,63 @@ public void testRetainAllEntriesWithEmptyCollectionArgumentWorksCorrectly() { } } + @Nested + class ReplaceAllTest { + @Test + public void singleEntry() { + var map = new TestObservableMapWrapper(Map.of("k1", "a", "k2", "b", "k3", "c")); + + map.replaceAll((key, value) -> switch (key) { + case "k2" -> "e"; + default -> value; + }); + + map.assertTraceEquals("b replaced by e at key k2"); + } + + @Test + public void multipleEntries() { + var map = new TestObservableMapWrapper(Map.of("k1", "a", "k2", "b", "k3", "c")); + + map.replaceAll((key, value) -> switch (key) { + case "k1" -> "d"; + case "k2" -> "e"; + case "k3" -> "f"; + default -> value; + }); + + map.assertTraceEquals( + "a replaced by d at key k1", + "b replaced by e at key k2", + "c replaced by f at key k3"); + } + } + + private static class TestObservableMapWrapper extends ObservableMapWrapper { + final Set bulkChangeTrace = new HashSet<>(); + final Set singleChangeTrace = new HashSet<>(); + + public TestObservableMapWrapper(Map map) { + super(new HashMap<>(map)); + + addListener((MapChangeListener) change -> { + do { + bulkChangeTrace.add(change.toString()); + } while ((change = change.next()) != null); + }); + + addListener((MapChangeListener) change -> { + singleChangeTrace.add(change.toString()); + }); + } + + void assertTraceEquals(String... expected) { + var expectedSet = Set.of(expected); + assertEquals(expectedSet, bulkChangeTrace); + assertEquals(expectedSet, singleChangeTrace); + } + } + private ObservableMapWrapper newNonIterableObservableMapWrapper() { return new ObservableMapWrapper<>( new HashMap<>(Map.of("k0", "v0", "k1", "v1", "k2", "v2")) { @@ -185,5 +353,4 @@ public boolean contains(Object o) { } }; } - } diff --git a/modules/javafx.base/src/test/java/test/com/sun/javafx/collections/ObservableSetWrapperTest.java b/modules/javafx.base/src/test/java/test/com/sun/javafx/collections/ObservableSetWrapperTest.java index f1e373464dc..6459c999ccf 100644 --- a/modules/javafx.base/src/test/java/test/com/sun/javafx/collections/ObservableSetWrapperTest.java +++ b/modules/javafx.base/src/test/java/test/com/sun/javafx/collections/ObservableSetWrapperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,18 +26,79 @@ package test.com.sun.javafx.collections; import com.sun.javafx.collections.ObservableSetWrapper; +import javafx.collections.SetChangeListener; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; public class ObservableSetWrapperTest { + @Test + public void partialChangeIterationCausesSubsequentListenerInvocation() { + var trace = new ArrayList(); + var invocations = new int[1]; + var set = new ObservableSetWrapper(new HashSet<>()); + + // This listener only processes 2 changes in each invocation. + set.addListener((SetChangeListener) change -> { + invocations[0]++; + trace.add(change.toString()); + + change = change.next(); + trace.add(change.toString()); + }); + + set.addAll(List.of("a", "b", "c", "d", "e", "f")); + assertEquals(3, invocations[0]); + assertEquals(List.of("added a", "added b", "added c", "added d", "added e", "added f"), trace); + } + + @Nested + class AddAllTest { + @Test + public void duplicateElement() { + var set = new TestObservableSetWrapper(Set.of("a", "b", "c")); + set.addAll(Set.of("b")); + set.assertTraceEquals(); + } + + @Test + public void singleElement() { + var set = new TestObservableSetWrapper(Set.of("a", "b", "c")); + set.addAll(Set.of("d")); + set.assertTraceEquals("added d"); + } + + @Test + public void multipleElements() { + var set = new TestObservableSetWrapper(Set.of("a", "b", "c")); + set.addAll(Set.of("b", "c", "d", "e")); + set.assertTraceEquals("added d", "added e"); + } + } + @Nested class RemoveAllTest { + @Test + public void singleElement() { + var set = new TestObservableSetWrapper(Set.of("a", "b", "c")); + set.removeAll(Set.of("b")); + set.assertTraceEquals("removed b"); + } + + @Test + public void multipleElements() { + var set = new TestObservableSetWrapper(Set.of("a", "b", "c")); + set.removeAll(Set.of("a", "b", "c")); + set.assertTraceEquals("removed a", "removed b", "removed c"); + } + @Test public void testNullArgumentThrowsNPE() { var set = new ObservableSetWrapper<>(Set.of("a", "b", "c")); @@ -66,6 +127,20 @@ public void testEmptyCollectionArgumentWorksCorrectly() { @Nested class RetainAllTest { + @Test + public void singleElement() { + var set = new TestObservableSetWrapper(Set.of("a", "b", "c")); + set.retainAll(Set.of("b")); + set.assertTraceEquals("removed a", "removed c"); + } + + @Test + public void multipleElements() { + var set = new TestObservableSetWrapper(Set.of("a", "b", "c")); + set.retainAll(Set.of("a", "c")); + set.assertTraceEquals("removed b"); + } + @Test public void testNullArgumentThrowsNPE() { var set = new ObservableSetWrapper<>(Set.of("a", "b", "c")); @@ -91,4 +166,28 @@ public void testEmptyCollectionArgumentWorksCorrectly() { } } + private static class TestObservableSetWrapper extends ObservableSetWrapper { + final Set bulkChangeTrace = new HashSet<>(); + final Set singleChangeTrace = new HashSet<>(); + + public TestObservableSetWrapper(Set set) { + super(new HashSet<>(set)); + + addListener((SetChangeListener) change -> { + do { + bulkChangeTrace.add(change.toString()); + } while ((change = change.next()) != null); + }); + + addListener((SetChangeListener) change -> { + singleChangeTrace.add(change.toString()); + }); + } + + void assertTraceEquals(String... expected) { + var expectedSet = Set.of(expected); + assertEquals(expectedSet, bulkChangeTrace); + assertEquals(expectedSet, singleChangeTrace); + } + } } diff --git a/modules/javafx.base/src/test/java/test/javafx/collections/ObservableMapTest.java b/modules/javafx.base/src/test/java/test/javafx/collections/ObservableMapTest.java index 6c61ca67389..aa088092959 100644 --- a/modules/javafx.base/src/test/java/test/javafx/collections/ObservableMapTest.java +++ b/modules/javafx.base/src/test/java/test/javafx/collections/ObservableMapTest.java @@ -25,8 +25,6 @@ package test.javafx.collections; -import org.junit.jupiter.api.Test; - import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -182,6 +180,21 @@ public void testPutAll(Callable> mapFactory) throw observer.assertMultipleCalls(call("oFoo", null, "OFoo"), call("pFoo", null, "PFoo"), call("foo", "bar", "foofoo")); } + @ParameterizedTest + @MethodSource("createParameters") + @SuppressWarnings("unchecked") + public void testReplaceAll(Callable> mapFactory) throws Exception { + setUp(mapFactory); + + observableMap.replaceAll((key, value) -> switch (key) { + case "one" -> "2"; + case "two" -> "3"; + default -> value; + }); + + observer.assertMultipleCalls(call("one", "1", "2"), call("two", "2", "3")); + } + @ParameterizedTest @MethodSource("createParameters") @SuppressWarnings("unchecked") diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PlatformPreferences.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PlatformPreferences.java index 93b12999d48..6bc97b6eefe 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PlatformPreferences.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PlatformPreferences.java @@ -25,7 +25,8 @@ package com.sun.javafx.application.preferences; -import com.sun.javafx.binding.MapExpressionHelper; +import com.sun.javafx.collections.IterableMapChange; +import com.sun.javafx.collections.MapListenerHelper; import javafx.application.ColorScheme; import javafx.application.Platform; import javafx.beans.InvalidationListener; @@ -38,12 +39,10 @@ import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.CopyOnWriteArrayList; /** * Contains the implementation of a read-only map of platform preferences. @@ -76,8 +75,7 @@ public final class PlatformPreferences extends AbstractMap imple /** Contains the implementation of the property-based API. */ private final PreferenceProperties properties = new PreferenceProperties(this); - private final List invalidationListeners = new CopyOnWriteArrayList<>(); - private final List> mapChangeListeners = new CopyOnWriteArrayList<>(); + private MapListenerHelper helper; /** * Initializes this {@code PlatformPreferences} instance with the given platform-specific keys and key mappings. @@ -105,22 +103,22 @@ public Object remove(Object key) { @Override public void addListener(InvalidationListener listener) { - invalidationListeners.add(listener); + helper = MapListenerHelper.addListener(helper, listener); } @Override public void removeListener(InvalidationListener listener) { - invalidationListeners.remove(listener); + helper = MapListenerHelper.removeListener(helper, listener); } @Override public void addListener(MapChangeListener listener) { - mapChangeListeners.add(listener); + helper = MapListenerHelper.addListener(helper, listener); } @Override public void removeListener(MapChangeListener listener) { - mapChangeListeners.remove(listener); + helper = MapListenerHelper.removeListener(helper, listener); } @Override @@ -311,25 +309,22 @@ public void update(Map preferences) { } private void fireValueChangedEvent(Map changedEntries) { - invalidationListeners.forEach(listener -> listener.invalidated(this)); - var change = new MapExpressionHelper.SimpleChange<>(this); + var change = new IterableMapChange.Generic<>(this); for (Map.Entry entry : changedEntries.entrySet()) { Object oldValue = entry.getValue().oldValue(); Object newValue = entry.getValue().newValue(); if (oldValue == null && newValue != null) { - change.setAdded(entry.getKey(), newValue); + change.nextAdded(entry.getKey(), newValue); } else if (oldValue != null && newValue == null) { - change.setRemoved(entry.getKey(), oldValue); + change.nextRemoved(entry.getKey(), oldValue); } else { - change.setPut(entry.getKey(), oldValue, newValue); - } - - for (var listener : mapChangeListeners) { - listener.onChanged(change); + change.nextReplaced(entry.getKey(), oldValue, newValue); } } + + MapListenerHelper.fireValueChangedEvent(helper, change); } /**