// Copyright 2011 Palantir Technologies
//
// 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.palantir.ptoss.cinch.swing;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPasswordField;
import javax.swing.JProgressBar;
import javax.swing.JRadioButton;
import javax.swing.JSlider;
import javax.swing.JToggleButton;
import javax.swing.text.JTextComponent;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.mutable.MutableBoolean;
import org.apache.log4j.Logger;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.palantir.ptoss.cinch.core.Binding;
import com.palantir.ptoss.cinch.core.BindingContext;
import com.palantir.ptoss.cinch.core.BindingException;
import com.palantir.ptoss.cinch.core.BindingWiring;
import com.palantir.ptoss.cinch.core.Bindings;
import com.palantir.ptoss.cinch.core.ModelUpdate;
import com.palantir.ptoss.cinch.core.ModelUpdates;
import com.palantir.ptoss.cinch.core.WiringHarness;
import com.palantir.ptoss.util.Reflections;
import com.palantir.ptoss.util.Throwables;
import com.palantir.ptoss.util.Visitor;
/**
* Used to bind a UI control to a model property, where property is meant in a strict
* Java sense.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Bound {
/**
* The property to bind to.
*/
String to();
/**
* <p>
* Specifies the {@link ModelUpdate}s on which the binding will occur.
* Defaults to empty, which indicates all updates.
* </p>
* <p>
* <em>Note:</em> when bound to a specific {@link ModelUpdate}, a control will not
* receive not sent specifically to any {@link ModelUpdate} - i.e. default updates. To
* selectively answer a specific {@link ModelUpdate} and also still get default notications,
* add {@link ModelUpdates#UNSPECIFIED} to the list of {@link ModelUpdate}s passed to {@link #on()}.
* @see ModelUpdates
*/
String[] on() default "";
/**
* Used for enum/boolean values for {@link JRadioButton}s and {@link JToggleButton}s. Valid
* values for booleans are "true", "false", and unset. When "true" the button will indicate
* a selected state, "false" indicates unselected state, and unset will indicate the state
* of the backing boolean.
*/
String value() default "";
/**
* Used to add a null value to a {@link JComboBox}.
*/
String nullValue() default "";
/**
* Inner utility class to simplify figuring out how to display null values in controls.
*/
public static class Utilities {
/**
* If the provided nullValue is null, empty, or just whitespace then the returned value
* will be null. Otherwise, this function will look in the provided {@link BindingContext}
* to find a constant with the name of the nullValue string and return that. If there is
* no constant then the nullValue string itself will be returned.
* @see JComboBoxWiringHarness
* @see BoundSelection.Wiring
*/
public static Object getNullValue(BindingContext context, String nullValue) {
final String finalNullValue;
if (StringUtils.isBlank(nullValue)) {
finalNullValue = null;
} else {
Object constant = context.getBindableConstant(nullValue);
if (constant != null) {
finalNullValue = constant.toString();
} else {
finalNullValue = nullValue;
}
}
return finalNullValue;
}
}
/**
* Inner utility class that performs the runtime wiring of all bindings in this package.
*
* @see Bindings#STANDARD_BINDINGS
*/
public static class Wiring implements BindingWiring {
static final Logger logger = Logger.getLogger(Bound.class);
private static Map<Class<?>, WiringHarness<Bound, Field>> wiringHarnesses =
ImmutableMap.<Class<?>, WiringHarness<Bound, Field>>builder()
.put(AbstractButton.class, new AbstractButtonWiringHarness())
.put(JRadioButton.class, new JToggleButtonWiringHarness())
.put(JToggleButton.class, new JToggleButtonWiringHarness())
.put(JSlider.class, new JSliderWiringHarness())
.put(JProgressBar.class, new JProgressBarWiringHarness())
.put(JList.class, new JListWiringHarness())
.put(JComboBox.class, new JComboBoxWiringHarness())
.put(JTextComponent.class, new JTextComponentWiringHarness())
.put(JPasswordField.class, new JPasswordFieldWiringHarness())
.put(JLabel.class, new JLabelWiringHarness())
.build();
public Collection<Binding> wire(BindingContext context) throws IllegalArgumentException {
List<Field> boundFields = context.getAnnotatedFields(Bound.class);
List<Binding> bindings = Lists.newArrayList();
for (Field field : boundFields) {
Bound bound = field.getAnnotation(Bound.class);
bindings.addAll(wire(bound, context, field));
}
return bindings;
}
private static Collection<Binding> wire(final Bound bound, final BindingContext context, final Field field) {
final MutableBoolean didBind = new MutableBoolean();
final List<Binding> bindings = Lists.newArrayList();
Reflections.visitClassHierarchy(field.getType(), new Visitor<Class<?>>() {
public void visit(Class<?> klass){
if (didBind.booleanValue()) {
return;
}
WiringHarness<Bound, Field> wiringHarness = wiringHarnesses.get(klass);
if (wiringHarness == null) {
return;
}
try {
bindings.addAll(wiringHarness.wire(bound, context, field));
didBind.setValue(true);
} catch (Exception e) {
throw Throwables.throwUncheckedException(e);
}
}
});
if (!didBind.booleanValue()) {
throw new BindingException("don't know how to wire up @Bound field: " + field.getName());
}
return bindings;
}
}
}