/* * (c) Copyright 2010-2011 AgileBirds * * This file is part of OpenFlexo. * * OpenFlexo is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OpenFlexo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenFlexo. If not, see <http://www.gnu.org/licenses/>. * */ package org.openflexo.fib.view; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Observable; import java.util.Observer; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import org.openflexo.antar.binding.AbstractBinding; import org.openflexo.antar.binding.AbstractBinding.BindingEvaluationContext; import org.openflexo.antar.binding.AbstractBinding.TargetObject; import org.openflexo.antar.binding.BindingVariable; import org.openflexo.antar.binding.DependingObjects; import org.openflexo.antar.binding.DependingObjects.HasDependencyBinding; import org.openflexo.antar.binding.TypeUtils; import org.openflexo.fib.controller.FIBComponentDynamicModel; import org.openflexo.fib.controller.FIBController; import org.openflexo.fib.model.DataBinding; import org.openflexo.fib.model.FIBComponent; import org.openflexo.fib.model.FIBWidget; import org.openflexo.xmlcode.AccessorInvocationException; import org.openflexo.xmlcode.InvalidObjectSpecificationException; /** * Abstract class representing a widget view * * @author sylvain */ public abstract class FIBWidgetView<M extends FIBWidget, J extends JComponent, T> extends FIBView<M, J> implements FocusListener, Observer, PropertyChangeListener, HasDependencyBinding { private static final Logger logger = Logger.getLogger(FIBWidgetView.class.getPackage().getName()); public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif", Font.PLAIN, 11); public static final Font DEFAULT_MEDIUM_FONT = new Font("SansSerif", Font.PLAIN, 10); protected boolean modelUpdating = false; protected boolean widgetUpdating = false; private boolean enabled = true; public static final Dimension MINIMUM_SIZE = new Dimension(30, 25); private final DynamicFormatter formatter; private DynamicValueBindingContext valueBindingContext; private final DynamicEventListener eventListener; private DependingObjects dependingObjects; protected FIBWidgetView(M model, FIBController aController) { super(model, aController); formatter = new DynamicFormatter(); valueBindingContext = new DynamicValueBindingContext(); eventListener = new DynamicEventListener(); } @Override public synchronized void delete() { if (dependingObjects != null) { dependingObjects.stopObserving(); dependingObjects = null; } super.delete(); } public M getWidget() { return getComponent(); } /** * Update the widget retrieving data from the model. This method is called when the observed property change. * * @return boolean indicating if changes were required or not */ public abstract boolean updateWidgetFromModel(); /** * Update the model given the actual state of the widget * * @return boolean indicating if changes were required or not */ public abstract boolean updateModelFromWidget(); @Override public void focusGained(FocusEvent event) { if (logger.isLoggable(Level.FINE)) { logger.fine("focusGained()"); } gainFocus(); } @Override public void focusLost(FocusEvent event) { if (logger.isLoggable(Level.FINE)) { logger.fine("focusLost()"); } if (event.getOppositeComponent() != null && SwingUtilities.isDescendingFrom(event.getOppositeComponent(), getJComponent())) { // Not relevant in this case } else { looseFocus(); } } protected boolean _hasFocus; public void gainFocus() { if (getController().getFocusedWidget() != null && getController().getFocusedWidget()._hasFocus == true) { getController().getFocusedWidget().looseFocus(); } logger.fine("Getting focus: " + getWidget()); _hasFocus = true; getController().setFocusedWidget(this); } public void looseFocus() { logger.fine("Loosing focus: " + getWidget()); if (!modelUpdating && !isDeleted()) { updateModelFromWidget(); } _hasFocus = false; } public boolean isFocused() { return _hasFocus; } public T getValue() { if (isDeleted()) { return null; } if (getWidget().getData() == null || getWidget().getData().isUnset()) { if (getDynamicModel() != null) { logger.fine("Get dynamic model value: " + getDynamicModel().getData()); return getDynamicModel().getData(); } else { return null; } } if (getDataObject() == null) { return null; } try { T returned = (T) getWidget().getData().getBindingValue(getController()); if (getDynamicModel() != null) { getDynamicModel().setData(returned); } return returned; } catch (InvalidObjectSpecificationException e) { logger.warning("Widget " + getWidget() + " InvalidObjectSpecificationException: " + e.getMessage()); return null; } } public void setValue(T aValue) { if (!isEnabled()) { return; } if (isDeleted()) { return; } if (getWidget().getValueTransform() != null && getWidget().getValueTransform().isValid()) { T old = aValue; aValue = (T) getWidget().getValueTransform().getBindingValue(getValueBindingContext(aValue)); if (!equals(old, aValue)) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateWidgetFromModel(); } }); } } boolean isValid = true; if (getWidget().getValueValidator() != null && getWidget().getValueValidator().isValid()) { Object object = getWidget().getValueValidator().getBindingValue(getValueBindingContext(aValue)); if (object == null) { isValid = false; } else if (object instanceof Boolean) { isValid = (Boolean) object; } } if (!isValid) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateWidgetFromModel(); } }); return; } if (getDynamicModel() != null) { logger.fine("Sets dynamic model value with " + aValue + " for " + getComponent()); getDynamicModel().setData(aValue); } else { logger.fine("Dynamic model is null for " + getComponent()); } if (getWidget().getData() == null || getWidget().getData().isUnset()) { } else { if (getDataObject() == null) { return; } try { getWidget().getData().setBindingValue(aValue, getController()); } catch (AccessorInvocationException e) { getController().handleException(e.getCause()); } } updateDependancies(new Vector<FIBComponent>()); /* * Iterator<FIBComponent> it = getWidget().getMayAltersIterator(); while(it.hasNext()) { FIBComponent c = it.next(); * logger.info("Modified "+aValue+" now update "+c); getController().viewForComponent(c).update(); } */ if (getWidget().getValueChangedAction().isValid()) { getWidget().getValueChangedAction().execute(getController()); } } protected void updateDependingObjects() { if (dependingObjects == null) { dependingObjects = new DependingObjects(this); } dependingObjects.refreshObserving(getController()); } @Override public void update(Observable o, Object arg) { // System.out.println("Widget "+getWidget()+" : receive notification "+o); if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { update(new Vector<FIBComponent>()); } }); } else { update(new Vector<FIBComponent>()); } } @Override public void propertyChange(PropertyChangeEvent evt) { // System.out.println("Widget "+getWidget()+" : propertyChange "+evt); if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { update(new Vector<FIBComponent>()); } }); } else { update(new Vector<FIBComponent>()); } } @Override protected boolean checkValidDataPath() { if (getParentView() != null && !getParentView().checkValidDataPath()) { return false; } if (getComponent().getDataType() != null) { Object value = getValue(); if (value != null && !TypeUtils.isTypeAssignableFrom(getComponent().getDataType(), value.getClass(), true)) { // logger.fine("INVALID data path for component "+getComponent()); // logger.fine("Value is "+getValue().getClass()+" while expected type is "+getComponent().getDataType()); return false; } } return true; } protected void appendToDependingObjects(DataBinding binding, List<AbstractBinding> returned) { if (binding.isSet()) { returned.add(binding.getBinding()); } } @Override public List<AbstractBinding> getDependencyBindings() { List<AbstractBinding> returned = new ArrayList<AbstractBinding>(); appendToDependingObjects(getWidget().getData(), returned); appendToDependingObjects(getWidget().getVisible(), returned); appendToDependingObjects(getWidget().getEnable(), returned); return returned; } @Override public List<TargetObject> getChainedBindings(AbstractBinding binding, TargetObject object) { return null; } /** * This method is called to update view representing a FIBComponent.<br> * Callers are all the components that have been updated during current update loop. If the callers contains the component itself, does * nothing and return. * * @param callers * all the components that have been previously updated during current update loop * @return a flag indicating if component has been updated */ @Override public boolean update(List<FIBComponent> callers) { try { if (!super.update(callers)) { return false; } updateEnability(); // logger.info("Updating "+getWidget()+" value="+getValue()); // Add the component to the list of callers to avoid loops callers.add(getComponent()); if (isComponentVisible()) { updateDynamicTooltip(); updateDependingObjects(); if (updateWidgetFromModel()) { updateDependancies(callers); } } else if (checkValidDataPath()) { // Even if the component is not visible, its visibility may depend // it self from some depending component (which in that situation, // are very important to know, aren'they ?) updateDependingObjects(); } return true; } catch (Exception e) { logger.warning("Unexpected exception: " + e.getMessage()); e.printStackTrace(); return false; } } protected void updateDependancies(List<FIBComponent> callers) { if (getController() == null) { return; } // logger.info("updateDependancies() for " + getWidget()); Iterator<FIBComponent> it = getWidget().getMayAltersIterator(); while (it.hasNext()) { FIBComponent c = it.next(); // logger.info("###### Component " + getWidget() + ", has been updated, now update " + c); FIBView<?, ?> v = getController().viewForComponent(c); if (v != null) { v.update(callers); } else { logger.warning("Cannot find FIBView for component " + c); } } // logger.info("END updateDependancies() for " + getWidget()); } @Override public void updateDataObject(final Object aDataObject) { if (!SwingUtilities.isEventDispatchThread()) { if (logger.isLoggable(Level.WARNING)) { logger.warning("Update data object invoked outside the EDT!!! please investigate and make sure this is no longer the case. \n\tThis is a very SERIOUS problem! Do not let this pass."); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { updateDataObject(aDataObject); } }); return; } update(new Vector<FIBComponent>()); } @Override public void updateLanguage() { if (getValue() != null && getValue().getClass().isEnum() && getWidget().getLocalize()) { for (Object o : getValue().getClass().getEnumConstants()) { getStringRepresentation(o); } } } private DynamicValueBindingContext getValueBindingContext(T aValue) { if (valueBindingContext == null) { valueBindingContext = new DynamicValueBindingContext(); } valueBindingContext.setValue(aValue); return valueBindingContext; } /** * Return the effective base component to be added to swing hierarchy This component may be encapsulated in a JScrollPane * * @return JComponent */ @Override public abstract JComponent getJComponent(); /** * Return the dynamic JComponent, ie the component on which dynamic is applied, and were actions are effective. This component must be * either the same or a child of the one returned by {@link #getJComponent()}. * * @return J */ @Override public abstract J getDynamicJComponent(); public boolean isReadOnly() { return getWidget().getReadOnly(); } public final boolean isWidgetEnabled() { return isComponentEnabled(); } public final boolean isComponentEnabled() { boolean componentEnabled = true; if (getComponent().getReadOnly()) { return false; } if (getComponent().getEnable() != null && getComponent().getEnable().isValid()) { Object isEnabled = getComponent().getEnable().getBindingValue(getController()); if (isEnabled instanceof Boolean) { componentEnabled = (Boolean) isEnabled; } } return componentEnabled; } private void updateDynamicTooltip() { if (getComponent().getTooltip() != null && getComponent().getTooltip().isValid()) { String tooltipText = (String) getComponent().getTooltip().getBindingValue(getController()); getDynamicJComponent().setToolTipText(tooltipText); } } public String getStringRepresentation(final Object value) { if (value == null) { return ""; } if (getWidget().getFormat() != null && getWidget().getFormat().isValid()) { formatter.setValue(value); String returned = (String) getWidget().getFormat().getBindingValue(formatter); if (getWidget().getLocalize() && returned != null) { return getLocalized(returned); } else { return returned; } } if (value instanceof Enum) { String returned = value != null ? ((Enum<?>) value).name() : null; if (getWidget().getLocalize() && returned != null) { return getLocalized(returned); } else { return returned; } } if (value instanceof String) { if (getWidget().getLocalize()) { return getLocalized((String) value); } } return value.toString(); } public Icon getIconRepresentation(final Object value) { if (value == null) { return null; } if (getWidget().getIcon() != null && getWidget().getIcon().isValid()) { formatter.setValue(value); return (Icon) getWidget().getIcon().getBindingValue(formatter); } return null; } public void applySingleClickAction(MouseEvent event) { eventListener.setEvent(event); getWidget().getClickAction().execute(eventListener); } public void applyDoubleClickAction(MouseEvent event) { eventListener.setEvent(event); getWidget().getDoubleClickAction().execute(eventListener); } public void applyRightClickAction(MouseEvent event) { eventListener.setEvent(event); getWidget().getRightClickAction().execute(eventListener); } @Override public FIBComponentDynamicModel<T> createDynamicModel() { if (getWidget().getManageDynamicModel()) { return (FIBComponentDynamicModel<T>) super.createDynamicModel(); } return null; } @Override public FIBComponentDynamicModel<T> getDynamicModel() { return super.getDynamicModel(); } @Override public T getDefaultData() { return null; } @Override public void updateFont() { if (getFont() != null) { getDynamicJComponent().setFont(getFont()); } } public boolean isEnabled() { return enabled; } public final void updateEnability() { if (isComponentEnabled()) { if (!enabled) { // Becomes enabled logger.fine("Component becomes enabled"); // System.out.println("Component becomes enabled "+getJComponent()); enableComponent(getJComponent()); enabled = true; } } else { if (enabled) { // Becomes disabled logger.fine("Component becomes disabled"); // System.out.println("Component becomes disabled "+getJComponent()); disableComponent(getJComponent()); enabled = false; } } } private void enableComponent(Component component) { if (component instanceof JScrollPane) { component = ((JScrollPane) component).getViewport().getView(); if (component == null) { return; } } component.setEnabled(true); if (component instanceof Container) { for (Component c : ((Container) component).getComponents()) { enableComponent(c); } } } private void disableComponent(Component component) { if (component instanceof JScrollPane) { component = ((JScrollPane) component).getViewport().getView(); if (component == null) { return; } } component.setEnabled(false); if (component instanceof Container) { for (Component c : ((Container) component).getComponents()) { disableComponent(c); } } } protected class DynamicValueBindingContext implements BindingEvaluationContext { private Object value; private void setValue(Object aValue) { value = aValue; } @Override public Object getValue(BindingVariable variable) { if (variable.getVariableName().equals("value")) { return value; } else { return getController().getValue(variable); } } } protected class DynamicFormatter implements BindingEvaluationContext { private Object value; private void setValue(Object aValue) { value = aValue; } @Override public Object getValue(BindingVariable variable) { if (variable.getVariableName().equals("object")) { return value; } else { return getController().getValue(variable); } } } protected class DynamicEventListener implements BindingEvaluationContext { private MouseEvent mouseEvent; private void setEvent(MouseEvent mouseEvent) { this.mouseEvent = mouseEvent; } @Override public Object getValue(BindingVariable variable) { if (variable.getVariableName().equals("event")) { return mouseEvent; } else { return getController().getValue(variable); } } } }