/* EditMenu.java
 *
 * created: Thu Dec  3 1998
 *
 * This file is part of Artemis
 *
 * Copyright (C) 1998,1999,2000  Genome Research Limited
 *
 * 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 2
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Header: /nfs/disk222/yeastpub/Repository/powmap/diana/components/EditMenu.java,v 1.76 2000/09/04 15:51:45 kmr Exp $
 **/

package diana.components;

import diana.*;
import diana.sequence.*;
import uk.ac.sanger.pathogens.*;
import uk.ac.sanger.pathogens.embl.Range;
import uk.ac.sanger.pathogens.embl.Key;
import uk.ac.sanger.pathogens.embl.Qualifier;
import uk.ac.sanger.pathogens.embl.QualifierVector;
import uk.ac.sanger.pathogens.embl.QualifierParseException;
import uk.ac.sanger.pathogens.embl.InvalidQualifierException;
import uk.ac.sanger.pathogens.embl.InvalidRelationException;
import uk.ac.sanger.pathogens.embl.EntryInformationException;

import java.awt.*;
import java.awt.event.*;

/**
 *  A menu with editing commands.
 *
 *  @author Kim Rutherford
 *  @version $Id: EditMenu.java,v 1.76 2000/09/04 15:51:45 kmr Exp $
 **/

public class EditMenu extends SelectionMenu
    implements EntryGroupChangeListener, EntryChangeListener {
  /**
   *  Create a new EditMenu object.
   *  @param frame The Frame that owns this Menu.
   *  @param selection The Selection that the commands in the menu will
   *    operate on.
   *  @param goto_event_source The object the we will call makeBaseVisible ()
   *    on.
   *  @param entry_group The EntryGroup object where new features/entries will
   *    be added.
   **/
  public EditMenu (final Frame frame,
                   final Selection selection,
                   final GotoEventSource goto_event_source,
                   final EntryGroup entry_group) {
    super (frame, "Edit", selection);

    this.entry_group = entry_group;
    this.goto_event_source = goto_event_source;

    getEntryGroup ().addEntryGroupChangeListener (this);
    getEntryGroup ().addEntryChangeListener (this);

    refreshMenu ();
  }

  /**
   *  The shortcut for Edit Selected Features.
   **/
  final static int EDIT_FEATURE_KEY = KeyEvent.VK_E;

  /**
   *  The shortcut for Merge Selected Features.
   **/
  final static int MERGE_FEATURES_KEY = KeyEvent.VK_M;

  /**
   *  The shortcut for Duplicate Selected Features.
   **/
  final static int DUPLICATE_KEY = KeyEvent.VK_D;

  /**
   *  The shortcut for Delete Selected Features.
   **/
  final static int DELETE_FEATURES_KEY = KeyEvent.VK_DELETE;

  /**
   *  Implementation of the EntryGroupChangeListener interface.  We listen to
   *  EntryGroupChange events so that we can update the display if entries
   *  are added or deleted.
   **/
  public void entryGroupChanged (final EntryGroupChangeEvent event) {
    switch (event.getType ()) {
    case EntryGroupChangeEvent.ENTRY_ADDED:
    case EntryGroupChangeEvent.ENTRY_DELETED:
    case EntryGroupChangeEvent.ENTRY_INACTIVE:
    case EntryGroupChangeEvent.ENTRY_ACTIVE:
    case EntryGroupChangeEvent.NEW_DEFAULT_ENTRY:
      refreshMenu ();
      break;
    default:
      System.err.println ("unknown event type in" +
                          " EntryGroupMenu.EntryGroupChangeEvent ()");
    }
  }

  /**
   *  Implementation of the EntryChangeListener interface.
   **/
  public void entryChanged (final EntryChangeEvent event) {
    if (event.getType () == EntryChangeEvent.NAME_CHANGED) {
      refreshMenu ();
    }
  }

  /**
   *  Update the menus to the reflect the current contents of the EntryGroup.
   **/
  private void refreshMenu () {
    removeAll ();

    edit_feature_item = new MenuItem ("Edit Selected Features",
                                      new MenuShortcut (EDIT_FEATURE_KEY));
    edit_feature_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        editSelectedFeatures (getParentFrame (), getSelection ());
      }
    });

    edit_subsequence_item = new MenuItem ("Edit Subsequence (and Features)");
    edit_subsequence_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        editSubSequence ();
      }
    });


