blob: 48d9512045a3f59db9f8fbd9e4d265bbffb45916 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 groovy.swing.factory
import groovy.swing.SwingBuilder
import groovy.swing.binding.AbstractButtonProperties
import groovy.swing.binding.JComboBoxProperties
import groovy.swing.binding.JComponentProperties
import groovy.swing.binding.JListProperties
import groovy.swing.binding.JScrollBarProperties
import groovy.swing.binding.JSliderProperties
import groovy.swing.binding.JSpinnerProperties
import groovy.swing.binding.JTableProperties
import groovy.swing.binding.JTextComponentProperties
import org.apache.groovy.swing.binding.AggregateBinding
import org.apache.groovy.swing.binding.BindingUpdatable
import org.apache.groovy.swing.binding.ClosureSourceBinding
import org.apache.groovy.swing.binding.ClosureTriggerBinding
import org.apache.groovy.swing.binding.EventTriggerBinding
import org.apache.groovy.swing.binding.FullBinding
import org.apache.groovy.swing.binding.MutualPropertyBinding
import org.apache.groovy.swing.binding.PropertyBinding
import org.apache.groovy.swing.binding.SourceBinding
import org.apache.groovy.swing.binding.TargetBinding
import org.apache.groovy.swing.binding.TriggerBinding
import java.util.Map.Entry
/**
* @since Groovy 1.1
*/
class BindFactory extends AbstractFactory {
public static final String CONTEXT_DATA_KEY = "BindFactoryData";
final Map<String, TriggerBinding> syntheticBindings
BindFactory() {
syntheticBindings = new HashMap()
// covers JTextField.text
// covers JTextPane.text
// covers JTextArea.text
// covers JEditorPane.text
syntheticBindings.putAll(JTextComponentProperties.syntheticProperties)
// covers JCheckBox.selected
// covers JCheckBoxMenuItem.selected
// covers JRadioButton.selected
// covers JRadioButtonMenuItem.selected
// covers JToggleButton.selected
syntheticBindings.putAll(AbstractButtonProperties.syntheticProperties)
// covers JSlider.value
syntheticBindings.putAll(JSliderProperties.syntheticProperties)
// covers JScrollBar.value
syntheticBindings.putAll(JScrollBarProperties.syntheticProperties)
// JComboBox.elements / items
// JComboBox.selectedElement / selectedItem
syntheticBindings.putAll(JComboBoxProperties.syntheticProperties)
// JList.selectedElement / selectedItem / selectedElements / selectedItems / selectedIndex
syntheticBindings.putAll(JListProperties.syntheticProperties)
// JSpinner.value
syntheticBindings.putAll(JSpinnerProperties.syntheticProperties)
// other properties handled in JSR-295
// JTable.elements
// JTable.selectedElement
// JTable.selectedElements
syntheticBindings.putAll(JTableProperties.syntheticProperties)
// JTree.root
// JTree.selectedElement
// JTree.selectedElements
// covers JComponent.size
// covers JComponent.width
// covers JComponent.height
// covers JComponent.bounds
// covers JComponent.x
// covers JComponent.y
// covers JComponent.visible
syntheticBindings.putAll(JComponentProperties.syntheticProperties)
}
/**
* Accepted Properties...
*
* group?
* source ((sourceProperty) | (sourceEvent sourceValue))
* (target targetProperty)? (? use default javabeans property if targetProperty is not present?)
*
* @param builder
* @param name
* @param value
* @param attributes
* @return the newly created instance
* @throws InstantiationException
* @throws IllegalAccessException
*/
Object newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) throws InstantiationException, IllegalAccessException {
Object source = attributes.remove("source")
Object target = attributes.remove("target")
Object update = attributes.get("update")
Map bindContext = builder.context.get(CONTEXT_DATA_KEY) ?: [:]
if (bindContext.isEmpty()) {
builder.context.put(CONTEXT_DATA_KEY, bindContext)
}
TargetBinding tb = null
if (target != null) {
Object targetProperty = attributes.remove("targetProperty") ?: value
if (!(targetProperty instanceof CharSequence)) {
throw new IllegalArgumentException("Invalid value for targetProperty: (or node value)." +
" Value for this attribute must be a String but it is " + (targetProperty != null ? targetProperty.getClass().getName() : null))
}
tb = new PropertyBinding(target, targetProperty.toString(), update)
if (source == null) {
// if we have a target but no source assume the build context is the source and return
def result
if (attributes.remove("mutual")) {
result = new MutualPropertyBinding(null, null, tb, this.&getTriggerBinding)
} else {
result = tb
}
def newAttributes = [:]
newAttributes.putAll(attributes)
bindContext.put(result, newAttributes)
attributes.clear()
return result
}
}
FullBinding fb
boolean sea = attributes.containsKey("sourceEvent")
boolean sva = attributes.containsKey("sourceValue")
boolean spa = attributes.containsKey("sourceProperty") || value
if (sea && sva && !spa) {
// entirely event triggered binding
Closure queryValue = (Closure) attributes.remove("sourceValue")
ClosureSourceBinding csb = new ClosureSourceBinding(queryValue)
String trigger = (String) attributes.remove("sourceEvent")
EventTriggerBinding etb = new EventTriggerBinding(source, trigger)
fb = etb.createBinding(csb, tb)
} else if (spa && !(sea && sva)) {
// partially property driven binding
Object property = attributes.remove("sourceProperty") ?: value
if (!(property instanceof CharSequence)) {
throw new IllegalArgumentException("Invalid value for sourceProperty: (or node value). " +
"Value for this attribute must be a String but it is " + (property != null ? property.getClass().getName() : null))
}
if (source == null) {
// if we have a sourceProperty but no source then we're in trouble
throw new IllegalArgumentException("Missing value for source: even though sourceProperty: (or node value) " +
"was specified. Please check you didn't write bind(model.someProperty) instead of bind{ model.someProperty }")
}
PropertyBinding pb = new PropertyBinding(source, property.toString(), update)
TriggerBinding trigger
if (sea) {
// source trigger comes from an event
String triggerName = (String) attributes.remove("sourceEvent")
trigger = new EventTriggerBinding(source, triggerName)
} else {
// source trigger comes from a property change
// this method will also check for synthetic properties
trigger = getTriggerBinding(pb)
}
SourceBinding sb
if (sva) {
// source value comes from a value closure
Closure queryValue = (Closure) attributes.remove("sourceValue")
sb = new ClosureSourceBinding(queryValue)
} else {
// source value is the property value
sb = pb
}
// check for a mutual binding (bi-directional)
if (attributes.remove("mutual")) {
fb = new MutualPropertyBinding(trigger, sb, tb, this.&getTriggerBinding)
} else {
fb = trigger.createBinding(sb, tb)
}
} else if (!(sea || sva || spa)) {
// if no sourcing is defined then assume we are a closure binding and return
def newAttributes = [:]
newAttributes.putAll(attributes)
def ctb = new ClosureTriggerBinding(syntheticBindings)
bindContext.put(ctb, newAttributes)
attributes.clear()
return ctb
} else {
throw new RuntimeException("Both sourceEvent: and sourceValue: cannot be specified along with sourceProperty: or a value argument")
}
if (attributes.containsKey("value")) {
bindContext.put(fb, [value: attributes.remove("value")])
}
bindContext.get(fb, [:]).put('update', update)
Object o = attributes.remove("bind")
if (((o == null) && !attributes.containsKey('group'))
|| ((o instanceof Boolean) && ((Boolean) o).booleanValue())) {
fb.bind()
}
if ((attributes.group instanceof AggregateBinding) && fb != null) {
attributes.remove('group').addBinding(fb)
}
builder.addDisposalClosure(fb.&unbind)
return fb
}
void onNodeCompleted(FactoryBuilderSupport builder, Object parent, Object node) {
super.onNodeCompleted(builder, parent, node);
if (node instanceof FullBinding && node.sourceBinding && node.targetBinding) {
try {
node.update()
} catch (Exception ignored) {
// don't throw out to top
}
try {
node.rebind()
} catch (Exception ignored) {
// don't throw out to top
}
}
}
boolean onHandleNodeAttributes(FactoryBuilderSupport builder, Object node, Map attributes) {
attributes.remove('update')
true
}
boolean isLeaf() {
return false
}
boolean isHandlesNodeChildren() {
return true
}
boolean onNodeChildren(FactoryBuilderSupport builder, Object node, Closure childContent) {
if ((node instanceof FullBinding) && (node.converter == null)) {
node.converter = childContent
return false
} else if (node instanceof ClosureTriggerBinding) {
node.closure = childContent
return false
} else if (node instanceof TriggerBinding) {
def bindAttrs = builder.context.get(CONTEXT_DATA_KEY)[node] ?: [:]
if (!bindAttrs.containsKey("converter")) {
bindAttrs["converter"] = childContent
return false
}
}
throw new RuntimeException("Binding nodes do not accept child content when a converter is already specified")
}
TriggerBinding getTriggerBinding(PropertyBinding psb) {
String property = psb.propertyName
Class currentClass = psb.bean.getClass()
while (currentClass != null) {
// should we check interfaces as well? if so at what level?
def trigger = (TriggerBinding) syntheticBindings.get("$currentClass.name#$property" as String)
if (trigger != null) {
return trigger
}
currentClass = currentClass.getSuperclass()
}
//TODO inspect the bean info and throw an error if the property is not observable and not bind:false?
return psb
}
def bindingAttributeDelegate(FactoryBuilderSupport builder, def node, def attributes) {
Iterator iter = attributes.entrySet().iterator()
Map bindContext = builder.context.get(CONTEXT_DATA_KEY) ?: [:]
while (iter.hasNext()) {
Entry entry = (Entry) iter.next()
String property = entry.key.toString()
Object value = entry.value
def bindAttrs = bindContext.get(value) ?: [:]
def idAttr = builder.getAt(SwingBuilder.DELEGATE_PROPERTY_OBJECT_ID) ?: SwingBuilder.DEFAULT_DELEGATE_PROPERTY_OBJECT_ID
def id = bindAttrs.remove(idAttr)
if (bindAttrs.containsKey("value")) {
node."$property" = bindAttrs.remove("value")
}
def update = bindAttrs.get('update')
FullBinding fb
if (value instanceof MutualPropertyBinding) {
fb = (FullBinding) value
PropertyBinding psb = new PropertyBinding(node, property, update)
if (fb.sourceBinding == null) {
fb.sourceBinding = psb
finishContextualBinding(fb, builder, bindAttrs, id)
} else if (fb.targetBinding == null) {
fb.targetBinding = psb
}
} else if (value instanceof FullBinding) {
fb = (FullBinding) value
fb.targetBinding = new PropertyBinding(node, property, update)
} else if (value instanceof TargetBinding) {
PropertyBinding psb = new PropertyBinding(node, property, update)
fb = getTriggerBinding(psb).createBinding(psb, value)
finishContextualBinding(fb, builder, bindAttrs, id)
} else if (value instanceof ClosureTriggerBinding) {
PropertyBinding psb = new PropertyBinding(node, property, update)
fb = value.createBinding(value, psb)
finishContextualBinding(fb, builder, bindAttrs, id)
} else {
continue
}
try {
fb.update()
} catch (Exception e) {
// just eat it?
}
try {
fb.rebind()
} catch (Exception e) {
// just eat it?
}
// this is why we cannot use entrySet().each { }
iter.remove()
}
}
private finishContextualBinding(FullBinding fb, FactoryBuilderSupport builder, bindAttrs, id) {
bindAttrs.remove('update')
Object bindValue = bindAttrs.remove("bind")
List propertiesToBeSkipped = ['group']
bindAttrs.each { k, v -> if (!(k in propertiesToBeSkipped)) fb."$k" = v }
if ((bindAttrs.group instanceof AggregateBinding) && fb != null) {
bindAttrs.group.addBinding(fb)
}
if ((bindValue == null)
|| ((bindValue instanceof Boolean) && ((Boolean) bindValue).booleanValue())) {
fb.bind()
}
builder.addDisposalClosure(fb.&unbind)
// replaces ourselves in the variables
// id: is lost to us by now, so we just assume that any storage of us is a goner as well
//builder.getVariables().each{ Map.Entry me -> if (value.is(me.value)) me.setValue fb}
if (id) builder.setVariable(id, fb)
}
}