Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8258777: SkinBase: add api to un-/register invalidation-/listChange listeners #409

Closed
wants to merge 10 commits into from
@@ -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
Expand All @@ -25,40 +25,96 @@

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.
*
kevinrushforth marked this conversation as resolved.
Show resolved Hide resolved
*/
public final class LambdaMultiplePropertyChangeListenerHandler {
// FIXME: name doesn't fit after widening to support more notification event types
kevinrushforth marked this conversation as resolved.
Show resolved Hide resolved

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

Choose a reason for hiding this comment

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

Minor: remove extra space between public and static.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

removed


// 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
Expand All @@ -70,17 +126,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
Expand All @@ -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;
Expand Down Expand Up @@ -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}
kevinrushforth marked this conversation as resolved.
Show resolved Hide resolved
*/
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) {
kevinrushforth marked this conversation as resolved.
Show resolved Hide resolved
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);
}


/***************************************************************************
Expand Down
Expand Up @@ -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);
Expand Down