/*
GanttProject is an opensource project management tool.
Copyright (C) 2011 Dmitry Barashev
This program 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.
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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.sourceforge.ganttproject;
import biz.ganttproject.core.model.task.TaskDefaultColumn;
import biz.ganttproject.core.option.DefaultBooleanOption;
import biz.ganttproject.core.option.ValidationException;
import biz.ganttproject.core.time.CalendarFactory;
import biz.ganttproject.core.time.GanttCalendar;
import biz.ganttproject.core.time.TimeDuration;
import biz.ganttproject.core.time.impl.GPTimeUnitStack;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import net.sourceforge.ganttproject.gui.UIFacade;
import net.sourceforge.ganttproject.language.GanttLanguage;
import net.sourceforge.ganttproject.task.CustomColumnsException;
import net.sourceforge.ganttproject.task.ResourceAssignment;
import net.sourceforge.ganttproject.task.Task;
import net.sourceforge.ganttproject.task.TaskManager;
import net.sourceforge.ganttproject.task.TaskNode;
import net.sourceforge.ganttproject.task.TaskProperties;
import net.sourceforge.ganttproject.task.dependency.TaskDependency;
import net.sourceforge.ganttproject.task.dependency.TaskDependencyException;
import org.jdesktop.swingx.treetable.DefaultMutableTreeTableNode;
import org.jdesktop.swingx.treetable.DefaultTreeTableModel;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import java.math.BigDecimal;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Comparator;
/**
* This class is the model for GanttTreeTable to display tasks.
*
* @author bbaranne (Benoit Baranne)
*/
public class GanttTreeTableModel extends DefaultTreeTableModel implements TableColumnModelListener {
private static class Icons {
static ImageIcon ALERT_TASK_INPROGRESS = new ImageIcon(GanttTreeTableModel.class.getResource("/icons/alert1_16.gif"));
static ImageIcon ALERT_TASK_OUTDATED = new ImageIcon(GanttTreeTableModel.class.getResource("/icons/alert2_16.gif"));
}
static Predicate<Task> NOT_SUPERTASK = new Predicate<Task>() {
@Override
public boolean apply(Task task) {
return !task.isSupertask();
}
};
static Predicate<Task> NOT_MILESTONE = new Predicate<Task>() {
@Override
public boolean apply(Task input) {
return !input.isMilestone();
}
};
static {
new DefaultBooleanOption("");
TaskDefaultColumn.setLocaleApi(new TaskDefaultColumn.LocaleApi() {
@Override
public String i18n(String key) {
return GanttLanguage.getInstance().getText(key);
}
});
}
private static GanttLanguage language = GanttLanguage.getInstance();
private final CustomPropertyManager myCustomColumnsManager;
private final UIFacade myUiFacade;
private final Runnable myDirtyfier;
private static final int STANDARD_COLUMN_COUNT = TaskDefaultColumn.values().length;
/**
* Creates an instance of GanttTreeTableModel with a root.
*
* @param root
* The root.
* @param customColumnsManager
* @param dirtyfier
*/
public GanttTreeTableModel(
TaskManager taskManager, CustomPropertyManager customColumnsManager, UIFacade uiFacade, Runnable dirtyfier) {
super(new TaskNode(taskManager.getRootTask()));
TaskDefaultColumn.BEGIN_DATE.setIsEditablePredicate(NOT_SUPERTASK);
TaskDefaultColumn.BEGIN_DATE.setSortComparator(new BeginDateComparator());
TaskDefaultColumn.END_DATE.setIsEditablePredicate(Predicates.and(NOT_SUPERTASK, NOT_MILESTONE));
TaskDefaultColumn.END_DATE.setSortComparator(new EndDateComparator());
TaskDefaultColumn.DURATION.setIsEditablePredicate(Predicates.and(NOT_SUPERTASK, NOT_MILESTONE));
myUiFacade = uiFacade;
myDirtyfier = dirtyfier;
myCustomColumnsManager = customColumnsManager;
}
private static class BeginDateComparator implements Comparator<Task> {
@Override
public int compare(Task t1, Task t2) {
return t1.getStart().compareTo(t2.getStart());
}
}
private static class EndDateComparator implements Comparator<Task> {
@Override
public int compare(Task t1, Task t2) {
return t1.getEnd().compareTo(t2.getEnd());
}
}
@Override
public int getColumnCount() {
return STANDARD_COLUMN_COUNT + myCustomColumnsManager.getDefinitions().size();
}
/**
* Invoked this to insert newChild at location index in parents children. This
* will then message nodesWereInserted to create the appropriate event. This
* is the preferred way to add children as it will create the appropriate
* event.
*/
// @Override
// public void insertNodeInto(MutableTreeTableNode newChild,
// MutableTreeTableNode parent, int index) {
// parent.insert(newChild, index);
//
// int[] newIndexs = new int[1];
//
// newIndexs[0] = index;
// modelSupport.fireChildAdded(TreeUtil.createPath(parent), index, child);
// }
/**
* Message this to remove node from its parent. This will message
* nodesWereRemoved to create the appropriate event. This is the preferred way
* to remove a node as it handles the event creation for you.
*/
// @Override
// public void removeNodeFromParent(MutableTreeTableNode node) {
// MutableTreeTableNode parent = (MutableTreeTableNode) node.getParent();
//
// if (parent == null)
// throw new IllegalArgumentException("node does not have a parent.");
//
// int[] childIndex = new int[1];
// Object[] removedArray = new Object[1];
//
// childIndex[0] = parent.getIndex(node);
// parent.remove(childIndex[0]);
// removedArray[0] = node;
// nodesWereRemoved(parent, childIndex, removedArray);
// }
@Override
public String getColumnName(int column) {
if (column >=0 && column < STANDARD_COLUMN_COUNT) {
return GanttLanguage.getInstance().getText(TaskDefaultColumn.values()[column].getNameKey());
}
CustomPropertyDefinition customColumn = getCustomProperty(column);
return customColumn.getName();
}
@Override
public int getHierarchicalColumn() {
return TaskDefaultColumn.NAME.ordinal();
}
@Override
public Class<?> getColumnClass(int column) {
if (column < 0) {
return null;
}
if (column >= 0 && column < STANDARD_COLUMN_COUNT) {
return TaskDefaultColumn.values()[column].getValueClass();
}
CustomPropertyDefinition customColumn = getCustomProperty(column);
Class<?> result = customColumn == null ? String.class : customColumn.getType();
return result;
}
private CustomPropertyDefinition getCustomProperty(int columnIndex) {
assert columnIndex >= STANDARD_COLUMN_COUNT : "We have " + STANDARD_COLUMN_COUNT + " default properties, and custom property index starts at " + STANDARD_COLUMN_COUNT + ". I've got index #"
+ columnIndex + ". Something must be wrong here";
List<CustomPropertyDefinition> definitions = myCustomColumnsManager.getDefinitions();
columnIndex -= STANDARD_COLUMN_COUNT;
return columnIndex < definitions.size() ? definitions.get(columnIndex) : null;
}
@Override
public boolean isCellEditable(Object node, int column) {
if (node instanceof TaskNode) {
Task task = (Task) ((TaskNode) node).getUserObject();
if (column >=0 && column < STANDARD_COLUMN_COUNT) {
return TaskDefaultColumn.values()[column].isEditable(task);
}
return true;
}
return false;
}
@Override
public Object getValueAt(Object node, int column) {
if (column < 0) {
return "";
}
if (!(node instanceof TaskNode)) {
return null;
}
Object res = null;
TaskNode tn = (TaskNode) node;
Task t = (Task) tn.getUserObject();
if (column < STANDARD_COLUMN_COUNT) {
TaskDefaultColumn defaultColumn = TaskDefaultColumn.values()[column];
switch (defaultColumn) {
case TYPE:
if (((Task) tn.getUserObject()).isProjectTask()) {
res = new ImageIcon(getClass().getResource("/icons/mproject.gif"));
} else if (!tn.isLeaf())
res = new ImageIcon(getClass().getResource("/icons/mtask.gif"));
else if (t.isMilestone()) {
res = new ImageIcon(getClass().getResource("/icons/meeting.gif"));
} else {
res = new ImageIcon(getClass().getResource("/icons/tasks2.png"));
}
break;
case PRIORITY:
GanttTask task = (GanttTask) tn.getUserObject();
res = new ImageIcon(getClass().getResource(task.getPriority().getIconPath()));
break;
case INFO:
// TODO(dbarashev): implement alerts some other way
if (t.getCompletionPercentage() < 100) {
Calendar c = GanttCalendar.getInstance();
if (t.getStart().before(c)) {
res = Icons.ALERT_TASK_INPROGRESS;
}
if (t.getEnd().before(GanttCalendar.getInstance())) {
res = Icons.ALERT_TASK_OUTDATED;
}
}
break;
case NAME:
res = tn.getName();
break;
case BEGIN_DATE:
res = tn.getStart();
break;
case END_DATE:
res = t.getDisplayEnd();
break;
case DURATION:
res = new Integer(tn.getDuration());
break;
case COMPLETION:
res = new Integer(tn.getCompletionPercentage());
break;
case COORDINATOR:
ResourceAssignment[] tAssign = t.getAssignments();
StringBuffer sb = new StringBuffer();
int nb = 0;
for (int i = 0; i < tAssign.length; i++) {
ResourceAssignment resAss = tAssign[i];
if (resAss.isCoordinator()) {
sb.append(nb++ == 0 ? "" : ", ").append(resAss.getResource().getName());
}
}
res = sb.toString();
break;
case PREDECESSORS:
res = TaskProperties.formatPredecessors(t, ",", true);
break;
case ID:
res = t.getTaskID();
break;
case OUTLINE_NUMBER:
List<Integer> outlinePath = t.getManager().getTaskHierarchy().getOutlinePath(t);
res = Joiner.on('.').join(outlinePath);
break;
case COST:
res = t.getCost().getValue();
break;
case RESOURCES:
List<String> resources = Lists.transform(Arrays.asList(t.getAssignments()), new Function<ResourceAssignment, String>() {
@Override
public String apply(ResourceAssignment ra) {
return ra.getResource().getName();
}
});
res = Joiner.on(',').join(resources);
break;
default:
break;
}
} else {
CustomPropertyDefinition customColumn = getCustomProperty(column);
res = t.getCustomValues().getValue(customColumn);
}
// if(tn.getParent()!=null){
return res;
}
@Override
public void setValueAt(final Object value, final Object node, final int column) {
if (value == null) {
return;
}
if (isCellEditable(node, column) && !Objects.equal(value, getValueAt(node, column))) {
// System.out.println("undoable column: " + column);
myUiFacade.getUndoManager().undoableEdit("Change properties column", new Runnable() {
@Override
public void run() {
setValue(value, node, column);
}
});
} else {
// System.out.println("NOT undoable column: " + column);
setValue(value, node, column);
}
myUiFacade.getActiveChart().reset();
}
/**
* Set value in left pane cell
*
* @param value
* @param node
* @param column
*/
private void setValue(final Object value, final Object node, final int column) {
myDirtyfier.run();
if (column >= STANDARD_COLUMN_COUNT) {
setCustomPropertyValue(value, node, column);
return;
}
assert node instanceof TaskNode : "Tree node=" + node + " is not a task node";
final Task task = (Task) ((TaskNode)node).getUserObject();
TaskDefaultColumn property = TaskDefaultColumn.values()[column];
switch (property) {
case NAME:
((TaskNode) node).setName(value.toString());
break;
case BEGIN_DATE:
((TaskNode) node).setStart((GanttCalendar) value);
((TaskNode) node).applyThirdDateConstraint();
break;
case END_DATE:
((TaskNode) node).setEnd(CalendarFactory.createGanttCalendar(
GPTimeUnitStack.DAY.adjustRight(((GanttCalendar)value).getTime())));
break;
case DURATION:
TimeDuration tl = task.getDuration();
((TaskNode) node).setDuration(task.getManager().createLength(tl.getTimeUnit(), ((Integer) value).intValue()));
break;
case COMPLETION:
((TaskNode) node).setCompletionPercentage(((Integer) value).intValue());
break;
case PREDECESSORS:
//List<Integer> newIds = Lists.newArrayList();
List<String> specs = Lists.newArrayList();
for (String s : String.valueOf(value).split(",")) {
if (!s.trim().isEmpty()) {
specs.add(s.trim());
}
}
Map<Integer, Supplier<TaskDependency>> promises;
try {
promises = TaskProperties.parseDependencies(
specs, task, new Function<Integer, Task>() {
@Override
public Task apply(@Nullable Integer id) {
return task.getManager().getTask(id);
}
});
TaskManager taskManager = task.getManager();
taskManager.getAlgorithmCollection().getScheduler().setEnabled(false);
task.getDependenciesAsDependant().clear();
for (Supplier<TaskDependency> promise : promises.values()) {
promise.get();
}
taskManager.getAlgorithmCollection().getScheduler().setEnabled(true);
} catch (IllegalArgumentException | TaskDependencyException e) {
throw new ValidationException(e);
}
break;
case COST:
try {
BigDecimal cost = new BigDecimal(String.valueOf(value));
task.getCost().setCalculated(false);
task.getCost().setValue(cost);
} catch (NumberFormatException e) {
throw new ValidationException(MessageFormat.format("Can't parse {0} as number", value));
}
break;
default:
break;
}
}
private void setCustomPropertyValue(Object value, Object node, int column) {
try {
((Task) ((TaskNode) node).getUserObject()).getCustomValues().setValue(getCustomProperty(column), value);
} catch (CustomColumnsException e) {
if (!GPLogger.log(e)) {
e.printStackTrace(System.err);
}
}
}
@Override
public void columnAdded(TableColumnModelEvent arg0) {
}
@Override
public void columnRemoved(TableColumnModelEvent arg0) {
}
@Override
public void columnMoved(TableColumnModelEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void columnMarginChanged(ChangeEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void columnSelectionChanged(ListSelectionEvent arg0) {
// TODO Auto-generated method stub
}
// public Task[] getNestedTasks(Task container) {
// return null;
// }
//
// public Task[] getDeepNestedTasks(Task container) {
// // TODO Auto-generated method stub
// return null;
// }
//
// /**
// * @return true if this Tasks has any nested subtasks.
// */
// public boolean hasNestedTasks(Task container) {
// TaskNode r = (TaskNode) root;
// if (r.getChildCount() > 0) {
// return true;
// } else {
// return false;
// }
// }
//
// public Task getRootTask() {
// return (Task) ((TaskNode) this.getRoot()).getUserObject();
// }
//
// /**
// * @return the corresponding task node according to the given task.
// *
// * @param task
// * The task whose TaskNode we want to get.
// * @return The corresponding TaskNode according to the given task.
// */
// public TaskNode getTaskNodeForTask(Task task) {
// for (MutableTreeTableNode tn : TreeUtil.collectSubtree(getRootNode())) {
// Task t = (Task) tn.getUserObject();
// if (t.equals(task)) {
// return tn;
// }
// }
// return null;
// }
//
// public Task getContainer(Task nestedTask) {
// // TODO Auto-generated method stub
// return null;
// }
//
// public void move(Task whatMove, Task whereMove) {
// // TODO Auto-generated method stub
// }
//
// public boolean areUnrelated(Task dependant, Task dependee) {
// // TODO Auto-generated method stub
// return false;
// }
//
// public int getDepth(Task task) {
// // TODO Auto-generated method stub
// return 0;
// }
public int compareDocumentOrder(Task next, Task dependeeTask) {
throw new UnsupportedOperationException();
}
public boolean contains(Task task) {
throw new UnsupportedOperationException();
}
public DefaultMutableTreeTableNode getRootNode() {
return (DefaultMutableTreeTableNode) getRoot();
}
}