//     raw_edit_feature_item = new MenuItem ("Raw Edit Selected Features");
//     raw_edit_feature_item.addActionListener (new ActionListener () {
//       public void actionPerformed (ActionEvent event) {
//         rawEditSelectedFeatures ();
//       }
//     });

    merge_features_item = new MenuItem ("Merge Selected Features",
                                        new MenuShortcut (MERGE_FEATURES_KEY));
    merge_features_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        mergeFeatures (getParentFrame (), getSelection ());
      }
    });

    duplicate_item = new MenuItem ("Duplicate Selected Features",
                                   new MenuShortcut (DUPLICATE_KEY));
    duplicate_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        duplicateFeatures (getParentFrame (), getSelection ());
      }
    });

    delete_features_item =
      new MenuItem ("Delete Selected Features",
                    new MenuShortcut (DELETE_FEATURES_KEY));
    delete_features_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        deleteSelectedFeatures (getParentFrame (), getSelection ());
      }
    });

    delete_segments_item = new MenuItem ("Delete Selected Exons");
    delete_segments_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        deleteSelectedSegments ();
      }
    });

    edit_header_item = new MenuItem ("Edit Header Of Default Entry");
    edit_header_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        editHeader ();
      }
    });

    paste_item = new MenuItem ("Paste");
    paste_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        //        pasteSelection (selection);
      }
    });

    duplicate_segments_item = new MenuItem ("Duplicate Selected Segments");
    duplicate_segments_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        duplicateSegments ();
      }
    });

    move_features_menu = new Menu ("Move Selected Features To");
    copy_features_menu = new Menu ("Copy Selected Features To");

    if (entry_group == null || getEntryGroup ().size () == 0) {
      move_features_menu.add (new MenuItem ("(No Entries Currently)"));
      copy_features_menu.add (new MenuItem ("(No Entries Currently)"));
    } else {
      for (int i = 0 ; i < getEntryGroup ().size () ; ++i) {
        final Entry this_entry = getEntryGroup ().elementAt (i);

        String entry_name = this_entry.getName ();

        if (entry_name == null) {
          entry_name = "no name";
        }

        final MenuItem move_to_item = new MenuItem (entry_name);

        move_to_item.addActionListener (new ActionListener () {
          public void actionPerformed (ActionEvent event) {
            // unselect, move, then reselect (for speed)
            final FeatureVector selected_features =
              getSelection ().getAllFeatures ();
            getSelection ().clear ();
            moveFeatures (selected_features, this_entry);
            getSelection ().set (selected_features);
          }
        });
        move_features_menu.add (move_to_item);

        final MenuItem copy_to_item = new MenuItem (entry_name);

        copy_to_item.addActionListener (new ActionListener () {
          public void actionPerformed (ActionEvent event) {
            copyFeatures (getSelection ().getAllFeatures (), this_entry);
          }
        });
        copy_features_menu.add (copy_to_item);
      }
    }

    fix_stop_codons_item = new MenuItem ("Fix Stop Codons");
    fix_stop_codons_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        fixStopCodons ();
      }
    });

    trim_to_any_item = new MenuItem ("Trim Selected Features To Any");
    trim_to_any_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        trimSelected (true, false);
      }
    });

    trim_item = new MenuItem ("Trim Selected Features To Met");
    trim_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        trimSelected (false, false);
      }
    });

    trim_to_next_any_item =
      new MenuItem ("Trim Selected Features To Next Any");
    trim_to_next_any_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        trimSelected (true, true);
      }
    });

    trim_to_next_item = new MenuItem ("Trim Selected Features To Next Met");
    trim_to_next_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        trimSelected (false, true);
      }
    });

    reverse_complement_item = new MenuItem ("Reverse And Complement");
    reverse_complement_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        reverseAndComplement ();
      }
    });

    delete_bases_item = new MenuItem ("Delete Selected Bases");
    delete_bases_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        deleteSelectedBases ();
      }
    });

    add_bases_item = new MenuItem ("Add Bases At Selection");
    add_bases_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        addBases ();
      }
    });

    auto_label_item = new MenuItem ("Automatically Create Gene Names");
    auto_label_item.addActionListener (new ActionListener () {
      public void actionPerformed (ActionEvent event) {
        autoGeneName ();
      }
    });

    add (edit_feature_item);
    add (edit_subsequence_item);
