// 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.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.beans.IntrospectionException;
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.Collections;
import java.util.List;
import java.util.Set;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.apache.log4j.Logger;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.palantir.ptoss.cinch.core.Binding;
import com.palantir.ptoss.cinch.core.BindingContext;
import com.palantir.ptoss.cinch.core.BindingWiring;
import com.palantir.ptoss.cinch.core.ModelUpdate;
import com.palantir.ptoss.util.Mutator;
import com.palantir.ptoss.util.Throwables;
/**
* A binding for an interface component that has a selection like a {@link JList} or
* {@link JComboBox}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface BoundSelection {
/**
* The model property to bind to.
*/
String to();
/**
* When this binding should occur.
*/
String[] on() default "";
/**
* How to render the null value in a JComboBox.
*/
String nullValue() default "";
/**
* Whether or not a JList should bind with multiselect.
*/
boolean multi() default false;
/**
* Inner utility class that performs the runtime wiring of all {@link BoundSelection} bindings.
*/
public static class Wiring implements BindingWiring {
private static final Logger logger = Logger.getLogger(BoundSelection.class);
public Collection<Binding> wire(BindingContext context) {
List<Field> boundFields = context.getAnnotatedFields(BoundSelection.class);
List<Binding> bindings = Lists.newArrayList();
for (Field field : boundFields) {
BoundSelection bound = field.getAnnotation(BoundSelection.class);
try {
bindings.addAll(wire(bound, context, field));
} catch (Exception e) {
throw Throwables.throwUncheckedException(e);
}
}
return bindings;
}
private static Collection<Binding> wire(BoundSelection bound, BindingContext context, Field field) throws IntrospectionException {
String target = bound.to();
Mutator mutator = Mutator.create(context, target);
if (JList.class.isAssignableFrom(field.getType())) {
final JList list = context.getFieldObject(field, JList.class);
return bindJList(bound, mutator, list);
} else if (JComboBox.class.isAssignableFrom(field.getType())) {
final JComboBox combo = context.getFieldObject(field, JComboBox.class);
final String nullValue = (String)Bound.Utilities.getNullValue(context, bound.nullValue());
return bindJComboBox(bound, mutator, combo, nullValue);
} else {
throw new IllegalArgumentException("don't know how to wire up @BoundSelection field: " + field.getName());
}
}
private static Collection<Binding> bindJList(BoundSelection bound,
final Mutator mutator,
final JList list) {
final boolean multi = bound.multi();
final List<Object> ons = BindingContext.getOnObjects(bound.on(), mutator.getModel());
list.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting()) {
try {
if (multi) {
mutator.set(ImmutableList.copyOf(list.getSelectedValues()));
} else {
mutator.set(list.getSelectedValue());
}
} catch (Exception ex) {
logger.error("could not invoke JList binding", ex);
}
}
}
});
Binding binding = new Binding() {
public <T extends Enum<?> & ModelUpdate> void update(T... changed) {
if (!BindingContext.isOn(ons, changed)) {
return;
}
try {
if (multi) {
Object[] selVals = list.getSelectedValues();
ImmutableList<Object> listCurrent = ImmutableList.copyOf(selVals);
Collection<?> current = (Collection<?>)mutator.get();
if (current == null) {
current = ImmutableList.of();
}
if (!Iterables.elementsEqual(listCurrent, current)) {
Set<?> currentSet = Sets.newHashSet(current);
List<Integer> selIndices = Lists.newArrayList();
for (int i = 0; i < list.getModel().getSize(); i++) {
if (currentSet.contains(list.getModel().getElementAt(i))) {
selIndices.add(i);
}
}
int[] sel = new int[selIndices.size()];
int i = 0;
for (Integer index : selIndices) {
sel[i++] = index;
}
list.setSelectedIndices(sel);
}
} else {
Object current = mutator.get();
Object listCurrent = list.getSelectedValue();
if (!Objects.equal(current, listCurrent)) {
if (current == null) {
list.clearSelection();
} else {
list.setSelectedValue(current, true);
}
}
}
} catch (Exception ex) {
logger.error("could not invoke JList binding", ex);
}
}
};
mutator.getModel().bind(binding);
return Collections.singleton(binding);
}
private static Collection<Binding> bindJComboBox(final BoundSelection bound,
final Mutator mutator,
final JComboBox combo,
final String nullValue) {
combo.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
try {
Object current = mutator.get();
Object newValue = combo.getSelectedItem();
if (newValue != null && newValue.equals(nullValue)) {
newValue = null;
}
if (!Objects.equal(current, newValue)) {
mutator.set(newValue);
}
} catch (Exception ex) {
logger.error("could not invoke JComboBox binding", ex);
}
}
});
Binding binding = new Binding() {
public <T extends Enum<?> & ModelUpdate> void update(T... changed) {
try {
Object current = mutator.get();
if (current == null) {
current = nullValue;
}
if (!Objects.equal(combo.getSelectedItem(), current)) {
combo.setSelectedItem(current);
}
} catch (Exception ex) {
logger.error("could not invoke JComboBox binding", ex);
}
}
};
mutator.getModel().bind(binding);
return Collections.singleton(binding);
}
}
}