| /* |
| * 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) |
| } |
| |
| } |