//    add (raw_edit_feature_item);
    addSeparator ();
    add (edit_header_item);
    addSeparator ();
    add (duplicate_item);
    add (merge_features_item);
    add (delete_features_item);
    add (delete_segments_item);
    addSeparator ();
    add (move_features_menu);
    add (copy_features_menu);
    addSeparator ();
    add (trim_item);
    add (trim_to_any_item);
    add (trim_to_next_item);
    add (trim_to_next_any_item);
    addSeparator ();
    add (auto_label_item);
    add (fix_stop_codons_item);
    add (reverse_complement_item);
    add (delete_bases_item);
    add (add_bases_item);

      //    add (paste_item);

  }

  /**
   *  If more than this number of features are selected than
   *  editSelectedFeatures () etc. will ask for confirmation.
   **/
  private static final int MAXIMUM_SELECTED_FEATURES = 10;

  /**
   *  Open an edit window (FeatureEdit) for each of the selected features.
   *  The edit component will listen for feature change events and update
   *  itself.
   *  @param frame The Frame to use for MessageDialog components.
   *  @param selection The selected features to edit.
   **/
  static void editSelectedFeatures (final Frame frame,
                                    final Selection selection) {
    if (!checkForSelectionFeatures (frame,
                                    selection,
                                    MAXIMUM_SELECTED_FEATURES,
                                    "really edit all (>" +
                                    MAXIMUM_SELECTED_FEATURES +
                                    ") selected features?")) {
      return;
    }

    final FeatureVector features_to_edit = selection.getAllFeatures ();

    for (int i = 0 ; i < features_to_edit.size () ; ++i) {
      final Feature selection_feature = features_to_edit.elementAt (i);

      new FeatureEdit (selection_feature, selection).show ();
    }

    selection.set (features_to_edit);
  }

  /**
   *  Create a new EntryEdit component that contains only the selected
   *  sequence and the features in the selected range.
   **/
  private void editSubSequence () {
    if (!checkForSelectionRange ()) {
      return;
    }

    final Range range = getSelection ().getMarkerRange ().getRawRange ();

    final EntryGroup new_entry_group = getEntryGroup ().truncate (range);

    new EntryEdit (new_entry_group).show ();
  }


  
  /**
   *  Open a EntryHeaderEdit window for the default entry.
   **/
  private void editHeader () {
    final Entry default_entry = getEntryGroup ().getDefaultEntry ();

    if (default_entry == null) {
      final String message =
        "there is no default entry";
      new MessageDialog (getParentFrame (), message);
    } else {
      new EntryHeaderEdit (entry_group, default_entry);
    }
  }

  /**
   *  Merge the selected features into one Feature.  If there are selected
   *  segments then the owning Feature of each segment will be the Feature
   *  that is merged.  This method will create a new Feature.
   *  @param frame The Frame to use for MessageDialog components.
   *  @param selection The Selection containing the features to merge.
   **/
  static void mergeFeatures (final Frame frame,
                             final Selection selection) {
    if (!checkForSelectionFeatures (frame, selection,
                                    10, "really merge all (>10) " +
                                    "selected features?")) {
      return;
    }

    final FeatureVector features_to_merge = selection.getAllFeatures ();

    final Feature merge_feature = features_to_merge.elementAt (0);

    // make sure all the features are on the same strand
    for (int i = 1 ; i < features_to_merge.size () ; ++i) {
      final Feature this_feature = features_to_merge.elementAt (i);

      if (this_feature.isForwardFeature () !=
          merge_feature.isForwardFeature ()) {
        new MessageDialog (frame,
                           "all the features in a merge must be on the same " +
                           "strand");
        return;
      }

      if (! this_feature.getKey ().equals (merge_feature.getKey ())) {
        new MessageDialog (frame,
                           "all the features in a merge must have the same " +
                           "key");
        return;
      }
    }

    if (Options.getOptions ().isNoddyMode ()) {
      final YesNoDialog dialog =
        new YesNoDialog (frame,
                         "Are you sure you want to merge the selected " +
                         "features?");

      if (!dialog.getResult ()) {
        return;
      }
    }

    try {
      final Feature new_feature = merge_feature.duplicate ();

      for (int i = 1 ; i < features_to_merge.size () ; ++i) {
        final Feature this_feature = features_to_merge.elementAt (i);

        final QualifierVector qualifiers = this_feature.getQualifiers ();

        for (int qualifier_index = 0 ;
             qualifier_index < qualifiers.size () ;
             ++qualifier_index) {
          final Qualifier this_qualifier =
            qualifiers.elementAt (qualifier_index);

          try {
            new_feature.addQualifierValues (this_qualifier);
          } catch (EntryInformationException e) {
            new_feature.removeFromEntry ();
            final String message =
              "destination entry does not support all the qualifiers " +
              "needed by " + this_feature.getIDString ();
            new MessageDialog (frame, message);
          }
        }

        final FeatureSegmentVector segments = this_feature.getSegments ();

        for (int segment_index = 0 ;
             segment_index < segments.size () ;
             ++segment_index) {
          final FeatureSegment this_segment =
            segments.elementAt (segment_index);

          final Range this_range = this_segment.getRawRange ();

          new_feature.addSegment (this_range);
        }
      }

      final boolean delete_old_features;

      if (Options.getOptions ().isNoddyMode ()) {
        final YesNoDialog delete_old_dialog =
          new YesNoDialog (frame, "delete old features?");

        delete_old_features = delete_old_dialog.getResult ();
      } else {
        delete_old_features = true;
      }

      if (delete_old_features) {
        for (int i = 0 ; i < features_to_merge.size () ; ++i) {
          features_to_merge.elementAt (i).removeFromEntry ();
        }
      }

      selection.set (new_feature);
    } catch (ReadOnlyException e) {
      final String message =
        "the destination feature is read-only or is in a read-only entry";
      new MessageDialog (frame, message);
    }
  }

  /**
   *  Duplicate the selected Feature objects.  If there are selected segments
   *  then the owning Feature of each segment will be duplicated.
   *  @param frame The Frame to use for MessageDialog components.
   *  @param selection The Selection containing the features to merge.
   **/
  static void duplicateFeatures (final Frame frame,
                                 final Selection selection) {
    if (Options.getOptions ().isNoddyMode ()) {
      final YesNoDialog dialog =
        new YesNoDialog (frame,
                         "Are you sure you want to duplicate the selected " +
                         "features?");

      if (!dialog.getResult ()) {
        return;
      }
    } else {
      if (!checkForSelectionFeatures (frame, selection,
                                      100, "really duplicate all (>100) " +
                                      "selected features?")) {
        return;
      }
    }

    final FeatureVector features_to_duplicate =
      selection.getAllFeatures ();

    for (int i = 0 ; i < features_to_duplicate.size () ; ++i) {
      final Feature this_feature = features_to_duplicate.elementAt (i);

      try {
        this_feature.duplicate ();
      } catch (ReadOnlyException e) {
        final String message =
          "one of the selected features (" + this_feature.getIDString () +
          ")  is read only - cannot continue";
        new MessageDialog (frame, message);
        return;
      }
    }
  }

  /**
   *  Duplicate the selected Feature objects.
   **/
  private void duplicateSegments () {
    if (!checkForSelectionFeatures ()) {
      return;
    }

    final FeatureSegmentVector segments_to_duplicate =
      getSelection ().getAllSegments ();

    for (int i = 0 ; i < segments_to_duplicate.size () ; ++i) {
      final FeatureSegment this_segment = segments_to_duplicate.elementAt (i);
      this_segment.duplicate ();
    }
  }

  /**
   *  Delete the selected features.
   *  @param frame The Frame to use for MessageDialog components.
   *  @param selection The Selection containing the features to merge.
   **/
  static void deleteSelectedFeatures (final Frame frame,
                                      final Selection selection) {
    final FeatureVector features_to_delete = selection.getAllFeatures ();

    if (Options.getOptions ().isNoddyMode ()) {
      // 0 means always popup a YesNoDialog
      if (!checkForSelectionFeatures (frame, selection,
                                      0, "really delete " +
                                      (features_to_delete.size () == 1 ?
                                       "the selected feature?" :
                                       features_to_delete.size () +
                                       " features?"))) {
        return;
      }
    }

    for (int i = 0 ; i < features_to_delete.size () ; ++i) {
      final Feature selection_feature = features_to_delete.elementAt (i);

      try {
        selection_feature.removeFromEntry ();
      } catch (ReadOnlyException e) {
        final String message =
          "one of the selected features (" +
          selection_feature.getIDString () +
          ") is in a read only entry - cannot continue";
        new MessageDialog (frame, message);
        return;
      }

      selection.remove (selection_feature);
    }
  }

  /**
   *  Delete the selected feature segments.
   **/
  private void deleteSelectedSegments () {
    final FeatureSegmentVector segments_to_delete =
      (FeatureSegmentVector) getSelection ().getSelectedSegments ().clone ();

    if (Options.getOptions ().isNoddyMode ()) {
      // 0 means always popup a YesNoDialog
      if (!checkForSelectionFeatureSegments (0, "really delete " +
                                             (segments_to_delete.size () == 1 ?
                                              "the selected exon?" :
                                              segments_to_delete.size () +
                                              " exons?"))) {
        return;
      }
    }

    for (int i = 0 ; i < segments_to_delete.size () ; ++i) {
      final FeatureSegment selection_segment =
        segments_to_delete.elementAt (i);

      try {
        getSelection ().remove (selection_segment);
        selection_segment.removeFromFeature ();
      } catch (ReadOnlyException e) {
        final String message =
          "one of the selected exons (in " +
          selection_segment.getFeature ().getIDString () +
          ") is in a read only entry - cannot continue";
        new MessageDialog (getParentFrame (), message);
        return;
      } catch (LastSegmentException e) {
        final String message =
          "the last exon in this feature: " +
          selection_segment.getFeature ().getIDString () +
          " cannot be removed (all features must have at least one exon)";
        new MessageDialog (getParentFrame (), message);
      }
    }
  }

  /**
   *  Move the given features to the given destination entry.
   **/
  private void moveFeatures (final FeatureVector features,
                             final Entry destination_entry) {
    for (int i = 0 ; i < features.size () ; ++i) {
      final Feature this_feature = features.elementAt (i);
      try {
        this_feature.moveTo (destination_entry, false);
      } catch (OutOfRangeException e) {
        throw new Error ("internal error - unexpected exception: " + e);
      } catch (EntryInformationException e) {
        final String message =
          "couldn't move one of the features (" +
          this_feature.getIDString () + "): " + e.getMessage ();
        new MessageDialog (getParentFrame (), message);
      } catch (ReadOnlyException e) {
        final String message =
          "one of the selected features (" +
          this_feature.getIDString () +
          ") is in a read only entry - cannot continue";
        new MessageDialog (getParentFrame (), message);
        return;
      }
    }
  }

  /**
   *  Copy the given features to the given destination entry.
   **/
  private void copyFeatures (final FeatureVector features,
                             final Entry destination_entry) {
    for (int i = 0 ; i < features.size () ; ++i) {
      final Feature this_feature = features.elementAt (i);
      try {
        this_feature.copyTo (destination_entry);
      } catch (OutOfRangeException e) {
        throw new Error ("internal error - unexpected exception: " + e);
      } catch (EntryInformationException e) {
        final String message =
          "couldn't move one of the features (" +
          this_feature.getIDString () + "): " + e.getMessage ();
        new MessageDialog (getParentFrame (), message);
      } catch (ReadOnlyException e) {
        final String message =
          "the destination entry is read only - cannot continue";
        new MessageDialog (getParentFrame (), message);
        return;
      }
    }
  }

  /**
   *  Calls fixStopCodon () on each selected feature.
   **/
  private void fixStopCodons () {
    if (!checkForSelectionFeatures ()) {
      return;
    }

    // features that can't be fixed
    final FeatureVector bad_features = new FeatureVector ();

    final FeatureVector features_to_fix = getSelection ().getAllFeatures ();

    for (int i = 0 ; i < features_to_fix.size () ; ++i) {
      final Feature selection_feature = features_to_fix.elementAt (i);

      if (!selection_feature.isCDS ()) {
        final String message =
          "Warning: some of the selected features are not coding features. " +
          "Continue?";

        final YesNoDialog yes_no_dialog =
          new YesNoDialog (getParentFrame (), message);

        if (yes_no_dialog.getResult ()) {
          break;
        } else {
          return;
        }
      }
    }

    for (int i = 0 ; i < features_to_fix.size () ; ++i) {
      final Feature selection_feature = features_to_fix.elementAt (i);

      try {
        if (!selection_feature.fixStopCodon ()) {
          bad_features.add (selection_feature);
        }
      } catch (ReadOnlyException e) {
        final String message =
          "one of the entries is read only - cannot continue";
        new MessageDialog (getParentFrame (), message);
        break;
      }
    }

    if (bad_features.size () > 0) {
      // select the bad feature
      getSelection ().set (bad_features);

      final String message =
        "Warning: could not fix the stop codon for some of the features. " +
        "View them now?";

      final YesNoDialog yes_no_dialog =
        new YesNoDialog (getParentFrame (), message);

      if (yes_no_dialog.getResult ()) {
        final FeaturePredicate predicate =
          new FeatureFromVectorPredicate (bad_features);

        final FilteredEntryGroup filtered_entry_group =
          new FilteredEntryGroup (entry_group, predicate);

        final FeatureListFrame feature_list_frame =
          new FeatureListFrame ("Features that can't be fixed" +
                                " (filtered from: " +
                                getParentFrame ().getTitle () + ")",
                                getSelection (),
                                goto_event_source, filtered_entry_group);

        feature_list_frame.setVisible (true);
      }
    }
  }

  /**
   *  Calls Feature.trimStart () on all selected Feature objects.
   *  @param trim_to_any If true then the features will be trimmed to ATG, GTG
   *    or TTG, otherwise the features will be trimmed to ATG only.
   *  @param trim_to_next If true then the features will be trimmed to the
   *    next start codon (dependent on trim_to_any) regardless of whether the
   *    feature currently start on a start codon.  If false then the feature
   *    will only be trimmed if the feature doesn't start on a start codon.
   **/
  private void trimSelected (final boolean trim_to_any,
                             final boolean trim_to_next) {
    if (!checkForSelectionFeatures ()) {
      return;
    }

    final FeatureVector features_to_trim = getSelection ().getAllFeatures ();

    // this will contain the features that could not be trimmed
    final FeatureVector failed_features = new FeatureVector ();

    for (int i = 0 ; i < features_to_trim.size () ; ++i) {
      final Feature selection_feature = features_to_trim.elementAt (i);

      try {
        if (!selection_feature.trimStart (trim_to_any, trim_to_next)) {
          failed_features.add (selection_feature);
        }
      } catch (ReadOnlyException e) {
        final String message =
          "one of the entries is read only - cannot continue";
        new MessageDialog (getParentFrame (), message);
        break;
      }
    }

    if (failed_features.size () > 0) {
      getSelection ().set (failed_features);

      if (failed_features.size () == 1) {
        final String message = "could not trim the feature";

        new MessageDialog (getParentFrame (), message);

      } else {
        final String message =
          "some of the features could not be trimmed (they are now selected)";

        new MessageDialog (getParentFrame (), message);
      }
    }
  }

  /**
   *  Reverse and complement the sequence and all the features in all Entry
   *  objects.
   **/
  private void reverseAndComplement () {
    final YesNoDialog dialog =
      new YesNoDialog (getParentFrame (),
                       "Are you sure you want to reverse complement all " +
                       "entries?");

    if (!dialog.getResult ()) {
      return;
    }

    try {
      getEntryGroup ().reverseComplement ();

      makeSelectionStartVisible ();
    } catch (ReadOnlyException e) {
      final String message =
        "one of the entries is read only - cannot continue";
      new MessageDialog (getParentFrame (), message);
      return;
    }
  }

  /**
   *  Delete the selected bases after asking the user for confimation.
   **/
  private void deleteSelectedBases () {
    if (!checkForSelectionRange ()) {
      return;
    }

    final MarkerRange range = getSelection ().getMarkerRange ();

    final FeatureVector features_in_range;

    final EntryVector old_active_entries =
      getEntryGroup ().getActiveEntries ();

    // make all the entries active so that getFeaturesInRange () gets
    // everything
    for (int i = 0 ; i < getEntryGroup ().size () ; ++i) {
      getEntryGroup ().setIsActive (i, true);
    }

    try {
      features_in_range =
        getEntryGroup ().getFeaturesInRange (range.getRawRange ());
    } catch (OutOfRangeException e) {
      throw new Error ("internal error - unexpected exception: " + e);
    }

    if (features_in_range.size () == 0) {
      // reset the EntryGroup to the state it was in originally
      for (int i = 0 ; i < getEntryGroup ().size () ; ++i) {
        final Entry this_entry = getEntryGroup ().elementAt (i);
        final boolean old_active_flag =
          old_active_entries.contains (this_entry);
        getEntryGroup ().setIsActive (i, old_active_flag);
      }

      final YesNoDialog dialog =
        new YesNoDialog (getParentFrame (),
                         "Are you sure you want to delete the " +
                         "selected bases?");

      if (dialog.getResult ()) {
        try {
          // deleteRange () is static and uses the strand reference from the
          // MarkerRange to decide which Strand to edit
          Strand.deleteRange (range);
        } catch (ReadOnlyException e) {
          new MessageDialog (getParentFrame (),
                             "sorry the bases are read only");
          return;
        }

        getSelection ().setMarkerRange (null);
      }
    } else {
      new MessageDialog (getParentFrame (),
                         "cannot delete - there are features within the " +
                         "selected range");
    }
  }

  /**
   *  Ask the user for some bases and then add them at the start of the
   *  selected range.
   **/
  private void addBases () {
    if (!checkForSelectionRange ()) {
      return;
    }

    final MarkerRange range = getSelection ().getMarkerRange ();

    final TextRequester text_requester =
      new TextRequester ("enter the bases to insert before base " +
                         range.getStart ().getPosition () +
                         (range.isForwardMarker () ?
                          " on the forward strand" :
                          " on the reverse strand") + ":",
                         18, "");

    text_requester.addTextRequesterListener (new TextRequesterListener () {
      public void actionPerformed (final TextRequesterEvent event) {
        if (event.getType () == TextRequesterEvent.CANCEL) {
          return;
        }

        final String bases_string = event.getRequesterText ().trim ();

        if (bases_string.length () == 0) {
          new MessageDialog (getParentFrame (), "no bases inserted");
          return;
        }

        try {
          // addBases () is static and uses the strand reference from the
          // start Marker to decide which Strand to edit
          Strand.addBases (range.getStart (), bases_string);
        } catch (ReadOnlyException e) {
          new MessageDialog (getParentFrame (),
                             "sorry the bases are read only");
          return;
        }
      }
    });

    text_requester.show ();
  }


  /**
   *  Automatically create systematic gene names for all CDS features in the
   *  active entries.
   **/
  private void autoGeneName () {
    // first make sure that none of the CDS features have gene names
    final FeatureEnumeration test_enumerator = getEntryGroup ().features ();

    while (test_enumerator.hasMoreFeatures ()) {
      final Feature this_feature = test_enumerator.nextFeature ();

      final Key key = this_feature.getKey ();

      if (key.equals ("CDS") &&
          this_feature.getGeneName () != null &&
          ! this_feature.getGeneName ().equals ("none")) {
        getSelection ().set (this_feature);

        new MessageDialog (getParentFrame (),
                           "cannot continue - this feature has a gene name: " +
                           this_feature.getGeneName ());
        return;
      }
    }

    final TextRequester text_requester =
      new TextRequester ("enter the start characters of the new gene names:",
                         18, "");

    text_requester.addTextRequesterListener (new TextRequesterListener () {
      public void actionPerformed (final TextRequesterEvent event) {
        if (event.getType () == TextRequesterEvent.CANCEL) {
          return;
        }

        final String name_start_string = event.getRequesterText ().trim ();

        if (name_start_string.length () == 0) {
          new MessageDialog (getParentFrame (), "no name given");
          return;
        }

        int current_number = 1;

        // now name the CDS features
        final FeatureEnumeration feature_enumerator =
          getEntryGroup ().features ();

        while (feature_enumerator.hasMoreFeatures ()) {
          final Feature this_feature = feature_enumerator.nextFeature ();

          final Key key = this_feature.getKey ();

          if (key.equals ("CDS")) {
            final String number_string;

            if (current_number < 10) {
              number_string = "0" + current_number;
            } else {
              number_string = String.valueOf (current_number);
            }

            try {
              final Qualifier new_qualifier =
                new Qualifier ("gene", name_start_string + "." +
                               number_string +
                               (this_feature.isForwardFeature () ? "" : "c"));

              this_feature.setQualifier (new_qualifier);
            } catch (EntryInformationException e) {
              new MessageDialog (getParentFrame (),
                                 "/gene not supported by this entry " +
                                 "- cannot continue");
              return;
            } catch (ReadOnlyException e) {
              new MessageDialog (getParentFrame (),
                                 "one of the feature is in a read-only " +
                                 "entry - cannot continue");
              return;
            }

            ++current_number;
          }
        }
      }
    });

    text_requester.show ();
  }

  /**
   *  Return the EntryGroup that was passed to the constructor.
   **/
  private EntryGroup getEntryGroup () {
    return entry_group;
  }

  /**
   *  This method sends an GotoEvent to all the GotoEvent listeners that will
   *  make the first base of the selection visible.
   **/
  private void makeSelectionStartVisible () {
    final GotoEvent new_event =
      new GotoEvent (this, getSelection ().getStartBaseOfSelection ());

    goto_event_source.sendGotoEvent (new_event);
  }

  /**
   *  The GotoEventSource object that was passed to the constructor.
   **/
  private GotoEventSource goto_event_source = null;

  /**
   *  The EntryGroup object that was passed to the constructor.
   **/
  private EntryGroup entry_group = null;

  private MenuItem edit_feature_item = null;
  private MenuItem raw_edit_feature_item = null;
  private MenuItem duplicate_item = null;
  private MenuItem merge_features_item = null;
  private MenuItem duplicate_segments_item = null;
  private MenuItem delete_features_item = null;
  private MenuItem edit_header_item = null;
  private MenuItem delete_segments_item = null;
  private MenuItem edit_subsequence_item = null;
  private MenuItem paste_item = null;
  private Menu move_features_menu = null;
  private Menu copy_features_menu = null;
  private MenuItem trim_item = null;
  private MenuItem trim_to_any_item = null;
  private MenuItem trim_to_next_item = null;
  private MenuItem trim_to_next_any_item = null;
  private MenuItem auto_label_item = null;
  private MenuItem fix_stop_codons_item = null;
  private MenuItem reverse_complement_item = null;
  private MenuItem delete_bases_item = null;
  private MenuItem add_bases_item = null;
}
