/* * This file is part of LibrePlan * * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e * Desenvolvemento Tecnolóxico de Galicia * Copyright (C) 2010-2011 Igalia, S.L. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.zkoss.ganttz; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.zkoss.ganttz.LeftTasksTreeRow.ILeftTasksTreeNavigator; import org.zkoss.ganttz.adapters.IDisabilityConfiguration; import org.zkoss.ganttz.data.Position; import org.zkoss.ganttz.data.Task; import org.zkoss.ganttz.data.TaskContainer; import org.zkoss.ganttz.data.TaskContainer.IExpandListener; import org.zkoss.ganttz.util.MutableTreeModel; import org.zkoss.zk.ui.Component; import org.zkoss.zk.ui.Executions; import org.zkoss.zk.ui.HtmlMacroComponent; import org.zkoss.zk.ui.event.OpenEvent; import org.zkoss.zul.Tree; import org.zkoss.zul.Treeitem; import org.zkoss.zul.TreeitemRenderer; /** * Tree element to display tasks structure in the planning Gantt. * <br /> * * @author Óscar González Fernández <ogonzalez@igalia.com> * @author Manuel Rego Casasnovas <mrego@igalia.com> * @author Lorenzo Tilve Álvaro <ltilve@igalia.com> */ public class LeftTasksTree extends HtmlMacroComponent { private DetailsForBeans detailsForBeans = new DetailsForBeans(); private final DeferredFiller deferredFiller = new DeferredFiller(); private final List<Task> tasks; private MutableTreeModel<Task> tasksTreeModel; private Tree tasksTree; private CommandContextualized<?> goingDownInLastArrowCommand; private final IDisabilityConfiguration disabilityConfiguration; private FilterAndParentExpandedPredicates predicate; private final List<Task> visibleTasks = new ArrayList<>(); private Planner planner; public LeftTasksTree(IDisabilityConfiguration disabilityConfiguration, Planner planner, FilterAndParentExpandedPredicates predicate) { this.disabilityConfiguration = disabilityConfiguration; this.tasks = planner.getTaskList().getAllTasks(); this.predicate = predicate; this.planner = planner; } @Override public void afterCompose() { setClass("listdetails"); super.afterCompose(); tasksTree = (Tree) getFellow("tasksTree"); tasksTreeModel = MutableTreeModel.create(Task.class); fillModel(tasks, true); tasksTree.setModel(tasksTreeModel); tasksTree.setItemRenderer(getTaskBeanRenderer()); /* Force call overridden render() */ try { if ( !tasks.isEmpty() ) { getTaskBeanRenderer().render(new Treeitem(""), tasks.get(0), 0); } } catch (Exception e) { e.printStackTrace(); } } private final class TaskBeanRenderer implements TreeitemRenderer<Task> { @Override public void render(final Treeitem treeitem, Task o, int i) throws Exception { Task task = o; treeitem.setOpen(isOpened(task)); if ( task instanceof TaskContainer ) { final TaskContainer container = (TaskContainer) task; IExpandListener expandListener = new IExpandListener() { @Override public void expandStateChanged(boolean isNowExpanded) { treeitem.setOpen(isNowExpanded); } }; container.addExpandListener(expandListener); } LeftTasksTreeRow leftTasksTreeRow = LeftTasksTreeRow.create(disabilityConfiguration, task, new TreeNavigator(task), planner); if ( task.isContainer() ) { expandWhenOpened((TaskContainer) task, treeitem); } /* Clear existing Treerows */ if ( !treeitem.getChildren().isEmpty() ) { treeitem.getChildren().clear(); } Component row = disabilityConfiguration.isTreeEditable() ? Executions .getCurrent() .createComponents("~./ganttz/zul/leftTasksTreeRow.zul", treeitem, null) : Executions .getCurrent() .createComponents("~./ganttz/zul/leftTasksTreeRowLabels.zul", treeitem, null); leftTasksTreeRow.doAfterCompose(row); detailsForBeans.put(task, leftTasksTreeRow); deferredFiller.isBeingRendered(task, treeitem); } private void expandWhenOpened(final TaskContainer taskBean, Treeitem item) { item.addEventListener("onOpen", event -> { OpenEvent openEvent = (OpenEvent) event; taskBean.setExpanded(openEvent.isOpen()); }); } } private TaskBeanRenderer getTaskBeanRenderer() { return new TaskBeanRenderer(); } public boolean isOpened(Task task) { return task.isLeaf() || task.isExpanded(); } private static final class DetailsForBeans { private Map<Task, LeftTasksTreeRow> map = new HashMap<>(); private Set<Task> focusRequested = new HashSet<>(); public void put(Task task, LeftTasksTreeRow leftTasksTreeRow) { map.put(task, leftTasksTreeRow); if ( focusRequested.contains(task) ) { focusRequested.remove(task); leftTasksTreeRow.receiveFocus(); } } public void requestFocusFor(Task task) { focusRequested.add(task); } public LeftTasksTreeRow get(Task taskbean) { return map.get(taskbean); } } private final class TreeNavigator implements ILeftTasksTreeNavigator { private final int[] pathToNode; private final Task task; private TreeNavigator(Task task) { this.task = task; this.pathToNode = tasksTreeModel.getPath(tasksTreeModel.getRoot(), task); } @Override public LeftTasksTreeRow getAboveRow() { Task parent = getParent(pathToNode); int lastPosition = pathToNode[pathToNode.length - 1]; if ( lastPosition != 0 ) { return getChild(parent, lastPosition - 1); } else if ( tasksTreeModel.getRoot() != parent ) { return getDetailFor(parent); } return null; } private LeftTasksTreeRow getChild(Task parent, int position) { return getDetailFor(tasksTreeModel.getChild(parent, position)); } private LeftTasksTreeRow getDetailFor(Task child) { return detailsForBeans.get(child); } @Override public LeftTasksTreeRow getBelowRow() { if ( isExpanded() && hasChildren() ) { return getChild(task, 0); } for (ChildAndParent childAndParent : group(task, tasksTreeModel.getParents(task))) { if ( childAndParent.childIsNotLast() ) { return getDetailFor(childAndParent.getNextToChild()); } } // It's the last one, it has none below return null; } public List<ChildAndParent> group(Task origin, List<Task> parents) { ArrayList<ChildAndParent> result = new ArrayList<>(); Task child = origin; for (Task parent : parents) { result.add(new ChildAndParent(child, parent)); child = parent; } return result; } private class ChildAndParent { private final Task parent; private final Task child; private Integer positionOfChildCached; private ChildAndParent(Task child, Task parent) { this.parent = parent; this.child = child; } public Task getNextToChild() { return tasksTreeModel.getChild(parent, getPositionOfChild() + 1); } public boolean childIsNotLast() { return getPositionOfChild() < numberOfChildrenForParent() - 1; } private int numberOfChildrenForParent() { return tasksTreeModel.getChildCount(parent); } private int getPositionOfChild() { if ( positionOfChildCached != null ) { return positionOfChildCached; } int[] path = tasksTreeModel.getPath(parent, child); positionOfChildCached = path[path.length - 1]; return positionOfChildCached; } } private boolean hasChildren() { return task.isContainer() && !task.getTasks().isEmpty(); } private boolean isExpanded() { return task.isContainer() && task.isExpanded(); } private Task getParent(int[] path) { Task current = tasksTreeModel.getRoot(); for (int i = 0; i < path.length - 1; i++) { current = tasksTreeModel.getChild(current, path[i]); } return current; } } /** * This class is a workaround for an issue with zk {@link Tree}. * Once the tree is created, adding a node with children is troublesome. * Only the top element is added to the tree, although the element has children. * The Tree discards the adding event for the children because the parent says it's not loaded. * * This is the condition that is not satisfied: * <br /> * <code> * if( parent != null && (!(parent instanceof Treeitem) || ((Treeitem) parent).isLoaded()) ) { * // ... * } * </code> * <br /> * * This problem is present in zk 3.6.1 at least. * * @author Óscar González Fernández <ogonzalez@igalia.com> * @see Tree#onTreeDataChange */ private class DeferredFiller { private Set<Task> pendingToAddChildren = new HashSet<>(); private Method setLoadedMethod = null; public void addParentOfPendingToAdd(Task parent) { pendingToAddChildren.add(parent); } public void isBeingRendered(final Task parent, final Treeitem item) { if ( !pendingToAddChildren.contains(parent) ) { return; } markLoaded(item); fillModel(parent, 0, parent.getTasks(), false); pendingToAddChildren.remove(parent); } private void markLoaded(Treeitem item) { try { Method method = getSetLoadedMethod(); method.invoke(item, true); } catch (Exception e) { throw new RuntimeException(e); } } private Method getSetLoadedMethod() { if ( setLoadedMethod != null ) { return setLoadedMethod; } try { Method method = Treeitem.class.getDeclaredMethod("setLoaded", Boolean.TYPE); method.setAccessible(true); setLoadedMethod = method; return setLoadedMethod; } catch (Exception e) { throw new RuntimeException(e); } } } private void fillModel(Collection<? extends Task> tasks, boolean firstTime) { fillModel(this.tasksTreeModel.getRoot(), 0, tasks, firstTime); } private void fillModel(Task parent, Integer insertionPosition, Collection<? extends Task> children, final boolean firstTime) { if ( predicate.isFilterContainers() ) { parent = this.tasksTreeModel.getRoot(); } if ( firstTime ) { for (Task node : children) { if ( predicate.accpetsFilterPredicateAndContainers(node) ) { if ( !visibleTasks.contains(node) ) { this.tasksTreeModel.add(parent, node); visibleTasks.add(node); } } else { if ( visibleTasks.contains(node) ) { this.tasksTreeModel.remove(node); visibleTasks.remove(node); } } if ( node.isContainer() ) { fillModel(node, 0, node.getTasks(), firstTime); } } } else { for (Task node : children) { if ( node.isContainer() ) { if ( predicate.accpetsFilterPredicateAndContainers(node) ) { if ( !visibleTasks.contains(node) ) { this.deferredFiller.addParentOfPendingToAdd(node); } } } } // The node must be added after, so the multistepTreeFiller is ready for (Task node : children) { if ( predicate.accpetsFilterPredicateAndContainers(node) ) { if ( !visibleTasks.contains(node) ) { this.tasksTreeModel.add(parent, insertionPosition, Collections.singletonList(node)); visibleTasks.add(node); } } else { if (visibleTasks.contains(node)) { this.tasksTreeModel.remove(node); removeTaskAndAllChildren(visibleTasks, node); } } if (node.isContainer()) { fillModel(node, 0, node.getTasks(), firstTime); } if (visibleTasks.contains(node)) { insertionPosition++; } } } } private void removeTaskAndAllChildren(List<Task> visibleTasks, Task task) { visibleTasks.remove(task); if ( task.isContainer() ) { for (Task node : task.getTasks()) { removeTaskAndAllChildren(visibleTasks, node); } } } public void taskRemoved(Task taskRemoved) { tasksTreeModel.remove(taskRemoved); } void addTask(Position position, Task task) { if ( position.isAppendToTop() ) { fillModel(Collections.singletonList(task), false); detailsForBeans.requestFocusFor(task); } else { List<Task> toAdd = Collections.singletonList(task); fillModel(position.getParent(), position.getInsertionPosition(), toAdd, false); } } public void addTasks(Position position, Collection<? extends Task> newTasks) { Task root = tasksTreeModel.getRoot(); if ( position.isAppendToTop() ) { fillModel(root, tasksTreeModel.getChildCount(root), newTasks, false); } else if ( position.isAtTop() ) { fillModel(root, position.getInsertionPosition(), newTasks, false); } else { fillModel(position.getParent(), position.getInsertionPosition(), newTasks, false); } } public CommandContextualized<?> getGoingDownInLastArrowCommand() { return goingDownInLastArrowCommand; } public void setGoingDownInLastArrowCommand(CommandContextualized<?> goingDownInLastArrowCommand) { this.goingDownInLastArrowCommand = goingDownInLastArrowCommand; } public void setPredicate(FilterAndParentExpandedPredicates predicate) { this.predicate = predicate; visibleTasks.clear(); tasksTreeModel = MutableTreeModel.create(Task.class); fillModel(tasks, true); tasksTree.setModel(tasksTreeModel); } }