/*
 * Licensed Materials - Property of Perforce Software, Inc. 
 * © Copyright Perforce Software, Inc. 2014, 2021 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */
package shared;

import ilog.views.IlvManagerViewInteractor;
import ilog.views.event.InteractorChangedEvent;
import ilog.views.event.InteractorListener;
import ilog.views.gantt.*;
import ilog.views.gantt.action.IlvAction;
import ilog.views.gantt.event.SelectionEvent;
import ilog.views.gantt.event.SelectionListener;
import ilog.views.gantt.graphic.IlvGanttSheet;
import ilog.views.gantt.graphic.interactor.IlvGanttSelectInteractor;
import ilog.views.gantt.graphic.interactor.IlvMakeActivityInteractor;
import ilog.views.gantt.graphic.interactor.IlvMakeConstraintInteractor;
import ilog.views.gantt.model.IlvDefaultGanttModel;
import ilog.views.util.time.IlvCalendarUtil;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Calendar;
import java.util.Date;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;


/**
 * This class groups a set of very useful action inner classes that can be
 * applied to a Gantt chart or Schedule chart. These actions can be invoked by a
 * menu item, a button in a toolbar, or an accelerator keystroke.
 */
public class GanttCommand {

  /**
   * Deletes the specified activity or resource from the data model.
   */
  public static void removeFromGanttModel(IlvGanttModel model,
                                          IlvHierarchyNode activityOrResource) {
    if (!model.contains(activityOrResource)) {
      return;
    }
    if (activityOrResource instanceof IlvResource) {
      IlvResource r = (IlvResource) activityOrResource;
      if (model.getRootResource() == r) {
        model.setRootResource(null);
      }
      else {
        model.removeResource(r);
      }
    } else if (activityOrResource instanceof IlvActivity) {
      IlvActivity a = (IlvActivity) activityOrResource;
      if (model.getRootActivity() == a) {
        model.setRootActivity(null);
      }
      else {
        model.removeActivity(a);
      }
    }
  }

  /**
   * Deletes the specified constraint from the data model.
   */
  public static void removeFromGanttModel(IlvGanttModel model,
                                          IlvConstraint constraint) {
    if (!model.contains(constraint)) {
      return;
    }
    model.removeConstraint(constraint);
  }

  /**
   * Deletes the specified reservation from the data model.
   */
  public static void removeFromGanttModel(IlvGanttModel model,
                                          IlvReservation reservation) {
    if (!model.contains(reservation)) {
      return;
    }
    model.removeReservation(reservation);
  }

  /**
   * An action that exits the current application.
   */
  public static class ExitAction extends IlvAction {

    /**
     * Creates a new <code>ExitAction</code>.
     */
    public ExitAction() {
      super("Exit",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.ALT_DOWN_MASK),
            "Exit",
            "Exits the example.");
    }

    /**
     * Performs the exit action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      System.exit(0);
    }

  }

  /**
   * An action that creates an empty default data model for the specified chart.
   */
  public static class NewDataModelAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvHierarchyChart chart;

