/* * (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.widget.browser; import java.beans.PropertyChangeEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; 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.tree.DefaultTreeModel; import javax.swing.tree.TreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; 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.fib.controller.FIBController; import org.openflexo.fib.model.FIBBrowser; import org.openflexo.fib.model.FIBBrowserElement; import org.openflexo.fib.model.FIBBrowserElement.FIBBrowserElementChildren; import org.openflexo.fib.view.widget.FIBBrowserWidget; import org.openflexo.toolbox.ToolBox; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; public class FIBBrowserModel extends DefaultTreeModel implements TreeModel { private static final Logger logger = Logger.getLogger(FIBBrowserModel.class.getPackage().getName()); private Map<FIBBrowserElement, FIBBrowserElementType> _elementTypes; private FIBBrowser _fibBrowser; private final Multimap<Object, BrowserCell> contents; /** * Stores controls: key is the JButton and value the PropertyListActionListener */ // private Hashtable<JButton,PropertyListActionListener> _controls; public FIBBrowserModel(FIBBrowser fibBrowser, FIBBrowserWidget widget, FIBController controller) { super(null); contents = Multimaps.synchronizedMultimap(ArrayListMultimap.<Object, BrowserCell> create()); _fibBrowser = fibBrowser; // _widget = widget; _elementTypes = new Hashtable<FIBBrowserElement, FIBBrowserElementType>(); for (FIBBrowserElement browserElement : fibBrowser.getElements()) { addToElementTypes(browserElement, buildBrowserElementType(browserElement, controller)); } // } public void delete() { for (FIBBrowserElement c : _elementTypes.keySet()) { _elementTypes.get(c).delete(); } _elementTypes.clear(); _elementTypes = null; _fibBrowser = null; } public FIBBrowserElement elementForClass(Class aClass) { return _fibBrowser.elementForClass(aClass); } public FIBBrowserElementType elementTypeForClass(Class aClass) { FIBBrowserElement element = elementForClass(aClass); if (element != null) { return _elementTypes.get(element); } else { logger.warning("Could not find element for class " + aClass); return null; } } public FIBBrowser getBrowser() { return _fibBrowser; } /** * @param root * @return flag indicating if change was required */ public boolean updateRootObject(Object root) { if (root == null) { return false; } BrowserCell rootCell = retrieveBrowserCell(root, null); if (getRoot() != rootCell) { logger.fine("updateRootObject() with " + root + " rootCell=" + rootCell); setRoot(rootCell); return true; } return false; } public void fireTreeRestructured() { if (getRoot() instanceof BrowserCell) { ((BrowserCell) getRoot()).update(true); // nodeStructureChanged((BrowserCell)getRoot()); } } public void addToElementTypes(FIBBrowserElement element, FIBBrowserElementType elementType) { _elementTypes.put(element, elementType); elementType.setModel(this); } public void removeFromElementTypes(FIBBrowserElement element, FIBBrowserElementType elementType) { _elementTypes.remove(element); elementType.setModel(null); } public Map<FIBBrowserElement, FIBBrowserElementType> getElementTypes() { return _elementTypes; } private FIBBrowserElementType buildBrowserElementType(FIBBrowserElement browserElement, FIBController controller) { return new FIBBrowserElementType(browserElement, this, controller); } public TreePath[] getPaths(Object o) { Collection<BrowserCell> cells = contents.get(o); if (cells == null) { return new TreePath[0]; } TreePath[] paths = new TreePath[cells.size()]; int i = 0; for (BrowserCell cell : cells) { paths[i++] = getTreePath(cell); } return paths; } private TreePath getTreePath(BrowserCell cell) { Object[] path = getPathToRoot(cell); TreePath returned = new TreePath(path); return returned; } @Override public void valueForPathChanged(TreePath path, Object newValue) { BrowserCell cell = (BrowserCell) path.getLastPathComponent(); if (cell.getBrowserElementType().isLabelEditable() && newValue instanceof String) { cell.getBrowserElementType().setEditableLabelFor(cell.getRepresentedObject(), (String) newValue); } } public Multimap<Object, BrowserCell> getContents() { return contents; } public Iterator<Object> retrieveContents() { return contents.keys().iterator(); } private BrowserCell retrieveBrowserCell(Object representedObject, BrowserCell parent) { ArrayList<BrowserCell> cells = new ArrayList<FIBBrowserModel.BrowserCell>(contents.get(representedObject)); // Collection<BrowserCell> cells = contents.get(representedObject); if (cells != null) { for (BrowserCell cell : cells) { if (cell.getFather() == parent) { return cell; } } } BrowserCell returned = new BrowserCell(representedObject, parent); contents.put(representedObject, returned); return returned; } private void removeBrowserCell(BrowserCell cell) { contents.remove(cell.getRepresentedObject(), cell); } public boolean containsObject(Object representedObject) { return contents.get(representedObject) != null; } public Collection<BrowserCell> getBrowserCell(Object representedObject) { return contents.get(representedObject); } public class BrowserCell implements TreeNode, Observer, HasDependencyBinding { private Object representedObject; private FIBBrowserElementType browserElementType; private BrowserCell father; private final Vector<BrowserCell> children; private boolean isDeleted = false; private boolean isVisible = true; private DependingObjects dependingObjects; public BrowserCell(Object representedObject, BrowserCell father) { // logger.info("Build new BrowserCell for "+representedObject); this.representedObject = representedObject; browserElementType = elementTypeForClass(representedObject.getClass()); this.father = father; children = new Vector<BrowserCell>(); if (browserElementType != null) { dependingObjects = new DependingObjects(this); dependingObjects.refreshObserving(browserElementType); } update(false); } @Override public List<AbstractBinding> getDependencyBindings() { return getBrowserElementType().getDependencyBindings(representedObject); } @Override public List<TargetObject> getChainedBindings(AbstractBinding binding, final TargetObject object) { for (FIBBrowserElementChildren child : browserElementType.getBrowserElement().getChildren()) { if (binding.equals(child.getData().getBinding()) && child.getCast().isSet() && binding.getStringRepresentation().endsWith(object.propertyName)) { final Object bindingValue = child.getData().getBindingValue(browserElementType); List<?> list = ToolBox.getListFromIterable(bindingValue); if (list != null) { List<TargetObject> targetObjects = new ArrayList<TargetObject>(); for (final Object o : list) { List<TargetObject> targetObjects2 = child.getCast().getBinding() .getTargetObjects(new BindingEvaluationContext() { @Override public Object getValue(BindingVariable variable) { if (variable.getVariableName().equals("child")) { return o; } else { return browserElementType.getValue(variable); } } }); if (targetObjects2 != null) { targetObjects.addAll(targetObjects2); } } return targetObjects; } else { return child.getCast().getBinding().getTargetObjects(new BindingEvaluationContext() { @Override public Object getValue(BindingVariable variable) { if (variable.getVariableName().equals("child")) { return bindingValue; } else { return browserElementType.getValue(variable); } } }); } } } return null; } public void delete() { logger.fine("Delete BrowserCell for " + representedObject); for (BrowserCell c : children) { c.delete(); } if (dependingObjects != null) { dependingObjects.stopObserving(); } if (representedObject != null) { removeBrowserCell(this); } /* * GPO: Commented next line. We should check why we drop this representedObject from the selection * Why not also check if we are the current selected object? * By all means, we should find another way to do this than by doing it in the TreeModel. We could do that in FIBBrowserWidget.treeNodesRemoved. if (selection.contains(representedObject)) { selection.remove(representedObject); } */ this.representedObject = null; browserElementType = null; this.father = null; children.clear(); isDeleted = true; } public void update(boolean recursively) { // logger.info("**************** update() "+this); if (browserElementType == null) { logger.warning("Not element type registered for " + representedObject); return; } // Special case for cells that were declared as invisible // When becomes visible, must tells to parent to update if (!isVisible) { if (browserElementType.isVisible(representedObject)) { logger.fine("Cell " + this + " becomes visible"); father.update(recursively); } } List<BrowserCell> oldChildren = new ArrayList<BrowserCell>(children); List<BrowserCell> removedChildren = new ArrayList<BrowserCell>(children); List<BrowserCell> newChildren = new ArrayList<BrowserCell>(); final List<?> newChildrenObjects = /*(isEnabled ?*/browserElementType.getChildrenFor(representedObject) /*: new Vector())*/; int index = 0; for (Object o : newChildrenObjects) { if (o != null && o != representedObject) { BrowserCell cell = retrieveBrowserCell(o, this); FIBBrowserElementType childElementType = elementTypeForClass(o.getClass()); if (childElementType != null && childElementType.isVisible(o)) { if (children.contains(cell)) { // OK, child still here removedChildren.remove(cell); if (recursively) { cell.update(true); } index = children.indexOf(cell) + 1; } else { newChildren.add(cell); children.insertElementAt(cell, index); index++; } } else { cell.isVisible = false; } } } for (BrowserCell c : removedChildren) { children.remove(c); c.delete(); } boolean requireSorting = false; for (int i = 0; i < children.size() - 1; i++) { BrowserCell c1 = children.elementAt(i); BrowserCell c2 = children.elementAt(i + 1); if (newChildrenObjects.indexOf(c1.representedObject) != newChildrenObjects.indexOf(c2.representedObject) - 1) { requireSorting = true; } } if (requireSorting) { if (logger.isLoggable(Level.FINE)) { logger.fine("Detected sorting required"); } // Sort children according to supplied list Collections.sort(children, new Comparator<BrowserCell>() { @Override public int compare(BrowserCell o1, BrowserCell o2) { return newChildrenObjects.indexOf(o1.representedObject) - newChildrenObjects.indexOf(o2.representedObject); } }); } // System.out.println("removedChildren ["+removedChildren.size()+"] "+removedChildren); // System.out.println("newChildren ["+newChildren.size()+"] "+newChildren); // System.out.println("children ["+children.size()+"] "+children); boolean structureChanged = false; if (removedChildren.size() > 0 || newChildren.size() > 0) { structureChanged = true; if (oldChildren.size() == 0) { // Special case, i don't undertand why (SGU) // OK, issue seems to be MacOS only but workaround works on all platforms. // To observe the issue, load WKF module on a project that imports other projects // Imported workflow tree is not correctly initiated after reload of project. try { nodeStructureChanged(this); } catch (Exception e) { // Might happen when a structural modification will call parent's nodeChanged() // An Exception might be raised here // We should investigate further, but since no real consequences are raised here, we just ignore exception e.printStackTrace(); logger.warning("Unexpected " + e.getClass().getSimpleName() + " when refreshing browser, no severity but please investigate"); } } else { if (removedChildren.size() > 0) { int[] childIndices = new int[removedChildren.size()]; Object[] removedChildrenObjects = new Object[removedChildren.size()]; for (int i = 0; i < removedChildren.size(); i++) { childIndices[i] = oldChildren.indexOf(removedChildren.get(i)); removedChildrenObjects[i] = removedChildren.get(i); } nodesWereRemoved(this, childIndices, removedChildrenObjects); } if (newChildren.size() > 0) { int[] childIndices = new int[newChildren.size()]; for (int i = 0; i < newChildren.size(); i++) { childIndices[i] = children.indexOf(newChildren.get(i)); } nodesWereInserted(this, childIndices); } } } try { nodeChanged(this); } catch (ArrayIndexOutOfBoundsException e) { // Might happen when a structural modification will call parent's nodeChanged() // An ArrayIndexOutOfBoundsException might be raised here // We should investigate further, but since no real consequences are raised here, we just ignore exception e.printStackTrace(); logger.warning("Unexpected ArrayIndexOutOfBoundsException when refreshing browser, no severity but please investigate"); } catch (NullPointerException e) { // Might happen when a structural modification will call parent's nodeChanged() // An NullPointerException might be raised here // We should investigate further, but since no real consequences are raised here, we just ignore exception e.printStackTrace(); logger.warning("Unexpected NullPointerException when refreshing browser, no severity but please investigate"); } if (requireSorting) { /* Object wasSelected = getSelectedObject(); if (logger.isLoggable(Level.FINE)) { logger.fine("Will reselect " + wasSelected); } */ try { nodeStructureChanged(this); } catch (Exception e) { // Might happen when a structural modification will call parent's nodeChanged() // An Exception might be raised here // We should investigate further, but since no real consequences are raised here, we just ignore exception e.printStackTrace(); logger.warning("Unexpected " + e.getClass().getSimpleName() + " when refreshing browser, no severity but please investigate"); } /* if (wasSelected != null) { resetSelection(); addToSelection(wasSelected); } */ } dependingObjects.refreshObserving(browserElementType); } @Override public void update(Observable o, Object arg) { // logger.info("Object " + o + " received " + arg); if (!isDeleted && o == representedObject) { update(false); } } @Override public void propertyChange(PropertyChangeEvent evt) { // logger.info("Object " + representedObject + " received " + evt); if (!isDeleted) { // System.out.println("cell " + this + " propertyChanged " + evt.getPropertyName() + " for " + evt.getSource()); update(false); } } public Object getRepresentedObject() { return representedObject; } public FIBBrowserElement getBrowserElement() { return browserElementType.getBrowserElement(); } public FIBBrowserElementType getBrowserElementType() { return browserElementType; } @Override public Enumeration<BrowserCell> children() { return children.elements(); } @Override public boolean getAllowsChildren() { return true; } @Override public TreeNode getChildAt(int childIndex) { return children.get(childIndex); } @Override public int getChildCount() { return children.size(); } @Override public int getIndex(TreeNode node) { return children.indexOf(node); } public BrowserCell getFather() { return father; } @Override public TreeNode getParent() { return getFather(); } @Override public boolean isLeaf() { return getChildCount() == 0; } @Override public String toString() { return "BrowserCell[" + getRepresentedObject() + "]"; } public TreePath getTreePath() { return new TreePath(getPathToRoot(this)); } } }