blob: c17130b020d41c362e4ae876df19967403f42a77 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.ui.properties;
import com.android.tools.idea.ui.properties.exceptions.BindingCycleException;
import com.android.tools.idea.ui.properties.expressions.bool.BooleanExpressions;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import com.google.common.collect.Queues;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
/**
* Class which manages associations between source and destination values, updating the destination
* values when their source values change.
* <p/>
* One-way bindings and two-way bindings are supported. For more details, see
* {@link #bind(SettableValue, ObservableValue)} and
* {@link #bindTwoWay(SettableValue, SettableValue)}.
* <p/>
* Note: This class is currently not thread-safe. You are expected to read, write, and bind
* values on the dispatch thread to avoid undefined behavior.
*/
public final class BindingsManager {
/**
* Ensure bindings aren't registered in a way that causes an infinite loop. Valid update loops
* usually settle within 2 or 3 steps.
*/
private static final int MAX_CYCLE_COUNT = 10;
/**
* A strategy on how to invoke a binding update. Instead of invoking immediately, we often want
* to postpone the invocation, as that will allow us to avoid doing expensive updates on
* redundant, intermediate changes, e.g. a repaint will happen if width and/or height changes,
* and we only want to repaint once if both width and height are changed on the same frame.
*/
public interface InvokeStrategy {
void invoke(@NotNull Runnable runnable);
}
/**
* Useful invoke strategy when developing an IDEA Plugin.
*/
public static final InvokeStrategy APPLICATION_INVOKE_LATER_STRATEGY = new InvokeStrategy() {
@Override
public void invoke(@NotNull Runnable runnable) {
ApplicationManager.getApplication().invokeLater(runnable, ModalityState.any());
}
};
/**
* Useful invoke strategy when working on a Swing application.
*/
public static final InvokeStrategy SWING_INVOKE_LATER_STRATEGY = new InvokeStrategy() {
@Override
public void invoke(@NotNull Runnable runnable) {
//noinspection SSBasedInspection
SwingUtilities.invokeLater(runnable);
}
};
/**
* Useful invoke strategy for testing, when you don't care about delaying your binding updates.
*/
public static final InvokeStrategy INVOKE_IMMEDIATELY_STRATEGY = new InvokeStrategy() {
@Override
public void invoke(@NotNull Runnable runnable) {
runnable.run();
}
};
private final List<OneWayBinding<?>> myOneWayBindings = Lists.newArrayList();
private final List<TwoWayBinding<?>> myTwoWayBindings = Lists.newArrayList();
private final Queue<DestUpdater> myUpdaters = Queues.newArrayDeque();
private final Queue<DestUpdater> myDeferredUpdaters = Queues.newArrayDeque();
private boolean myUpdateInProgress;
private int myCycleCount;
private final InvokeStrategy myInvokeStrategy;
public BindingsManager() {
this(APPLICATION_INVOKE_LATER_STRATEGY);
}
public BindingsManager(InvokeStrategy invokeStrategy) {
myInvokeStrategy = invokeStrategy;
}
/**
* Binds one value to another. Whenever the source value changes, the destination value will
* be updated to reflect it.
* <p/>
* Setting a bound value is allowed but discouraged, as it will be overwritten as soon as the
* target value changes, and this may be hard to debug. If you are careful and know what you're
* doing, this can still be useful - for example, you might also wish to add a listener
* to the bound value and, detecting an external change, release the binding.
*/
public <T> void bind(@NotNull SettableValue<T> dest, @NotNull ObservableValue<T> src) {
bind(dest, src, BooleanExpressions.TRUE);
}
/**
* Like {@link #bind(SettableValue, ObservableValue)}, but takes an additional observable boolean
* which, while set to false, disables the binding.
* <p/>
* This can be useful for UI fields that are initially linked to each other but which may break
* that link later on.
*/
public <T> void bind(@NotNull SettableValue<T> dest, @NotNull ObservableValue<T> src, @NotNull ObservableValue<Boolean> enabled) {
release(dest);
myOneWayBindings.add(new OneWayBinding<T>(dest, src, enabled));
}
/**
* Binds two values to each other. Whenever either value changes, the other value will
* be updated to reflect it.
* <p/>
* Although both values can influence the other once bound, when this method is first called,
* the first parameter will be initialized with that of the second.
*/
public <T> void bindTwoWay(@NotNull SettableValue<T> first, @NotNull SettableValue<T> second) {
releaseTwoWay(first, second);
myTwoWayBindings.add(new TwoWayBinding<T>(first, second));
}
/**
* Releases a one-way binding previously registered via {@link #bind(SettableValue, ObservableValue)}
*/
public void release(@NotNull SettableValue<?> dest) {
Iterator<OneWayBinding<?>> i = myOneWayBindings.iterator();
while (i.hasNext()) {
OneWayBinding<?> binding = i.next();
if (binding.myDest == dest) {
binding.dispose();
i.remove();
return;
}
}
}
/**
* Releases a two-way binding previously registered via
* {@link #bindTwoWay(SettableValue, SettableValue)}.
*/
public <T> void releaseTwoWay(@NotNull SettableValue<T> first, @NotNull SettableValue<T> second) {
Iterator<TwoWayBinding<?>> i = myTwoWayBindings.iterator();
while (i.hasNext()) {
TwoWayBinding<?> binding = i.next();
if (binding.myLhs == first && binding.myRhs == second) {
binding.dispose();
i.remove();
return;
}
}
}
/**
* Release all bindings (one-way and two-way) registered with this bindings manager.
*/
public void releaseAll() {
for (OneWayBinding<?> oneWayBinding : myOneWayBindings) {
oneWayBinding.dispose();
}
myOneWayBindings.clear();
for (TwoWayBinding<?> twoWayBinding : myTwoWayBindings) {
twoWayBinding.dispose();
}
myTwoWayBindings.clear();
}
private void enqueueUpdater(@NotNull DestUpdater updater) {
if (myUpdateInProgress) {
if (!myDeferredUpdaters.contains(updater)) {
myDeferredUpdaters.add(updater);
}
return;
}
// Prepare to run an update if we're the first update request. Any other requests that are made
// before the update runs will get lumped in with it.
boolean shouldInvoke = myUpdaters.isEmpty();
if (!myUpdaters.contains(updater)) {
myUpdaters.add(updater);
}
if (shouldInvoke) {
invokeUpdate();
}
}
private void invokeUpdate() {
myInvokeStrategy.invoke(new Runnable() {
@Override
public void run() {
myUpdateInProgress = true;
for (DestUpdater updater : myUpdaters) {
updater.update();
}
myUpdaters.clear();
myUpdateInProgress = false;
if (!myDeferredUpdaters.isEmpty()) {
myCycleCount++;
if (myCycleCount > MAX_CYCLE_COUNT) {
throw new BindingCycleException();
}
myUpdaters.addAll(myDeferredUpdaters);
myDeferredUpdaters.clear();
invokeUpdate(); // Call self again with any bindings invalidated by this last cycle
}
else {
myCycleCount = 0;
}
}
});
}
private class OneWayBinding<T> extends InvalidationListener {
private final SettableValue<T> myDest;
private final ObservableValue<T> mySrc;
private final ObservableValue<Boolean> myEnabled;
@Override
protected void onInvalidated(@NotNull Observable sender) {
if (myEnabled.get()) {
enqueueUpdater(new DestUpdater<T>(myDest, mySrc));
}
}
public OneWayBinding(SettableValue<T> dest, ObservableValue<T> src, ObservableValue<Boolean> enabled) {
myDest = dest;
mySrc = src;
myEnabled = enabled;
mySrc.addListener(this);
myEnabled.addListener(this);
// Once bound, force the dest value to initialize itself with the src value
onInvalidated(src);
}
public void dispose() {
mySrc.removeListener(this);
myEnabled.removeListener(this);
}
}
private class TwoWayBinding<T> {
private final SettableValue<T> myLhs;
private final SettableValue<T> myRhs;
private final InvalidationListener myLeftChangedListener = new InvalidationListener() {
@Override
public void onInvalidated(@NotNull Observable sender) {
enqueueUpdater(new DestUpdater<T>(myRhs, myLhs));
}
};
private final InvalidationListener myRightChangedListener = new InvalidationListener() {
@Override
public void onInvalidated(@NotNull Observable sender) {
enqueueUpdater(new DestUpdater<T>(myLhs, myRhs));
}
};
public TwoWayBinding(SettableValue<T> lhs, SettableValue<T> rhs) {
myLhs = lhs;
myRhs = rhs;
myLhs.addListener(myLeftChangedListener);
myRhs.addListener(myRightChangedListener);
// Once bound, force the left value to initialize itself with the right value
myRightChangedListener.onInvalidated(rhs);
}
public void dispose() {
myLhs.removeListener(myLeftChangedListener);
myRhs.removeListener(myRightChangedListener);
}
}
/**
* Simple helper class which wraps source and destination values and can update the destination
* value on request. This class is used by both {@link OneWayBinding} and {@link TwoWayBinding}
* to enqueue an update after they detect a change.
*/
private static final class DestUpdater<T> {
private final SettableValue<T> myDest;
private final ObservableValue<T> mySrc;
public DestUpdater(SettableValue<T> dest, ObservableValue<T> src) {
myDest = dest;
mySrc = src;
}
public void update() {
myDest.set(mySrc.get());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DestUpdater<?> that = (DestUpdater<?>)o;
return Objects.equal(myDest, that.myDest) && Objects.equal(mySrc, that.mySrc);
}
@Override
public int hashCode() {
return Objects.hashCode(myDest, mySrc);
}
}
}