    /**
     * Creates a new <code>NewDataModelAction</code>.
     */
    public NewDataModelAction(IlvHierarchyChart chart) {
      super("New",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_N, KeyEvent.CTRL_DOWN_MASK),
            "Create New Project",
            "Creates a new project.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/new.gif");
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      chart.setGanttModel(new IlvDefaultGanttModel());
    }

  }

  /**
   * An action that inserts a new activity into the specified Gantt chart.
   */
  public static class InsertActivityAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvGanttChart chart;

    /**
     * Creates a new <code>InsertActivityAction</code>.
     */
    public InsertActivityAction(IlvGanttChart chart) {
      super("Insert Activity",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0),
            "Insert Activity",
            "Inserts a new activity below the selected activity.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/insertrow.gif");
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvGanttModel model = chart.getGanttModel();
      if (model == null) {
        return;
      }
      IlvActivityFactory factory = chart.getActivityFactory();
      Calendar calendar = Calendar.getInstance();
      IlvCalendarUtil.dayFloor(calendar);
      Date today = calendar.getTime();
      IlvTimeInterval interval =
          new IlvTimeInterval(today, IlvDuration.ONE_DAY.multiply(4));
      IlvActivity newact = factory.createActivity(interval);
      IlvActivity root = model.getRootActivity();
      if (root == null) {
        model.setRootActivity(newact);
      } else {
        IlvActivity[] selected = chart.getSelectedActivities();
        if (selected.length > 0 && selected[0] != root) {
          IlvActivity curact = selected[0];
          IlvActivity parent = model.getParentActivity(curact);
          int index = model.getParentActivityIndex(curact);
          model.addActivity(newact, parent, index + 1);
        } else {
          int index = model.getChildActivityCount(root);
          model.addActivity(newact, root, index);
        }
      }
      chart.deSelectAllRows();
      chart.select(newact, true);
      makeRowDisplayedLater(chart, newact);
    }

  }

  /**
   * An action that inserts a new milestone into the specified Gantt chart.
   */
  public static class InsertMilestoneAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvGanttChart chart;

    /**
     * Creates a new <code>InsertMilestoneAction</code>.
     */
    public InsertMilestoneAction(IlvGanttChart chart) {
      super("Insert Milestone",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_M, KeyEvent.ALT_DOWN_MASK),
            "Insert Milestone",
            "Inserts a new milestone below the selected activity.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/insertmilestone.gif");
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvGanttModel model = chart.getGanttModel();
      if (model == null) {
        return;
      }
      IlvActivityFactory factory = chart.getActivityFactory();
      Calendar calendar = Calendar.getInstance();
      IlvCalendarUtil.dayFloor(calendar);
      Date today = calendar.getTime();
      IlvTimeInterval interval = new IlvTimeInterval(today, today);
      IlvActivity newact = factory.createActivity(interval);
      IlvActivity root = model.getRootActivity();
      if (root == null) {
        model.setRootActivity(newact);
      } else {
        IlvActivity[] selected = chart.getSelectedActivities();
        if (selected.length > 0 && selected[0] != root) {
          IlvActivity curact = selected[0];
          IlvActivity parent = model.getParentActivity(curact);
          int index = model.getParentActivityIndex(curact);
          model.addActivity(newact, parent, index + 1);
        } else {
          int index = model.getChildActivityCount(root);
          model.addActivity(newact, root, index);
        }
      }
      chart.deSelectAllRows();
      chart.select(newact, true);
      makeRowDisplayedLater(chart, newact);
    }

  }

  /**
   * An action that inserts a new resource into the specified Schedule chart.
   */
  public static class InsertResourceAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvScheduleChart chart;

    /**
     * Creates a new <code>InsertResourceAction</code>.
     */
    public InsertResourceAction(IlvScheduleChart chart) {
      super("Insert Resource",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0),
            "Insert Resource",
            "Inserts a new resource below the selected resource.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/insertrow.gif");
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvGanttModel model = chart.getGanttModel();
      if (model == null) {
        return;
      }
      IlvResourceFactory factory = chart.getResourceFactory();
      IlvResource newres = factory.createResource();
      IlvResource root = model.getRootResource();
      if (root == null) {
        model.setRootResource(newres);
      } else {
        IlvResource[] selected = chart.getSelectedResources();
        if (selected.length > 0 && selected[0] != root) {
          IlvResource curres = selected[0];
          IlvResource parent = model.getParentResource(curres);
          int index = model.getParentResourceIndex(curres);
          model.addResource(newres, parent, index + 1);
        } else {
          int index = model.getChildResourceCount(root);
          model.addResource(newres, root, index);
        }
      }
      chart.deSelectAllRows();
      chart.select(newres, true);
      makeRowDisplayedLater(chart, newres);
    }

  }

  /**
   * An action that deletes all the selected rows and graphics from the
   * specified charts.
   */
  public static class DeleteSelectedAction extends IlvAction {

    /**
     * The array of charts.
     */
    private IlvHierarchyChart[] charts;

    /**
     * Creates a new <code>DeleteSelectedAction</code>.
     *
     * @param charts The array of charts to which the action applies.
     */
    public DeleteSelectedAction(IlvHierarchyChart... charts) {
      super("Delete",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),
            "Delete",
            "");
      this.charts = charts;
      setIcon(GanttCommand.class, "images/delete.gif");
      setLongDescription(computeLongDescription());
      SelectionListener selectionListener = new SelectionListener() {
        Override
        public void selectionChanged(SelectionEvent event) {
          computeEnabled();
        }
      };
      for (IlvHierarchyChart chart : charts) {
        chart.addSelectionListener(selectionListener);
      }
      computeEnabled();
    }

    /**
     * Computes the long description of this action.
     */
    private String computeLongDescription() {
      boolean activities = false;
      boolean resources = false;
      for (IlvHierarchyChart chart : charts) {
        if (chart.getRowType() == IlvGanttConfiguration.ACTIVITY_ROWS) {
          activities = true;
        } else {
          resources = true;
        }
      }
      if (activities && resources) {
        return "Deletes the selected activities, resources, constraints, and reservations.";
      } else if (activities) {
        return "Deletes the selected activities and constraints.";
      } else {
        return "Deletes the selected resources and reservations.";
      }
    }

    /**
     * Computes whether this action should be enabled or not.
     */
    private void computeEnabled() {
      for (IlvHierarchyChart chart : charts) {
        if (chart.getSelectedRows().length > 0
            || chart.getSelectedGraphics().length > 0) {
          setEnabled(true);
          return;
        }
      }
      setEnabled(false);
    }

    /**
     * Performs the action.
     *
     * @param event The event that triggers the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      for (IlvHierarchyChart chart : charts) {
        IlvGanttModel model = chart.getGanttModel();

        // First, delete all the selected rows.
        IlvHierarchyNode[] selected = chart.getSelectedRows();
        for (IlvHierarchyNode row : selected) {
          removeFromGanttModel(model, row);
        }
        // Next, delete the selected reservations.
        if (chart instanceof IlvScheduleChart) {
          IlvReservation[] reservations =
              ((IlvScheduleChart) chart).getSelectedReservations();
          for (IlvReservation reservation : reservations) {
            removeFromGanttModel(model, reservation);
          }
        }
        // Finally, delete the selected constraints.
        IlvConstraint[] constraints = chart.getSelectedConstraints();
        for (IlvConstraint constraint : constraints) {
          removeFromGanttModel(model, constraint);
        }
      }
    }

  }

  /**
   * An action that deletes the selected rows from the specified chart. If the
   * chart is a Gantt chart, the action deletes activities. If the chart is a
   * Schedule chart, the action deletes resources.
   */
  public static class DeleteSelectedRowsAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvHierarchyChart chart;

    /**
     * Creates a new <code>DeleteSelectedRowsAction</code>.
     */
    public DeleteSelectedRowsAction(IlvHierarchyChart chart) {
      super(chart instanceof IlvGanttChart
            ? "Delete Activity"
            : "Delete Resource",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK),
            chart instanceof IlvGanttChart
            ? "Delete Activity"
            : "Delete Resource",
            chart instanceof IlvGanttChart
            ? "Deletes the selected activities."
            : "Deletes the selected resources.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/deleterow.gif");
      chart.addSelectionListener(new SelectionListener()
      {
        Override
        public void selectionChanged(SelectionEvent event) {
          computeEnabled();
        }
      });
      computeEnabled();
    }

    /**
     * Computes whether this action should be enabled or not.
     */
    private void computeEnabled() {
      setEnabled(chart.getSelectedRows().length > 0);
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvGanttModel model = chart.getGanttModel();
      IlvHierarchyNode[] selected = chart.getSelectedRows();
      for (IlvHierarchyNode row : selected) {
        removeFromGanttModel(model, row);
      }
    }

  }

  /**
   * An action that deletes the selected constraints from the specified
   * chart.
   */
  public static class DeleteSelectedConstraintsAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvHierarchyChart chart;

    /**
     * Creates a new <code>DeleteSelectedConstraintsAction</code>.
     */
    public DeleteSelectedConstraintsAction(IlvHierarchyChart chart) {
      super("Delete Constraint",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.SHIFT_DOWN_MASK),
            "Delete Constraint",
            "Deletes the selected constraints.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/deleteconstraint.gif");
      chart.addSelectionListener(new SelectionListener()
      {
        Override
        public void selectionChanged(SelectionEvent event) {
          computeEnabled();
        }
      });
      computeEnabled();
    }

    /**
     * Computes whether this action should be enabled or not.
     */
    private void computeEnabled() {
      setEnabled(chart.getSelectedConstraints().length > 0);
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvGanttModel model = chart.getGanttModel();
      IlvConstraint[] constraints = chart.getSelectedConstraints();
      for (IlvConstraint constraint : constraints) {
        removeFromGanttModel(model, constraint);
      }
    }

  }

  /**
   * An action that deletes the selected reservations from the specified
   * Schedule chart.
   */
  public static class DeleteSelectedReservationsAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvScheduleChart chart;

    /**
     * Creates a new <code>DeleteSelectedReservationsAction</code>.
     */
    public DeleteSelectedReservationsAction(IlvScheduleChart chart) {
      super("Delete Reservation",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.SHIFT_DOWN_MASK),
            "Delete Reservation",
            "Deletes the selected reservations.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/deleteactivity.gif");
      chart.addSelectionListener(new SelectionListener()
      {
        Override
        public void selectionChanged(SelectionEvent event) {
          computeEnabled();
        }
      });
      computeEnabled();
    }

    /**
     * Computes whether this action should be enabled or not.
     */
    private void computeEnabled() {
      setEnabled(chart.getSelectedReservations().length > 0);
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvGanttModel model = chart.getGanttModel();
      IlvReservation[] reservations = chart.getSelectedReservations();
      for (IlvReservation reservation : reservations) {
        removeFromGanttModel(model, reservation);
      }
    }

  }

  /**
   * An action that installs the constraint creation interactor on the specified
   * chart.
   */
  public static class MakeConstraintAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvHierarchyChart chart;

    /**
     * The ESC key.
     */
    private KeyStroke escKey;

    /**
     * The ESC key action.
     */
    private ActionListener escAction;

    /**
     * Creates a new <code>MakeConstraintAction</code>.
     */
    public MakeConstraintAction(IlvHierarchyChart chart) {
      super("Create Constraint",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.ALT_DOWN_MASK),
            "Create Constraint",
            "Creates a new constraint using the mouse.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/constraint.gif");
      escKey = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
      // Define the ESC action that will abort the current make-constraint
      // interactor. This supplements the built-in ESC key handling of the
      // interactor itself, since the interactor only receives keystrokes
      // when the Gantt sheet has the focus. This action will be registered
      // as an application-wide accelerator.
      escAction = new ActionListener() {
        Override
        public void actionPerformed(ActionEvent actionEvent) {
          IlvHierarchyChart chart = MakeConstraintAction.this.chart;
          IlvGanttSheet ganttSheet = chart.getGanttSheet();
          IlvManagerViewInteractor current = ganttSheet.getInteractor();
          if (current instanceof IlvMakeConstraintInteractor) {
            ((IlvMakeConstraintInteractor) current).abort();
          }
        }
      };
      // Listen for changes in the Gantt sheet's current interactor. When
      // a make-constraint interactor is set, we register the ESC action
      // application-wide. When a make-constraint interactor is unset, we
      // unregister the ESC action.
      chart.getGanttSheet().addInteractorListener(new InteractorListener() {
        Override
        public void interactorChanged(InteractorChangedEvent event) {
          IlvHierarchyChart chart = MakeConstraintAction.this.chart;
          if (event.getOldValue()instanceof IlvMakeConstraintInteractor) {
            chart.unregisterKeyboardAction(escKey);
          }
          if (event.getNewValue()instanceof IlvMakeConstraintInteractor) {
            chart.registerKeyboardAction(escAction,
                                         escKey,
                                         JComponent.WHEN_IN_FOCUSED_WINDOW);
          }
        }
      });
    }

    /**
     * Performs the action by setting the make-constraint interactor onto
     * the Gantt sheet.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvGanttSheet ganttSheet = chart.getGanttSheet();
      IlvManagerViewInteractor current = ganttSheet.getInteractor();
      if (current instanceof IlvMakeConstraintInteractor) {
        return;
      }
      IlvMakeConstraintInteractor lnkinter = new IlvMakeConstraintInteractor();
      ganttSheet.pushInteractor(lnkinter);
    }

  }

  /**
   * An action that installs the activity creation interactor on the specified
   * chart.
   */
  public static class MakeActivityAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvHierarchyChart chart;

    /**
     * Creates a new <code>MakeActivityAction</code>.
     */
    public MakeActivityAction(IlvHierarchyChart chart) {
      super(chart instanceof IlvGanttChart
            ? "Create Activity"
            : "Create Reservation",
            null,
            chart instanceof IlvGanttChart
            ? KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.ALT_DOWN_MASK)
            : KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.ALT_DOWN_MASK),
            chart instanceof IlvGanttChart
            ? "Create Activity"
            : "Create Reservation",
            chart instanceof IlvGanttChart
            ? "Creates a new activity using the mouse."
            : "Creates a new reservation using the mouse.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/newactivity.gif");
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvManagerViewInteractor current = chart.getGanttSheet().getInteractor();
      if (current instanceof IlvMakeActivityInteractor) {
        return;
      }
      IlvMakeActivityInteractor inter = new IlvMakeActivityInteractor();
      if (current instanceof IlvGanttSelectInteractor) {
        inter.
            setGhostMode(!((IlvGanttSelectInteractor) current).isOpaqueMove());
      }
      chart.getGanttSheet().pushInteractor(inter);
    }

  }

  /**
   * An action that outdents the first selected row of the specified chart to a
   * higher hierarchy level. If the chart is a Gantt chart, the action outdents
   * activities. If the chart is a Schedule chart, the action outdents
   * resources.
   */
  public static class RowOutdentAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvHierarchyChart chart;

    /**
     * Creates a new <code>RowOutdentAction</code>.
     */
    public RowOutdentAction(IlvHierarchyChart chart) {
      super(chart instanceof IlvGanttChart
            ? "Outdent Activity"
            : "Outdent Resource",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.CTRL_DOWN_MASK),
            chart instanceof IlvGanttChart
            ? "Outdent Activity"
            : "Outdent Resource",
            chart instanceof IlvGanttChart
            ? "Outdents the selected activity."
            : "Outdents the selected resource.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/outdent.gif");
      chart.addSelectionListener(new SelectionListener()
      {
        Override
        public void selectionChanged(SelectionEvent event) {
          computeEnabled();
        }
      });
      computeEnabled();
    }

    /**
     * Computes whether this action should be enabled or not.
     */
    private void computeEnabled() {
      setEnabled(chart.getSelectedRows().length > 0);
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvHierarchyNode[] selectedRows = chart.getSelectedRows();
      if (selectedRows.length == 0) {
        return;
      }
      IlvHierarchyNode row = selectedRows[0];
      IlvGanttModelUtil.promote(chart.getGanttModel(), row);
      makeRowDisplayedLater(chart, row);
    }

  }

  /**
   * An action that indents the first selected row of the specified chart to a
   * lower hierarchy level. If the chart is a Gantt chart, the action indents
   * activities. If the chart is a Schedule chart, the action indents resources.
   */
  public static class RowIndentAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvHierarchyChart chart;

    /**
     * Creates a new <code>RowIndentAction</code>.
     */
    public RowIndentAction(IlvHierarchyChart chart) {
      super(chart instanceof IlvGanttChart
            ? "Indent Activity"
            : "Indent Resource",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.CTRL_DOWN_MASK),
            chart instanceof IlvGanttChart
            ? "Indent Activity"
            : "Indent Resource",
            chart instanceof IlvGanttChart
            ? "Indents the selected activity."
            : "Indents the selected resource.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/indent.gif");
      chart.addSelectionListener(new SelectionListener()
      {
        Override
        public void selectionChanged(SelectionEvent event) {
          computeEnabled();
        }
      });
      computeEnabled();
    }

    /**
     * Computes whether this action should be enabled or not.
     */
    private void computeEnabled() {
      setEnabled(chart.getSelectedRows().length > 0);
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvHierarchyNode[] selectedRows = chart.getSelectedRows();
      if (selectedRows.length == 0) {
        return;
      }
      IlvGanttModel model = chart.getGanttModel();
      IlvHierarchyNode row = selectedRows[0];
      IlvGanttModelUtil.demote(model, row);
      IlvHierarchyNode newParent = model.getParent(row);
      if (newParent != null && !chart.isRowExpanded(newParent)) {
        chart.expandRow(newParent);
        chart.selectRow(row, true);
      }
      makeRowDisplayedLater(chart, row);
    }

  }

  /**
   * An action that moves the first selected row of the specified chart upwards.
   * If the chart is a Gantt chart, the action moves activities. If the chart is
   * a Schedule chart, the action moves resources.
   */
  public static class RowUpAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvHierarchyChart chart;

    /**
     * Creates a new <code>RowUpAction</code>.
     */
    public RowUpAction(IlvHierarchyChart chart) {
      super(chart instanceof IlvGanttChart
            ? "Move Activity Up"
            : "Move Resource Up",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.CTRL_DOWN_MASK),
            chart instanceof IlvGanttChart
            ? "Move Activity Up"
            : "Move Resource Up",
            chart instanceof IlvGanttChart
            ? "Moves the selected activity up."
            : "Moves the selected resource up.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/up.gif");
      chart.addSelectionListener(new SelectionListener()
      {
        Override
        public void selectionChanged(SelectionEvent event) {
          computeEnabled();
        }
      });
      computeEnabled();
    }

    /**
     * Computes whether this action should be enabled or not.
     */
    private void computeEnabled() {
      setEnabled(chart.getSelectedRows().length > 0);
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvHierarchyNode[] selectedRows = chart.getSelectedRows();
      if (selectedRows.length == 0) {
        return;
      }
      IlvGanttModel model = chart.getGanttModel();
      IlvHierarchyNode row = selectedRows[0];
      IlvHierarchyNode parent = model.getParent(row);
      if (parent == null) {
        return;
      }
      int index = model.getParentIndex(row);
      if (index == 0) {
        return;
      }
      if (row instanceof IlvActivity) {
        model.moveActivity((IlvActivity) row, (IlvActivity) parent, index - 1);
      }
      else {
        model.moveResource((IlvResource) row, (IlvResource) parent, index - 1);
      }
      makeRowDisplayedLater(chart, row);
    }

  }

  /**
   * An action that moves the first selected row of the specified chart
   * downwards.
   * If the chart is a Gantt chart, the action moves activities. If the chart is
   * a Schedule chart, the action moves resources.
   */
  public static class RowDownAction extends IlvAction {

    /**
     * The chart.
     */
    private IlvHierarchyChart chart;

    /**
     * Creates a new <code>RowDownAction</code>.
     */
    public RowDownAction(IlvHierarchyChart chart) {
      super(chart instanceof IlvGanttChart
            ? "Move Activity Down"
            : "Move Resource Down",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.CTRL_DOWN_MASK),
            chart instanceof IlvGanttChart
            ? "Move Activity Down"
            : "Move Resource Down",
            chart instanceof IlvGanttChart
            ? "Moves the selected activity down."
            : "Moves the selected resource down.");
      this.chart = chart;
      setIcon(GanttCommand.class, "images/down.gif");
      chart.addSelectionListener(new SelectionListener()
      {
        Override
        public void selectionChanged(SelectionEvent event) {
          computeEnabled();
        }
      });
      computeEnabled();
    }

    /**
     * Computes whether this action should be enabled or not.
     */
    private void computeEnabled() {
      setEnabled(chart.getSelectedRows().length > 0);
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      IlvHierarchyNode[] selectedRows = chart.getSelectedRows();
      if (selectedRows.length == 0) {
        return;
      }
      IlvGanttModel model = chart.getGanttModel();
      IlvHierarchyNode row = selectedRows[0];
      IlvHierarchyNode parent = model.getParent(row);
      if (parent == null) {
        return;
      }
      int index = model.getParentIndex(row);
      if (index == model.getChildCount(parent) - 1) {
        return;
      }
      if (row instanceof IlvActivity) {
        model.moveActivity((IlvActivity) row, (IlvActivity) parent, index + 1);
      }
      else {
        model.moveResource((IlvResource) row, (IlvResource) parent, index + 1);
      }
      makeRowDisplayedLater(chart, row);
    }

  }

  /**
   * An action that toggles the expansion of the selected rows in the
   * specified charts.
   */
  public static class RowExpandCollapseAction extends IlvAction {

    /**
     * The array of charts.
     */
    private IlvHierarchyChart[] charts;

    /**
     * Creates a new <code>RowExpandCollapseAction</code>.
     *
     * @param charts The array of charts to which the action applies.
     */
    public RowExpandCollapseAction(IlvHierarchyChart... charts) {
      super("",
            null,
            KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK),
            "",
            "");
      this.charts = charts;
      setIcon(GanttCommand.class, "images/plusminus.gif");
      setName(computeShortDescription());
      setShortDescription(computeShortDescription());
      setLongDescription(computeLongDescription());
      SelectionListener selectionListener = new SelectionListener() {
        Override
        public void selectionChanged(SelectionEvent event) {
          computeEnabled(event);
        }
      };
      for (IlvHierarchyChart chart : charts) {
        chart.addSelectionListener(selectionListener);
      }
      computeEnabled(null);
    }

    /**
     * Computes the short description of this action.
     */
    private String computeShortDescription() {
      boolean activities = false;
      boolean resources = false;
      for (IlvHierarchyChart chart : charts) {
        if (chart.getRowType() == IlvGanttConfiguration.ACTIVITY_ROWS) {
          activities = true;
        } else {
          resources = true;
        }
      }
      if (activities && resources) {
        return "Expand/Collapse Activities and Resources";
      } else if (activities) {
        return "Expand/Collapse Activities";
      } else {
        return "Expand/Collapse Resources";
      }
    }

    /**
     * Computes the long description of this action.
     */
    private String computeLongDescription() {
      boolean activities = false;
      boolean resources = false;
      for (IlvHierarchyChart chart : charts) {
        if (chart.getRowType() == IlvGanttConfiguration.ACTIVITY_ROWS) {
          activities = true;
        } else {
          resources = true;
        }
      }
      if (activities && resources) {
        return "Toggles expansion of the selected activities and resources.";
      } else if (activities) {
        return "Toggles expansion of the selected activities.";
      } else {
        return "Toggles expansion of the selected resources.";
      }
    }

    /**
     * Computes whether this action should be enabled or not for the specified chart.
     * This method returns <code>null</code> if the event cannot be applied to the
     * chart.
     */
    private Boolean computeEnabled(IlvHierarchyChart chart, SelectionEvent event) {
      int chartType = chart.getRowType();
      if (event != null) {
        if (event.isAdjusting()) {
          return null;
        }
        Object eventSource = event.getSource();
        if (chartType == IlvGanttConfiguration.ACTIVITY_ROWS) {
          if (!(eventSource instanceof IlvActivity)) {
            return null;
          }
        } else if (chartType == IlvGanttConfiguration.RESOURCE_ROWS) {
          if (!(eventSource instanceof IlvResource)) {
            return null;
          }
        }
      }
      IlvGanttModel model = chart.getGanttModel();
      IlvHierarchyNode[] selectedRows = chart.getSelectedRows();
      for (IlvHierarchyNode row : selectedRows) {
        if (IlvGanttModelUtil.hasChildren(model, row)) {
          return true;
        }
      }
      return false;
    }

    /**
     * Computes whether this action should be enabled or not.
     */
    private void computeEnabled(SelectionEvent event) {
      Boolean enable = null;
      for (IlvHierarchyChart chart : charts) {
        Boolean chartResult = computeEnabled(chart, event);
        if (chartResult != null) {
          if (enable == null) {
            enable = chartResult;
          } else {
            enable = enable || chartResult;
          }
        }
      }
      if (enable != null) {
        setEnabled(enable);
      }
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      Boolean expanded = null;
      for (IlvHierarchyChart chart : charts) {
        IlvGanttModel model = chart.getGanttModel();
        IlvHierarchyNode[] selectedRows = chart.getSelectedRows();
        for (IlvHierarchyNode row : selectedRows) {
          if (IlvGanttModelUtil.hasChildren(model, row)) {
            if (expanded == null) {
              expanded = chart.isRowExpanded(row);
            }
            if (expanded) {
              chart.collapseRow(row);
            } else {
              chart.expandRow(row);
            }
          }
        }
      }
    }

  }

  /**
   * An action that changes the height of the rows in the specified charts.
   */
  public static class RowHeightAction extends IlvAction {

    /**
     * The array of charts.
     */
    private IlvHierarchyChart[] charts;

    /**
     * The amount to change the row height.
     */
    private int delta;

    /**
     * The minimum row height.
     */
    private int minRowHeight = 12;

    /**
     * The maximum row height.
     */
    private int maxRowHeight = 50;

    /**
     * Creates a new <code>RowHeightAction</code>.
     *
     * @param delta  The number of pixels to change the height of the chart rows by.
     * @param charts The array of charts to which the action applies.
     */
    public RowHeightAction(int delta, IlvHierarchyChart... charts) {
      super((delta >= 0)
            ? "Increase " + delta + " Pixels"
            : "Decrease " + (-delta) + " Pixels",
            null,
            null,
            (delta >= 0)
            ? "Increase Row Height " + delta + " Pixels"
            : "Decrease Row Height " + (-delta) + " Pixels",
            (delta >= 0)
            ? "Increases the row height by " + delta + " pixels."
            : "Decreases the row height by " + (-delta) + " pixels.");
      this.charts = charts;
      this.delta = delta;
    }

    /**
     * Returns the amount that the row height will be changed.
     */
    public int getDelta() {
      return delta;
    }

    /**
     * Returns the minimum row height.
     */
    public int getMinRowHeight() {
      return minRowHeight;
    }

    /**
     * Sets the minimum row height.
     */
    public void setMinRowHeight(int min) {
      minRowHeight = min;
    }

    /**
     * Returns the maximum row height.
     */
    public int getMaxRowHeight() {
      return maxRowHeight;
    }

    /**
     * Sets the maximum row height.
     */
    public void setMaxRowHeight(int max) {
      maxRowHeight = max;
    }

    /**
     * Performs the action.
     */
    Override
    public void actionPerformed(ActionEvent event) {
      for (IlvHierarchyChart chart : charts) {
        int height = chart.getRowHeight();
        height += delta;
        height = Math.max(height, minRowHeight);
        height = Math.min(height, maxRowHeight);
        chart.setRowHeight(height);
      }
    }

  }


  /**
   * Ensures that the specified <i>row</i> is displayed, by calling the chart's
   * <code>makeRowDisplayed()</code> method on the Swing event queue. It is
   * necessary to use this technique and delay execution of the chart's
   * <code>makeRowDisplayed()</code> method when row visibilities have
   * just been modified. This is because the chart also updates its internal
   * knowledge of which rows are within the display area on the Swing event
   * queue.
   */
  public static void makeRowDisplayedLater(final IlvHierarchyChart chart,
                                           final IlvHierarchyNode row) {
    SwingUtilities.invokeLater(new Runnable() {
      Override
      public void run() {
        chart.makeRowDisplayed(row);
      }
    });
  }

}