| /* |
| * 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 org.apache.cocoon.forms.formmodel.tree; |
| |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| |
| import org.apache.cocoon.environment.Request; |
| import org.apache.cocoon.forms.FormContext; |
| import org.apache.cocoon.forms.event.WidgetEvent; |
| import org.apache.cocoon.forms.event.WidgetEventMulticaster; |
| import org.apache.cocoon.forms.formmodel.AbstractWidget; |
| import org.apache.cocoon.forms.formmodel.Widget; |
| import org.apache.cocoon.forms.formmodel.WidgetDefinition; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * A tree widget, heavily inspired by Swing's <code>JTree</code>. |
| * |
| * @version $Id$ |
| */ |
| public class Tree extends AbstractWidget { |
| |
| public static final int SINGLE_SELECTION = 0; |
| public static final int MULTIPLE_SELECTION = 1; |
| |
| public interface ActionHandler { |
| public void act(Tree tree, FormContext context); |
| } |
| |
| private TreeDefinition treeDef; |
| |
| private TreeModel treeModel; |
| |
| private Set expandedPaths = new HashSet(); |
| |
| private Set selectedPaths = new HashSet(); |
| |
| private Set changedPaths = new HashSet(); |
| |
| private HashMap pathWidgets = new HashMap(); |
| |
| private boolean rootVisible = true; |
| |
| private boolean expandSelectedPath = false; |
| |
| private TreeSelectionListener selectionListener; |
| |
| private int selectionModel = MULTIPLE_SELECTION; |
| |
| private TreeModelListener modelListener = new TreeModelListener() { |
| public void treeStructureChanged(TreeModelEvent event) { |
| markForRefresh(event.getPath()); |
| } |
| }; |
| |
| protected Tree(TreeDefinition definition) { |
| super(definition); |
| this.treeDef = definition; |
| this.rootVisible = definition.isRootVisible(); |
| if (!this.rootVisible) { |
| // Expand it so that first-level children are visible |
| this.expandedPaths.add(TreePath.ROOT_PATH); |
| } |
| this.treeModel = definition.createModel(); |
| this.treeModel.addTreeModelListener(modelListener); |
| this.selectionListener = definition.getSelectionListener(); |
| this.selectionModel = definition.getSelectionModel(); |
| } |
| |
| public WidgetDefinition getDefinition() { |
| return this.treeDef; |
| } |
| |
| protected String getXMLElementName() { |
| return "tree"; |
| } |
| |
| protected void generateItemSaxFragment(ContentHandler contentHandler, Locale locale) throws SAXException { |
| throw new UnsupportedOperationException(this + " cannot be rendered using <ft:widget>. Please use <ft:tree>."); |
| } |
| |
| public void readFromRequest(FormContext formContext) { |
| //TODO: crawl open nodes, calling their widget's readFromRequest |
| |
| Request req = formContext.getRequest(); |
| String paramName = getRequestParameterName(); |
| |
| //--------------------------------------------------------------------- |
| // Handle node selection using checkboxes named <id>$select |
| //--------------------------------------------------------------------- |
| String[] selectValues = req.getParameterValues(paramName + ":select"); |
| if (selectValues != null) { |
| // Create the set of paths given by the request |
| Set newSelection = new HashSet(); |
| for (int i = 0; i < selectValues.length; i++) { |
| newSelection.add(TreePath.valueOf(selectValues[i])); |
| } |
| |
| // Check if all visible selections are in the new selection |
| TreePath[] currentSelection = (TreePath[])this.selectedPaths.toArray(new TreePath[this.selectedPaths.size()]); |
| for (int i = 0; i < currentSelection.length; i++) { |
| TreePath p = currentSelection[i]; |
| if (!newSelection.contains(p) && isVisible(p)) { |
| removeSelectionPath(p); |
| } |
| } |
| |
| // Now add the currently selected items (may be selected already) |
| Iterator iter = newSelection.iterator(); |
| while(iter.hasNext()) { |
| addSelectionPath((TreePath)iter.next()); |
| } |
| } |
| |
| //--------------------------------------------------------------------- |
| // Handle tree actions: |
| // - action is in <name>$action |
| // - path is in <name>$path |
| //--------------------------------------------------------------------- |
| String action = req.getParameter(paramName + ":action"); |
| |
| if (action == null || action.length() == 0) { |
| // Nothing more to do here |
| return; |
| } |
| |
| getForm().setSubmitWidget(this); |
| String pathValue = req.getParameter(paramName + ":path"); |
| |
| if (pathValue == null || pathValue.length() == 0) { |
| //this.treeDef.getLogger().warn("No tree path given"); |
| return; |
| } |
| |
| // Parse the path |
| TreePath path = TreePath.valueOf(pathValue); |
| |
| if ("expand".equals(action)) { |
| this.expandPath(path); |
| } else if ("collapse".equals(action)) { |
| this.collapsePath(path); |
| } else if ("toggle-collapse".equals(action)) { |
| if (this.isExpanded(path)) { |
| this.collapsePath(path); |
| } else { |
| this.expandPath(path); |
| } |
| } else if ("select".equals(action)) { |
| this.addSelectionPath(path); |
| } else if ("unselect".equals(action)) { |
| this.removeSelectionPath(path); |
| } else if ("toggle-select".equals(action)) { |
| if (this.isPathSelected(path)) { |
| this.removeSelectionPath(path); |
| } else { |
| this.addSelectionPath(path); |
| } |
| } else { |
| // Unknown action |
| //this.treeDef.getLogger().warn("Unknown action " + action); |
| } |
| } |
| |
| public TreeModel getModel() { |
| return this.treeModel; |
| } |
| |
| public void setModel(TreeModel model) { |
| if (model == null) { |
| model = DefaultTreeModel.UNSPECIFIED_MODEL; |
| } |
| this.treeModel.removeTreeModelListener(this.modelListener); |
| this.treeModel = model; |
| model.addTreeModelListener(this.modelListener); |
| } |
| |
| private void markForRefresh(TreePath path) { |
| this.changedPaths.add(path); |
| getForm().addWidgetUpdate(this); |
| } |
| |
| private void markForRefresh(List paths) { |
| this.changedPaths.addAll(paths); |
| getForm().addWidgetUpdate(this); |
| } |
| |
| //--------------------------------------------------------------------------------------------- |
| // Selection |
| //--------------------------------------------------------------------------------------------- |
| |
| public void setSelectionModel(int model) { |
| if (model < 0 || model > MULTIPLE_SELECTION) { |
| throw new IllegalArgumentException("Illegal selection model " + model); |
| } |
| |
| if (model == this.selectionModel) { |
| return; |
| } |
| |
| this.selectionModel = model; |
| if (model == SINGLE_SELECTION && getSelectionCount() > 1) { |
| clearSelection(); |
| } |
| } |
| |
| public int getSelectionCount() { |
| return this.selectedPaths.size(); |
| } |
| |
| public TreePath getSelectionPath() { |
| if (this.selectedPaths.isEmpty()) { |
| return null; |
| } else { |
| return (TreePath)this.selectedPaths.iterator().next(); |
| } |
| } |
| |
| public TreePath[] getSelectionPaths() { |
| return (TreePath[])this.selectedPaths.toArray(new TreePath[this.selectedPaths.size()]); |
| } |
| |
| public boolean isPathSelected(TreePath path) { |
| return this.selectedPaths.contains(path); |
| } |
| |
| public boolean isSelectionEmpty() { |
| return this.selectedPaths.isEmpty(); |
| } |
| |
| public void setSelectionPath(TreePath path) { |
| clearSelection(); |
| addSelectionPath(path); |
| } |
| |
| public void setSelectionPaths(TreePath paths[]) { |
| clearSelection(); |
| addSelectionPaths(paths); |
| } |
| |
| public void addSelectionPath(TreePath path) { |
| if (this.selectionModel == SINGLE_SELECTION) { |
| clearSelection(); |
| } |
| |
| if (this.selectedPaths.add(path)) { |
| markForRefresh(path); |
| if (this.expandSelectedPath) { |
| expandPath(path); |
| } |
| this.getForm().addWidgetEvent(new TreeSelectionEvent(this, path, true)); |
| } |
| } |
| |
| public void addSelectionPaths(TreePath paths[]) { |
| if (this.selectionModel == SINGLE_SELECTION) { |
| setSelectionPath(paths[0]); |
| } else { |
| List pathsList = Arrays.asList(paths); |
| if (this.selectedPaths.addAll(pathsList)) { |
| markForRefresh(pathsList); |
| if (this.expandSelectedPath) { |
| this.expandedPaths.addAll(pathsList); |
| } |
| this.getForm().addWidgetEvent(new TreeSelectionEvent(this, paths, true)); |
| } |
| } |
| } |
| |
| public void removeSelectionPath(TreePath path) { |
| if (this.selectedPaths.remove(path)) { |
| // Need to redisplay the parent |
| markForRefresh(path.getParentPath()); |
| this.getForm().addWidgetEvent(new TreeSelectionEvent(this, path, false)); |
| } |
| } |
| |
| public void removeSelectionPaths(TreePath paths[]) { |
| List pathsList = Arrays.asList(paths); |
| if (this.selectedPaths.removeAll(pathsList)) { |
| markForRefresh(pathsList); |
| this.getForm().addWidgetEvent(new TreeSelectionEvent(this, paths, false)); |
| } |
| } |
| |
| public void clearSelection() { |
| if (this.isSelectionEmpty()) { |
| return; |
| } |
| |
| TreePath[] paths = (TreePath[])this.selectedPaths.toArray(new TreePath[this.selectedPaths.size()]); |
| for (int i = 0; i < paths.length; i++) { |
| // Need to redisplay the parent |
| markForRefresh(paths[i].getParentPath()); |
| } |
| this.selectedPaths.clear(); |
| this.getForm().addWidgetEvent(new TreeSelectionEvent(this, paths, false)); |
| } |
| |
| public void addTreeSelectionListener(TreeSelectionListener listener) { |
| this.selectionListener = WidgetEventMulticaster.add(this.selectionListener, listener); |
| } |
| |
| public void removeTreeSelectionListener(TreeSelectionListener listener) { |
| this.selectionListener = WidgetEventMulticaster.remove(this.selectionListener, listener); |
| } |
| |
| //--------------------------------------------------------------------------------------------- |
| // Visibility, expand & collapse |
| //--------------------------------------------------------------------------------------------- |
| |
| public boolean isCollapsed(TreePath path) { |
| return !isExpanded(path); |
| } |
| |
| public boolean isExpanded(TreePath path) { |
| if (this.expandedPaths.contains(path)) { |
| // Ensure all parents are expanded |
| TreePath parent = path.getParentPath(); |
| return parent == null ? true : isExpanded(parent); |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Returns true if the value identified by path is currently viewable, |
| * which means it is either the root or all of its parents are expanded. |
| * Otherwise, this method returns false. |
| * |
| * @return true if the node is viewable, otherwise false |
| */ |
| public boolean isVisible(TreePath path) { |
| if (path == TreePath.ROOT_PATH) { |
| return true; |
| } |
| if (path != null) { |
| TreePath parent = path.getParentPath(); |
| if (parent != null) { |
| return isExpanded(parent); |
| } else { |
| // root node |
| return true; |
| } |
| } else { |
| return false; |
| } |
| } |
| |
| public void makeVisible(TreePath path) { |
| if (path != null) { |
| TreePath parent = path.getParentPath(); |
| if (parent != null) { |
| expandPath(parent); |
| // Make visible also all parent paths |
| makeVisible(parent); |
| } |
| } |
| } |
| |
| public boolean isRootVisible() { |
| return this.rootVisible; |
| } |
| |
| public void setRootVisible(boolean visible) { |
| if (this.rootVisible != visible) { |
| this.markForRefresh(TreePath.ROOT_PATH); |
| this.rootVisible = visible; |
| if (!visible) { |
| // Expand it so that first-level children are visible |
| this.expandPath(TreePath.ROOT_PATH); |
| } |
| } |
| } |
| |
| public void collapsePath(TreePath path) { |
| if (path != null) { |
| if (this.expandedPaths.remove(path)) { |
| markForRefresh(path); |
| } |
| } |
| } |
| |
| public void expandPath(TreePath path) { |
| if (path != null) { |
| if (this.expandedPaths.add(path)) { |
| markForRefresh(path); |
| } |
| } |
| } |
| |
| public void collapseAll() { |
| this.expandedPaths.clear(); |
| if (!this.rootVisible) { |
| this.expandedPaths.add(TreePath.ROOT_PATH); |
| } |
| } |
| |
| public void expandAll() { |
| collapseAll(); |
| this.expandedPaths.add(TreePath.ROOT_PATH); |
| TreeWalker tw = new TreeWalker(this); |
| tw.enterChildren(); |
| while (tw.hasNext()) { |
| tw.next(); |
| if (!tw.isLeaf()) { |
| expandPath(tw.getPath()); |
| tw.enterChildren(); |
| } |
| if (!tw.hasNext()) { |
| tw.leave(); |
| } |
| } |
| } |
| |
| |
| public void setExpandsSelectedPath(boolean value) { |
| this.expandSelectedPath = value; |
| } |
| |
| //--------------------------------------------------------------------------------------------- |
| // Widget management |
| //--------------------------------------------------------------------------------------------- |
| |
| public Widget getWidgetForPath(TreePath path) { |
| Widget result = (Widget)this.pathWidgets.get(path); |
| if (result == null && !this.pathWidgets.containsKey(path)) { |
| result = createWidgetForPath(path); |
| if (result != null) { |
| result.setAttribute("TreePath", path); |
| } |
| this.pathWidgets.put(path, result); |
| } |
| |
| return result; |
| } |
| |
| private Widget createWidgetForPath(TreePath path) { |
| //TODO |
| return null; |
| } |
| |
| public void broadcastEvent(WidgetEvent event) { |
| if (event instanceof TreeSelectionEvent) { |
| if (this.selectionListener != null) { |
| this.selectionListener.selectionChanged((TreeSelectionEvent)event); |
| } |
| } else { |
| super.broadcastEvent(event); |
| } |
| } |
| //--------------------------------------------------------------------------------------------- |
| // TreeNode widget, which is the actual parent of widgets contained in a node |
| //--------------------------------------------------------------------------------------------- |
| // TODO |
| } |