Skip to content
Permalink
Browse files
8258777: SkinBase: add api to un-/register invalidation-/listChange l…
…isteners

Reviewed-by: kcr, arapte
  • Loading branch information
Jeanette Winzenburg committed Apr 28, 2021
1 parent 33bbf3f commit cc70cdf
Show file tree
Hide file tree
Showing 6 changed files with 985 additions and 41 deletions.
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2021, 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
@@ -25,40 +25,95 @@

package com.sun.javafx.scene.control;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;

import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.function.Consumer;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;

/**
* Handler to manage multiple listeners to multiple observables. It handles
* adding/removing listeners for common notification events (change,
* invalidation and list change) on particular instances of observables. The
* listeners are wrapped into their weak counterparts to minimize the potential of
* memory leaks.
* <p>
* Clients can register consumers to be invoked on receiving notification.
* Un-/Registration api is separate per notification type and per observable.
* It is allowed to register multiple consumers per observable. They
* are executed in the order of registration. Note that unregistration
* of a given observable stops observing that observable (for the notification
* type of the unregistration) completely, that is none of the consumers
* previously registered with this handler will be executed after unregistering.
* <p>
* Disposing removes all listeners added by this handler from all registered observables.
*/
public final class LambdaMultiplePropertyChangeListenerHandler {
// FIXME JDK-8265401: name doesn't fit after widening to support more notification event types

@SuppressWarnings("rawtypes")
private static final Consumer EMPTY_CONSUMER = e -> {};

// support change listeners
private final Map<ObservableValue<?>, Consumer<ObservableValue<?>>> propertyReferenceMap;
private final ChangeListener<Object> propertyChangedListener;
private final WeakChangeListener<Object> weakPropertyChangedListener;

private static final Consumer<ObservableValue<?>> EMPTY_CONSUMER = e -> {};
// support invalidation listeners
private final Map<Observable, Consumer<Observable>> observableReferenceMap;
private final InvalidationListener invalidationListener;
private final WeakInvalidationListener weakInvalidationListener;

// support list change listeners
private final Map<ObservableList<?>, Consumer<Change<?>>> observableListReferenceMap;
private final ListChangeListener<Object> listChangeListener;
private final WeakListChangeListener<Object> weakListChangeListener;

public LambdaMultiplePropertyChangeListenerHandler() {
// change listening support
this.propertyReferenceMap = new HashMap<>();
this.propertyChangedListener = (observable, oldValue, newValue) -> {
// because all consumers are chained, this calls each consumer for the given property
// in turn.
propertyReferenceMap.getOrDefault(observable, EMPTY_CONSUMER).accept(observable);
};
this.weakPropertyChangedListener = new WeakChangeListener<>(propertyChangedListener);

// invalidation listening support
this.observableReferenceMap = new HashMap<>();
this.invalidationListener = obs -> {
observableReferenceMap.getOrDefault(obs, EMPTY_CONSUMER).accept(obs);
};
this.weakInvalidationListener = new WeakInvalidationListener(invalidationListener);

// list change listening support
this.observableListReferenceMap = new IdentityHashMap<>();
this.listChangeListener = change -> {
observableListReferenceMap.getOrDefault(change.getList(), EMPTY_CONSUMER).accept(change);
};
this.weakListChangeListener = new WeakListChangeListener<>(listChangeListener);
}

/**
* Subclasses can invoke this method to register that we want to listen to
* property change events for the given property.
* Registers a consumer to be invoked on change notification from the given property. Does nothing
* if property or consumer is null. Consumers registered to the same property will be executed
* in the order they have been registered.
*
* @param property
* @param property the property to observe for change notification
* @param consumer the consumer to be invoked on change notification from the property
*/
public final void registerChangeListener(ObservableValue<?> property, Consumer<ObservableValue<?>> consumer) {
if (consumer == null) return;
if (property == null || consumer == null) return;

// we only add a listener if the propertyReferenceMap does not contain the property
// (that is, we've added a consumer to this specific property for the first
@@ -70,17 +125,109 @@ public final void registerChangeListener(ObservableValue<?> property, Consumer<O
propertyReferenceMap.merge(property, consumer, Consumer::andThen);
}

// need to be careful here - removing all listeners on the specific property!
/**
* Stops observing the given property for change notification. Returns
* a single chained consumer consisting of all consumers registered with
* {@link #registerChangeListener(ObservableValue, Consumer)} in the order they
* have been registered.
*
* @param property the property to stop observing for change notification
* @return a single chained consumer consisting of all consumers registered for the given property
* or null if none has been registered or the property is null
*/
public final Consumer<ObservableValue<?>> unregisterChangeListeners(ObservableValue<?> property) {
if (property == null) return null;
property.removeListener(weakPropertyChangedListener);
return propertyReferenceMap.remove(property);
}

/**
* Registers a consumer to be invoked on invalidation notification from the given observable.
* Does nothing if observable or consumer is null. Consumers registered to the same observable will be executed
* in the order they have been registered.
*
* @param observable the observable to observe for invalidation notification
* @param consumer the consumer to be invoked on invalidation notification from the observable
*
*/
public final void registerInvalidationListener(Observable observable, Consumer<Observable> consumer) {
if (observable == null || consumer == null) return;
if (!observableReferenceMap.containsKey(observable)) {
observable.addListener(weakInvalidationListener);
}
observableReferenceMap.merge(observable, consumer, Consumer::andThen);
}

/**
* Stops observing the given observable for invalidation notification.
* Returns a single chained consumer consisting of all consumers registered with
* {@link #registerInvalidationListener(Observable, Consumer)} in the
* order they have been registered.
*
* @param observable the observable to stop observing for invalidation notification
* @return a single chained consumer consisting of all consumers registered for given observable
* or null if none has been registered or the observable is null
*
*/
public final Consumer<Observable> unregisterInvalidationListeners(Observable observable) {
if (observable == null) return null;
observable.removeListener(weakInvalidationListener);
return observableReferenceMap.remove(observable);
}

/**
* Registers a consumer to be invoked on list change notification from the given observable list.
* Does nothing if list or consumer is null. Consumers registered to the same observable list
* will be executed in the order they have been registered.
*
* @param list the observable list observe for list change notification
* @param consumer the consumer to be invoked on list change notification from the list
*
*/
public final void registerListChangeListener(ObservableList<?> list, Consumer<Change<?>> consumer) {
if (list == null || consumer == null) return;
if (!observableListReferenceMap.containsKey(list)) {
list.addListener(weakListChangeListener);
}
observableListReferenceMap.merge(list, consumer, Consumer::andThen);
}

/**
* Stops observing the given observable list for list change notification.
* Returns a single chained consumer consisting of all consumers registered with
* {@link #registerListChangeListener(ObservableList, Consumer)} in the order they have been registered.
*
* @param list the observable list to stop observing for list change notification
* @return a single chained consumer consisting of all consumers added for the given list
* or null if none has been registered or the list is null
*/
public final Consumer<Change<?>> unregisterListChangeListeners(ObservableList<?> list) {
if (list == null) return null;
list.removeListener(weakListChangeListener);
return observableListReferenceMap.remove(list);
}


/**
* Stops observing all types of notification from all registered observables.
* <p>
* Note: this handler is still usable after calling this method.
*/
public void dispose() {
// unhook listeners
// unhook change listeners
for (ObservableValue<?> value : propertyReferenceMap.keySet()) {
value.removeListener(weakPropertyChangedListener);
}
propertyReferenceMap.clear();
// unhook invalidation listeners
for (Observable value : observableReferenceMap.keySet()) {
value.removeListener(weakInvalidationListener);
}
observableReferenceMap.clear();
// unhook list change listeners
for (ObservableList<?> list : observableListReferenceMap.keySet()) {
list.removeListener(weakListChangeListener);
}
observableListReferenceMap.clear();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2021, 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
@@ -25,15 +25,18 @@

package javafx.scene.control;

import com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler;
import javafx.beans.value.ObservableValue;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

import com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler;

import javafx.beans.Observable;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.Styleable;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
@@ -206,39 +209,112 @@ protected final void consumeMouseEvents(boolean value) {


/**
* Subclasses can invoke this method to register that they want to listen to
* property change events for the given property. Registered {@link Consumer} instances
* will be executed in the order in which they are registered.
* @param property the property
* @param consumer the consumer
* Registers an operation to perform when the given {@code observable} sends a change event.
* Does nothing if either {@code observable} or {@code operation} are {@code null}.
* If multiple operations are registered on the same observable, they will be performed in the
* order in which they were registered.
*
* @param observable the observable to observe for change events, may be {@code null}
* @param operation the operation to perform when the observable sends a change event,
* may be {@code null}
*/
protected final void registerChangeListener(ObservableValue<?> property, Consumer<ObservableValue<?>> consumer) {
protected final void registerChangeListener(ObservableValue<?> observable, Consumer<ObservableValue<?>> operation) {
if (lambdaChangeListenerHandler == null) {
lambdaChangeListenerHandler = new LambdaMultiplePropertyChangeListenerHandler();
}
lambdaChangeListenerHandler.registerChangeListener(property, consumer);
lambdaChangeListenerHandler.registerChangeListener(observable, operation);
}

/**
* Unregisters all change listeners that have been registered using {@link #registerChangeListener(ObservableValue, Consumer)}
* for the given property. The end result is that the given property is no longer observed by any of the change
* listeners, but it may still have additional listeners registered on it through means outside of
* {@link #registerChangeListener(ObservableValue, Consumer)}.
* Unregisters all operations that have been registered using
* {@link #registerChangeListener(ObservableValue, Consumer)}
* for the given {@code observable}. Does nothing if {@code observable} is {@code null}.
*
* @param property The property for which all listeners should be removed.
* @return A single chained {@link Consumer} consisting of all {@link Consumer consumers} registered through
* {@link #registerChangeListener(ObservableValue, Consumer)}. If no consumers have been registered on this
* property, null will be returned.
* @param observable the observable for which the registered operations should be removed,
* may be {@code null}
* @return a composed consumer representing all previously registered operations, or
* {@code null} if none have been registered or the observable is {@code null}
* @since 9
*/
protected final Consumer<ObservableValue<?>> unregisterChangeListeners(ObservableValue<?> property) {
protected final Consumer<ObservableValue<?>> unregisterChangeListeners(ObservableValue<?> observable) {
if (lambdaChangeListenerHandler == null) {
return null;
}
return lambdaChangeListenerHandler.unregisterChangeListeners(property);
return lambdaChangeListenerHandler.unregisterChangeListeners(observable);
}

/**
* Registers an operation to perform when the given {@code observable} sends an invalidation event.
* Does nothing if either {@code observable} or {@code operation} are {@code null}.
* If multiple operations are registered on the same observable, they will be performed in the
* order in which they were registered.
*
* @param observable the observable to observe for invalidation events, may be {@code null}
* @param operation the operation to perform when the observable sends an invalidation event,
* may be {@code null}
* @since 17
*/
protected final void registerInvalidationListener(Observable observable, Consumer<Observable> operation) {
if (lambdaChangeListenerHandler == null) {
lambdaChangeListenerHandler = new LambdaMultiplePropertyChangeListenerHandler();
}
lambdaChangeListenerHandler.registerInvalidationListener(observable, operation);
}

/**
* Unregisters all operations that have been registered using
* {@link #registerInvalidationListener(Observable, Consumer)}
* for the given {@code observable}. Does nothing if {@code observable} is {@code null}.
*
* @param observable the observable for which the registered operations should be removed,
* may be {@code null}
* @return a composed consumer representing all previously registered operations, or
* {@code null} if none have been registered or the observable is {@code null}
* @since 17
*/
protected final Consumer<Observable> unregisterInvalidationListeners(Observable observable) {
if (lambdaChangeListenerHandler == null) {
return null;
}
return lambdaChangeListenerHandler.unregisterInvalidationListeners(observable);
}


/**
* Registers an operation to perform when the given {@code observableList} sends a list change event.
* Does nothing if either {@code observableList} or {@code operation} are {@code null}.
* If multiple operations are registered on the same observable list, they will be performed in the
* order in which they were registered.
*
* @param observableList the observableList to observe for list change events, may be {@code null}
* @param operation the operation to perform when the observableList sends a list change event,
* may be {@code null}
* @since 17
*/
protected final void registerListChangeListener(ObservableList<?> observableList, Consumer<Change<?>> operation) {
if (lambdaChangeListenerHandler == null) {
lambdaChangeListenerHandler = new LambdaMultiplePropertyChangeListenerHandler();
}
lambdaChangeListenerHandler.registerListChangeListener(observableList, operation);
}

/**
* Unregisters all operations that have been registered using
* {@link #registerListChangeListener(ObservableList, Consumer)}
* for the given {@code observableList}. Does nothing if {@code observableList} is {@code null}.
*
* @param observableList the observableList for which the registered operations should be removed,
* may be {@code null}
* @return a composed consumer representing all previously registered operations, or
* {@code null} if none have been registered or the observableList is {@code null}
* @since 17
*/
protected final Consumer<Change<?>> unregisterListChangeListeners(ObservableList<?> observableList) {
if (lambdaChangeListenerHandler == null) {
return null;
}
return lambdaChangeListenerHandler.unregisterListChangeListeners(observableList);
}


/***************************************************************************
@@ -28,7 +28,7 @@
import java.util.List;
import javafx.scene.Node;

public class SkinBaseShim<C extends Control> extends SkinBase {
public class SkinBaseShim<C extends Control> extends SkinBase<C> {

public SkinBaseShim(final C control) {
super(control);

1 comment on commit cc70cdf

@openjdk-notifier
Copy link

Choose a reason for hiding this comment

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

Please sign in to comment.