/*
GanttProject is an opensource project management tool.
Copyright (C) 2005-2016 GanttProject team
This file is part of GanttProject.
GanttProject 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.
GanttProject 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 GanttProject. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sourceforge.ganttproject.gui;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.util.List;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import net.sourceforge.ganttproject.GPLogger;
import net.sourceforge.ganttproject.IGanttProject;
import net.sourceforge.ganttproject.action.GPAction;
import net.sourceforge.ganttproject.action.OkAction;
import net.sourceforge.ganttproject.document.Document;
import net.sourceforge.ganttproject.language.GanttLanguage;
import net.sourceforge.ganttproject.task.Task;
import net.sourceforge.ganttproject.task.TaskImpl;
import net.sourceforge.ganttproject.task.TaskManager;
import net.sourceforge.ganttproject.task.algorithm.AlgorithmCollection;
import org.jdesktop.swingx.JXRadioGroup;
import biz.ganttproject.core.option.DefaultEnumerationOption;
import biz.ganttproject.core.time.TimeDuration;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
/**
* When we open a file, we need to complete a number of steps in order to be sure
* that should task dates change after the first scheduler run, user is informed about that.
*
* The steps are chained so that client can't invoke them in wrong order. Strategy is auto-closeable
* and should be closed after using.
*
* @author bard
*/
class ProjectOpenStrategy implements AutoCloseable {
private static enum ConvertMilestones {
UNKNOWN, TRUE, FALSE
}
private static final DefaultEnumerationOption<ConvertMilestones> ourConvertMilestonesOption = new DefaultEnumerationOption<ConvertMilestones>(
"milestones_to_zero", ConvertMilestones.values());
static DefaultEnumerationOption<ConvertMilestones> getMilestonesOption() {
return ourConvertMilestonesOption;
}
private final UIFacade myUiFacade;
private final IGanttProject myProject;
private final ProjectOpenDiagnosticImpl myDiagnostics;
private final List<AutoCloseable> myCloseables = Lists.newArrayList();
private final AutoCloseable myEnableAlgorithmsCmd;
private final AlgorithmCollection myAlgs;
private final List<Runnable> myTasks = Lists.newArrayList();
private TimeDuration myOldDuration;
private final GanttLanguage i18n = GanttLanguage.getInstance();
private boolean myResetModifiedState = true;
ProjectOpenStrategy(IGanttProject project, UIFacade uiFacade) {
myProject = Preconditions.checkNotNull(project);
myUiFacade = Preconditions.checkNotNull(uiFacade);
myDiagnostics = new ProjectOpenDiagnosticImpl(myUiFacade);
myAlgs = myProject.getTaskManager().getAlgorithmCollection();
myEnableAlgorithmsCmd = new AutoCloseable() {
public void close() throws Exception {
myAlgs.getScheduler().setEnabled(true);
myAlgs.getRecalculateTaskScheduleAlgorithm().setEnabled(true);
myAlgs.getAdjustTaskBoundsAlgorithm().setEnabled(true);
}
};
}
public void close() {
for (AutoCloseable c : myCloseables) {
try {
c.close();
} catch (Exception e) {
GPLogger.log(e);
}
}
}
// First we open file "as is", that is, without running any algorithms which
// change task dates.
Step1 openFileAsIs(Document document) throws Exception {
myCloseables.add(myEnableAlgorithmsCmd);
myAlgs.getScheduler().setEnabled(false);
myAlgs.getRecalculateTaskScheduleAlgorithm().setEnabled(false);
myAlgs.getAdjustTaskBoundsAlgorithm().setEnabled(false);
myAlgs.getScheduler().setDiagnostic(myDiagnostics);
try {
myProject.open(document);
} finally {
myAlgs.getScheduler().setDiagnostic(null);
}
if (document.getPortfolio() != null) {
Document defaultDocument = document.getPortfolio().getDefaultDocument();
myProject.open(defaultDocument);
}
myOldDuration = myProject.getTaskManager().getProjectLength();
return new Step1();
}
// This step checks if there are legacy 1-day milestones in the project.
// If there are legacy milestones, we ask the user what shall we do with them.
// This involves interaction with Swing thread and later task of patching
// milestones.
class Step1 {
Step2 checkLegacyMilestones() {
final TaskManager taskManager = myProject.getTaskManager();
boolean hasLegacyMilestones = false;
for (Task t : taskManager.getTasks()) {
if (((TaskImpl)t).isLegacyMilestone()) {
hasLegacyMilestones = true;
break;
}
}
if (hasLegacyMilestones && taskManager.isZeroMilestones() == null) {
ConvertMilestones option = ourConvertMilestonesOption.getSelectedValue() == null
? ConvertMilestones.UNKNOWN
: ourConvertMilestonesOption.getSelectedValue();
switch (option) {
case UNKNOWN:
myTasks.add(new Runnable() {
@Override
public void run() {
try {
myProject.getTaskManager().getAlgorithmCollection().getScheduler().setDiagnostic(myDiagnostics);
tryPatchMilestones(myProject, taskManager);
} finally {
myProject.getTaskManager().getAlgorithmCollection().getScheduler().setDiagnostic(null);
}
}
});
break;
case TRUE:
taskManager.setZeroMilestones(true);
myResetModifiedState = false;
break;
case FALSE:
taskManager.setZeroMilestones(false);
break;
}
}
return new Step2();
}
// Asks user what shall we do with milestones and updates milestones if user
// decides to convert them. This code is executed by Step3.runUiTasks
private void tryPatchMilestones(final IGanttProject project, final TaskManager taskManager) {
final JRadioButton buttonConvert = new JRadioButton(i18n.getText("legacyMilestones.choice.convert"));
final JRadioButton buttonKeep = new JRadioButton(i18n.getText("legacyMilestones.choice.keep"));
buttonConvert.setSelected(true);
JXRadioGroup<JRadioButton> group = JXRadioGroup.create(new JRadioButton[] {buttonConvert, buttonKeep});
group.setLayoutAxis(BoxLayout.PAGE_AXIS);
final JCheckBox remember = new JCheckBox(i18n.getText("legacyMilestones.choice.remember"));
Box content = Box.createVerticalBox();
JLabel question = new JLabel(i18n.getText("legacyMilestones.question"), SwingConstants.LEADING);
question.setOpaque(true);
question.setAlignmentX(0.5f);
content.add(question);
content.add(Box.createVerticalStrut(15));
content.add(group);
content.add(Box.createVerticalStrut(5));
content.add(remember);
Box icon = Box.createVerticalBox();
icon.add(new JLabel(GPAction.getIcon("64", "dialog-question.png")));
icon.add(Box.createVerticalGlue());
JPanel result = new JPanel(new BorderLayout());
result.add(content, BorderLayout.CENTER);
result.add(icon, BorderLayout.WEST);
result.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
myUiFacade.createDialog(result, new Action[] {new OkAction() {
@Override
public void actionPerformed(ActionEvent e) {
taskManager.setZeroMilestones(buttonConvert.isSelected());
if (remember.isSelected()) {
ourConvertMilestonesOption.setSelectedValue(buttonConvert.isSelected() ? ConvertMilestones.TRUE : ConvertMilestones.FALSE);
}
adjustTasks(taskManager);
project.setModified(true);
}
}}, i18n.getText("legacyMilestones.title")).show();
}
private void adjustTasks(TaskManager taskManager) {
try {
taskManager.getAlgorithmCollection().getScheduler().run();
} catch (Exception e) {
GPLogger.logToLogger(e);
}
}
}
// This step runs the scheduler and checks if there are tasks with earliest start constraints
// which changed their dates. Such tasks will be reported in the dialog.
class Step2 {
Step3 checkEarliestStartConstraints() throws Exception {
myAlgs.getScheduler().setDiagnostic(myDiagnostics);
try {
// This actually runs the scheduler by enabling it
myEnableAlgorithmsCmd.close();
// We enabled algoritmhs so we don't need to keep them in the list of closeables
myCloseables.remove(myEnableAlgorithmsCmd);
} finally {
myAlgs.getScheduler().setDiagnostic(null);
}
// Analyze earliest start dates
for (Task t : myProject.getTaskManager().getTasks()) {
if (t.getThird() != null && myDiagnostics.myModifiedTasks.containsKey(t)) {
myDiagnostics.addReason(t, "scheduler.warning.table.reason.earliestStart");
}
}
TimeDuration newDuration = myProject.getTaskManager().getProjectLength();
if (!myDiagnostics.myModifiedTasks.isEmpty()) {
// Some tasks have been modified, so let's add introduction text to the dialog
myDiagnostics.info(i18n.getText("scheduler.warning.summary.item0"));
if (newDuration.getLength() != myOldDuration.getLength()) {
myDiagnostics.info(i18n.formatText("scheduler.warning.summary.item1", myOldDuration, newDuration));
}
}
return new Step3();
}
}
// This step runs the collected UI tasks. First (optional) task is legacy milestones question;
// the remaining are added here.
class Step3 {
void runUiTasks() {
myTasks.add(new Runnable() {
@Override
public void run() {
if (!myDiagnostics.myMessages.isEmpty()) {
myDiagnostics.showDialog();
}
}
});
if (myDiagnostics.myMessages.isEmpty() && myResetModifiedState) {
myTasks.add(new Runnable() {
@Override
public void run() {
myProject.setModified(false);
}
});
}
myTasks.add(new Runnable() {
@Override
public void run() {
try {
ProjectOpenStrategy.this.close();
} catch (Exception e) {
GPLogger.log(e);
}
}
});
processTasks(myTasks);
}
private void processTasks(final List<Runnable> tasks) {
if (tasks.isEmpty()) {
return;
}
final Runnable task = tasks.get(0);
Runnable wrapper = new Runnable() {
@Override
public void run() {
task.run();
tasks.remove(0);
processTasks(tasks);
}
};
SwingUtilities.invokeLater(wrapper);
}
}
}