From 0cb52d6f867579abede6e5c6a067084518d19b1b Mon Sep 17 00:00:00 2001
From: tjc <tjc@ee4ac58c-ac51-4696-9907-e4b3aa274f04>
Date: Wed, 9 Jun 2004 09:48:01 +0000
Subject: [PATCH] Java2 version

git-svn-id: svn+ssh://svn.internal.sanger.ac.uk/repos/svn/pathsoft/artemis/trunk@1635 ee4ac58c-ac51-4696-9907-e4b3aa274f04
---
 uk/ac/sanger/artemis/components/ActMain.java  |  283 +
 uk/ac/sanger/artemis/components/AddMenu.java  | 1070 ++++
 .../artemis/components/AlignMatchViewer.java  |  366 ++
 .../artemis/components/AlignmentEvent.java    |   57 +
 .../artemis/components/AlignmentListener.java |   43 +
 .../artemis/components/AlignmentViewer.java   | 1604 ++++++
 .../components/ArtemisCheckboxMenuItem.java   |  127 +
 .../artemis/components/ArtemisMain.java       |  634 +++
 uk/ac/sanger/artemis/components/BasePlot.java |  627 +++
 .../artemis/components/BasePlotGroup.java     |  337 ++
 .../components/BioJavaEntrySource.java        |  223 +
 .../artemis/components/CanvasPanel.java       |  240 +
 .../artemis/components/ChoiceFrame.java       |  111 +
 .../artemis/components/ComparatorDialog.java  |  331 ++
 .../artemis/components/ComparatorGlue.java    |  464 ++
 .../artemis/components/CorbaEntrySource.java  |   90 +
 .../components/DbfetchEntrySource.java        |  253 +
 .../components/DisplayAdjustmentEvent.java    |  186 +
 .../components/DisplayAdjustmentListener.java |   45 +
 .../artemis/components/DisplayComponent.java  |   52 +
 .../components/EMBLCorbaEntrySource.java      |  318 ++
 uk/ac/sanger/artemis/components/EditMenu.java | 2447 +++++++++
 .../components/EntryActionListener.java       |   63 +
 .../sanger/artemis/components/EntryEdit.java  | 1434 +++++
 .../artemis/components/EntryEditVector.java   |   92 +
 .../artemis/components/EntryFileDialog.java   |  463 ++
 .../artemis/components/EntryGroupDisplay.java |  240 +
 .../components/EntryGroupInfoDisplay.java     |  536 ++
 .../artemis/components/EntryGroupMenu.java    |  336 ++
 .../artemis/components/EntryGroupPanel.java   |  376 ++
 .../artemis/components/EntryHeaderEdit.java   |  209 +
 .../components/ExternalProgramOptions.java    |   66 +
 .../components/FeatureAminoAcidViewer.java    |  172 +
 .../artemis/components/FeatureBaseViewer.java |  166 +
 .../artemis/components/FeatureDisplay.java    | 4699 +++++++++++++++++
 .../artemis/components/FeatureEdit.java       | 1276 +++++
 .../artemis/components/FeatureInfo.java       |  658 +++
 .../artemis/components/FeatureList.java       |  971 ++++
 .../artemis/components/FeatureListFrame.java  |  195 +
 .../artemis/components/FeaturePlot.java       |  389 ++
 .../artemis/components/FeaturePlotGroup.java  |  337 ++
 .../artemis/components/FeaturePopup.java      |  758 +++
 .../artemis/components/FeatureViewer.java     |  151 +
 .../components/FileDialogEntrySource.java     |  286 +
 .../artemis/components/FileManager.java       |  401 ++
 uk/ac/sanger/artemis/components/FileNode.java |  159 +
 uk/ac/sanger/artemis/components/FileTree.java |  941 ++++
 .../sanger/artemis/components/FileViewer.java |  293 +
 uk/ac/sanger/artemis/components/GotoMenu.java |  510 ++
 .../sanger/artemis/components/GraphMenu.java  |  403 ++
 .../components/InputStreamProgressDialog.java |  126 +
 .../sanger/artemis/components/KeyChoice.java  |  203 +
 .../sanger/artemis/components/KeyChooser.java |  118 +
 .../sanger/artemis/components/ListDialog.java |  128 +
 .../artemis/components/LogReadListener.java   |   76 +
 .../sanger/artemis/components/LogViewer.java  |  102 +
 .../components/MarkerRangeRequester.java      |  106 +
 .../components/MarkerRangeRequesterEvent.java |  199 +
 .../MarkerRangeRequesterListener.java         |   42 +
 .../artemis/components/MessageDialog.java     |  143 +
 .../artemis/components/MessageFrame.java      |  106 +
 .../artemis/components/MultiComparator.java   | 1091 ++++
 .../sanger/artemis/components/Navigator.java  |  933 ++++
 uk/ac/sanger/artemis/components/Plot.java     |  723 +++
 .../artemis/components/PlotMouseListener.java |   64 +
 .../artemis/components/ProcessWatcher.java    |  194 +
 .../components/ProcessWatcherEvent.java       |   70 +
 .../components/ProcessWatcherListener.java    |   42 +
 .../artemis/components/ProgressThread.java    |   64 +
 .../artemis/components/QualifierChoice.java   |  181 +
 .../artemis/components/QualifierEditor.java   |  332 ++
 .../artemis/components/QualifierTextArea.java |   96 +
 uk/ac/sanger/artemis/components/RunMenu.java  |  278 +
 .../artemis/components/ScoreChangeEvent.java  |   58 +
 .../components/ScoreChangeListener.java       |   40 +
 .../artemis/components/ScoreChanger.java      |  222 +
 .../artemis/components/ScoreScrollbar.java    |  120 +
 .../components/SearchResultViewer.java        |  108 +
 .../sanger/artemis/components/SelectMenu.java |  827 +++
 .../components/SelectionInfoDisplay.java      |  435 ++
 .../artemis/components/SelectionMenu.java     |  294 ++
 .../artemis/components/SelectionViewer.java   |  352 ++
 uk/ac/sanger/artemis/components/Selector.java |  885 ++++
 .../artemis/components/SequenceViewer.java    |  165 +
 uk/ac/sanger/artemis/components/Splash.java   |  526 ++
 .../artemis/components/StickyFileChooser.java |   99 +
 .../artemis/components/SwingWorker.java       |  150 +
 .../artemis/components/TaskViewerFrame.java   |   88 +
 .../sanger/artemis/components/TextDialog.java |  119 +
 .../artemis/components/TextFieldSink.java     |  142 +
 .../artemis/components/TextRequester.java     |  158 +
 .../components/TextRequesterEvent.java        |   87 +
 .../components/TextRequesterListener.java     |   44 +
 .../sanger/artemis/components/Utilities.java  |  144 +
 uk/ac/sanger/artemis/components/ViewMenu.java | 1430 +++++
 .../WritableEMBLCorbaEntrySource.java         |  226 +
 .../sanger/artemis/components/WriteMenu.java  |  852 +++
 .../artemis/components/YesNoDialog.java       |  115 +
 98 files changed, 39591 insertions(+)
 create mode 100644 uk/ac/sanger/artemis/components/ActMain.java
 create mode 100644 uk/ac/sanger/artemis/components/AddMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/AlignMatchViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/AlignmentEvent.java
 create mode 100644 uk/ac/sanger/artemis/components/AlignmentListener.java
 create mode 100644 uk/ac/sanger/artemis/components/AlignmentViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/ArtemisCheckboxMenuItem.java
 create mode 100644 uk/ac/sanger/artemis/components/ArtemisMain.java
 create mode 100644 uk/ac/sanger/artemis/components/BasePlot.java
 create mode 100644 uk/ac/sanger/artemis/components/BasePlotGroup.java
 create mode 100644 uk/ac/sanger/artemis/components/BioJavaEntrySource.java
 create mode 100644 uk/ac/sanger/artemis/components/CanvasPanel.java
 create mode 100644 uk/ac/sanger/artemis/components/ChoiceFrame.java
 create mode 100644 uk/ac/sanger/artemis/components/ComparatorDialog.java
 create mode 100644 uk/ac/sanger/artemis/components/ComparatorGlue.java
 create mode 100644 uk/ac/sanger/artemis/components/CorbaEntrySource.java
 create mode 100644 uk/ac/sanger/artemis/components/DbfetchEntrySource.java
 create mode 100644 uk/ac/sanger/artemis/components/DisplayAdjustmentEvent.java
 create mode 100644 uk/ac/sanger/artemis/components/DisplayAdjustmentListener.java
 create mode 100644 uk/ac/sanger/artemis/components/DisplayComponent.java
 create mode 100644 uk/ac/sanger/artemis/components/EMBLCorbaEntrySource.java
 create mode 100644 uk/ac/sanger/artemis/components/EditMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/EntryActionListener.java
 create mode 100644 uk/ac/sanger/artemis/components/EntryEdit.java
 create mode 100644 uk/ac/sanger/artemis/components/EntryEditVector.java
 create mode 100644 uk/ac/sanger/artemis/components/EntryFileDialog.java
 create mode 100644 uk/ac/sanger/artemis/components/EntryGroupDisplay.java
 create mode 100644 uk/ac/sanger/artemis/components/EntryGroupInfoDisplay.java
 create mode 100644 uk/ac/sanger/artemis/components/EntryGroupMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/EntryGroupPanel.java
 create mode 100644 uk/ac/sanger/artemis/components/EntryHeaderEdit.java
 create mode 100644 uk/ac/sanger/artemis/components/ExternalProgramOptions.java
 create mode 100644 uk/ac/sanger/artemis/components/FeatureAminoAcidViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/FeatureBaseViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/FeatureDisplay.java
 create mode 100644 uk/ac/sanger/artemis/components/FeatureEdit.java
 create mode 100644 uk/ac/sanger/artemis/components/FeatureInfo.java
 create mode 100644 uk/ac/sanger/artemis/components/FeatureList.java
 create mode 100644 uk/ac/sanger/artemis/components/FeatureListFrame.java
 create mode 100644 uk/ac/sanger/artemis/components/FeaturePlot.java
 create mode 100644 uk/ac/sanger/artemis/components/FeaturePlotGroup.java
 create mode 100644 uk/ac/sanger/artemis/components/FeaturePopup.java
 create mode 100644 uk/ac/sanger/artemis/components/FeatureViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/FileDialogEntrySource.java
 create mode 100644 uk/ac/sanger/artemis/components/FileManager.java
 create mode 100644 uk/ac/sanger/artemis/components/FileNode.java
 create mode 100644 uk/ac/sanger/artemis/components/FileTree.java
 create mode 100644 uk/ac/sanger/artemis/components/FileViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/GotoMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/GraphMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/InputStreamProgressDialog.java
 create mode 100644 uk/ac/sanger/artemis/components/KeyChoice.java
 create mode 100644 uk/ac/sanger/artemis/components/KeyChooser.java
 create mode 100644 uk/ac/sanger/artemis/components/ListDialog.java
 create mode 100644 uk/ac/sanger/artemis/components/LogReadListener.java
 create mode 100644 uk/ac/sanger/artemis/components/LogViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/MarkerRangeRequester.java
 create mode 100644 uk/ac/sanger/artemis/components/MarkerRangeRequesterEvent.java
 create mode 100644 uk/ac/sanger/artemis/components/MarkerRangeRequesterListener.java
 create mode 100644 uk/ac/sanger/artemis/components/MessageDialog.java
 create mode 100644 uk/ac/sanger/artemis/components/MessageFrame.java
 create mode 100644 uk/ac/sanger/artemis/components/MultiComparator.java
 create mode 100644 uk/ac/sanger/artemis/components/Navigator.java
 create mode 100644 uk/ac/sanger/artemis/components/Plot.java
 create mode 100644 uk/ac/sanger/artemis/components/PlotMouseListener.java
 create mode 100644 uk/ac/sanger/artemis/components/ProcessWatcher.java
 create mode 100644 uk/ac/sanger/artemis/components/ProcessWatcherEvent.java
 create mode 100644 uk/ac/sanger/artemis/components/ProcessWatcherListener.java
 create mode 100644 uk/ac/sanger/artemis/components/ProgressThread.java
 create mode 100644 uk/ac/sanger/artemis/components/QualifierChoice.java
 create mode 100644 uk/ac/sanger/artemis/components/QualifierEditor.java
 create mode 100644 uk/ac/sanger/artemis/components/QualifierTextArea.java
 create mode 100644 uk/ac/sanger/artemis/components/RunMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/ScoreChangeEvent.java
 create mode 100644 uk/ac/sanger/artemis/components/ScoreChangeListener.java
 create mode 100644 uk/ac/sanger/artemis/components/ScoreChanger.java
 create mode 100644 uk/ac/sanger/artemis/components/ScoreScrollbar.java
 create mode 100644 uk/ac/sanger/artemis/components/SearchResultViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/SelectMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/SelectionInfoDisplay.java
 create mode 100644 uk/ac/sanger/artemis/components/SelectionMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/SelectionViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/Selector.java
 create mode 100644 uk/ac/sanger/artemis/components/SequenceViewer.java
 create mode 100644 uk/ac/sanger/artemis/components/Splash.java
 create mode 100644 uk/ac/sanger/artemis/components/StickyFileChooser.java
 create mode 100644 uk/ac/sanger/artemis/components/SwingWorker.java
 create mode 100644 uk/ac/sanger/artemis/components/TaskViewerFrame.java
 create mode 100644 uk/ac/sanger/artemis/components/TextDialog.java
 create mode 100644 uk/ac/sanger/artemis/components/TextFieldSink.java
 create mode 100644 uk/ac/sanger/artemis/components/TextRequester.java
 create mode 100644 uk/ac/sanger/artemis/components/TextRequesterEvent.java
 create mode 100644 uk/ac/sanger/artemis/components/TextRequesterListener.java
 create mode 100644 uk/ac/sanger/artemis/components/Utilities.java
 create mode 100644 uk/ac/sanger/artemis/components/ViewMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/WritableEMBLCorbaEntrySource.java
 create mode 100644 uk/ac/sanger/artemis/components/WriteMenu.java
 create mode 100644 uk/ac/sanger/artemis/components/YesNoDialog.java

diff --git a/uk/ac/sanger/artemis/components/ActMain.java b/uk/ac/sanger/artemis/components/ActMain.java
new file mode 100644
index 000000000..486d2a4a6
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ActMain.java
@@ -0,0 +1,283 @@
+/* ActMain.java
+ *
+ * created: Wed May 10 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ActMain.java,v 1.1 2004-06-09 09:45:56 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.Bases;
+
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.SimpleEntryInformation;
+
+import java.awt.event.*;
+import java.io.IOException;
+import javax.swing.JFrame;
+
+/**
+ *  The main window for the Artemis Comparison Tool.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: ActMain.java,v 1.1 2004-06-09 09:45:56 tjc Exp $
+ **/
+
+public class ActMain extends Splash 
+{
+  /** Version String use for banner messages and title bars. */
+  public static final String version = "Release 2";
+  /** File manager */
+  protected static FileManager filemanager = null;
+
+  /**
+   *  The constructor creates all the components for the main ACT window
+   *  and sets up all the menu callbacks.
+   **/
+  public ActMain() 
+  {
+    super("Artemis Comparison Tool", "ACT", version);
+
+    ActionListener open_listener = new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event) 
+      {
+        makeOpenDialog();
+      }
+    };
+
+    makeMenuItem(file_menu, "Open ...", open_listener);
+
+    ActionListener quit_listener = new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event) 
+      {
+        exit();
+      }
+    };
+
+    makeMenuItem(file_menu, "Quit", quit_listener);
+  }
+
+  /**
+   *  Make a new Comparator component from the given files.
+   *  @param frame The JFrame used when making a new MessageDialog.
+   *  @param progress_listener The object to which InputStreamProgressEvents
+   *    will be send while reading.  Can be null.
+   *  @param file_names Alternating sequence and comparison data file names.
+   *    Must be >= 3.  I there are an even number of file names the first
+   *    file/sequence object will be added to the send of the display and the
+   *    last comparison file will be assumed to be a a comparison between the
+   *    last and first sequence files.
+   **/
+  public static boolean makeMultiComparator(final JFrame frame,
+                          final InputStreamProgressListener progress_listener,
+                          final String[] file_names) 
+  {
+
+    final ProgressThread progress_thread = new ProgressThread(null,
+                                                "Loading Entry...");
+
+    SwingWorker entryWorker = new SwingWorker()
+    {
+      public Object construct()
+      {
+        progress_thread.start();
+        final EntryGroup[] entry_group_array =
+          new EntryGroup[file_names.length / 2 + 1];
+
+        final ComparisonData[] comparison_data_array =
+          new ComparisonData[file_names.length / 2];
+
+        for(int i = 0; i<file_names.length; i += 2) 
+        {
+          final EntryInformation entry_information =
+            new SimpleEntryInformation(Options.getArtemisEntryInformation());
+
+          final String this_file_name = file_names[i];
+
+          final Document entry_document =
+            DocumentFactory.makeDocument(this_file_name);
+
+          if(progress_listener != null) 
+            entry_document.addInputStreamProgressListener(progress_listener);
+
+          final uk.ac.sanger.artemis.io.Entry embl_entry =
+            EntryFileDialog.getEntryFromFile(frame, entry_document,
+                                         entry_information,
+                                         true);
+
+          // getEntryFromFile() has alerted the user so we just need to quit
+          if(embl_entry == null) 
+            return null;
+
+          final uk.ac.sanger.artemis.io.Sequence sequence =
+                                             embl_entry.getSequence();
+
+          if(sequence == null) 
+          {
+            new MessageDialog(frame, "This file contains no sequence: " +
+                               this_file_name);
+            return null;
+          }
+
+          final Bases embl_bases = new Bases(sequence);
+          final EntryGroup entry_group = new SimpleEntryGroup(embl_bases);
+
+          try 
+          {
+            final Entry entry = new Entry(entry_group.getBases(), embl_entry);
+            entry_group.add(entry);
+            entry_group_array[i / 2] = entry_group;
+          } 
+          catch(OutOfRangeException e) 
+          {
+            new MessageDialog(frame, "read failed: one of the features has an " +
+                               "out of range location: " + e.getMessage());
+            return null;
+          }
+        }
+
+        // add the first entry at the end to make the MultiComparator
+        // circular(-ish)
+        if(file_names.length % 2 == 0) 
+          entry_group_array[entry_group_array.length - 1] = entry_group_array[0];
+
+        try
+        {
+          for(int i = 1 ; i < file_names.length ; i += 2) 
+          {
+            final String comparison_data_file_name = file_names[i];
+            final Document comparison_data_document =
+              DocumentFactory.makeDocument(comparison_data_file_name);
+
+            comparison_data_array[i / 2] =
+              ComparisonDataFactory.readComparisonData(comparison_data_document);
+
+            final Bases prev_bases = entry_group_array[i/2].getBases();
+            final Bases next_bases = entry_group_array[i/2 + 1].getBases();
+
+            final ComparisonData swapped_comparison_data =
+              comparison_data_array[i / 2].flipMatchesIfNeeded(prev_bases,
+                                                            next_bases);
+
+            if(swapped_comparison_data != null) 
+              comparison_data_array[i / 2] = swapped_comparison_data;
+
+            if(swapped_comparison_data != null)
+            {
+              final MessageFrame message_frame =
+                new MessageFrame("note: hits from " + comparison_data_file_name +
+                                  " have been flipped to match the " +
+                                  "sequences");
+
+              message_frame.setVisible(true);
+            }
+          }
+        } 
+        catch(IOException e) 
+        {
+          new MessageDialog(frame, "error while reading: " + e.getMessage());
+          return null;
+        }
+        catch(OutOfRangeException e) 
+        {
+          new MessageDialog(frame, "comparison file read failed:  " +
+                             "out of range error: " + e.getMessage());
+          return null;
+        }
+        
+        final MultiComparator comparator =
+          new MultiComparator(entry_group_array,
+                              comparison_data_array,
+                              progress_listener);
+
+        comparator.setVisible(true);
+        return null;
+      }
+
+      public void finished()
+      {
+        if(progress_thread !=null)
+          progress_thread.finished();
+      }
+
+    };
+    entryWorker.start();
+
+    return true;
+  }
+
+  /**
+   *  Create a dialog that allow the user to the choose two files to compare
+   *  and a file containing comparison data.
+   **/
+  private void makeOpenDialog() 
+  {
+    if(filemanager == null)
+      filemanager = new FileManager(this,null);
+    else
+      filemanager.setVisible(true);
+    new ComparatorDialog(this).setVisible(true);
+  }
+
+  /**
+   *  Exit from ACT.
+   **/
+  protected void exit() 
+  {
+    System.exit(0);
+  }
+
+  /**
+   *  Main entry point for ACT
+   **/
+  public static void main(final String [] args) 
+  {
+    final ActMain main_window = new ActMain();
+    main_window.setVisible(true);
+
+    final InputStreamProgressListener progress_listener =
+      main_window.getInputStreamProgressListener();
+    
+    if(args.length >= 3) 
+      ActMain.makeMultiComparator(main_window, progress_listener,
+                                  args);
+    else 
+    {
+      if(args.length != 0) 
+      {
+        System.err.println("Error - this program needs either no " +
+                           " arguments or an odd number\n" +
+                           "(3 or more):");
+        System.err.println("   act sequence_1 comparison_data sequence_2");
+        System.err.println("or");
+        System.err.println("   act seq_1 comparison_data_2_v_1 seq_2 comparison_data_3_v_2 seq_3");
+        System.err.println("or");
+        System.err.println("   act");
+        System.exit(1);
+      }
+    }
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/AddMenu.java b/uk/ac/sanger/artemis/components/AddMenu.java
new file mode 100644
index 000000000..c432f8748
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/AddMenu.java
@@ -0,0 +1,1070 @@
+/* AddMenu.java
+ *
+ * created: Tue Dec 29 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1998,1999,2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/AddMenu.java,v 1.1 2004-06-09 09:45:57 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.plot.CodonUsageAlgorithm;
+
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.Range;
+import uk.ac.sanger.artemis.io.RangeVector;
+import uk.ac.sanger.artemis.io.Location;
+import uk.ac.sanger.artemis.io.QualifierVector;
+import uk.ac.sanger.artemis.io.Qualifier;
+import uk.ac.sanger.artemis.io.QualifierParseException;
+import uk.ac.sanger.artemis.io.InvalidQualifierException;
+import uk.ac.sanger.artemis.io.InvalidRelationException;
+import uk.ac.sanger.artemis.io.InvalidKeyException;
+import uk.ac.sanger.artemis.io.LocationParseException;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A Menu with commands that add new features/entries to an EntryGroup.  This
+ *  should have been called CreateMenu.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: AddMenu.java,v 1.1 2004-06-09 09:45:57 tjc Exp $
+ **/
+
+public class AddMenu extends SelectionMenu {
+  /**
+   *  The shortcut for "Create From Base Range".
+   **/
+  final static KeyStroke CREATE_FROM_BASE_RANGE_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_C, InputEvent.CTRL_MASK);
+
+  final static public int CREATE_FROM_BASE_RANGE_KEY_CODE = KeyEvent.VK_C;
+
+  /**
+   *  Create a new AddMenu object.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param entry_group The EntryGroup object where new features/entries will
+   *    be added.
+   *  @param goto_event_source The object the we will call makeBaseVisible ()
+   *    on.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   *  @param menu_name The name of the new menu.
+   **/
+  public AddMenu (final JFrame frame,
+                  final Selection selection,
+                  final EntryGroup entry_group,
+                  final GotoEventSource goto_event_source,
+                  final BasePlotGroup base_plot_group,
+                  final String menu_name) {
+    super (frame, menu_name, selection);
+
+    this.entry_group = entry_group;
+    this.base_plot_group = base_plot_group;
+
+    new_feature_item = new JMenuItem ("New Feature");
+    new_feature_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        makeNewFeature ();
+      }
+    });
+
+    add (new_feature_item);
+
+    create_feature_from_range_item =
+      new JMenuItem ("Create Feature From Base Range");
+    create_feature_from_range_item.setAccelerator (CREATE_FROM_BASE_RANGE_KEY);
+    create_feature_from_range_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        createFeatureFromBaseRange (getParentFrame (), getSelection (),
+                                    entry_group, getGotoEventSource ());
+      }
+    });
+
+    add (create_feature_from_range_item);
+
+    create_intron_features_item =
+      new JMenuItem ("Create Intron Features");
+    create_intron_features_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        createIntronFeatures (getParentFrame (), getSelection (),
+                              entry_group);
+      }
+    });
+
+    add (create_intron_features_item);
+
+    create_exon_features_item =
+      new JMenuItem ("Create Exon Features");
+    create_exon_features_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        createExonFeatures (getParentFrame (), getSelection (),
+                            entry_group);
+      }
+    });
+
+    add (create_exon_features_item);
+
+    create_gene_features_item =
+      new JMenuItem ("Create Gene Features");
+    create_gene_features_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        createGeneFeatures (getParentFrame (), getSelection (),
+                            entry_group);
+      }
+    });
+
+    add (create_gene_features_item);
+
+    addSeparator ();
+
+    new_entry_item = new JMenuItem ("New Entry");
+    new_entry_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        makeNewEntry ();
+      }
+    });
+
+    add (new_entry_item);
+
+    addSeparator ();
+
+    mark_orfs_with_size_item = new JMenuItem ("Mark Open Reading Frames ...");
+
+    mark_orfs_with_size_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        markORFSWithSize (false);
+      }
+    });
+
+    add (mark_orfs_with_size_item);
+
+
+    mark_empty_orfs_with_size_item = new JMenuItem ("Mark Empty ORFs ...");
+
+    mark_empty_orfs_with_size_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        markORFSWithSize (true);
+      }
+    });
+
+    add (mark_empty_orfs_with_size_item);
+
+
+    mark_orfs_range_item = new JMenuItem ("Mark ORFs In Range ...");
+    mark_orfs_range_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        markOpenReadingFramesInRange ();
+      }
+    });
+
+    add (mark_orfs_range_item);
+
+
+    mark_pattern_item = new JMenuItem ("Mark From Pattern ...");
+    mark_pattern_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        makeFeaturesFromPattern ();
+      }
+    });
+
+    add (mark_pattern_item);
+
+    mark_ambiguities_item = new JMenuItem ("Mark Ambiguities");
+    mark_ambiguities_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        markAmbiguities ();
+      }
+    });
+
+    add (mark_ambiguities_item);
+  }
+
+  /**
+   *  Create a new AddMenu object.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param entry_group The EntryGroup object where new features/entries will
+   *    be added.
+   *  @param goto_event_source The object the we will call makeBaseVisible ()
+   *    on.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public AddMenu (final JFrame frame,
+                  final Selection selection,
+                  final EntryGroup entry_group,
+                  final GotoEventSource goto_event_source,
+                  final BasePlotGroup base_plot_group) {
+    this (frame, selection, entry_group,
+          goto_event_source, base_plot_group, "Create");
+  }
+
+  /**
+   *  Create a new feature in the default Entry of the entry group.  See
+   *  EntryGroup.createFeature () for details.
+   **/
+  private void makeNewFeature () {
+    if (entry_group.size () > 0) {
+      if (entry_group.getDefaultEntry () == null) {
+        new MessageDialog (getParentFrame (), "There is no default entry");
+      } else {
+
+        try {
+          entry_group.getActionController ().startAction ();
+
+          final Feature new_feature = entry_group.createFeature ();
+
+          final FeatureEdit feature_edit =
+            new FeatureEdit (new_feature, entry_group, getSelection (),
+                             getGotoEventSource ());
+
+          final ActionListener cancel_listener =
+            new ActionListener () {
+              public void actionPerformed (ActionEvent e) {
+                try {
+                  new_feature.removeFromEntry ();
+                } catch (ReadOnlyException exception) {
+                  throw new Error ("internal error - unexpected exception: " +
+                                   exception);
+                }
+              }
+            };
+
+          feature_edit.addCancelActionListener (cancel_listener);
+
+          feature_edit.addApplyActionListener (new ActionListener () {
+            public void actionPerformed (ActionEvent e) {
+              // after apply is pressed cancel should not remove the new
+              // feature
+              feature_edit.removeCancelActionListener (cancel_listener);
+            }
+          });
+
+          feature_edit.show ();
+        } catch (ReadOnlyException e) {
+          new MessageDialog (getParentFrame (), "feature not created: " +
+                             "the default entry is read only");
+        } finally {
+          entry_group.getActionController ().endAction ();
+        }
+      }
+    } else {
+      new MessageDialog (getParentFrame (),
+                         "Cannot make a feature without an existing entry");
+    }
+  }
+
+  /**
+   *  Create a new Entry in the first Entry of the entry group.
+   **/
+  private void makeNewEntry () {
+    entry_group.createEntry ();
+  }
+
+  /**
+   *  Create a new Feature in entry_group from the selected range of bases and
+   *  then display a FeatureEdit component for the new Feature.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The Selection containing the sequence to create the
+   *    feature from.
+   *  @param entry_group The EntryGroup to create the feature in.
+   *  @param goto_event_source Needed to create a FeatureEdit component.
+   **/
+  static void createFeatureFromBaseRange (final JFrame frame,
+                                          final Selection selection,
+                                          final EntryGroup entry_group,
+                                          final GotoEventSource
+                                            goto_event_source) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      if (!checkForSelectionRange (frame, selection)) {
+        return;
+      }
+
+      final MarkerRange range = selection.getMarkerRange ();
+      final Entry default_entry = entry_group.getDefaultEntry ();
+
+      if (default_entry == null) {
+        new MessageDialog (frame, "There is no default entry");
+        return;
+      }
+
+      try {
+        final Location new_location = range.createLocation ();
+
+        /*final*/ Feature temp_feature;
+
+        try {
+          temp_feature = default_entry.createFeature (Key.CDS, new_location);
+        } catch (EntryInformationException e) {
+          // use the default key instead
+
+          final Key default_key =
+            default_entry.getEntryInformation ().getDefaultKey ();
+
+          try {
+            temp_feature =
+              default_entry.createFeature (default_key, new_location);
+          } catch (EntryInformationException ex) {
+            throw new Error ("internal error - unexpected exception: " + ex);
+          }
+        }
+
+        final Feature new_feature = temp_feature;
+
+        selection.setMarkerRange (null);
+        selection.set (new_feature);
+
+        final FeatureEdit feature_edit =
+          new FeatureEdit (new_feature, entry_group,
+                           selection, goto_event_source);
+
+        final ActionListener cancel_listener =
+          new ActionListener () {
+            public void actionPerformed (ActionEvent e) {
+              try {
+                new_feature.removeFromEntry ();
+                selection.setMarkerRange (range);
+              } catch (ReadOnlyException exception) {
+                throw new Error ("internal error - unexpected exception: " +
+                                 exception);
+              }
+            }
+          };
+
+        feature_edit.addCancelActionListener (cancel_listener);
+
+        feature_edit.addApplyActionListener (new ActionListener () {
+          public void actionPerformed (ActionEvent e) {
+            // after apply is pressed cancel should not remove the new
+            // feature
+            feature_edit.removeCancelActionListener (cancel_listener);
+          }
+        });
+
+        feature_edit.show ();
+      } catch (ReadOnlyException e) {
+        new MessageDialog (frame, "feature not created: " +
+                           "the default entry is read only");
+      } catch (OutOfRangeException e) {
+        throw new Error ("internal error - unexpected exception: " + e);
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Create a new intron between each pair of exons in the selected CDS
+   *  features.  The introns are created in the Entry that contains the CDSs.
+   *  @param frame The Frame to use for MessageDialog components.
+   *  @param selection The Selection containing the CDS features to create the
+   *    introns for.
+   *  @param entry_group The EntryGroup to create the features in.
+   **/
+  static void createIntronFeatures (final JFrame frame,
+                                    final Selection selection,
+                                    final EntryGroup entry_group) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      if (!checkForSelectionFeatures (frame, selection)) {
+        return;
+      }
+
+      final FeatureVector selected_features = selection.getAllFeatures ();
+
+      for (int feature_index = 0 ;
+           feature_index < selected_features.size () ;
+           ++feature_index) {
+
+        final Feature selection_feature =
+          selected_features.elementAt (feature_index);
+
+        if (!selection_feature.isProteinFeature ()) {
+          continue;
+        }
+
+        final Location cds_location = selection_feature.getLocation ();
+
+        final RangeVector cds_ranges = cds_location.getRanges ();
+
+        if (cds_ranges.size () < 2) {
+          continue;
+        }
+
+        if (cds_location.isComplement ()) {
+          cds_ranges.reverse ();
+        }
+
+        for (int range_index = 0 ;
+             range_index < cds_ranges.size () - 1 ;
+             ++range_index) {
+          final int end_of_range_1 =
+            cds_ranges.elementAt (range_index).getEnd ();
+          final int start_of_range_2 =
+            cds_ranges.elementAt (range_index + 1).getStart ();
+
+          if (end_of_range_1 > start_of_range_2) {
+            // ignore - the exons overlap so there is no room for an intron
+            continue;
+          }
+
+          final Range new_range;
+
+          try {
+            new_range = new Range (end_of_range_1 + 1,
+                                   start_of_range_2 - 1);
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          }
+
+          final RangeVector intron_ranges = new RangeVector ();
+
+          intron_ranges.add (new_range);
+
+          final Key intron_key = new Key ("intron");
+          final Location intron_location =
+            new Location (intron_ranges, cds_location.isComplement ());
+          final QualifierVector qualifiers = new QualifierVector ();
+
+          try {
+            selection_feature.getEntry ().createFeature (intron_key,
+                                                         intron_location,
+                                                         qualifiers);
+          } catch (ReadOnlyException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          } catch (EntryInformationException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          }
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Create a new exon for each FeatureSegment in the selected CDS features.
+   *  The exons are created in the Entry that contains the CDSs.
+   *  @param frame The Frame to use for MessageDialog components.
+   *  @param selection The Selection containing the CDS features to create the
+   *    exons for.
+   *  @param entry_group The EntryGroup to create the features in.
+   **/
+  static void createExonFeatures (final JFrame frame,
+                                  final Selection selection,
+                                  final EntryGroup entry_group) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      if (!checkForSelectionFeatures (frame, selection)) {
+        return;
+      }
+
+      final FeatureVector selected_features = selection.getAllFeatures ();
+
+      for (int feature_index = 0 ;
+           feature_index < selected_features.size () ;
+           ++feature_index) {
+
+        final Feature selection_feature =
+          selected_features.elementAt (feature_index);
+
+        if (!selection_feature.isProteinFeature ()) {
+          continue;
+        }
+
+        final Location cds_location = selection_feature.getLocation ();
+
+        final RangeVector cds_ranges = cds_location.getRanges ();
+
+        for (int range_index = 0 ;
+             range_index < cds_ranges.size () ;
+             ++range_index) {
+          final Range this_range = cds_ranges.elementAt (range_index);
+
+          final RangeVector exon_ranges = new RangeVector ();
+
+          exon_ranges.add (this_range);
+
+          final Key exon_key = new Key ("exon");
+          final Location exon_location =
+            new Location (exon_ranges, cds_location.isComplement ());
+          final QualifierVector qualifiers = new QualifierVector ();
+
+          try {
+            selection_feature.getEntry ().createFeature (exon_key,
+                                                         exon_location,
+                                                         qualifiers);
+          } catch (ReadOnlyException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          } catch (EntryInformationException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          }
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Create a new gene for each of the selected CDS features.
+   *  @param frame The Frame to use for MessageDialog components.
+   *  @param selection The Selection containing the CDS features.
+   *  @param entry_group The EntryGroup to create the features in.
+   **/
+  static void createGeneFeatures (final JFrame frame,
+                                  final Selection selection,
+                                  final EntryGroup entry_group) {
+    /*
+     *  XXX - FIXME - include 5'UTR at the start and 3'UTR at the end and if
+     *  two (or more) CDSs have the same primary name create one gene feature
+     *  that covers them.
+     */    
+    try {
+      entry_group.getActionController ().startAction ();
+
+      if (!checkForSelectionFeatures (frame, selection)) {
+        return;
+      }
+
+      final FeatureVector selected_features = selection.getAllFeatures ();
+
+      for (int feature_index = 0 ;
+           feature_index < selected_features.size () ;
+           ++feature_index) {
+
+        final Feature selection_feature =
+          selected_features.elementAt (feature_index);
+
+        if (!selection_feature.isProteinFeature ()) {
+          continue;
+        }
+
+        final Range max_range = selection_feature.getMaxRawRange ();
+        final boolean complement_flag =
+          selection_feature.getLocation ().isComplement ();
+
+        final RangeVector ranges = new RangeVector ();
+        ranges.add (max_range);
+
+        final Key gene_key = new Key ("gene");
+        final Location gene_location =
+          new Location (ranges, complement_flag);
+        final QualifierVector qualifiers = new QualifierVector ();
+
+        try {
+          selection_feature.getEntry ().createFeature (gene_key,
+                                                       gene_location,
+                                                       qualifiers);
+        } catch (ReadOnlyException e) {
+          throw new Error ("internal error - unexpected exception: " + e);
+        } catch (EntryInformationException e) {
+          throw new Error ("internal error - unexpected exception: " + e);
+        } catch (OutOfRangeException e) {
+          throw new Error ("internal error - unexpected exception: " + e);
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Open a TextRequester to ask the user for the minimum ORF size then call
+   *  markOpenReadingFrames ().
+   *  @param empty_only If true only those ORFS that don't already contain a
+   *    segment will be marked.
+   **/
+  private void markORFSWithSize (final boolean empty_only) {
+    final int default_minimum_orf_size =
+      Options.getOptions ().getMinimumORFSize ();
+
+    final TextRequester text_requester =
+      new TextRequester ("minimum open reading frame size?",
+                         18, String.valueOf (default_minimum_orf_size));
+
+    text_requester.addTextRequesterListener (new TextRequesterListener () {
+      public void actionPerformed (final TextRequesterEvent event) {
+        if (event.getType () == TextRequesterEvent.CANCEL) {
+          return;
+        }
+
+        final String requester_text = event.getRequesterText ().trim ();
+
+        if (requester_text.length () == 0) {
+          return;
+        }
+
+        try {
+          final int minimum_orf_size =
+            Integer.valueOf (requester_text).intValue ();
+
+          markOpenReadingFrames (minimum_orf_size, empty_only);
+        } catch (NumberFormatException e) {
+          new MessageDialog (getParentFrame (),
+                             "this is not a number: " + requester_text);
+        }
+      }
+    });
+
+    text_requester.setVisible (true);
+  }
+
+  /**
+   *  Create a new Feature for each open reading frame.
+   *  @param minimum_orf_size All the returned ORFs will be at least this many
+   *    amino acids long.
+   *  @param empty_only If true only those ORFS that don't already contain a
+   *    segment will be marked.
+   **/
+  private void markOpenReadingFrames (final int minimum_orf_size,
+                                      final boolean empty_only) {
+    try {
+      final Entry new_entry =
+        entry_group.createEntry ("ORFS_" + minimum_orf_size + '+');
+
+      final int sequence_length = entry_group.getSequenceLength ();
+
+      final Strand forward_strand =
+        entry_group.getBases ().getForwardStrand ();
+
+      final MarkerRange forward_range =
+        forward_strand.makeMarkerRangeFromPositions (1, sequence_length);
+
+      markOpenReadingFrames (new_entry, forward_range, minimum_orf_size,
+                             empty_only);
+
+      final Strand backward_strand =
+        entry_group.getBases ().getReverseStrand ();
+
+      final MarkerRange backward_range =
+        backward_strand.makeMarkerRangeFromPositions (1, sequence_length);
+
+      markOpenReadingFrames (new_entry, backward_range, minimum_orf_size,
+                             empty_only);
+    } catch (OutOfRangeException e) {
+      throw new Error ("internal error - unexpected OutOfRangeException");
+    }
+  }
+
+  /**
+   *  Create a new Feature for each open reading frame.  The minimum size of
+   *  the ORFS is specified in the options file.
+   **/
+  private void markOpenReadingFramesInRange () {
+    if (!checkForSelectionRange (getParentFrame (), getSelection ())) {
+      return;
+    }
+
+    final int default_minimum_orf_size =
+      Options.getOptions ().getMinimumORFSize ();
+
+    final TextRequester text_requester =
+      new TextRequester ("minimum open reading frame size?",
+                         18, String.valueOf (default_minimum_orf_size));
+
+    text_requester.addTextRequesterListener (new TextRequesterListener () {
+      public void actionPerformed (final TextRequesterEvent event) {
+        if (event.getType () == TextRequesterEvent.CANCEL) {
+          return;
+        }
+
+        final String requester_text = event.getRequesterText ().trim ();
+
+        if (requester_text.length () == 0) {
+          return;
+        }
+
+        try {
+          final int minimum_orf_size =
+            Integer.valueOf (requester_text).intValue ();
+
+          final Entry new_entry =
+            entry_group.createEntry ("ORFS_" + minimum_orf_size + '+');
+
+          final MarkerRange selection_range =
+            getSelection ().getMarkerRange ();
+
+          markOpenReadingFrames (new_entry, selection_range, minimum_orf_size,
+                                 false);
+
+
+        } catch (NumberFormatException e) {
+          new MessageDialog (getParentFrame (),
+                             "this is not a number: " + requester_text);
+        }
+      }
+    });
+
+    text_requester.setVisible (true);
+
+  }
+
+  /**
+   *  Create a new Feature in the given Entry for each open reading frame that
+   *  overlaps the given range.  The minimum size of the ORFS is specified in
+   *  the options file.
+   *  @param entry The new features are created in this entry.
+   *  @param search_range The range of bases to search for ORFs.
+   *  @param minimum_orf_size All the returned ORFs will be at least this many
+   *    amino acids long.
+   *  @param empty_only If true only those ORFS that don't already contain a
+   *    segment will be marked.
+   **/
+  private void markOpenReadingFrames (final Entry entry,
+                                      final MarkerRange search_range,
+                                      final int minimum_orf_size,
+                                      final boolean empty_only) {
+    final MarkerRange [] forward_orf_ranges =
+      Strand.getOpenReadingFrameRanges (search_range, minimum_orf_size);
+
+    for (int i = 0 ; i < forward_orf_ranges.length ; ++i) {
+      final MarkerRange this_range = forward_orf_ranges[i];
+
+      final Feature new_feature;
+
+      try {
+        new_feature = makeFeatureFromMarkerRange (entry, this_range, Key.CDS);
+      } catch (EntryInformationException e) {
+        new MessageDialog (getParentFrame (), "cannot continue: " +
+                           "the default entry does not support CDS features");
+        return;
+      } catch (ReadOnlyException e) {
+        new MessageDialog (getParentFrame (), "cannot continue: " +
+                           "the default entry is read only");
+        return;
+      }
+
+      if (empty_only && overlapsAnActiveSegment (new_feature)) {
+        try {
+          new_feature.removeFromEntry ();
+        } catch (ReadOnlyException exception) {
+          throw new Error ("internal error - unexpected exception: " +
+                           exception);
+        }
+      }
+    }
+  }
+
+  /**
+   *  Return true if and only if the given feature overlaps (and is in the
+   *  same frame as) a segment in an active entry.
+   **/
+  private boolean overlapsAnActiveSegment (final Feature test_feature) {
+    final Range test_feature_range = test_feature.getMaxRawRange ();
+
+    FeatureVector overlapping_features;
+
+    try {
+      overlapping_features =
+        entry_group.getFeaturesInRange (test_feature_range);
+    } catch (OutOfRangeException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+
+    for (int feature_index = 0 ;
+         feature_index < overlapping_features.size () ;
+         ++feature_index ) {
+
+      final Feature current_feature =
+        overlapping_features.elementAt (feature_index);
+
+      if (current_feature != test_feature && current_feature.isCDS ()) {
+        final FeatureSegmentVector segments = current_feature.getSegments ();
+
+        for (int segment_index = 0;
+             segment_index < segments.size () ;
+             ++segment_index) {
+          final FeatureSegment this_segment =
+            segments.elementAt (segment_index);
+
+          if (test_feature_range.overlaps (this_segment.getRawRange ())) {
+            final int test_feature_frame =
+              test_feature.getSegments ().elementAt (0).getFrameID ();
+            final int this_segment_frame = this_segment.getFrameID ();
+
+            if (test_feature_frame == this_segment_frame) {
+              return true;
+            }
+          }
+        }
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   *  Make a new Feature from the given MarkerRange in the given Entry.  The
+   *  new feature will be given the key 'CDS' and it's location will match the
+   *  MarkerRange.
+   *  @param entry The new feature is created in this entry.
+   *  @param range The location of the new feature.
+   *  @param key The key give the new feature
+   *  @exception EntryInformationException Thrown if this Entry does not
+   *    support features with the given key.  Also thrown if any of these
+   *    qualifiers aren't supported: note, label or gene.
+   **/
+  private Feature makeFeatureFromMarkerRange (final Entry entry,
+                                              final MarkerRange range,
+                                              final Key key)
+      throws EntryInformationException, ReadOnlyException {
+    try {
+      final Location new_location = range.createLocation ();
+
+      final QualifierVector qualifiers = new QualifierVector ();
+
+      qualifiers.setQualifier (new Qualifier ("note", "none"));
+
+      final Feature new_feature =
+        entry.createFeature (key, new_location, qualifiers);
+
+      final CodonUsageAlgorithm codon_usage_algorithm =
+        base_plot_group.getCodonUsageAlgorithm ();
+
+      if (codon_usage_algorithm != null) {
+        int score =
+          (int) (codon_usage_algorithm.getFeatureScore (new_feature) * 50);
+
+        if (score < 0) {
+          score = 0;
+        }
+
+        if (score > 100) {
+          score = 100;
+        }
+
+        final String score_string = String.valueOf (score);
+        new_feature.addQualifierValues (new Qualifier ("score",
+                                                       score_string));
+
+        final int var_colour = 255 - score * 5 / 2;
+        final String colour_string = var_colour + " " + var_colour + " 255";
+        new_feature.addQualifierValues (new Qualifier ("colour",
+                                                       colour_string));
+      }
+
+      return new_feature;
+
+    } catch (OutOfRangeException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+  }
+
+  /**
+   *  This method will ask the user for a BasePattern (using a TextRequester
+   *  component) then search the sequence for the given pattern and make a new
+   *  feature from each match.  The new features will created in an Entry
+   *  called "matches: <pattern>".
+   **/
+  private void makeFeaturesFromPattern () {
+    final TextRequester text_requester =
+      new TextRequester ("create features from this pattern:", 18, "");
+
+    text_requester.addTextRequesterListener (new TextRequesterListener () {
+      public void actionPerformed (final TextRequesterEvent event) {
+        final String pattern_string = event.getRequesterText ().trim ();
+
+        try {
+          if (pattern_string.length () == 0) {
+            new MessageDialog (getParentFrame (), "the pattern is too short");
+            return;
+          }
+
+          final BasePattern pattern = new BasePattern (pattern_string);
+
+          makeFeaturesFromPattern (pattern);
+        } catch (BasePatternFormatException e) {
+          new MessageDialog (getParentFrame (),
+                             "Illegal base pattern: " +
+                             pattern_string);
+        }
+      }
+    });
+
+    text_requester.show ();
+  }
+
+  /**
+   *  Search the sequence for the given pattern and make a new feature from
+   *  each match.  The new features will created in an Entry called "matches:
+   *  <pattern>".
+   **/
+  private void makeFeaturesFromPattern (final BasePattern pattern) {
+    final MarkerRangeVector matches =
+      pattern.findMatches (entry_group.getBases (),
+                           null,        // search from start
+                           entry_group.getSequenceLength ());
+
+    if (matches.size () == 0) {
+      new MessageDialog (getParentFrame (),
+                         "no matches found for: " + pattern);
+      return;
+    }
+
+    final int TOO_MANY_MATCHES = 100;
+
+    if (matches.size () > TOO_MANY_MATCHES) {
+      final YesNoDialog dialog =
+        new YesNoDialog (getParentFrame (),
+                         matches.size () + " matches, continue?");
+
+      if (dialog.getResult ()) {
+        // yes - continue
+      } else {
+        // no
+        return;
+      }
+    }
+
+    final Entry new_entry = entry_group.createEntry ("matches: " + pattern);
+
+    final Key key = new_entry.getEntryInformation ().getDefaultKey ();
+
+    for (int i = 0 ; i < matches.size () ; ++i) {
+      try {
+        final Feature new_feature =
+          makeFeatureFromMarkerRange (new_entry, matches.elementAt (i), key);
+        new_feature.setQualifier (new Qualifier ("note", pattern.toString ()));
+      } catch (EntryInformationException e) {
+        new MessageDialog (getParentFrame (), "cannot continue: " +
+                           e.getMessage ());
+        return;
+      } catch (ReadOnlyException e) {
+        new MessageDialog (getParentFrame (), "cannot continue: " +
+                           "the default entry is read only");
+        return;
+      }
+    }
+  }
+
+  /**
+   *  Create a misc_feature for each block of ambiguous bases.  The new
+   *  features will created in an Entry called "ambiguous bases".
+   **/
+  private void markAmbiguities () {
+    Entry new_entry = null;
+
+    final Bases bases = entry_group.getBases ();
+
+    for (int i = 1 ; i <= bases.getLength () ; ++i) {
+      try {
+        if (! Bases.isLegalBase (bases.getBaseAt (i))) {
+          final int start_index = i;
+
+          while (i < bases.getLength () &&
+                 ! Bases.isLegalBase (bases.getBaseAt (i + 1))) {
+            ++i;
+          }
+
+          final int end_index = i;
+
+          if (new_entry == null) {
+            new_entry = entry_group.createEntry ("ambiguous bases");
+          }
+
+          final Range range = new Range (start_index, end_index);
+
+          final String unsure_bases =
+            bases.getSubSequence (range, Bases.FORWARD);
+
+          final Location location = new Location (range);
+
+          final QualifierVector qualifiers = new QualifierVector ();
+
+          qualifiers.setQualifier (new Qualifier ("note", unsure_bases));
+
+          final Feature feature =
+            new_entry.createFeature (new Key ("unsure"), location, qualifiers);
+        }
+      } catch (ReadOnlyException e) {
+        throw new Error ("internal error - unexpected exception: " + e);
+      } catch (EntryInformationException e) {
+        throw new Error ("internal error - unexpected exception: " + e);
+      } catch (OutOfRangeException e) {
+        throw new Error ("internal error - unexpected exception: " + e);
+      }
+    }
+
+    if (new_entry == null) {
+      new MessageDialog (getParentFrame (), "No ambiguities found");
+    } else {
+      if (new_entry.getFeatureCount () == 1) {
+        new MessageDialog (getParentFrame (), "Created one feature");
+
+      } else {
+        new MessageDialog (getParentFrame (), "Created " +
+                           new_entry.getFeatureCount () + " features");
+
+      }
+    }
+  }
+
+  /**
+   *  Return the GotoEventSource object that was passed to the constructor.
+   **/
+  private GotoEventSource getGotoEventSource () {
+    return goto_event_source;
+  }
+
+  /**
+   *  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;
+
+  private JMenuItem new_feature_item = null;
+  private JMenuItem new_entry_item = null;
+  private JMenuItem create_feature_from_range_item = null;
+  private JMenuItem create_intron_features_item = null;
+  private JMenuItem create_exon_features_item = null;
+  private JMenuItem create_gene_features_item = null;
+  private JMenuItem mark_orfs_with_size_item = null;
+  private JMenuItem mark_empty_orfs_with_size_item = null;
+  private JMenuItem mark_orfs_range_item = null;
+  private JMenuItem mark_pattern_item = null;
+  private JMenuItem mark_ambiguities_item = null;
+
+  private BasePlotGroup base_plot_group;
+}
diff --git a/uk/ac/sanger/artemis/components/AlignMatchViewer.java b/uk/ac/sanger/artemis/components/AlignMatchViewer.java
new file mode 100644
index 000000000..12297cff6
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/AlignMatchViewer.java
@@ -0,0 +1,366 @@
+/* AlignMatchViewer.java
+ *
+ * created: Tue Feb 13 2001
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/AlignMatchViewer.java,v 1.1 2004-06-09 09:45:58 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+import java.util.Comparator;
+import java.util.Collections;
+
+/**
+ *  A component for viewing AlignMatchVectors selected in an AlignmentViewer.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: AlignMatchViewer.java,v 1.1 2004-06-09 09:45:58 tjc Exp $
+ **/
+
+public class AlignMatchViewer extends JFrame {
+  /**
+   *  Create a new AlignMatchViewer which shows the given matches.
+   *  @param alignment_viewer The AlignmentViewer to call alignAt () and
+   *    setSelection () on when the user clicks on a match.
+   *  @param matches The Vector of AlignMatch objects to show.
+   **/
+  public AlignMatchViewer (final AlignmentViewer alignment_viewer,
+                           final AlignMatchVector matches) {
+    this.alignment_viewer = alignment_viewer;
+    this.matches = matches;
+
+    final JMenuBar menu_bar = new JMenuBar ();
+
+    final JMenu file_menu = new JMenu ("File");
+
+    final JMenuItem close = new JMenuItem ("Close");
+    close.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        setVisible (false);
+        AlignMatchViewer.this.dispose ();
+      }
+    });
+
+    file_menu.add (close);
+
+    menu_bar.add (file_menu);
+
+    final JMenu sort_menu = new JMenu ("Sort");
+
+    sort_menu.add (sort_by_score_menu_item);
+
+    sort_by_score_menu_item.addItemListener (new ItemListener () {
+      public void itemStateChanged(ItemEvent e) {
+        if (sort_by_score_menu_item.getState ()) {
+          sort_by_percent_id_menu_item.setState (false);
+          sort_by_query_start.setState (false);
+          sort_by_subject_start.setState (false);
+        }
+
+        setList ();
+      }
+    });
+
+    sort_menu.add (sort_by_percent_id_menu_item);
+
+    sort_by_percent_id_menu_item.addItemListener (new ItemListener () {
+      public void itemStateChanged(ItemEvent e) {
+        if (sort_by_percent_id_menu_item.getState ()) {
+          sort_by_score_menu_item.setState (false);
+          sort_by_query_start.setState (false);
+          sort_by_subject_start.setState (false);
+        }
+
+        setList ();
+      }
+    });
+
+    sort_menu.add (sort_by_query_start);
+
+    sort_by_query_start.addItemListener (new ItemListener () {
+      public void itemStateChanged(ItemEvent e) {
+        if (sort_by_query_start.getState ()) {
+          sort_by_percent_id_menu_item.setState (false);
+          sort_by_score_menu_item.setState (false);
+          sort_by_subject_start.setState (false);
+        }
+
+        setList ();
+      }
+    });
+
+    sort_menu.add (sort_by_subject_start);
+
+    sort_by_subject_start.addItemListener (new ItemListener () {
+      public void itemStateChanged(ItemEvent e) {
+        if (sort_by_subject_start.getState ()) {
+          sort_by_percent_id_menu_item.setState (false);
+          sort_by_score_menu_item.setState (false);
+          sort_by_query_start.setState (false);
+        }
+
+        setList ();
+      }
+    });
+
+    menu_bar.add (sort_menu);
+
+    setJMenuBar (menu_bar);
+
+    list = new List ();
+
+    list.setBackground (Color.white);
+
+    getContentPane ().add (list, "Center");
+
+    final JPanel panel = new JPanel ();
+
+    final JButton close_button = new JButton ("Close");
+
+    panel.add (close_button);
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        AlignMatchViewer.this.dispose ();
+      }
+    });
+
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        AlignMatchViewer.this.dispose ();
+      }
+    });
+
+    getContentPane ().add (panel, "South");
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        AlignMatchViewer.this.dispose ();
+      }
+    });
+
+    pack ();
+
+    setList ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+
+    int screen_height = screen.height;
+    int screen_width = screen.width;
+
+    if (screen_height <= 600) {
+      setSize (350, screen_height * 9 / 10);
+    } else {
+      setSize (350, screen_height - 200);
+    }
+
+    setLocation (new Point (screen.width - getSize ().width - 5,
+                            (screen.height - getSize ().height) / 2));
+  }
+
+  /**
+   *  Sort the matches depending on the setting of sort_by_score_menu_item and
+   *  sort_by_percent_id_menu_item.
+   **/
+  private AlignMatchVector getSortedMatches () {
+    final AlignMatchVector matches_copy = (AlignMatchVector) matches.clone ();
+
+    final Comparator comparator;
+
+    if (sort_by_score_menu_item.getState ()) {
+      comparator =
+        new Comparator () {
+          public int compare (Object fst, Object snd) {
+            final int fst_score = ((AlignMatch) fst).getScore ();
+            final int snd_score = ((AlignMatch) snd).getScore ();
+
+            if (fst_score < snd_score) {
+              return 1;
+            } else {
+              if (fst_score > snd_score) {
+                return -1;
+              } else {
+                return 0;
+              }
+            }
+          }
+        };
+    } else {
+      if (sort_by_percent_id_menu_item.getState ()) {
+        comparator =
+          new Comparator () {
+            public int compare (Object fst, Object snd) {
+              final int fst_value = ((AlignMatch) fst).getPercentID ();
+              final int snd_value = ((AlignMatch) snd).getPercentID ();
+              
+              if (fst_value < snd_value) {
+                return 1;
+              } else {
+                if (fst_value > snd_value) {
+                  return -1;
+                } else {
+                  return 0;
+                }
+              }
+            }
+          };
+      } else {
+        if (sort_by_query_start.getState ()) {
+          comparator =
+            new Comparator () {
+              public int compare (Object fst, Object snd) {
+                final int fst_value =
+                  ((AlignMatch) fst).getQuerySequenceStart ();
+                final int snd_value =
+                  ((AlignMatch) snd).getQuerySequenceStart ();
+                
+                if (fst_value > snd_value) {
+                  return 1;
+                } else {
+                  if (fst_value < snd_value) {
+                    return -1;
+                  } else {
+                    return 0;
+                  }
+                }
+              }
+            };
+        } else {
+          if (sort_by_subject_start.getState ()) {
+            comparator =
+              new Comparator () {
+                public int compare (Object fst, Object snd) {
+                  final int fst_value =
+                    ((AlignMatch) fst).getSubjectSequenceStart ();
+                  final int snd_value =
+                    ((AlignMatch) snd).getSubjectSequenceStart ();
+                  
+                  if (fst_value > snd_value) {
+                    return 1;
+                  } else {
+                    if (fst_value < snd_value) {
+                      return -1;
+                    } else {
+                      return 0;
+                    }
+                  }
+                }
+              };
+          } else {
+            return matches;
+          }
+        }
+      }
+    }
+
+    matches_copy.sort (comparator);
+
+    return matches_copy;
+  }
+
+  /**
+   *  Clear the List and then fill it with the matches in the order
+   **/
+  private void setList () {
+    final AlignMatchVector sorted_matches = getSortedMatches ();
+
+    list.setEnabled (false);
+    list.setVisible (false);
+    list.removeAll ();
+
+    for (int i = 0 ; i < sorted_matches.size () ; ++i) {
+      final AlignMatch this_align_match = sorted_matches.elementAt (i);
+
+      String match_string =
+        this_align_match.getQuerySequenceStart () + ".." +
+        this_align_match.getQuerySequenceEnd () + " -> " +
+        this_align_match.getSubjectSequenceStart () + ".." +
+        this_align_match.getSubjectSequenceEnd () + " " +
+        (this_align_match.isRevMatch () ? "rev " : "") +
+        this_align_match.getPercentID () + "% id score " +
+        this_align_match.getScore ();
+
+      list.add (match_string);
+    }
+
+    list.addItemListener (new ItemListener () {
+      public void itemStateChanged (final ItemEvent _) {
+        final int item_number = list.getSelectedIndex ();
+
+        final AlignMatch selected_match = matches.elementAt (item_number);
+
+        alignment_viewer.setSelection (selected_match);
+        alignment_viewer.alignAt (selected_match);
+      }
+    });
+
+    list.setEnabled (true);
+    list.setVisible (true);
+  }
+
+  /**
+   *  The AlignmentViewer to call alignAt () and setSelection () on when the
+   *  user clicks on a match.
+   **/
+  private AlignmentViewer alignment_viewer = null;
+
+  /**
+   *  The Vector of AlignMatch objects that was passed to the constructor.
+   **/
+  private AlignMatchVector matches = null;
+
+  /**
+   *  The list that contains the matches.
+   **/
+  private List list;
+
+  /**
+   *  If selected the list items will be sorted by score.
+   **/
+  private JCheckBoxMenuItem sort_by_score_menu_item =
+    new JCheckBoxMenuItem ("Sort by Score");
+
+  /**
+   *  If selected the list items will be sorted by percent identity.
+   **/
+  private JCheckBoxMenuItem sort_by_percent_id_menu_item =
+    new JCheckBoxMenuItem ("Sort by Percent Identity");
+
+  /**
+   *  If selected the list items will be sorted by the start position of the
+   *  query.
+   **/
+  private JCheckBoxMenuItem sort_by_query_start =
+    new JCheckBoxMenuItem ("Sort by Hit Query Start");
+
+  /**
+   *  If selected the list items will be sorted by the start position of the
+   *  subject.
+   **/
+  private JCheckBoxMenuItem sort_by_subject_start =
+    new JCheckBoxMenuItem ("Sort by Hit Subject start");
+}
diff --git a/uk/ac/sanger/artemis/components/AlignmentEvent.java b/uk/ac/sanger/artemis/components/AlignmentEvent.java
new file mode 100644
index 000000000..0cbf0e2f0
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/AlignmentEvent.java
@@ -0,0 +1,57 @@
+/* AlignmentEvent.java
+ *
+ * created: Mon Sep 10 2001
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/AlignmentEvent.java,v 1.1 2004-06-09 09:45:59 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+/**
+ *  This event is generated when an AlignmentViewer centres on a particular
+ *  AlignMatch. 
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: AlignmentEvent.java,v 1.1 2004-06-09 09:45:59 tjc Exp $
+ **/
+
+public class AlignmentEvent {
+  /**
+   *  Create a new AlignmentEvent and store the given AlignMatch.
+   **/
+  public AlignmentEvent (AlignMatch match) {
+    this.match = match;
+  }
+
+  /**
+   *  Return the AlignMatch that was passed to the constructor.
+   **/
+  public AlignMatch getMatch () {
+    return match;
+  }
+
+  /**
+   *  The AlignMatch that was passed to the constructor.
+   **/
+  final private AlignMatch match;
+}
diff --git a/uk/ac/sanger/artemis/components/AlignmentListener.java b/uk/ac/sanger/artemis/components/AlignmentListener.java
new file mode 100644
index 000000000..30618170f
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/AlignmentListener.java
@@ -0,0 +1,43 @@
+/* AlignmentListener.java
+ *
+ * created: Mon Sep 10 2001
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/AlignmentListener.java,v 1.1 2004-06-09 09:46:00 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  This interface is implemented by those objects that wish to know when an
+ *  AlignmentViewer centres on a particular AlignMatch
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: AlignmentListener.java,v 1.1 2004-06-09 09:46:00 tjc Exp $
+ **/
+
+public interface AlignmentListener {
+  /**
+   *  Called when an AlignmentViewer centres on a particular AlignMatch.  The
+   *  Comparator component implements this so that it can scroll the
+   *  FeatureDisplay components appropriately.
+   **/
+  public void alignMatchChosen (AlignmentEvent e);
+}
diff --git a/uk/ac/sanger/artemis/components/AlignmentViewer.java b/uk/ac/sanger/artemis/components/AlignmentViewer.java
new file mode 100644
index 000000000..b53ca88e1
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/AlignmentViewer.java
@@ -0,0 +1,1604 @@
+/* AlignmentViewer.java
+ *
+ * created: Mon Jul 12 1999
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1999,2000,2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/AlignmentViewer.java,v 1.1 2004-06-09 09:46:01 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.util.StringVector;
+import uk.ac.sanger.artemis.io.Range;
+import uk.ac.sanger.artemis.io.RangeVector;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This component shows an alignment of two sequences using the data from a
+ *  ComparisonData object.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: AlignmentViewer.java,v 1.1 2004-06-09 09:46:01 tjc Exp $
+ **/
+
+public class AlignmentViewer extends CanvasPanel
+    implements SequenceChangeListener {
+  /**
+   *  Create a new AlignmentViewer for the given entries.
+   *  @param subject_feature_display The FeatureDisplay that is above this
+   *    component.
+   *  @param query_feature_display The FeatureDisplay that is below this
+   *    component.
+   *  @param comparison_data Provides the AlignMatch objects that will be
+   *    displayed.
+   **/
+  public AlignmentViewer (final FeatureDisplay subject_feature_display,
+                          final FeatureDisplay query_feature_display,
+                          final ComparisonData comparison_data) {
+    this.subject_feature_display = subject_feature_display;
+    this.query_feature_display   = query_feature_display;
+    this.comparison_data         = comparison_data;
+    this.all_matches             = getComparisonData ().getMatches ();
+
+    subject_entry_group          = getSubjectDisplay ().getEntryGroup ();
+    query_entry_group            = getQueryDisplay ().getEntryGroup ();
+
+    final Bases subject_bases = getSubjectForwardStrand ().getBases ();
+    final Bases query_bases   = getQueryForwardStrand ().getBases ();
+
+    final Selection subject_selection = getSubjectDisplay ().getSelection ();
+    final Selection query_selection = getQueryDisplay ().getSelection ();
+
+    final SelectionChangeListener subject_listener =
+      new SelectionChangeListener () {
+        public void selectionChanged (SelectionChangeEvent event) {
+          final RangeVector ranges = subject_selection.getSelectionRanges ();
+          selectFromSubjectRanges (ranges);
+        }
+      };
+
+    final SelectionChangeListener query_listener =
+      new SelectionChangeListener () {
+        public void selectionChanged (SelectionChangeEvent event) {
+          final RangeVector ranges = query_selection.getSelectionRanges ();
+          selectFromQueryRanges (ranges);
+        }
+      };
+
+    makeColours ();
+
+    subject_selection.addSelectionChangeListener (subject_listener);
+    query_selection.addSelectionChangeListener (query_listener);
+
+    subject_bases.addSequenceChangeListener (this, 0);
+    query_bases.addSequenceChangeListener (this, 0);
+
+    orig_subject_forward_strand = getSubjectForwardStrand ();
+    orig_query_forward_strand = getQueryForwardStrand ();
+
+    getCanvas ().addMouseListener (new MouseAdapter () {
+      public void mousePressed (final MouseEvent event) {
+        // on windows we have to check isPopupTrigger in mouseReleased(),
+        // but do it in mousePressed() on UNIX
+        if (isMenuTrigger (event)) {
+          popupMenu (event);
+        } else {
+          handleCanvasMousePress (event);
+        }
+      }
+    });
+
+    getCanvas ().addMouseMotionListener (new MouseMotionAdapter () {
+      public void mouseDragged (final MouseEvent event) {
+        if (!modifiersForLockToggle (event)) {
+          if (!event.isShiftDown ()) {
+            selected_matches = null;
+            toggleSelection (event.getPoint ());
+          }
+          repaintCanvas ();
+        }
+      }
+    });
+
+    scroll_bar = new JScrollBar (Scrollbar.VERTICAL);
+    scroll_bar.setValues (1, 1, 1, 1000);
+    scroll_bar.setBlockIncrement (10);
+
+    scroll_bar.addAdjustmentListener (new AdjustmentListener () {
+      public void adjustmentValueChanged(AdjustmentEvent e) {
+        repaintCanvas ();
+      }
+    });
+
+    maximum_score = getComparisonData ().getMaximumScore ();
+
+    add (scroll_bar, "East");
+  }
+
+  /**
+   *  Returns true if and only if the given MouseEvent should toggle the lock
+   *  displays toggle.
+   **/
+  private boolean modifiersForLockToggle (final MouseEvent event) {
+    return (event.getModifiers () & InputEvent.BUTTON2_MASK) != 0 ||
+      event.isAltDown ();
+  }
+
+  /**
+   *  Select those matches that overlap the given range on the subject
+   *  sequence.
+   **/
+  public void selectFromSubjectRanges (final RangeVector select_ranges) {
+    if (disable_selection_from_ranges) {
+      return;
+    }
+
+    selected_matches = null;
+
+    for (int range_index = 0 ;
+         range_index < select_ranges.size () ;
+         ++range_index) {
+
+      final Range select_range = select_ranges.elementAt (range_index);
+
+      for (int match_index = 0 ;
+           match_index < all_matches.length ;
+           ++match_index) {
+        final AlignMatch this_match = all_matches [match_index];
+
+        if (!isVisible (this_match)) {
+          // not visible
+          continue;
+        }
+
+        final Strand current_subject_forward_strand =
+          getSubjectForwardStrand ();
+
+        final int subject_length =
+          current_subject_forward_strand.getSequenceLength ();
+
+        int subject_sequence_start =
+          getRealSubjectSequenceStart (this_match,
+                                       subject_length,
+                                       (getOrigSubjectForwardStrand () !=
+                                        current_subject_forward_strand));
+        int subject_sequence_end =
+          getRealSubjectSequenceEnd (this_match,
+                                     subject_length,
+                                     (getOrigSubjectForwardStrand () !=
+                                      current_subject_forward_strand));
+
+        if (subject_sequence_end < subject_sequence_start) {
+          final int tmp = subject_sequence_start;
+          subject_sequence_start = subject_sequence_end;
+          subject_sequence_end = tmp;
+        }
+
+        if (select_range.getStart () < subject_sequence_start &&
+            select_range.getEnd () < subject_sequence_start) {
+          continue;
+        }
+
+        if (select_range.getStart () > subject_sequence_end &&
+            select_range.getEnd () > subject_sequence_end) {
+          continue;
+        }
+
+        if (selected_matches == null) {
+          selected_matches = new AlignMatchVector ();
+        }
+
+        if (!selected_matches.contains (this_match)) {
+          selected_matches.add (this_match);
+        }
+      }
+    }
+
+    selectionChanged ();
+  }
+
+  /**
+   *  Select those matches that overlap the given range on the query sequence.
+   **/
+  public void selectFromQueryRanges (final RangeVector select_ranges) {
+    if (disable_selection_from_ranges) {
+      return;
+    }
+
+    selected_matches = null;
+
+    for (int range_index = 0 ;
+         range_index < select_ranges.size () ;
+         ++range_index) {
+
+      final Range select_range = select_ranges.elementAt (range_index);
+
+      for (int match_index = 0 ;
+           match_index < all_matches.length ;
+           ++match_index) {
+        final AlignMatch this_match = all_matches [match_index];
+
+        if (!isVisible (this_match)) {
+          // not visible
+          continue;
+        }
+
+        final Strand current_query_forward_strand = getQueryForwardStrand ();
+
+        final int query_length =
+          current_query_forward_strand.getSequenceLength ();
+
+        int query_sequence_start =
+          getRealQuerySequenceStart (this_match,
+                                     query_length,
+                                     (getOrigQueryForwardStrand () !=
+                                      current_query_forward_strand));
+        int query_sequence_end =
+          getRealQuerySequenceEnd (this_match,
+                                   query_length,
+                                   (getOrigQueryForwardStrand () !=
+                                    current_query_forward_strand));
+
+        if (query_sequence_end < query_sequence_start) {
+          final int tmp = query_sequence_start;
+          query_sequence_start = query_sequence_end;
+          query_sequence_end = tmp;
+        }
+
+        if (select_range.getStart () < query_sequence_start &&
+            select_range.getEnd () < query_sequence_start) {
+          continue;
+        }
+
+        if (select_range.getStart () > query_sequence_end &&
+            select_range.getEnd () > query_sequence_end) {
+          continue;
+        }
+
+        if (selected_matches == null) {
+          selected_matches = new AlignMatchVector ();
+        }
+
+        if (!selected_matches.contains (this_match)) {
+          selected_matches.add (this_match);
+        }
+      }
+    }
+
+    selectionChanged ();
+  }
+
+  /**
+   *  Select the given match and move it to the top of the display.
+   **/
+  public void setSelection (final AlignMatch match) {
+    selected_matches = new AlignMatchVector ();
+
+    selected_matches.add (match);
+
+    selectionChanged ();
+  }
+
+  /**
+   *  This method tells this AlignmentViewer component where the subject
+   *  sequence is now.
+   **/
+  public void setSubjectSequencePosition (final DisplayAdjustmentEvent event) {
+    last_subject_event = event;
+    repaintCanvas ();
+  }
+
+  /**
+   *  This method tells this AlignmentViewer component where the query
+   *  sequence is now.
+   **/
+  public void setQuerySequencePosition (final DisplayAdjustmentEvent event) {
+    last_query_event = event;
+    repaintCanvas ();
+  }
+
+  /**
+   *  Implementation of the SequenceChangeListener interface.  The display is
+   *  redrawn if there is an event.
+   **/
+  public void sequenceChanged (final SequenceChangeEvent event) {
+    repaintCanvas ();
+  }
+
+  /**
+   *  Return true if and only if the given MouseEvent (a mouse press) should
+   *  pop up a JPopupMenu.
+   **/
+  private boolean isMenuTrigger (final MouseEvent event) {
+    if (event.isPopupTrigger () ||
+        (event.getModifiers () & InputEvent.BUTTON3_MASK) != 0) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   *  Popup a menu.
+   **/
+  private void popupMenu (final MouseEvent event) {
+    final JPopupMenu popup = new JPopupMenu ();
+
+    final JMenuItem alignmatch_list_item =
+      new JMenuItem ("View Selected Matches");
+
+    popup.add (alignmatch_list_item);
+
+    alignmatch_list_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        if (selected_matches == null) {
+          new MessageFrame ("No matches selected").setVisible (true);
+        } else {
+          final AlignMatchVector matches =
+            (AlignMatchVector) selected_matches.clone ();
+
+          final AlignMatchViewer viewer =
+            new AlignMatchViewer (AlignmentViewer.this, matches);
+
+          viewer.setVisible (true);
+        }
+      }
+    });
+
+    final JMenuItem flip_subject_item =
+      new JMenuItem ("Flip Subject Sequence");
+
+    popup.add (flip_subject_item);
+
+    flip_subject_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        if (getSubjectDisplay ().isRevCompDisplay ()) {
+          getSubjectDisplay ().setRevCompDisplay (false);
+        } else {
+          getSubjectDisplay ().setRevCompDisplay (true);
+        }
+      }
+    });
+
+    final JMenuItem flip_query_item =
+      new JMenuItem ("Flip Query Sequence");
+
+    popup.add (flip_query_item);
+
+    flip_query_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        if (getQueryDisplay ().isRevCompDisplay ()) {
+          getQueryDisplay ().setRevCompDisplay (false);
+        } else {
+          getQueryDisplay ().setRevCompDisplay (true);
+        }
+      }
+    });
+
+    final JMenuItem lock_item = new JMenuItem ("Lock Sequences");
+
+    popup.add (lock_item);
+
+    lock_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        lockDisplays ();
+      }
+    });
+
+    final JMenuItem unlock_item = new JMenuItem ("Unlock Sequences");
+
+    popup.add (unlock_item);
+
+    unlock_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        unlockDisplays ();
+      }
+    });
+
+    final JMenuItem cutoffs_item = new JMenuItem ("Set Score Cutoffs ...");
+
+    popup.add (cutoffs_item);
+
+    cutoffs_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        final ScoreChangeListener minimum_listener =
+          new ScoreChangeListener () {
+            public void scoreChanged (final ScoreChangeEvent event) {
+              minimum_score = event.getValue ();
+              repaintCanvas ();
+            }
+          };
+
+        final ScoreChangeListener maximum_listener =
+          new ScoreChangeListener () {
+            public void scoreChanged (final ScoreChangeEvent event) {
+              maximum_score = event.getValue ();
+              repaintCanvas ();
+            }
+          };
+
+        final ScoreChanger score_changer =
+          new ScoreChanger ("Score Cutoffs",
+                            minimum_listener, maximum_listener,
+                            getComparisonData ().getMinimumScore (),
+                            getComparisonData ().getMaximumScore ());
+
+        score_changer.setVisible (true);
+      }
+    });
+
+    final JMenuItem percent_id_cutoffs_item =
+      new JMenuItem ("Set Percent ID Cutoffs ...");
+
+    popup.add (percent_id_cutoffs_item);
+
+    percent_id_cutoffs_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        final ScoreChangeListener minimum_listener =
+          new ScoreChangeListener () {
+            public void scoreChanged (final ScoreChangeEvent event) {
+              minimum_percent_id = event.getValue ();
+              repaintCanvas ();
+            }
+          };
+
+        final ScoreChangeListener maximum_listener =
+          new ScoreChangeListener () {
+            public void scoreChanged (final ScoreChangeEvent event) {
+              maximum_percent_id = event.getValue ();
+              repaintCanvas ();
+            }
+          };
+
+        final ScoreChanger score_changer =
+          new ScoreChanger ("Percent Identity Cutoffs",
+                            minimum_listener, maximum_listener,
+                            0, 100);
+
+        score_changer.setVisible (true);
+      }
+    });
+
+    popup.addSeparator ();
+
+    final JCheckBoxMenuItem offer_to_flip_item =
+      new JCheckBoxMenuItem ("Offer To RevComp", offer_to_flip_flag);
+
+    offer_to_flip_item.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent e) {
+        offer_to_flip_flag = !offer_to_flip_flag;
+      }
+    });
+
+    popup.add (offer_to_flip_item);
+
+    final JCheckBoxMenuItem ignore_self_match_item =
+      new JCheckBoxMenuItem ("Ignore Self Matches");
+
+    ignore_self_match_item.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent event) {
+        ignore_self_match_flag = ignore_self_match_item.getState ();
+        repaintCanvas ();
+      }
+    });
+
+    ignore_self_match_item.setState (ignore_self_match_flag);
+
+    popup.add (ignore_self_match_item);
+
+    getCanvas ().add (popup);
+    popup.show (getCanvas (), event.getX (), event.getY ());
+  }
+
+
+  /**
+   *  Handle a mouse press event on the drawing canvas - select on click,
+   *  select and broadcast it on double click.
+   **/
+  private void handleCanvasMousePress (final MouseEvent event) {
+    if (event.getID() != MouseEvent.MOUSE_PRESSED) {
+      return;
+    }
+
+    if (event.getClickCount () == 2) {
+      handleCanvasDoubleClick (event);
+    } else {
+      handleCanvasSingleClick (event);
+    }
+
+    repaintCanvas ();
+  }
+
+  /**
+   *  Handle a double click on the canvas.
+   **/
+  private void handleCanvasDoubleClick (final MouseEvent event) {
+    if (selected_matches != null) {
+      // there should be only one match in the array because
+      // handleCanvasSingleClick () has been called
+      alignAt (selected_matches.elementAt (0));
+    }
+  }
+
+  /**
+   *  Send an AlignmentEvent to all the AlignmentListeners.
+   *  @param align_match The AlignMatch that we have just centred on.
+   **/
+  public void alignAt (final AlignMatch align_match) {
+    final java.util.Vector targets;
+    // copied from a book - synchronizing the whole method might cause a
+    // deadlock
+    synchronized (this) {
+      targets = (java.util.Vector) alignment_event_listeners.clone ();
+    }
+
+    for (int i = 0 ; i < targets.size () ; ++i) {
+      final AlignmentListener listener =
+        (AlignmentListener) targets.elementAt (i);
+
+      listener.alignMatchChosen (new AlignmentEvent (align_match));
+    }
+  }
+
+  /**
+   *  Handle a single click on the canvas.
+   **/
+  private void handleCanvasSingleClick (final MouseEvent event) {
+    if (modifiersForLockToggle (event)) {
+      toggleDisplayLock ();
+    } else {
+      if (!event.isShiftDown ()) {
+        selected_matches = null;
+      }
+      toggleSelection (event.getPoint ());
+    }
+  }
+
+  /**
+   *  Add or remove the match at the given mouse position to the selection.
+   **/
+  private void toggleSelection (final Point point) {
+    final AlignMatch clicked_align_match =
+      getAlignMatchFromPosition (point);
+
+    if (clicked_align_match != null) {
+      if (selected_matches == null) {
+        selected_matches = new AlignMatchVector ();
+        selected_matches.add (clicked_align_match);
+      } else {
+        if (selected_matches.contains (clicked_align_match)) {
+          selected_matches.remove (clicked_align_match);
+          if (selected_matches.size () == 0) {
+            selected_matches = null;
+          }
+        } else {
+          selected_matches.add (clicked_align_match);
+        }
+      }
+
+    }
+
+    selectionChanged ();
+  }
+
+  /**
+   *  Return the AlignMatch at the given Point on screen or null if there is
+   *  no match at that point.  The alignment_data_array is searched in reverse
+   *  order.
+   **/
+  private AlignMatch getAlignMatchFromPosition (final Point click_point) {
+    final int canvas_height = getCanvas ().getSize ().height;
+    final int canvas_width = getCanvas ().getSize ().width;
+
+    for (int i = all_matches.length - 1 ; i >= 0  ; --i) {
+      final AlignMatch this_match = all_matches [i];
+
+      final int [] match_x_positions =
+        getMatchCoords (canvas_width, this_match);
+
+      if (match_x_positions == null) {
+        continue;
+      }
+
+      if (!isVisible (this_match)) {
+        continue;
+      }
+
+      final int subject_start_x = match_x_positions[0];
+      final int subject_end_x = match_x_positions[1];
+      final int query_start_x = match_x_positions[2];
+      final int query_end_x = match_x_positions[3];
+
+      // this is the x coordinate of the point where the line y = click_point
+      // hits the left edge of the match box
+      final double match_left_x =
+        subject_start_x +
+        (1.0 * (query_start_x - subject_start_x)) *
+        (1.0 * click_point.y / canvas_height);
+
+      // this is the x coordinate of the point where the line y = click_point
+      // hits the right edge of the match box
+      final double match_right_x =
+        subject_end_x +
+        (1.0 * (query_end_x - subject_end_x)) *
+        (1.0 * click_point.y / canvas_height);
+
+      if (click_point.x >= match_left_x - 1 &&
+          click_point.x <= match_right_x + 1 ||
+          click_point.x <= match_left_x + 1 &&
+          click_point.x >= match_right_x - 1) {
+        return this_match;
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   *  This method is called by setSelection () and others whenever the list of
+   *  selected/highlighted hits changes.  Calls alignmentSelectionChanged ()
+   *  on all interested AlignmentSelectionChangeListener objects, moves the
+   *  selected matches to the top of the display and then calls
+   *  repaintCanvas ().
+   **/
+  private void selectionChanged () {
+    for (int i = 0 ; i < selection_change_listeners.size () ; ++i) {
+      final AlignMatchVector matches =
+        (AlignMatchVector) selected_matches.clone ();
+
+      final AlignmentSelectionChangeEvent ev =
+        new AlignmentSelectionChangeEvent (this, matches);
+
+      final AlignmentSelectionChangeListener listener =
+        (AlignmentSelectionChangeListener) selection_change_listeners.elementAt (i);
+
+      listener.alignmentSelectionChanged (ev);
+    }
+
+    if (selected_matches != null && selected_matches.size () > 0) {
+      // a count of the number of selected matches seen so far
+      int seen_and_selected_count = 0;
+
+      for (int i = 0 ; i < all_matches.length ; ++i) {
+        final AlignMatch this_match = all_matches[i];
+
+        if (selected_matches.contains (this_match)) {
+          ++seen_and_selected_count;
+        } else {
+          if (seen_and_selected_count > 0) {
+            // move the matches down to fill the gap
+            all_matches[i-seen_and_selected_count] = all_matches[i];
+          }
+        }
+      }
+
+      // put the selected_matches at the end of all_matches
+      for (int i = 0 ; i < selected_matches.size () ; ++i) {
+        all_matches[all_matches.length - selected_matches.size () + i] =
+          selected_matches.elementAt (i);
+      }
+    }
+
+    repaintCanvas ();
+  }
+
+  /**
+   *  Add the AlignmentSelectionChangeListener to the list of objects
+   *  listening for AlignmentSelectionChangeEvents.
+   **/
+  public void
+    addAlignmentSelectionChangeListener (final AlignmentSelectionChangeListener l) {
+    selection_change_listeners.addElement (l);
+  }
+
+  /**
+   *  Remove the AlignmentSelectionChangeListener from the list of objects
+   *  listening for AlignmentSelectionChangeEvents.
+   **/
+  public void
+    removeAlignmentSelectionChangeListener (final AlignmentSelectionChangeListener l) {
+    selection_change_listeners.removeElement (l);
+  }
+
+  /**
+   *  Adds the specified AlignmentEvent listener to receive events from this
+   *  object.
+   *  @param l the listener.
+   **/
+  public void addAlignmentListener (AlignmentListener l) {
+    alignment_event_listeners.addElement (l);
+  }
+
+  /**
+   *  Removes the specified AlignmentEvent listener so that it no longer
+   *  receives events from this object.
+   *  @param l the listener.
+   **/
+  public void removeAlignmentListener (AlignmentListener l) {
+    alignment_event_listeners.removeElement (l);
+  }
+
+  /**
+   *  Returns true if and only if we should offer to flip the query
+   *  sequence when the user double clicks on a flipped match.
+   **/
+  public boolean offerToFlip () {
+    return offer_to_flip_flag;
+  }
+
+  /**
+   *  The main paint function for the canvas.  An off screen image used for
+   *  double buffering when drawing the canvas.
+   *  @param g The Graphics object of the canvas.
+   **/
+  protected void paintCanvas (final Graphics g) {
+    fillBackground (g);
+
+    if (last_subject_event != null && last_query_event != null) {
+      drawAlignments (g);
+      drawLabels (g);
+    }
+  }
+
+  /**
+   *  Draw the background colour of the frames.
+   **/
+  private void fillBackground (final Graphics g) {
+    g.setColor (Color.white);
+
+    g.fillRect (0, 0, getCanvas ().getSize ().width,
+                getCanvas ().getSize ().height);
+  }
+
+  /**
+   *  Draw the labels into the given Graphics object.  There is a label at the
+   *  top left showing info about the current AlignMatch object,
+   *
+   *  XXX
+   *  a label at
+   *  the bottom left showing whether or not the query sequence is reverse
+   *  complemented
+   *  XXX
+   *
+   *  and a label beside the Scrollbar showing the current score
+   *  cutoff.
+   **/
+  private void drawLabels (final Graphics g) {
+    final FontMetrics fm = g.getFontMetrics ();
+    final int canvas_width = getCanvas ().getSize ().width;
+    final int canvas_height = getCanvas ().getSize ().height;
+
+    final String cutoff_label =
+      Integer.toString (scroll_bar.getValue ());
+
+    final int cutoff_label_width = fm.stringWidth (cutoff_label);
+
+    int cutoff_label_position =
+      (int)((scroll_bar.getValue () -
+             scroll_bar.getMinimum ()) / (1.0 *
+            (scroll_bar.getMaximum () -
+             scroll_bar.getMinimum ())) * canvas_height);
+
+    if (cutoff_label_position < getFontAscent ()) {
+      cutoff_label_position = getFontAscent ();
+    }
+
+
+    final int [] cutoff_x_points = {
+      canvas_width - cutoff_label_width,
+      canvas_width - 2,
+      canvas_width - 2,
+      canvas_width - cutoff_label_width,
+    };
+
+    final int [] cutoff_y_points = {
+      cutoff_label_position + 1,
+      cutoff_label_position + 1,
+      cutoff_label_position - getFontAscent (),
+      cutoff_label_position - getFontAscent (),
+    };
+
+    g.setColor (Color.white);
+    g.fillPolygon (cutoff_x_points, cutoff_y_points, 4);
+
+    g.setColor (Color.black);
+
+    g.drawString (cutoff_label, canvas_width - cutoff_label_width,
+                  cutoff_label_position);
+
+    final int font_height = getFontAscent () + getFontDescent ();
+
+    if (selected_matches != null) {
+      final String match_string_1;
+
+      if (selected_matches.size () > 1) {
+        match_string_1 = selected_matches.size () + " matches selected";
+      } else {
+        final AlignMatch selected_align_match = selected_matches.elementAt (0);
+
+        match_string_1 =
+          selected_align_match.getQuerySequenceStart () + ".." +
+          selected_align_match.getQuerySequenceEnd () + " -> " +
+          selected_align_match.getSubjectSequenceStart () + ".." +
+          selected_align_match.getSubjectSequenceEnd ();
+      }
+
+      final int match_string_1_width = fm.stringWidth (match_string_1);
+
+      final int [] match_1_x_points = {
+        0, 0, match_string_1_width, match_string_1_width
+      };
+
+      final int [] match_1_y_points = {
+        0, font_height, font_height, 0
+      };
+
+      g.setColor (Color.white);
+      g.fillPolygon (match_1_x_points, match_1_y_points, 4);
+
+      g.setColor (Color.black);
+      g.drawString (match_string_1, 0, getFontAscent ());
+
+      if (selected_matches.size () == 1) {
+        final AlignMatch selected_align_match = selected_matches.elementAt (0);
+
+        final String match_string_2 = "score: " +
+          selected_align_match.getScore () + "  percent id: " +
+          selected_align_match.getPercentID () + "%";
+        
+        final int match_string_2_width = fm.stringWidth (match_string_2);
+        
+        final int [] match_2_x_points = {
+          0, 0, match_string_2_width, match_string_2_width
+        };
+        
+        final int [] match_2_y_points = {
+          font_height, font_height * 2, font_height * 2, font_height
+        };
+        
+        g.setColor (Color.white);
+        g.fillPolygon (match_2_x_points, match_2_y_points, 4);
+        
+        g.setColor (Color.black);
+        g.drawString (match_string_2, 0, getFontAscent () + font_height);
+      }
+    }
+
+    final StringVector status_strings = new StringVector ();
+
+    if (displaysAreLocked ()) {
+      status_strings.add ("LOCKED");
+    }
+
+    if (getSubjectDisplay ().isRevCompDisplay ()) {
+      status_strings.add ("Subject: Flipped");
+    }
+
+    if (getQueryDisplay ().isRevCompDisplay ()) {
+      status_strings.add ("Query: Flipped");
+    }
+
+    if (getOrigSubjectForwardStrand () != getSubjectForwardStrand ()) {
+      status_strings.add ("Subject: Reverse Complemented");
+    }
+
+    if (getOrigQueryForwardStrand () != getQueryForwardStrand ()) {
+      status_strings.add ("Query: Reverse Complemented");
+    }
+
+    g.setColor (Color.white);
+
+    for (int i = 0 ; i < status_strings.size () ; ++i) {
+      final String status_string = status_strings.elementAt (i);
+
+      final int status_string_width = fm.stringWidth (status_string);
+
+      final int [] x_points = {
+        0, 0, status_string_width, status_string_width
+      };
+
+      final int string_offset = font_height * (status_strings.size () - i - 1);
+
+      final int [] y_points = {
+        canvas_height - string_offset,
+        canvas_height - font_height - string_offset,
+        canvas_height - font_height - string_offset,
+        canvas_height - string_offset
+      };
+
+      g.fillPolygon (x_points, y_points, 4);
+    }
+
+    g.setColor (Color.black);
+
+    for (int i = 0 ; i < status_strings.size () ; ++i) {
+      final String status_string = status_strings.elementAt (i);
+
+      final int string_offset = font_height * (status_strings.size () - i - 1);
+
+      g.drawString (status_string, 0,
+                    canvas_height - string_offset - getFontDescent ());
+    }
+  }
+
+  /**
+   *  Draw the alignments into the given Graphics object.
+   **/
+  private void drawAlignments (final Graphics g) {
+    final int canvas_height = getCanvas ().getSize ().height;
+    final int canvas_width = getCanvas ().getSize ().width;
+
+    for (int i = 0 ; i < all_matches.length ; ++i) {
+      final AlignMatch this_match = all_matches [i];
+
+      final int [] match_x_positions =
+        getMatchCoords (canvas_width, this_match);
+
+      if (match_x_positions == null) {
+        continue;
+      }
+
+      if (!isVisible (this_match)) {
+        // not visible
+        continue;
+      }
+
+      final int subject_start_x = match_x_positions[0];
+      final int subject_end_x = match_x_positions[1];
+      final int query_start_x = match_x_positions[2];
+      final int query_end_x = match_x_positions[3];
+
+      final int [] x_coords = new int [4];
+      final int [] y_coords = new int [4];
+
+      x_coords[0] = subject_start_x;
+      y_coords[0] = 0;
+      x_coords[1] = query_start_x;
+      y_coords[1] = canvas_height;
+      x_coords[2] = query_end_x;
+      y_coords[2] = canvas_height;
+      x_coords[3] = subject_end_x;
+      y_coords[3] = 0;
+
+      final boolean highlight_this_match;
+
+      if (selected_matches != null &&
+          selected_matches.contains (this_match)) {
+        highlight_this_match = true;
+      } else {
+        highlight_this_match = false;
+      }
+
+      final int OFFSCREEN = 3000;
+
+      final int percent_id = this_match.getPercentID ();
+
+      if (highlight_this_match) {
+        g.setColor (Color.yellow);
+      } else {
+        if (percent_id == -1) {
+          if (this_match.isRevMatch ()) {
+            g.setColor (Color.blue);
+          } else {
+            g.setColor (Color.red);
+          }
+        } else {
+          int colour_index = red_percent_id_colours.length - 1;
+
+          if (maximum_percent_id > minimum_percent_id) {
+            colour_index =
+              (int)(red_percent_id_colours.length * 0.999 *
+                    (percent_id - minimum_percent_id) /
+                    (maximum_percent_id - minimum_percent_id));
+          }
+
+          if (this_match.isRevMatch ()) {
+            g.setColor (blue_percent_id_colours[colour_index]);
+          } else {
+            g.setColor (red_percent_id_colours[colour_index]);
+          }
+        }
+      }
+
+      g.fillPolygon (x_coords, y_coords, x_coords.length);
+
+      if (subject_end_x - subject_start_x < 5 &&
+          subject_end_x - subject_start_x > -5 ||
+          subject_start_x < -OFFSCREEN ||
+          subject_end_x > OFFSCREEN ||
+          query_start_x < -OFFSCREEN ||
+          query_end_x > OFFSCREEN) {
+
+        // match is (probably) narrow so draw the border to the same colour as
+        // the polygon
+      } else {
+
+        // draw a black outline the make the match stand out
+        g.setColor (Color.black);
+      }
+
+      g.drawLine (subject_start_x, 0, query_start_x, canvas_height);
+      g.drawLine (subject_end_x, 0, query_end_x, canvas_height);
+    }
+  }
+
+  /**
+   *  Return true if and only if the given match is currently visible.
+   **/
+  private boolean isVisible (final AlignMatch match) {
+    if (ignore_self_match_flag && match.isSelfMatch ()) {
+      return false;
+    }
+
+    final int score = match.getScore ();
+    final int percent_id = match.getPercentID ();
+
+    if (score > -1) {
+      if (score < minimum_score || score > maximum_score) {
+        return false;
+      }
+    }
+
+    if (percent_id > -1) {
+      if (percent_id < minimum_percent_id || percent_id > maximum_percent_id) {
+        return false;
+      }
+    }
+
+    final int match_length =
+      Math.abs (match.getSubjectSequenceStart () -
+                match.getSubjectSequenceEnd ());
+
+    if (match_length < scroll_bar.getValue ()) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   *  Return the start position in the subject sequence of the given
+   *  AlignMatch, taking into account the current orientation of the
+   *  sequences. If the reverse_position argument is true reverse the
+   *  complemented match coordinates will be returned.
+   **/
+  static int getRealSubjectSequenceStart (final AlignMatch match,
+                                          final int sequence_length,
+                                          final boolean reverse_position) {
+    if (reverse_position) {
+      return sequence_length - match.getSubjectSequenceStart () + 1;
+    } else {
+      return match.getSubjectSequenceStart ();
+    }
+  }
+
+  /**
+   *  Return the end position in the subject sequence of the given AlignMatch,
+   *  taking into account the current orientation of the sequences.  If the
+   *  reverse_position argument is true reverse the complemented match
+   *  coordinates will be returned.
+   **/
+  static int getRealSubjectSequenceEnd (final AlignMatch match,
+                                        final int sequence_length,
+                                        final boolean reverse_position) {
+    if (reverse_position) {
+      return sequence_length - match.getSubjectSequenceEnd () + 1;
+    } else {
+      return match.getSubjectSequenceEnd ();
+    }
+  }
+
+  /**
+   *  Return the start position in the query sequence of the given AlignMatch,
+   *  taking into account the current orientation of the sequences.    If the
+   *  reverse_position argument is true reverse the complemented match
+   *  coordinates will be returned.
+   **/
+  static int getRealQuerySequenceStart (final AlignMatch match,
+                                        final int sequence_length,
+                                        final boolean reverse_position) {
+    if (reverse_position) {
+      return sequence_length - match.getQuerySequenceStart () + 1;
+    } else {
+      return match.getQuerySequenceStart ();
+    }
+  }
+
+  /**
+   *  Return the end position in the query sequence of the given AlignMatch,
+   *  taking into account the current orientation of the sequences.  If the
+   *  reverse_position argument is true reverse the complemented match
+   *  coordinates will be returned.
+   **/
+  static int getRealQuerySequenceEnd (final AlignMatch match,
+                                      final int sequence_length,
+                                      final boolean reverse_position) {
+    if (reverse_position) {
+      return sequence_length - match.getQuerySequenceEnd () + 1;
+    } else {
+      return match.getQuerySequenceEnd ();
+    }
+  }
+
+  /**
+   *  Return the screen x positions of the corners of the given match.  The
+   *  order is Top Left, Top Right, Bottom Left, Bottom Right, unless the
+   *  match is an inversion, in which case it will be TL,TR,BR,BL.  Returns
+   *  null if and only if the match is not currently visible.
+   **/
+  private int [] getMatchCoords (final int canvas_width,
+                                 final AlignMatch this_match) {
+    final int subject_length = getSubjectForwardStrand ().getSequenceLength ();
+    final int query_length = getQueryForwardStrand ().getSequenceLength ();
+
+    int subject_sequence_start =
+      getRealSubjectSequenceStart (this_match,
+                                   subject_length,
+                                   subjectIsRevComp ());
+    int subject_sequence_end =
+      getRealSubjectSequenceEnd (this_match,
+                                 subject_length,
+                                 subjectIsRevComp ());
+    int query_sequence_start =
+      getRealQuerySequenceStart (this_match,
+                                 query_length,
+                                 queryIsRevComp ());
+    int query_sequence_end =
+      getRealQuerySequenceEnd (this_match,
+                               query_length,
+                               queryIsRevComp ());
+
+    // add one base because we want to draw to the end of the base
+    if (subjectIsRevComp ()) {
+      subject_sequence_start += 1;
+    } else {
+      subject_sequence_end += 1;
+    }
+
+    if (this_match.isRevMatch () && !queryIsRevComp () ||
+        !this_match.isRevMatch () && queryIsRevComp ()) {
+      query_sequence_start += 1;
+    } else {
+      query_sequence_end += 1;
+    }
+
+    final int subject_start_x =
+      getScreenPosition (canvas_width, last_subject_event,
+                         subject_sequence_start);
+    final int subject_end_x =
+      getScreenPosition (canvas_width, last_subject_event,
+                         subject_sequence_end);
+
+    final int query_start_x =
+      getScreenPosition (canvas_width, last_query_event,
+                         query_sequence_start);
+    final int query_end_x =
+      getScreenPosition (canvas_width, last_query_event,
+                         query_sequence_end);
+
+    boolean subject_off_left = false;
+    boolean subject_off_right = false;
+    boolean query_off_left = false;
+    boolean query_off_right = false;
+
+    if (subject_start_x < 0 && subject_end_x < 0) {
+      subject_off_left = true;
+    }
+
+    if (subject_start_x >= canvas_width && subject_end_x >= canvas_width) {
+      subject_off_right = true;
+    }
+
+    if (query_start_x < 0 && query_end_x < 0) {
+      query_off_left = true;
+    }
+
+    if (query_start_x >= canvas_width && query_end_x >= canvas_width) {
+      query_off_right = true;
+    }
+
+    if ((subject_off_left ? 1 : 0) +
+        (query_off_left ? 1 : 0) +
+        (subject_off_right ? 1 : 0) +
+        (query_off_right ? 1 : 0) == 2) {
+      return null;
+    } else {
+      final int [] return_values = new int [4];
+
+      return_values[0] = subject_start_x;
+      return_values[1] = subject_end_x;
+      return_values[2] = query_start_x;
+      return_values[3] = query_end_x;
+
+      return return_values;
+    }
+  }
+
+  /**
+   *  Return the current forward Strand of the subject EntryGroup.
+   **/
+  private Strand getSubjectForwardStrand () {
+    return getSubjectEntryGroup ().getBases ().getForwardStrand ();
+  }
+
+  /**
+   *  Return the current forward Strand of the query EntryGroup.
+   **/
+  private Strand getQueryForwardStrand () {
+    return getQueryEntryGroup ().getBases ().getForwardStrand ();
+  }
+
+  /**
+   *  Return the current reverse Strand of the subject EntryGroup.
+   **/
+  private Strand getSubjectReverseStrand () {
+    return getSubjectEntryGroup ().getBases ().getReverseStrand ();
+  }
+
+  /**
+   *  Return the current reverse Strand of the query EntryGroup.
+   **/
+  private Strand getQueryReverseStrand () {
+    return getQueryEntryGroup ().getBases ().getReverseStrand ();
+  }
+
+  /**
+   *  Return the subject EntryGroup that was passed to the constructor.
+   **/
+  private EntryGroup getSubjectEntryGroup () {
+    return subject_entry_group;
+  }
+
+  /**
+   *  Return the subject EntryGroup that was passed to the constructor.
+   **/
+  private EntryGroup getQueryEntryGroup () {
+    return query_entry_group;
+  }
+
+  /**
+   *  Return the reference of the subject FeatureDisplay.
+   **/
+  public FeatureDisplay getSubjectDisplay () {
+    return subject_feature_display;
+  }
+
+  /**
+   *  Return the reference of the query FeatureDisplay.
+   **/
+  public FeatureDisplay getQueryDisplay () {
+    return query_feature_display;
+  }
+
+  /**
+   *  Returns true if and only if the subject sequence has been flipped since
+   *  this object was created.
+   **/
+  boolean subjectIsRevComp () {
+    final Strand current_subject_forward_strand = getSubjectForwardStrand ();
+
+    if (getOrigSubjectForwardStrand () == current_subject_forward_strand ^
+        getSubjectDisplay ().isRevCompDisplay ()) {
+      return false;
+    } else {
+      return true;
+    }
+  }
+
+  /**
+   *  Returns true if and only if the query sequence has been flipped since
+   *  this object was created.
+   **/
+  boolean queryIsRevComp () {
+    final Strand current_query_forward_strand = getQueryForwardStrand ();
+
+    if (getOrigQueryForwardStrand () == current_query_forward_strand ^
+        getQueryDisplay ().isRevCompDisplay ()) {
+      return false;
+    } else {
+      return true;
+    }
+  }
+
+  /**
+   *  Return the forward Strand of the subject EntryGroup from when the
+   *  Comparator was created.
+   **/
+  public Strand getOrigSubjectForwardStrand () {
+    return orig_subject_forward_strand;
+  }
+
+  /**
+   *  Return the forward Strand of the query EntryGroup from when the
+   *  Comparator was created.
+   **/
+  public Strand getOrigQueryForwardStrand () {
+    return orig_query_forward_strand;
+  }
+
+  /**
+   *  Return the reverse Strand of the subject EntryGroup from when the
+   *  Comparator was created.
+   **/
+  public Strand getOrigSubjectReverseStrand () {
+    return orig_subject_reverse_strand;
+  }
+
+  /**
+   *  Return the reverse Strand of the query EntryGroup from when the
+   *  Comparator was created.
+   **/
+  public Strand getOrigQueryReverseStrand () {
+    return orig_query_reverse_strand;
+  }
+
+  /**
+   *  Arrange for the two FeatureDisplay objects to scroll in parallel.
+   **/
+  public void lockDisplays () {
+    displays_are_locked = true;
+    repaintCanvas ();
+  }
+
+  /**
+   *  Arrange for the two FeatureDisplay objects to scroll independently.
+   **/
+  public void unlockDisplays () {
+    displays_are_locked = false;
+    repaintCanvas ();
+  }
+
+  /**
+   *  Return true if and only if the displays are currently locked.
+   **/
+  public boolean displaysAreLocked () {
+    return displays_are_locked;
+  }
+
+  /**
+   *  Toggle whether the two displays are locked.
+   **/
+  public void toggleDisplayLock () {
+    displays_are_locked = !displays_are_locked;
+    repaintCanvas ();
+  }
+
+  /**
+   *  Calling this method will temporarily disable selectFromQueryRange() and
+   *  selectFromSubjectRange() until enableSelection().  This is need to allow
+   *  the selections of the top and bottom FeatureDisplays to be set without
+   *  changing which AlignMatches are selected.
+   **/
+  public void disableSelection () {
+    disable_selection_from_ranges = true;
+  }
+
+  /**
+   *  Enable selectFromQueryRange() and selectFromSubjectRange().
+   **/
+  public void enableSelection () {
+    disable_selection_from_ranges = false;
+  }
+
+  /**
+   *  Convert a base position into a screen x coordinate.
+   **/
+  private int getScreenPosition (final int canvas_width,
+                                 final DisplayAdjustmentEvent event,
+                                 final int base_position) {
+    // this is the base that is at the left of the screen
+    final int screen_start_base = event.getStart ();
+
+    final double base_pos_float =
+      event.getBaseWidth () * (base_position - screen_start_base);
+
+    if (base_pos_float > 30000) {
+      return 30000;
+    } else {
+      if (base_pos_float < -30000) {
+        return -30000;
+      } else {
+        return (int) base_pos_float;
+      }
+    }
+  }
+
+  /**
+   *  Return an array of colours that will be used for colouring the matches
+   *  (depending on score).
+   **/
+  private void makeColours () {
+    red_percent_id_colours = new Color [NUMBER_OF_SHADES];
+    blue_percent_id_colours = new Color [NUMBER_OF_SHADES];
+
+    for (int i = 0 ; i < blue_percent_id_colours.length ; ++i) {
+      final int shade_value = 255 - (int) (256 * 1.0 * i / NUMBER_OF_SHADES);
+      red_percent_id_colours[i] = new Color (255, shade_value, shade_value);
+      blue_percent_id_colours[i] = new Color (shade_value, shade_value, 255);
+    }
+  }
+
+  /**
+   *  Return the ComparisonData object that was passed to the constructor.
+   **/
+  private ComparisonData getComparisonData () {
+    return comparison_data;
+  }
+
+  /**
+   *  The comparison data that will be displayed in this component.
+   **/
+  final private ComparisonData comparison_data;
+
+  /**
+   *  All the AlignMatch objects from comparison_data (possibly in a
+   *  different order.
+   **/
+  private AlignMatch [] all_matches = null;
+
+  /**
+   *  This is the last DisplayAdjustmentEvent reference that was passed to
+   *  setSubjectSeqeuencePosition ().
+   **/
+  private DisplayAdjustmentEvent last_subject_event;
+
+  /**
+   *  This is the last DisplayAdjustmentEvent reference that was passed to
+   *  setQuerySeqeuencePosition ().
+   **/
+  private DisplayAdjustmentEvent last_query_event;
+
+  /**
+   *  The FeatureDisplay that is above this component.  (From the constructor).
+   **/
+  private FeatureDisplay subject_feature_display;
+
+  /**
+   *  The FeatureDisplay that is below this component.  (From the constructor).
+   **/
+  private FeatureDisplay query_feature_display;
+
+  /**
+   *  Set by the constructor to be the original forward strand for the subject
+   *  sequence.  This is use to determine whether to subject sequence has been
+   *  reverse-complemented or not.
+   **/
+  private Strand orig_subject_forward_strand;
+
+  /**
+   *  Set by the constructor to be the original forward strand for the query
+   *  sequence.  This is use to determine whether to query sequence has been
+   *  reverse-complemented or not.
+   **/
+  private Strand orig_query_forward_strand;
+
+  /**
+   *  Set by the constructor to be the original reverse strand for the subject
+   *  sequence.
+   **/
+  private Strand orig_subject_reverse_strand;
+
+  /**
+   *  Set by the constructor to be the original reverse strand for the query
+   *  sequence.
+   **/
+  private Strand orig_query_reverse_strand;
+
+  /**
+   *  One of the two Entry objects that we are comparing.
+   **/
+  final private EntryGroup subject_entry_group;
+
+  /**
+   *  One of the two Entry objects that we are comparing.
+   **/
+  final private EntryGroup query_entry_group;
+
+  /**
+   *  The selected matches.  null means no matches are selected.
+   **/
+  private AlignMatchVector selected_matches = null;
+
+  /**
+   *  The objects that are listening for AlignmentSelectionChangeEvents.
+   **/
+  private java.util.Vector selection_change_listeners =
+    new java.util.Vector ();
+
+  /**
+   *  The number of shades of red and blue to use for percentage ID colouring.
+   **/
+  private static int NUMBER_OF_SHADES = 13;
+
+  /**
+   *  The Reds used to display the percent identity of matches.
+   **/
+  private Color [] red_percent_id_colours;
+
+  /**
+   *  The Blues used to display the percent identity of matches.
+   **/
+  private Color [] blue_percent_id_colours;
+
+  /**
+   *  The scroll bar used to set the minimum length of the visible matches.
+   **/
+  private JScrollBar scroll_bar = null;
+
+  /**
+   *  Matches with scores below this value will not be shown.
+   **/
+  private int minimum_score = 0;
+
+  /**
+   *  Matches with scores above this value will not be shown.
+   **/
+  private int maximum_score = 99999999;
+
+  /**
+   *  Matches with percent identity values below this number will not be shown.
+   **/
+  private int minimum_percent_id = 0;
+
+  /**
+   *  Matches with percent identity values above this number will not be shown.
+   **/
+  private int maximum_percent_id = 100;
+
+  /**
+   *  True if we should offer to flip the query sequence when the user
+   *  double clicks on a flipped match.
+   **/
+  private boolean offer_to_flip_flag = false;
+
+  /**
+   *  If true ignore self matches (ie query start == subject start && query
+   *  end == subject end)
+   **/
+  private boolean ignore_self_match_flag = false;
+
+  /**
+   *  A Vector of those objects that are listening for AlignmentEvents
+   **/
+  private java.util.Vector alignment_event_listeners =
+    new java.util.Vector ();
+
+  /**
+   *  If true then the FeatureDisplays above and below this AlignmentViewer
+   *  should scroll together.
+   **/
+  private boolean displays_are_locked = true;
+
+  /**
+   *  Setting this to true will temporarily disable selectFromQueryRange() and
+   *  selectFromSubjectRange() until enableSelection().  This is need to allow
+   *  the selections of the top and bottom FeatureDisplays to be set without
+   *  changing which AlignMatches are selected.
+   **/
+  private boolean disable_selection_from_ranges = false;
+}
diff --git a/uk/ac/sanger/artemis/components/ArtemisCheckboxMenuItem.java b/uk/ac/sanger/artemis/components/ArtemisCheckboxMenuItem.java
new file mode 100644
index 000000000..fd3bf5357
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ArtemisCheckboxMenuItem.java
@@ -0,0 +1,127 @@
+/* CheckboxMenuItem.java
+ *
+ * created: Mon Aug 19 2002
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ArtemisCheckboxMenuItem.java,v 1.1 2004-06-09 09:46:02 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import java.util.Vector;
+
+/**
+ *  This component is a replacement for the CheckboxMenuItem component.  It is
+ *  needed on GNU/Linux with versions 1.2, 1.3 and 1.4 or the VM.  See this
+ *  bug for more:
+ *    http://developer.java.sun.com/developer/bugParade/bugs/4533641.html
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: ArtemisCheckboxMenuItem.java,v 1.1 2004-06-09 09:46:02 tjc Exp $
+ **/
+
+public class ArtemisCheckboxMenuItem
+    extends MenuItem
+    implements ActionListener, ItemSelectable {
+  /**
+   *  Make a new ArtemisCheckboxMenuItem with the given label.
+   **/
+  public ArtemisCheckboxMenuItem (final String label) {
+    super (label);
+
+    this.orig_label = label;
+
+    addActionListener (this);
+  }
+
+  /**
+   *  Implementation of ActionListener.
+   **/
+  public void actionPerformed (ActionEvent _) {
+    final ItemEvent e;
+    
+    if (getState ()) {
+      setState (false);
+      e = new ItemEvent (this, ItemEvent.ITEM_STATE_CHANGED,
+                         this, ItemEvent.DESELECTED);
+    } else {
+      setState (true);
+      e = new ItemEvent (this, ItemEvent.ITEM_STATE_CHANGED,
+                         this, ItemEvent.SELECTED);
+    }
+
+    for (int i = 0 ; i < listeners.size () ; ++i) {
+      ((ItemListener) listeners.elementAt (i)).itemStateChanged (e) ;
+    }
+  }
+
+  /**
+   *  Returns null (implementation of ItemSelectable).
+   **/
+  public Object[] getSelectedObjects() {
+    return null;
+  }
+
+  /**
+   *  Sets this check box menu item to the specifed state. The boolean value
+   *  true indicates "on" while false indicates "off."
+   *  @param state true if the check box menu item is on, otherwise false
+   **/
+  public void setState (final boolean state) {
+    this.state = state;
+    if (state) {
+      setLabel ("Disable " + orig_label + " (current on)");
+    } else {
+      setLabel ("Enable " + orig_label + " (current off)");
+    }
+  }
+
+  /**
+   *  Determines whether the state of this check box menu item is "on" or
+   *  "off."
+   *  @return the state of this check box menu item, where true indicates "on"
+   *    and false indicates "off" 
+   **/
+  public boolean getState () {
+    return state;
+  }
+
+  /**
+   *  Add the given listener.
+   **/
+  public void addItemListener (final ItemListener listener) {
+    listeners.addElement (listener);
+  }
+
+  /**
+   *  remove the given listener.
+   **/
+  public void removeItemListener (final ItemListener listener) {
+    listeners.removeElement (listener);
+  }
+
+  private String orig_label;
+  private boolean state = false;
+
+  private Vector listeners = new Vector ();
+}
diff --git a/uk/ac/sanger/artemis/components/ArtemisMain.java b/uk/ac/sanger/artemis/components/ArtemisMain.java
new file mode 100644
index 000000000..c51696133
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ArtemisMain.java
@@ -0,0 +1,634 @@
+/* ArtemisMain.java
+ *
+ * created: Wed Feb 23 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ArtemisMain.java,v 1.1 2004-06-09 09:46:03 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.NoSequenceException;
+import uk.ac.sanger.artemis.sequence.Bases;
+import uk.ac.sanger.artemis.util.Document;
+import uk.ac.sanger.artemis.util.DocumentFactory;
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.util.InputStreamProgressListener;
+import uk.ac.sanger.artemis.io.EntryInformation;
+
+import org.biojava.bio.seq.io.SequenceFormat;
+
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.io.*;
+
+/**
+ *  The main window for the Artemis sequence editor.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: ArtemisMain.java,v 1.1 2004-06-09 09:46:03 tjc Exp $
+ **/
+
+public class ArtemisMain extends Splash 
+{
+  /** Version String use for banner messages and title bars. */
+  public static final String version = "Swing 1";
+
+  /** A vector containing all EntryEdit object we have created. */
+  private EntryEditVector entry_edit_objects = new EntryEditVector();
+
+  protected static FileManager filemanager = null;
+  /**
+   *  The constructor creates all the components for the main Artemis 
+   *  window and sets up all the menu callbacks.
+   **/
+  public ArtemisMain() 
+  {
+    super("Artemis", "Artemis", version);
+
+    ActionListener menu_listener = new ActionListener()
+    {
+      public void actionPerformed(ActionEvent event)
+      {
+        if(filemanager == null)
+          filemanager = new FileManager(ArtemisMain.this);
+        else
+          filemanager.setVisible(true);
+      }
+    };
+    makeMenuItem(file_menu, "Open File Manager ...", menu_listener);
+
+    final EntrySourceVector entry_sources = getEntrySources(this);
+
+    for(int source_index=0; source_index<entry_sources.size();
+                            ++source_index) 
+    {
+      final EntrySource this_entry_source =
+        entry_sources.elementAt(source_index);
+
+      String entry_source_name = this_entry_source.getSourceName();
+      String menu_name = null;
+
+      if(entry_source_name.equals("Filesystem")) 
+        menu_name = "Open ...";
+      else 
+        menu_name = "Open from " + entry_source_name + " ...";
+
+      menu_listener = new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          getEntryEditFromEntrySource(this_entry_source);
+        }
+      };
+      makeMenuItem(file_menu, menu_name, menu_listener);
+    }
+
+    menu_listener = new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event)
+      {
+        exit();
+      }
+    };
+    makeMenuItem(file_menu, "Quit", menu_listener);
+
+//      getCanvas().addMouseListener(new MouseAdapter() {
+//        /**
+//         *  Listen for mouse press events so that we can do popup menus and
+//         *  selection.
+//         **/
+//        public void mousePressed(MouseEvent event) {
+//          handleCanvasMousePress(event);
+//        }
+//      });
+
+//  java.util.Properties props = System.getProperties();
+//  java.util.Enumeration en = props.propertyNames();
+//  while(en.hasMoreElements())
+//  {
+//    String prop = (String)en.nextElement();
+//    System.out.println(prop+":: "+props.getProperty(prop));
+//  }
+
+  }
+
+
+// XXX add pasteClipboard() one day
+
+//    /**
+//     *  Handle a mouse press event on the drawing canvas - select on click,
+//     *  select and broadcast it on double click.
+//     **/
+//    private void handleCanvasMousePress(MouseEvent event) {
+//      if(event.getID() != MouseEvent.MOUSE_PRESSED) {
+//        return;
+//      }
+
+//      if((event.getModifiers() & InputEvent.BUTTON2_MASK) != 0) {
+//        pasteClipboard();
+//      }
+//    }
+
+  /**
+   *  Read the entries named in args and in the diana.ini file.
+   **/
+  public void readArgsAndOptions(final String [] args,
+                                 ProgressThread progress_thread) 
+  {
+    if(args.length == 0) 
+    {
+      // open the entries given in the options file(diana.ini)
+      readDefaultEntries();
+      return;
+    }
+
+    if(args[0].equals("-biojava")) 
+    {
+      handleBioJava(args);
+      return;
+    }
+
+    final EntryInformation artemis_entry_information =
+                          Options.getArtemisEntryInformation();
+
+    EntryEdit last_entry_edit = null;
+    boolean seen_plus = false;
+
+    if(progress_thread != null)
+      progress_thread.start();
+
+    for(int i = 0 ; i<args.length ; ++i) 
+    {
+      String new_entry_name = args[i];
+
+      if(new_entry_name.length() == 0) 
+        continue;
+
+      if(new_entry_name.equals("+")) 
+      {
+        seen_plus = true;
+        continue;
+      }
+
+      
+      if(new_entry_name.startsWith("+") && last_entry_edit != null ||
+         seen_plus) 
+      {
+        // new feature file
+
+        final Document entry_document;
+
+        if(seen_plus) 
+          entry_document = DocumentFactory.makeDocument(new_entry_name);
+        else 
+          entry_document =
+            DocumentFactory.makeDocument(new_entry_name.substring(1));
+
+        final InputStreamProgressListener progress_listener =
+                                     getInputStreamProgressListener();
+
+        entry_document.addInputStreamProgressListener(progress_listener);
+
+        final uk.ac.sanger.artemis.io.Entry new_embl_entry =
+          EntryFileDialog.getEntryFromFile(this, entry_document,
+                                           artemis_entry_information,
+                                           false);
+
+        if(new_embl_entry == null)  // the read failed
+          break;
+
+        try
+        {
+          final Entry new_entry =
+            new Entry(last_entry_edit.getEntryGroup().getBases(),
+                      new_embl_entry);
+
+          last_entry_edit.getEntryGroup().add(new_entry);
+        } 
+        catch(OutOfRangeException e) 
+        {
+          new MessageDialog(this, "read failed: one of the features in " +
+                             new_entry_name + " has an out of range " +
+                             "location: " + e.getMessage());
+        }
+      } 
+      else
+      {
+        // new sequence file
+
+        if(last_entry_edit != null) 
+        {
+          last_entry_edit.setVisible(true);
+          last_entry_edit = null;
+        }
+
+        final Document entry_document =
+          DocumentFactory.makeDocument(new_entry_name);
+
+        entry_document.addInputStreamProgressListener(getInputStreamProgressListener());
+ 
+        final uk.ac.sanger.artemis.io.Entry new_embl_entry =
+          EntryFileDialog.getEntryFromFile(this, entry_document,
+                                           artemis_entry_information,
+                                           false);
+
+        if(new_embl_entry == null)  // the read failed
+          break;
+
+        try 
+        {
+          final Entry entry = new Entry(new_embl_entry);
+          last_entry_edit = makeEntryEdit(entry);
+          addEntryEdit(last_entry_edit);
+          getStatusLabel().setText("");
+        }
+        catch(OutOfRangeException e) 
+        {
+          new MessageDialog(this, "read failed: one of the features in " +
+                             new_entry_name + " has an out of range " +
+                             "location: " + e.getMessage());
+          break;
+        } 
+        catch(NoSequenceException e) 
+        {
+          new MessageDialog(this, "read failed: " +
+                             new_entry_name + " contains no sequence");
+          break;
+        }
+      }
+    }
+
+    for(int entry_index=0; entry_index<entry_edit_objects.size();
+        ++entry_index) 
+      entry_edit_objects.elementAt(entry_index).show();
+  }
+
+  /**
+   *  Handle the -biojava option
+   **/
+  private void handleBioJava(final String [] args) 
+  {
+    if(args.length == 3) 
+    {
+      final String class_name = args[1];
+      final String location = args[2];
+
+      final Document location_document =
+        DocumentFactory.makeDocument(location);
+
+      try 
+      {
+        final Object biojava_object =
+          Class.forName(class_name).newInstance();
+
+        final EntryInformation entry_information =
+          Options.getArtemisEntryInformation();
+
+        final uk.ac.sanger.artemis.io.BioJavaEntry emblEntry;
+
+        if(biojava_object instanceof SequenceFormat)
+        {
+          final SequenceFormat sequence_format =
+           (SequenceFormat) biojava_object;
+
+          emblEntry =
+            new uk.ac.sanger.artemis.io.BioJavaEntry(entry_information,
+                                                          location_document,
+                                                          sequence_format);
+
+          final Entry new_entry = new Entry(emblEntry);
+
+          final EntryEdit new_entry_edit = makeEntryEdit(new_entry);
+
+          new_entry_edit.setVisible(true);
+        } 
+        else 
+          new MessageDialog(this, "not a SequenceFormat: " + class_name);
+      } 
+      catch(IllegalAccessException e) 
+      {
+        new MessageDialog(this, "cannot create class: " + class_name +
+                           " - IllegalAccessException");
+      }
+      catch(ClassNotFoundException e)
+      {
+        new MessageDialog(this, "cannot find class: " + class_name);
+      } 
+      catch(ClassCastException e)
+      {
+        new MessageDialog(this, class_name + " is not a sub-class of " +
+                           "SequenceFormat");
+      }
+      catch(IOException e) 
+      {
+        new MessageDialog(this, "I/O error while reading from " +
+                           location + ": " + e.getMessage());
+      }
+      catch(NoSequenceException e) 
+      {
+        new MessageDialog(this, location + " contained no sequence");
+      } 
+      catch(InstantiationException e) 
+      {
+        new MessageDialog(this, "cannot instantiate " + class_name);
+      } 
+      catch(OutOfRangeException e) 
+      {
+        new MessageDialog(this, "read failed: one of the features in " +
+                           location +
+                           " has an out of range location: " +
+                           e.getMessage());
+      }
+    } 
+    else 
+      new MessageDialog(this, "the -biojava option needs two arguments");
+  }
+
+  /**
+   *  Read the entries given in the uk.ac.sanger.artemis.ini file.
+   **/
+  private void readDefaultEntries() 
+  {
+    final EntryInformation artemis_entry_information =
+                     Options.getArtemisEntryInformation();
+
+    final String default_sequence_file_name =
+                     Options.getOptions().getDefaultSequenceFileName();
+
+    final String default_feature_file_name =
+                     Options.getOptions().getDefaultFeatureFileName();
+
+    if(default_sequence_file_name != null) 
+    {
+      final String default_sequence_file_name_embl =
+                     default_sequence_file_name + "_embl";
+
+      uk.ac.sanger.artemis.io.Entry new_embl_entry = null;
+
+      // try opening the default sequence file with "_embl" added to the name
+      // if that fails try the plain sequence file name
+
+      final Document entry_document =
+        DocumentFactory.makeDocument(default_sequence_file_name_embl);
+
+      final InputStreamProgressListener progress_listener =
+        getInputStreamProgressListener();
+
+      entry_document.addInputStreamProgressListener(progress_listener);
+
+      if(entry_document.readable()) 
+      {
+        new_embl_entry =
+          EntryFileDialog.getEntryFromFile(this,
+                                            entry_document,
+                                            artemis_entry_information,
+                                            false);
+      }
+
+      if(new_embl_entry == null || new_embl_entry.getSequence() == null ||
+          new_embl_entry.getSequence().length() == 0) 
+      {
+        final File entry_file = new File(default_sequence_file_name);
+
+        if(entry_file.exists())
+        {
+          new_embl_entry =
+            EntryFileDialog.getEntryFromFile(this,
+                                              entry_document,
+                                              artemis_entry_information,
+                                              false);
+        }
+        else
+        {
+          // read failed
+          System.err.println("file does not exist: " +
+                              default_sequence_file_name +
+                              "(given in options files)");
+          return;
+        }
+      }
+
+      if(new_embl_entry == null || new_embl_entry.getSequence() == null ||
+          new_embl_entry.getSequence().length() == 0) 
+      {
+        // read failed
+        System.err.println("failed to read " + default_sequence_file_name +
+                            "(given in options files)");
+        return;
+      }
+
+      getStatusLabel().setText("");
+
+      try 
+      {
+        final Entry entry = new Entry(new_embl_entry);
+
+        final EntryEdit new_entry_edit = makeEntryEdit(entry);
+
+        new_entry_edit.setVisible(true);
+
+        if(default_feature_file_name != null) 
+        {
+          final Document feature_document =
+            DocumentFactory.makeDocument(default_feature_file_name);
+
+          final uk.ac.sanger.artemis.io.Entry new_embl_table_entry =
+            EntryFileDialog.getEntryFromFile(this,
+                                              feature_document,
+                                              artemis_entry_information,
+                                              false);
+
+          if(new_embl_table_entry == null)  // open failed
+            return;
+
+          final EntryGroup entry_group = new_entry_edit.getEntryGroup();
+
+          final Entry new_table_entry =
+            new Entry(entry.getBases(), new_embl_table_entry);
+
+          entry_group.add(new_table_entry);
+        }
+      } catch(OutOfRangeException e) {
+        new MessageDialog(this, "read failed: one of the features in " +
+                           default_feature_file_name +
+                           " has an out of range location: " +
+                           e.getMessage());
+      } catch(NoSequenceException e) {
+        new MessageDialog(this, "read failed: " +
+                           new_embl_entry.getName() +
+                           " contains no sequence");
+      }
+    }
+  }
+
+  /**
+   *  Make an EntryEdit component from the given Entry.
+   **/
+  private EntryEdit makeEntryEdit(final Entry entry) 
+  {
+    final Bases bases = entry.getBases();
+    final EntryGroup entry_group = new SimpleEntryGroup(bases);
+    entry_group.add(entry);
+    final EntryEdit entry_edit = new EntryEdit(entry_group);
+
+    return entry_edit;
+  }
+
+  /**
+   *  This method gets rid of an EntryEdit object and it's frame.  Each object
+   *  removed with this method must have been added previously with
+   *  addEntryEdit().
+   *  @param entry_edit The object to get rid of.
+   **/
+  public void entryEditFinished(EntryEdit entry_edit) 
+  {
+    if(null == entry_edit) 
+      throw new Error("entryEditFinished() was passed a null object");
+
+    if(!entry_edit_objects.removeElement(entry_edit)) 
+      throw new Error("entryEditFinished() - could not remove a " +
+                       "object from an empty vector");
+
+    entry_edit.setVisible(false);
+    entry_edit.dispose();
+  }
+
+  /**
+   *  Add an EntryEdit object to our list of objects.
+   *  @param entry_edit The object to add.
+   **/
+  public synchronized void addEntryEdit(EntryEdit entry_edit) 
+  {
+    entry_edit_objects.addElement(entry_edit);
+  }
+
+  /**
+   *  Read an Entry from the given EntrySource and make a new EntryEdit
+   *  component for the Entry.
+   **/
+  private void getEntryEditFromEntrySource(final EntrySource entry_source) 
+  {
+    final ProgressThread progress_thread = new ProgressThread(null,
+                                                "Loading Entry...");
+
+    SwingWorker entryWorker = new SwingWorker()
+    { 
+      EntryEdit entry_edit;
+      public Object construct()
+      {
+        try
+        {
+          final Entry entry = entry_source.getEntry(true,progress_thread);
+          if(entry == null)
+            return null;
+
+          final EntryGroup entry_group =
+              new SimpleEntryGroup(entry.getBases());
+
+          entry_group.add(entry);
+          entry_edit = new EntryEdit(entry_group);
+        }
+        catch(OutOfRangeException e)
+        {
+          new MessageDialog(ArtemisMain.this, "read failed: one of the features in " +
+                     " the entry has an out of range " +
+                     "location: " + e.getMessage());
+        }
+        catch(NoSequenceException e)
+        {
+          new MessageDialog(ArtemisMain.this, "read failed: entry contains no sequence");
+        }
+        catch(IOException e)
+        {
+          new MessageDialog(ArtemisMain.this, "read failed due to IO error: " + e);
+        }
+        return null;
+      }
+
+      public void finished()
+      {
+        if(entry_edit != null)
+          entry_edit.setVisible(true);
+        if(progress_thread !=null)
+          progress_thread.finished();
+      }
+    };
+    entryWorker.start();
+  }
+
+  /**
+   *  Force all the EntryEdit components to be redisplayed.
+   **/
+  private void redisplayAll() 
+  {
+    for(int i=0 ; i<entry_edit_objects.size() ; ++i) 
+      entry_edit_objects.elementAt(i).redisplay();
+  }
+
+  /**
+   *  Close the main frame and all EntryEdit frames and then this frame,
+   *  then exit.
+   **/
+  protected void exit() 
+  {
+//  for(int i=0 ; i<entry_edit_objects.size() ;++i) 
+//    entryEditFinished(entry_edit_objects.elementAt(i));
+ 
+//  if(filemanager != null)
+//    filemanager.setVisible(false);
+
+//  setVisible(false);
+//  dispose();
+//  System.gc();
+    System.exit(0);
+  }
+
+  /**
+   *  Main entry point for the stand-alone version of Artemis.
+   **/
+  public static void main(final String [] args) 
+  {
+    final ArtemisMain main_window = new ArtemisMain();
+    main_window.setVisible(true);
+
+    final ProgressThread progress_thread = new ProgressThread(null,
+                                         "Loading Entry...");
+
+    SwingWorker entryWorker = new SwingWorker()
+    {
+      public Object construct()
+      {
+        // read the entries given on the command line and in the diana.ini file
+        main_window.readArgsAndOptions(args,progress_thread);
+        return null;
+      }
+     
+      public void finished()
+      {
+        if(progress_thread !=null)
+          progress_thread.finished();
+      }
+    };
+    entryWorker.start();
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/BasePlot.java b/uk/ac/sanger/artemis/components/BasePlot.java
new file mode 100644
index 000000000..e71ccb918
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/BasePlot.java
@@ -0,0 +1,627 @@
+/* BasePlot.java
+ *
+ * created: Tue Dec 15 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1998,1999,2000,2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/BasePlot.java,v 1.1 2004-06-09 09:46:04 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.plot.*;
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A component for plotting functions over the base sequence.  Scrolling and
+ *  scale is tied to a FeatureDisplay component.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: BasePlot.java,v 1.1 2004-06-09 09:46:04 tjc Exp $
+ **/
+
+public class BasePlot extends Plot
+    implements DisplayAdjustmentListener, SelectionChangeListener {
+  /**
+   *  Create a new FeatureDisplay object.
+   *  @param algorithm The object that will generate the value we plot in
+   *    this component.
+   *  @param selection Used to set and display the current selection in the
+   *    BasePlot.
+   *  @param goto_event_source The object the we will call gotoBase () on.
+   *    This allows the user to double click on a base in a BasePlot and have
+   *    the FeatureDisplay follow.
+   **/
+  public BasePlot (final BaseAlgorithm algorithm,
+                   final Selection selection,
+                   final GotoEventSource goto_event_source) {
+    super (algorithm, false);   // false means don't draw the scale line
+
+    this.selection = selection;
+    this.goto_event_source = goto_event_source;
+    this.bases = getBaseAlgorithm ().getBases ();
+
+    getSelection ().addSelectionChangeListener (this);
+
+    addPlotMouseListener (new PlotMouseListener () {
+      /**
+       *  Set the selection to be a range from start_base to end_base.
+       **/
+      private void setSelectionRange (final int start_base,
+                                      final int end_base) {
+        final Strand strand = bases.getForwardStrand ();
+
+        try {
+          final MarkerRange marker_range =
+            strand.makeMarkerRangeFromPositions (start_base, end_base);
+          getSelection ().setMarkerRange (marker_range);
+        } catch (uk.ac.sanger.artemis.util.OutOfRangeException e) {
+          getSelection ().clear ();
+        }
+      }
+
+      /**
+       *  Called when the user clicks somewhere on the plot canvas.
+       *  @param position the base/amino acid position of the click.  This is
+       *    -1 if and only if the click was outside the graph (eg. in the
+       *    label at the top)
+       **/
+      public void mouseClick (final int position) {
+
+      }
+
+      /**
+       *  Called when the user drags the mouse over the plot.
+       *  @param drag_start_position The base/amnino acid position where the
+       *    drag started or -1 if the drag was started outside the graph.
+       *  @param current_position the base/amino acid position of the click.
+       *    This is -1 if and only if the user has dragged the mouse out of
+       *    the graph (eg. in the label at the top)
+       **/
+      public void mouseDrag (int drag_start_position,
+                             int current_position) {
+        if (rev_comp_display) {
+          drag_start_position =
+            bases.getComplementPosition (drag_start_position);
+          current_position =
+            bases.getComplementPosition (current_position);
+        }
+        setSelectionRange (drag_start_position,
+                           current_position);
+      }
+
+      /**
+       *  Called when the user double-clicks somewhere on the plot.
+       *  @param position the base/amino acid position of the click.  This is
+       *    -1 if and only if the click was outside the graph (eg. in the
+       *    label at the top)
+       **/
+      public void mouseDoubleClick (int position) {
+        if (rev_comp_display) {
+          position = bases.getComplementPosition (position);
+        }
+        setSelectionRange (position, position);
+        getGotoEventSource ().gotoBase (position);
+      }
+    });
+  }
+
+  /**
+   *  Used by getPreferredSize () and getMinimumSize ();
+   **/
+  private final static int HEIGHT;
+
+  static {
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+
+    final Integer base_plot_height = 
+      Options.getOptions ().getIntegerProperty ("base_plot_height");
+
+    if (base_plot_height == null) {
+      if (screen.height <= 600) {
+        HEIGHT = 100;
+      } else {
+        HEIGHT = 150;
+      }
+    } else {
+      HEIGHT = base_plot_height.intValue ();
+    }
+  }
+
+  /**
+   *  Overridden to set the component height to 150.
+   **/
+  public Dimension getPreferredSize() {
+    return (new Dimension(getSize ().width, HEIGHT));
+  }
+
+  /**
+   *  Overridden to set the component height to 150.
+   **/
+  public Dimension getMinimumSize() {
+    return (new Dimension(getSize ().width, HEIGHT));
+  }
+
+  /**
+   *  Implementation of the DisplayAdjustmentListener interface.  Invoked when
+   *  a component or changes the scale.
+   **/
+  public void displayAdjustmentValueChanged (DisplayAdjustmentEvent event) {
+    start_base = event.getStart ();
+    end_base = event.getEnd ();
+    width_in_bases = event.getWidthInBases ();
+    rev_comp_display = event.isRevCompDisplay ();
+    recalculate_flag = true;
+
+    if (event.getType () == DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT) {
+      selection_start_marker = null;
+      selection_end_marker = null;
+    }
+
+    repaintCanvas ();
+  }
+
+  /**
+   *  We listen for SelectionChange events so that we can update the
+   *  crosshairs.
+   **/
+  public void selectionChanged (SelectionChangeEvent event) {
+    selection_start_marker = null;
+    selection_end_marker = null;
+    repaintCanvas ();
+  }
+
+  /**
+   *  Return the algorithm that was passed to the constructor.
+   **/
+  public BaseAlgorithm getBaseAlgorithm () {
+    return (BaseAlgorithm) super.getAlgorithm ();
+  }
+
+  /**
+   *  Return the new start base to display, from the last event.
+   **/
+  private int getStart () {
+    return start_base;
+  }
+
+  /**
+   *  Return the new end base to display, from the last event.
+   **/
+  private int getEnd () {
+    return end_base;
+  }
+
+  /**
+   *  Return the width in bases of the display, from the last event.
+   **/
+  private int getWidthInBases () {
+    return width_in_bases;
+  }
+
+  /**
+   *  This array is used by drawMultiValueGraph ().  It is reallocated when
+   *  the scale changes.
+   **/
+  private float [][] value_array_array = null;
+
+  /**
+   *  The number of bases to step before each evaluation of the algorithm.
+   *  (Set by recalculateValues ()).
+   **/
+  private int step_size = 0;
+
+  /**
+   *  The maximum of the values in value_array_array.
+   **/
+  private float min_value = Float.MAX_VALUE;
+
+  /**
+   *  The minimum of the values in value_array_array.
+   **/
+  private float max_value = Float.MIN_VALUE;
+
+  /**
+   *  Recalculate the values in value_array_array, step_size, min_value and
+   *  max_value.
+   **/
+  protected void recalculateValues () {
+    final Float algorithm_minimum = getAlgorithm ().getMinimum ();
+    final Float algorithm_maximum = getAlgorithm ().getMaximum ();
+
+    // use the Algorithm specified maximum if there is one - otherwise
+    // calculate it
+    if (algorithm_maximum == null) {
+      max_value = Float.MIN_VALUE;
+    } else {
+      max_value = algorithm_maximum.floatValue ();
+    }
+
+    // use the Algorithm specified minimum if there is one - otherwise
+    // calculate it
+    if (algorithm_minimum == null) {
+      min_value = Float.MAX_VALUE;
+    } else {
+      min_value = algorithm_minimum.floatValue ();
+    }
+
+    final int window_size = getWindowSize ();
+
+    final Integer default_step_size =
+      getAlgorithm ().getDefaultStepSize (window_size);
+
+    if (default_step_size == null) {
+      step_size = 1;
+    } else {
+      if (default_step_size.intValue () < window_size) {
+        step_size = default_step_size.intValue ();
+      } else {
+        step_size = window_size;
+      }
+    }
+
+    int real_start = getStart ();
+
+    if (real_start < 1) {
+      real_start = 1;
+    }
+
+    final int unit_count = getEnd () - real_start;
+
+    // the number of plot points in the graph
+    final int number_of_values =
+      (unit_count - (window_size - step_size)) / step_size;
+
+    if (number_of_values < 2) {
+      // there is nothing to plot
+      value_array_array = null;
+      return;
+    }
+
+    getBaseAlgorithm ().setRevCompDisplay (rev_comp_display);
+
+    // the number of values that getValues () will return
+    final int get_values_return_count =
+      getBaseAlgorithm ().getValueCount ();
+
+    if (value_array_array == null) {
+      value_array_array = new float [get_values_return_count][];
+    }
+
+    if (value_array_array[0] == null ||
+        value_array_array[0].length != number_of_values) {
+      for (int i = 0 ; i < value_array_array.length ; ++i) {
+        value_array_array[i] = new float [number_of_values];
+      }
+    } else {
+      // reuse the previous arrays
+    }
+
+    float [] temp_values = new float [get_values_return_count];
+
+    for (int i = 0 ; i < number_of_values ; ++i) {
+      getBaseAlgorithm ().getValues (real_start + i * step_size,
+                                     real_start + i * step_size +
+                                     window_size - 1,
+                                     temp_values);
+
+      for (int value_index = 0 ;
+           value_index < get_values_return_count ;
+           ++value_index) {
+        final float current_value = temp_values[value_index];
+
+        value_array_array[value_index][i] = current_value;
+
+        // use the Algorithm specified maximum if there is one - otherwise
+        // calculate it
+        if (algorithm_maximum == null) {
+          if (current_value > max_value) {
+            max_value = current_value;
+          }
+        }
+
+        // use the Algorithm specified minimum if there is one - otherwise
+        // calculate it
+        if (algorithm_minimum == null) {
+          if (current_value < min_value) {
+            min_value = current_value;
+          }
+        }
+      }
+    }
+
+    recalculate_flag = false;
+  }
+
+  /**
+   *  Redraw the graph on the canvas using the algorithm, start_base and
+   *  end_base.  This method plots BaseWindowAlgorithm objects only.
+   *  @param g The object to draw into.
+   **/
+  public void drawMultiValueGraph (Graphics g) {
+    if (recalculate_flag) {
+      recalculateValues ();
+    }
+
+    if (value_array_array == null) {
+      // there is nothing to draw - probably because the sequence is too short
+      drawMinMax (g, 0, 1);
+
+      return;
+    }
+
+    final int window_size = getWindowSize ();
+
+    // the number of values to plot at each x position
+    final int get_values_return_count =
+      getBaseAlgorithm ().getValueCount ();
+
+    final int number_of_values = value_array_array[0].length;
+
+    if (number_of_values > 1) {
+      drawGlobalAverage (g, min_value, max_value);
+    }
+
+    for (int value_index = 0 ;
+         value_index < get_values_return_count ;
+         ++value_index) {
+      if (get_values_return_count == 1) {
+        g.setColor (Color.black);
+      } else {
+        switch (value_index) {
+        case 0:
+          g.setColor (new Color (255, 0, 0));
+          break;
+        case 1:
+          g.setColor (new Color (0, 200, 0));
+          break;
+        case 2:
+          g.setColor (new Color (0, 0, 255));
+          break;
+        default:
+          g.setColor (Color.black);
+        }
+      }
+
+      final int offset;
+
+      if (getStart () < 1) {
+        offset = 1 - getStart ();
+      } else {
+        offset = 0;
+      }
+
+      drawPoints (g, min_value, max_value, step_size, window_size,
+                  getWidthInBases (),
+                  offset,
+                  value_array_array[value_index]);
+    }
+
+    drawMinMax (g, min_value, max_value);
+
+    if (getCrossHairPosition () >= 0) {
+      final int cross_hair_position = getCrossHairPosition ();
+
+      final int selection_base = getPointPosition (cross_hair_position);
+
+      if (selection_base >= 1) {
+        if (selection_base > end_base) {
+          cancelCrossHairs ();
+        } else {
+          final String label_string;
+
+          if (rev_comp_display) {
+            label_string =
+              String.valueOf (bases.getLength () - selection_base + 1);
+          } else {
+            label_string = String.valueOf (selection_base);
+          }
+
+          drawCrossHair (g, cross_hair_position, label_string, 0);
+        }
+      }
+    }
+
+    if (getCrossHairPosition () >= 0 && getSelectionStartMarker () != null) {
+      final int selection_first_base =
+        getSelectionStartMarker ().getRawPosition ();
+
+      final String label_string;
+
+      if (rev_comp_display) {
+        label_string =
+          String.valueOf (bases.getLength () - selection_first_base + 1);
+      } else {
+        label_string = String.valueOf (selection_first_base);
+      }
+
+      if (Math.abs (selection_first_base -
+                    getPointPosition (getCrossHairPosition ())) > 3) {
+        drawCrossHair (g, getCanvasPosition (selection_first_base),
+                       label_string, 1);
+      } else {
+        // don't draw - too close to main cross hair
+      }
+    }
+
+    if (getCrossHairPosition () >= 0 && getSelectionEndMarker () != null) {
+      if (getSelectionStartMarker () != null &&
+          Math.abs ((getSelectionEndMarker ().getRawPosition () -
+                     getSelectionStartMarker ().getRawPosition ())) >= 3) {
+        final int selection_last_base =
+          getSelectionEndMarker ().getRawPosition ();
+
+        final String label_string;
+
+        if (rev_comp_display) {
+          label_string =
+            String.valueOf (bases.getLength () - selection_last_base + 1);
+        } else {
+          label_string = String.valueOf (selection_last_base);
+        }
+
+        if (Math.abs (selection_last_base -
+                      getPointPosition (getCrossHairPosition ())) > 3) {
+          drawCrossHair (g, getCanvasPosition (selection_last_base),
+                         label_string, 2);
+        } else {
+          // don't draw - too close to main cross hair
+        }
+      }
+    }
+  }
+
+  /**
+   *  Get the position in the Feature of the given canvas x position.  This
+   *  base position is the label used when the user clicks the mouse in on the
+   *  canvas (see drawCrossHair ()).
+   **/
+  protected int getPointPosition (final int canvas_x_position) {
+    return (int) ((1.0 * canvas_x_position / getCanvas ().getSize ().width) *
+                  getWidthInBases ()) + getStart ();
+  }
+
+  /**
+   *  Return the canvas position of the given base,
+   **/
+  private int getCanvasPosition (final int base) {
+    return (int) ((1.0 * base - getStart ()) / getWidthInBases () *
+                  getCanvas ().getSize ().width);
+  }
+
+  /**
+   *  Return the Marker of the start base of the Selection or null if there is
+   *  nothing selected.
+   **/
+  private Marker getSelectionStartMarker () {
+    if (selection_start_marker == null) {
+      selection_start_marker = getSelection ().getLowestBaseOfSelection ();
+
+      if (selection_start_marker != null &&
+          rev_comp_display) {
+        final Strand strand = bases.getReverseStrand ();
+        final int orig_position = selection_start_marker.getRawPosition ();
+        final int rev_comp_position =
+          bases.getComplementPosition (orig_position);
+        try {
+          selection_start_marker = strand.makeMarker (orig_position);
+        } catch (OutOfRangeException e) {
+          throw new Error ("internal error - unexpected exception: " + e);
+        }
+      }
+    }
+
+    return selection_start_marker;
+  }
+
+  /**
+   *  Return the Marker of the end base of the Selection or null if there is
+   *  nothing selected.
+   **/
+  private Marker getSelectionEndMarker () {
+    if (selection_end_marker == null) {
+      selection_end_marker = getSelection ().getHighestBaseOfSelection ();
+
+      if (selection_end_marker != null &&
+          rev_comp_display) {
+        final Strand strand = bases.getReverseStrand ();
+        final int orig_position = selection_end_marker.getRawPosition ();
+        final int rev_comp_position =
+          bases.getComplementPosition (orig_position);
+        try {
+          selection_end_marker = strand.makeMarker (orig_position);
+        } catch (OutOfRangeException e) {
+          throw new Error ("internal error - unexpected exception: " + e);
+        }
+      }
+    }
+
+    return selection_end_marker;
+  }
+
+  /**
+   *  Return the Selection object that was passed to the constructor.
+   **/
+  private Selection getSelection () {
+    return selection;
+  }
+
+  /**
+   *  Return the GotoEventSource object that was passed to the constructor.
+   **/
+  private GotoEventSource getGotoEventSource () {
+    return goto_event_source;
+  }
+
+  /**
+   *  The start base to plot, as obtained from the DisplayAdjustmentEvent.
+   **/
+  private int start_base;
+
+  /**
+   *  The end base to plot, as obtained from the DisplayAdjustmentEvent.
+   **/
+  private int end_base;
+
+  /**
+   *  The width in bases of the display, as obtained from the
+   *  DisplayAdjustmentEvent.
+   **/
+  private int width_in_bases;
+
+  /**
+   *  True if and only if the FeatureDisplay is drawing in reverse complement
+   *  mode.
+   **/
+  private boolean rev_comp_display;
+
+  /**
+   *  The Bases that this BasePlot is graphing.
+   **/
+  private Bases bases;
+
+  /**
+   *  The Marker of the start of the selection.  This is a cache used by
+   *  getSelectionStartMarker ().
+   **/
+  private Marker selection_start_marker = null;
+
+  /**
+   *  The Marker of the end of the selection.  This is a cache used by
+   *  getSelectionEndMarker ()
+   **/
+  private Marker selection_end_marker = null;
+
+  /**
+   *  The Selection that was passed to the constructor.
+   **/
+  private Selection selection;
+
+  /**
+   *  The GotoEventSource that was passed to the constructor.
+   **/
+  private GotoEventSource goto_event_source;
+}
diff --git a/uk/ac/sanger/artemis/components/BasePlotGroup.java b/uk/ac/sanger/artemis/components/BasePlotGroup.java
new file mode 100644
index 000000000..09a5bbaaf
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/BasePlotGroup.java
@@ -0,0 +1,337 @@
+/* BasePlotGroup.java
+ *
+ * created: Tue Dec 15 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/BasePlotGroup.java,v 1.1 2004-06-09 09:46:05 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.plot.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.IOException;
+import java.io.File;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  This is a super-component containing several BasePlot components, each of
+ *  which can toggled off and on.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: BasePlotGroup.java,v 1.1 2004-06-09 09:46:05 tjc Exp $
+ **/
+
+public class BasePlotGroup extends JPanel
+  implements DisplayAdjustmentListener {
+  /**
+   *  Create a new BasePlotGroup component for the given EntryGroup.
+   *  @param selection Used to set and display the current selection in the
+   *    BasePlot.
+   *  @param goto_event_source The object the we will call gotoBase () on.
+   *    This allows the user to double click on a base in a BasePlot and have
+   *    the FeatureDisplay follow.
+   **/
+  public BasePlotGroup (final EntryGroup entry_group,
+                        final Component owning_component,
+                        final Selection selection,
+                        final GotoEventSource goto_event_source) {
+    this.owning_component = owning_component;
+    this.entry_group = entry_group;
+    this.selection = selection;
+    this.goto_event_source = goto_event_source;
+
+    final Strand forward_strand =
+      entry_group.getBases ().getForwardStrand ();
+
+    final Strand reverse_strand =
+      entry_group.getBases ().getReverseStrand ();
+
+    // the following arrays are from failed tests
+    final float [] [] test_weights = {
+      {1, 0,39,99,11}, // a
+      {76,8,15,1,45}, // c
+      {2,0,42,0,6}, // g
+      {21,91,4,0,38}  // t
+    };
+
+    final float [] [] test_weights2 = {
+      {11,11,10, 8,11,10,11,11, 7, 8,25, 3,100,  0,27},
+      {29,33,30,30,32,34,37,38,39,36,26,75,  0,  0,14},
+      {14,12,10,10, 9,11,10, 9, 7, 6,26, 1,  0,100,49},
+      {46,44,50,52,48,45,42,43,47,51,23,21,  0,  0,10}
+    };
+
+    final float [] [] test_weights3 = {
+      {0,0,1,0,0,0},
+      {0,0,0,1,0,0},
+      {1,0,0,0,1,0},
+      {0,1,0,0,0,1},
+    };
+
+    final float [] cai_test = {
+      0.113F, 1.0F,   0.117F, 1.0F,
+      1.0F,   0.693F, 0.036F, 0.005F,
+      0.071F, 1.0F,   0.0F,   0.0F,
+      1.0F,   0.077F, 0.0F,   1.0F,
+
+      0.006F, 0.003F, 0.039F, 0.003F,
+      0.047F, 0.009F, 1.0F,   0.002F,
+      0.245F, 1.0F,   1.0F,   0.007F,
+      0.137F, 0.002F, 0.002F, 0.002F,
+
+      0.823F, 1.0F,   0.003F, 1.0F,
+      0.921F, 1.0F,   0.012F, 0.006F,
+      0.053F, 1.0F,   0.135F, 1.0F,
+      0.021F, 0.031F, 1.0F,   0.003F,
+
+      1.0F,   0.831F, 0.002F, 0.018F,
+      1.0F,   0.316F, 0.015F, 0.001F,
+      0.554F, 1.0F,   1.0F,   0.016F,
+      1.0F,   0.02F,  0.002F, 0.004F
+    };
+
+//    plot_algorithms_temp.add (new CAIWindowAlgorithm (forward_strand, cai_test));
+//    plot_algorithms_temp.add (new UserBaseAlgorithm (forward_strand, "test", test_weights));
+//    plot_algorithms_temp.add (new UserBaseAlgorithm (forward_strand, "test2", test_weights2));
+//    plot_algorithms_temp.add (new UserBaseAlgorithm (forward_strand, "test3", test_weights3));
+
+    setLayout (gridbag);
+
+    c.fill = GridBagConstraints.HORIZONTAL;
+    c.anchor = GridBagConstraints.NORTH;
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    c.gridheight = 1;
+    c.weightx = 1;
+    c.weighty = 0;
+
+    c.insets = new Insets (0,0,5,0);
+
+    addAlgorithm (new GCWindowAlgorithm (forward_strand));
+    addAlgorithm (new GCSDWindowAlgorithm (forward_strand));
+    addAlgorithm (new AGWindowAlgorithm (forward_strand));
+    addAlgorithm (new GCFrameAlgorithm (forward_strand));
+    addAlgorithm (new GCFrameAlgorithm (reverse_strand));
+    addAlgorithm (new Codon12CorrelationAlgorithm(forward_strand));
+    addAlgorithm (new Codon12CorrelationAlgorithm(reverse_strand));
+    addAlgorithm (new GCDeviationAlgorithm (forward_strand));
+    addAlgorithm (new ATDeviationAlgorithm (forward_strand));
+    addAlgorithm (new KarlinSigAlgorithm (forward_strand));
+  }
+
+  /**
+   *  Implementation of the DisplayAdjustmentListener interface.  Invoked when
+   *  a component (FeatureDisplay) scrolls or changes the scale.  Sends the
+   *  event to all the BasePlot components in this BasePlotGroup.
+   **/
+  public void displayAdjustmentValueChanged (DisplayAdjustmentEvent event) {
+    final Component [] children = getComponents ();
+
+    for (int i = 0 ; i < children.length ; ++i) {
+      if (children[i] instanceof BasePlot) {
+        ((BasePlot)children[i]).displayAdjustmentValueChanged (event);
+      }
+    }
+  }
+
+  /**
+   *  Add a new BasePlot component for the given algorithm.
+   *  @return The new BasePlot
+   **/
+  public BasePlot addAlgorithm (final BaseAlgorithm algorithm) {
+    plot_value_producers.addElement (algorithm);
+
+    return makePlot (algorithm, gridbag, c);
+  }
+
+  /**
+   *  Return the first CodonUsageAlgorithm or null if there isn't one in the
+   *  group.
+   **/
+  public CodonUsageAlgorithm getCodonUsageAlgorithm () {
+    for (int i = 0 ; i < plot_value_producers.size () ; ++i) {
+      final BaseAlgorithm this_algorithm =
+        (BaseAlgorithm) plot_value_producers.elementAt (i);
+
+      if (this_algorithm instanceof CodonUsageAlgorithm) {
+        return (CodonUsageAlgorithm) this_algorithm;
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   *  Return an array containing the Algorithm objects of the BasePlot
+   *  components in this BasePlotGroup.
+   **/
+  public BaseAlgorithm [] getPlotAlgorithms () {
+    final BaseAlgorithm [] return_array =
+      new BaseAlgorithm [plot_value_producers.size ()];
+
+    for (int i = 0 ; i < plot_value_producers.size () ; ++i) {
+      final BaseAlgorithm this_algorithm =
+        (BaseAlgorithm) plot_value_producers.elementAt (i);
+      return_array[i] = this_algorithm;
+    }
+
+    return return_array;
+  }
+
+  /**
+   *  Return true if and only if the BasePlot for the given Algorithm is
+   *  visible.
+   **/
+  public boolean basePlotIsVisible (final Algorithm algorithm) {
+    final Component base_plot = findPlotByAlgorithm (algorithm);
+
+    return base_plot.isVisible ();
+  }
+
+  /**
+   *  Given an Algorithm, find and set the visibility of the corresponding
+   *  BasePlot component.
+   **/
+  public void setVisibleByAlgorithm (final Algorithm algorithm,
+                                     final boolean visible) {
+    final JComponent base_plot = findPlotByAlgorithm (algorithm);
+
+    base_plot.setVisible (visible);
+    if (getParent () != null) {
+      // XXX change to revalidate().
+      getParent ().validate ();
+    }
+  }
+
+  /**
+   *  Find the BasePlot component in this BasePlotGroup object that is
+   *  plotting the given Algorithm or null if no such BasePlot object exists.
+   **/
+  private JComponent findPlotByAlgorithm (final Algorithm algorithm) {
+    final Component [] children = getComponents ();
+
+    for (int i = 0 ; i < children.length ; ++i) {
+      if (children[i] instanceof BasePlot) {
+        final Algorithm component_algorithm =
+          ((BasePlot)children[i]).getAlgorithm ();
+        if (component_algorithm == algorithm) {
+          return (JComponent)children[i];
+        }
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   *  Create a Plot component for the given Algorithm and then make a button
+   *  for it so that it can be shown and hidden.  Note that button making is
+   *  currently disabled
+   *  @param algorithm The Algorithm to create a Plot of.
+   *  @param gridbag The GridBagLayout to use to lay out the Plot components.
+   *  @param constraints The GridBagConstraints object to use to lay out the
+   *    Plot components.
+   *  @return The new BasePlot
+   **/
+  private BasePlot makePlot (BaseAlgorithm algorithm,
+                             GridBagLayout gridbag,
+                             GridBagConstraints constraints) {
+
+    final BasePlot new_base_plot =
+      new BasePlot (algorithm, getSelection (), getGotoEventSource ());
+
+    gridbag.setConstraints (new_base_plot, constraints);
+    add (new_base_plot);
+    new_base_plot.setVisible (false);
+
+    getSelection ().addSelectionChangeListener (new_base_plot);
+
+    if (getParent () != null) {
+      // XXX change to revalidate().
+      getParent ().validate ();
+    }
+
+    return new_base_plot;
+  }
+
+  /**
+   *  Return the Selection object that was passed to the constructor.
+   **/
+  private Selection getSelection () {
+    return selection;
+  }
+
+  /**
+   *  Return the EntryGroup object that was passed to the constructor.
+   **/
+  private EntryGroup getEntryGroup () {
+    return entry_group;
+  }
+
+  /**
+   *  Return the GotoEventSource object that was passed to the constructor.
+   **/
+  private GotoEventSource getGotoEventSource () {
+    return goto_event_source;
+  }
+
+  /**
+   *  The EntryGroup that contains the sequence that this JComponent is
+   *  displaying.
+   **/
+  private final EntryGroup entry_group;
+
+  /**
+   *  This array contains the Algorithm objects of the BasePlot components in
+   *  this BasePlotGroup, as set by the constructor.
+   **/
+  private final Vector plot_value_producers = new Vector ();
+
+  /**
+   *  The layout object used by this component.
+   **/
+  private final GridBagLayout gridbag = new GridBagLayout();
+
+  /**
+   *  The constraints object used by this component.
+   **/
+  private final GridBagConstraints c = new GridBagConstraints();
+
+  /**
+   *  The Selection that was passed to the constructor.
+   **/
+  private Selection selection;
+
+  /**
+   *  The GotoEventSource that was passed to the constructor.
+   **/
+  private GotoEventSource goto_event_source;
+
+  /**
+   *  This is a reference to the parent Container of this BasePlot object.
+   **/
+  private Component owning_component;
+}
diff --git a/uk/ac/sanger/artemis/components/BioJavaEntrySource.java b/uk/ac/sanger/artemis/components/BioJavaEntrySource.java
new file mode 100644
index 000000000..8f9a64ce7
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/BioJavaEntrySource.java
@@ -0,0 +1,223 @@
+/* BioJavaEntrySource.java
+ *
+ * created: Tue Apr 10 2001
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/BioJavaEntrySource.java,v 1.1 2004-06-09 09:46:06 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.net.*;
+import java.io.*;
+import java.util.*;
+import java.lang.RuntimeException;
+
+import org.biojava.bio.program.das.*;
+import org.biojava.bio.BioException;
+import org.biojava.bio.seq.io.SequenceBuilderFactory;
+import org.biojava.bio.seq.io.EmblProcessor;
+import org.biojava.bio.seq.io.SmartSequenceBuilder;
+import org.biojava.bio.seq.io.SymbolTokenization;
+import org.biojava.bio.seq.io.StreamReader;
+import org.biojava.bio.seq.io.SequenceFormat;
+import org.biojava.bio.seq.io.EmblLikeFormat;
+import org.biojava.bio.seq.SequenceIterator;
+import org.biojava.bio.symbol.Alphabet;
+import org.biojava.bio.seq.DNATools;
+
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.io.BioJavaEntry;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.SimpleEntryInformation;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+/**
+ *  This is an EntrySource that reads Entry objects from a BioJava Sequence
+ *  object.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: BioJavaEntrySource.java,v 1.1 2004-06-09 09:46:06 tjc Exp $
+ **/
+
+public class BioJavaEntrySource implements EntrySource 
+{
+  /**
+   *  Create a new BioJavaEntrySource.
+   **/
+  public BioJavaEntrySource () {
+
+  }
+
+  /**
+   *  Get an Entry object from this source (by reading from a file, reading
+   *  from a CORBA server, or whatever).
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @return null if and only if the read is cancelled by the user or if the
+   *    read fails.
+   **/
+    public Entry getEntry (final Bases bases, final ProgressThread progress_thread,
+                           final boolean show_progress)
+        throws OutOfRangeException, IOException
+    {
+      final String fileName = "/nfs/team81/kmr/pow/java2/AB000095.embl";
+      final FileDocument document = new FileDocument (new File (fileName));
+
+      final BioJavaEntry emblEntry =
+        new BioJavaEntry (document, new EmblLikeFormat ());
+
+      return new Entry (bases, emblEntry);
+    }
+
+  /**
+   *  Get an Entry object from this source (by reading from a file, reading
+   *  from a CORBA server, or whatever).
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @return null if and only if the read is cancelled by the user or if the
+   *    read fails.
+   **/
+    public Entry getEntry (final Bases bases, final boolean show_progress)
+        throws OutOfRangeException, IOException
+    {
+      return getEntry(bases, null, show_progress);
+    }
+
+  /**
+   *  Get an Entry object from this source (by reading from a file, reading
+   *  from a CORBA server, or whatever).  A Bases object will be created for
+   *  the sequence of the new Entry.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if the entry that we read has no
+   *    sequence.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+    public Entry getEntry(final boolean show_progress, 
+                          final ProgressThread progress_thread)
+        throws OutOfRangeException, NoSequenceException, IOException
+    {
+      return getEntry(show_progress);
+    }
+
+  /**
+   *  Get an Entry object from this source (by reading from a file, reading
+   *  from a CORBA server, or whatever).  A Bases object will be created for
+   *  the sequence of the new Entry.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if the entry that we read has no
+   *    sequence.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+    public Entry getEntry (final boolean show_progress)
+        throws OutOfRangeException, NoSequenceException, IOException
+    {
+//       DASSequence dasSeq = null;
+
+//       try {
+//         final URL dbURL =
+//           new URL("http://genome.ornl.gov/das/das.cgi/ecoli");
+// //        new URL("http://genome.cse.ucsc.edu:80/cgi-bin/das/cb1");
+// //        new URL("http://servlet.sanger.ac.uk:8080/das/ens1131cds/");
+//         final DASSequenceDB dasDB = new DASSequenceDB (dbURL);
+        
+//         final Set ids = dasDB.ids();
+
+//         for (Iterator i = ids.iterator(); i.hasNext(); ) {
+//           System.out.println(i.next().toString());
+//         }
+        
+//         // dasSeq = (DASSequence) dasDB.getSequence((String) ids.iterator().next());
+        
+//         SequenceIterator si = dasDB.sequenceIterator();
+        
+//         if (si.hasNext()) {
+//           dasSeq = (DASSequence) si.nextSequence();
+          
+//           System.err.println("Got: " + dasSeq);
+
+//           final BioJavaEntry bioJavaEntry = new BioJavaEntry (dasSeq);
+
+//           return new Entry (bioJavaEntry);
+//         } else {
+//           System.err.println("No more sequences...");
+//         }
+
+        
+//         //       final BioJavaEntry emblEntry =
+//         //         new BioJavaEntry (document, new EmblLikeFormat ());
+        
+//         //      return new Entry (emblEntry);
+//       } catch (MalformedURLException mue) {
+//           mue.printStackTrace();
+//       } catch (BioException be) {
+//           be.printStackTrace();
+//       }
+
+//       return null;
+
+      final String fileName = "/nfs/team81/kmr/pow/java2/AE002734.game";
+      final FileDocument document = new FileDocument (new File (fileName));
+      
+      final BioJavaEntry emblEntry =
+        new BioJavaEntry (document, new EmblLikeFormat ());
+      
+      return new Entry (emblEntry);
+
+//       final String fileName = "/nfs/team81/kmr/pow/java2/AB000095.embl";
+//       final FileDocument document = new FileDocument (new File (fileName));
+      
+//       final BioJavaEntry emblEntry =
+//         new BioJavaEntry (document, new EmblLikeFormat ());
+      
+//       return new Entry (emblEntry);
+    }
+
+  /**
+   *  Returns true if and only if this EntrySource always returns "full"
+   *  entries.  ie. entries that contain features and sequence.
+   **/
+  public boolean isFullEntrySource () {
+    return true;
+  }
+
+  /**
+   *  Return the name of this source (for display to the user in menus).
+   **/
+  public String getSourceName () {
+    return "BioJava";
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/CanvasPanel.java b/uk/ac/sanger/artemis/components/CanvasPanel.java
new file mode 100644
index 000000000..cd0c1386a
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/CanvasPanel.java
@@ -0,0 +1,240 @@
+/* CanvasPanel.java
+ *
+ * created: Sat Jun 17 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/CanvasPanel.java,v 1.1 2004-06-09 09:46:07 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Options;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+
+/**
+ *  This is a JPanel that contains a JPanel containing a JComponent.  Both Panels
+ *  have BorderLayout.  The JComponent is added at "Center".
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: CanvasPanel.java,v 1.1 2004-06-09 09:46:07 tjc Exp $
+ **/
+
+abstract public class CanvasPanel extends JPanel 
+{
+
+  /**
+   *  Off screen image used for double buffering when drawing the canvas.
+   **/
+  private Image offscreen;
+
+  /** Contains the canvas. */
+  private JPanel mid_panel = null;
+
+  /** The drawing area for this component. */
+  private JComponent canvas = null;
+
+  /** The height of the font used in this component. */
+  private int font_ascent;
+
+  /** maximum height of the font used in this component. */
+  private int font_max_ascent;
+
+  /** descent of the font used in this component. */
+  private int font_descent;
+
+  /** The(maximum) width of the font used in this component. */
+  private int font_width;
+
+  /** base line of the font used in this component. */
+  private int font_base_line;
+
+  /**
+   *  Create a new JPanel(mid_panel) and a JComponent.
+   **/
+  public CanvasPanel() 
+  {
+    setLayout(new BorderLayout());
+    setFontInfo();
+    createCanvas();
+  }
+
+  /**
+   *  Call repaint() on the canvas object.
+   **/
+  protected void repaintCanvas() 
+  {
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Create a JPanel(mid_panel) and a JComponent object.
+   **/
+  private void createCanvas() 
+  {
+    mid_panel = new JPanel();
+    mid_panel.setLayout(new BorderLayout());
+
+    canvas = new JComponent() 
+    {
+      /**
+       *  Set the offscreen buffer to null as part of invalidation.
+       **/
+      public void invalidate() 
+      {
+        super.invalidate();
+        offscreen = null;
+      }
+
+      /**
+       *  Override update to *not* erase the background before painting
+       */
+      public void update(final Graphics g) 
+      {
+        paint(g);
+      }
+
+      /**
+       *  Paint the canvas.
+       */
+      public void paint(final Graphics g) 
+      {
+        final int canvas_width = canvas.getSize().width;
+        final int canvas_height = canvas.getSize().height;
+
+        // there is no point drawing into a zero width canvas
+        if(canvas_height <= 0 || canvas_width <= 0) 
+          return;
+
+        if(!canvas.isVisible()) 
+          return;
+
+        if(offscreen == null) 
+          offscreen = canvas.createImage(canvas_width, canvas_height);
+
+        Graphics og = offscreen.getGraphics();
+        og.setClip(0, 0, canvas_width, canvas_height);
+
+        paintCanvas(og);
+        g.drawImage(offscreen, 0, 0, null);
+        g.dispose();
+      }
+    };
+
+    mid_panel.add(canvas, "Center");
+    add(mid_panel, "Center");
+  }
+
+  /**
+   *  Return the JComponent that was created by createCanvas().
+   **/
+  protected JComponent getCanvas() 
+  {
+    return canvas;
+  }
+
+  /**
+   *  Returns the sub-JPanel that contains the JComponent.
+   **/
+  protected JPanel getMidPanel() 
+  {
+    return mid_panel;
+  }
+
+  /**
+   *  Called by canvas.paint() when
+   **/
+  abstract protected void paintCanvas(final Graphics graphics);
+
+  /**
+   *  Set font_width and font_ascent from the default font.
+   **/
+  private void setFontInfo() 
+  {
+    FontMetrics fm = getFontMetrics(getFont());
+
+    // find the width of a wide character
+    font_width = fm.charWidth('M');
+    font_ascent = fm.getAscent();
+    font_max_ascent = fm.getMaxAscent();
+    font_descent = fm.getDescent();
+  }
+
+  /**
+   *  Return the width of the canvas.
+   **/
+  public int getCanvasWidth() 
+  {
+    return getCanvas().getSize().width;
+  }
+
+  /**
+   *  Return the height of the canvas.
+   **/
+  public int getCanvasHeight() 
+  {
+    return getCanvas().getSize().height;
+  }
+
+  /**
+   *  Return the width of our font, as calculated by setFontInfo().
+   **/
+  public int getFontWidth() 
+  {
+    return font_width;
+  }
+
+  /**
+   *  Return the ascent(height above the baseline) of our font, as calculated
+   *  by setFontInfo().
+   **/
+  public int getFontAscent() 
+  {
+    return font_ascent;
+  }
+
+  /**
+   *  Return the max ascent(height above the baseline) of our font, as
+   *  calculated by setFontInfo().
+   **/
+  public int getFontMaxAscent() 
+  {
+    return font_ascent;
+  }
+
+  /**
+   *  Return the descent of our font, as calculated by setFontInfo().
+   **/
+  public int getFontDescent() 
+  {
+    return font_descent;
+  }
+
+  /**
+   *  The max ascent + descent of the default font.
+   **/
+  public int getFontHeight() 
+  {
+    return getFontMaxAscent() + getFontDescent();
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/ChoiceFrame.java b/uk/ac/sanger/artemis/components/ChoiceFrame.java
new file mode 100644
index 000000000..e63fba0ed
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ChoiceFrame.java
@@ -0,0 +1,111 @@
+/* ChoiceFrame.java
+ *
+ * created: Tue Aug  6 2002
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ChoiceFrame.java,v 1.1 2004-06-09 09:46:08 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.util.StringVector;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A Choice in a JFrame.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: ChoiceFrame.java,v 1.1 2004-06-09 09:46:08 tjc Exp $
+ **/
+
+public class ChoiceFrame extends JFrame {
+  /**
+   *  Create a new ChoiceFrame component with the given list of Strings.
+   **/
+  public ChoiceFrame (final String choice_title, final StringVector strings) {
+    super (choice_title);
+
+    choice = new JComboBox ();
+
+    for (int i = 0 ; i < strings.size () ; ++i) {
+      choice.addItem (strings.elementAt (i));
+    }
+
+    final JPanel choice_panel = new JPanel ();
+    choice_panel.add (choice);
+    
+    getContentPane ().add (choice_panel, "Center");
+
+    final JPanel panel = new JPanel ();
+
+    panel.add (ok_button);
+    ok_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        ChoiceFrame.this.dispose ();
+      }
+    });
+
+    panel.add (close_button);
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        ChoiceFrame.this.dispose ();
+      }
+    });
+
+    getContentPane ().add (panel, "South");
+    pack ();
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        ChoiceFrame.this.dispose ();
+      }
+    });
+
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+  }
+
+  /**
+   *  Return the Choice component that is displayed in this JFrame.
+   **/
+  public JComboBox getChoice () {
+    return choice;
+  }
+
+  /**
+   *  Return the reference of the OK button of this Chooser.
+   **/
+  public JButton getOKButton () {
+    return ok_button;
+  }
+
+  private JComboBox choice;
+
+  final private JButton ok_button = new JButton ("OK");
+
+  final private JButton close_button = new JButton ("Cancel");
+}
diff --git a/uk/ac/sanger/artemis/components/ComparatorDialog.java b/uk/ac/sanger/artemis/components/ComparatorDialog.java
new file mode 100644
index 000000000..6b02d052e
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ComparatorDialog.java
@@ -0,0 +1,331 @@
+/* ComparatorDialog.java
+ *
+ * created: Wed May 10 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ComparatorDialog.java,v 1.1 2004-06-09 09:46:09 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+import uk.ac.sanger.artemis.util.InputStreamProgressListener;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  ComparatorDialog a dialog that allows the user to the choose two files to
+ *  compare with a Comparator component and a file containing comparison data.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: ComparatorDialog.java,v 1.1 2004-06-09 09:46:09 tjc Exp $
+ **/
+
+public class ComparatorDialog extends JFrame 
+{
+  /**
+   *  Create a dialog that allow the user to the choose two files to compare
+   *  and a file containing comparison data.
+   *  @param ActMain the object to call makeMultiComparator() on
+   **/
+  public ComparatorDialog(final ActMain act_main)
+  {
+    final JPanel top_panel = new JPanel();
+
+    getContentPane().add(top_panel, "Center");
+
+    final GridBagLayout gridbag = new GridBagLayout();
+    top_panel.setLayout(gridbag);
+
+    final Vector text_field_vector = new Vector();
+
+    for(int i = 0; i < 3; ++i) 
+    {
+      final String label;
+      switch(i) 
+      {
+        case 0:
+          label = "Sequence file 1";
+          break;
+        case 1:
+          label = "Comparison file 1";
+          break;
+        case 2:
+          label = "Sequence file 2";
+          break;
+        default:
+          throw new Error("internal error");
+      }
+
+      final JTextField text_field =
+        makeFileNamePanel(label, top_panel, gridbag);
+
+      text_field_vector.addElement(text_field);
+    }
+
+
+    final GridBagConstraints c = new GridBagConstraints();
+    c.fill = GridBagConstraints.HORIZONTAL;
+    c.anchor = GridBagConstraints.NORTH;
+    c.weighty = 0;
+    c.gridwidth = 1;
+
+    final JButton more_button = new JButton("more files ...");
+
+    final JPanel more_button_panel = new JPanel();
+    more_button_panel.setLayout(new FlowLayout(FlowLayout.LEFT));
+    more_button_panel.add(more_button);
+
+    c.gridwidth = 1;
+    gridbag.setConstraints(more_button_panel, c);
+    top_panel.add(more_button_panel);
+
+    more_button.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent e) 
+      {
+        top_panel.remove(more_button_panel);
+
+        final boolean add_sequence_flag;
+
+        if(text_field_vector.size() % 2 == 0 ||
+            Options.getOptions().isNoddyMode()) 
+          add_sequence_flag = true;
+        else 
+          add_sequence_flag = false;
+
+        if(text_field_vector.size() % 2 == 1 ||
+            Options.getOptions().isNoddyMode()) 
+        {
+          final String seq_label =
+            "Comparison file " +(text_field_vector.size() / 2 + 1);
+          final JTextField seq_text_field =
+            makeFileNamePanel(seq_label, top_panel, gridbag);
+
+          text_field_vector.addElement(seq_text_field);
+        }
+
+        if(add_sequence_flag) 
+        {
+          final String comp_label =
+            "Sequence file " +(text_field_vector.size() / 2 + 1);
+          final JTextField comp_text_field =
+            makeFileNamePanel(comp_label, top_panel, gridbag);
+
+          text_field_vector.addElement(comp_text_field);
+        }
+
+        top_panel.add(more_button_panel);
+
+        packAndCentre();
+      }
+    });
+
+
+    final JButton apply_button = new JButton("Apply");
+
+    apply_button.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent e) 
+      {
+        doApply(act_main, text_field_vector);
+      }
+    });
+
+
+    final JButton close_button = new JButton("Close");
+
+    close_button.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent e) 
+      {
+        ComparatorDialog.this.dispose();
+      }
+    });
+
+
+    final FlowLayout flow_layout =
+      new FlowLayout(FlowLayout.CENTER, 15, 5);
+
+    final JPanel close_and_apply_panel = new JPanel(flow_layout);
+
+    close_and_apply_panel.add(apply_button);
+    close_and_apply_panel.add(close_button);
+
+    getContentPane().add(close_and_apply_panel, "South");
+
+
+    addWindowListener(new WindowAdapter() 
+    {
+      public void windowClosing(WindowEvent event) 
+      {
+        ComparatorDialog.this.dispose();
+      }
+    });
+
+    packAndCentre();
+  }
+
+  /**
+   *  Call pack() and then centre the JFrame in the middle of the screen.
+   **/
+  private void packAndCentre() 
+  {
+    pack();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation(new Point((screen.width - getSize().width) / 2,
+                           (screen.height - getSize().height) / 2));
+  }
+
+  /**
+   *  Make a panel for choosing one file name panel.
+   **/
+  private JTextField makeFileNamePanel(final String label_string,
+                                       final JPanel parent_panel,
+                                       final GridBagLayout gridbag) 
+  {
+    GridBagConstraints c = new GridBagConstraints();
+
+    c.fill = GridBagConstraints.HORIZONTAL;
+    c.anchor = GridBagConstraints.NORTH;
+    c.weighty = 0;
+
+    final JLabel label = new JLabel(label_string);
+
+    final JPanel panel = new JPanel();
+    panel.setLayout(new FlowLayout(FlowLayout.LEFT));
+    panel.add(label);
+
+    c.gridwidth = 1;
+    gridbag.setConstraints(panel, c);
+    parent_panel.add(panel);
+
+    final TextFieldSink text_field = new TextFieldSink("", 28);
+    gridbag.setConstraints(text_field, c);
+    parent_panel.add(text_field);
+
+    final JButton choose_button = new JButton("Choose ...");
+
+    choose_button.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent _) 
+      {
+        final StickyFileChooser file_dialog =
+          new StickyFileChooser();
+
+        file_dialog.setDialogTitle("Choose first sequence ...");
+        file_dialog.setFileSelectionMode(JFileChooser.FILES_ONLY);
+        file_dialog.setDialogType(JFileChooser.OPEN_DIALOG);
+
+        file_dialog.showOpenDialog(ComparatorDialog.this);
+
+        if(file_dialog.getSelectedFile() != null) 
+        {
+          final File selected_file =
+            new File(file_dialog.getCurrentDirectory(),
+                      file_dialog.getSelectedFile().getName());
+
+          text_field.setText(selected_file.toString());
+        }
+      }
+    });
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints(choose_button, c);
+    parent_panel.add(choose_button);
+
+    return text_field;
+  }
+
+  /**
+   *  Attempt to call ActMain.makeComparator() for the given file names.
+   *  @param ActMain the object to call makeMultiComparator() on
+   **/
+  private void doApply(final ActMain act_main,
+                        final Vector text_field_vector) 
+  {
+    if(text_field_vector.size() < 3) 
+      throw new Error("internal error - not enough file names given to " +
+                       "ComparatorDialog.doApply()");
+
+    String [] file_names = new String [text_field_vector.size()];
+
+    for(int i = 0; i < text_field_vector.size(); ++i) 
+    {
+      file_names[i] =
+       ((JTextField) text_field_vector.elementAt(i)).getText().trim();
+
+      if(file_names[i].length() == 0) 
+      {
+        // ignore the problem if there are at least 3 files listed(ie. seq1
+        // comp1v2 seq2) and the remaining files lengths are zero
+        if(i > 3)
+        {
+          // set to true if a text field is found that isn't zero length
+          boolean found_file = false;
+
+          for(int sub_index = i; sub_index < text_field_vector.size();
+              ++sub_index) 
+          {
+            final JTextField text_field =
+             (JTextField) text_field_vector.elementAt(sub_index);
+
+            final String this_text = text_field.getText().trim();
+
+            if(this_text.length() != 0) 
+              found_file = true;
+          }
+
+          if(!found_file)
+          {
+            // truncate file_names
+            final String [] new_file_names = new String [i];
+            System.arraycopy(file_names, 0, new_file_names, 0, i);
+            file_names = new_file_names;
+            break;
+          }
+        }
+
+        new MessageDialog(this, "one of the file names is missing");
+        return;
+      }
+    }
+
+    final MessageFrame reading_message = new MessageFrame("reading ...");
+
+    final InputStreamProgressListener progress_listener =
+      act_main.getInputStreamProgressListener();
+
+    if(ActMain.makeMultiComparator(act_main, progress_listener,
+                                   file_names)) 
+      ComparatorDialog.this.dispose();
+    
+    reading_message.dispose();
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/ComparatorGlue.java b/uk/ac/sanger/artemis/components/ComparatorGlue.java
new file mode 100644
index 000000000..8da6a0ab9
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ComparatorGlue.java
@@ -0,0 +1,464 @@
+/* ComparatorGlue.java
+ *
+ * created: Tue Sep 11 2001
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ComparatorGlue.java,v 1.1 2004-06-09 09:46:10 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.util.ReadOnlyException;
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+
+import java.util.*;
+import java.awt.*;
+
+import javax.swing.*;
+
+/**
+ *  This class contains the Event glue needed to combine two FeatureDisplays
+ *  and an AlignmentViewer.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: ComparatorGlue.java,v 1.1 2004-06-09 09:46:10 tjc Exp $
+ **/
+
+public class ComparatorGlue {
+  /**
+   *  Create a new ComparatorGlue object that glues the given components
+   *  togeather.
+   **/
+  public ComparatorGlue (final JFrame parent_frame,
+                         final FeatureDisplay subject_feature_display,
+                         final FeatureDisplay query_feature_display,
+                         final AlignmentViewer alignment_viewer) {
+    this.parent_frame = parent_frame;
+    this.subject_feature_display = subject_feature_display;
+    this.query_feature_display = query_feature_display;
+    this.alignment_viewer = alignment_viewer;
+
+    getSubjectEntryGroup ().addFeatureChangeListener (getSubjectSelection ());
+    getQueryEntryGroup ().addEntryChangeListener (getQuerySelection ());
+
+    addDisplayListeners (subject_feature_display, query_feature_display);
+
+    orig_subject_forward_strand =
+      getSubjectEntryGroup ().getBases ().getForwardStrand ();
+    orig_query_forward_strand =
+      getQueryEntryGroup ().getBases ().getForwardStrand ();
+
+    makeAlignmentEventListener ();
+  }
+
+  /**
+   *  Wire-up the two FeatureDisplay objects with DisplayAdjustmentListeners.
+   **/
+  private void addDisplayListeners (final FeatureDisplay subject_display,
+                                    final FeatureDisplay query_display) {
+
+    subject_listener = new DisplayAdjustmentListener () {
+      public void displayAdjustmentValueChanged (DisplayAdjustmentEvent e) {
+        if (e.getType () == DisplayAdjustmentEvent.REV_COMP_EVENT) {
+          getAlignmentViewer ().unlockDisplays ();
+          return;
+        }
+
+        subject_display.removeDisplayAdjustmentListener (subject_listener);
+        query_display.removeDisplayAdjustmentListener (query_listener);
+
+        query_display.setScaleFactor (e.getScaleFactor ());
+
+        if (getAlignmentViewer ().displaysAreLocked () &&
+            (e.getType () == DisplayAdjustmentEvent.SCROLL_ADJUST_EVENT ||
+             e.getType () == DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT)) {
+          final int difference = e.getStart () - subject_first_base_position;
+
+          query_first_base_position =
+            query_display.getForwardBaseAtLeftEdge () + difference;
+
+          query_display.setFirstBase (query_first_base_position);
+        }
+
+        subject_first_base_position = e.getStart ();
+
+        subject_display.addDisplayAdjustmentListener (subject_listener);
+        query_display.addDisplayAdjustmentListener (query_listener);
+      }
+    };
+
+    query_listener = new DisplayAdjustmentListener () {
+      public void displayAdjustmentValueChanged (DisplayAdjustmentEvent e) {
+        if (e.getType () == DisplayAdjustmentEvent.REV_COMP_EVENT) {
+          getAlignmentViewer ().unlockDisplays ();
+          return;
+        }
+
+        subject_display.removeDisplayAdjustmentListener (subject_listener);
+        query_display.removeDisplayAdjustmentListener (query_listener);
+
+        subject_display.setScaleFactor (e.getScaleFactor ());
+
+        if (getAlignmentViewer ().displaysAreLocked () &&
+            (e.getType () == DisplayAdjustmentEvent.SCROLL_ADJUST_EVENT ||
+             e.getType () == DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT)) {
+          final int difference = e.getStart () - query_first_base_position;
+
+          subject_first_base_position =
+            subject_display.getForwardBaseAtLeftEdge () + difference;
+
+          subject_display.setFirstBase (subject_first_base_position);
+        }
+
+        query_first_base_position = e.getStart ();
+
+        subject_display.addDisplayAdjustmentListener (subject_listener);
+        query_display.addDisplayAdjustmentListener (query_listener);
+      }
+    };
+
+    subject_display.addDisplayAdjustmentListener (subject_listener);
+    query_display.addDisplayAdjustmentListener (query_listener);
+
+    final DisplayAdjustmentListener subject_align_listener =
+      new DisplayAdjustmentListener () {
+        public void displayAdjustmentValueChanged (DisplayAdjustmentEvent e) {
+          getAlignmentViewer ().setSubjectSequencePosition (e);
+        }
+      };
+
+    subject_display.addDisplayAdjustmentListener (subject_align_listener);
+
+    final DisplayAdjustmentListener query_align_listener =
+      new DisplayAdjustmentListener () {
+        public void displayAdjustmentValueChanged (DisplayAdjustmentEvent e) {
+          getAlignmentViewer ().setQuerySequencePosition (e);
+        }
+      };
+
+    query_display.addDisplayAdjustmentListener (query_align_listener);
+  }
+
+  /**
+   *  Make an AlignmentListener which calls alignAt ().
+   **/
+  private void makeAlignmentEventListener () {
+    final AlignmentListener listener =
+      new AlignmentListener () {
+        public void alignMatchChosen (AlignmentEvent e) {
+          alignAt (e.getMatch ());
+        }
+      };
+
+    getAlignmentViewer ().addAlignmentListener (listener);
+  }
+
+  /**
+   *  Scroll both the subject and query so that they centre on the given
+   *  AlignMatch.
+   **/
+  private void alignAt (final AlignMatch align_match) {
+    getAlignmentViewer ().unlockDisplays ();
+    getAlignmentViewer ().disableSelection ();
+
+    maybeRevCompQuery (align_match);
+
+    final int subject_length = getSubjectForwardStrand ().getSequenceLength ();
+    final int query_length = getQueryForwardStrand ().getSequenceLength ();
+
+    int subject_sequence_start =
+      AlignmentViewer.getRealSubjectSequenceStart (align_match,
+                                                   subject_length,
+                                                   getAlignmentViewer ().subjectIsRevComp ());
+    int subject_sequence_end =
+      AlignmentViewer.getRealSubjectSequenceEnd (align_match,
+                                                 subject_length,
+                                                 getAlignmentViewer ().subjectIsRevComp ());
+    int query_sequence_start =
+      AlignmentViewer.getRealQuerySequenceStart (align_match,
+                                                 query_length,
+                                                 getAlignmentViewer ().queryIsRevComp ());
+    int query_sequence_end =
+      AlignmentViewer.getRealQuerySequenceEnd (align_match,
+                                               query_length,
+                                               getAlignmentViewer ().queryIsRevComp ());
+
+    if (getSubjectDisplay ().isRevCompDisplay ()) {
+      subject_sequence_start =
+        getSubjectDisplay ().getSequenceLength () - subject_sequence_start + 1;
+      subject_sequence_end =
+        getSubjectDisplay ().getSequenceLength () - subject_sequence_end + 1;
+    }
+
+    if (getQueryDisplay ().isRevCompDisplay ()) {
+      query_sequence_start =
+        getQueryDisplay ().getSequenceLength () - query_sequence_start + 1;
+      query_sequence_end =
+        getQueryDisplay ().getSequenceLength () - query_sequence_end + 1;
+    }
+
+    final int new_subject_base =
+      subject_sequence_start +
+      (subject_sequence_end - subject_sequence_start) / 2;
+    getSubjectDisplay ().makeBaseVisible (new_subject_base);
+
+    try {
+      final Strand subject_strand;
+
+      if (align_match.isRevMatch () ^
+          !getOrigSubjectForwardStrand ().isForwardStrand ()) {
+        subject_strand = getSubjectReverseStrand ();
+      } else {
+        subject_strand = getSubjectForwardStrand ();
+      }
+
+      final MarkerRange new_subject_marker =
+        subject_strand.makeMarkerRangeFromRawPositions (subject_sequence_start,
+                                                        subject_sequence_end);
+      getSubjectSelection ().setMarkerRange (new_subject_marker);
+    } catch (OutOfRangeException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+
+    final int new_query_base =
+      query_sequence_start +
+      (query_sequence_end - query_sequence_start) / 2;
+    getQueryDisplay ().makeBaseVisible (new_query_base);
+
+    try {
+      final Strand query_strand;
+
+      if (getOrigQueryForwardStrand ().isForwardStrand ()) {
+        query_strand = getQueryForwardStrand ();
+      } else {
+        query_strand = getQueryReverseStrand ();
+      }
+
+      final MarkerRange new_query_marker_range =
+        query_strand.makeMarkerRangeFromRawPositions (query_sequence_start,
+                                                      query_sequence_end);
+
+      getQuerySelection ().setMarkerRange (new_query_marker_range);
+    } catch (OutOfRangeException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+
+    getAlignmentViewer ().lockDisplays ();
+    getAlignmentViewer ().enableSelection ();
+  }
+
+  /**
+   *  Called by alignAt () to reverse and complement the query sequence if the
+   *  given AlignMatch is currently a match from the subject sequence to the
+   *  reverse complement of the query sequence.
+   **/
+  private void maybeRevCompQuery (final AlignMatch align_match) {
+    if (!getAlignmentViewer ().offerToFlip ()) {
+      return;
+    }
+
+    // true if and only if exactly one of the query and subject is rev-comped
+    final boolean display_is_rev_comped;
+
+    if (getOrigSubjectForwardStrand ().isForwardStrand () ^
+        getOrigQueryForwardStrand ().isForwardStrand () ^
+        getSubjectDisplay ().isRevCompDisplay () ^
+        getQueryDisplay ().isRevCompDisplay ()) {
+      display_is_rev_comped = true;
+    } else {
+      display_is_rev_comped = false;
+    }
+
+    if (align_match.isRevMatch () ^ display_is_rev_comped) {
+      final YesNoDialog yes_no_dialog =
+        new YesNoDialog (parent_frame,
+                         "reverse and complement query sequence display?");
+      if (yes_no_dialog.getResult ()) {
+        if (getQueryDisplay ().isRevCompDisplay ()) {
+          getQueryDisplay ().setRevCompDisplay (false);
+        } else {
+          getQueryDisplay ().setRevCompDisplay (true);
+        }
+      }
+    }
+  }
+
+  /**
+   *  Send an event to those object listening for it.
+   *  @param listeners A Vector of the objects that the event should be sent
+   *    to.
+   *  @param event The event to send
+   **/
+  private void fireAction (final Vector listeners, final EventObject event) {
+    final Vector targets;
+    // copied from a book - synchronising the whole method might cause a
+    // deadlock
+    synchronized (this) {
+      targets = (Vector) listeners.clone ();
+    }
+
+    for (int i = 0 ; i < targets.size () ; ++i) {
+      GotoListener target = (GotoListener) targets.elementAt (i);
+
+      if (event instanceof GotoEvent) {
+        final GotoListener goto_listener = (GotoListener) target;
+        goto_listener.performGoto ((GotoEvent) event);
+      } else {
+        throw new Error ("EntryEdit.fireAction () - unknown event");
+      }
+    }
+  }
+
+  /**
+   *  Return the forward Strand of the subject EntryGroup from when the
+   *  Comparator was created.
+   **/
+  public Strand getOrigSubjectForwardStrand () {
+    return orig_subject_forward_strand;
+  }
+
+  /**
+   *  Return the forward Strand of the query EntryGroup from when the
+   *  Comparator was created.
+   **/
+  public Strand getOrigQueryForwardStrand () {
+    return orig_query_forward_strand;
+  }
+
+  /**
+   *  Return the GotoEventSource object used by the subject FeatureDisplay.
+   **/
+  public GotoEventSource getSubjectGotoEventSource () {
+    return getSubjectDisplay ().getGotoEventSource ();
+  }
+
+  /**
+   *  Return the GotoEventSource object used by the query FeatureDisplay.
+   **/
+  public GotoEventSource getQueryGotoEventSource () {
+    return getQueryDisplay ().getGotoEventSource ();
+  }
+
+  /**
+   *  Return the reference of the EntryGroup in view in the subject
+   *  FeatureDisplay.
+   **/
+  public EntryGroup getSubjectEntryGroup () {
+    return getSubjectDisplay ().getEntryGroup ();
+  }
+
+  /**
+   *  Return the reference of the EntryGroup in view in the query
+   *  FeatureDisplay.
+   **/
+  public EntryGroup getQueryEntryGroup () {
+    return getQueryDisplay ().getEntryGroup ();
+  }
+
+  /**
+   *  Return the Selection object used by the subject FeatureDisplay.
+   **/
+  public Selection getSubjectSelection () {
+    return getSubjectDisplay ().getSelection ();
+  }
+
+  /**
+   *  Return the Selection object used by the query FeatureDisplay.
+   **/
+  public Selection getQuerySelection () {
+    return getQueryDisplay ().getSelection ();
+  }
+
+  /**
+   *  Return the AlignmentViewer that was created in the constructor.
+   **/
+  private AlignmentViewer getAlignmentViewer () {
+    return alignment_viewer;
+  }
+
+  /**
+   *  Return the reference of the subject FeatureDisplay.
+   **/
+  public FeatureDisplay getSubjectDisplay () {
+    return subject_feature_display;
+  }
+
+  /**
+   *  Return the reference of the query FeatureDisplay.
+   **/
+  public FeatureDisplay getQueryDisplay () {
+    return query_feature_display;
+  }
+
+  /**
+   *  Return the current forward Strand of the subject EntryGroup.
+   **/
+  private Strand getSubjectForwardStrand () {
+    return getSubjectEntryGroup ().getBases ().getForwardStrand ();
+  }
+
+  /**
+   *  Return the current forward Strand of the query EntryGroup.
+   **/
+  private Strand getQueryForwardStrand () {
+    return getQueryEntryGroup ().getBases ().getForwardStrand ();
+  }
+
+  /**
+   *  Return the current reverse Strand of the subject EntryGroup.
+   **/
+  private Strand getSubjectReverseStrand () {
+    return getSubjectEntryGroup ().getBases ().getReverseStrand ();
+  }
+
+  /**
+   *  Return the current reverse Strand of the query EntryGroup.
+   **/
+  private Strand getQueryReverseStrand () {
+    return getQueryEntryGroup ().getBases ().getReverseStrand ();
+  }
+
+  final private JFrame parent_frame;
+  final private FeatureDisplay subject_feature_display;
+  final private FeatureDisplay query_feature_display;
+  final private AlignmentViewer alignment_viewer;
+
+  private int subject_first_base_position = 1;
+  private int query_first_base_position = 1;
+
+  private DisplayAdjustmentListener subject_listener = null;
+  private DisplayAdjustmentListener query_listener = null;
+
+  /**
+   *  The forward Strand of the subject EntryGroup when the Comparator was
+   *  created.
+   **/
+  final private Strand orig_subject_forward_strand;
+
+  /**
+   *  The forward Strand of the query EntryGroup when the Comparator was
+   *  created.
+   **/
+  final private Strand orig_query_forward_strand;
+
+}
diff --git a/uk/ac/sanger/artemis/components/CorbaEntrySource.java b/uk/ac/sanger/artemis/components/CorbaEntrySource.java
new file mode 100644
index 000000000..2c1950df2
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/CorbaEntrySource.java
@@ -0,0 +1,90 @@
+/* CorbaEntrySource.java
+ *
+ * created: Wed Jun  7 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/CorbaEntrySource.java,v 1.1 2004-06-09 09:46:11 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.io.*;
+import java.net.*;
+import java.awt.*;
+
+import javax.swing.*;
+
+/**
+ *  This class contains the methods common to all EntrySource implementations
+ *  that read from a CORBA server.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: CorbaEntrySource.java,v 1.1 2004-06-09 09:46:11 tjc Exp $
+ **/
+
+abstract public class CorbaEntrySource {
+  /**
+   *  Create a new CorbaEntrySource using the given URL as the location of the
+   *  server IOR.
+   *  @param frame The component that created this EntrySource.  (Used for
+   *    requesters.)
+   *  @param ior_url_string A String containing the URL of the IOR for the
+   *    server.
+   **/
+  public CorbaEntrySource (final JFrame frame,
+                           final String ior_url_string)
+      throws MalformedURLException //, IOException
+  {
+    this.frame = frame;
+    this.ior_url = new URL (ior_url_string);
+  }
+
+  /**
+   *  Finds and returns a stringified object reference (IOR) for a CORBA
+   *  server.  The IOR is read from the URL that was passed to the
+   *  constructor.
+   *  @return The stringified IOR.
+   *  @exception IOException Thrown if an error occurs while reading.
+   **/
+  public String getIOR ()
+      throws IOException {
+    //  get URL of IOR
+    final BufferedReader in =
+      new BufferedReader (new InputStreamReader (ior_url.openStream()));
+    return in.readLine();
+  }
+
+  /**
+   *  Return the JFrame that was passed to the constructor.
+   **/
+  public JFrame getFrame () {
+    return frame;
+  }
+
+  /**
+   *  The URL containing the IOR of the CORBA server.
+   **/
+  private URL ior_url = null;
+
+  /**
+   *  The JFrame that was passed to the constructor.
+   **/
+  private JFrame frame = null;
+}
diff --git a/uk/ac/sanger/artemis/components/DbfetchEntrySource.java b/uk/ac/sanger/artemis/components/DbfetchEntrySource.java
new file mode 100644
index 000000000..435af2e29
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/DbfetchEntrySource.java
@@ -0,0 +1,253 @@
+/* DbfetchEntrySource.java
+ *
+ * created: Fri Nov 28 2003
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2003  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/DbfetchEntrySource.java,v 1.1 2004-06-09 09:46:12 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.util.Document;
+import uk.ac.sanger.artemis.util.DocumentFactory;
+import uk.ac.sanger.artemis.io.DocumentEntryFactory;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+import uk.ac.sanger.artemis.io.SimpleEntryInformation;
+
+import java.io.*;
+import javax.swing.*;
+
+/**
+ *  This is an EntrySource that reads Entry objects from the EMBL Dbfetch
+ *  server.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: DbfetchEntrySource.java,v 1.1 2004-06-09 09:46:12 tjc Exp $
+ **/
+
+public class DbfetchEntrySource
+    implements EntrySource 
+{
+  /**
+   *  Create a new DbfetchEntrySource.
+   *  @param frame The component that created this EntrySource.  (Used for
+   *    requesters.)
+   **/
+  public DbfetchEntrySource (final JFrame frame) 
+  {
+
+  }
+
+  /**
+   *  Get an Entry object from the Ensembl Dbfetch server.
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final Bases bases, ProgressThread progress_thread,
+                         final boolean show_progress)
+      throws OutOfRangeException, IOException
+  {
+    return getEntry(bases,show_progress,progress_thread);
+  }
+
+  /**
+   *  Get an Entry object from the Ensembl Dbfetch server.
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final Bases bases, final boolean show_progress)
+      throws OutOfRangeException, IOException
+  {
+    return getEntry(bases,show_progress, null);
+  }
+
+  /**
+   *  Get an Entry object from the Ensembl Dbfetch server.
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final Bases bases, final boolean show_progress,
+                         ProgressThread progress_thread)
+      throws OutOfRangeException, IOException 
+  {
+    final TextDialog text_dialog =
+      new TextDialog (getFrame (), "Enter an accession number:", 10, "");
+
+    final String text = text_dialog.getText ();
+
+    if (text == null) {
+      // user cancel
+      return null;
+    }
+
+    final String embl_id = text.trim ();
+
+    if(embl_id.length () > 0) 
+    {
+      final LogReadListener read_event_logger = new LogReadListener (embl_id);
+
+      final EntryInformation entry_information =
+        new SimpleEntryInformation (Options.getArtemisEntryInformation ());
+
+//    final MessageDialog message_frame =
+//      new MessageDialog (getFrame (),
+//                         "reading entry - please wait", false);
+
+      if(progress_thread != null)
+        progress_thread.start();
+
+      final String url_string =
+        "http://www.ebi.ac.uk/cgi-bin/dbfetch?db=EMBL&id=" + embl_id;
+
+      final Document url_document =
+        DocumentFactory.makeDocument (url_string);
+
+      try
+      {
+        final uk.ac.sanger.artemis.io.Entry new_embl_entry =
+          DocumentEntryFactory.makeDocumentEntry (entry_information,
+                                                  url_document,
+                                                  read_event_logger);
+
+        if (read_event_logger.seenMessage ()) {
+          final YesNoDialog yes_no_dialog =
+            new YesNoDialog (frame,
+                             "there were warnings while reading - view now?");
+
+          if (yes_no_dialog.getResult ()) {
+            Splash.showLog ();
+          }
+        }
+
+        final Bases real_bases;
+
+        if (bases == null) {
+          if (new_embl_entry.getSequence () == null) {
+            final String message =
+              "the entry contains no sequence: " + embl_id;
+            new MessageDialog (getFrame (), message);
+            return null;
+          }
+
+          real_bases = new Bases (new_embl_entry.getSequence ());
+        } else {
+          real_bases = bases;
+        }
+
+        return new Entry (real_bases, new_embl_entry);
+      }
+      catch (EntryInformationException e) 
+      {
+        throw new Error ("internal error - unexpected exception: " + e);
+      } 
+//    finally
+//    {
+//      message_frame.dispose ();
+//    }
+    }
+
+    return null;
+  }
+
+  /**
+   *  Get an Entry object from the Ensembl Dbfetch server.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if the entry that we read has no
+   *    sequence.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final boolean show_progress)
+      throws OutOfRangeException, NoSequenceException, IOException 
+  {
+    return getEntry (null, show_progress);
+  }
+
+  /**
+   *  Get an Entry object from the Ensembl Dbfetch server.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @param progress_thread Progress thread to monitor the entry reading.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if the entry that we read has no
+   *    sequence.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final boolean show_progress, ProgressThread progress_thread)
+      throws OutOfRangeException, NoSequenceException, IOException 
+  {
+    return getEntry (null, show_progress, progress_thread);
+  }
+
+
+  /**
+   *  Return the name of this source (for display to the user in menus).
+   **/
+  public String getSourceName () {
+    return "EBI - Dbfetch";
+  }
+
+  /**
+   *  Returns true if and only if this EntrySource always returns "full"
+   *  entries.  ie. entries that contain features and sequence.  Entries that
+   *  are read from EMBL always contain sequence so in this class this method
+   *  returns false.
+   **/
+  public boolean isFullEntrySource () {
+    return true;
+  }
+
+  /**
+   *  Return the JFrame that was passed to the constructor.
+   **/
+  public JFrame getFrame () {
+    return frame;
+  }
+
+  /**
+   *  The JFrame that was passed to the constructor.
+   **/
+  private JFrame frame = null;
+}
diff --git a/uk/ac/sanger/artemis/components/DisplayAdjustmentEvent.java b/uk/ac/sanger/artemis/components/DisplayAdjustmentEvent.java
new file mode 100644
index 000000000..fa70a88ba
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/DisplayAdjustmentEvent.java
@@ -0,0 +1,186 @@
+/* DisplayAdjustmentEvent.java
+ *
+ * created: Tue Dec 15 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/DisplayAdjustmentEvent.java,v 1.1 2004-06-09 09:46:13 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.ChangeEvent;
+
+/**
+ *  This event is sent when a FeatureDisplay is scrolled.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: DisplayAdjustmentEvent.java,v 1.1 2004-06-09 09:46:13 tjc Exp $
+ **/
+
+public class DisplayAdjustmentEvent extends ChangeEvent 
+{
+
+  /** The new start base, as passed to the constructor. */
+  private int start_base;
+
+  /** The new end base, as passed to the constructor. */
+  private int end_base;
+
+  /**
+   *  The width in bases of the display, as passed to the constructor.
+   **/
+  private int width_in_bases;
+
+  /**
+   *  The width of each base on the display, as passed to the constructor.
+   **/
+  private float base_width;
+
+  /** The scale factor, as passed to the constructor. */
+  private int scale_factor;
+
+  /**
+   *  True if and only if the FeatureDisplay is drawing in
+   *  reverse complement mode.
+   **/
+  private boolean rev_comp_display;
+
+  /**
+   *  The type of event.  One of: SCALE_ADJUST_EVENT, SCROLL_ADJUST_EVENT or
+   *  ALL_CHANGE_ADJUST_EVENT.
+   **/
+  private int type;
+
+  /**
+   *  The type of DisplayAdjustmentEvent where the scale (only) has changed
+   **/
+  final static public int SCALE_ADJUST_EVENT = 0;
+
+  /**
+   *  The type of DisplayAdjustmentEvent where the display has scrolled but
+   *  the scale has stayed the same.
+   **/
+  final static public int SCROLL_ADJUST_EVENT = 1;
+
+  /**
+   *  The type of DisplayAdjustmentEvent where the display has been reverse
+   *  complemented.
+   **/
+  final static public int REV_COMP_EVENT = 2;
+
+  /**
+   *  The type of DisplayAdjustmentEvent where the display has scrolled and
+   *  the the scale has changed.
+   **/
+  final static public int ALL_CHANGE_ADJUST_EVENT = 3;
+
+  /**
+   *  Create a new DisplayAdjustmentEvent.
+   *  @param source The Object that generated the event - probably a component.
+   *  @param start_base The new start base on the display.
+   *  @param end_base The new end base on the display.
+   *  @param width_in_bases The width of the drawing area in bases.  We need
+   *    this because if the user scrolls to the end of the sequence, the last
+   *    visible base (end_base) may not be at the right of the screen.  This
+   *    might be greater than end_base - start_base.
+   *  @param base_width The width in pixels of a base on screen.
+   *  @param scale_factor This is the scale factor use by the FeatureDisplay
+   *    component.  A factor of zero means the full translation will be
+   *    visible.  At higher scale factors only stop codons are visible, and
+   *    a bigger number will mean more bases are visible.
+   *  @param type the type of event: SCALE_ADJUST_EVENT, SCROLL_ADJUST_EVENT
+   *    or ALL_CHANGE_ADJUST_EVENT.
+   **/
+  public DisplayAdjustmentEvent(Object source,
+                                int start_base, int end_base,
+                                int width_in_bases, float base_width,
+                                int scale_factor,
+                                boolean rev_comp_display, int type)
+  {
+    super(source);
+    this.start_base     = start_base;
+    this.end_base       = end_base;
+    this.width_in_bases = width_in_bases;
+    this.base_width     = base_width;
+    this.scale_factor   = scale_factor;
+    this.rev_comp_display = rev_comp_display;
+    this.type           = type;
+  }
+
+  /**
+   *  Return the new start base to display, as passed to the constructor.
+   **/
+  public int getStart() 
+  {
+    return start_base;
+  }
+
+  /**
+   *  Return the new end base to display, as passed to the constructor.
+   **/
+  public int getEnd() 
+  {
+    return end_base;
+  }
+
+  /**
+   *  Return the width in bases of the display, as passed to the constructor.
+   **/
+  public int getWidthInBases() 
+  {
+    return width_in_bases;
+  }
+
+  /**
+   *  Return the width of a base on the display, as passed to the constructor.
+   **/
+  public float getBaseWidth() 
+  {
+    return base_width;
+  }
+
+  /**
+   *  Return the scale factor that was passed to the constructor.
+   **/
+  public int getScaleFactor() 
+  {
+    return scale_factor;
+  }
+
+  /**
+   *  Return true if and only if the FeatureDisplay is drawing in reverse
+   *  complement mode.
+   **/
+  public boolean isRevCompDisplay() 
+  {
+    return rev_comp_display;
+  }
+
+  /**
+   *  Return the type that was passed to the constructor.
+   **/
+  public int getType() 
+  {
+    return type;
+  }
+
+}
+
+
diff --git a/uk/ac/sanger/artemis/components/DisplayAdjustmentListener.java b/uk/ac/sanger/artemis/components/DisplayAdjustmentListener.java
new file mode 100644
index 000000000..31bcc8296
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/DisplayAdjustmentListener.java
@@ -0,0 +1,45 @@
+/* DisplayAdjustmentListener.java
+ *
+ * created: Tue Dec 15 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/DisplayAdjustmentListener.java,v 1.1 2004-06-09 09:46:14 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  The DisplayAdjustmentListener interface is implemented by those
+ *  components that need to track the scrolling of a component that can
+ *  display bases.  For example, BasePlot objects listen to FeatureDisplay
+ *  objects.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: DisplayAdjustmentListener.java,v 1.1 2004-06-09 09:46:14 tjc Exp $
+ **/
+
+public interface DisplayAdjustmentListener extends uk.ac.sanger.artemis.ChangeListener {
+  /**
+   *  Invoked when a component scrolls or changes the scale.
+   **/
+  void displayAdjustmentValueChanged (DisplayAdjustmentEvent event);
+}
+
+
diff --git a/uk/ac/sanger/artemis/components/DisplayComponent.java b/uk/ac/sanger/artemis/components/DisplayComponent.java
new file mode 100644
index 000000000..7953d56d1
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/DisplayComponent.java
@@ -0,0 +1,52 @@
+/* DisplayComponent.java (formally SelectionDisplayer.java)
+ *
+ * created: Fri Nov 13 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/DisplayComponent.java,v 1.1 2004-06-09 09:46:15 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.EntryGroup;
+import uk.ac.sanger.artemis.Selection;
+import uk.ac.sanger.artemis.GotoEventSource;
+
+import javax.swing.*;
+
+/**
+ *  Interface discribing those methods common to all the classes in
+ *  uk.ac.sanger.artemis.components that can display EntryGroup objects.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: DisplayComponent.java,v 1.1 2004-06-09 09:46:15 tjc Exp $
+ **/
+
+public interface DisplayComponent {
+  /**
+   *  Return an object that implements the GotoEventSource interface.
+   **/
+  GotoEventSource getGotoEventSource ();
+
+  /**
+   *  Return the reference of the JFrame that owns this component.
+   **/
+  JFrame getParentFrame ();
+}
diff --git a/uk/ac/sanger/artemis/components/EMBLCorbaEntrySource.java b/uk/ac/sanger/artemis/components/EMBLCorbaEntrySource.java
new file mode 100644
index 000000000..084012483
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EMBLCorbaEntrySource.java
@@ -0,0 +1,318 @@
+/* EMBLCorbaEntrySource.java
+ *
+ * created: Wed Jun  7 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EMBLCorbaEntrySource.java,v 1.1 2004-06-09 09:46:17 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.io.LocationParseException;
+import uk.ac.sanger.artemis.io.InvalidKeyException;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.SimpleEntryInformation;
+
+import nsdb.EmblSeq;
+import nsdb.EmblPackage.Superceded;
+import type.NoResult;
+
+import java.io.*;
+import java.awt.*;
+import java.net.*;
+
+import javax.swing.*;
+
+/**
+ *  This is an EntrySource that reads Entry objects from the EMBL CORBA
+ *  server.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: EMBLCorbaEntrySource.java,v 1.1 2004-06-09 09:46:17 tjc Exp $
+ **/
+
+public class EMBLCorbaEntrySource
+    extends CorbaEntrySource
+    implements EntrySource 
+{
+  /**
+   *  Create a new EMBLCorbaEntrySource from the given String.
+   *  @param frame The component that created this EntrySource.  (Used for
+   *    requesters.)
+   *  @param ior_url_string A String containing the URL of the IOR for the
+   *    server.
+   **/
+  public EMBLCorbaEntrySource (final JFrame frame,
+                               final String ior_url_string)
+      throws MalformedURLException//, IOException
+  {
+    super (frame, ior_url_string);
+  }
+
+  /**
+   *  Get an Entry object from the Ensembl CORBA server.
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @param progress_thread Progress thread to monitor entry reading.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final Bases bases, final ProgressThread progress_thread,
+                         final boolean show_progress)
+      throws OutOfRangeException, IOException
+  {
+    return makeCorbaDialog (bases, false);
+  }
+
+  /**
+   *  Get an Entry object from the Ensembl CORBA server.
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final Bases bases, final boolean show_progress)
+      throws OutOfRangeException, IOException 
+  {
+    return makeCorbaDialog (bases, false);
+  }
+
+   /**
+   *  Get an Entry object from the Ensembl CORBA server.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @param progress_thread Progress thread to monitor entry reading.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final boolean show_progress, 
+                         final ProgressThread progress_thread)
+      throws OutOfRangeException, IOException 
+  {
+    return makeCorbaDialog (null, false);
+  }
+
+
+  /**
+   *  Get an Entry object from the Ensembl CORBA server.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.  (Not implemented)
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if the entry that we read has no
+   *    sequence.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final boolean show_progress)
+      throws OutOfRangeException, NoSequenceException, IOException {
+    return makeCorbaDialog (null, false);
+  }
+
+  /**
+   *  Return the name of this source (for display to the user in menus).
+   **/
+  public String getSourceName () {
+    return "EBI - CORBA";
+  }
+
+  /**
+   *  Returns true if and only if this EntrySource always returns "full"
+   *  entries.  ie. entries that contain features and sequence.  Entries that
+   *  are read from EMBL always contain sequence so in this class this method
+   *  returns false.
+   **/
+  public boolean isFullEntrySource () {
+    return true;
+  }
+  
+  /**
+   *  Create a TextRequester, wait for the user to type an accession number
+   *  and then read that entry from the EMBL CORBA server.
+   *  @param bases If this is null a new Bases object will be created for the
+   *    Entry once it has been read from the server.  If not null then it will
+   *    be passed to the Entry constructor.
+   *  @param read_only true if and only if a read-only Entry should be created
+   *    (some are always read only).
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   **/
+  protected Entry makeCorbaDialog (final Bases bases,
+                                   final boolean read_only)
+      throws OutOfRangeException, IOException {
+    final org.omg.CORBA.ORB orb =
+      org.omg.CORBA.ORB.init (new String [0], new java.util.Properties());
+
+    final TextDialog text_dialog =
+      new TextDialog (getFrame (), "Enter an accession number:", 10, "");
+
+    final String text = text_dialog.getText ();
+
+    if (text == null) {
+      // user cancel
+      return null;
+    } else {
+      final String corba_id = text.trim ();
+
+      if (corba_id.length () > 0) {
+        final MessageDialog message_frame =
+          new MessageDialog (getFrame (),
+                             "reading entry - please wait", false);
+
+        try {
+          return makeFromCorbaID (bases, corba_id, read_only);
+        } finally {
+          message_frame.dispose ();
+        }
+      } else {
+        return null;
+      }
+    }
+  }
+
+  /**
+   *  Given an accession number and the handle of an EMBL corba server, this
+   *  method will ask the user (using a TextRequester) for the id of a entry
+   *  in the server and will then attempt to get it.
+   *  @param bases If this is null a new Bases object will be created for the
+   *    Entry once it has been read from the server.  If not null then it will
+   *    be passed to the Entry constructor.
+   *  @param corba_handle The handle of the nsdb.Embl object from which we
+   *    will read the entry.
+   *  @param corba_id The id of the entry in the database
+   *  @param read_only true if and only if a read-only Entry should be created
+   *    (some are always read only).
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   **/
+  protected Entry makeFromCorbaID (final Bases bases,
+                                   final String corba_id,
+                                   final boolean read_only)
+      throws OutOfRangeException, IOException {
+    try {
+      final nsdb.Embl corba_handle = getServerHandle ();
+
+      final uk.ac.sanger.artemis.io.Entry new_embl_entry;
+
+      final nsdb.EmblWriter embl_writer =
+        nsdb.EmblWriterHelper.narrow (corba_handle);
+
+      final EntryInformation entry_information =
+        new SimpleEntryInformation (Options.getArtemisEntryInformation ());
+
+      if (read_only || embl_writer == null) {
+   // first try to make a plain EmblSeq object
+        final EmblSeq embl_seq = corba_handle.getEmblSeq (corba_id);
+
+        new_embl_entry =
+          new uk.ac.sanger.artemis.io.CorbaEntry (entry_information,
+                                                      embl_seq);
+      } else {
+        // make a read-write object
+        final nsdb.EmblSeqWriter embl_write_seq =
+          embl_writer.getEmblSeqWriter (corba_id);
+
+        new_embl_entry =
+          new uk.ac.sanger.artemis.io.RWCorbaEntry (entry_information,
+                                                        embl_write_seq);
+      }
+
+      final Bases real_bases;
+
+      if (bases == null) {
+        if (new_embl_entry.getSequence () == null) {
+          final String message =
+            "the entry contains no sequence: " + corba_id;
+          new MessageDialog (getFrame (), message);
+          return null; 
+       }
+
+        real_bases = new Bases (new_embl_entry.getSequence ());
+      } else {
+        real_bases = bases;
+      }
+
+      return new Entry (real_bases, new_embl_entry);
+    } catch (NoResult e) {
+      final String message =
+        "Database query failed (no result) while getting id: " + corba_id;
+      new MessageDialog (getFrame (), message);
+    } catch (Superceded e) {
+      //  Superceded is thrown by getEmblSeq method if accession number
+      //  doesn't exist anymore because it was merged or split
+      final String message =
+        "This accession number has been superceded: " + corba_id;
+      new MessageDialog (getFrame (), message);
+    } catch (LocationParseException e) {
+      final String message =
+        "Unexpected error while accessing " + corba_id + ": " + e;
+      new MessageDialog (getFrame (), message);
+    } catch (InvalidKeyException e) {
+      final String message =
+        "Unexpected error while accessing " + corba_id + ": " + e;
+      new MessageDialog (getFrame (), message);
+    } catch (org.omg.CORBA.OBJECT_NOT_EXIST e) {
+      final String message =
+        "the object you requested (" + corba_id + ") does not exist";
+      new MessageDialog (getFrame (), message);
+    } catch (org.omg.CORBA.COMM_FAILURE e) {
+      final String message =
+        "Failed to get an object from Corba: " + e;
+      new MessageDialog (getFrame (), message);
+    } catch (EntryInformationException e) {
+      final String message =
+        "Failed to get an object from Corba: " + e;
+      new MessageDialog (getFrame (), message);
+    }
+
+    return null;
+  }
+
+  /**
+   *  Return the handle of the EMBL database.  The database IOR is found by
+   *  calling getIOR ().
+   **/
+  protected nsdb.Embl getServerHandle ()
+      throws org.omg.CORBA.COMM_FAILURE, IOException {
+    final org.omg.CORBA.ORB orb =
+      org.omg.CORBA.ORB.init (new String [0], new java.util.Properties());
+
+    final org.omg.CORBA.Object obj;
+
+    obj = orb.string_to_object (getIOR ());
+
+    return nsdb.EmblHelper.narrow (obj);
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/EditMenu.java b/uk/ac/sanger/artemis/components/EditMenu.java
new file mode 100644
index 000000000..aafe11393
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EditMenu.java
@@ -0,0 +1,2447 @@
+/* EditMenu.java
+ *
+ * created: Thu Dec  3 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1998,1999,2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EditMenu.java,v 1.1 2004-06-09 09:46:18 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.io.Range;
+import uk.ac.sanger.artemis.io.RangeVector;
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.Location;
+import uk.ac.sanger.artemis.io.Qualifier;
+import uk.ac.sanger.artemis.io.QualifierInfo;
+import uk.ac.sanger.artemis.io.QualifierInfoVector;
+import uk.ac.sanger.artemis.io.QualifierVector;
+import uk.ac.sanger.artemis.io.QualifierParseException;
+import uk.ac.sanger.artemis.io.InvalidQualifierException;
+import uk.ac.sanger.artemis.io.InvalidRelationException;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+import uk.ac.sanger.artemis.io.OutOfDateException;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.IOException;
+
+import javax.swing.*;
+
+/**
+ *  A menu with editing commands.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: EditMenu.java,v 1.1 2004-06-09 09:46:18 tjc Exp $
+ **/
+
+public class EditMenu extends SelectionMenu
+    implements EntryGroupChangeListener, EntryChangeListener {
+  /**
+   *  Create a new EditMenu object.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @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.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   *  @param menu_name The name of the new menu.
+   **/
+  public EditMenu (final JFrame frame,
+                   final Selection selection,
+                   final GotoEventSource goto_event_source,
+                   final EntryGroup entry_group,
+                   final BasePlotGroup base_plot_group,
+                   final String menu_name) {
+    super (frame, menu_name, selection);
+
+    this.entry_group = entry_group;
+    this.goto_event_source = goto_event_source;
+    this.base_plot_group = base_plot_group;
+
+    getEntryGroup ().addEntryGroupChangeListener (this);
+    getEntryGroup ().addEntryChangeListener (this);
+
+    refreshMenu ();
+  }
+
+  /**
+   *  Create a new EditMenu object and use "Edit" as the menu name.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @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.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public EditMenu (final JFrame frame,
+                   final Selection selection,
+                   final GotoEventSource goto_event_source,
+                   final EntryGroup entry_group,
+                   final BasePlotGroup base_plot_group) {
+    this (frame, selection, goto_event_source, entry_group,
+          base_plot_group, "Edit");
+  }
+
+  /**
+   *  The shortcut for Edit Selected Features.
+   **/
+  final static KeyStroke EDIT_FEATURES_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_E, InputEvent.CTRL_MASK);
+  final static public int EDIT_FEATURES_KEY_CODE = KeyEvent.VK_E;
+
+  /**
+   *  The shortcut for Merge Selected Features.
+   **/
+  final static KeyStroke MERGE_FEATURES_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_M, InputEvent.CTRL_MASK);
+  final static public int MERGE_FEATURES_KEY_CODE = KeyEvent.VK_M;
+
+  /**
+   *  The shortcut for Duplicate Selected Features.
+   **/
+  final static KeyStroke DUPLICATE_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_D, InputEvent.CTRL_MASK);
+  final static public int DUPLICATE_KEY_CODE = KeyEvent.VK_D;
+
+  /**
+   *  The shortcut for Delete Selected Features.
+   **/
+  final static KeyStroke DELETE_FEATURES_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_DELETE, InputEvent.CTRL_MASK);
+  final static public int DELETE_FEATURES_KEY_CODE = KeyEvent.VK_DELETE;
+
+  /**
+   *  The shortcut for Trim Selected Features.
+   **/
+  final static KeyStroke TRIM_FEATURES_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_T, InputEvent.CTRL_MASK);
+  final static public int TRIM_FEATURES_KEY_CODE = KeyEvent.VK_T;
+
+  /**
+   *  The shortcut for Trim Selected Features To Next Any.
+   **/
+  final static KeyStroke TRIM_FEATURES_TO_NEXT_ANY_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_Y, InputEvent.CTRL_MASK);
+  final static public int TRIM_FEATURES_TO_NEXT_ANY_KEY_CODE = KeyEvent.VK_Y;
+
+  /**
+   *  The shortcut for Extend to Previous Stop Codon.
+   **/
+  final static public int EXTEND_TO_PREVIOUS_STOP_CODON_KEY_CODE =
+    KeyEvent.VK_Q;
+  final static KeyStroke EXTEND_TO_PREVIOUS_STOP_CODON_KEY =
+    makeMenuKeyStroke (EXTEND_TO_PREVIOUS_STOP_CODON_KEY_CODE);
+
+  /**
+   *  The shortcut for Undo.
+   **/
+  final static public int UNDO_KEY_CODE = KeyEvent.VK_U;
+  final static KeyStroke UNDO_KEY =
+    KeyStroke.getKeyStroke (UNDO_KEY_CODE, InputEvent.CTRL_MASK);
+
+  /**
+   *  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;
+    }
+  }
+
+  /**
+   *  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 ();
+
+    undo_item = new JMenuItem ("Undo");
+    undo_item.setAccelerator (UNDO_KEY);
+    undo_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        undo (getParentFrame (), getSelection (), getEntryGroup ());
+      }
+    });
+
+    edit_feature_item = new JMenuItem ("Edit Selected Features");
+    edit_feature_item.setAccelerator (EDIT_FEATURES_KEY);
+    edit_feature_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        editSelectedFeatures (getParentFrame (), getEntryGroup (),
+                              getSelection (), getGotoEventSource ());
+      }
+    });
+
+    edit_subsequence_item = new JMenuItem ("Edit Subsequence (and Features)");
+    edit_subsequence_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        editSubSequence ();
+      }
+    });
+
+    add_qualifiers_item = new JMenuItem ("Change Qualifiers Of Selected ...");
+    add_qualifiers_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        addQualifiers (getParentFrame (), getSelection ());
+      }
+    });
+
+    remove_qualifier_item = new JMenuItem ("Remove Qualifier Of Selected ...");
+    remove_qualifier_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        removeQualifier (getParentFrame (), getSelection ());
+      }
+    });
+
+
+    merge_features_item = new JMenuItem ("Merge Selected Features");
+    merge_features_item.setAccelerator (MERGE_FEATURES_KEY);
+    merge_features_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        mergeFeatures (getParentFrame (), getSelection (), getEntryGroup ());
+      }
+    });
+
+    unmerge_feature_item = new JMenuItem ("Unmerge Selected Feature");
+    unmerge_feature_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        unmergeFeature (getParentFrame (), getSelection (), getEntryGroup ());
+      }
+    });
+
+    duplicate_item = new JMenuItem ("Duplicate Selected Features");
+    duplicate_item.setAccelerator (DUPLICATE_KEY);
+    duplicate_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        duplicateFeatures (getParentFrame (), getSelection (),
+                           getEntryGroup ());
+      }
+    });
+
+    delete_features_item = new JMenuItem ("Delete Selected Features");
+    delete_features_item.setAccelerator (DELETE_FEATURES_KEY);
+    delete_features_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        deleteSelectedFeatures (getParentFrame (), getSelection (),
+                                getEntryGroup ());
+      }
+    });
+
+    delete_segments_item = new JMenuItem ("Delete Selected Exons");
+    delete_segments_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        deleteSelectedSegments ();
+      }
+    });
+
+    delete_introns_item =
+      new JMenuItem ("Remove Introns of Selected Features");
+    delete_introns_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        removeIntrons ();
+      }
+    });
+
+    edit_header_item = new JMenuItem ("Edit Header Of Default Entry");
+    edit_header_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        editHeader ();
+      }
+    });
+
+    move_features_menu = new JMenu ("Move Selected Features To");
+    copy_features_menu = new JMenu ("Copy Selected Features To");
+
+    if (entry_group == null || getEntryGroup ().size () == 0) {
+      move_features_menu.add (new JMenuItem ("(No Entries Currently)"));
+      copy_features_menu.add (new JMenuItem ("(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 JMenuItem move_to_item = new JMenuItem (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 JMenuItem copy_to_item = new JMenuItem (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);
+      }
+    }
+
+    trim_to_any_item = new JMenuItem ("Trim Selected Features To Any");
+    trim_to_any_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        EditMenu.trimSelected (getParentFrame (), getSelection (),
+                               getEntryGroup (), true, false);
+      }
+    });
+
+    trim_item = new JMenuItem ("Trim Selected Features To Met");
+    trim_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        EditMenu.trimSelected (getParentFrame (), getSelection (),
+                               getEntryGroup (), false, false);
+      }
+    });
+
+    trim_to_next_any_item =
+      new JMenuItem ("Trim Selected Features To Next Any");
+    trim_to_next_any_item.setAccelerator (TRIM_FEATURES_TO_NEXT_ANY_KEY);
+    trim_to_next_any_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        EditMenu.trimSelected (getParentFrame (), getSelection (),
+                               getEntryGroup (), true, true);
+      }
+    });
+
+    trim_to_next_item = new JMenuItem ("Trim Selected Features To Next Met");
+    trim_to_next_item.setAccelerator (TRIM_FEATURES_KEY);
+    trim_to_next_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        EditMenu.trimSelected (getParentFrame (), getSelection (),
+                               getEntryGroup (), false, true);
+      }
+    });
+
+    extend_to_prev_stop_item =
+      new JMenuItem ("Extend to Previous Stop Codon");
+    extend_to_prev_stop_item.setAccelerator (EXTEND_TO_PREVIOUS_STOP_CODON_KEY);
+    extend_to_prev_stop_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        extendToORF (getParentFrame (), getSelection (),
+                     getEntryGroup (), false);
+      }
+    });
+
+    extend_to_next_stop_item = new JMenuItem ("Extend to Next Stop Codon");
+    extend_to_next_stop_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        extendToORF (getParentFrame (), getSelection (),
+                     getEntryGroup (), true);
+      }
+    });
+
+    fix_stop_codons_item = new JMenuItem ("Fix Stop Codons");
+    fix_stop_codons_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        fixStopCodons ();
+      }
+    });
+
+    auto_gene_name_item = new JMenuItem ("Automatically Create Gene Names");
+    auto_gene_name_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        autoGeneName ();
+      }
+    });
+
+    fix_gene_names_item = new JMenuItem ("Fix Gene Names");
+    fix_gene_names_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        fixGeneNames (getParentFrame (), getEntryGroup (),
+                      getSelection ());
+      }
+    });
+
+    reverse_complement_item = new JMenuItem ("Reverse And Complement");
+    reverse_complement_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        reverseAndComplement ();
+      }
+    });
+
+    delete_bases_item = new JMenuItem ("Delete Selected Bases");
+    delete_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        deleteSelectedBases ();
+      }
+    });
+
+    add_bases_item = new JMenuItem ("Add Bases At Selection");
+    add_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        addBases ();
+      }
+    });
+
+    if (Options.readWritePossible ()) {
+      // only the standalone version can save or read
+      add_bases_from_file_item = new JMenuItem ("Add Bases From File ...");
+      add_bases_from_file_item.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent event) {
+          addBasesFromFile ();
+        }
+      });
+    }
+
+    if (Options.getOptions ().getPropertyTruthValue ("val_mode")) {
+      add (edit_feature_item);
+      add (edit_subsequence_item);
+      addSeparator ();
+      add (edit_header_item);
+      addSeparator ();
+    }
+
+    if (Options.getOptions ().getUndoLevels () > 0) {
+      add (undo_item);
+      addSeparator ();
+    }
+
+    if (!Options.getOptions ().getPropertyTruthValue ("val_mode")) {
+      add (edit_feature_item);
+      add (edit_subsequence_item);
+      addSeparator ();
+      add (edit_header_item);
+      addSeparator ();
+    }
+
+    add (add_qualifiers_item);
+    add (remove_qualifier_item);
+    add (duplicate_item);
+    add (merge_features_item);
+    add (unmerge_feature_item);
+    add (delete_features_item);
+    add (delete_segments_item);
+    add (delete_introns_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);
+    add (extend_to_prev_stop_item);
+    add (extend_to_next_stop_item);
+    add (fix_stop_codons_item);
+    addSeparator ();
+    add (auto_gene_name_item);
+    add (fix_gene_names_item);
+    add (reverse_complement_item);
+    add (delete_bases_item);
+    add (add_bases_item);
+    if (Options.readWritePossible ()) {
+      // only the standalone version can save or read
+      add (add_bases_from_file_item);
+    }
+  }
+
+  /**
+   *  Undo the last change by calling ActionController.undo().
+   *  @param frame The Frame to use for MessageDialog components.
+   *  @param selection The current Selection - needs to be cleared before undo
+   *  @param entry_group Used to get the ActionController for calling
+   *    ActionController.undo().
+   **/
+  public static void undo (final JFrame frame,
+                           final Selection selection,
+                           final EntryGroup entry_group) {
+    if (Options.getOptions ().getUndoLevels () == 0) {
+      // undo disabled
+      return;
+    }
+
+    if (entry_group.getActionController ().canUndo ()) {
+      // clear the selection because something in the selection might
+      // disappear after the undo() eg. create a feature, select it then undo
+      selection.clear ();
+    }
+
+    if (!entry_group.getActionController ().undo ()) {
+      new MessageDialog (frame, "sorry - no further undo information");
+    }
+  }
+
+  /**
+   *  editSelectedFeatures () etc. will only edit this many features.
+   **/
+  private static final int MAXIMUM_SELECTED_FEATURES = 25;
+
+  /**
+   *  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 JFrame to use for MessageDialog components.
+   *  @param selection The selected features to edit.
+   *  @param entry_group Used to get the ActionController for calling
+   *    startAction() and endAction().
+   **/
+  static void editSelectedFeatures (final JFrame frame,
+                                    final EntryGroup entry_group,
+                                    final Selection selection,
+                                    final GotoEventSource goto_event_source) {
+    final FeatureVector features_to_edit = selection.getAllFeatures ();
+
+    if (features_to_edit.size () > MAXIMUM_SELECTED_FEATURES) {
+      new MessageDialog (frame, "warning: only editing the first " +
+                         MAXIMUM_SELECTED_FEATURES +
+                         " selected features");
+    }
+
+    for (int i = 0 ;
+         i < features_to_edit.size () && i < MAXIMUM_SELECTED_FEATURES ;
+         ++i) {
+      final Feature selection_feature = features_to_edit.elementAt (i);
+
+      new FeatureEdit (selection_feature, entry_group, selection,
+                       goto_event_source).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 (getSelection ().isEmpty ()) {
+      new MessageDialog (getParentFrame (), "nothing selected");
+    }
+
+    final Range range = getSelection ().getSelectionRange ();
+
+    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 {
+      if (default_entry.isReadOnly ()) {
+        new MessageDialog (getParentFrame (),
+                           "the default entry is read-only " +
+                           "- cannot continue");
+        return;
+      }
+
+      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 JFrame to use for MessageDialog components.
+   *  @param selection The Selection containing the features to merge.
+   *  @param entry_group Used to get the ActionController for calling
+   *    startAction() and endAction().
+   **/
+  static void mergeFeatures (final JFrame frame,
+                             final Selection selection,
+                             final EntryGroup entry_group) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      if (!checkForSelectionFeatures (frame, selection,
+                                      10, "really merge all (>10) " +
+                                      "selected features?")) {
+        return;
+      }
+
+      final FeatureVector features_to_merge = selection.getAllFeatures ();
+
+      if (features_to_merge.size () < 2) {
+        new MessageDialog (frame,
+                           "nothing to merge - select more than one feature");
+        return;
+      }
+
+      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;
+        }
+      }
+
+      final Feature new_feature;
+
+      try {
+        new_feature = merge_feature.duplicate ();
+      } catch (ReadOnlyException e) {
+        final String message =
+          "one or more of the features is read-only or is in a " +
+          "read-only entry - cannot continue";
+        new MessageDialog (frame, message);
+        return;
+      }
+
+      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) {
+            try {
+              new_feature.removeFromEntry ();
+            } catch (ReadOnlyException _) {
+              // give up ...
+            }
+            final String message =
+              "destination entry does not support all the qualifiers " +
+              "needed by " + this_feature.getIDString ();
+            new MessageDialog (frame, message);
+          } catch (ReadOnlyException e) {
+            final String message = "the new feature is read-only so " +
+              "some qualifiers have been lost";
+            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 ();
+
+          try {
+            new_feature.addSegment (this_range);
+          } catch (ReadOnlyException e) {
+            final String message =
+              "merging failed because the entry is read-only";
+            new MessageDialog (frame, message);
+            try {
+              new_feature.removeFromEntry ();
+            } catch (ReadOnlyException _) {
+              // oh dear ...
+            }
+          }
+        }
+      }
+
+      // this is set to true when a merge is done in the next (inner) loop
+      boolean keep_looping = true;
+
+      // this is a bit inefficient, but there aren't normally many segments
+  LOOP:
+      while (keep_looping) {
+        final FeatureSegmentVector feature_segments =
+          new_feature.getSegments ();
+
+        keep_looping = false;
+
+        // now merge overlapping ranges
+        for (int segment_index = 0 ;
+             segment_index < feature_segments.size () - 1;
+             ++segment_index) {
+
+          final FeatureSegment this_segment =
+            feature_segments.elementAt (segment_index);
+          final MarkerRange this_range = this_segment.getMarkerRange ();
+
+          final FeatureSegment next_segment =
+            feature_segments.elementAt (segment_index + 1);
+          final MarkerRange next_range = next_segment.getMarkerRange ();
+
+          // if it overlaps the next Range then merge it
+          if (this_range.overlaps (next_range) &&
+              this_segment.getFrameID () == next_segment.getFrameID ()) {
+            try {
+              final Range new_range =
+                this_range.combineRanges (next_range, false).getRawRange ();
+              new_feature.addSegment (new_range);
+              new_feature.removeSegment (this_segment);
+              new_feature.removeSegment (next_segment);
+
+              // start again
+              keep_looping = true;
+              continue LOOP;
+            } catch (ReadOnlyException e) {
+              final String message =
+                "merging failed because the entry is read-only";
+              new MessageDialog (frame, message);
+            } catch (LastSegmentException e) {
+              throw new Error ("internal error - tried to remove " +
+                               "last segment: " + e);
+            }
+          }
+        }
+      }
+
+      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) {
+        if (getReadOnlyFeatures (features_to_merge).size () > 0) {
+          new MessageDialog (frame, "deletion failed because the features " +
+                             "are read-only");
+       } else {
+          for (int i = 0 ; i < features_to_merge.size () ; ++i) {
+            try {
+              features_to_merge.elementAt (i).removeFromEntry ();
+            } catch (ReadOnlyException e) {
+              new MessageDialog (frame, "deletion failed one or more of the " +
+                                 "features are read-only");
+            }
+          }
+        }
+      }
+
+      selection.set (new_feature);
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  If the selection contains exactly two segments and those segments are
+   *  adjacent in the same feature, split the feature into two pieces.  The
+   *  orignal feature is truncated and a new feature is created.  The
+   *  qualifiers of the old feature are copied to new feature.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The Selection containing the segments to unmerge.
+   *  @param entry_group Used to get the ActionController for calling
+   *    startAction() and endAction().
+   **/
+  static void unmergeFeature (final JFrame frame,
+                              final Selection selection,
+                              final EntryGroup entry_group) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      final FeatureSegmentVector selected_segments =
+        selection.getSelectedSegments ();
+
+      if (selected_segments.size () != 2) {
+        final String message =
+          "you need to select exactly two exons use unmerge";
+        new MessageDialog (frame, message);
+        return;
+      }
+
+      FeatureSegment first_segment = selected_segments.elementAt (0);
+      FeatureSegment second_segment = selected_segments.elementAt (1);
+
+      if (first_segment.getFeature () != second_segment.getFeature ()) {
+        final String message =
+          "you need to select two exons from the same feature to use unmerge";
+        new MessageDialog (frame, message);
+        return;
+      }
+
+      final Feature segment_feature = first_segment.getFeature ();
+
+      final FeatureSegmentVector all_feature_segments =
+        segment_feature.getSegments ();
+
+      int index_of_first_segment =
+        all_feature_segments.indexOf (first_segment);
+      int index_of_second_segment =
+        all_feature_segments.indexOf (second_segment);
+
+      if (index_of_first_segment - index_of_second_segment < -1 ||
+          index_of_first_segment - index_of_second_segment > 1) {
+        final String message =
+          "you need to select two adjacent exons to use unmerge";
+        new MessageDialog (frame, message);
+        return;
+      }
+
+      if (index_of_second_segment <  index_of_first_segment) {
+        // swap the segments for consistency
+        final FeatureSegment temp_segment = first_segment;
+        final int temp_segment_index = index_of_first_segment;
+
+        first_segment = second_segment;
+        index_of_first_segment = index_of_second_segment;
+
+        second_segment = temp_segment;
+        index_of_second_segment = temp_segment_index;
+      }
+
+      try {
+        final Feature new_feature = segment_feature.duplicate ();
+
+        // we set the Selection later
+        selection.clear ();
+
+        // delete the segments starting at index_of_second_segment from
+        // segment_feature and delete the segments up to (and including)
+        // index_of_first_segment from new_feature
+
+        for (int i = all_feature_segments.size () - 1 ;
+             i >= index_of_second_segment ;
+             --i) {
+          segment_feature.getSegments ().elementAt (i).removeFromFeature ();
+        }
+
+        // remove the first segment of new_feature index_of_first_segment times
+        for (int i = 0 ; i <= index_of_first_segment ; ++i) {
+          new_feature.getSegments ().elementAt (0).removeFromFeature ();
+        }
+
+        selection.set (segment_feature.getSegments ().lastElement ());
+        selection.add (new_feature.getSegments ().elementAt (0));
+      } catch (ReadOnlyException e) {
+        final String message =
+          "the selected exons (in " +
+          segment_feature.getIDString () +
+          ") are in a read only entry - cannot continue";
+        new MessageDialog (frame, message);
+      } catch (LastSegmentException e) {
+        throw new Error ("internal error - unexpected exception: " + e);
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Create a QualifierEditor JFrame that acts on the selected features.
+   *  @param frame The JFrame to use for MessageDialog components.
+   **/
+  private void addQualifiers (final JFrame frame, final Selection selection) {
+    if (!checkForSelectionFeatures (frame, selection)) {
+      return;
+    }
+
+    final FeatureVector selected_features = selection.getAllFeatures ();
+
+    if (getReadOnlyFeatures (selected_features).size () > 0) {
+      new MessageDialog (frame,
+                         "one or more of the selected features is read-only " +
+                         "- cannot continue");
+      return;
+    }
+
+    final QualifierEditor qualifier_editor =
+      new QualifierEditor (selected_features, getEntryGroup ());
+
+    qualifier_editor.setVisible (true);
+  }
+
+  /**
+   *  Offer the user a choice of qualifier to remove from the selected features
+   **/
+  private void removeQualifier (final JFrame frame, final Selection selection) {
+    if (!checkForSelectionFeatures (frame, selection)) {
+      return;
+    }
+
+    final FeatureVector selected_features = selection.getAllFeatures ();
+
+    final StringVector qualifier_names =
+      Feature.getAllQualifierNames (selected_features);
+
+    if (qualifier_names.size () == 0) {
+      new MessageDialog (getParentFrame (), "feature has no qualifiers");
+      return;
+    }
+
+    final ChoiceFrame choice_frame =
+      new ChoiceFrame ("Select a qualifer name", qualifier_names);
+
+    final JComboBox choice = choice_frame.getChoice ();
+
+
+    final int MAX_VISIBLE_ROWS = 30;
+    
+    choice.setMaximumRowCount (MAX_VISIBLE_ROWS);
+    
+    choice.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent _) {
+        removeQualifierFromFeatures (selected_features,
+                                     (String) choice.getSelectedItem ());
+        choice_frame.setVisible (false);
+        choice_frame.dispose ();
+      }
+    });
+
+    choice_frame.getOKButton ().addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        removeQualifierFromFeatures (selected_features,
+                                     (String) choice.getSelectedItem ());
+      }
+    });
+
+    choice_frame.setVisible (true);
+  }
+
+  /**
+   *  Remove the qualifier given by qualifier_name from the given features.
+   *  Silently ignore features that don't have that qualifier.  Warn the user
+   *  about read-only features.
+   **/
+  private void removeQualifierFromFeatures (final FeatureVector features,
+                                            final String qualifier_name) {
+    boolean found_read_only = false;
+
+    try {
+      entry_group.getActionController ().startAction ();
+
+      for (int i = 0 ; i < features.size () ; ++i) {
+        final Feature this_feature = features.elementAt (i);
+        try {
+          this_feature.removeQualifierByName (qualifier_name);
+        } catch (OutOfDateException _) {
+          // ignore
+        } catch (EntryInformationException _) {
+          // ignore
+        } catch (ReadOnlyException _) {
+          found_read_only = true;
+        }
+      }
+
+      if (found_read_only) {
+        final String message =
+          "ignored one or more read-only features";
+        new MessageDialog (getParentFrame (), message);
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Duplicate the selected Feature objects.  If there are selected segments
+   *  then the owning Feature of each segment will be duplicated.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The Selection containing the features to merge.
+   *  @param entry_group Used to get the ActionController for calling
+   *    startAction() and endAction().
+   **/
+  static void duplicateFeatures (final JFrame frame,
+                                 final Selection selection,
+                                 final EntryGroup entry_group) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      if (getReadOnlyFeatures (selection.getAllFeatures ()).size () > 0) {
+        new MessageDialog (frame,
+                           "one or more of the selected features is read-only " +
+                           "- cannot continue");
+        return;
+      }
+
+      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;
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Delete the selected features.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The Selection containing the features to merge.
+   *  @param entry_group Used to get the ActionController for calling
+   *    startAction() and endAction().
+   **/
+  static void deleteSelectedFeatures (final JFrame frame,
+                                      final Selection selection,
+                                      final EntryGroup entry_group) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      final FeatureVector features_to_delete = selection.getAllFeatures ();
+
+      final String feature_count_string;
+
+      if (features_to_delete.size () == 1) {
+        feature_count_string = "the selected feature";
+      } else {
+        feature_count_string = features_to_delete.size () + " features";
+      }
+
+      if (Options.getOptions ().isNoddyMode ()) {
+        // 0 means always popup a YesNoDialog
+        if (!checkForSelectionFeatures (frame, selection, 0,
+                                        "really delete " +
+                                        feature_count_string + "?")) {
+          return;
+        }
+      }
+
+      // clear the selection now so that it doesn't need to be updated as each
+      // feature is deleted
+      selection.clear ();
+
+      while (features_to_delete.size () > 0) {
+        // delete in reverse order for speed
+
+        final Feature current_selection_feature =
+          features_to_delete.lastElement ();
+
+        features_to_delete.removeElementAt (features_to_delete.size () - 1);
+
+        try {
+          current_selection_feature.removeFromEntry ();
+        } catch (ReadOnlyException e) {
+          selection.set (current_selection_feature);
+
+          if (features_to_delete.size () == 1) {
+            final String message =
+              "the selected feature (" +
+              current_selection_feature.getIDString () +
+              ") is read only - cannot continue";
+            new MessageDialog (frame, message);
+
+          } else {
+            final String message =
+              "one of the selected features (" +
+              current_selection_feature.getIDString () +
+              ") is read only - cannot continue";
+            new MessageDialog (frame, message);
+          }
+
+          features_to_delete.add (current_selection_feature);
+
+          // reset the select so that the user can see what it was
+          selection.set (features_to_delete);
+
+          return;
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Delete the selected feature segments.
+   **/
+  private void deleteSelectedSegments () {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      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 - cannot continue";
+          new MessageDialog (getParentFrame (), message);
+        } 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);
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Remove all introns from the selected features.
+   **/
+  private void removeIntrons () {
+    if (!checkForSelectionFeatures ()) {
+      return;
+    }
+
+    boolean found_read_only = false;
+
+    try {
+      entry_group.getActionController ().startAction ();
+
+      final FeatureVector features = getSelection ().getAllFeatures ();
+
+      for (int i = 0 ; i < features.size () ; ++i) {
+        final Feature this_feature = features.elementAt (i);
+        try {
+          final RangeVector new_ranges = new RangeVector ();
+          final Range new_range = this_feature.getMaxRawRange ();
+          new_ranges.add (new_range);
+          final Location old_location = this_feature.getLocation ();
+          final Location new_location =
+            new Location (new_ranges, old_location.isComplement ());
+          this_feature.setLocation (new_location);
+        } catch (OutOfRangeException e) {
+          throw new Error ("internal error - unexpected exception: " + e);
+        } catch (ReadOnlyException _) {
+          found_read_only = true;
+        }
+      }
+
+      if (found_read_only) {
+        final String message =
+          "ignored one or more read-only features";
+        new MessageDialog (getParentFrame (), message);
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Move the given features to the given destination entry.
+   **/
+  private void moveFeatures (final FeatureVector features,
+                             final Entry destination_entry) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      if (features.size () == 0) {
+        return;
+      }
+
+      final FeatureVector read_only_features =
+        getReadOnlyFeatures (features);
+
+      if (read_only_features.size () > 0) {
+        final String message =
+          (features.size () == 1 ?
+           "the selected feature (" +
+           read_only_features.elementAt (0).getIDString () +
+           ") is read only " :
+           "some of the selected features (eg. " +
+           read_only_features.elementAt (0).getIDString () +
+           ") are read only - ")  +
+          "cannot continue";
+        new MessageDialog (getParentFrame (), message);
+        return;
+      }
+
+  FEATURES:
+      for (int i = 0 ; i < features.size () ; ++i) {
+        final Feature this_feature = features.elementAt (i);
+
+        // check that we don't loop forever (perhaps due to bugs in
+        // handleOpenException())
+        final int MAX_COUNT = 100;
+
+        int count = 0;
+
+        while (count++ < MAX_COUNT) {
+          try {
+            this_feature.moveTo (destination_entry, false);
+            continue FEATURES;
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          } catch (EntryInformationException e) {
+            final EntryInformation dest_entry_information =
+              destination_entry.getEntryInformation ();
+            dest_entry_information.fixException (e);
+            continue;
+          } catch (ReadOnlyException e) {
+            final String message =
+              "either the source or destination for one of the features is " +
+              "read only - cannot continue";
+            new MessageDialog (getParentFrame (), message);
+            return;
+          }
+        }
+
+        final String message =
+          "internal error while copying";
+        new MessageDialog (getParentFrame (), message);
+
+        throw new Error ("internal error in EditMenu.copyFeatures() - infinite loop");
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Copy the given features to the given destination entry.
+   **/
+  private void copyFeatures (final FeatureVector features,
+                             final Entry destination_entry) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+  FEATURES:
+      for (int i = 0 ; i < features.size () ; ++i) {
+        final Feature this_feature = features.elementAt (i);
+
+        // check that we don't loop forever (perhaps due to bugs in
+        // handleOpenException())
+        final int MAX_COUNT = 100;
+
+        int count = 0;
+
+        while (count++ < MAX_COUNT) {
+          try {
+            this_feature.copyTo (destination_entry);
+            continue FEATURES;
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          } catch (EntryInformationException e) {
+            final EntryInformation dest_entry_information =
+              destination_entry.getEntryInformation ();
+            dest_entry_information.fixException (e);
+            continue;
+          } catch (ReadOnlyException e) {
+            final String message =
+              "the destination entry is read only - cannot continue";
+            new MessageDialog (getParentFrame (), message);
+            return;
+          }
+        }
+
+        final String message = "internal error while copying";
+        new MessageDialog (getParentFrame (), message);
+
+        throw new Error ("internal error in EditMenu.copyFeatures() - " +
+                           "infinite loop");
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Calls fixStopCodon () on each selected feature.
+   **/
+  private void fixStopCodons () {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      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 String filter_name =
+            "features that can't be fixed (filtered from: " +
+            getParentFrame ().getTitle () + ")";
+
+          final FilteredEntryGroup filtered_entry_group =
+            new FilteredEntryGroup (entry_group, predicate, filter_name);
+
+          final FeatureListFrame feature_list_frame =
+            new FeatureListFrame (filter_name,
+                                  getSelection (),
+                                  goto_event_source, filtered_entry_group,
+                                  base_plot_group);
+
+          feature_list_frame.setVisible (true);
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Calls Feature.trimStart () on all selected Feature objects.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param entry_group Used to get the ActionController for calling
+   *    startAction().
+   *  @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.
+   **/
+  static public void trimSelected (final JFrame frame,
+                                   final Selection selection,
+                                   final EntryGroup entry_group,
+                                   final boolean trim_to_any,
+                                   final boolean trim_to_next) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      if (!checkForSelectionFeatures (frame, selection)) {
+        return;
+      }
+
+      final FeatureVector features_to_trim = selection.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 or more of the of the selected features are read only " +
+            "- cannot continue";
+          new MessageDialog (frame, message);
+          break;
+        }
+      }
+
+      if (failed_features.size () > 0) {
+        selection.set (failed_features);
+
+        if (failed_features.size () == 1) {
+          final String message = "could not trim the feature";
+
+          new MessageDialog (frame, message);
+
+        } else {
+          final String message =
+            "some features could not be trimmed (they are now selected)";
+
+          new MessageDialog (frame, message);
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Extend the selected segments/features so that they reach to the start or
+   *  end of their containing ORF.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param entry_group Used to get the ActionController for calling
+   *    startAction() and endAction().
+   *  @param extend_to_next_stop If true the feature is extended to the next
+   *    stop codon (but it won't include the next stop).  If false the feature
+   *    is extended to the previous stop codon (but it won't include the
+   *    previous stop).
+   **/
+  public static void extendToORF (final JFrame frame,
+                                  final Selection selection,
+                                  final EntryGroup entry_group,
+                                  final boolean extend_to_next_stop) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      if (!checkForSelectionFeatures (frame, selection)) {
+        return;
+      }
+
+      final FeatureVector features_to_extend = selection.getAllFeatures ();
+
+      for (int i = 0 ; i < features_to_extend.size () ; ++i) {
+        final Feature selection_feature = features_to_extend.elementAt (i);
+
+        if (selection_feature.isReadOnly ()) {
+          final String message =
+            "one or more of the of the selected features are read only " +
+            "- cannot continue";
+          new MessageDialog (frame, message);
+          break;
+        }
+
+        if (selection_feature.getSegments ().size () < 1) {
+          continue;
+        }
+
+        final FeatureSegmentVector feature_segments =
+          selection_feature.getSegments ();
+
+        if (feature_segments.size () == 0) {
+          continue;
+        }
+
+        final Strand strand = selection_feature.getStrand ();
+
+        if (extend_to_next_stop) {
+          if (selection_feature.hasValidStopCodon ()) {
+            continue;
+          }
+
+          final Marker feature_end_marker =
+            selection_feature.getLastBaseMarker ();
+
+          Marker end_marker_copy = null;
+
+          // get the marker of the first base of the last codon in the range
+          // (we want to keep in the same frame)
+          try {
+            if (selection_feature.getBaseCount () == 1) {
+              end_marker_copy = feature_end_marker;
+            } else {
+              if (selection_feature.getBaseCount () == 2) {
+                end_marker_copy = feature_end_marker.moveBy (-1);
+              } else {
+                // go to the start of the codon
+                end_marker_copy = feature_end_marker.moveBy (-2);
+              }
+            }
+
+            // adjust for /codon_start
+            final int frame_shift =
+              selection_feature.getCodonStart () - 1;
+
+            // the number of bases from the start of translation to the
+            // end of the feature
+            final int bases_from_start_pos_mod_3 =
+              (3 + selection_feature.getBaseCount () - frame_shift) % 3;
+
+            end_marker_copy =
+              end_marker_copy.moveBy (-bases_from_start_pos_mod_3);
+          } catch (OutOfRangeException e) {
+            end_marker_copy = feature_end_marker;
+          }
+
+          final MarkerRange end_orf_range =
+            strand.getORFAroundMarker (end_marker_copy, true);
+
+          if (end_orf_range == null) {
+            // end_marker_copy is at the start base of a stop codon
+            continue;
+          }
+
+          final Marker new_end_marker = end_orf_range.getEnd ();
+
+          try {
+            feature_end_marker.setPosition (new_end_marker.getPosition ());
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          }
+
+        } else {
+          final AminoAcidSequence translation =
+            selection_feature.getTranslation ();
+
+          if (translation.length () > 0) {
+            final char first_aa = translation.elementAt (0);
+
+            if (AminoAcidSequence.isStopCodon (first_aa)) {
+              continue;
+            }
+          }
+
+          final Marker feature_start_marker =
+            selection_feature.getFirstBaseMarker ();
+
+          Marker start_marker_copy = feature_start_marker;
+
+          final int frame_shift =
+            selection_feature.getCodonStart () - 1;
+
+          if (frame_shift != 0) {
+            try {
+              start_marker_copy =
+                feature_start_marker.moveBy (frame_shift);
+            } catch (OutOfRangeException e) {
+              // ignore and hope for the best
+            }
+          }
+
+          final MarkerRange start_orf_range =
+            strand.getORFAroundMarker (start_marker_copy, true);
+
+          final Marker new_start_marker = start_orf_range.getStart ();
+
+          try {
+            feature_start_marker.setPosition (new_start_marker.getPosition ());
+            selection_feature.removeQualifierByName ("codon_start");
+          } catch (EntryInformationException _) {
+            // ignore - if we can't remove codon_start, then it won't have
+            // been there to start with
+          } catch (OutOfDateException _) {
+            // ignore
+          } catch (ReadOnlyException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          }
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Reverse and complement the sequence and all the features in all Entry
+   *  objects.
+   **/
+  private void reverseAndComplement () {
+    if (getEntryGroup ().isReadOnly ()) {
+      final String message =
+        "one or more of the entries or features are read only - " +
+        "cannot continue";
+      new MessageDialog (getParentFrame (), message);
+      return;
+    }
+
+    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 marker_range = getSelection ().getMarkerRange ();
+
+    if (marker_range.getCount () == getEntryGroup ().getSequenceLength ()) {
+      new MessageDialog (getParentFrame (), "You can't delete every base");
+      return;
+    }
+
+    if (getEntryGroup ().isReadOnly ()) {
+      new MessageDialog (getParentFrame (),
+                         "one of the current entries or features is " +
+                         "read-only - connot continue");
+      return;
+    }
+
+    final Range raw_range = marker_range.getRawRange ();
+
+    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 (raw_range);
+    } catch (OutOfRangeException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+
+    if (features_in_range.size () != 0) {
+      final FeatureVector read_only_features =
+        getReadOnlyFeatures (features_in_range);
+
+      if (read_only_features.size () > 0) {
+        getSelection ().set (read_only_features);
+        final String message =
+          "one or more of the features in the selected range are " +
+          "read only - cannot continue";
+        new MessageDialog (getParentFrame (), message);
+        getSelection ().setMarkerRange (marker_range);
+        return;
+      }
+
+      for (int feature_index = 0 ;
+           feature_index < features_in_range.size () ;
+           ++feature_index) {
+        final Feature this_feature =
+          features_in_range.elementAt (feature_index);
+
+        final FeatureSegmentVector segments = this_feature.getSegments ();
+
+        for (int i = 0 ; i < segments.size () ; ++i) {
+          final FeatureSegment this_segment = segments.elementAt (i);
+
+          final Range this_segment_range = this_segment.getRawRange ();
+
+          if (raw_range.getStart () <= this_segment_range.getStart () &&
+              raw_range.getEnd () >= this_segment_range.getStart () ||
+              raw_range.getStart () <= this_segment_range.getEnd () &&
+              raw_range.getEnd () >= this_segment_range.getEnd ()) {
+            new MessageDialog (getParentFrame (),
+                               "the selected range overlaps the " +
+                               "start or end of an exon - cannot continue");
+            return;
+          }
+        }
+      }
+    }
+
+    // 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);
+    }
+
+    if (Options.getOptions ().isNoddyMode ()) {
+      final YesNoDialog dialog =
+        new YesNoDialog (getParentFrame (),
+                         "Are you sure you want to delete the " +
+                         "selected bases?");
+      if (!dialog.getResult ()) {
+        return;
+      }
+    }
+
+    try {
+      getSelection ().setMarkerRange (null);
+
+      // deleteRange () is static and uses the strand reference from the
+      // MarkerRange to decide which Strand to edit
+      Strand.deleteRange (marker_range);
+    } catch (ReadOnlyException e) {
+      getSelection ().setMarkerRange (marker_range);
+
+      new MessageDialog (getParentFrame (),
+                         "the bases are read only - cannot delete");
+      return;
+    }
+  }
+
+  /**
+   *  Ask the user for some bases and then add them at the start of the
+   *  selected range.
+   **/
+  private void addBases () {
+    if (!checkForSelectionRange ()) {
+      return;
+    }
+
+    if (getEntryGroup ().isReadOnly ()) {
+      new MessageDialog (getParentFrame (),
+                         "one of the current entries or features is " +
+                         "read-only - connot continue");
+      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;
+        } catch (org.biojava.bio.symbol.IllegalSymbolException e) {
+          new MessageDialog (getParentFrame (),
+                             "sorry - new sequence contains non-IUB base " +
+                             "codes");
+          return;
+        }
+      }
+    });
+
+    text_requester.show ();
+  }
+
+
+  /**
+   *  Ask the user for some bases and then add them at the start of the
+   *  selected range.
+   **/
+  private void addBasesFromFile () {
+    if (!checkForSelectionRange ()) {
+      return;
+    }
+
+    if (getEntryGroup ().isReadOnly ()) {
+      new MessageDialog (getParentFrame (),
+                         "one of the current entries or features is " +
+                         "read-only - connot continue");
+      return;
+    }
+
+    final MarkerRange range = getSelection ().getMarkerRange ();
+
+    // XXX add an InputStreamProgressListener
+    final EntrySourceVector entry_sources =
+      Utilities.getEntrySources (getParentFrame (), null);
+
+    EntrySource filesystem_entry_source = null;
+
+    for (int source_index = 0 ;
+         source_index < entry_sources.size () ;
+         ++source_index) {
+      final EntrySource this_source =
+        entry_sources.elementAt (source_index);
+
+      if (this_source.isFullEntrySource ()) {
+        continue;
+      }
+
+      if (this_source.getSourceName ().equals ("Filesystem")) {
+        filesystem_entry_source = this_source;
+      }
+    }
+
+    if (filesystem_entry_source == null) {
+      throw new Error ("internal error - can't find a file system to read " +
+                       "from");
+    }
+
+    try {
+      final Entry new_entry = filesystem_entry_source.getEntry (true);
+
+      final Bases bases = new_entry.getBases ();
+
+      final String bases_string = bases.toString ();
+
+      if (bases_string.length () == 0) {
+        new MessageDialog (getParentFrame (), "no bases inserted");
+        return;
+      }
+
+      // 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;
+    } catch (IOException e) {
+      new MessageDialog (getParentFrame (),
+                         "error while reading: " + e.getMessage ());
+      return;
+    } catch (OutOfRangeException e) {
+      new MessageDialog (getParentFrame (),
+                         "out of range exception while reading: " +
+                         e.getMessage ());
+      return;
+    } catch (NoSequenceException e) {
+      new MessageDialog (getParentFrame (),
+                         "sorry the file did not contain any sequence");
+      return;
+    } catch (org.biojava.bio.symbol.IllegalSymbolException e) {
+      new MessageDialog (getParentFrame (),
+                         "sorry - new sequence contains non-IUB base " +
+                         "codes");
+      return;
+    }
+  }
+
+  /**
+   *  Add a qualifier to the given features.  The qualifier will normally be a
+   *  gene name.
+   *  @param prefix_string prefix of the new qualifier eg. SPAC
+   *  @param start_number The number to start counting at eg. 1 give SPAC0001
+   *    for the first CDS
+   *  @param increment The amount to increment by at each gene.
+   *  @param qualifier_name The name of the qualifier to add
+   *  @param tag_complement_names If True a lowercase "c" will be appended to
+   *    the new qualifier values on the reverse strand. 
+   **/
+  private void autoGeneNameHelper (final FeatureVector features_to_name,
+                                   final String prefix_string,
+                                   final int start_number,
+                                   final int increment,
+                                   final String qualifier_name,
+                                   final boolean tag_complement_names) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      int current_number = start_number;
+
+      for (int i = 0 ; i < features_to_name.size () ; ++i) {
+        final Feature this_feature = features_to_name.elementAt (i);
+
+        final Key key = this_feature.getKey ();
+
+        if (key.equals ("CDS")) {
+          final String number_string;
+
+          if (current_number < 10) {
+            number_string = "000" + current_number;
+          } else {
+            if (current_number < 100) {
+              number_string = "00" + current_number;
+            } else {
+              if (current_number < 1000) {
+                number_string = "0" + current_number;
+              } else {
+                number_string = String.valueOf (current_number);
+              }
+            }
+          }
+
+          try {
+            final Qualifier new_qualifier =
+              new Qualifier (qualifier_name, prefix_string +
+                             number_string +
+                             (!tag_complement_names ||
+                              this_feature.isForwardFeature () ?
+                              "" :
+                              "c"));
+
+            this_feature.addQualifierValues (new_qualifier);
+          } catch (EntryInformationException e) {
+            new MessageDialog (getParentFrame (),
+                               "/" + qualifier_name +
+                               " not supported by this entry " +
+                               "- cannot continue");
+            return;
+          } catch (ReadOnlyException e) {
+            new MessageDialog (getParentFrame (),
+                               "one of the features is in a read-only " +
+                               "entry - cannot continue");
+            return;
+          }
+
+          current_number += increment;
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Automatically create gene names for all CDS features in the active
+   *  entries.
+   **/
+  private void autoGeneName () {
+    if (!checkForSelectionFeatures (getParentFrame (), getSelection ())) {
+      return;
+    }
+
+    final FeatureVector features_to_name = getSelection ().getAllFeatures ();
+
+    if (getReadOnlyFeatures (features_to_name).size () > 0) {
+      new MessageDialog (getParentFrame (),
+                         "one or more of the current features is " +
+                         "read-only - cannot continue");
+      return;
+    }
+
+    final TextDialog prefix_dialog =
+      new TextDialog (getParentFrame (),
+                      "enter the start characters of the new gene names:",
+                      18, "");
+
+    final String prefix_text = prefix_dialog.getText ();
+
+    if (prefix_text == null) {
+      return;
+    }
+
+    final String prefix_string = prefix_text.trim ();
+
+    if (prefix_string.length () == 0) {
+      new MessageDialog (getParentFrame (), "no prefix given");
+      return;
+    }
+
+    final TextDialog start_number_dialog =
+      new TextDialog (getParentFrame (),
+                      "start counting at:", 18, "1");
+
+    final String start_number_text = start_number_dialog.getText ();
+
+    if (start_number_text == null) {
+      return;
+    }
+
+    String start_string = start_number_text.trim ();
+
+    if (start_string.length () == 0) {
+      new MessageDialog (getParentFrame (), "no start given");
+      return;
+    }
+
+    final int start_value;
+
+    try {
+      start_value = Integer.valueOf (start_string).intValue ();
+    } catch (NumberFormatException e) {
+      new MessageDialog (getParentFrame (),
+                         "this is not a number: " + start_string);
+      return;
+    }
+
+    final TextDialog increment_dialog =
+      new TextDialog (getParentFrame (),
+                      "increment number by:", 18, "1");
+
+    final String increment_text = increment_dialog.getText ();
+
+    if (increment_text == null) {
+      return;
+    }
+
+    String increment_string = increment_text.trim ();
+
+    if (increment_string.length () == 0) {
+      new MessageDialog (getParentFrame (), "no increment given");
+      return;
+    }
+
+    final int increment_value;
+
+    try {
+      increment_value = Integer.valueOf (increment_string).intValue ();
+    } catch (NumberFormatException e) {
+      new MessageDialog (getParentFrame (),
+                         "this is not a number: " + increment_string);
+      return;
+    }
+
+    final TextDialog qualifier_name_dialog =
+      new TextDialog (getParentFrame (),
+                      "enter a qualifier name to use",
+                      18, "gene");
+
+    final String qualifier_name_text = qualifier_name_dialog.getText ();
+
+    if (qualifier_name_text == null) {
+      return;
+    }
+
+    final String qualifier_name_string = qualifier_name_text.trim ();
+
+    if (qualifier_name_string.length () == 0) {
+      new MessageDialog (getParentFrame (), "no qualifier name given");
+      return;
+    }
+
+    final YesNoDialog complement_tag_dialog =
+      new YesNoDialog (getParentFrame (),
+                       "append \"c\" to names of reverse strand features?");
+
+    autoGeneNameHelper (features_to_name,
+                        prefix_string, start_value, increment_value,
+                        qualifier_name_string,
+                        complement_tag_dialog.getResult ());
+  }
+
+  /**
+   *  Helper function for fixGeneNames().  Add the gene name from the CDS to
+   *  neighbouring/overlapping mRNA, intron, exon, gene, 5'UTR and 3'UTR
+   *  features.  Warn about inconsistencies.
+   *  @return true if all went well, false if fixing should stop immediately
+   **/
+  private static boolean fixGeneNamesHelper (final JFrame frame,
+                                             final EntryGroup entry_group,
+                                             final Feature cds_to_fix) {
+    if (cds_to_fix.isReadOnly ()) {
+      final String message =
+        "one or more of the of the selected features are read only " +
+        "- cannot continue";
+      new MessageDialog (frame, message);
+      return false;
+    }
+
+    try {
+      final Strand cds_to_fix_strand = cds_to_fix.getStrand ();
+
+      Marker cds_start_marker = cds_to_fix.getFirstBaseMarker ();
+      Marker cds_end_marker = cds_to_fix.getLastBaseMarker ();
+
+      // move the start one base back and the end one base forward so that
+      // when we call getFeaturesInRange() we get the UTRs
+
+      try {
+        cds_start_marker = cds_start_marker.moveBy (-1);
+      } catch (OutOfRangeException _) {
+        // ignore and use the original cds_start_marker
+      }
+
+      try {
+        cds_end_marker = cds_end_marker.moveBy (1);
+      } catch (OutOfRangeException _) {
+        // ignore and use the original cds_end_marker
+      }
+
+      final Range search_range;
+
+      if (cds_start_marker.getRawPosition () <
+          cds_end_marker.getRawPosition ()) {
+        search_range =
+          new Range (cds_start_marker.getRawPosition (),
+                     cds_end_marker.getRawPosition ());
+     } else {
+       search_range =
+         new Range (cds_end_marker.getRawPosition (),
+                    cds_start_marker.getRawPosition ());
+     }
+
+      final FeatureVector features_in_range =
+        entry_group.getFeaturesInRange (search_range);
+
+      final FeatureVector features_to_change =
+        new FeatureVector ();
+
+      for (int i = 0 ; i < features_in_range.size () ; ++i) {
+        final Feature this_feature = features_in_range.elementAt (i);
+
+        if (this_feature.getStrand () == cds_to_fix_strand &&
+            (this_feature.isCDS () ||
+             this_feature.getKey ().equals ("mRNA") ||
+             this_feature.getKey ().equals ("intron") ||
+             this_feature.getKey ().equals ("exon") ||
+             this_feature.getKey ().equals ("5'UTR") &&
+             (cds_to_fix.getFirstBase () ==
+              this_feature.getLastBase () + 1) ||
+             this_feature.getKey ().equals ("3'UTR") &&
+             (cds_to_fix.getLastBase () + 1 ==
+              this_feature.getFirstBase ()) ||
+             this_feature.getKey ().equals ("gene"))) {
+
+          if (this_feature.isReadOnly ()) {
+            final String message =
+              "one or more of the of the overlapping features are read only " +
+              "- cannot continue";
+            new MessageDialog (frame, message);
+            return false;
+          }
+          features_to_change.add (this_feature);
+        }
+      }
+
+      final FeatureVector overlapping_cds_features = new FeatureVector ();
+
+      for (int i = 0 ; i < features_to_change.size () ; ++i) {
+        final Feature this_test_feature = features_to_change.elementAt (i);
+
+        if (this_test_feature != cds_to_fix &&
+            this_test_feature.isCDS ()) {
+          overlapping_cds_features.add (this_test_feature);
+          features_to_change.remove (this_test_feature);
+        }
+      }
+
+      if (overlapping_cds_features.size () > 0) {
+        final String message =
+          "your CDS (" + cds_to_fix.getIDString () +
+          ") overlaps " + overlapping_cds_features.size () +
+          " other CDS feature" +
+          (overlapping_cds_features.size () == 1 ?
+           "" : "s") + " - continue?";
+
+        final YesNoDialog dialog =
+          new YesNoDialog (frame, message);
+
+        if (!dialog.getResult ()) {
+          return false;
+        }
+      }
+
+      final StringVector gene_names = new StringVector ();
+
+      for (int feature_index = 0 ;
+           feature_index < features_to_change.size () ;
+           ++feature_index) {
+        final Feature test_feature =
+          features_to_change.elementAt (feature_index);
+
+        final StringVector test_feature_gene_names =
+          test_feature.getValuesOfQualifier ("gene");
+
+        if (test_feature_gene_names != null) {
+          for (int test_feature_gene_name_index = 0 ;
+               test_feature_gene_name_index < test_feature_gene_names.size ();
+               ++test_feature_gene_name_index) {
+            final String this_gene_name =
+              test_feature_gene_names.elementAt (test_feature_gene_name_index);
+
+            if (!gene_names.contains (this_gene_name)) {
+              gene_names.add (this_gene_name);
+            }
+          }
+        }
+      }
+
+      if (gene_names.size () == 0) {
+        // ignore this feature, but continue with the other features
+        return true;
+      }
+
+      for (int feature_index = 0 ;
+           feature_index < features_to_change.size () ;
+           ++feature_index) {
+        final Feature this_feature =
+          features_to_change.elementAt (feature_index);
+        final Qualifier qualifier = new Qualifier ("gene", gene_names);
+
+        this_feature.setQualifier (qualifier);
+      }
+    } catch (OutOfRangeException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    } catch (InvalidRelationException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    } catch (EntryInformationException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    } catch (ReadOnlyException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+
+    return true;
+  }
+
+  /**
+   *  For each selected CDS, add the gene name from the CDS to
+   *  neighbouring/overlapping mRNA, intron, exon, gene, 5'UTR and 3'UTR
+   *  features.  Warn about inconsistencies.
+   *  @param frame The Frame to use for MessageDialog components.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   **/
+  public static void fixGeneNames (final JFrame frame,
+                                   final EntryGroup entry_group,
+                                   final Selection selection) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      final FeatureVector features_to_fix = selection.getAllFeatures ();
+
+      int cds_features_found = 0;
+
+      for (int i = 0 ; i < features_to_fix.size () ; ++i) {
+        final Feature selection_feature = features_to_fix.elementAt (i);
+
+        if (selection_feature.isCDS ()) {
+          ++cds_features_found;
+          if (!fixGeneNamesHelper (frame, entry_group, selection_feature)) {
+            return;
+          }
+        }
+      }
+
+      if (cds_features_found == 0) {
+        new MessageDialog (frame, "no CDS features selected");
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  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);
+  }
+
+  /**
+   *  Return the GotoEventSource object that was passed to the constructor.
+   **/
+  private GotoEventSource getGotoEventSource () {
+    return goto_event_source;
+  }
+
+  /**
+   *  Returns a Vector containing those features in the given vector of
+   *  features which are read only or are in a read only entry.
+   **/
+  private static FeatureVector
+    getReadOnlyFeatures (final FeatureVector features) {
+    final FeatureVector return_vector = new FeatureVector ();
+
+    for (int i = 0 ; i < features.size () ; ++i) {
+      final Feature this_feature = features.elementAt (i);
+      if (this_feature.isReadOnly ()) {
+        return_vector.add (this_feature);
+      }
+    }
+
+    return return_vector;
+  }
+
+  /**
+   *  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;
+
+  /**
+   *  The BasePlotGroup object that was passed to the constructor.
+   **/
+  private BasePlotGroup base_plot_group = null;
+
+  private JMenuItem undo_item = null;
+  private JMenuItem edit_feature_item = null;
+  private JMenuItem raw_edit_feature_item = null;
+  private JMenuItem duplicate_item = null;
+  private JMenuItem merge_features_item = null;
+  private JMenuItem unmerge_feature_item = null;
+  private JMenuItem delete_features_item = null;
+  private JMenuItem edit_header_item = null;
+  private JMenuItem add_qualifiers_item = null;
+  private JMenuItem remove_qualifier_item = null;
+  private JMenuItem delete_segments_item = null;
+  private JMenuItem delete_introns_item = null;
+  private JMenuItem edit_subsequence_item = null;
+  private JMenuItem paste_item = null;
+  private JMenu move_features_menu = null;
+  private JMenu copy_features_menu = null;
+  private JMenuItem trim_item = null;
+  private JMenuItem trim_to_any_item = null;
+  private JMenuItem trim_to_next_item = null;
+  private JMenuItem trim_to_next_any_item = null;
+  private JMenuItem extend_to_next_stop_item = null;
+  private JMenuItem extend_to_prev_stop_item = null;
+  private JMenuItem auto_label_item = null;
+  private JMenuItem auto_gene_name_item = null;
+  private JMenuItem fix_gene_names_item = null;
+  private JMenuItem fix_stop_codons_item = null;
+  private JMenuItem reverse_complement_item = null;
+  private JMenuItem delete_bases_item = null;
+  private JMenuItem add_bases_item = null;
+  private JMenuItem add_bases_from_file_item = null;
+}
diff --git a/uk/ac/sanger/artemis/components/EntryActionListener.java b/uk/ac/sanger/artemis/components/EntryActionListener.java
new file mode 100644
index 000000000..057a82d11
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EntryActionListener.java
@@ -0,0 +1,63 @@
+/* EntryActionListener.java
+ *
+ * created: Mon Jan 31 2000
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EntryActionListener.java,v 1.1 2004-06-09 09:46:21 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import java.awt.event.*;
+
+/**
+ *  This class is an implementation of the ActionListener interface that can
+ *  remember an entry.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: EntryActionListener.java,v 1.1 2004-06-09 09:46:21 tjc Exp $
+ **/
+
+abstract public class EntryActionListener implements ActionListener {
+  /**
+   *  Make a new EntryActionListener from the given Entry.
+   **/
+  EntryActionListener (final EntryEdit entry_edit,
+                       final Entry entry) {
+    this.entry = entry;
+    this.entry_edit = entry_edit;
+  }
+
+  abstract public void actionPerformed (final ActionEvent event);
+
+  public Entry getEntry () {
+    return entry;
+  }
+
+  public EntryEdit getEntryEdit () {
+    return entry_edit;
+  }
+
+  final private Entry entry;
+  final private EntryEdit entry_edit;
+}
+
diff --git a/uk/ac/sanger/artemis/components/EntryEdit.java b/uk/ac/sanger/artemis/components/EntryEdit.java
new file mode 100644
index 000000000..2bec3a1eb
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EntryEdit.java
@@ -0,0 +1,1434 @@
+/* EntryEdit.java
+ *
+ * created: Fri Oct  9 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 1998,1999,2000,2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EntryEdit.java,v 1.1 2004-06-09 09:46:24 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.Marker;
+import uk.ac.sanger.artemis.sequence.Bases;
+
+import uk.ac.sanger.artemis.util.FileDocument;
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.util.ReadOnlyException;
+import uk.ac.sanger.artemis.io.DocumentEntryFactory;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.SimpleEntryInformation;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.awt.dnd.*;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import javax.swing.*;
+import javax.swing.border.Border;
+import javax.swing.border.BevelBorder;
+
+/**
+ *  Each object of this class is used to edit an EntryGroup object.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: EntryEdit.java,v 1.1 2004-06-09 09:46:24 tjc Exp $
+ *
+ */
+
+public class EntryEdit extends JFrame
+    implements EntryGroupChangeListener, EntryChangeListener,
+               DropTargetListener 
+{
+
+  /** The shortcut for Delete Selected Features. */
+  final static KeyStroke SAVE_DEFAULT_KEY =
+    KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_MASK);
+
+  /**
+   *  A vector containing the Entry objects that this 
+   *  EntryEdit object knows about.
+   **/
+  private EntryGroup entry_group;
+
+  /**
+   *  Created by the constructor to pass to those objects
+   *  that are interested in GotoEvents.
+   **/
+  private GotoEventSource goto_event_source;
+
+  private final JMenuBar menu_bar  = new JMenuBar();
+  private final JMenu    file_menu = new JMenu("File");
+
+  private EntryGroupDisplay group_display;
+  private FeatureDisplay one_line_per_entry_display;
+  private FeatureDisplay feature_display;
+  private FeatureDisplay base_display;
+  private BasePlotGroup  base_plot_group;
+  private FeatureList feature_list;
+
+  /** This Object contains the current selection. */
+  private Selection selection = null;
+
+ /**
+  *  The EntrySourceVector reference that is created in the constructor.
+  **/
+  private EntrySourceVector entry_sources;
+
+
+  /**
+   *  Create a new EntryEdit object and JFrame.
+   *  @param entry_group The EntryGroup object that this component is editing.
+   */
+  public EntryEdit(final EntryGroup entry_group) 
+  {
+    super("Artemis Entry Edit");
+
+    setDropTarget(new DropTarget(this,this));
+    entry_group.ref();
+    this.entry_group = entry_group;
+
+    // XXX add a InputStreamProgressListener
+    this.entry_sources     = Utilities.getEntrySources(this, null);
+    this.goto_event_source = new SimpleGotoEventSource(getEntryGroup());
+
+    selection = new Selection(null);
+
+    getEntryGroup().addFeatureChangeListener(selection);
+    getEntryGroup().addEntryChangeListener(selection);
+    getEntryGroup().addEntryGroupChangeListener(this);
+    getEntryGroup().addEntryChangeListener(this);
+
+    if(getEntryGroup().getDefaultEntry() != null) 
+    {
+      final String name = getEntryGroup().getDefaultEntry().getName();
+      if(name != null) 
+        setTitle("Artemis Entry Edit: " + name);
+    }
+
+    final int font_height;
+    final int font_base_line;
+
+    final Font default_font = getDefaultFont();
+
+    if(default_font != null) 
+    {
+      FontMetrics fm = getFontMetrics(default_font);
+      font_height = fm.getHeight();
+      font_base_line = fm.getMaxAscent();
+    }
+    else 
+    {
+      font_height = -1;
+      font_base_line = -1;
+    }
+
+    addWindowListener(new WindowAdapter() 
+    {
+      public void windowClosing(WindowEvent event) 
+      {
+        closeEntryEdit();
+      }
+    });
+
+    Box box_panel = Box.createVerticalBox();
+
+    getContentPane().setLayout(new BorderLayout());
+    getContentPane().add(box_panel, "North");
+    
+    menu_bar.setFont(default_font);
+
+    SelectionInfoDisplay selection_info =
+      new SelectionInfoDisplay(getEntryGroup(), getSelection());
+    box_panel.add(selection_info);
+
+    group_display = new EntryGroupDisplay(this);
+    box_panel.add(group_display);
+
+    final boolean entry_buttons_option =
+      Options.getOptions().getPropertyTruthValue("show_entry_buttons");
+
+    group_display.setVisible(entry_buttons_option);
+
+    base_plot_group =
+      new BasePlotGroup(getEntryGroup(), this, getSelection(),
+                        getGotoEventSource());
+    box_panel.add(base_plot_group);
+
+    base_plot_group.setVisible(true);
+
+    one_line_per_entry_display =
+      new FeatureDisplay(getEntryGroup(), getSelection(),
+                         getGotoEventSource(), base_plot_group);
+
+    one_line_per_entry_display.setShowLabels(false);
+    one_line_per_entry_display.setOneLinePerEntry(true);
+
+    box_panel.add(one_line_per_entry_display);
+    one_line_per_entry_display.setVisible(false);
+    
+    feature_display =
+      new FeatureDisplay(getEntryGroup(), getSelection(),
+                         getGotoEventSource(), base_plot_group);
+
+    final Options options = Options.getOptions();
+
+    if(options.getProperty("overview_feature_labels") != null) 
+    {
+      final boolean option_value =
+        options.getPropertyTruthValue("overview_feature_labels");
+      feature_display.setShowLabels(option_value);
+    }
+
+    if(options.getProperty("overview_one_line_per_entry") != null) 
+    {
+      final boolean option_value =
+        options.getPropertyTruthValue("overview_one_line_per_entry");
+      feature_display.setOneLinePerEntry(option_value);
+    }
+
+    feature_display.addDisplayAdjustmentListener(base_plot_group);
+    feature_display.addDisplayAdjustmentListener(one_line_per_entry_display);
+
+    one_line_per_entry_display.addDisplayAdjustmentListener(feature_display);
+
+    box_panel.add(feature_display);
+
+    feature_display.setVisible(true);
+
+    base_display =
+      new FeatureDisplay(getEntryGroup(), getSelection(),
+                         getGotoEventSource(), base_plot_group);
+    base_display.setShowLabels(false);
+    base_display.setScaleFactor(0);
+    box_panel.add(base_display);
+
+    final boolean show_base_view;
+
+    if(Options.getOptions().getProperty("show_base_view") != null) 
+      show_base_view =
+        Options.getOptions().getPropertyTruthValue("show_base_view");
+    else 
+      show_base_view = true;
+
+    base_display.setVisible(show_base_view);
+
+    feature_list =
+      new FeatureList(getEntryGroup(), getSelection(),
+                      getGotoEventSource(), base_plot_group);
+    feature_list.setFont(default_font);
+
+    final boolean list_option_value =
+      Options.getOptions().getPropertyTruthValue("show_list");
+
+    if(list_option_value) 
+      feature_list.setVisible(true);
+    else 
+      feature_list.setVisible(false);
+
+    getContentPane().add(feature_list, "Center");
+    makeMenus();
+    pack();
+
+    ClassLoader cl = this.getClass().getClassLoader();
+    ImageIcon icon = new ImageIcon(cl.getResource("images/icon.gif"));
+
+    if(icon != null) 
+    {
+      Toolkit toolkit = Toolkit.getDefaultToolkit();
+      final Image icon_image = icon.getImage();
+      MediaTracker tracker = new MediaTracker(this);
+      tracker.addImage(icon_image, 0);
+  
+      try 
+      {
+        tracker.waitForAll();
+        setIconImage(icon_image);
+      }
+      catch(InterruptedException e) 
+      {
+        // ignore and continue
+      }
+    }
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    int screen_height = screen.height;
+    int screen_width  = screen.width;
+
+    if(screen_width <= 900 || screen_height <= 700) 
+      setSize(screen_width * 9 / 10, screen_height * 9 / 10);
+    else 
+      setSize(900, 700);
+
+    Utilities.centreFrame(this);
+  }
+
+  /**
+   *  If there are no unsaved changes, close this EntryEdit.  Otherwise ask
+   *  the user first.
+   **/
+  private void closeEntryEdit() 
+  {
+    if(getEntryGroup().hasUnsavedChanges() &&
+       getEntryGroup().refCount() == 1) 
+    {
+      final YesNoDialog yes_no_dialog =
+        new YesNoDialog(EntryEdit.this,
+                        "there are unsaved changes - really close?");
+
+      if(!yes_no_dialog.getResult()) 
+        return;
+    }
+
+    entryEditFinished();
+  }
+
+  /**
+   *  Redraw this component.  This method is public so that other classes can
+   *  force an update if, for example, the options files is re-read.
+   **/
+  public void redisplay() 
+  {
+    feature_display.redisplay();
+    base_display.redisplay();
+  }
+
+  /**
+   *  This method sends an GotoEvent to all the GotoEvent listeners that will
+   *  make the start of the first selected Feature or FeatureSegment visible.
+   **/
+  public void makeSelectionVisible() 
+  {
+    final Marker first_base = getSelection().getStartBaseOfSelection();
+    final GotoEvent new_event = new GotoEvent(this, first_base);
+    getGotoEventSource().sendGotoEvent(new_event);
+  }
+
+  /**
+   *  Return an object that implements the GotoEventSource interface, and is
+   *  the controlling object for Goto events associated with this object.
+   **/
+  public GotoEventSource getGotoEventSource()
+  {
+    return goto_event_source;
+  }
+
+  /**
+   *  Return the EntryGroup object that was passed to the constructor.
+   **/
+  public EntryGroup getEntryGroup() 
+  {
+    return entry_group;
+  }
+
+  /**
+   *  Returns a Selection object containing the selected features/exons.
+   **/
+  public Selection getSelection() 
+  {
+    return selection;
+  }
+
+  /**
+   *  Implementation of the EntryGroupChangeListener interface.  We listen to
+   *  EntryGroupChange events so that we can update the File menu when the
+   *  EntryGroup changes.
+   **/
+  public void entryGroupChanged(final EntryGroupChangeEvent event) 
+  {
+    switch(event.getType()) 
+    {
+      case EntryGroupChangeEvent.ENTRY_ADDED:
+      case EntryGroupChangeEvent.ENTRY_DELETED:
+        makeFileMenu();
+        break;
+    }
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so that we can update the File menu when an Entry
+   *  changes.
+   **/
+  public void entryChanged(final EntryChangeEvent event) 
+  {
+    if(event.getType() == EntryChangeEvent.NAME_CHANGED)
+      makeFileMenu();
+  }
+
+  /* private members: */
+
+  /**
+   *  This method arranges for the EntryEdit JFrame to go away.  This EntryEdit
+   *  object was created by the main program, so the main program must be the
+   *  one to delete us.
+   **/
+  private void entryEditFinished() 
+  {
+    setVisible(false);
+    getEntryGroup().removeFeatureChangeListener(selection);
+    getEntryGroup().removeEntryChangeListener(selection);
+
+    getEntryGroup().removeEntryGroupChangeListener(this);
+    getEntryGroup().removeEntryChangeListener(this);
+
+    getEntryGroup().unref();
+    dispose();
+  }
+
+  /**
+   *  Write the default Entry in the EntryGroup to the file it came from.
+   **/
+  private void saveDefaultEntry() 
+  {
+    if(getEntryGroup().getDefaultEntry() == null) 
+      new MessageDialog(EntryEdit.this, "There is no default entry");
+    else 
+      saveEntry(entry_group.getDefaultEntry(), true, false, true,
+                 DocumentEntryFactory.ANY_FORMAT);
+  }
+
+  /**
+   *  Save the given entry, prompting for a file name if necessary.
+   *  @param include_diana_extensions If true then any diana additions to
+   *    the embl file format will be included in the output, otherwise they
+   *    will be removed.  Also possible problems that would cause an entry to
+   *    bounce from the EMBL submission process will be flagged if this is
+   *    true.
+   *  @param ask_for_name If true then always prompt for a new filename,
+   *    otherwise prompt only when the entry name is not set.
+   *  @param keep_new_name If ask_for_name is true a file will be written with
+   *    the new name the user selects - if keep_new_name is true as well, then
+   *    the entry will have it's name set to the new name, otherwise it will
+   *    be used for this save and then discarded.
+   *  @param destination_type Should be one of EMBL_FORMAT, GENBANK_FORMAT or
+   *    ANY_FORMAT.  If ANY_FORMAT then the Entry will be saved in the
+   *    same format it was created, otherwise it will be saved in the given
+   *    format.
+   **/
+  void saveEntry(final Entry entry,
+                 final boolean include_diana_extensions,
+                 final boolean ask_for_name, final boolean keep_new_name,
+                 final int destination_type) 
+  {
+
+    if(!include_diana_extensions) 
+    {
+      if(displaySaveWarnings(entry)) 
+        return;
+    }
+
+    if(destination_type != DocumentEntryFactory.ANY_FORMAT &&
+       entry.getHeaderText() != null) 
+    {
+      final YesNoDialog yes_no_dialog =
+        new YesNoDialog(this, "header section will be lost. continue?");
+
+      if(!yes_no_dialog.getResult()) 
+        return;
+    }
+
+    if(!System.getProperty("os.arch").equals("alpha"))
+    {
+      final EntryFileDialog file_dialog = new EntryFileDialog(this,
+                                                              false);
+
+      file_dialog.saveEntry(entry, include_diana_extensions, ask_for_name,
+                          keep_new_name, destination_type);
+    }
+    else
+      alphaBug(entry, include_diana_extensions, ask_for_name,
+               keep_new_name, destination_type);
+  }
+
+
+  /**
+  *
+  * This routine is needed to circumvent the problem
+  * of opening a JFileChooser on alpha o/s only.
+  *
+  */
+  private void alphaBug(Entry entry, 
+                        boolean include_diana_extensions,
+                        boolean ask_for_name,
+                        boolean keep_new_name,
+                        int destination_type)
+  {
+    try
+    {
+      if(ask_for_name || entry.getName() == null)
+      {
+        JCheckBox emblHeader = new JCheckBox("Add EMBL Header",
+                                              false);
+        Box bdown = Box.createVerticalBox();
+        JTextField fileField = new JTextField(
+                   System.getProperty("user.dir")+
+                   System.getProperty("file.separator"));
+        fileField.selectAll();
+        bdown.add(fileField);
+   
+        if(destination_type == DocumentEntryFactory.EMBL_FORMAT)
+          bdown.add(emblHeader);
+
+        int n = JOptionPane.showConfirmDialog(null, bdown,
+                            "Enter filename",
+                            JOptionPane.OK_CANCEL_OPTION,
+                            JOptionPane.QUESTION_MESSAGE);
+        if(n == JOptionPane.CANCEL_OPTION)
+          return;
+
+        if(emblHeader.isSelected())
+        {
+          if( entry.getHeaderText() == null || 
+              isHeaderEMBL(entry.getHeaderText()) )
+            System.out.println("NO HEADER PLEASE SET");
+        }
+
+        File file = new File(fileField.getText());
+
+        if(file.exists())
+        {
+          final YesNoDialog yes_no_dialog = new YesNoDialog(this,
+                           "this file exists: " + file.getName() +
+                           " overwrite it?");
+          if(!yes_no_dialog.getResult())
+            return;
+        }
+
+        final MessageDialog message = new MessageDialog(this,
+                        "saving to " + file.getName() + " ...",
+                        false);
+        try
+        {
+          if(include_diana_extensions)
+            entry.save(file, destination_type, false);
+          else
+            entry.saveStandardOnly(file, destination_type, true);
+        }
+        catch(EntryInformationException e)
+        {
+          final YesNoDialog yes_no_dialog = new YesNoDialog(this,
+                         "destination format can't handle all " +
+                         "keys/qualifiers - continue?");
+          if(yes_no_dialog.getResult())
+          {
+            try
+            {
+              if(include_diana_extensions)
+                entry.save(file, destination_type, true);
+              else
+                entry.saveStandardOnly(file, destination_type, true);
+            }
+            catch(EntryInformationException e2)
+            {
+              throw new Error("internal error - unexpected exception: "+ e);
+            }
+            catch(IOException ioe)
+            {
+              new MessageDialog(this, "error while saving: " + ioe);
+              return;
+            }
+          }
+          else
+            return;
+        }
+        finally
+        {
+          if(message != null)
+            message.dispose();
+        }
+
+        if(keep_new_name)
+          entry.setName(file.getName());
+      }
+      else
+      {
+        final MessageDialog message = new MessageDialog(this,
+                             "saving to " + entry.getName() + " ...",
+                             false);
+        try
+        {
+          if(include_diana_extensions)
+            entry.save(destination_type);
+          else
+            entry.saveStandardOnly(destination_type);
+        }
+        finally
+        {
+          message.dispose();
+        }
+      }
+    }
+    catch(ReadOnlyException e)
+    {
+      new MessageDialog(this, "this entry is read only");
+      return;
+    }
+    catch(IOException e)
+    {
+      new MessageDialog(this, "error while saving: " + e);
+      return;
+    }
+    catch(EntryInformationException e)
+    {
+      new MessageDialog(this, "error while saving: " + e);
+      return;
+    }
+  }
+
+
+  private boolean isHeaderEMBL(String header)
+  {
+    StringReader reader = new StringReader(header);
+    BufferedReader buff_reader = new BufferedReader(reader);
+ 
+    try
+    {   
+      if(!buff_reader.readLine().startsWith("ID"))
+        return false;
+    }
+    catch(IOException ioe){}
+    return true;
+  }
+
+  /**
+   *  Save the changes to all the Entry objects in the entry_group back to
+   *  where the they came from.
+   **/
+  public void saveAllEntries() 
+  {
+    for(int entry_index = 0 ;
+        entry_index < entry_group.size() ;
+        ++entry_index) 
+      saveEntry(entry_group.elementAt(entry_index), true, false, true,
+                DocumentEntryFactory.ANY_FORMAT);
+  }
+
+  /**
+   *  Check the given Entry for invalid EMBL features(such as CDS features
+   *  without a stop codon) then display a FeatureListFrame list the problem
+   *  features.
+   *  @return true if and only if the save should be aborted.
+   **/
+  private boolean displaySaveWarnings(final Entry entry) 
+  {
+    final FeatureVector invalid_starts = entry.checkFeatureStartCodons();
+    final FeatureVector invalid_stops = entry.checkFeatureStopCodons();
+    final FeatureVector invalid_keys = entry.checkForNonEMBLKeys();
+    final FeatureVector duplicate_features = entry.checkForEMBLDuplicates();
+    final FeatureVector overlapping_cds_features =
+      entry.checkForOverlappingCDSs();
+    final FeatureVector missing_qualifier_features =
+      entry.checkForMissingQualifiers();
+
+    // this predicate will filter out those features that aren't in the
+    // entry we are trying to save
+    final FeaturePredicate predicate =
+      new FeaturePredicate() 
+    {
+      public boolean testPredicate(final Feature feature)
+      {
+        if(feature.getEntry() == entry) 
+          return true;
+        else 
+          return false;
+      }
+    };
+
+    String entry_name = entry.getName();
+
+    if(entry_name == null) 
+      entry_name = "no name";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup(getEntryGroup(), predicate,
+                             "features from " + entry_name);
+
+    if(invalid_starts.size() + invalid_stops.size() +
+       invalid_keys.size() + duplicate_features.size() +
+       overlapping_cds_features.size() > 0) 
+    {
+
+      final YesNoDialog yes_no_dialog =
+        new YesNoDialog(this,
+                         "warning: some features may have problems. " +
+                         "continue with save?");
+
+      if(!yes_no_dialog.getResult()) 
+      {
+        getSelection().clear();
+
+        if(invalid_starts.size() > 0) 
+        {
+          getSelection().add(invalid_starts);
+
+          ViewMenu.showBadStartCodons(this,
+                                      getSelection(),
+                                      filtered_entry_group,
+                                      getGotoEventSource(),
+                                      base_plot_group);
+        }
+
+        if(invalid_stops.size() > 0) 
+        {
+          getSelection().add(invalid_stops);
+
+          ViewMenu.showBadStopCodons(this, getSelection(),
+                                     filtered_entry_group,
+                                     getGotoEventSource(),
+                                     base_plot_group);
+        }
+
+        if(invalid_keys.size() > 0) 
+        {
+          getSelection().add(invalid_keys);
+
+          ViewMenu.showNonEMBLKeys(this, getSelection(),
+                                   filtered_entry_group,
+                                   getGotoEventSource(),
+                                   base_plot_group);
+        }
+
+        if(duplicate_features.size() > 0) 
+        {
+          getSelection().add(duplicate_features);
+
+          ViewMenu.showDuplicatedFeatures(this, getSelection(),
+                                          filtered_entry_group,
+                                          getGotoEventSource(),
+                                          base_plot_group);
+        }
+
+        if(overlapping_cds_features.size() > 0)
+        {
+          getSelection().add(overlapping_cds_features);
+
+          ViewMenu.showOverlappingCDSs(this, getSelection(),
+                                       filtered_entry_group,
+                                       getGotoEventSource(),
+                                       base_plot_group);
+        }
+
+        if(missing_qualifier_features.size() > 0) 
+        {
+          getSelection().add(missing_qualifier_features);
+
+          ViewMenu.showMissingQualifierFeatures(this, getSelection(),
+                                                filtered_entry_group,
+                                                getGotoEventSource(),
+                                                base_plot_group);
+        }
+
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   *  Make and add the menus for this component.
+   **/
+  private void makeMenus() 
+  {
+    final Font default_font = getDefaultFont();
+
+    setJMenuBar(menu_bar);
+    makeFileMenu();
+    menu_bar.add(file_menu);
+
+    // don't add the menu if this is an applet and we have just one entry
+    if(Options.readWritePossible() || getEntryGroup().size() > 1) 
+    {
+      JMenu entry_group_menu = new EntryGroupMenu(this, getEntryGroup());
+      menu_bar.add(entry_group_menu);
+    }
+
+    JMenu select_menu = new SelectMenu(this, getSelection(),
+                     getGotoEventSource(), getEntryGroup(),
+                     base_plot_group);
+    menu_bar.add(select_menu);
+
+    ViewMenu view_menu = new ViewMenu(this, getSelection(),
+                             getGotoEventSource(), getEntryGroup(),
+                             base_plot_group);
+    menu_bar.add(view_menu);
+
+    JMenu goto_menu = new GotoMenu(this, getSelection(),
+                             getGotoEventSource(), getEntryGroup());
+    menu_bar.add(goto_menu);
+
+    if(Options.readWritePossible()) 
+    {
+      JMenu edit_menu = new EditMenu(this, getSelection(),
+                               getGotoEventSource(), getEntryGroup(),
+                               base_plot_group);
+      menu_bar.add(edit_menu);
+
+      AddMenu add_menu = new AddMenu(this, getSelection(), getEntryGroup(),
+                             getGotoEventSource(), base_plot_group);
+      menu_bar.add(add_menu);
+
+      JMenu write_menu = new WriteMenu(this, getSelection(), getEntryGroup());
+      menu_bar.add(write_menu);
+
+      if(Options.isUnixHost()) 
+      {
+        JMenu run_menu = new RunMenu(this, getSelection());
+        menu_bar.add(run_menu);
+      }
+    }
+
+    JMenu graph_menu = new GraphMenu(this, getEntryGroup(),
+                                     base_plot_group,
+                                     feature_display);
+    menu_bar.add(graph_menu);
+
+    final JMenu display_menu = new JMenu("Display");
+    final JCheckBoxMenuItem show_entry_buttons_item =
+                           new JCheckBoxMenuItem("Show Entry Buttons");
+    show_entry_buttons_item.setState(true);
+    show_entry_buttons_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        group_display.setVisible(show_entry_buttons_item.getState());
+        // XXX change to revalidate().
+        validate();
+      }
+    });
+    display_menu.add(show_entry_buttons_item);
+
+    final JCheckBoxMenuItem show_one_line =
+      new JCheckBoxMenuItem("Show One Line Per Entry View", false);
+    show_one_line.setState(one_line_per_entry_display.isVisible());
+    show_one_line.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        one_line_per_entry_display.setVisible(show_one_line.getState());
+        validate();
+      }
+    });
+    display_menu.add(show_one_line);
+
+    final JCheckBoxMenuItem show_base_display_item =
+                                new JCheckBoxMenuItem("Show Base View");
+
+    show_base_display_item.setState(base_display.isVisible());
+    show_base_display_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        base_display.setVisible(show_base_display_item.getState());
+        // XXX change to revalidate().
+        validate();
+      }
+    });
+    display_menu.add(show_base_display_item);
+
+    final JCheckBoxMenuItem show_feature_list_item =
+                        new JCheckBoxMenuItem("Show Feature List");
+    show_feature_list_item.setState(feature_list.isVisible());
+    show_feature_list_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        feature_list.setVisible(show_feature_list_item.getState());
+        // XXX change to revalidate().
+        validate();
+      }
+    });
+    display_menu.add(show_feature_list_item);
+
+    menu_bar.add(display_menu);
+  }
+
+
+  /**
+   *  Make a new File menureplacing the current one (if any).
+   **/
+  private void makeFileMenu() 
+  {
+    file_menu.removeAll();
+
+    if(Options.readWritePossible()) 
+    {
+
+      JMenuItem popFileManager = new JMenuItem("Show File Manager ...");
+      popFileManager.addActionListener(new ActionListener()
+      {
+        public void actionPerformed(ActionEvent event)
+        {
+          if(ArtemisMain.filemanager == null)
+            ArtemisMain.filemanager = new FileManager(EntryEdit.this);
+          else
+            ArtemisMain.filemanager.setVisible(true);
+        }
+      });
+      file_menu.add(popFileManager);
+
+      // only the standalone version can save or read
+      EntrySource filesystem_entry_source = null;
+
+      for(int source_index = 0; source_index < entry_sources.size();
+          ++source_index) 
+      {
+        final EntrySource this_source =
+          entry_sources.elementAt(source_index);
+
+        if(this_source.isFullEntrySource()) 
+          continue;
+
+        if(this_source.getSourceName().equals("Filesystem")) 
+          filesystem_entry_source = this_source;
+
+        String entry_source_name = this_source.getSourceName();
+        String menu_item_name = null;
+
+        if(entry_source_name.equals("Filesystem")) 
+          menu_item_name = "Read An Entry ...";
+        else 
+          menu_item_name = "Read An Entry From " + entry_source_name + " ...";
+
+        final JMenuItem read_entry = new JMenuItem(menu_item_name);
+
+        read_entry.addActionListener(new ActionListener() 
+        {
+          public void actionPerformed(ActionEvent event) 
+          {
+            readAnEntry(this_source);
+          }
+        });
+
+        file_menu.add(read_entry);
+      }
+
+      JMenu read_features_menu = null;
+
+      if(filesystem_entry_source != null &&
+          entry_group != null && entry_group.size() > 0) 
+      {
+        read_features_menu = new JMenu("Read Entry Into");
+        file_menu.add(read_features_menu);
+      }
+
+      file_menu.addSeparator();
+
+      final JMenuItem save_default =
+        new JMenuItem("Save Default Entry");
+      save_default.setAccelerator(SAVE_DEFAULT_KEY);
+      save_default.addActionListener(new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          saveDefaultEntry();
+        }
+      });
+
+      file_menu.add(save_default);
+
+      if(entry_group == null || entry_group.size() == 0) 
+      {
+        // do nothing
+      } 
+      else 
+      {
+        final JMenu save_entry_menu = new JMenu("Save An Entry");
+        final JMenu save_as_menu    = new JMenu("Save An Entry As");
+        final JMenu save_as         = new JMenu("New File");
+        final JMenu save_as_embl    = new JMenu("EMBL Format");
+        final JMenu save_as_genbank = new JMenu("GENBANK Format");
+        final JMenu save_as_gff     = new JMenu("GFF Format");
+        final JMenu save_embl_only  = new JMenu("EMBL Submission Format");
+
+        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 ActionListener save_entry_listener =
+            new SaveEntryActionListener(this, this_entry);
+
+          final JMenuItem save_entry_item = new JMenuItem(entry_name);
+
+          save_entry_item.addActionListener(save_entry_listener);
+
+          final ActionListener save_as_listener =
+            new SaveEntryAsActionListener(this, this_entry);
+
+          final JMenuItem save_as_item = new JMenuItem(entry_name);
+
+          save_as_item.addActionListener(save_as_listener);
+
+          final ActionListener save_as_embl_listener =
+            new SaveEntryAsEMBLActionListener(this, this_entry);
+
+          final JMenuItem save_as_embl_item = new JMenuItem(entry_name);
+
+          save_as_embl_item.addActionListener(save_as_embl_listener);
+
+          final ActionListener save_as_genbank_listener =
+            new SaveEntryAsGenbankActionListener(this, this_entry);
+
+          final JMenuItem save_as_genbank_item = new JMenuItem(entry_name);
+
+          save_as_genbank_item.addActionListener(save_as_genbank_listener);
+
+          final ActionListener save_as_gff_listener =
+            new SaveEntryAsGFFActionListener(this, this_entry);
+
+          final JMenuItem save_as_gff_item = new JMenuItem(entry_name);
+
+          save_as_gff_item.addActionListener(save_as_gff_listener);
+
+          final ActionListener save_embl_only_listener =
+            new SaveEntryAsSubmissionActionListener(this, this_entry);
+
+          final JMenuItem save_embl_only_item = new JMenuItem(entry_name);
+
+          save_embl_only_item.addActionListener(save_embl_only_listener);
+
+          if(read_features_menu != null)  
+          {
+            final ActionListener read_into_listener =
+              new ReadFeaturesActionListener(this, filesystem_entry_source,
+                                              this_entry);
+
+            final JMenuItem read_into_item = new JMenuItem(entry_name);
+            read_into_item.addActionListener(read_into_listener);
+            read_features_menu.add(read_into_item);
+          }
+
+          save_entry_menu.add(save_entry_item);
+
+          save_as.add(save_as_item);
+          save_as_embl.add(save_as_embl_item);
+          save_as_genbank.add(save_as_genbank_item);
+          save_as_gff.add(save_as_gff_item);
+          save_embl_only.add(save_embl_only_item);
+        }
+
+        save_as_menu.add(save_as);
+        save_as_menu.add(save_as_embl);
+        save_as_menu.add(save_as_genbank);
+        save_as_menu.add(save_as_gff);
+        save_as_menu.addSeparator();
+        save_as_menu.add(save_embl_only);
+
+        file_menu.add(save_entry_menu);
+        file_menu.add(save_as_menu);
+      }
+
+      final JMenuItem save_all = new JMenuItem("Save All Entries");
+      save_all.addActionListener(new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          saveAllEntries();
+        }
+      });
+
+      file_menu.add(save_all);
+      file_menu.addSeparator();
+    }
+
+    final JMenuItem clone_entry_edit = new JMenuItem("Clone This Window");
+    clone_entry_edit.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event) 
+      {
+        new EntryEdit(getEntryGroup()).show();
+      }
+    });
+
+    file_menu.add(clone_entry_edit);
+    file_menu.addSeparator();
+
+
+    final JMenuItem close = new JMenuItem("Close");
+    close.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event) 
+      {
+        closeEntryEdit();
+      }
+    });
+
+    file_menu.add(close);
+  }
+
+  /**
+   *  Read an entry
+   **/
+  private void readAnEntry(final EntrySource this_source)
+  {
+    final ProgressThread progress_thread = new ProgressThread(null,
+                                               "Loading Entry...");
+
+    SwingWorker entryWorker = new SwingWorker()
+    {
+      public Object construct()
+      {
+        try
+        {
+          final Entry new_entry = this_source.getEntry(entry_group.getBases(),
+                                                   progress_thread, true);
+          if(new_entry != null)
+            getEntryGroup().add(new_entry);
+        }
+        catch(final OutOfRangeException e)
+        {
+          new MessageDialog(EntryEdit.this,
+                         "read failed: one of the features " +
+                         "in the entry has an out of " +
+                         "range location: " +
+                         e.getMessage());
+        }
+        catch(final IOException e)
+        {
+          new MessageDialog(EntryEdit.this,
+                         "read failed due to an IO error: " +
+                         e.getMessage());
+        }
+        return null;
+      }
+
+       public void finished()
+       {
+         if(progress_thread !=null)
+           progress_thread.finished();
+       }
+    };
+    entryWorker.start();
+  }
+
+  /**
+   *  Read an entry
+   **/
+  private void readAnEntryFromFile(final File file)
+  {
+    final ProgressThread progress_thread = new ProgressThread(null,
+                                               "Loading Entry...");
+    progress_thread.start();
+
+    SwingWorker entryWorker = new SwingWorker()
+    {
+      public Object construct()
+      {
+        try
+        {
+          EntryInformation new_entry_information =
+             new SimpleEntryInformation(Options.getArtemisEntryInformation());
+
+          final Entry new_entry =  new Entry(entry_group.getBases(),
+                         EntryFileDialog.getEntryFromFile(null,
+                          new FileDocument(file),
+                          new_entry_information, false));
+
+          if(new_entry != null)
+            getEntryGroup().add(new_entry);
+        }
+        catch(final OutOfRangeException e)
+        {
+          new MessageDialog(EntryEdit.this,
+                         "read failed: one of the features " +
+                         "in the entry has an out of " +
+                         "range location: " +
+                         e.getMessage());
+        }
+        return null;
+      }
+
+       public void finished()
+       {
+         if(progress_thread !=null)
+           progress_thread.finished();
+       }
+    };
+    entryWorker.start();
+  }
+
+  /**
+   *  Return the current default font(from Diana.options).
+   **/
+  private Font getDefaultFont() 
+  {
+    return Options.getOptions().getFont();
+  }
+
+  // DropTargetListener methods
+  protected static Border dropBorder = new BevelBorder(BevelBorder.LOWERED);
+  public void drop(DropTargetDropEvent e)
+  {
+    Transferable t = e.getTransferable();
+    try
+    {
+      if(t.isDataFlavorSupported(FileNode.FILENODE))
+      {
+        FileNode fn = (FileNode)t.getTransferData(FileNode.FILENODE);
+        readAnEntryFromFile(fn.getFile());
+      }
+      else
+        e.rejectDrop();
+    }
+    catch(UnsupportedFlavorException ufe)
+    {
+      ufe.printStackTrace();
+    }
+    catch(IOException ioe)
+    {
+      ioe.printStackTrace();
+    }
+    finally
+    {
+      ((JComponent)getContentPane()).setBorder(null);
+    }
+  }
+
+  public void dragExit(DropTargetEvent e)
+  {
+    ((JComponent)getContentPane()).setBorder(null);
+  }
+  public void dropActionChanged(DropTargetDragEvent e) {}
+
+  public void dragOver(DropTargetDragEvent e)
+  {
+    if(e.isDataFlavorSupported(FileNode.FILENODE))
+    {
+      Point ploc = e.getLocation();
+      if(this.contains(ploc.x,ploc.y))
+      {
+        ((JComponent)getContentPane()).setBorder(dropBorder);
+        e.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+      }
+      else
+        e.rejectDrag();
+    }
+  }
+
+  public void dragEnter(DropTargetDragEvent e)
+  {
+    if(e.isDataFlavorSupported(FileNode.FILENODE))
+      e.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+  }
+
+}
+
+/**
+ *  This is an EntryActionListener that will get an entry from entry_source
+ *  and then copy them to destination_entry when actionPerformed() is called.
+ **/
+class ReadFeaturesActionListener extends EntryActionListener 
+{
+  final EntrySource entry_source;
+
+  ReadFeaturesActionListener(final EntryEdit entry_edit,
+                             final EntrySource entry_source,
+                             final Entry destination_entry) 
+  {
+    super(entry_edit, destination_entry);
+    this.entry_source = entry_source;
+  }
+
+  public void actionPerformed(final ActionEvent event) 
+  {
+    try 
+    {
+      if(getEntry().isReadOnly()) 
+      {
+        final String message =
+          "the default entry is read only - cannot continue";
+        new MessageDialog(getEntryEdit(), message);
+      }
+
+      final Entry source_entry =
+        entry_source.getEntry(getEntryEdit().getEntryGroup().getBases(),
+                              true);
+
+      for(int i = 0; i < source_entry.getFeatureCount(); ++i) 
+      {
+        final Feature this_feature = source_entry.getFeature(i);
+        try 
+        {
+          this_feature.copyTo(getEntry());
+        }
+        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(getEntryEdit(), message);
+        }
+        catch(ReadOnlyException e) 
+        {
+          final String message =
+            "the default entry is read only - cannot continue";
+          new MessageDialog(getEntryEdit(), message);
+          return;
+        }
+      }
+    } 
+    catch(OutOfRangeException e) 
+    {
+      new MessageDialog(getEntryEdit(),
+                         "read failed: one of the features in " +
+                         "the source entry has an out of range location");
+    } 
+    catch(IOException e) 
+    {
+      new MessageDialog(getEntryEdit(),
+                         "read failed due to an IO error: " +
+                         e.getMessage());
+    }
+    catch(NullPointerException e)
+    {
+      new MessageDialog(getEntryEdit(),
+                         "read failed due to a null pointer error: " +
+                         e.getMessage());
+    }
+
+  }
+
+}
+
+/**
+ *  This is an EntryActionListener that will call saveEntry() when
+ *  actionPerformed() is called.
+ **/
+class SaveEntryActionListener extends EntryActionListener 
+{
+  SaveEntryActionListener(final EntryEdit entry_edit,
+                           final Entry entry) 
+  {
+    super(entry_edit, entry);
+  }
+
+  public void actionPerformed(final ActionEvent event) 
+  {
+    getEntryEdit().saveEntry(getEntry(), true, false, true,
+                               DocumentEntryFactory.ANY_FORMAT);
+  }
+}
+
+/**
+ *  This is an EntryActionListener that will call saveEntry() when
+ *  actionPerformed() is called.
+ **/
+class SaveEntryAsActionListener extends EntryActionListener 
+{
+  SaveEntryAsActionListener(final EntryEdit entry_edit,
+                             final Entry entry) 
+  {
+    super(entry_edit, entry);
+  }
+
+  public void actionPerformed(final ActionEvent event) 
+  {
+    getEntryEdit().saveEntry(getEntry(), true, true, true,
+                               DocumentEntryFactory.ANY_FORMAT);
+  }
+}
+
+/**
+ *  This is an EntryActionListener that will call saveEntry() when
+ *  actionPerformed() is called.  The output file type will be EMBL.
+ **/
+class SaveEntryAsEMBLActionListener extends EntryActionListener 
+{
+  SaveEntryAsEMBLActionListener(final EntryEdit entry_edit,
+                                 final Entry entry) 
+  {
+    super(entry_edit, entry);
+  }
+
+  public void actionPerformed(final ActionEvent event) 
+  {
+    getEntryEdit().saveEntry(getEntry(), true, true, false,
+                               DocumentEntryFactory.EMBL_FORMAT);
+  }
+}
+
+/**
+ *  This is an EntryActionListener that will call saveEntry() when
+ *  actionPerformed() is called.  The output file type will be GENBANK.
+ **/
+class SaveEntryAsGenbankActionListener extends EntryActionListener 
+{
+  SaveEntryAsGenbankActionListener(final EntryEdit entry_edit,
+                                   final Entry entry) 
+  {
+    super(entry_edit, entry);
+  }
+
+  public void actionPerformed(final ActionEvent event) 
+  {
+    getEntryEdit().saveEntry(getEntry(), true, true, false,
+                             DocumentEntryFactory.GENBANK_FORMAT);
+  }
+}
+
+/**
+ *  This is an EntryActionListener that will call saveEntry() when
+ *  actionPerformed() is called.  The output file type will be GFF, with the
+ *  sequence(if any) in FASTA format.
+ **/
+class SaveEntryAsGFFActionListener extends EntryActionListener 
+{
+  SaveEntryAsGFFActionListener(final EntryEdit entry_edit,
+                                    final Entry entry) 
+  {
+    super(entry_edit, entry);
+  }
+
+  public void actionPerformed(final ActionEvent event) 
+  {
+    getEntryEdit().saveEntry(getEntry(), true, true, false,
+                             DocumentEntryFactory.GFF_FORMAT);
+  }
+}
+
+/**
+ *  This is an EntryActionListener that will call saveEntry() when
+ *  actionPerformed() is called.
+ **/
+class SaveEntryAsSubmissionActionListener extends EntryActionListener 
+{
+  SaveEntryAsSubmissionActionListener(final EntryEdit entry_edit,
+                                       final Entry entry) 
+  {
+    super(entry_edit, entry);
+  }
+
+  public void actionPerformed(final ActionEvent event) 
+  {
+    getEntryEdit().saveEntry(getEntry(), false, true, false,
+                             DocumentEntryFactory.ANY_FORMAT);
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/EntryEditVector.java b/uk/ac/sanger/artemis/components/EntryEditVector.java
new file mode 100644
index 000000000..68421296a
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EntryEditVector.java
@@ -0,0 +1,92 @@
+/* EntryEditVector.java
+ *
+ * created: Sat Oct 17 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EntryEditVector.java,v 1.1 2004-06-09 09:46:27 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.util.Vector;
+
+/**
+ *  This class implements a Vector of EntryEdit objects.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: EntryEditVector.java,v 1.1 2004-06-09 09:46:27 tjc Exp $
+ *
+ **/
+
+public class EntryEditVector {
+  /**
+   *  Create a new, entry EntryEditVector.
+   **/
+  public EntryEditVector () {
+
+  }
+
+  /**
+   *  Performs the same function as Vector.addElement ()
+   */
+  public void addElement (EntryEdit node) {
+    vector.addElement (node);
+  }
+  
+  /**
+   *  Performs the same function as Vector.elementAt ()
+   */
+  public EntryEdit elementAt (int index) {
+    return (EntryEdit) vector.elementAt (index);
+  }
+
+//    /**
+//     *  Performs the same function as Vector.lastElement ()
+//     **/
+//    public Feature lastElement () {
+//      return (Feature) vector.lastElement ();
+//    }
+
+  /**
+   *  Performs the same function as Vector.removeElement ()
+   **/
+  public boolean removeElement (EntryEdit entry_edit) {
+    return vector.removeElement (entry_edit);
+  }
+
+  /**
+   *  Performs the same function as Vector.removeElement ()
+   **/
+  public int indexOf (EntryEdit entry_edit) {
+    return vector.indexOf (entry_edit);
+  }
+
+  /**
+   *  Performs the same function as Vector.size ()
+   */
+  public int size () {
+    return vector.size ();
+  }
+  
+  /**
+   *  Storage for EntryEdit objects.
+   */
+  final private Vector vector = new Vector ();
+}
diff --git a/uk/ac/sanger/artemis/components/EntryFileDialog.java b/uk/ac/sanger/artemis/components/EntryFileDialog.java
new file mode 100644
index 000000000..86c1e9d1a
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EntryFileDialog.java
@@ -0,0 +1,463 @@
+/* EntryFileDialog.java
+ *
+ * created: Mon Dec  7 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 1998-2003  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EntryFileDialog.java,v 1.1 2004-06-09 09:46:28 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Options;
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.io.Entry;
+import uk.ac.sanger.artemis.io.DocumentEntryFactory;
+import uk.ac.sanger.artemis.io.ReadFormatException;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+import javax.swing.*;
+
+/**
+ *  This class is a JFileChooser that can read EMBL Entry objects.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: EntryFileDialog.java,v 1.1 2004-06-09 09:46:28 tjc Exp $
+ **/
+
+public class EntryFileDialog extends StickyFileChooser 
+{
+
+  /** JFrame reference that was passed to the constructor. */
+  private JFrame owner = null;
+
+  /**
+   *  Create a new EntryFileDialog component.
+   *  @param owner The component where this dialog was created.
+   *  @param sequence_only If true default to showing only file that have
+   *    suffixes that suggest that the files contain sequence(eg. .embl,
+   *    .seq, .dna).  Of false show all files that can be read.
+   **/
+  public EntryFileDialog(final JFrame owner,
+                         final boolean show_sequence_only) 
+  {
+    super();
+    this.owner = owner;
+
+    setFileSelectionMode(JFileChooser.FILES_ONLY);
+    setMultiSelectionEnabled(false);
+
+    final StringVector sequence_suffixes =
+      Options.getOptions().getOptionValues("sequence_file_suffixes");
+
+    final StringVector feature_suffixes =
+      Options.getOptions().getOptionValues("feature_file_suffixes");
+
+    final javax.swing.filechooser.FileFilter artemis_filter =
+      new javax.swing.filechooser.FileFilter()
+    {
+        public boolean accept(final File file) 
+        {
+          if(file.isDirectory()) 
+            return true;
+
+          for(int i = 0; i<sequence_suffixes.size(); ++i) 
+          {
+            final String this_suffix = sequence_suffixes.elementAt(i);
+
+            if(file.getName().endsWith("." + this_suffix) ||
+                file.getName().endsWith("." + this_suffix + ".gz")) 
+              return true;
+          }
+
+          for(int i = 0; i<feature_suffixes.size(); ++i) 
+          {
+            final String this_suffix = feature_suffixes.elementAt(i);
+
+            if(file.getName().endsWith("." + this_suffix) ||
+               file.getName().endsWith("." + this_suffix + ".gz")) 
+              return true;
+          }
+          return false;
+        }
+
+        public String getDescription() 
+        {
+          return "Artemis files";
+        }
+      };
+
+    final javax.swing.filechooser.FileFilter feature_filter =
+      new javax.swing.filechooser.FileFilter() 
+    {
+        public boolean accept(final File file) 
+        {
+          if(file.isDirectory()) 
+            return true;
+
+          for(int i = 0 ; i < feature_suffixes.size() ; ++i) 
+          {
+            final String this_suffix = feature_suffixes.elementAt(i);
+
+            if(file.getName().endsWith("." + this_suffix) ||
+               file.getName().endsWith("." + this_suffix + ".gz")) 
+              return true;
+          }
+
+          return false;
+        }
+
+        public String getDescription() 
+        {
+          return "Feature files";
+        }
+      };
+
+    final javax.swing.filechooser.FileFilter sequence_filter =
+      new javax.swing.filechooser.FileFilter() 
+      {
+        public boolean accept(final File file) 
+        {
+          if(file.isDirectory()) 
+            return true;
+
+          for(int i = 0 ; i<sequence_suffixes.size() ; ++i) 
+          {
+            final String this_suffix = sequence_suffixes.elementAt(i);
+
+            if(file.getName().endsWith("." + this_suffix) ||
+               file.getName().endsWith("." + this_suffix + ".gz")) 
+              return true;
+          }
+
+          return false;
+        }
+
+        public String getDescription() 
+        {
+          return "Sequence files";
+        }
+      };
+
+    addChoosableFileFilter(artemis_filter);
+    addChoosableFileFilter(feature_filter);
+    addChoosableFileFilter(sequence_filter); 
+
+    if(show_sequence_only) 
+      setFileFilter(sequence_filter);
+    else 
+      setFileFilter(artemis_filter);
+  }
+
+  /**
+   *  Return an uk.ac.sanger.artemis.io.Entry object representing the file
+   *  the user has selected with the dialog or null if the read failed for any
+   *  reason.
+   *  @param entry_information The EntryInformation to use when reading.  This
+   *    supplies the list of valid keys and qualifiers.  If a key or qualifier
+   *    is read that is incompatible with this EntryInformation object the
+   *    EntryInformation will be changed to cope.
+   *  @param listener The object to which InputStreamProgressEvents will be
+   *    send while reading.
+   *  @exception EntryInformationException Thrown if an Entry using the given
+   *    EntryInformation object cannot contain the Key, Qualifier or
+   *    Key/Qualifier combination of one of the features in the Document.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.
+   **/
+  public Entry getEntry(final EntryInformation entry_information,
+                        final InputStreamProgressListener listener,
+                        final ProgressThread progress_thread,
+                        final boolean show_progress) 
+  {
+    setDialogTitle("Select a file ...");
+    setDialogType(JFileChooser.OPEN_DIALOG);
+    SecurityManager sm = System.getSecurityManager();
+    System.setSecurityManager(null);
+
+    final int status = showOpenDialog(owner);
+
+    System.setSecurityManager(sm);
+
+    if(status != JFileChooser.APPROVE_OPTION ||
+       getSelectedFile() == null) 
+      return null;
+
+    if(progress_thread != null)
+      progress_thread.start();
+
+    final File file = new File(getCurrentDirectory(),
+                               getSelectedFile().getName());
+
+    return getEntryFromFile(owner, new FileDocument(file),
+                            entry_information, show_progress); 
+  }
+
+  /**
+   *  This exists only to get around a bug in the 1.1/1.2 code generator,
+   *  which generates unverifiable code.
+   *  @param frame Used when creating MessageDialog components.
+   *  @param file_document The Document to read the Entry from(should be made
+   *    from entry_file).
+   *  @param entry_information The EntryInformation to use when reading.  This
+   *    supplies the list of valid keys and qualifiers.  If a key or qualifier
+   *    is read that is incompatible with this EntryInformation object the
+   *    EntryInformation will be changed to cope.
+   **/
+  private static Entry getEntryFromFileHelper(final JFrame frame,
+                            final Document file_document,
+                            final EntryInformation entry_information)
+      throws ReadFormatException, IOException 
+  {
+
+    final LogReadListener read_event_logger =
+      new LogReadListener(file_document.getName());
+
+    final Entry new_entry;
+
+    try 
+    {
+      new_entry =
+        DocumentEntryFactory.makeDocumentEntry(entry_information,
+                                 file_document,read_event_logger);
+    }
+    catch(EntryInformationException e) 
+    {
+      throw new Error("internal error - unexpected exception: " + e);
+    }
+
+    if(read_event_logger.seenMessage())
+    {
+      final YesNoDialog yes_no_dialog = new YesNoDialog(frame,
+                         "there were warnings while reading - view now?");
+
+      if(yes_no_dialog.getResult()) 
+        Splash.showLog();
+    }
+
+    return new_entry;
+  }
+
+  /**
+   *  Read and return an Entry from the given File.
+   *  @param frame Used when creating MessageDialog components.
+   *  @param entry_file The file to read the Entry from.
+   *  @param entry_information The EntryInformation to use when reading.  This
+   *    supplies the list of valid keys and qualifiers.  If a key or qualifier
+   *    is read that is incompatible with this EntryInformation object the
+   *    EntryInformation will be changed to cope.
+   *  @param listener The object to which InputStreamProgressEvents will be
+   *    send while reading.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.
+   **/
+  public static Entry getEntryFromFile(final JFrame frame,
+                     final Document entry_document,
+                     final EntryInformation entry_information,
+                     final boolean show_progress) 
+  {
+    InputStreamProgressDialog progress_dialog = null;
+
+    if(show_progress) {
+// XXX FIXME
+
+// This doesn't work because getEntryFromFile() is called from the Swing
+// thread so the Dialog never gets updated
+
+//     progress_dialog =
+//       new InputStreamProgressDialog(frame, "Reading ...",
+//                                      "Reading from " +
+//                                      entry_document.getName(), false);
+//     final InputStreamProgressListener listener =
+//       progress_dialog.getInputStreamProgressListener();
+
+//     entry_document.addInputStreamProgressListener(listener);
+    }
+
+    try 
+    {
+      return getEntryFromFileHelper(frame, entry_document,
+                                    entry_information);
+    }
+    catch(ReadFormatException e) 
+    {
+      final String message =
+        "failed to open " + entry_document.getName() + ": " +
+        e.getMessage() +(e.getLineNumber() > 1 ?
+                           " at line: " + e.getLineNumber() :
+                           "");
+      System.out.println(message);
+      new MessageDialog(frame, message);
+    }
+    catch(FileNotFoundException e) 
+    {
+      final String message =
+        "failed to open " + entry_document.getName() + ": file not found";
+      new MessageDialog(frame, message);
+    }
+    catch(IOException e) 
+    {
+      final String message =
+        "failed to open " + entry_document.getName() + ": " + e.getMessage();
+      new MessageDialog(frame, message);
+    }
+    finally
+    {
+//    if(progress_dialog != null) 
+//      progress_dialog.dispose();
+    }
+    return null;
+  }
+
+  /**
+   *  Save the given entry, prompting for a file name if necessary.
+   *  @param include_diana_extensions If true then the any diana additions to
+   *    the embl file format will be included in the output, otherwise they
+   *    will be removed.  Also possible problems that would cause an entry to
+   *    bounce from the EMBL submission process will be flagged if this is
+   *    true.
+   *  @param ask_for_name If true then always prompt for a new filename,
+   *    otherwise prompt only when the entry name is not set.
+   *  @param keep_new_name If ask_for_name is true a file will be written with
+   *    the new name the user selects - if keep_new_name is true as well, then
+   *    the entry will have it's name set to the new name, otherwise it will
+   *    be used for this save and then discarded.
+   *  @param destination_type Should be one of EMBL_FORMAT, GENBANK_FORMAT or
+   *    ANY_FORMAT.  If ANY_FORMAT then the Entry will be saved in the
+   *    same format it was created, otherwise it will be saved in the given
+   *    format.
+   **/
+  void saveEntry(final uk.ac.sanger.artemis.Entry entry,
+                  final boolean include_diana_extensions,
+                  final boolean ask_for_name,
+                  final boolean keep_new_name,
+                  final int destination_type) 
+  {
+    try 
+    {
+      if(ask_for_name || entry.getName() == null) 
+      {
+        JCheckBox emblHeader = new JCheckBox("Add EMBL Header",
+                                             false);
+        setDialogTitle("Save to ...");
+        setDialogType(JFileChooser.SAVE_DIALOG);
+
+//      if(destination_type == DocumentEntryFactory.EMBL_FORMAT)
+//        setAccessory(emblHeader);
+        int status = showSaveDialog(owner);
+
+        if(status != JFileChooser.APPROVE_OPTION ||
+           getSelectedFile() == null) 
+          return;
+
+        File file = new File(getCurrentDirectory(),
+                        getSelectedFile().getName());
+
+        if(file.exists()) 
+        {
+          final YesNoDialog yes_no_dialog = new YesNoDialog(owner,
+                           "this file exists: " + file.getName() +
+                           " overwrite it?");
+
+          if(!yes_no_dialog.getResult()) 
+            return;
+        }
+
+        final MessageDialog message = new MessageDialog(owner,
+                             "saving to " + file.getName() + " ...",
+                             false);
+        try 
+        {
+          if(include_diana_extensions) 
+            entry.save(file, destination_type, false);
+          else 
+            entry.saveStandardOnly(file, destination_type, true);
+        }
+        catch(EntryInformationException e) 
+        {
+          final YesNoDialog yes_no_dialog = new YesNoDialog(owner,
+                             "destination format can't handle all " +
+                             "keys/qualifiers - continue?");
+          if(yes_no_dialog.getResult()) 
+          {
+            try 
+            {
+              if(include_diana_extensions) 
+                entry.save(file, destination_type, true);
+              else 
+                entry.saveStandardOnly(file, destination_type, true);
+            }
+            catch(EntryInformationException e2) 
+            {
+              throw new Error("internal error - unexpected exception: "+ e);
+            }
+          } 
+          else 
+            return;
+        }
+        finally 
+        {
+          if(message != null) 
+            message.dispose();
+        }
+
+        if(keep_new_name) 
+          entry.setName(file.getName());
+      }
+      else 
+      {
+        final MessageDialog message = new MessageDialog(owner,
+                             "saving to " + entry.getName() + " ...",
+                             false);
+        try 
+        {
+          if(include_diana_extensions) 
+            entry.save(destination_type);
+          else 
+            entry.saveStandardOnly(destination_type);
+        }
+        finally 
+        {
+          message.dispose();
+        }
+      }
+    } 
+    catch(ReadOnlyException e) 
+    {
+      new MessageDialog(owner, "this entry is read only");
+      return;
+    }
+    catch(IOException e) 
+    {
+      new MessageDialog(owner, "error while saving: " + e);
+      return;
+    } 
+    catch(EntryInformationException e) 
+    {
+      new MessageDialog(owner, "error while saving: " + e);
+      return;
+    }
+  }
+
+}
+
diff --git a/uk/ac/sanger/artemis/components/EntryGroupDisplay.java b/uk/ac/sanger/artemis/components/EntryGroupDisplay.java
new file mode 100644
index 000000000..a37b6f6cf
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EntryGroupDisplay.java
@@ -0,0 +1,240 @@
+/* EntryGroupDisplay.java
+ *
+ * created: Mon Dec  7 1998
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright(C) 1998,1999,2000,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EntryGroupDisplay.java,v 1.1 2004-06-09 09:46:29 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import java.awt.FlowLayout;
+import java.awt.Color;
+import java.awt.event.*;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  This component allows the user to change the "active" setting of the
+ *  objects in an EntryGroup.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: EntryGroupDisplay.java,v 1.1 2004-06-09 09:46:29 tjc Exp $
+ **/
+
+public class EntryGroupDisplay extends JPanel
+    implements EntryGroupChangeListener, EntryChangeListener 
+{
+  final private static Color background_colour = new Color(200, 200, 200);
+
+  /**
+   *  This is a reference to the EntryEdit component that created this
+   *  EntryGroupDisplay.  We need this reference so that we can watch the
+   *  entry group.
+   **/
+  private EntryEdit owning_component;
+
+  /**
+   *  A vector containing the Entry objects that this EntryEdit object knows
+   *  about.  This reference is obtained from owning_component.
+   **/
+  private EntryGroup entry_group;
+
+  /**
+   *  A vector containing one JCheckBox or Label for each Entry in the
+   *  EntryGroup object.
+   **/
+  private Vector entry_components = new Vector();
+
+  /**
+   *  A label containing the message "Entry:".
+   **/
+  private JLabel label;
+
+  /**
+   *  Create a new EntryGroupDisplay object.
+   *  @param owning_component The EntryEdit object that this EntryGroupDisplay
+   *    component is in.
+   **/
+  public EntryGroupDisplay(final EntryEdit owning_component) 
+  {
+    this.owning_component = owning_component;
+    this.entry_group = owning_component.getEntryGroup();
+
+    entry_group.addEntryGroupChangeListener(this);
+    entry_group.addEntryChangeListener(this);
+
+    final FlowLayout flow_layout = new FlowLayout(FlowLayout.LEFT); 
+
+    label = new JLabel("Entry: ");
+
+    setLayout(flow_layout);
+    refreshButtons();
+    
+    setBackground(background_colour);
+  }
+
+
+  /**
+   *  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:
+        refreshButtons();
+        break;
+      case EntryGroupChangeEvent.ENTRY_INACTIVE:
+      case EntryGroupChangeEvent.ENTRY_ACTIVE:
+        updateActive();
+        break;
+      case EntryGroupChangeEvent.NEW_DEFAULT_ENTRY:
+        highlightDefaultEntry(event);
+        break;
+    }
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.
+   **/
+  public void entryChanged(final EntryChangeEvent event) 
+  {
+    if(event.getType() == EntryChangeEvent.NAME_CHANGED) 
+      refreshButtons();
+  }
+
+  /**
+   *  Remove and then recreate the Buttons to the reflect the current contents
+   *  of the EntryGroup.
+   **/
+  private void refreshButtons() 
+  {
+    removeAll();
+    add(label);
+
+    entry_components = new Vector();
+
+    if(entry_group == null) 
+      return;
+    else 
+    {
+      for(int i = 0; i < entry_group.size(); ++i) 
+        add(entry_group.elementAt(i));
+    }
+
+    doLayout();
+  }
+
+  /**
+   *  Update the buttons to reflect the current state of the EntryGroup.
+   **/
+  private void updateActive()
+  {
+    for(int i = 0 ; i < entry_group.size() ; ++i) 
+    {
+      final JCheckBox menu_item = (JCheckBox)entry_components.elementAt(i);
+      menu_item.setSelected(entry_group.isActive(entry_group.elementAt(i)));
+    }
+  }
+
+  /**
+   *  Add a JLabel or JCheckBox for the given Entry to this component.
+   **/
+  private void add(final Entry entry) 
+  {
+    final JCheckBox new_component;
+    String entry_name = entry.getName();
+
+    if(entry_name == null) 
+      entry_name = "no name";
+    
+    new_component = new JCheckBox(entry_name, entry_group.isActive(entry));
+
+    setEntryHighlight(entry, new_component);
+
+    new_component.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        final int button_index =
+          entry_components.indexOf(event.getSource());
+
+        if(event.getStateChange() == ItemEvent.SELECTED) 
+          entry_group.setIsActive(button_index, true);
+        else 
+          entry_group.setIsActive(button_index, false);
+      }
+    });
+
+    new_component.addMouseListener(new MouseAdapter() 
+    {
+      /**
+       *  Listen for mouse press events so that we can change the default
+       *  Entry when the popup trigger is pressed.
+       **/
+      public void mousePressed(MouseEvent event) 
+      {
+        if(event.isPopupTrigger()) 
+          entry_group.setDefaultEntry(entry);
+      }
+    });
+
+    entry_components.addElement(new_component);
+    add(new_component);
+  }
+
+  /**
+   *  Given a EntryGroupChangeEvent, this method will highlight the new
+   *  default entry
+   **/
+  private void highlightDefaultEntry(final EntryGroupChangeEvent event) 
+  {
+    final EntryGroup entry_group = owning_component.getEntryGroup();
+
+    for(int i = 0 ; i < entry_group.size() ; ++i) 
+    {
+      final JCheckBox check_box =(JCheckBox) entry_components.elementAt(i);
+      
+      setEntryHighlight(entry_group.elementAt(i), check_box);
+    }
+  }
+
+  /**
+   *  Highlight the given JCheckBox/Entry appropriately.  The default Entry
+   *  will look different to the others
+   **/
+  private void setEntryHighlight(final Entry entry,
+                                  final JCheckBox component)
+  {
+    final String label = component.getText();
+
+    if(entry_group.getDefaultEntry() == entry) 
+      component.setBackground(Color.yellow);
+    else 
+      component.setBackground(background_colour);
+  }
+  
+}
diff --git a/uk/ac/sanger/artemis/components/EntryGroupInfoDisplay.java b/uk/ac/sanger/artemis/components/EntryGroupInfoDisplay.java
new file mode 100644
index 000000000..960ab6112
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EntryGroupInfoDisplay.java
@@ -0,0 +1,536 @@
+/* EntryGroupInfoDisplay.java
+ *
+ * created: Fri Mar 12 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EntryGroupInfoDisplay.java,v 1.1 2004-06-09 09:46:30 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.util.StringVector;
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.Qualifier;
+import uk.ac.sanger.artemis.io.Location;
+import uk.ac.sanger.artemis.io.FuzzyRange;
+import uk.ac.sanger.artemis.io.RangeVector;
+import uk.ac.sanger.artemis.io.InvalidRelationException;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import java.util.Hashtable;
+import java.util.Enumeration;
+import java.awt.event.*;
+
+import javax.swing.JFrame;
+
+/**
+ *  This component will show general information about an EntryGroup.  It will
+ *  show the sequence length, GC content and a summary of the active entries.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: EntryGroupInfoDisplay.java,v 1.1 2004-06-09 09:46:30 tjc Exp $
+ **/
+
+public class EntryGroupInfoDisplay
+    implements FeatureChangeListener, EntryChangeListener,
+               EntryGroupChangeListener, SequenceChangeListener {
+  /**
+   *  Create a new EntryGroupInfoDisplay object to summarize all features on
+   *  both strands of the given EntryGroup.
+   *  @param parent_frame The reference of the parent JFrame.
+   *  @param entry_group The EntryGroup to view.
+   **/
+  public EntryGroupInfoDisplay (final JFrame parent_frame,
+                                final EntryGroup entry_group) {
+    this (parent_frame, entry_group, BOTH);
+  }
+
+  /**
+   *  Create a new EntryGroupInfoDisplay object to summarize the given
+   *  EntryGroup.
+   *  @param parent_frame The reference of the parent JFrame.
+   *  @param entry_group The EntryGroup to view.
+   *  @param strand_flag Indicates which strand to summarize.  BOTH means
+   *    summarize all features on both strands.  FORWARD means summarize
+   *    forward strand only.  REVERSE means summarize reverse strand only.
+   **/
+  public EntryGroupInfoDisplay (final JFrame parent_frame,
+                                EntryGroup entry_group,
+                                final int strand_flag) {
+    this.strand_flag = strand_flag;
+    this.parent_frame = parent_frame;
+    this.entry_group = entry_group;
+
+    if (strand_flag == FORWARD) {
+      final FeaturePredicate forward_feature_predicate =
+        new FeaturePredicate () {
+          public boolean testPredicate (final Feature test_feature) {
+            return test_feature.isForwardFeature ();
+          }
+        };
+
+      final FilteredEntryGroup filtered_entry_group =
+        new FilteredEntryGroup (entry_group, forward_feature_predicate,
+                                "forward strand features");
+
+      entry_group = filtered_entry_group;
+    } else {
+      if (strand_flag == REVERSE) {
+        final FeaturePredicate reverse_feature_predicate =
+          new FeaturePredicate () {
+            public boolean testPredicate (final Feature test_feature) {
+              return !test_feature.isForwardFeature ();
+            }
+          };
+
+        final FilteredEntryGroup filtered_entry_group =
+          new FilteredEntryGroup (entry_group, reverse_feature_predicate,
+                                  "reverse strand features");
+
+        entry_group = filtered_entry_group;
+      } else {
+        if (strand_flag != BOTH) {
+          throw new Error ("internal error - illegal argument");
+        }
+      }
+    }
+
+    file_viewer = new FileViewer (getFrameName ());
+
+    updateView ();
+
+    entry_group.addEntryChangeListener (this);
+    entry_group.addFeatureChangeListener (this);
+    entry_group.addEntryGroupChangeListener (this);
+    entry_group.getBases ().addSequenceChangeListener (this,
+                                                       Bases.MAX_PRIORITY);
+
+    file_viewer.addWindowListener (new WindowAdapter () {
+      public void windowClosed (WindowEvent event) {
+        stopListening ();
+      }
+    });
+  }
+
+  /**
+   *  Return the String that should be used for the title of the Overview Frame
+   **/
+  private String getFrameName () {
+    if (entry_group instanceof FilteredEntryGroup) {
+      final FilteredEntryGroup filtered_entry_group =
+        (FilteredEntryGroup) entry_group;
+
+      final String filter_name = filtered_entry_group.getFilterName ();
+
+      if (filter_name == null) {
+        return "Artemis overview of: " + parent_frame.getTitle ();
+      } else {
+        return "Artemis overview of: " + parent_frame.getTitle () +
+          " (" + filter_name + ")";
+      }
+    } else {
+      final StringBuffer buffer = new StringBuffer ("Artemis Overview");
+
+      if (entry_group.size () > 0) {
+
+        buffer.append (" of:");
+
+        for (int i = 0 ; i < entry_group.size () ; ++i) {
+          final Entry this_entry = entry_group.elementAt (i); 
+
+          if (this_entry.getName () == null) {
+            buffer.append (" [no name]");
+          } else {
+            buffer.append (" " + this_entry.getName ());
+          }
+        }
+      }
+
+      return buffer.toString ();
+    }
+  }
+  
+  /**
+   *  Use the forward strand when summarizing.
+   **/
+  final static public int FORWARD = Bases.FORWARD;
+
+  /**
+   *  Use the reverse strand when summarizing.
+   **/
+  final static public int REVERSE = Bases.REVERSE;
+
+  /**
+   *  Summarize both strands.
+   **/
+  final static public int BOTH = 3;
+
+  /**
+   *  Remove this object as a selection change listener.
+   **/
+  private void stopListening () {
+    entry_group.removeEntryChangeListener (this);
+    entry_group.removeEntryGroupChangeListener (this);
+    entry_group.removeFeatureChangeListener (this);
+    entry_group.getBases ().removeSequenceChangeListener (this);
+  }
+
+  /**
+   *  Implementation of the FeatureChangeListener interface.   We listen to
+   *  FeatureChange events so that we can update the display if qualifiers
+   *  change.
+   **/
+  public void featureChanged (final FeatureChangeEvent event) {
+    updateView ();
+  }
+
+  /**
+   *  Implementation of the EntryGroupChangeListener interface.  We listen to
+   *  EntryGroupChange events so that we can update the display if entries
+   *  are added, deleted, activated or deactivated.
+   **/
+  public void entryGroupChanged (final EntryGroupChangeEvent event) {
+    if (event.getType () == EntryGroupChangeEvent.DONE_GONE) {
+      stopListening ();
+      file_viewer.dispose ();
+    } else {
+      updateView ();
+    }
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so that we can update the display if features are
+   *  added or deleted.
+   **/
+  public void entryChanged (final EntryChangeEvent event) {
+    updateView ();
+  }
+
+  /**
+   *  Implementation of the SequenceChangeListener interface.  We listen to
+   *  SequenceChange events so that we can update the display if the sequence
+   *  changes.
+   **/
+  public void sequenceChanged (final SequenceChangeEvent event) {
+    updateView ();
+  }
+
+  /**
+   *  Update the FileViewer component to reflect the current state of the
+   *  EntryGroup.
+   **/
+  private void updateView () {
+    final String frame_name = getFrameName ();
+
+    file_viewer.setTitle (frame_name);
+    
+    final StringBuffer buffer = new StringBuffer ();
+
+    buffer.append (frame_name + "\n\n");
+
+    buffer.append ("Number of bases: " +
+                   entry_group.getSequenceLength () + "\n");
+    buffer.append ("Number of features in the active entries: " +
+                   entry_group.getAllFeaturesCount () + "\n\n");
+
+    final Bases entry_group_bases = entry_group.getBases ();
+
+    // cds_count - gene_count = pseudo gene count
+    int gene_count = 0;
+    int cds_count = 0;
+    int spliced_gene_count = 0;
+    int spliced_gene_bases_count = 0;
+    StringBuffer gene_bases_buffer = new StringBuffer ();
+    int gene_bases_count_with_introns = 0;
+    int cds_bases_count = 0;
+    int exon_count = 0;
+    int intron_count = 0;
+    int partial_count = 0;
+
+    final Hashtable table = new Hashtable ();
+
+    final FeatureEnumeration feature_enumerator = entry_group.features ();
+
+    while (feature_enumerator.hasMoreFeatures ()) {
+      final Feature this_feature = feature_enumerator.nextFeature ();
+
+      final Key key = this_feature.getKey ();
+      final String key_string = key.toString ();
+
+      final Location location = this_feature.getLocation ();
+
+      final RangeVector ranges = location.getRanges ();
+
+      for (int i = 0 ; i < ranges.size () ; ++i) {
+        if (ranges.elementAt (i) instanceof FuzzyRange) {
+          ++partial_count;
+        }
+      }
+
+      try {
+        String colour = this_feature.getValueOfQualifier ("colour");
+
+        if (colour == null || colour.length () == 0) {
+          colour = "no colour";
+        }
+
+        if (table.containsKey (key_string)) {
+          final Hashtable colour_table = (Hashtable) table.get (key_string);
+
+          final Integer colour_value = (Integer) colour_table.get (colour);
+
+          if (colour_value == null) {
+              colour_table.put (colour, new Integer (1));
+          } else {
+            final int old_value = ((Integer) colour_value).intValue ();
+
+            colour_table.put (colour, new Integer (old_value + 1));
+          }
+        } else {
+          final Hashtable colour_table = new Hashtable ();
+          colour_table.put (colour, new Integer (1));
+          table.put (key_string, colour_table);
+        }
+      } catch (InvalidRelationException e) {
+        throw new Error ("internal error - unexpected exception: " + e);
+      }
+
+      if (this_feature.isCDS ()) {
+        cds_count++;
+
+        cds_bases_count += this_feature.getBaseCount ();
+
+        final Qualifier pseudo_qualifier;
+
+        try {
+          pseudo_qualifier = this_feature.getQualifierByName ("pseudo");
+        } catch (InvalidRelationException e) {
+          throw new Error ("internal error - unexpected exception: " + e);
+        }
+
+        if (pseudo_qualifier == null) {
+          ++gene_count;
+
+          final FeatureSegmentVector segments = this_feature.getSegments ();
+
+          if (segments.size () > 1) {
+            ++spliced_gene_count;
+            spliced_gene_bases_count += this_feature.getBaseCount ();
+            intron_count += segments.size () - 1;
+          }
+
+          exon_count += segments.size ();
+
+          gene_bases_buffer.append (this_feature.getBases ());
+
+          gene_bases_count_with_introns +=
+            this_feature.getRawLastBase () -
+            this_feature.getRawFirstBase () + 1;
+        }
+      }
+    }
+
+    final String gene_bases = gene_bases_buffer.toString ();
+    final int gene_bases_count = gene_bases.length ();
+
+    final int pseudo_gene_count = cds_count - gene_count;
+    final int pseudo_gene_bases_count = cds_bases_count - gene_bases_count;
+
+    if (cds_count > 0) {
+      if (gene_count > 0) {
+        buffer.append ("Genes (CDS features without a /pseudo qualifier):\n");
+
+        final int non_spliced_gene_count = gene_count - spliced_gene_count;
+        final int non_spliced_gene_bases_count =
+          gene_bases_count - spliced_gene_bases_count;
+
+        if (spliced_gene_count > 0) {
+          buffer.append ("   spliced:\n");
+          buffer.append ("      count: " + spliced_gene_count + "\n");
+          buffer.append ("      bases: " + spliced_gene_bases_count + "\n");
+          buffer.append ("      introns: " + intron_count + "\n");
+        }
+
+        if (non_spliced_gene_count > 0) {
+          buffer.append ("   non-spliced:\n");
+          buffer.append ("      count: " + non_spliced_gene_count + "\n");
+          buffer.append ("      bases: " + non_spliced_gene_bases_count +
+                         "\n");
+        }
+
+        buffer.append ("   all:\n");
+        buffer.append ("      count: " + gene_count + "\n");
+        buffer.append ("      partials: " + partial_count + "\n");
+        if (exon_count == gene_count) {
+          buffer.append ("      bases: "
+                         + gene_bases_count + "\n");
+        } else {
+          buffer.append ("      bases (excluding introns): "
+                         + gene_bases_count + "\n");
+          buffer.append ("      bases (including introns): "
+                         + gene_bases_count_with_introns + "\n");
+          buffer.append ("      exons: " + exon_count + "\n");
+          buffer.append ("      average exon length: " +
+                         10L * gene_bases_count / exon_count / 10.0 + "\n");
+          buffer.append ("      average intron length: " +
+                         10L * (gene_bases_count_with_introns -
+                                gene_bases_count) /
+                         (exon_count - gene_count) / 10.0 + "\n");
+          buffer.append ("      average number of exons per gene: " +
+                         100L * exon_count / gene_count / 100.00 + "\n");
+        }
+        buffer.append ("      density: " +
+                       1000000L * gene_count /
+                       entry_group.getSequenceLength () / 1000.0 +
+                       " genes per kb   (" +
+                       entry_group.getSequenceLength () / gene_count +
+                       " bases per gene)\n");
+        buffer.append ("      average length: " +
+                       gene_bases_count / gene_count + "\n");
+        buffer.append ("      average length (including introns): " +
+                       gene_bases_count_with_introns / gene_count + "\n");
+        buffer.append ("      coding percentage: " +
+                       1000L * gene_bases_count /
+                       entry_group.getSequenceLength () / 10.0 + "\n");
+        buffer.append ("      coding percentage (including introns): " +
+                       1000L * gene_bases_count_with_introns /
+                       entry_group.getSequenceLength () / 10.0 + "\n\n");
+
+        buffer.append ("      gene sequence composition:\n\n");
+
+        final StringVector gene_base_summary =
+          SelectionViewer.getBaseSummary (gene_bases);
+
+        for (int i = 0 ; i < gene_base_summary.size () ; ++i) {
+          buffer.append ("         ");
+          buffer.append (gene_base_summary.elementAt (i)).append ("\n");
+        }
+
+        buffer.append ("\n");
+      }
+
+      if (pseudo_gene_count > 0) {
+        buffer.append ("Pseudo genes (CDS features with a /pseudo " +
+                       "qualifier):\n");
+
+        buffer.append ("   count: " + pseudo_gene_count + "\n");
+        buffer.append ("   bases: " + pseudo_gene_bases_count + "\n");
+        buffer.append ("   average length: " +
+                       pseudo_gene_bases_count / pseudo_gene_count + "\n\n");
+      }
+
+      if (pseudo_gene_count > 0) {
+        buffer.append ("All CDS features:\n");
+
+        buffer.append ("   count: " + cds_count + "\n");
+        buffer.append ("   bases: " + cds_bases_count + "\n");
+        buffer.append ("   average length: " +
+                       cds_bases_count / cds_count + "\n\n");
+      }
+    }
+
+    final Strand strand;
+
+    if (strand_flag == FORWARD || strand_flag == BOTH) {
+      strand = entry_group.getBases ().getForwardStrand ();
+    } else {
+      strand = entry_group.getBases ().getReverseStrand ();
+    }
+
+    final StringVector base_summary =
+      SelectionViewer.getBaseSummary (strand.getStrandBases ());
+
+    buffer.append ("\nOverall sequence composition:\n\n");
+
+    for (int i = 0 ; i < base_summary.size () ; ++i) {
+      buffer.append (base_summary.elementAt (i)).append ("\n");
+    }
+
+    buffer.append ("\nSummary of the active entries:\n");
+
+    final Enumeration e = table.keys ();
+
+    while (e.hasMoreElements()) {
+      final String this_key = (String) e.nextElement();
+      final Hashtable colour_table = (Hashtable) table.get (this_key);
+
+      buffer.append (this_key + ": ");
+
+      final StringBuffer colour_string = new StringBuffer ();
+
+      final Enumeration colour_enum = colour_table.keys ();
+
+      int total = 0;
+
+      while (colour_enum.hasMoreElements()) {
+        final String this_colour = (String) colour_enum.nextElement();
+
+        final int colour_count =
+          ((Integer) colour_table.get (this_colour)).intValue ();
+
+        total += colour_count;
+
+        final String end_string;
+
+        if (this_colour.equals ("no colour")) {
+          end_string = "no colour";
+        } else {
+          end_string = "colour: " + this_colour;
+        }
+
+        if (colour_count == 1) {
+          colour_string.append ("  one has " + end_string + "\n");
+        } else {
+          colour_string.append ("  " + colour_count + " have " +
+                                end_string + "\n");
+        }
+      }
+
+      buffer.append (total + "\n");
+
+      buffer.append (colour_string);
+    }
+
+    file_viewer.setText (buffer.toString ());
+  }
+
+  /**
+   *  This is the EntryGroup object that we are viewing.
+   **/
+  private EntryGroup entry_group;
+
+  /**
+   *  The strand indicator that was passed to the constructor.
+   **/
+  private int strand_flag;
+
+  /**
+   *  The FileViewer object that is displaying the EntryGroup.
+   **/
+  private FileViewer file_viewer;
+
+  /**
+   *  The Frame that was passed to the constructor.
+   **/
+  private JFrame parent_frame;
+}
diff --git a/uk/ac/sanger/artemis/components/EntryGroupMenu.java b/uk/ac/sanger/artemis/components/EntryGroupMenu.java
new file mode 100644
index 000000000..5e02cbbd5
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EntryGroupMenu.java
@@ -0,0 +1,336 @@
+/* EntryGroupMenu.java
+ *
+ * created: Fri Aug 27 1999
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EntryGroupMenu.java,v 1.1 2004-06-09 09:46:31 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import java.util.*;
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A menu containing the current entries in an EntryGroup.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: EntryGroupMenu.java,v 1.1 2004-06-09 09:46:31 tjc Exp $
+ **/
+
+public class EntryGroupMenu extends JMenu
+    implements EntryGroupChangeListener, EntryChangeListener {
+  /**
+   *  Create a new EntryGroupMenu object that will show the entries in the
+   *  given EntryGroup.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param entry_group The EntryGroup object to display.
+   *  @param menu_name The name of the new menu.
+   **/
+  public EntryGroupMenu (final JFrame frame,
+                         final EntryGroup entry_group,
+                         final String menu_name) {
+    super (menu_name);
+
+    this.frame = frame;
+    this.entry_group = entry_group;
+
+    entry_group.addEntryGroupChangeListener (this);
+    entry_group.addEntryChangeListener (this);
+
+    refreshMenu ();
+  }
+
+  /**
+   *  Create a new EntryGroupMenu object that will show the entries in the
+   *  given EntryGroup.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param entry_group The EntryGroup object to display.
+   **/
+  public EntryGroupMenu (final JFrame frame,
+                         final EntryGroup entry_group) {
+    this (frame, entry_group, "Entries");
+  }
+
+  /**
+   *  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;
+    }
+  }
+
+  /**
+   *  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 ();
+
+    if (entry_group == null || entry_group.size () == 0) {
+      add (new JMenuItem ("(No Entries Currently)"));
+      return;
+    }
+
+    final JMenu set_entry_name_menu = new JMenu ("Set Name Of Entry");
+
+    add (set_entry_name_menu);
+
+    final JMenu set_default_menu = new JMenu ("Set Default Entry");
+    final JMenu delete_entry_menu = new JMenu ("Remove An Entry");
+
+    addSeparator ();
+
+    add (set_default_menu);
+    add (delete_entry_menu);
+
+    final JMenuItem delete_active_entries_menu =
+      new JMenuItem ("Remove Active Entries");
+
+    delete_active_entries_menu.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        deleteActiveEntries ();
+      }
+    });
+    
+    add (delete_active_entries_menu);
+
+    final JMenuItem deactivate_all = new JMenuItem ("Deactivate All Entries");
+
+    deactivate_all.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        for (int i = 0 ; i < entry_group.size () ; ++i) {
+          final Entry this_entry = entry_group.elementAt (i);
+
+          entry_group.setIsActive (this_entry, false);
+        }
+      }
+    });
+
+    add (deactivate_all);
+
+    entry_components = new Vector ();
+
+    addSeparator ();
+
+    for (int i = 0 ; i < entry_group.size () ; ++i) {
+      final Entry this_entry = entry_group.elementAt (i);
+
+      String entry_name = this_entry.getName ();
+
+      if (entry_name == null) {
+        entry_name = "no name";
+      }
+
+      final JMenuItem set_entry_name_item = new JMenuItem (entry_name);
+      set_entry_name_item.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent event) {
+         setEntryName (this_entry);
+        }
+      });
+
+      set_entry_name_menu.add (set_entry_name_item);
+
+      final JMenuItem delete_entry_item = new JMenuItem (entry_name);
+      delete_entry_item.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent event) {
+          if (this_entry.hasUnsavedChanges ()) {
+            final String message;
+
+            if (this_entry.getName () != null) {
+              message = "there are unsaved changes in " +
+                this_entry.getName () + " - really remove?";
+            } else {
+              message = "there are unsaved changes in entry #" +
+                entry_group.indexOf (this_entry) + " - really remove?";
+            }
+
+            final YesNoDialog yes_no_dialog =
+              new YesNoDialog (frame, message);
+
+            if (!yes_no_dialog.getResult ()) {
+              return;
+            }
+          }
+          entry_group.remove (this_entry);
+        }
+      });
+
+      delete_entry_menu.add (delete_entry_item);
+
+      final JMenuItem set_default_entry_item = new JMenuItem (entry_name);
+      set_default_entry_item.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent event) {
+          entry_group.setDefaultEntry (this_entry);
+        }
+      });
+
+      set_default_menu.add (set_default_entry_item);
+
+      add (entry_group.elementAt (i));
+    }
+  }
+
+  /**
+   *  Add a Label or Checkbox for the given Entry to this component.
+   **/
+  private void add (final Entry entry) {
+    String entry_name = entry.getName ();
+
+    if (entry_name == null) {
+      entry_name = "no name";
+    }
+
+    if (entry_group.getDefaultEntry () == entry) {
+      entry_name = entry_name + "  (default entry)";
+    }
+
+    final JCheckBoxMenuItem new_component =
+      new JCheckBoxMenuItem (entry_name, entry_group.isActive (entry));
+
+    new_component.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent event) {
+        final int button_index =
+          entry_components.indexOf (event.getSource ());
+
+        if (event.getStateChange () == ItemEvent.SELECTED) {
+          entry_group.setIsActive (button_index, true);
+        } else {
+          entry_group.setIsActive (button_index, false);
+        }
+      }
+    });
+
+    entry_components.addElement (new_component);
+    add (new_component);
+  }
+
+  /**
+   *  Create a new TextRequester component and set the name of the default
+   *  Entry to whatever the user types.
+   **/
+  private void setEntryName (final Entry entry) {
+    final TextRequester text_requester =
+      new TextRequester ("New name for the entry?", 18, "");
+
+    text_requester.addTextRequesterListener (new TextRequesterListener () {
+      public void actionPerformed (final TextRequesterEvent event) {
+        if (event.getType () == TextRequesterEvent.CANCEL) {
+          return;
+        }
+
+        final String requester_text = event.getRequesterText ().trim ();
+        if (requester_text.length () > 0) {
+          if (entry.setName (requester_text)) {
+            // it worked
+          } else {
+            new MessageDialog (frame,
+                               "could not set the name of the default entry");
+          }
+        }
+      }
+    });
+
+    text_requester.show ();
+  }
+
+  /**
+   *  Delete the active entries after asking the user.
+   **/
+  private void deleteActiveEntries () {
+    if (Options.getOptions ().isNoddyMode ()) {
+      final YesNoDialog dialog =
+        new YesNoDialog (frame,
+                         "Are you sure you want to remove the " +
+                         "active entries?");
+          
+      if (!dialog.getResult ()) {
+        return;
+      }
+    }
+        
+    for (int i = entry_group.size () - 1 ; i >= 0 ; --i) {
+      final Entry this_entry = entry_group.elementAt (i);
+          
+      if (this_entry.hasUnsavedChanges ()) {
+        final String message;
+            
+        if (this_entry.getName () != null) {
+          message = "there are unsaved changes in " +
+            this_entry.getName () + " - really remove?";
+        } else {
+          message = "there are unsaved changes in entry #" +
+            (i + 1) + " - really remove?";
+        }
+            
+        final YesNoDialog yes_no_dialog =
+          new YesNoDialog (frame, message);
+            
+        if (!yes_no_dialog.getResult ()) {
+          continue;
+        }
+      }
+          
+      if (entry_group.isActive (this_entry)) {
+        entry_group.remove (this_entry);
+      }
+    }
+  }
+
+  /**
+   *  A vector containing the Entry objects that this EntryEdit object knows
+   *  about.  This reference is obtained from owning_component.
+   **/
+  private EntryGroup entry_group;
+
+  /**
+   *  A vector containing one Checkbox or Label for each Entry in the
+   *  EntryGroup object.
+   **/
+  private Vector entry_components = new Vector ();
+
+  /**
+   *  The JFrame reference that was passed to the constructor.
+   **/
+  private JFrame frame = null;
+}
+
diff --git a/uk/ac/sanger/artemis/components/EntryGroupPanel.java b/uk/ac/sanger/artemis/components/EntryGroupPanel.java
new file mode 100644
index 000000000..b846af6db
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EntryGroupPanel.java
@@ -0,0 +1,376 @@
+/* EntryGroupPanel.java
+ *
+ * created: Wed Jun 21 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EntryGroupPanel.java,v 1.1 2004-06-09 09:46:32 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+
+/**
+ *  A JPanel that can show an EntryGroup(in some way).
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: EntryGroupPanel.java,v 1.1 2004-06-09 09:46:32 tjc Exp $
+ **/
+
+abstract public class EntryGroupPanel extends CanvasPanel 
+{
+  /** shortcut for View FASTA in browser.  */
+  final int fasta_in_browser_key = KeyEvent.VK_F;
+
+  /** shortcut for View FASTA.  */
+  final int view_fasta_key = KeyEvent.VK_R;
+
+  /** shortcut for View BLASTP in browser. */
+  final int blastp_in_browser_key = KeyEvent.VK_B;
+
+  /** shortcut for View BLASTP. */
+  final int view_blastp_key = KeyEvent.VK_TAB;
+
+  /** EntryGroup this component is displaying */
+  private EntryGroup entry_group;
+
+  /**
+   *  This is a reference to the Selection object that was passed to the
+   *  constructor.
+   **/
+  private Selection selection;
+
+  /**
+   *  This is a reference to the GotoEventSource object that was passed to the
+   *  constructor.
+   **/
+  private GotoEventSource goto_event_source;
+
+  /**
+   *  The BasePlotGroup object that was passed to the constructor.
+   **/
+  private BasePlotGroup base_plot_group;
+
+  /**
+   *  Create a new EntryGroupPanel for the given EntryGroup.
+   *  @param entry_group The EntryGroup that this component will display.
+   *  @param selection The Selection object for this component.  Selected
+   *    objects will be highlighted.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public EntryGroupPanel(final EntryGroup entry_group,
+                         final Selection selection,
+                         final GotoEventSource goto_event_source,
+                         final BasePlotGroup base_plot_group) 
+  {
+    this.entry_group       = entry_group;
+    this.selection         = selection;
+    this.goto_event_source = goto_event_source;
+    this.base_plot_group   = base_plot_group;
+
+    getCanvas().addKeyListener(new KeyAdapter() 
+    {
+      public void keyPressed(final KeyEvent event) 
+      {
+        handleKeyPress(event);
+      }
+    });
+  }
+
+  /**
+   *  Return an object that implements the GotoEventSource interface and which
+   *  is relative to the sequence that this DisplayComponent is displaying.
+   **/
+  public GotoEventSource getGotoEventSource() 
+  {
+    return goto_event_source;
+  }
+
+  /**
+   *  Walk up through parents of this JComponent and return the JFrame at the
+   *  top.
+   **/
+  public JFrame getParentFrame() 
+  {
+    Component parent = this.getParent();
+
+    while(parent != null && !(parent instanceof JFrame)) 
+      parent = parent.getParent();
+
+    return (JFrame)parent;
+  }
+
+  /**
+   *  Return the EntryGroup object that this FeatureDisplay is displaying.
+   **/
+  public EntryGroup getEntryGroup() 
+  {
+    return entry_group;
+  }
+
+  /**
+   *  Returns the Selection that was passed to the constructor.
+   **/
+  public Selection getSelection() 
+  {
+    return selection;
+  }
+
+  /**
+   *  Returns the BasePlotGroup that was passed to the constructor.
+   **/
+  public BasePlotGroup getBasePlotGroup()
+  {
+    return base_plot_group;
+  }
+
+  /**
+   *  This method sends an GotoEvent to all the GotoEvent listeners that will
+   *  make the start of the first selected Feature or FeatureSegment visible.
+   **/
+  protected void makeSelectionVisible() 
+  {
+    final Marker first_base = getSelection().getStartBaseOfSelection();
+
+    final GotoEvent new_event = new GotoEvent(this, first_base);
+
+    getGotoEventSource().sendGotoEvent(new_event);
+  }
+
+  /**
+   *  Return true if and only if the given MouseEvent(a mouse press) should
+   *  pop up a JPopupMenu.
+   **/
+  protected boolean isMenuTrigger(final MouseEvent event)
+  {
+    if(event.isPopupTrigger() ||
+       (event.getModifiers() & InputEvent.BUTTON3_MASK) != 0) 
+      return true;
+    else 
+      return false;
+  }
+  
+  /**
+   *  Handle key press events.
+   **/
+  private void handleKeyPress(final KeyEvent event) 
+  {
+    // this is done so that menu shortcuts don't cause each action to be
+    // performed twice
+    if(!event.isShiftDown() && event.getModifiers() != 0) 
+      return;
+
+    final FeatureVector selected_features = getSelection().getAllFeatures();
+
+    if(selected_features.size() == 0) 
+    {
+      final MarkerRange marker_range = getSelection().getMarkerRange();
+
+      if(marker_range != null) 
+      {
+        switch(event.getKeyCode()) 
+        {
+          case AddMenu.CREATE_FROM_BASE_RANGE_KEY_CODE:
+            AddMenu.createFeatureFromBaseRange(getParentFrame(),
+                                               getSelection(),
+                                               entry_group,
+                                               getGotoEventSource());
+            break;
+        }
+      }
+
+    } 
+    else
+    {
+      final Feature first_selected_feature = selected_features.elementAt(0);
+
+      final int feature_index =
+        getEntryGroup().indexOf(first_selected_feature);
+
+      final int event_key_code = event.getKeyCode();
+
+      switch(event_key_code) 
+      {
+        case KeyEvent.VK_UP:
+        case KeyEvent.VK_DOWN:
+        case KeyEvent.VK_LEFT:
+        case KeyEvent.VK_RIGHT:
+          {
+            if((event_key_code == KeyEvent.VK_LEFT ||
+                 event_key_code == KeyEvent.VK_RIGHT) &&
+                !event.isShiftDown()) 
+              break;
+          
+            final FeatureSegmentVector selected_segments =
+                        getSelection().getSelectedSegments();
+
+            if(event.isShiftDown()) 
+            {
+              if(selected_segments.size() > 0) 
+              {
+                final FeatureSegment selected_segment =
+                               selected_segments.elementAt(0);
+
+                final Feature selected_segment_feature =
+                                selected_segment.getFeature();
+
+                final FeatureSegmentVector feature_segments =
+                       selected_segment_feature.getSegments();
+
+                final int segment_index =
+                  feature_segments.indexOf(selected_segment);
+
+                if(event_key_code == KeyEvent.VK_UP ||
+                   event_key_code == KeyEvent.VK_LEFT &&
+                   !selected_segment_feature.isForwardFeature() ||
+                   event_key_code == KeyEvent.VK_RIGHT &&
+                   selected_segment_feature.isForwardFeature()) 
+                {
+                  final int segment_count = feature_segments.size();
+                  final int new_index = segment_index + 1;
+                  if(segment_index < segment_count - 1) 
+                    selection.set(feature_segments.elementAt(new_index));
+                } 
+                else
+                {
+                  if(segment_index > 0) 
+                  {
+                    final int new_index = segment_index - 1;
+                    selection.set(feature_segments.elementAt(new_index));
+                  }
+                }
+              }
+            } 
+            else
+            {
+              if(event_key_code == KeyEvent.VK_DOWN) 
+              {
+                if(feature_index <
+                    getEntryGroup().getAllFeaturesCount() - 1) 
+                {
+                  selection.set(getEntryGroup().featureAt(feature_index + 1));
+                  makeSelectionVisible();
+                }
+              }
+              else
+              {
+                if(feature_index > 0) 
+                {
+                  selection.set(getEntryGroup().featureAt(feature_index - 1));
+                  makeSelectionVisible();
+                }
+              }
+            }
+          }
+          break;
+        case EditMenu.EDIT_FEATURES_KEY_CODE:
+          EditMenu.editSelectedFeatures(getParentFrame(),
+                                         getEntryGroup(),
+                                         getSelection(),
+                                         getGotoEventSource());
+          break;
+        case EditMenu.UNDO_KEY_CODE:
+          EditMenu.undo(getParentFrame(), getSelection(), getEntryGroup());
+          break;
+        case EditMenu.MERGE_FEATURES_KEY_CODE:
+          EditMenu.mergeFeatures(getParentFrame(), getSelection(),
+                                  getEntryGroup());
+          break;
+        case EditMenu.DUPLICATE_KEY_CODE:
+          EditMenu.duplicateFeatures(getParentFrame(), getSelection(),
+                                      getEntryGroup());
+          break;
+        case EditMenu.DELETE_FEATURES_KEY_CODE:
+          EditMenu.deleteSelectedFeatures(getParentFrame(), getSelection(),
+                                           getEntryGroup());
+          break;
+        case EditMenu.TRIM_FEATURES_KEY_CODE:
+          EditMenu.trimSelected(getParentFrame(), getSelection(),
+                                 getEntryGroup(), false, true);
+          break;
+        case EditMenu.TRIM_FEATURES_TO_NEXT_ANY_KEY_CODE:
+          EditMenu.trimSelected(getParentFrame(), getSelection(),
+                                 getEntryGroup(), true, true);
+          break;
+        case EditMenu.EXTEND_TO_PREVIOUS_STOP_CODON_KEY_CODE:
+          EditMenu.extendToORF(getParentFrame(), getSelection(),
+                                getEntryGroup(), false);
+          break;
+        case AddMenu.CREATE_FROM_BASE_RANGE_KEY_CODE:
+          AddMenu.createFeatureFromBaseRange(getParentFrame(), getSelection(),
+                                              entry_group,
+                                              getGotoEventSource());
+          break;
+        case ViewMenu.VIEW_FEATURES_KEY_CODE:
+          ViewMenu.viewSelectedFeatures(getParentFrame(), getSelection());
+          break;
+        case ViewMenu.PLOT_FEATURES_KEY_CODE:
+          ViewMenu.plotSelectedFeatures(getParentFrame(), getSelection());
+          break;
+        case ViewMenu.OVERVIEW_KEY_CODE:
+          new EntryGroupInfoDisplay(getParentFrame(), entry_group);
+          break;
+        case ViewMenu.FASTA_IN_BROWSER_KEY_CODE:
+          viewResults("fasta", true);
+          break;
+        case ViewMenu.BLASTP_IN_BROWSER_KEY_CODE:
+          viewResults("blastp", true);
+          break;
+        case ViewMenu.VIEW_FASTA_KEY_CODE:
+          viewResults("fasta", false);
+          break;
+        case ViewMenu.VIEW_BLASTP_KEY_CODE:
+          viewResults("blastp", false);
+          break;
+        case ViewMenu.VIEW_HTH_KEY_CODE:
+          viewResults("hth", false);
+          break;
+      }
+    }
+  }
+
+  /**
+   *  Show the output file from an external program (like fasta) for the
+   *  selected Feature objects.  The name of the file to read is stored in a
+   *  feature qualifier.  The qualifier used is the program name plus "_file".
+   *  @param send_to_browser if true the results should be sent straight to
+   *    the web browser rather than using a SearchResultViewer object.
+   **/
+  private void viewResults(final String program_name,
+                           final boolean send_to_browser) 
+  {
+    final boolean sanger_options =
+      Options.getOptions().getPropertyTruthValue("sanger_options");
+
+    if(sanger_options && send_to_browser) 
+      ViewMenu.viewExternalResults(getParentFrame(), getSelection(),
+                                   program_name, true);
+    else 
+      ViewMenu.viewExternalResults(getParentFrame(), getSelection(),
+                                   program_name, false);
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/EntryHeaderEdit.java b/uk/ac/sanger/artemis/components/EntryHeaderEdit.java
new file mode 100644
index 000000000..007460e96
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/EntryHeaderEdit.java
@@ -0,0 +1,209 @@
+/* EntryHeaderEdit.java
+ *
+ * created: Wed Jun 16 1999
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/EntryHeaderEdit.java,v 1.1 2004-06-09 09:46:33 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+
+import java.io.*;
+
+/**
+ *  Objects of this class are used to edit the header of an Entry.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: EntryHeaderEdit.java,v 1.1 2004-06-09 09:46:33 tjc Exp $
+ **/
+
+public class EntryHeaderEdit
+    implements EntryChangeListener, EntryGroupChangeListener,
+               DocumentListener {
+  /**
+   *  Create a new EntryHeaderEdit object for the given Entry.
+   **/
+  public EntryHeaderEdit (final EntryGroup entry_group,
+                          final Entry edit_entry) {
+    
+    this.edit_entry = edit_entry;
+    this.entry_group = entry_group;
+    
+    file_viewer = new FileViewer ("Artemis Entry Header Editor: " +
+                                  edit_entry.getName () == null ?
+                                  "" : edit_entry.getName ());
+
+    file_viewer.getContentPane ().add (error_text, "North");
+
+    readHeader ();
+
+    file_viewer.getTextArea ().setEditable (true);
+    file_viewer.getTextArea ().getDocument ().addDocumentListener (this);
+
+    getEntry ().addEntryChangeListener (this);
+    entry_group.addEntryGroupChangeListener (this);
+
+    file_viewer.addWindowListener (new WindowAdapter () {
+      public void windowClosed (WindowEvent event) {
+        stopListening ();
+      }
+    });
+  }
+
+
+  /**
+   *  Remove this object as a feature and entry change listener.
+   **/
+  public void stopListening () {
+    getEntry ().removeEntryChangeListener (this);
+    entry_group.removeEntryGroupChangeListener (this);
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so we can update this component if the header changes
+   *  for another reason.
+   **/
+  public void entryChanged (EntryChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryChangeEvent.HEADER_CHANGED:
+      
+      if (event.getSource () == current_text) {
+        // don't bother with events from us
+        return;
+      }
+    
+      // re-read the information from the entry
+      readHeader ();
+      break;
+    default:
+      // do nothing
+      break;
+    }
+  }
+
+  /**
+   *  Implementation of the EntryGroupChangeListener interface.  We listen to
+   *  EntryGroupChange events so we can notify the user if of this component
+   *  if the entry gets deleted.
+   **/
+  public void entryGroupChanged (final EntryGroupChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryGroupChangeEvent.ENTRY_DELETED:
+      if (event.getEntry () == edit_entry) {
+        stopListening ();
+        file_viewer.dispose ();
+      }
+      break;
+    default:
+      // do nothing
+      break;
+    }
+  }
+
+  public void changedUpdate(DocumentEvent e) {
+    textValueChanged ();
+  }
+  public void insertUpdate(DocumentEvent e) {
+    textValueChanged ();
+  }
+  public void removeUpdate(DocumentEvent e) {
+    textValueChanged ();
+  }
+  
+  /**
+   *  Implementation of the TextListener interface.  When the text changes we
+   *  update the Feature object that we are showing.
+   **/
+  public void textValueChanged () {
+    if (current_text.equals (file_viewer.getText ())) {
+      // the text hasn't really changed
+      return;
+    }
+    
+    current_text = file_viewer.getText ();
+
+    try {
+      // ignore text change events while reading
+      edit_entry.setHeaderText (current_text);
+      error_text.setText ("");
+    } catch (uk.ac.sanger.artemis.io.ReadFormatException e) {
+      error_text.setText (e + (e.getLineNumber () > 1 ?
+                               " at line: " + e.getLineNumber () :
+                               ""));
+    } catch (java.io.IOException e) {
+      error_text.setText (e.toString ());
+    }
+  }
+
+  /**
+   *  Read the header of edit_entry into this component.
+   **/
+  public void readHeader () {
+    final String header = edit_entry.getHeaderText ();
+
+    if (header != null) {
+      file_viewer.setText (header);
+
+      current_text = file_viewer.getText ();
+    }
+  }
+
+  /**
+   *  Return the Entry that this object is displaying.
+   **/
+  private Entry getEntry () {
+    return edit_entry;
+  }
+
+  /**
+   *  The EntryGroup that passed to the constructor.
+   **/
+  private EntryGroup entry_group;
+
+  /**
+   *  The Entry that contains the Feature this object is displaying.
+   **/
+  private Entry edit_entry;
+
+  /**
+   *  The FileViewer object that is displaying the feature.
+   **/
+  private FileViewer file_viewer;
+
+  /**
+   *  The text version of the Feature we are currently displaying.
+   **/
+  private String current_text = "";
+
+  /**
+   *  A Label for showing errors and messages.
+   **/
+  private JLabel error_text = new JLabel ("");
+}
+
diff --git a/uk/ac/sanger/artemis/components/ExternalProgramOptions.java b/uk/ac/sanger/artemis/components/ExternalProgramOptions.java
new file mode 100644
index 000000000..01714cbb0
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ExternalProgramOptions.java
@@ -0,0 +1,66 @@
+/* ExternalProgramOptions.java
+ *
+ * created: Mon Oct  4 1999
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ExternalProgramOptions.java,v 1.1 2004-06-09 09:46:34 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import java.awt.*;
+
+/**
+ *  This component allows the user to set the options of an ExternalProgram.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: ExternalProgramOptions.java,v 1.1 2004-06-09 09:46:34 tjc Exp $
+ **/
+
+public class ExternalProgramOptions {
+  /**
+   *  Create a new ExternalProgramOptions object for the given
+   *  ExternalProgram.
+   **/
+  public ExternalProgramOptions (final ExternalProgram external_program) {
+    if (external_program.getType () == ExternalProgram.AA_PROGRAM ||
+        external_program.getType () == ExternalProgram.DNA_PROGRAM) {
+
+      final TextRequester requester =
+        new TextRequester ("Options for " + external_program.getName () + ":",
+                           18, external_program.getProgramOptions ());
+
+      requester.addTextRequesterListener (new TextRequesterListener () {
+        public void actionPerformed (final TextRequesterEvent event) {
+          final String requester_text = event.getRequesterText ().trim ();
+          if (requester_text.length () > 0) {
+            external_program.setProgramOptions (requester_text);
+          }
+        }
+      });
+
+      requester.show ();
+    } else {
+      throw new Error ("internal error - please hit the programmer");
+    }
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/FeatureAminoAcidViewer.java b/uk/ac/sanger/artemis/components/FeatureAminoAcidViewer.java
new file mode 100644
index 000000000..17f6a6586
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeatureAminoAcidViewer.java
@@ -0,0 +1,172 @@
+/* FeatureAminoAcidViewer.java
+ *
+ * created: Sat Dec 19 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeatureAminoAcidViewer.java,v 1.1 2004-06-09 09:46:35 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import java.awt.event.*;
+
+/**
+ *  A component for viewing the amino acids of a protein feature.  Once
+ *  created this component listens for FeatureChange events to keep the the
+ *  sequence up to date.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeatureAminoAcidViewer.java,v 1.1 2004-06-09 09:46:35 tjc Exp $
+ **/
+
+public class FeatureAminoAcidViewer
+    implements EntryChangeListener, FeatureChangeListener {
+  /**
+   *  Create a new FeatureAminoAcidViewer component to display the amino acids
+   *  of the given Feature.
+   *  @param feature The feature to view.
+   *  @param include_numbers If true then the amino acids will be numbered
+   *    (every second line of the display will be numbers rather than
+   *    sequence).
+   **/
+  public FeatureAminoAcidViewer (final Feature feature,
+                                 final boolean include_numbers) {
+    this.feature = feature;
+    this.entry = feature.getEntry ();
+    this.include_numbers = include_numbers;
+
+    sequence_viewer =
+      new SequenceViewer ("Feature base viewer for feature:" +
+                          getFeature ().getIDString (), include_numbers);
+
+    redisplay ();
+
+    getFeature ().getEntry ().addEntryChangeListener (this);
+    getFeature ().addFeatureChangeListener (this);
+
+    sequence_viewer.addWindowListener (new WindowAdapter () {
+      public void windowClosed (WindowEvent event) {
+        stopListening ();
+      }
+    });
+  }
+
+  /**
+   *  Remove this object as a entry and feature change listener.
+   **/
+  private void stopListening () {
+    getEntry ().removeEntryChangeListener (this);
+    getFeature ().removeFeatureChangeListener (this);
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so we can delete this component if the feature gets
+   *  deleted.
+   **/
+  public void entryChanged (EntryChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryChangeEvent.FEATURE_DELETED:
+      if (event.getFeature () == getFeature ()) {
+        stopListening ();
+        sequence_viewer.dispose ();
+      }
+      break;
+    default:
+      // do nothing;
+      break;
+    }
+  }
+  /**
+   *  Implementation of the FeatureChangeListener interface.  We need to
+   *  listen to feature change events from the Features in this object so that
+   *  we can keep the display up to date.
+   *  @param event The change event.
+   **/
+  public void featureChanged (FeatureChangeEvent event) {
+//       System.out.println ("FeatureViewer: feature change event type: " +
+//                           event.getType ());
+
+    // re-read the information from the feature
+    redisplay ();
+  }
+
+  /**
+   *  Redisplay the amino acids.
+   **/
+  private void redisplay () {
+    String small_note = getFeature ().getNote ();
+
+    if (small_note == null) {
+      small_note = getFeature ().getIDString ();
+    } else {
+      if (small_note.length () > 50) {
+        small_note = small_note.substring (0, 50);
+      }
+    }
+
+    final String comment = ">" + small_note + "   - " +
+      getFeature ().getFirstBase () + ": " +
+      getFeature ().getLastBase () + "  MW: " +
+      getFeature ().getMolecularWeight ();
+
+    final String sequence =
+      getFeature ().getTranslation ().toString ().toUpperCase ();
+
+    sequence_viewer.setSequence (comment, sequence);
+  }
+
+
+  /**
+   *  Return the feature this component is showing information about.
+   **/
+  private Feature getFeature () {
+    return feature;
+  }
+
+  /**
+   *  Return the Entry that contains the Feature this object is displaying.
+   **/
+  private Entry getEntry () {
+    return entry;
+  }
+
+  /**
+   *  The Feature that this component is showing information about.
+   **/
+  private Feature feature = null;
+
+  /**
+   *  The SequenceViewer object that is displaying the feature bases.
+   **/
+  private SequenceViewer sequence_viewer;
+
+  /**
+   *  The Entry that contains the Feature this object is displaying.
+   **/
+  private Entry entry;
+
+  /**
+   *  If true then the amino acids will be numbered (every second line of the
+   *  display will be numbers rather than sequence).
+   **/
+  private final boolean include_numbers;
+}
diff --git a/uk/ac/sanger/artemis/components/FeatureBaseViewer.java b/uk/ac/sanger/artemis/components/FeatureBaseViewer.java
new file mode 100644
index 000000000..929377d56
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeatureBaseViewer.java
@@ -0,0 +1,166 @@
+/* FeatureBaseViewer.java
+ *
+ * created: Sat Dec 19 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeatureBaseViewer.java,v 1.1 2004-06-09 09:46:36 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import java.awt.event.*;
+
+/**
+ *  A component for viewing the bases of a feature.  Once created this
+ *  component listens for FeatureChange events to keep the the sequence up to
+ *  date.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeatureBaseViewer.java,v 1.1 2004-06-09 09:46:36 tjc Exp $
+ **/
+
+public class FeatureBaseViewer
+    implements EntryChangeListener, FeatureChangeListener {
+  /**
+   *  Create a new FeatureBaseViewer component to display the bases of the
+   *  given Feature.
+   *  @param feature The feature to view.
+   *  @param include_numbers If true then the sequence will be numbered
+   *    (every second line of the display will be numbers rather than
+   *    sequence).
+   **/
+  public FeatureBaseViewer (final Feature feature,
+                            final boolean include_numbers) {
+    this.feature = feature;
+    this.entry = feature.getEntry ();
+    this.include_numbers = include_numbers;
+
+    sequence_viewer =
+      new SequenceViewer ("Feature base viewer for feature:" +
+                          getFeature ().getIDString (), include_numbers);
+    
+    redisplay ();
+
+    getFeature ().getEntry ().addEntryChangeListener (this);
+    getFeature ().addFeatureChangeListener (this);
+
+    sequence_viewer.addWindowListener (new WindowAdapter () {
+      public void windowClosed (WindowEvent event) {
+        stopListening ();
+      }
+    });
+  }
+  
+  /**
+   *  Remove this object as a entry and feature change listener.
+   **/
+  private void stopListening () {
+    getEntry ().removeEntryChangeListener (this);
+    getFeature ().removeFeatureChangeListener (this);
+  }
+  
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so we can delete this component if the feature gets
+   *  deleted.
+   **/
+  public void entryChanged (EntryChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryChangeEvent.FEATURE_DELETED:
+      if (event.getFeature () == getFeature ()) {
+        stopListening ();
+        sequence_viewer.dispose ();
+      }
+      break;
+    default:
+      // do nothing;
+      break;
+    }
+  }
+
+  /**
+   *  Implementation of the FeatureChangeListener interface.  We need to
+   *  listen to feature change events from the Features in this object so that
+   *  we can keep the display up to date.
+   *  @param event The change event.
+   **/
+  public void featureChanged (FeatureChangeEvent event) {
+    // re-read the information from the feature
+    redisplay ();
+  }
+
+  /**
+   *  Redisplay the bases.
+   **/
+  private void redisplay () {
+    String small_note = getFeature ().getNote ();
+
+    if (small_note == null) {
+      small_note = "";
+    } else {
+      if (small_note.length () > 50) {
+        small_note = small_note.substring (0, 50);
+      }
+    }
+
+    final String comment = ">" + small_note + "   - " +
+      getFeature ().getFirstBase () + ": " +
+      getFeature ().getLastBase ();
+      
+    final String bases = getFeature ().getBases ().toUpperCase ();
+    sequence_viewer.setSequence (comment, bases);
+  }
+
+  /**
+   *  Return the feature this component is showing information about.
+   **/
+  private Feature getFeature () {
+    return feature;
+  }
+
+  /**
+   *  Return the Entry that contains the Feature this object is displaying.
+   **/
+  private Entry getEntry () {
+    return entry;
+  }
+
+  /**
+   *  The Feature that this component is showing information about.
+   **/
+  private Feature feature = null;
+
+  /**
+   *  The SequenceViewer object that is displaying the feature bases.
+   **/
+  private SequenceViewer sequence_viewer;
+
+  /**
+   *  The Entry that contains the Feature this object is displaying.
+   **/
+  private Entry entry;
+
+  /**
+   *  If true then the amino acids will be numbered (every second line of the
+   *  display will be numbers rather than sequence).
+   **/
+  private final boolean include_numbers;
+}
diff --git a/uk/ac/sanger/artemis/components/FeatureDisplay.java b/uk/ac/sanger/artemis/components/FeatureDisplay.java
new file mode 100644
index 000000000..db5a79546
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeatureDisplay.java
@@ -0,0 +1,4699 @@
+/* FeatureDisplay.java
+ *
+ * created: Fri Oct  9 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 1998,1999,2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeatureDisplay.java,v 1.1 2004-06-09 09:46:37 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.util.StringVector;
+import uk.ac.sanger.artemis.io.Range;
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import java.awt.event.*;
+import java.awt.*;
+import java.lang.Math;
+import java.util.Vector;
+import java.util.Comparator;
+
+import javax.swing.JScrollBar;
+import javax.swing.JComponent;
+
+/**
+ *  This component is used for displaying an Entry.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeatureDisplay.java,v 1.1 2004-06-09 09:46:37 tjc Exp $
+ **/
+
+public class FeatureDisplay extends EntryGroupPanel
+  implements EntryGroupChangeListener,
+             EntryChangeListener, FeatureChangeListener,
+             SelectionChangeListener, GotoListener, SequenceChangeListener,
+             DisplayComponent, OptionChangeListener, DisplayAdjustmentListener
+{
+
+  /** Key code for calling zoomToSelection(). */
+  final static public int ZOOM_TO_SELECTION_KEY = KeyEvent.VK_Z;
+
+  final static public int SCROLLBAR_AT_TOP = 1;
+  final static public int SCROLLBAR_AT_BOTTOM = 2;
+  final static public int NO_SCROLLBAR = 3;
+
+  public final static int FORWARD = Bases.FORWARD;
+  public final static int REVERSE = Bases.REVERSE;
+
+  public final static int NO_FRAME        = FeatureSegment.NO_FRAME;
+  public final static int FORWARD_STRAND  = FeatureSegment.FORWARD_STRAND;
+  public final static int REVERSE_STRAND  = FeatureSegment.REVERSE_STRAND;
+  public final static int FORWARD_FRAME_1 = FeatureSegment.FORWARD_FRAME_1;
+  public final static int FORWARD_FRAME_2 = FeatureSegment.FORWARD_FRAME_2;
+  public final static int FORWARD_FRAME_3 = FeatureSegment.FORWARD_FRAME_3;
+  public final static int REVERSE_FRAME_3 = FeatureSegment.REVERSE_FRAME_3;
+  public final static int REVERSE_FRAME_2 = FeatureSegment.REVERSE_FRAME_2;
+  public final static int REVERSE_FRAME_1 = FeatureSegment.REVERSE_FRAME_1;
+  public final static int SCALE_LINE      = FeatureSegment.SCALE_LINE;
+
+
+  /**
+   *  The JScrollBar for this FeatureDisplay object.  We create the scrollbar
+   *  as part of this object rather than in the EntryEdit component because we
+   *  may need to change the parameters of the scrollbar later.
+   **/
+  private JScrollBar scrollbar = null;
+
+  /** A scroll bar for changing the viewing scale. */
+  private JScrollBar scale_changer = null;
+
+  /** Used to colour the frames. */
+  private Color light_grey = new Color(240, 240, 240);
+
+  /** Used to colour sequence line. */
+  private Color not_so_light_grey = new Color(200, 200, 200);
+
+  /**
+   *  The colour used for the active entry line when 
+   *  one_line_per_entry is set.
+   **/
+  private Color active_entry_colour = new Color(255, 255, 140);
+
+  /**
+   *  This Vector containing the references of those features that are
+   *  currently visible.
+   **/
+  private FeatureVector visible_features = new FeatureVector();
+
+  /**
+   *  If true updateVisibleFeatureVector() will be called by paint().
+   *  updateVisibleFeatureVector() sets this to false,
+   *  needVisibleFeatureVectorUpdate() sets this to true.
+   **/
+  private boolean update_visible_features = true;
+
+  /** Contains those objects listening for adjustment events. */
+  final private Vector adjustment_listener_list = new Vector();
+
+  /**
+   *  The index of the first base that we are displaying.  
+   *  Can be negative if hard_left_edge is true.
+   **/
+  private int left_edge_base = 1;
+
+  /** See getScaleFactor(). */
+  private int scale_factor = 3;
+
+  /** See getScaleFactor(). */
+  private float scale_value = 1;
+
+  /** true if labels should be shown. */
+  private boolean show_labels = true;
+
+  /**
+   *  This variable is true if the forward frame lines(or forward entry lines
+   *  - see one_line_per_entry) should be drawn.
+   **/
+  private boolean show_forward_lines = true;
+
+  /**
+   *  This variable is true if the reverse frame lines(or reverse entry lines
+   *  - see one_line_per_entry) should be drawn.
+   **/
+  private boolean show_reverse_lines = true;
+
+  /**
+   *  If true draw all features, sequence and scale lines reverse complemented.
+   **/
+  private boolean rev_comp_display = false;
+
+  /**
+   *  This variable is true if(for each strand) each entry should be on a
+   *  separate line.
+   **/
+  private boolean one_line_per_entry = false;
+
+  /**
+   *  If true the there will never be a gap between the left edge of the
+   *  screen and the first visible base.
+   **/
+  private boolean hard_left_edge = true;
+
+  /** true if source features should be shown. */
+  private boolean show_source_features = false;
+
+  /** true if stop codons should be shown.     */
+  private boolean show_stop_codons = true;
+
+  /** true if start codons should be shown.    */
+  private boolean show_start_codons = false;
+
+  /** true if directional arrows should be shown on features. */
+  private boolean show_feature_arrows;
+
+  /** true a black border will be drawn around each feature.  */
+  private boolean show_feature_borders;
+
+  /**
+   *  This variable is true if each base should be drawn in a different colour
+   *  at scale feature 1.
+   **/
+  private boolean show_base_colours = false;
+
+  /**
+   *  All features(not just CDS features) will be drawn on the frame lines if
+   *  and only if this variable is true.  See setFrameFeaturesFlag() and
+   *  getFrameFeaturesFlag().
+   **/
+  private boolean frame_features_flag = false;
+
+  /**
+   *  The position(s) of the last mouse click on the dna line.  The
+   *  MarkerRange contains a reference to the appropriate Strand and contains
+   *  the base positions.  See getMarkerRangeFromPosition() to understand why
+   *  one click can give multiple bases.
+   **/
+  private MarkerRange click_range = null;
+
+  /**
+   *  The last(FeatureSegment) Marker that the user clicked on.  This is used
+   *  for dragging the ends of segments.
+   **/
+  private Marker click_segment_marker = null;
+
+  /**
+   *  This is true if click_segment_marker is the Marker at the start of
+   *  segment false otherwise.  The value is only useful if
+   *  click_segment_marker is set.
+   **/
+  private boolean click_segment_marker_is_start_marker = false;
+
+  /**
+   *  When a FeatureSegment Marker drag starts, this is set to the Marker at
+   *  the other end of the segment.  This is used to check that the drag has
+   *  not move the Marker too far(past the end of the segment).
+   **/
+  private Marker other_end_of_segment_marker = null;
+
+  /**
+   *  Features with a /score qualifier less than this value will not be shown.
+   **/
+  private int current_min_score = 0;
+
+  /**
+   *  Features with a /score qualifier greater than this value will not be
+   *  shown.
+   **/
+  private int current_max_score = 100;
+
+  private MouseEvent last_mouse_press_event;
+
+  /**
+   *  If set no DisplayAdjustment events will be sent.  This is set by
+   *  displayAdjustmentValueChanged() to prevent an event we send from
+   *  returning to us(a FeatureDisplay can listen for DisplayAdjustment
+   *  events from another FeatureDisplay).
+   **/
+  private boolean disable_display_events = false;
+
+  /**
+   *  Set to true by selectionChanged() to tell updateVisibleFeatureVector()
+   *  to raise the contents of the select before updating.
+   **/
+  private boolean raise_selection_flag = false;
+ 
+  /** the minimum distance in pixels between the labels. */
+  private final static int MINIMUM_LABEL_SPACING = 80;
+ 
+  /** colour used for A. */
+  private static Color dark_green = new Color(0, 150, 0);
+
+  /**
+   *  Used by drawOneLetter() - declared here so that we we don't need to
+   *  allocated a whole array each time drawOneLetter() is called.
+   **/
+  private final char [] draw_one_char_temp_array = new char [1];
+
+ 
+  /**
+   *  Create a new FeatureDisplay object with the horizontal scrollbar at the
+   *  bottom of the component.
+   *  @param entry_group The EntryGroup that this component will display.
+   *  @param selection The Selection object for this component.  Selected
+   *    objects will be highlighted.
+   *  @param goto_event_source The object to use when we need to call
+   *    gotoBase().
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public FeatureDisplay(final EntryGroup entry_group,
+                        final Selection selection,
+                        final GotoEventSource goto_event_source,
+                        final BasePlotGroup base_plot_group) 
+  {
+    this(entry_group, selection, goto_event_source,
+          base_plot_group, SCROLLBAR_AT_BOTTOM);
+  }
+
+  /**
+   *  Create a new FeatureDisplay object.
+   *  @param entry_group The EntryGroup that this component will display.
+   *  @param owning_component The EntryEdit object that contains the selection
+   *    that this component uses.
+   *  @param scrollbar_at_top If true the horizontal scrollbar will be at the
+   *    top of component.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   *  @param scrollbar_style Controls the type of horizontal scrollbar.  Must
+   *    be one of SCROLLBAR_AT_TOP, SCROLLBAR_AT_BOTTOM or NO_SCROLLBAR.
+   **/
+  public FeatureDisplay(final EntryGroup entry_group,
+                        final Selection selection,
+                        final GotoEventSource goto_event_source,
+                        final BasePlotGroup base_plot_group,
+                        final int scrollbar_style) 
+  {
+    super(entry_group, selection, goto_event_source, base_plot_group);
+
+    show_feature_arrows =
+      Options.getOptions().getPropertyTruthValue("draw_feature_arrows");
+
+    show_feature_borders =
+      Options.getOptions().getPropertyTruthValue("draw_feature_borders");
+
+    frame_features_flag =
+      Options.getOptions().getPropertyTruthValue("features_on_frame_lines");
+
+    one_line_per_entry =
+      Options.getOptions().getPropertyTruthValue("one_line_per_entry");
+
+    show_labels =
+      Options.getOptions().getPropertyTruthValue("feature_labels");
+
+//  getCanvas().addComponentListener(new ComponentAdapter()
+    addComponentListener(new ComponentAdapter()
+    {
+      public void componentResized(ComponentEvent e) 
+      {
+        // update the scroll bar as soon as we know the size of the canvas
+        fixScrollbar();
+        needVisibleFeatureVectorUpdate();
+        fireAdjustmentEvent(DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT);
+      }
+
+      public void componentShown(ComponentEvent e) 
+      {
+        // update the scroll bar as soon as we know the size of the canvas
+        fixScrollbar();
+        needVisibleFeatureVectorUpdate();
+        fireAdjustmentEvent(DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT);
+      }
+    });
+
+    setScaleValue();
+
+    if(scrollbar_style == SCROLLBAR_AT_TOP) 
+      createScrollbar(true);
+    else
+    {
+      if(scrollbar_style == SCROLLBAR_AT_BOTTOM) 
+        createScrollbar(false);
+    }
+
+    createScaleScrollbar();
+    fixCanvasSize();
+    fixScrollbar();
+    addListeners();
+
+    needVisibleFeatureVectorUpdate();
+
+    fireAdjustmentEvent(DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT);
+
+    getSelection().addSelectionChangeListener(this);
+    getGotoEventSource().addGotoListener(this);
+
+    getEntryGroup().addEntryGroupChangeListener(this);
+    getEntryGroup().addEntryChangeListener(this);
+    getEntryGroup().addFeatureChangeListener(this);
+
+    getBases().addSequenceChangeListener(this, Bases.MIN_PRIORITY);
+
+    Options.getOptions().addOptionChangeListener(this);
+  }
+
+  /**
+   *  Returns the value of a flag that indicates whether this component can be
+   *  traversed using Tab or Shift-Tab keyboard focus traversal - returns true
+   *  for FeatureDisplay components
+   **/
+// tjc - deprecated replaced by isFocusable().
+//public boolean isFocusTraversable()
+//{
+//  return true;
+//}
+
+  /**
+   *  Overriden to call fixCanvasSize()
+   **/
+  public void setVisible(final boolean visible) 
+  {
+    super.setVisible(visible);
+    fixCanvasSize();
+  }
+
+  /**
+   *  Set value of the show label flag.
+   *  @param show_label Show labels if and only if this argument is true.
+   **/
+  public void setShowLabels(boolean show_labels) 
+  {
+    if(this.show_labels != show_labels) 
+    {
+      this.show_labels = show_labels;
+      fixCanvasSize();
+    } 
+  }
+
+  /**
+   *  Get the value of the "show label" flag.
+   **/
+  public boolean getShowLabels() 
+  {
+    return show_labels;
+  }
+
+  /**
+   *  Set value of the "show forward frame lines" flag.
+   *  @param show_forward_lines Show forward frame lines if and only if
+   *    this argument is true.
+   **/
+  public void setShowForwardFrameLines(boolean show_forward_lines) 
+  {
+    if(this.show_forward_lines != show_forward_lines) 
+    {
+      this.show_forward_lines = show_forward_lines;
+      fixCanvasSize();
+    } 
+  }
+
+  /**
+   *  Get the value of the "show forward frame lines" flag.
+   **/
+  public boolean getShowForwardFrameLines() 
+  {
+    return show_forward_lines;
+  }
+
+  /**
+   *  Set value of the "show reverse frame lines" flag.
+   *  @param show_reverse_lines Show frame lines if and only if this
+   *    argument is true.
+   **/
+  public void setShowReverseFrameLines(boolean show_reverse_lines) 
+  {
+    if(this.show_reverse_lines != show_reverse_lines) 
+    {
+      this.show_reverse_lines = show_reverse_lines;
+      fixCanvasSize();
+    }
+  }
+
+  /**
+   *  Get the value of the "show source features" flag.
+   **/
+  public boolean getShowSourceFeatures() 
+  {
+    return show_source_features;
+  }
+
+  /**
+   *  Set value of the "show source features" flag.
+   *  @param show_source_features Show features with a "source" key if and
+   *    only if this argument is true.
+   **/
+  public void setShowSourceFeatures(boolean show_source_features) 
+  {
+    if(this.show_source_features != show_source_features) 
+    {
+      this.show_source_features = show_source_features;
+      needVisibleFeatureVectorUpdate();
+//    getCanvas().repaint();
+      repaint();
+    } 
+  }
+
+  /**
+   *  Get the value of the "show frame lines" flag.
+   **/
+  public boolean getShowReverseFrameLines()
+  {
+    return show_reverse_lines;
+  }
+
+  /**
+   *  Set value of the show base colours flag.
+   *  @param show_base_colours At scale_factor less than two show each base in
+   *    a different colour if and only if this argument is true.
+   **/
+  public void setShowBaseColours(boolean show_base_colours) 
+  {
+    if(this.show_base_colours != show_base_colours) 
+    {
+      this.show_base_colours = show_base_colours;
+      if(getScaleFactor() > 1) 
+        setScaleFactor(1);
+//    getCanvas().repaint();
+      repaint();
+    } 
+  }
+
+  /**
+   *  Get the value of the "show base colours" flag.
+   **/
+  public boolean getShowBaseColours() 
+  {
+    return show_base_colours;
+  }
+
+  /**
+   *  Set value of the "one line per entry" flag.
+   *  @param one_line_per_entry If true then each entry will be shown on a
+   *    different line, instead of showing frame lines.
+   **/
+  public void setOneLinePerEntry(final boolean one_line_per_entry)
+  {
+    if(this.one_line_per_entry != one_line_per_entry) 
+    {
+      this.one_line_per_entry = one_line_per_entry;
+      fixCanvasSize();
+    }
+  }
+
+  /**
+   *  Get the value of the "one line per entry" flag.
+   **/
+  public boolean getOneLinePerEntryFlag() 
+  {
+    return one_line_per_entry;
+  }
+
+  /**
+   *  Set value of the "hard left edge" flag.
+   *  @param hard_left_edge If true the there will never be a gap between the
+   *    left edge of the screen and the first visible base.  If false base one
+   *    can be moved to the centre of the display.
+   **/
+  public void setHardLeftEdge(final boolean hard_left_edge) 
+  {
+    if(this.hard_left_edge != hard_left_edge) 
+    {
+      this.hard_left_edge = hard_left_edge;
+      if(hard_left_edge && getForwardBaseAtLeftEdge() < 1) 
+        setFirstVisibleForwardBase(1);
+      
+      fixScrollbar();
+    } 
+  }
+
+  /**
+   *  Get the value of the "hard left edge" flag.
+   **/
+  public boolean getHardLeftEdgeFlag() 
+  {
+    return hard_left_edge;
+  }
+
+  /**
+   *  Set value of the show stop codons flag.
+   *  @param show_stop_codons Show stop codons if and only if this argument is
+   *    true.
+   **/
+  public void setShowStopCodons(boolean show_stop_codons) 
+  {
+    if(this.show_stop_codons != show_stop_codons) 
+    {
+      this.show_stop_codons = show_stop_codons;
+      getCanvas().repaint();
+    } 
+  }
+
+  /**
+   *  Return the value of the "show stop codons" flag.
+   **/
+  public boolean getShowStopCodons() 
+  {
+    return show_stop_codons;
+  }
+
+  /**
+   *  Set value of the show start codons flag.
+   *  @param show_start_codons Show start codons if and only if this argument
+   *    is true.
+   **/
+  public void setShowStartCodons(boolean show_start_codons) 
+  {
+    if(this.show_start_codons != show_start_codons) 
+    {
+      this.show_start_codons = show_start_codons;
+      getCanvas().repaint();
+    } 
+  }
+
+  /**
+   *  Return the value of the "show start codons" flag.
+   **/
+  public boolean getShowStartCodons() 
+  {
+    return show_start_codons;
+  }
+
+  /**
+   *  Set value of the reverse complement display flag.
+   *  @param show_start_codons Draw all features and sequence reverse
+   *    complemented if and only if this argument is true.
+   **/
+  public void setRevCompDisplay(boolean rev_comp_display) 
+  {
+    if(this.rev_comp_display != rev_comp_display) 
+    {
+      this.rev_comp_display = rev_comp_display;
+      int remember_position = getCentreForwardBase();
+
+      // we want to keep the selection visible after the flip, so
+      // that will override the centre position
+      final Marker first_base_marker =
+        getSelection().getStartBaseOfSelection();
+
+      if(first_base_marker != null && baseVisible(first_base_marker)) 
+        remember_position = first_base_marker.getRawPosition();
+
+      final Marker last_base_marker =
+        getSelection().getStartBaseOfSelection();
+
+      if(last_base_marker != null && baseVisible(last_base_marker)) 
+        remember_position = last_base_marker.getRawPosition();
+
+      fireAdjustmentEvent(DisplayAdjustmentEvent.REV_COMP_EVENT);
+
+      makeBaseVisibleInternal(remember_position, isRevCompDisplay(), true);
+
+      needVisibleFeatureVectorUpdate();
+      fixScrollbar();
+      getCanvas().repaint();
+    }
+  }
+
+  /**
+   *  Return the value of the "reverse complement display" flag.
+   **/
+  public boolean isRevCompDisplay() 
+  {
+    return rev_comp_display;
+  }
+
+  /**
+   *  Set value of the show feature arrows flag.
+   *  @param show_feature_arrows Show directional arrows if and only if this
+   *    argument is true.
+   **/
+  public void setShowFeatureArrows(boolean show_feature_arrows)
+  {
+    if(this.show_feature_arrows != show_feature_arrows)
+    {
+      this.show_feature_arrows = show_feature_arrows;
+      getCanvas().repaint();
+    } 
+  }
+
+  /**
+   *  Return the value of the "show feature arrows" flag.
+   **/
+  public boolean getShowFeatureArrows() 
+  {
+    return show_feature_arrows;
+  }
+
+  /**
+   *  Set value of the show feature borders flag.
+   *  @param show_feature_borders Draw a border around each feature if and
+   *    only if this argument is true.
+   **/
+  public void setShowFeatureBorders(boolean show_feature_borders) 
+  {
+    if(this.show_feature_borders != show_feature_borders) 
+    {
+      this.show_feature_borders = show_feature_borders;
+      getCanvas().repaint();
+    } 
+  }
+
+  /**
+   *  Return the value of the "show feature borders" flag.
+   **/
+  public boolean getShowFeatureBorders() 
+  {
+    return show_feature_borders;
+  }
+
+  /**
+   *  Set value of the show frame features flag.
+   *  @param frame_features_flag All features(not just CDS features) will be
+   *    drawn on the frame lines if and only if this argument is true.
+   **/
+  public void setFrameFeaturesFlag(boolean frame_features_flag) 
+  {
+    if(this.frame_features_flag != frame_features_flag) 
+    {
+      this.frame_features_flag = frame_features_flag;
+      getCanvas().repaint();
+    } 
+  }
+
+  /**
+   *  Return the value of the "show frame features" flag.
+   **/
+  public boolean getFrameFeaturesFlag() 
+  {
+    return frame_features_flag;
+  }
+
+  /**
+   *  Set the value of the minimum score for this FeatureDisplay - features
+   *  that have a /score lower than this value are never shown.
+   **/
+  public void setMinimumScore(final int minimum_score) 
+  {
+    current_min_score = minimum_score;
+    needVisibleFeatureVectorUpdate();
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Return the value of the minimum score for this FeatureDisplay - see
+   *  setMinimumScore().
+   **/
+  public int getMinimumScore() 
+  {
+    return current_min_score;
+  }
+
+  /**
+   *  Set the value of the maximum score for this FeatureDisplay - features
+   *  that have a /score higher than this value are never shown.
+   **/
+  public void setMaximumScore(final int maximum_score) 
+  {
+    current_max_score = maximum_score;
+    needVisibleFeatureVectorUpdate();
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Return the value of the maximum score for this FeatureDisplay - see
+   *  setMaximumScore().
+   **/
+  public int getMaximumScore() 
+  {
+    return current_max_score;
+  }
+
+  /**
+   *  Redraw this component.  This method is public so that other classes can
+   *  force an update.  for example this is called when the options files is
+   *  re-read.
+   **/
+  public void redisplay() 
+  {
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Adds the specified event adjustment listener to receive adjustment
+   *  change events from this object.
+   *  @param l the event change listener.
+   **/
+  public void addDisplayAdjustmentListener(DisplayAdjustmentListener l) 
+  {
+    adjustment_listener_list.addElement(l);
+  }
+
+  /**
+   *  Removes the specified event listener so that it no longer receives
+   *  adjustment change events from this object.
+   *  @param l the event change listener.
+   **/
+  public void removeDisplayAdjustmentListener(DisplayAdjustmentListener l) 
+  {
+    adjustment_listener_list.removeElement(l);
+  }
+
+  /**
+   *  Handle key press events.  This is static because making it non-static
+   *  triggered a java.lang.VerifyError
+   **/
+  private static void handleKeyPress(final FeatureDisplay feature_display,
+                                      final KeyEvent event) 
+  {
+    // this is done so that menu shortcuts don't cause each action to be
+    // performed twice
+    if(event.getModifiers() != 0) 
+      return;
+
+    switch(event.getKeyCode()) 
+    {
+      case ZOOM_TO_SELECTION_KEY:
+        FeaturePopup.zoomToSelection(feature_display);
+        break;
+      default:
+        break;
+    }
+  }
+
+  /**
+   *  Set the scale factor and update the display if the scale factor has
+   *  changed.  A factor of zero means the full translation will be visible.
+   *  At higher scale factors only stop codons are visible, and a bigger
+   *  number will mean more bases are visible.
+   **/
+  public void setScaleFactor(int scale_factor) 
+  {
+    if(this.scale_factor != scale_factor) 
+    {
+      // we will try to keep the base in the centre of the view to stay where
+      // it is, so we save it's position in remember_position.
+      int remember_position = getCentreForwardBase();
+
+      // if the first base is visible then keep it visible
+      if(hard_left_edge && getFirstVisibleForwardBase() == 1) 
+        remember_position = 1;
+
+      if(!getSelection().isEmpty()) 
+      {
+        // but, we want to keep the selection visible after a scale change, so
+        // that will override the centre position
+        final Marker first_base_marker =
+          getSelection().getStartBaseOfSelection();
+
+        final int first_base_marker_raw_position =
+                  first_base_marker.getRawPosition();
+        final int first_base_marker_position;
+
+        if(isRevCompDisplay()) 
+          first_base_marker_position =
+              getBases().getComplementPosition(first_base_marker_raw_position);
+        else 
+          first_base_marker_position = first_base_marker_raw_position;
+
+        final Marker last_base_marker =
+                              getSelection().getEndBaseOfSelection();
+
+        final int last_base_marker_raw_position =
+                                   last_base_marker.getRawPosition();
+
+        final int last_base_marker_position;
+
+        if(isRevCompDisplay()) 
+          last_base_marker_position =
+            getBases().getComplementPosition(last_base_marker_raw_position);
+        else 
+          last_base_marker_position = last_base_marker_raw_position;
+
+        final int lowest_visible_base = getFirstVisibleForwardBase();
+        final int highest_visible_base = getLastVisibleForwardBase();
+
+        // first selected base or first visible base, whichever is greater
+        int restricted_first_selected_base = lowest_visible_base;
+
+        // last selected base or last visible base, whichever is smaller
+        int restricted_last_selected_base = highest_visible_base;
+
+        if(first_base_marker != null) 
+        {
+          if(first_base_marker_position > lowest_visible_base &&
+              first_base_marker_position < highest_visible_base) 
+            restricted_first_selected_base = first_base_marker_position;
+        }
+
+        if(last_base_marker != null) 
+        {
+          if(last_base_marker_position < highest_visible_base &&
+              last_base_marker_position > lowest_visible_base) 
+            restricted_last_selected_base = last_base_marker_position;
+        }
+
+        if(getSelection().getMarkerRange() == null) 
+          remember_position = restricted_first_selected_base;
+        else 
+        {
+          // keep the centre of the selection in the middle of the display if
+          // a range of bases is selected
+          remember_position = restricted_first_selected_base +
+                             (restricted_last_selected_base -
+                               restricted_first_selected_base) / 2;
+        }
+      }
+
+      this.scale_factor = scale_factor;
+
+      setScaleValue();
+      scale_changer.setValue(scale_factor);
+      setCentreVisibleForwardBase(remember_position);
+      fixScrollbar();
+      fireAdjustmentEvent(DisplayAdjustmentEvent.SCALE_ADJUST_EVENT);
+      needVisibleFeatureVectorUpdate();
+
+      getCanvas().repaint();
+    }
+  }
+
+  /**
+   *  Implementation of the FeatureChangeListener interface.  We listen to
+   *  FeatureChange events so that we can update the display if qualifiers
+   *  change.
+   **/
+  public void featureChanged(final FeatureChangeEvent event) 
+  {
+    final Feature event_feature = event.getFeature();
+
+    // the feature isn't in an active entry
+    if(!getEntryGroup().contains(event_feature)) 
+      return;
+
+    // if the feature is visible now or is in the list of visible features
+    //(ie. it was visible previously) then redisplay.
+    if(featureVisible(event_feature) ||
+       getVisibleFeatures().contains(event_feature)) 
+    {
+      // update the visible_features vector
+      if(getVisibleFeatures().contains(event_feature) &&
+         !featureVisible(event_feature)) 
+        getVisibleFeatures().remove(event_feature);
+      else 
+      {
+        // the visibility of the feature has changed
+        if(!getVisibleFeatures().contains(event_feature) &&
+            featureVisible(event_feature)) 
+          getVisibleFeatures().add(event_feature);
+      }
+
+      getCanvas().repaint();
+    }
+  }
+
+  /**
+   *  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_ACTIVE:
+      case EntryGroupChangeEvent.ENTRY_DELETED:
+      case EntryGroupChangeEvent.ENTRY_INACTIVE:
+      if(getOneLinePerEntryFlag()) 
+        fixCanvasSize();
+      
+      needVisibleFeatureVectorUpdate();
+      break;
+    }
+
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so that we can update the display if features are
+   *  added or deleted.
+   **/
+  public void entryChanged(final EntryChangeEvent event) 
+  {
+    switch(event.getType()) 
+    {
+      case EntryChangeEvent.FEATURE_DELETED:
+        remove(event.getFeature());
+        break;
+      case EntryChangeEvent.FEATURE_ADDED:
+        add(event.getFeature());
+        break;
+    }
+  }
+
+  /**
+   *  Implementation of the SelectionChangeListener interface.  We listen to
+   *  SelectionChange events so that we can update the list to reflect the
+   *  current selection.
+   **/
+  public void selectionChanged(final SelectionChangeEvent event) 
+  {
+    // don't bother with events we sent ourself
+    if(event.getSource() == this) 
+      return;
+
+    needVisibleFeatureVectorUpdate();
+
+    if(event.getType() == SelectionChangeEvent.SELECTION_CHANGED) 
+      raise_selection_flag = true;
+
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Implementation of the SequenceChangeListener interface.
+   **/
+  public void sequenceChanged(final SequenceChangeEvent event) 
+  {
+    visible_features = new FeatureVector();
+
+    if(event.getType() == SequenceChangeEvent.REVERSE_COMPLEMENT) 
+    {
+      final int old_centre_position = getCentreForwardBase();
+
+      final int new_centre_position =
+                   getBases().getComplementPosition(old_centre_position);
+
+      makeBaseVisibleInternal(new_centre_position, true, false);
+    } 
+    else 
+      makeBaseVisibleInternal(event.getPosition(), true, false);
+
+    fixScrollbar();
+    needVisibleFeatureVectorUpdate();
+    getCanvas().repaint();
+
+    if(event.getType() == SequenceChangeEvent.REVERSE_COMPLEMENT) 
+      fireAdjustmentEvent(DisplayAdjustmentEvent.REV_COMP_EVENT);
+    else 
+      fireAdjustmentEvent(DisplayAdjustmentEvent.SCROLL_ADJUST_EVENT);
+  }
+
+  /**
+   *  Invoked when an Option is changed.
+   **/
+  public void optionChanged(OptionChangeEvent event) 
+  {
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Implementation of the GotoListener interface.  Invoked when the listener
+   *  should goto to the given base.
+   **/
+  public void performGoto(final GotoEvent event) 
+  {
+    makeBaseVisible(event.getMarker());
+  }
+
+  /**
+   *  Implementation of the DisplayAdjustmentListener interface.  Invoked when
+   *  a component(FeatureDisplay) scrolls or changes the scale.  Set the
+   *  position and scale of this FeatureDisplay to match the event
+   **/
+  public void displayAdjustmentValueChanged(DisplayAdjustmentEvent event) 
+  {
+    disable_display_events = true;
+    try 
+    {
+      setScaleFactor(event.getScaleFactor());
+      setFirstVisibleForwardBase(event.getStart());
+    }
+    finally 
+    {
+      disable_display_events = false;
+    }
+  }
+
+  /**
+   *  Return a MarkerRange that exactly covers the visible bases
+   **/
+  public MarkerRange getVisibleMarkerRange() 
+  {
+    final int first_base = getFirstVisibleForwardBase();
+    final int last_base  = getLastVisibleForwardBase();
+
+    final Strand strand = getBases().getForwardStrand();
+
+    try 
+    {
+      return strand.makeMarkerRangeFromPositions(first_base, last_base);
+    }
+    catch(OutOfRangeException e) 
+    {
+      throw new Error("internal error - unexpected exception: " + e);
+    }
+  }
+
+  /**
+   *  Arrange for the given feature to be drawn last - ie. so that it will
+   *  appear at the front of the other features.
+   **/
+  void raiseFeature(Feature feature) 
+  {
+    if(getVisibleFeatures().remove(feature)) 
+    {
+      getVisibleFeatures().addElementAtEnd(feature);
+      getCanvas().repaint();
+    }
+  }
+
+  /**
+   *  Arrange for the given feature to be drawn first - ie. so that it will
+   *  appear at the back of the other features.
+   **/
+  void lowerFeature(Feature feature) 
+  {
+    if(getVisibleFeatures().remove(feature))
+    {
+      getVisibleFeatures().insertElementAt(feature, 0);
+      getCanvas().repaint();
+    }
+  }
+
+  /**
+   *  Redraw the display with the smallest features on top.  Selected features
+   *  will always be on top.
+   **/
+  void smallestToFront() 
+  {
+    visible_features = new FeatureVector();
+    needVisibleFeatureVectorUpdate();
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Scroll the display so the given base on the Strand currently display on
+   *  top will be in the centre of the display.  No events are sent.
+   *  @param base_position The new centre base position.
+   **/
+  private void setCentreVisibleForwardBase(final int base_position) 
+  {
+    final int max_visible_bases = getMaxVisibleBases();
+    final int possible_base_position = base_position - max_visible_bases / 2;
+    int real_base_position;
+
+    if(possible_base_position < 1 && hard_left_edge) 
+      real_base_position = 1;
+    else 
+      real_base_position = possible_base_position;
+
+    if(real_base_position > getSequenceLength()) 
+      real_base_position = getSequenceLength();
+
+    setFirstVisibleForwardBase(real_base_position);
+  }
+
+  /**
+   *  Scroll the display so that the given base is in the middle of the
+   *  screen.  This method obeys the rev_comp_display flag.
+   *  @param base_position The base position to make visible.
+   *  @param forward true means FORWARD - the base_position refers to a
+   *    position on the forward strand, false means REVERSE - the
+   *    base_position refers to the reverse strand.
+   *  @param send_event Send a DisplayAdjustmentEvent if and only if this is
+   *    true
+   **/
+  private void makeBaseVisibleInternal(final int base_position,
+                                        final boolean forward,
+                                        final boolean send_event) 
+  {
+    int forward_base_position = base_position;
+
+    if(!forward ^ isRevCompDisplay()) 
+      forward_base_position =
+        getBases().getComplementPosition(forward_base_position);
+
+    setCentreVisibleForwardBase(forward_base_position);
+
+    if(send_event) 
+      fireAdjustmentEvent(DisplayAdjustmentEvent.SCROLL_ADJUST_EVENT);
+  }
+
+  /**
+   *  Scroll the display so that the given base is in the middle of the screen
+   *  @param base_marker The Marker of base to make visible.
+   **/
+  private void makeBaseVisible(final Marker base_marker) 
+  {
+    makeBaseVisibleInternal(base_marker.getPosition(),
+                             base_marker.getStrand().isForwardStrand(),
+                             true);
+  }
+
+  /**
+   *  Scroll the display so that the given base is in the middle of the screen.
+   *  @param base The base to scroll to.
+   **/
+  public void makeBaseVisible(final int base) 
+  {
+    makeBaseVisibleInternal(base, true, true);
+  }
+
+  /**
+   *  This method is called to add a single feature to the display by adding it
+   *  to the vector of visible features(if it is visible that is).
+   *  @param feature The object to add
+   **/
+  private void add(Feature feature) 
+  {
+    if(getEntryGroup().isActive(feature.getEntry()) &&
+        featureVisible(feature)) 
+    {
+      if(visible_features.contains(feature)) 
+      {
+//        throw new Error("internal error - feature added a second time");
+      } 
+      else 
+        getVisibleFeatures().addElementAtEnd(feature);
+    }
+
+    getCanvas().repaint();
+  }
+
+
+  /**
+   *  The method is called when a Feature should be removed from the display.
+   *  The feature is removed from the vector of visible features.
+   *  @param feature The object to remove
+   **/
+  private void remove(Feature feature) 
+  {
+    if(visible_features != null) 
+      visible_features.remove(feature);
+
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Returns the vector containing those features that are currently visible.
+   **/
+  private FeatureVector getVisibleFeatures() 
+  {
+    return visible_features;
+  }
+
+  /**
+   *  Returns a copy of the vector containing those features that are
+   *  currently visible.
+   **/
+  public FeatureVector getCurrentVisibleFeatures() 
+  {
+    return(FeatureVector)visible_features.clone();
+  }
+
+  /**
+   *  Returns a Range that starts at the first visible base and ends at the
+   *  last visible base.
+   **/
+  private Range getVisibleRange()
+  {
+    final int first_visible_base = getFirstVisibleForwardBase();
+    final int last_visible_base = getLastVisibleForwardBase();
+
+    if(first_visible_base <= last_visible_base) 
+      return newRange(first_visible_base, last_visible_base);
+    else 
+      return null;
+  }
+
+  /**
+   *  This is called after scrolling to add all features that have become
+   *  visible and remove those that have become invisible.  The method changes
+   *  visible_features as little as possible so that features that were at the
+   *  end(and hence drawn last/on top) stay there.  Selected features and
+   *  segments will always be on top of unselected ones.
+   **/
+  private void updateVisibleFeatureVector() 
+  {
+    final Range visible_range;
+
+    if(getCanvas().getSize().width == 0)
+    {
+      // don't bother doing any thinking
+      visible_features = new FeatureVector();
+      return;
+    }
+
+    if(raise_selection_flag)
+    {
+      final FeatureVector all_features = getSelection().getAllFeatures();
+
+      for(int i = 0 ; i < all_features.size() ; ++i) 
+        raiseFeature(all_features.elementAt(i));
+
+      raise_selection_flag = false;
+    }
+
+    if(isRevCompDisplay())
+    {
+      final int first_visible_base = getFirstVisibleReverseBase();
+      final int last_visible_base = getLastVisibleReverseBase();
+      visible_range = newRange(first_visible_base, last_visible_base);
+    } 
+    else 
+      visible_range = getVisibleRange();
+
+    if(visible_range == null) 
+    {
+      visible_features = new FeatureVector();
+      return;
+    }
+
+    final FeatureVector real_visible_features =
+      getSortedFeaturesInRange(visible_range);
+
+    final FeatureVector new_visible_features = new FeatureVector();
+
+    // add features that are in visible_features and
+    // real_visible_features - ie features that are still visible
+    for(int i = 0 ; i < visible_features.size() ; ++i) 
+    {
+      final Feature new_feature = visible_features.elementAt(i);
+      if(real_visible_features.contains(new_feature)) 
+        new_visible_features.addElementAtEnd(new_feature);
+    }
+
+    // add features that are in real_visible_features and not currently
+    // in visible_features and are not selected(selected features will be
+    // added last so that they stay on top).
+    for(int i = 0 ; i < real_visible_features.size() ; ++i) 
+    {
+      final Feature new_feature = real_visible_features.elementAt(i);
+
+      if(!visible_features.contains(new_feature) &&
+          !getSelection().contains(new_feature)) {
+        new_visible_features.addElementAtEnd(new_feature);
+      }
+    }
+
+    final FeatureVector selection_features = getSelection().getAllFeatures();
+
+    // now add features that are in real_visible_features, are not in
+    // visible_features and are selected(selected features are added last so
+    // that they stay on top).
+    for(int i = 0 ; i < real_visible_features.size() ; ++i) 
+    {
+      final Feature new_feature = real_visible_features.elementAt(i);
+      if(!visible_features.contains(new_feature) &&
+          selection_features.contains(new_feature)) 
+        new_visible_features.addElementAtEnd(new_feature);
+    }
+
+    visible_features = new_visible_features;
+    update_visible_features = false;
+  }
+
+  /**
+   *  This is used by getSortedFeaturesInRange().
+   **/
+  final private static Comparator feature_comparator = new Comparator() 
+  {
+    /**
+     *  Compare two Objects with respect to ordering.
+     *  @return a negative number if feature1_object is less than
+     *    feature2_object ; a positive number if feature1_object is greater
+     *    than feature2_object; else 0
+     **/
+    public int compare(final Object feature1_object,
+                       final Object feature2_object) 
+    {
+      final Feature feature1 =(Feature) feature1_object;
+      final Feature feature2 =(Feature) feature2_object;
+
+      final int feature1_size = feature1.getBaseCount();
+      final int feature2_size = feature2.getBaseCount();
+
+      if(feature1_size > feature2_size) 
+        return -1;
+      else 
+      {
+        if(feature1_size < feature2_size) 
+          return 1;
+        else
+        {
+          // use hash value as a last resort
+          if(feature1.hashCode() < feature2.hashCode()) 
+            return -1;
+          else 
+          {
+            if(feature1.hashCode() == feature2.hashCode())
+              return 0;
+            else
+              return 1;
+          }
+        }
+      }
+    }
+  };
+
+  /**
+   *  Return a vector containing the references of the Feature objects in the
+   *  EntryGroup that are within the given range are which should be
+   *  displayed.  source features are not returned unless show_source_features
+   *  is true.  features with a score less than getMinimumScore() or higher
+   *  than getMaximumScore() aren't returned.
+   *  @param range Return features that overlap this range - ie the start of
+   *    the feature is less than or equal to the end of the range and the end
+   *    of the feature is greater than or equal to the start of the range.
+   *  @return The features the are within the given range.  The returned
+   *    object is a copy - changes will not effect the EntryGroup object
+   *    itself.  The Features are sorted so that the features with the least
+   *    bases come last, which is the reverse of Entry.getFeaturesInRange().
+   **/
+  private FeatureVector getSortedFeaturesInRange(final Range range) 
+  {
+    try
+    {
+      FeatureVector features_from_entry =
+        getEntryGroup().getFeaturesInRange(range);
+
+      final int min_score = getMinimumScore();
+      final int max_score = getMaximumScore();
+
+      final FeatureVector filtered_features = new FeatureVector();
+
+      // filter out low and high scoring features and(possibly) source
+      // features
+      for(int i = features_from_entry.size() - 1 ; i >= 0 ; --i)
+      {
+        final Feature this_feature = features_from_entry.elementAt(i);
+
+        if(this_feature.getKey().equals("source") &&
+            !getShowSourceFeatures() &&
+            !getSelection().contains(this_feature)) 
+          continue;
+
+        if(min_score > 0 || max_score < 100) 
+        {
+          final int this_feature_score = this_feature.getScore();
+
+          // features with no /score are always shown
+          if(this_feature_score != -1 &&
+             (this_feature_score < getMinimumScore() ||
+               this_feature_score > getMaximumScore()) &&
+              !getSelection().contains(this_feature)) 
+            continue;
+        }
+
+        filtered_features.add(this_feature);
+      }
+
+      features_from_entry = filtered_features;
+
+      final FeatureVector sorted_features =
+        features_from_entry.sort(feature_comparator);
+
+      return sorted_features;
+    }
+    catch(OutOfRangeException e) 
+    {
+      throw new Error("internal error - unexpected exception: " + e);
+    }
+  }
+
+  /**
+   *  The main paint function for the canvas.  An off screen image 
+   *  used for double buffering when drawing the canvas.
+   *  @param g The Graphics object of the canvas.
+   **/
+  protected void paintCanvas(Graphics g) 
+  {
+    if(!isVisible()) 
+      return;
+
+    if(update_visible_features) 
+      updateVisibleFeatureVector();
+
+    fillBackground(g);
+
+    final Selection selection = getSelection();
+    final FeatureVector selected_features = selection.getAllFeatures();
+
+    final FeatureSegmentVector selected_segments =
+      selection.getSelectedSegments();
+
+    // we draw the feature backgrounds first then the visible indication
+    // that there is a MarkerRange selected, then the feature outlines.
+    for(int i = 0 ; i < getVisibleFeatures().size() ; ++i) 
+      drawFeature(g, getVisibleFeatures().elementAt(i),
+                  true, selected_features, selected_segments);
+
+    drawBaseSelection(g);
+    drawScale(g);
+    drawCodons(g);
+    drawBases(g);
+
+    // now draw the feature outlines
+    for(int i = 0 ; i < getVisibleFeatures().size() ; ++i) 
+      drawFeature(g, getVisibleFeatures().elementAt(i),
+                  false, selected_features, selected_segments);
+  }
+
+  /**
+   *  Draw the background colour of the frames.
+   **/
+  private void fillBackground(Graphics g) 
+  {
+    g.setColor(Color.white);
+
+    g.fillRect(0, 0, getCanvas().getSize().width,
+                     getCanvas().getSize().height);
+
+    if(getOneLinePerEntryFlag()) 
+    {
+      for(int i = 0 ; i < getEntryGroup().size() ; ++i) 
+      {
+        final int forward_entry_line = getDisplayLineOfEntryIndex(i, true);
+        final int reverse_entry_line = getDisplayLineOfEntryIndex(i, false);
+
+        final Entry current_entry = getEntryGroup().elementAt(i);
+
+        if(getEntryGroup().getDefaultEntry() == current_entry &&
+            Options.getOptions().highlightActiveEntryFlag())
+        {
+          fillLane(g, forward_entry_line, active_entry_colour);
+          fillLane(g, reverse_entry_line, active_entry_colour);
+        } 
+        else
+        {
+          fillLane(g, forward_entry_line, light_grey);
+          fillLane(g, reverse_entry_line, light_grey);
+        }
+      }
+    } 
+    else
+    {
+      if(show_forward_lines) 
+      {
+        fillLane(g, getFrameDisplayLine(FORWARD_FRAME_1), light_grey);
+        fillLane(g, getFrameDisplayLine(FORWARD_FRAME_2), light_grey);
+        fillLane(g, getFrameDisplayLine(FORWARD_FRAME_3), light_grey);
+      }
+
+      if(show_reverse_lines) 
+      {
+        fillLane(g, getFrameDisplayLine(REVERSE_FRAME_1), light_grey);
+        fillLane(g, getFrameDisplayLine(REVERSE_FRAME_2), light_grey);
+        fillLane(g, getFrameDisplayLine(REVERSE_FRAME_3), light_grey);
+      }
+    }
+
+    fillLane(g, getFrameDisplayLine(FORWARD_STRAND), not_so_light_grey);
+    fillLane(g, getFrameDisplayLine(REVERSE_STRAND), not_so_light_grey);
+  }
+
+  /**
+   *  Fill one lane/frame line with the given colour.
+   **/
+  private void fillLane(Graphics g, int fill_line_number, Color colour) 
+  {
+    final int fill_line_top = fill_line_number*getLineHeight() + 1;
+
+    g.setColor(colour);
+
+    final int first_visible_base_coord =
+      getLowXPositionOfBase(getFirstVisibleForwardBase());
+
+    final int last_visible_base_coord =
+      getHighXPositionOfBase(getLastVisibleForwardBase());
+
+    g.fillRect(first_visible_base_coord, fill_line_top,
+                last_visible_base_coord - first_visible_base_coord + 1,
+                getFeatureHeight());
+  }
+
+
+  /**
+   *  Draw the line showing the base numbers in the middle of the canvas. 
+   *  The smallest numbers will be to the left.
+   **/
+  private void drawScale(Graphics g) 
+  {
+    g.setColor(Color.black);
+
+    final int scale_line = getFrameDisplayLine(SCALE_LINE);
+    final int scale_number_y_pos = scale_line * getLineHeight();
+
+    final float bases_per_pixel =
+     (float)getMaxVisibleBases() / getCanvasWidth();
+
+    final int base_label_spacing;
+
+    if(getScaleFactor() == 0) 
+    {
+      // set the spacing so that the labels are at multiples of ten at the
+      // lowest scale factor
+      base_label_spacing =
+          (int) Math.ceil(MINIMUM_LABEL_SPACING * bases_per_pixel / 10) * 10;
+    } 
+    else 
+    {
+      // set the spacing so that the labels are at multiples of 100 otherwise
+      base_label_spacing =
+          (int) Math.ceil(MINIMUM_LABEL_SPACING * bases_per_pixel / 100) * 100;
+    }
+
+    final int label_spacing =(int)(base_label_spacing / bases_per_pixel);
+
+    final int possible_index_of_first_label;
+
+    if(isRevCompDisplay()) 
+      possible_index_of_first_label = (getSequenceLength() -
+                         getLastVisibleForwardBase() + 1) / base_label_spacing;
+    else 
+      possible_index_of_first_label =
+                         getFirstVisibleForwardBase() / base_label_spacing;
+
+    final int index_of_first_label;
+
+    if(possible_index_of_first_label <= 0) 
+      index_of_first_label = 1;
+    else 
+      index_of_first_label = possible_index_of_first_label;
+
+    final int index_of_last_label;
+
+    if(isRevCompDisplay()) 
+      index_of_last_label = (getSequenceLength() -
+                   getFirstVisibleForwardBase() + 1) / base_label_spacing;
+    else 
+      index_of_last_label =
+                   getLastVisibleForwardBase() / base_label_spacing;
+
+    for(int i = index_of_first_label; i <= index_of_last_label; ++i)
+    {
+      final String label_string=
+          String.valueOf((int)(i * base_label_spacing));
+      final int scale_number_x_pos;
+
+      if(isRevCompDisplay()) 
+        scale_number_x_pos =
+          getLowXPositionOfBase(getSequenceLength() -
+                                 i * base_label_spacing + 1);
+      else 
+        scale_number_x_pos = 
+          getLowXPositionOfBase(i * base_label_spacing);
+
+      g.setFont(getFont());
+
+      g.drawString(label_string,
+                    scale_number_x_pos + 2,
+                    scale_number_y_pos + getFontAscent() + 1);
+
+      g.drawLine(scale_number_x_pos, scale_number_y_pos,
+                  scale_number_x_pos, scale_number_y_pos + getLineHeight());
+
+      if(isRevCompDisplay())
+        g.drawLine(scale_number_x_pos, scale_number_y_pos,
+                    scale_number_x_pos + getFontWidth(), scale_number_y_pos);
+      else
+        g.drawLine(scale_number_x_pos,
+                    scale_number_y_pos + getLineHeight(),
+                    scale_number_x_pos + getFontWidth(),
+                    scale_number_y_pos + getLineHeight());
+    }
+  }
+
+  /**
+   *  Draw the bases of the forward and reverse strands into a Graphics
+   *  object.
+   **/
+  private void drawBases(Graphics g) 
+  {
+    if(getScaleFactor() > 1 ||
+        getScaleFactor() == 1 && !show_base_colours) 
+      return;
+
+    final Strand forward_strand;
+    final Strand reverse_strand;
+
+    if(isRevCompDisplay()) 
+    {
+      reverse_strand = getBases().getForwardStrand();
+      forward_strand = getBases().getReverseStrand();
+    } 
+    else 
+    {
+      forward_strand = getBases().getForwardStrand();
+      reverse_strand = getBases().getReverseStrand();
+    }
+
+    final Range forward_range =
+      newRange(getFirstVisibleForwardBase(),
+                getLastVisibleForwardBase());
+
+    String forward_visible_bases =
+      forward_strand.getSubSequence(forward_range).toUpperCase();
+
+    final int forward_frame_line = getFrameDisplayLine(FORWARD_STRAND);
+
+    final int forward_sequence_length = forward_visible_bases.length();
+
+    final int offset;
+
+    if(getForwardBaseAtLeftEdge() < 1) 
+      offset = 1 - getForwardBaseAtLeftEdge();
+    else 
+      offset = 0;
+    
+
+    for(int base_index = 0; base_index < forward_sequence_length;
+        ++base_index)
+    {
+      final char char_to_draw =
+        forward_visible_bases.charAt(base_index);
+
+      if(getScaleFactor() == 0) 
+        drawOneBaseLetter(g,
+                           char_to_draw,
+                          (offset + base_index) * getFontWidth(),
+                           forward_frame_line * getLineHeight());
+      else 
+        drawOnePixelBase(g,
+                          char_to_draw,
+                          offset + base_index,
+                          forward_frame_line * getLineHeight());
+    }
+
+    final Range reverse_range = newRange(getFirstVisibleReverseBase(),
+                                          getLastVisibleReverseBase());
+
+    String reverse_visible_bases =
+      reverse_strand.getSubSequence(reverse_range).toUpperCase();
+
+    final int reverse_frame_line = getFrameDisplayLine(REVERSE_STRAND);
+
+    final int reverse_sequence_length = reverse_visible_bases.length();
+
+    for(int base_index = 0; base_index < reverse_sequence_length;
+        ++base_index) 
+    {
+      final char char_to_draw =
+        reverse_visible_bases.charAt(reverse_sequence_length -
+                                      base_index - 1);
+
+      if(getScaleFactor() == 0) 
+        drawOneBaseLetter(g,
+                           char_to_draw,
+                          (offset + base_index) * getFontWidth(),
+                           reverse_frame_line * getLineHeight());
+      else 
+        drawOnePixelBase(g,
+                          char_to_draw,
+                          offset + base_index,
+                          reverse_frame_line * getLineHeight());
+    }
+  }
+
+
+  /**
+   *  Draw the codons of the sequence into a graphics object.  If the scale is
+   *  0 then the codon letters will be drawn, otherwise just the stop codons
+   *  will marked.
+   *  @param g The object to draw into.
+   **/
+  private void drawCodons(Graphics g) 
+  {
+    g.setColor(Color.black);
+
+    if(getScaleFactor() == 0)  
+    {
+      if(!getOneLinePerEntryFlag()) 
+      {
+        if(show_forward_lines) 
+          drawForwardCodonLetters(g);
+        
+        if(show_reverse_lines) 
+          drawReverseCodonLetters(g);
+      }
+    }
+    else 
+    {
+      final int MAX_STOP_CODON_SCALE_FACTOR = 7;
+      if((show_stop_codons || show_start_codons) &&
+          getScaleFactor() <= MAX_STOP_CODON_SCALE_FACTOR) 
+      {
+        if(!getOneLinePerEntryFlag()) 
+        {
+          if(show_forward_lines) 
+            drawForwardCodons(g);
+
+          if(show_reverse_lines) 
+            drawReverseCodons(g);
+        }
+      }
+    }
+  }
+
+  /**
+   *  Mark the start and stop codons into the three forward frame lines.
+   *  @param g The object to draw into.
+   **/
+  private void drawForwardCodons(Graphics g) 
+  {
+    final Strand strand;
+
+    if(isRevCompDisplay()) 
+      strand = getBases().getReverseStrand();
+    else 
+      strand = getBases().getForwardStrand();
+
+    final int first_visible_base = getForwardBaseAtLeftEdge();
+
+    final int frame_shift =(first_visible_base - 1) % 3;
+
+    // base to start translation at - we start slightly off the left of the
+    // screen
+    final int start_base = first_visible_base - frame_shift;
+
+    // base to end translation at
+    // we + 3 to the upper bound because partial codons do not get translated
+    // by getTranslation()
+    final int end_base = getLastVisibleForwardBase() + 3;
+
+    // not used if show_stop_codons is false
+    int [][] forward_stop_codons = null;
+
+    if(show_stop_codons) 
+    {
+      forward_stop_codons = new int [][] {
+        strand.getStopCodons(newRange(start_base, end_base)),
+        strand.getStopCodons(newRange(start_base + 1, end_base)),
+        strand.getStopCodons(newRange(start_base + 2, end_base))
+      };
+    }
+
+    // not used if show_start_codons is false
+    int [][] forward_start_codons = null;
+
+    if(show_start_codons) 
+    {
+      final StringVector start_codons;
+
+      if(Options.getOptions().isEukaryoticMode()) 
+        start_codons = Options.getOptions().getEukaryoticStartCodons();
+      else 
+        start_codons = Options.getOptions().getProkaryoticStartCodons();
+
+      forward_start_codons = new int [][] {
+        strand.getMatchingCodons(newRange(start_base, end_base),
+                                  start_codons),
+        strand.getMatchingCodons(newRange(start_base + 1, end_base),
+                                  start_codons),
+        strand.getMatchingCodons(newRange(start_base + 2, end_base),
+                                  start_codons)
+      };
+    }
+
+    for(int i = 0 ; i < 3 ; ++i) 
+    {
+      final int frame_line = getFrameDisplayLine(FORWARD_FRAME_1 + i);
+
+      final int frame_x_start = i;
+
+      if(show_start_codons) 
+      {
+        final int [] this_frame_start_codons = forward_start_codons [i];
+
+        drawCodonMarkLine(g,
+                           frame_line,
+                           this_frame_start_codons,
+                           FORWARD,
+                           80);
+      }
+
+      if(show_stop_codons) 
+      {
+        final int [] this_frame_stop_codons = forward_stop_codons [i];
+
+        drawCodonMarkLine(g,
+                           frame_line,
+                           this_frame_stop_codons,
+                           FORWARD,
+                           100);
+      }
+    }
+  }
+
+  /**
+   *  Mark the stop codons into the three reverse frame lines.
+   *  @param g The object to draw into.
+   **/
+  private void drawReverseCodons(Graphics g) 
+  {
+    final Strand strand;
+
+    if(isRevCompDisplay()) 
+      strand = getBases().getForwardStrand();
+    else 
+      strand = getBases().getReverseStrand();
+
+    final int first_visible_base = getFirstVisibleReverseBase();
+
+    final int frame_shift = (first_visible_base - 1) % 3;
+
+    // base to start translation at
+    final int start_base = first_visible_base - frame_shift;
+
+    // base to end translation at
+    // we + 3 to the upper bound because partial codons do not get translated
+    // by getTranslation()
+    final int end_base = getLastVisibleReverseBase() + 3;
+
+    // not used if show_stop_codons is false
+    int [][] reverse_stop_codons = null;
+
+    if(show_stop_codons) 
+    {
+      reverse_stop_codons = new int [][] {
+        strand.getStopCodons(newRange(start_base, end_base)),
+        strand.getStopCodons(newRange(start_base + 1, end_base)),
+        strand.getStopCodons(newRange(start_base + 2, end_base))
+      };
+    }
+
+    // not used if show_start_codons is false
+    int [][] reverse_start_codons = null;
+
+    if(show_start_codons) 
+    {
+      final StringVector start_codons;
+
+      if(Options.getOptions().isEukaryoticMode()) 
+        start_codons = Options.getOptions().getEukaryoticStartCodons();
+      else 
+        start_codons = Options.getOptions().getProkaryoticStartCodons();
+
+      reverse_start_codons = new int [][] {
+        strand.getMatchingCodons(newRange(start_base, end_base),
+                                  start_codons),
+        strand.getMatchingCodons(newRange(start_base + 1, end_base),
+                                  start_codons),
+        strand.getMatchingCodons(newRange(start_base + 2, end_base),
+                                  start_codons)
+      };
+    }
+
+    for(int i = 0 ; i < 3 ; ++i) 
+    {
+      final int frame_line = getFrameDisplayLine(REVERSE_FRAME_1 - i);
+      final int frame_x_start = i;
+
+      if(show_start_codons) 
+      {
+        final int [] this_frame_start_codons = reverse_start_codons [i];
+        drawCodonMarkLine(g,
+                          frame_line,
+                          this_frame_start_codons,
+                          REVERSE,
+                          80);
+      }
+
+      if(show_stop_codons) 
+      {
+        final int [] this_frame_stop_codons = reverse_stop_codons [i];
+        drawCodonMarkLine(g,
+                          frame_line,
+                          this_frame_stop_codons,
+                          REVERSE,
+                          100);
+      }
+    }
+  }
+
+  /**
+   *  Draw the codon letters into the three forward frame lines.
+   *  @param g The object to draw into.
+   **/
+  private void drawForwardCodonLetters(Graphics g) 
+  {
+    final Strand strand;
+
+    if(isRevCompDisplay()) 
+      strand = getBases().getReverseStrand();
+    else 
+      strand = getBases().getForwardStrand();
+
+    final int first_visible_base = getFirstVisibleForwardBase();
+
+    for(int i = 0 ; i < 3 ; ++i) 
+    {
+      final int frame_shift = 1 -(first_visible_base + 3 - i) % 3;
+
+      // base to start translation at - we start slightly off the left of the
+      // screen so that the first partial codon is translated as '.'
+      final int start_base = first_visible_base + frame_shift;
+
+      // base to end translation at - we end slightly off the right of the
+      // screen
+      final int end_base = getLastVisibleForwardBase() + 1;
+
+      final int frame_line = getFrameDisplayLine(FORWARD_FRAME_1 + i);
+
+      int frame_x_start = frame_shift;
+
+      final AminoAcidSequence this_frame_translation =
+        strand.getTranslation(newRange(start_base, end_base),
+                               false);
+
+      final String this_frame_translation_string =
+        this_frame_translation.toString();
+
+      drawCodonLine(g,
+                     frame_x_start,
+                     frame_line,
+                     this_frame_translation_string,
+                     FORWARD);
+    }
+  }
+
+  /**
+   *  Draw the codon letters into the three reverse frame lines.
+   *  @param g The object to draw into.
+   **/
+  private void drawReverseCodonLetters(Graphics g) 
+  {
+    final Strand strand;
+
+    if(isRevCompDisplay()) 
+      strand = getBases().getForwardStrand();
+    else 
+      strand = getBases().getReverseStrand();
+
+    final int first_visible_base = getFirstVisibleReverseBase();
+
+    for(int i = 0 ; i < 3 ; ++i) 
+    {
+      final int frame_shift = 1 -(first_visible_base + 3 - i) % 3;
+
+      // base to start translation at - we start slightly off the right of the
+      // screen so that the first partial codon is translated as '.'
+      final int start_base = first_visible_base + frame_shift;
+
+      // base to end translation at - we end slightly off the left of the
+      // screen
+      final int end_base = getLastVisibleReverseBase() + 1;
+
+      final int frame_line = getFrameDisplayLine(REVERSE_FRAME_1 - i);
+
+      int frame_x_start = frame_shift;
+
+      final AminoAcidSequence this_frame_translation =
+        strand.getTranslation(newRange(start_base, end_base),
+                               false);
+
+      final String this_frame_translation_string =
+        this_frame_translation.toString();
+
+      drawCodonLine(g,
+                     frame_x_start,
+                     frame_line,
+                     this_frame_translation_string,
+                     REVERSE);
+    }
+  }
+
+
+  /**
+   *  Draw one line of codons.
+   *  @param g The object to draw into.
+   *  @param frame_start When drawing in the FORWARD direction this is the
+   *    offset from the left of the screen at which to start drawing the line.
+   *    For REVERSE it is the offset from the last visible base.
+   *  @param line_number The frame line to draw into. (see
+   *    getFrameDisplayLine() for more details about these line numbers).
+   *  @param codons A String containing the codon letters to draw.
+   *  @param direction The direction to draw the letters in(see the
+   *    frame_start parameter).
+   **/
+  private void drawCodonLine(Graphics g,
+                              int frame_start,
+                              int line_number,
+                              String codons,
+                              int direction) 
+  {
+    final int offset;
+
+    if(getForwardBaseAtLeftEdge() < 1 && direction != REVERSE) 
+      offset = 1 - getForwardBaseAtLeftEdge();
+    else 
+      offset = 0;
+
+    final String upper_case_codons = codons.toUpperCase();
+    final int draw_y_position = line_number * getLineHeight();
+
+    // draw each AA letter
+    for(int i = 0 ; i < upper_case_codons.length() ; ++i) 
+    {
+      final char aa_char = upper_case_codons.charAt(i);
+
+      int draw_x_position =
+       (int)((offset + frame_start + 3 * i + 1) * getScaleValue());
+
+      if(direction == REVERSE) 
+      {
+        // draw in the opposite direction
+        draw_x_position =
+          getLowXPositionOfBase(getLastVisibleForwardBase()) -
+          draw_x_position;
+      }
+
+      drawOneLetter(g, aa_char, draw_x_position, draw_y_position);
+    }
+  }
+
+  /**
+   *  Draw one line of codons marks(stop codons or start codons).
+   *  @param g The object to draw into.
+   *  @param line_number The frame line to draw into. (see
+   *    getFrameDisplayLine() for more details about these line numbers).
+   *  @param codon_positions A vector containing the base positions of the
+   *    codons to mark.
+   *  @param direction The direction to draw the letters in(see the
+   *    frame_start parameter).
+   *  @param height_percent The height of the mark as a percentage of the
+   *    frame line height.
+   **/
+  private void drawCodonMarkLine(final Graphics g,
+                                 final int line_number,
+                                 final int [] codon_positions,
+                                 final int direction,
+                                 final int height_percent) 
+  {
+
+    final int first_visible_forward_base = getForwardBaseAtLeftEdge();
+    final int first_visible_reverse_base = getFirstVisibleReverseBase();
+    final int last_visible_forward_base = getLastVisibleForwardBase();
+    final double scale_value = getScaleValue();
+
+    final int line_height = getLineHeight();
+
+    final Integer colour_number =
+      Options.getOptions().getIntegerProperty("colour_of_start_codon");
+
+    final Color start_codon_colour;
+
+    if(colour_number != null) 
+    {
+      final int int_colour_number = colour_number.intValue();
+      start_codon_colour =
+        Options.getOptions().getColorFromColourNumber(int_colour_number);
+    } 
+    else 
+      start_codon_colour = Color.blue;
+
+    final int draw_y_position = line_number * line_height +
+        (int)(1.0 * line_height *(100 - height_percent) / 100 / 2 + 0.5);
+
+    final int mark_height = height_percent * line_height / 100;
+
+    // used to remember the position of the previously drawn codon
+    int last_x_position = -1;
+
+    for(int i = 0 ; i < codon_positions.length ; ++i) 
+    {
+      final int codon_base_start = codon_positions[i];
+
+      // zero is the end of data marker
+      if(codon_base_start == 0) 
+        break;
+
+      int draw_x_position;
+
+      if(direction == FORWARD) 
+        draw_x_position = getLowXPositionOfBase(codon_base_start);
+      else
+      { 
+        final int raw_base_position =
+          getBases().getComplementPosition(codon_base_start);
+        draw_x_position = getLowXPositionOfBase(raw_base_position);
+      }
+
+      // draw only if we haven't drawn on the position already
+      if(last_x_position == -1 || draw_x_position != last_x_position) 
+      {
+        if(height_percent < 100) 
+          drawOneCodonMark(g, draw_x_position, draw_y_position,
+                            direction, mark_height, start_codon_colour);
+        else 
+          drawOneCodonMark(g, draw_x_position, draw_y_position,
+                            direction, mark_height, Color.black);
+
+        last_x_position = draw_x_position;
+      }
+    }
+  }
+
+  /**
+   *  Return the colour in which the given base should be drawn: blue for C,
+   *  red for T, green for A and black for G.
+   **/
+  private static Color getColourOfBase(final char base_char) 
+  {
+    switch(base_char) 
+    {
+      case 'c': case 'C':
+        return Color.blue;
+      case 't': case 'T':
+        return Color.red;
+      case 'a': case 'A':
+        return dark_green;
+      case 'g': case 'G':
+        return Color.black;
+      default:
+        return Color.white;
+    }
+  }
+
+
+  /**
+   *  Draw a letter at a specified position in a Graphics object.
+   *  @param g The object to draw into.
+   *  @param letter The character to draw.
+   *  @param x_pos The x position at which to draw the letter.
+   *  @param y_pos The y position at which to draw the letter.
+   **/
+  private void drawOneLetter(final Graphics g, final char letter,
+                              final int x_pos, final int y_pos) 
+  {
+    draw_one_char_temp_array[0] = letter;
+
+    g.setFont(getFont());
+    g.drawChars(draw_one_char_temp_array, 0, 1, x_pos,
+                y_pos + getFontAscent() + 1);
+  }
+
+  /**
+   *  Draw a letter at a specified position in a Graphics object.
+   *  @param g The object to draw into.
+   *  @param base_char The character of the base to draw(A, T, G or C).
+   *  @param x_pos The x position at which to draw the letter.
+   *  @param y_pos The y position at which to draw the letter.
+   **/
+  private void drawOneBaseLetter(final Graphics g, final char base_char,
+                                  final int x_pos, final int y_pos) 
+  {
+    if(show_base_colours) 
+      g.setColor(getColourOfBase(base_char));
+
+    drawOneLetter(g, base_char, x_pos, y_pos);
+  }
+
+
+  /**
+   *  Draw a codon mark at the given position.  The codon is represented
+   *  by a one pixel wide line for scale factor greater than 1 and 3 pixels
+   *  wide if the scale factor is one.
+   *  @param g The object to draw into.
+   *  @param x_pos The x position at which to draw the codon.
+   *  @param y_pos The y position at which to draw the codon.  The line
+   *    is drawn down from the given x,y position.
+   *  @param direction If this is FORWARD the line is draw from the y position
+   *    to the right, otherwise it is drawn to the left.
+   *  @param height The height in pixels of the codon mark.
+   *  @param colour The colour to use when drawing the mark.
+   **/
+  private void drawOneCodonMark(final Graphics g,
+                                 final int x_pos, final int y_pos,
+                                 final int direction,
+                                 final int height,
+                                 final Color colour) 
+  {
+    g.setColor(colour);
+
+    if(getScaleFactor() == 1) 
+    {
+      // draw a line three pixels/bases wide
+      if(direction == FORWARD) 
+      {
+        g.drawLine(x_pos + 1, y_pos,
+                    x_pos + 1, y_pos + height - 1);
+        g.drawLine(x_pos + 2, y_pos,
+                    x_pos + 2, y_pos + height - 1);
+      } 
+      else
+      {
+        g.drawLine(x_pos - 1, y_pos,
+                    x_pos - 1, y_pos + height - 1);
+        g.drawLine(x_pos - 2, y_pos,
+                    x_pos - 2, y_pos + height - 1);
+      }
+    }
+    g.drawLine(x_pos, y_pos, x_pos, y_pos + height - 1);
+  }
+
+  /**
+   *  Draw a one pixel wide line representing a single base on the forward or
+   *  reverse strand.  The base is coloured according to which base is passed
+   *  to the method: blue for C, red for T, green for A and black for G.
+   *  @param g The object to draw into.
+   *  @param aa_char The character to draw.
+   *  @param x_pos The x position at which to draw the line representing the
+   *    base.
+   *  @param y_pos The y position at which to draw the top of the line
+   *    representing the base.
+   **/
+  private void drawOnePixelBase(Graphics g, char base_char,
+                                 int x_pos, int y_pos) 
+  {
+    g.setColor(getColourOfBase(base_char));
+    g.drawLine(x_pos, y_pos, x_pos, y_pos + getLineHeight() - 1);
+  }
+
+  /**
+   *  Return the line on the canvas where this feature segment should be drawn.
+   *  @param segment The segment in question.
+   *  @return The line to draw into.
+   **/
+  private int getSegmentDisplayLine(FeatureSegment segment)
+  {
+    if(getOneLinePerEntryFlag()) 
+    {
+      final Feature feature = segment.getFeature();
+      if((feature.isProteinFeature() || frame_features_flag) &&
+         (show_forward_lines &&(feature.isForwardFeature() ^
+                                  isRevCompDisplay()) ||
+          show_reverse_lines &&(!feature.isForwardFeature() ^
+                                  isRevCompDisplay()))) 
+      {
+        return getFeatureDisplayLine(feature);
+      } 
+      else
+      {
+        if(feature.isForwardFeature() ^ isRevCompDisplay()) 
+          return getFrameDisplayLine(FORWARD_STRAND);
+        else 
+          return getFrameDisplayLine(REVERSE_STRAND);
+      }
+    } 
+    else
+    {
+      final int frame_id =
+        maybeFlipFrameDirection(getSegmentFrameID(segment));
+
+      return getFrameDisplayLine(frame_id);
+    }
+  }
+
+  /**
+   *  Return the frame ID of to use when drawing the given segment.
+   **/
+  private int getSegmentFrameID(final FeatureSegment segment) 
+  {
+    final int frame_id = segment.getFrameID();
+
+    final Feature feature = segment.getFeature();
+
+    if((feature.isProteinFeature() || frame_features_flag) &&
+       (show_forward_lines &&(feature.isForwardFeature() ^
+                                isRevCompDisplay())||
+         show_reverse_lines &&(! feature.isForwardFeature() ^
+                                isRevCompDisplay()))) 
+    {
+      return frame_id;
+    }
+    else 
+    {
+      switch(frame_id) 
+      {
+        case FORWARD_FRAME_1:
+          // fall through
+        case FORWARD_FRAME_2:
+          // fall through
+        case FORWARD_FRAME_3:
+          // fall through
+        case FORWARD_STRAND:
+          return FORWARD_STRAND;
+        case REVERSE_FRAME_1:
+          // fall through
+        case REVERSE_FRAME_2:
+          // fall through
+        case REVERSE_FRAME_3:
+          // fall through
+        case REVERSE_STRAND:
+          return REVERSE_STRAND;
+        default:
+          return frame_id;
+      }
+    }
+  }
+
+  /**
+   *  Return the line on the canvas where the given feature should be drawn
+   *  when one_line_per_entry is true.  Call getLineCount() to find out the
+   *  total number of lines.  Each line is getLineHeight() pixels high.
+   *  @return The line to draw into.
+   **/
+  private int getFeatureDisplayLine(final Feature feature) 
+  {
+    final int entry_index = getEntryGroup().indexOf(feature.getEntry());
+    return getDisplayLineOfEntryIndex(entry_index,
+                                      feature.isForwardFeature() ^
+                                      isRevCompDisplay());
+  }
+
+  /**
+   *  Return the line in the display where features from the entry specified
+   *  by the given entry_index should be drawn.
+   *  @param is_forward_feature indicates whether the feature of interest is
+   *    on the forward or reverse strand.
+   **/
+  private int getDisplayLineOfEntryIndex(final int entry_index,
+                                         final boolean is_forward_feature) 
+  {
+    if(is_forward_feature) 
+    {
+      if(getShowForwardFrameLines()) 
+      {
+        if(getShowLabels()) 
+          return entry_index * 2;
+        else 
+          return entry_index;
+      }
+      else
+        return 0;
+    } 
+    else
+    {
+      int return_value = 0;
+      if(getShowLabels()) 
+      {
+        return_value = getEntryGroup().size() * 2 + 3 - entry_index * 2;
+
+        if(getShowForwardFrameLines()) 
+          return_value += getEntryGroup().size() * 2;
+      }
+      else 
+      {
+        return_value = getEntryGroup().size() + 2 - entry_index;
+
+        if(getShowForwardFrameLines()) 
+          return_value += getEntryGroup().size();
+      }
+      return return_value;
+    }
+  }
+
+  /**
+   *  If rev_comp_display is true return the corresponding frame_id from the
+   *  opposite strand(eg. FORWARD_FRAME_1 gives REVERSE_FRAME_3) otherwise
+   *  return the frame_id unchanged.
+   **/
+  private int maybeFlipFrameDirection(final int frame_id) 
+  {
+    if(isRevCompDisplay()) 
+    {
+      // flip the frame so that forward becomes reverse and reverse becomes
+      // forward
+      switch(frame_id) 
+      {
+        case FORWARD_FRAME_1:
+          return REVERSE_FRAME_1;
+        case FORWARD_FRAME_2:
+          return REVERSE_FRAME_2;
+        case FORWARD_FRAME_3:
+          return REVERSE_FRAME_3;
+        case FORWARD_STRAND:
+          return REVERSE_STRAND;
+        case REVERSE_FRAME_1:
+          return FORWARD_FRAME_1;
+        case REVERSE_FRAME_2:
+          return FORWARD_FRAME_2;
+        case REVERSE_FRAME_3:
+          return FORWARD_FRAME_3;
+        case REVERSE_STRAND:
+          return FORWARD_STRAND;
+        default:
+          return frame_id;
+      }
+    }
+    return frame_id;
+  }
+
+  /**
+   *  Return the line on the canvas where this frame ID should be drawn.  Call
+   *  getLineCount() to find out the total number of lines.  Each line is
+   *  getLineHeight() pixels high.
+   *  @param frame_id The frame ID.
+   *  @return The line to draw into.
+   **/
+  private int getFrameDisplayLine(int frame_id) 
+  {
+    if(getOneLinePerEntryFlag()) 
+    {
+      int return_value;
+      switch(frame_id) 
+      {
+        case FORWARD_STRAND:
+          return_value = 0;
+          break;
+        case REVERSE_STRAND:
+          if(show_labels) 
+            return_value = 3;
+          else 
+            return_value = 2;
+          break;
+        case SCALE_LINE:
+          if(show_labels) 
+            return_value = 2;
+          else 
+            return_value = 1;
+          break;
+        default:
+          throw new Error("internal error - unexpected value: " + frame_id);
+      }
+
+      if(show_forward_lines) 
+      {
+        if(show_labels)
+          return_value += getEntryGroup().size();
+
+        return return_value + getEntryGroup().size();
+      } 
+      else 
+        return return_value;
+    }
+
+    final int line_number;
+
+    switch(frame_id) 
+    {
+      case FORWARD_FRAME_1:
+        line_number = 0;
+        break;
+      case FORWARD_FRAME_2:
+        if(show_labels) 
+          line_number = 2;
+        else 
+          line_number = 1;
+        break;
+      case FORWARD_FRAME_3:
+        if(show_labels) 
+          line_number = 4;
+        else 
+          line_number = 2;
+        break;
+      case FORWARD_STRAND:
+        if(show_forward_lines)
+        {
+          if(show_labels)
+            line_number = 6;
+          else
+            line_number = 3;
+        }
+        else
+          line_number = 0;
+        break;
+      case REVERSE_STRAND:
+        if(show_forward_lines) 
+        {
+          if(show_labels) 
+            line_number = 9;
+          else
+            line_number = 5;
+        }
+        else
+        {
+          if(show_labels)
+            line_number = 3;
+          else 
+            line_number = 2;
+        }
+        break;
+      case REVERSE_FRAME_3:
+        if(show_forward_lines) 
+        {
+          if(show_labels) 
+            line_number = 11;
+          else 
+            line_number = 6;
+        } 
+        else
+        {
+          if(show_labels) 
+            line_number = 5;
+          else 
+            line_number = 3;
+        }
+        break;
+      case REVERSE_FRAME_2:
+        if(show_forward_lines) 
+        {
+          if(show_labels) 
+            line_number = 13;
+          else 
+            line_number = 7;
+        }
+        else
+        {
+          if(show_labels) 
+            line_number = 7;
+          else 
+            line_number = 4;
+        }
+        break;
+      case REVERSE_FRAME_1:
+        if(show_forward_lines) 
+        {
+          if(show_labels) 
+            line_number = 15;
+          else 
+            line_number = 8;
+        }
+        else
+        {
+          if(show_labels) 
+            line_number = 9;
+          else 
+            line_number = 5;
+        }
+        break;
+      default:
+        if(show_forward_lines) 
+        {
+          if(show_labels)
+            line_number = 8;
+          else
+            line_number = 4;
+        }
+        else
+        {
+          if(show_labels) 
+            line_number = 2;
+          else 
+            line_number = 1;
+        }
+        break;
+    }
+
+    return line_number;
+  }
+
+  /**
+   *  Return the number of lines of text we need to fit on the canvas.  This
+   *  is used to set the height of the canvas.
+   **/
+  private int getLineCount() 
+  {
+    int line_count;
+
+    if(show_labels) 
+      line_count = 5;
+    else 
+      line_count = 3;
+
+    int extra_line_count;
+
+    if(getOneLinePerEntryFlag())
+    {
+      // some number of entry lines
+      extra_line_count = getEntryGroup().size();
+    } 
+    else
+    {
+      // three frame line
+      extra_line_count = 3;
+    }
+
+    if(show_labels) 
+      extra_line_count *= 2;
+
+    if(show_forward_lines) 
+      line_count += extra_line_count;
+
+    if(show_reverse_lines) 
+      line_count += extra_line_count;
+
+    return line_count;
+  }
+
+  /**
+   *  Return the vertical offset from the top of the canvas for this feature.
+   *  The returned value will be the y-coordinate of the top of the lane that
+   *  this feature should be draw into.
+   **/
+  private int getSegmentVerticalOffset(FeatureSegment segment) 
+  {
+    // one "line" is font_height pixels high,(the number of lines times the
+    // font_height is the height of the height of canvas)
+    final int line = getSegmentDisplayLine(segment);
+
+    return getLineOffset(line);
+  }
+
+  /**
+   *  Given a line return the vertical offset(in pixels) from the top of the
+   *  canvas.
+   **/
+  private int getLineOffset(int line) 
+  {
+    return line * getLineHeight();
+  }
+
+  /**
+   *  Return the lowest on screen(with respect to the canvas) x coordinate of
+   *  a base.  If the scale_factor is zero then one base will be font_width
+   *  wide and this method will return a different value than
+   *  getHighXPositionOfBase().  If scale_factor is greater than one then
+   *  the two methods will return the same thing.
+   *  @param base_number The(forward) base to calculate the position of.
+   **/
+  private int getLowXPositionOfBase(int base_number) 
+  {
+    return(int)((base_number - getForwardBaseAtLeftEdge()) *
+                  getScaleValue());
+  }
+
+  /**
+   *  Return the highest on screen(ie with respect to the canvas) x
+   *  coordinate of a base.  See comment on getLowXPositionOfBase().
+   *  @param base_number The(forward) base to calculate the position of.
+   **/
+  private int getHighXPositionOfBase(int base_number) 
+  {
+    if(getScaleFactor() == 0) 
+      return getLowXPositionOfBase(base_number) + getFontWidth() - 1;
+    else
+      return getLowXPositionOfBase(base_number);
+  }
+
+  /**
+   *  Return the low on screen x coordinate of the base at the given Marker
+   *  position.  If the scale_factor is zero then one base will be font_width
+   *  wide so that this method will return a different position than
+   *  getHighXPositionOfMarker().
+   *  @param marker The Marker position to return the screen position of.
+   **/
+  private int getLowXPositionOfMarker(Marker marker) 
+  {
+    int position = marker.getRawPosition();
+
+    if(isRevCompDisplay()) 
+      position = getSequenceLength() - position + 1;
+
+    if(marker.getStrand().isForwardStrand() ^ isRevCompDisplay()) 
+      return getLowXPositionOfBase(position);
+    else 
+      return getHighXPositionOfBase(position);
+  }
+
+  /**
+   *  Return the high on screen x coordinate of the base at the given Marker
+   *  position.  If the scale_factor is zero then one base will be font_width
+   *  wide so that this method will return a different position than
+   *  getLowXPositionOfMarker().
+   *  @param marker The Marker position to return the screen position of.
+   **/
+  private int getHighXPositionOfMarker(Marker marker) 
+  {
+    int position = marker.getRawPosition();
+
+    if(isRevCompDisplay()) 
+      position = getSequenceLength() - position + 1;
+
+    if(marker.getStrand().isForwardStrand() ^ isRevCompDisplay()) 
+      return getHighXPositionOfBase(position);
+    else 
+      return getLowXPositionOfBase(position);
+  }
+
+
+  /**
+   *  This is the approximate opposite of getLowXPositionOfBase() - it returns
+   *  MarkerRange corresponding to the given screen position.  It returns a
+   *  MarkerRange rather than a Marker because if the user clicks on a FRAME
+   *  line then we want to select a whole codon not just one base.
+   *  @param position A position on the canvas.
+   *  @return A MarkerRange covering the clicked on bases.
+   **/
+  private MarkerRange getMarkerRangeFromPosition(Point position)
+  {
+    return getMarkerRangeFromPosition(position, true);
+  }
+
+  /**
+   *  Given a Point and a direction(either FORWARD_STRAND or REVERSE_STRAND),
+   *  return a the corresponding base position on that Strand.
+   **/
+  private int getBasePositionOfPoint(final Point position,
+                                      final int direction) 
+  {
+    if(direction == FORWARD_STRAND) 
+    {
+      return(int)(1.0 * position.x / getScaleValue() +
+                    getForwardBaseAtLeftEdge());
+    } 
+    else
+    {
+      if(direction == REVERSE_STRAND) 
+      {
+        final int raw_base_position =
+         (int)(1.0 * position.x / getScaleValue() +
+                 getForwardBaseAtLeftEdge());
+        return getBases().getComplementPosition(raw_base_position);
+      } 
+      else 
+        throw new Error("internal error - unexpected value: " + direction);
+    }
+  }
+
+  /**
+   *  This is the approximate opposite of getLowXPositionOfBase() - it returns
+   *  MarkerRange corresponding to the given screen position.  It returns a
+   *  MarkerRange rather than a Marker because if the user clicks on a FRAME
+   *  line then we want to select a whole codon not just one base.
+   *  @param position A position on the canvas.
+   *  @param whole_codon If true then return a range that covers a whole codon
+   *    if the click was on a frame line.
+   *  @return A MarkerRange covering the clicked on bases.
+   **/
+  private MarkerRange getMarkerRangeFromPosition(final Point position,
+                                                  final boolean whole_codon) 
+  {
+
+    final int frame_id;
+
+    int base_position;
+    final Strand strand;
+
+    if(getOneLinePerEntryFlag()) 
+    {
+      final int scale_line = getFrameDisplayLine(SCALE_LINE);
+      final int position_line = getLineFromPoint(position);
+
+      if(position_line < scale_line) 
+      {
+        if(isRevCompDisplay()) 
+          strand = getBases().getReverseStrand();
+        else 
+          strand = getBases().getForwardStrand();
+        
+        frame_id = FORWARD_STRAND;
+      } 
+      else 
+      {
+        if(position_line > scale_line)
+        {
+          if(isRevCompDisplay()) 
+            strand = getBases().getForwardStrand();
+          else 
+            strand = getBases().getReverseStrand();
+          
+          frame_id = REVERSE_STRAND;
+        } 
+        else 
+          return null;
+      }
+
+      base_position = getBasePositionOfPoint(position, frame_id);
+    } 
+    else
+    {
+      frame_id = getFrameFromPoint(position);
+      // calculate the frame/strand line and base position
+      switch(frame_id) 
+      {
+        case FORWARD_FRAME_1:
+        case FORWARD_FRAME_2:
+        case FORWARD_FRAME_3:
+        case FORWARD_STRAND:
+          if(isRevCompDisplay()) 
+            strand = getBases().getReverseStrand();
+          else 
+            strand = getBases().getForwardStrand();
+          
+          base_position = getBasePositionOfPoint(position, FORWARD_STRAND);
+          break;
+        case REVERSE_FRAME_1:
+        case REVERSE_FRAME_2:
+        case REVERSE_FRAME_3:
+        case REVERSE_STRAND:
+          if(isRevCompDisplay()) 
+            strand = getBases().getForwardStrand();
+          else 
+            strand = getBases().getReverseStrand();
+          
+          base_position = getBasePositionOfPoint(position, REVERSE_STRAND);
+          break;
+        default:
+          base_position = 0;
+          strand = null;
+      }
+    }
+
+    final int start_base_position;
+    final int end_base_position;
+
+    if(whole_codon) 
+    {
+      start_base_position =
+        adjustBasePositionForFrame(frame_id, base_position, true);
+      end_base_position =
+        adjustBasePositionForFrame(frame_id, base_position, false);
+    }
+    else
+    {
+      start_base_position = base_position;
+      end_base_position = base_position;
+    }
+
+    if(strand == null) 
+      return null;
+    else
+    {
+      try 
+      {
+        return strand.makeMarkerRangeFromPositions(start_base_position,
+                                                    end_base_position);
+      }
+      catch(OutOfRangeException e) 
+      {
+        // XXX
+        return null;
+      }
+    }
+  }
+
+
+  /**
+   *  Helper method for getMarkerRangeFromPosition().  Returns a base
+   *  position offset to the start/end of the codon if the given frame id on a
+   *  FRAME.
+   *  @param get_start If true then the base position returned is base
+   *    position of the start of the codon, otherwise the end base is
+   *    returned.
+   **/
+  private int adjustBasePositionForFrame(int frame_id,
+                                         final int base_position,
+                                         final boolean get_start) 
+  {
+    final int return_base_position;
+    final int end_offset;
+
+    if(get_start) 
+      end_offset = 0;
+    else 
+    {
+      // the end is two bases ahead of the start
+      end_offset = 2;
+    }
+
+    switch(frame_id) 
+    {
+      case FORWARD_FRAME_1:
+      case REVERSE_FRAME_1:
+        {
+          final int base_pos_mod3 =(base_position - 1) % 3;
+          return_base_position = base_position + end_offset - base_pos_mod3;
+        }
+        break;
+      case FORWARD_FRAME_2:
+      case REVERSE_FRAME_2:
+        {
+          final int base_pos_mod3 =(base_position - 2) % 3;
+          return_base_position = base_position + end_offset - base_pos_mod3;
+        }
+        break;
+      case FORWARD_FRAME_3:
+      case REVERSE_FRAME_3:
+        {
+          final int base_pos_mod3 =(base_position - 3) % 3;
+          return_base_position = base_position + end_offset - base_pos_mod3;
+        }
+        break;
+      default:
+        return_base_position = base_position;
+        // do nothing
+    }
+
+    return return_base_position;
+  }
+
+  /**
+   *  Draw one feature at the correct place in a Graphics object.
+   *  @param g The Graphics object on which to draw.
+   *  @param draw_feature_fill If true then draw just the solid block of colour
+   *    inside the segments.  If false then draw just the outline.
+   *  @param feature The feature to draw.
+   **/
+  private void drawFeature(final Graphics g,
+                           final Feature feature,
+                           final boolean draw_feature_fill,
+                           final FeatureVector selected_features,
+                           final FeatureSegmentVector selected_segments) 
+  {
+    final FeatureSegmentVector this_feature_segments = feature.getSegments();
+
+    // don't try to draw a feature with no segments
+    if(this_feature_segments.size() == 0) 
+      return;
+
+    // set to true if and only if the whole of this feature should be
+    // highlighted
+    final boolean highlight_feature_flag;
+
+    if(selected_features.contains(feature))
+    {
+      // ignore the possibility that a feature and a segment from the same
+      // feature could be in the selection vector at the same time
+      highlight_feature_flag = true;
+    }
+    else
+    {
+      // if the feature border flag is off and the feature is not selected
+      // then don't draw the border
+      if(!show_feature_borders && !draw_feature_fill) 
+        return;
+
+      highlight_feature_flag = false;
+    }
+
+    // draw each segment/exon
+    for(int i = 0 ; i < this_feature_segments.size() ; ++i) 
+    {
+      final FeatureSegment current_segment =
+                           this_feature_segments.elementAt(i);
+      final boolean highlight_segment_flag;
+
+      if(selected_segments.indexOf(current_segment) == -1) 
+        highlight_segment_flag = false;
+      else 
+        highlight_segment_flag = true;
+
+      final boolean draw_direction_arrow_flag;
+
+      // draw an arrow only on the last segment
+      if(i == this_feature_segments.size() - 1 && show_feature_arrows) 
+        draw_direction_arrow_flag = true;
+      else 
+        draw_direction_arrow_flag = false;
+
+      drawSegment(g, current_segment,
+                   highlight_feature_flag, highlight_segment_flag,
+                   draw_feature_fill,
+                   draw_direction_arrow_flag);
+
+      if(i + 1 < this_feature_segments.size()) 
+      {
+          // draw a line between the segments
+
+        final FeatureSegment next_segment =
+          this_feature_segments.elementAt(i + 1);
+
+        drawSegmentConnection(g, current_segment, next_segment);
+      }
+    }
+
+    // draw the label last if the is no label line because in this case the
+    // label is draw on top of the feature segments
+    if(draw_feature_fill) 
+      drawFeatureLabel(g, feature);
+
+    Thread.yield();
+  }
+
+  /**
+   *  Draw a bent line between two segments which represents the connection
+   *  between exons in feature.
+   *  @param g The Graphics object on which to draw.
+   *  @param lower_segment The reference of the segment that is closest to the
+   *    start of the Strand.  The connection line will start at the beginning
+   *    of this segment.
+   *  @param upper_segment The reference of the segment that is furthest from
+   *    the start of the Strand.  The connection line will finish at the end
+   *    of this segment.
+   **/
+  private void drawSegmentConnection(Graphics g,
+                                     FeatureSegment lower_segment,
+                                     FeatureSegment upper_segment) 
+  {
+    final Marker upper_segment_start_marker = upper_segment.getStart();
+    final Marker lower_segment_end_marker = lower_segment.getEnd();
+
+    int next_segment_start_coord =
+      getLowXPositionOfMarker(upper_segment_start_marker);
+
+    // make sure we don't wrap around when drawing
+    if(next_segment_start_coord > 16000) 
+      next_segment_start_coord = 16000;
+
+    // make sure we don't wrap around when drawing
+    if(next_segment_start_coord < -16000) 
+      next_segment_start_coord = -16000;
+
+    int this_segment_end_coord =
+      getHighXPositionOfMarker(lower_segment_end_marker);
+
+    // make sure we don't wrap around when drawing
+    if(this_segment_end_coord > 16000) 
+      this_segment_end_coord = 16000;
+
+    // make sure we don't wrap around when drawing
+    if(this_segment_end_coord < -16000) 
+      this_segment_end_coord = -16000;
+
+    final int this_segment_vertical_offset =
+      getSegmentVerticalOffset(lower_segment);
+    final int next_segment_vertical_offset =
+      getSegmentVerticalOffset(upper_segment);
+
+    // we draw the line with a bend in the middle - this is the vertical
+    // position of the bend
+    final int line_y_position_for_centre;
+
+    if(this_segment_vertical_offset < next_segment_vertical_offset) 
+      line_y_position_for_centre = this_segment_vertical_offset;
+    else 
+      line_y_position_for_centre = next_segment_vertical_offset;
+
+    final int line_y_position_for_this =
+      this_segment_vertical_offset + getFeatureHeight() / 2;
+    final int line_y_position_for_next =
+      next_segment_vertical_offset + getFeatureHeight() / 2;
+
+    final int horizontal_position_of_centre =
+     (this_segment_end_coord + next_segment_start_coord) / 2;
+
+    final Feature segment_feature = lower_segment.getFeature();
+    final Color feature_colour = segment_feature.getColour();
+
+    // draw in black if no colour is specified or if the feature is selected
+    if(feature_colour == null ||
+       getSelection().contains(lower_segment.getFeature())) 
+      g.setColor(Color.black);
+    else 
+      g.setColor(feature_colour);
+
+    g.drawLine(this_segment_end_coord,
+                line_y_position_for_this,
+                horizontal_position_of_centre,
+                line_y_position_for_centre);
+
+    g.drawLine(horizontal_position_of_centre,
+                line_y_position_for_centre,
+                next_segment_start_coord,
+                line_y_position_for_next);
+  }
+
+  /**
+   *  Draw a label a feature.  This is a helper function for drawFeature().
+   *  If show_labels is true the labels will be drawn below the features,
+   *  otherwise they will be drawn within the features.
+   *  @param g The Graphics object on which to draw.
+   *  @param feature The feature to draw the label for.
+   **/
+  private void drawFeatureLabel(Graphics g, Feature feature) 
+  {
+
+    // the three frame translation is visible when the scale factor is 0,
+    // don't draw labels over it
+    if(!show_labels && getScaleFactor() == 0) 
+      return;
+
+    String label_or_gene = feature.getIDString();
+    final String label = feature.getLabel();
+
+    // special case - don't display a label if the label qualifier is "*"
+    if(label != null && label.equals("*")) 
+      label_or_gene = "";
+
+    // don't waste time drawing nothing
+    if(label_or_gene.length() == 0) 
+      return;
+
+    final FontMetrics fm = g.getFontMetrics();
+    final int string_width = fm.stringWidth(label_or_gene);
+
+    final FeatureSegment first_segment = feature.getSegments().elementAt(0);
+    final int label_x_coord;
+
+    if(feature.isForwardFeature() ^ isRevCompDisplay())
+    {
+      int segment_start_pos =
+        first_segment.getStart().getRawPosition();
+
+      if(isRevCompDisplay()) 
+        segment_start_pos = getSequenceLength() - segment_start_pos + 1;
+
+      label_x_coord = getLowXPositionOfBase(segment_start_pos);
+    } 
+    else
+    {
+      int segment_end_pos =
+        first_segment.getEnd().getRawPosition();
+
+      if(isRevCompDisplay()) 
+        segment_end_pos = getSequenceLength() - segment_end_pos + 1;
+
+      // reverse the start and end on the complementary strand
+      label_x_coord = getLowXPositionOfBase(segment_end_pos);
+    }
+
+    if(label_x_coord >= getCanvas().getSize().width) 
+      return;
+
+//  if(label_x_coord + string_width <= 0) {
+      // don't draw the label if it is not visible on screen
+//  }
+
+
+    int vertical_offset =
+      getSegmentVerticalOffset(first_segment);
+
+    if(show_labels) 
+      vertical_offset += getLineHeight(); // move to the label line
+
+    // save this so we can restore it later
+    final Shape saved_clip = g.getClip();
+
+    // if we have a labels line draw a white background behind the
+    // label
+    if(show_labels) 
+    {
+      g.setColor(Color.white);
+
+      g.fillRect(label_x_coord - getFontWidth(),
+                 vertical_offset,
+                 string_width + getFontWidth() * 2,
+                 getLineHeight());
+    } 
+    else
+    {
+      // if there is no label line clip to the size of the first segment
+      // and draw in there
+      final int segment_start_coord =
+        getSegmentStartCoord(first_segment);
+      final int segment_end_coord =
+        getSegmentEndCoord(first_segment);
+
+      if(Math.abs(segment_end_coord - segment_start_coord) > 5) 
+      {
+        if(segment_end_coord > segment_start_coord)
+          g.setClip(segment_start_coord, vertical_offset,
+                     segment_end_coord - segment_start_coord,
+                     getFeatureHeight());
+        else 
+          g.setClip(segment_end_coord, vertical_offset,
+                     segment_start_coord - segment_end_coord,
+                     getFeatureHeight());
+      }
+      else 
+        return;  // don't draw small labels if there is no room
+    }
+
+    g.setColor(Color.black);
+    g.setFont(getFont());
+    g.drawString(label_or_gene, label_x_coord + 1,
+                  vertical_offset + getFontAscent() + 1);
+
+    if(!show_labels)
+      g.setClip(saved_clip);
+  }
+
+  /**
+   *  Return the position on the canvas where this segment starts.  The
+   *  returned value will be -1 if the position is off the left of the screen
+   *  and will be(width of the canvas) if the position is off the right of
+   *  the screen.
+   **/
+  private int getSegmentStartCoord(FeatureSegment segment) 
+  {
+    final Marker segment_start_marker = segment.getStart();
+    int segment_start_coord =
+      getLowXPositionOfMarker(segment_start_marker);
+
+    // make sure we don't wrap around when drawing
+    if(segment_start_coord > getCanvas().getSize().width) 
+      segment_start_coord = getCanvas().getSize().width;
+
+    // make sure we don't wrap around when drawing
+    if(segment_start_coord < 0) 
+      segment_start_coord = -1;
+
+    return segment_start_coord;
+  }
+
+  /**
+   *  Return the position on the canvas where this segment ends.  The returned
+   *  value will be -1 if the position is off the left of the screen and will
+   *  be(width of the canvas) if the position is off the right of the screen.
+   **/
+  private int getSegmentEndCoord(FeatureSegment segment) 
+  {
+    final Marker segment_end_marker = segment.getEnd();
+    int segment_end_coord = getHighXPositionOfMarker(segment_end_marker);
+
+    // make sure we don't wrap around when drawing
+    if(segment_end_coord > getCanvas().getSize().width) 
+      segment_end_coord = getCanvas().getSize().width;
+
+    // make sure we don't wrap around when drawing
+    if(segment_end_coord < 0) 
+      segment_end_coord = -1;
+
+    return segment_end_coord;
+  }
+
+  /**
+   *  Return true if the base given by the Marker is visible.
+   **/
+  private boolean baseVisible(Marker marker) 
+  {
+    final int first_visible_base = getForwardBaseAtLeftEdge();
+    final int last_visible_base = getLastVisibleForwardBase();
+
+    int marker_position = marker.getRawPosition();
+
+    if(isRevCompDisplay()) 
+      marker_position = getBases().getComplementPosition(marker_position);
+
+    if(marker_position < first_visible_base ||
+        marker_position > last_visible_base) 
+      return false;
+    else 
+      return true;
+  }
+
+  /**
+   *  Return if and only if the segment is(partly) within the range of bases
+   *  we are currently displaying.
+   *  @param segment The FeatureSegment to check.
+   **/
+  private boolean segmentVisible(FeatureSegment segment) 
+  {
+    int segment_start_coord = getSegmentStartCoord(segment);
+    int segment_end_coord = getSegmentEndCoord(segment);
+
+    if(segment_end_coord < 0 && segment_start_coord < 0 ||
+        segment_start_coord >= getCanvas().getSize().width &&
+        segment_end_coord >= getCanvas().getSize().width) 
+      return false;
+    else 
+      return true;
+  }
+
+  /**
+   *  Return if and only if some part of a feature is(partly) within the
+   *  range of bases we are currently displaying.
+   *  @param feature The Feature to check.
+   **/
+  private boolean featureVisible(Feature feature) 
+  {
+    if(getMinimumScore() > 0) 
+    {
+      final int feature_score = feature.getScore();
+
+      if(feature_score != -1 && feature_score < getMinimumScore()) 
+        return false;
+    }
+
+    final FeatureSegmentVector segments = feature.getSegments();
+
+    for(int i = 0 ; i < segments.size() ; ++i) 
+    {
+      if(segmentVisible(segments.elementAt(i))) 
+        return true;
+    }
+
+    return false;
+  }
+
+  /**
+   *  Draw one FeatureSegment into a Graphics object.
+   *  @param g The Graphics object on which to draw.
+   *  @param segment The feature segment to draw
+   *  @param highlight_feature If true draw an extra thick line
+   *  @param highlight_segment If true draw the segment with a doubly thick
+   *    line.
+   *  @param draw_feature_fill If true then just the solid block of colour
+   *    inside the segments will be drawn.  If false then only the feature
+   *    outline is drawn.
+   *  @param draw_arrow If true draw a direction arrow at the end of the
+   *    segment.
+   **/
+  private void drawSegment(Graphics g, FeatureSegment segment,
+                            boolean highlight_feature,
+                            boolean highlight_segment,
+                            boolean draw_feature_fill,
+                            boolean draw_arrow) 
+  {
+    // not on screen
+    if(!segmentVisible(segment)) 
+      return;
+
+    final Feature segment_feature = segment.getFeature();
+
+    final int vertical_offset = getSegmentVerticalOffset(segment) + 1;
+
+    int segment_start_coord = getSegmentStartCoord(segment);
+    int segment_end_coord = getSegmentEndCoord(segment);
+
+    final int segment_height = getFeatureHeight() - 1;
+
+    // this is 1 if the feature is on the forward strand or on a forward frame
+    // and -1 otherwise.  this used to draw the feature arrow in the right
+    // direction.
+    final int feature_direction;
+    if(segment.getFeature().isForwardFeature() ^ isRevCompDisplay()) 
+      feature_direction = 1;
+    else 
+      feature_direction = -1;
+
+    final int [] x_points = {
+      segment_start_coord, segment_end_coord,
+      segment_end_coord, segment_start_coord
+    };
+
+    final int [] y_points = {
+      vertical_offset, vertical_offset,
+      vertical_offset + segment_height, vertical_offset + segment_height
+    };
+
+    if(draw_feature_fill) 
+    {
+      final Color feature_colour = segment_feature.getColour();
+
+      // no colour means draw in white
+      if(feature_colour == null) 
+        g.setColor(Color.white);
+      else 
+        g.setColor(feature_colour);
+
+      if(segment_feature.isForwardFeature() ^ isRevCompDisplay()) 
+      {
+        final int segment_width = segment_end_coord - segment_start_coord + 1;
+
+        g.fillRect(segment_start_coord, vertical_offset,
+                    segment_width, segment_height + 1);
+      }
+      else
+      {
+        final int segment_width = segment_start_coord - segment_end_coord + 1;
+
+        g.fillRect(segment_end_coord, vertical_offset,
+                    segment_width, segment_height + 1);
+      }
+    }
+    else 
+    {
+      g.setColor(Color.black);
+      g.drawPolygon(x_points, y_points, 4);
+
+      if(highlight_feature)  // highlight selected features
+      {
+        // selected - highlight by drawing a thicker line
+
+        x_points[0] -= feature_direction; x_points[1] += feature_direction;
+        x_points[2] += feature_direction; x_points[3] -= feature_direction;
+        y_points[0] -= 1; y_points[1] -= 1;
+        y_points[2] += 1; y_points[3] += 1;
+        g.drawPolygon(x_points, y_points, 4);
+
+        x_points[0] -= feature_direction; x_points[1] += feature_direction;
+        x_points[2] += feature_direction; x_points[3] -= feature_direction;
+        y_points[0] -= 1; y_points[1] -= 1;
+        y_points[2] += 1; y_points[3] += 1;
+        g.drawPolygon(x_points, y_points, 4);
+
+        if(highlight_segment) {
+          x_points[0] -= feature_direction; x_points[1] += feature_direction;
+          x_points[2] += feature_direction; x_points[3] -= feature_direction;
+          y_points[0] -= 1; y_points[1] -= 1;
+          y_points[2] += 1; y_points[3] += 1;
+          g.drawPolygon(x_points, y_points, 4);
+
+          x_points[0] -= feature_direction; x_points[1] += feature_direction;
+          x_points[2] += feature_direction; x_points[3] -= feature_direction;
+          y_points[0] -= 1; y_points[1] -= 1;
+          y_points[2] += 1; y_points[3] += 1;
+          g.drawPolygon(x_points, y_points, 4);
+        }
+      }
+
+      if(draw_arrow)
+      {
+        // now draw the arrow point
+        final int arrow_tip_x =
+          x_points[1] + feature_direction * getFontWidth() * 8 / 10;
+        final int arrow_tip_y =(y_points[1] + y_points[2]) / 2;
+
+        g.drawLine(x_points[1], y_points[1], arrow_tip_x, arrow_tip_y);
+        g.drawLine(arrow_tip_x, arrow_tip_y, x_points[2], y_points[2]);
+      }
+    }
+  }
+
+  /**
+   *  Draw/highlight the selected range of bases on the FORWARD_STRAND or
+   *  REVERSE_STRAND lines.
+   **/
+  private void drawBaseSelection(Graphics g) 
+  {
+    final MarkerRange selection_range = getSelection().getMarkerRange();
+
+    if(selection_range == null) 
+      return;
+
+    final Marker raw_start_base;
+    final Marker raw_end_base;
+
+    if(selection_range.getStrand().isForwardStrand()) 
+    {
+      raw_start_base = selection_range.getRawStart();
+      raw_end_base = selection_range.getRawEnd();
+    }
+    else
+    {
+      raw_start_base = selection_range.getRawEnd();
+      raw_end_base = selection_range.getRawStart();
+    }
+
+    final int start_coord = getLowXPositionOfMarker(raw_start_base);
+    final int end_coord = getHighXPositionOfMarker(raw_end_base);
+
+    // not on screen
+    if(start_coord > getCanvas().getSize().width &&
+        end_coord > getCanvas().getSize().width) 
+      return;
+
+    // not on screen
+    if(start_coord < 0 && end_coord < 0) 
+      return;
+
+    // the ID of the strand on which to draw the highlighting - this will be
+    // FORWARD_STRAND or REVERSE_STRAND.
+    final int strand_id;
+
+    if(selection_range.getStrand().isForwardStrand() ^
+        isRevCompDisplay()) 
+      strand_id = FORWARD_STRAND;
+    else
+      strand_id = REVERSE_STRAND;
+
+    if(!getOneLinePerEntryFlag()) 
+    {
+      // the ID of the frame on which to draw the highlighting - this will be
+      // FORWARD_FRAME_1, 2 or 3 or REVERSE_FRAME_1, 2 or 3.
+      final int frame_id;
+
+      if(selection_range.getStrand().isForwardStrand() ^
+          isRevCompDisplay()) 
+      {
+        switch((selection_range.getStart().getPosition() - 1) % 3) 
+        {
+          case 0:
+            frame_id = FORWARD_FRAME_1;
+            break;
+          case 1:
+            frame_id = FORWARD_FRAME_2;
+            break;
+          case 2:
+            frame_id = FORWARD_FRAME_3;
+            break;
+          default:
+            frame_id = NO_FRAME;
+        }
+      } 
+      else 
+      {
+        switch((selection_range.getStart().getPosition() - 1) % 3)
+        {
+          case 0:
+            frame_id = REVERSE_FRAME_1;
+            break;
+          case 1:
+            frame_id = REVERSE_FRAME_2;
+            break;
+          case 2:
+            frame_id = REVERSE_FRAME_3;
+            break;
+          default:
+            frame_id = NO_FRAME;
+        }
+      }
+
+      if(show_forward_lines && strand_id == FORWARD_STRAND ||
+          show_reverse_lines && strand_id == REVERSE_STRAND) 
+        drawBaseRange(g, start_coord, end_coord, frame_id, Color.pink);
+    }
+
+    drawBaseRange(g, start_coord, end_coord, strand_id, Color.yellow);
+  }
+
+  /**
+   *  Draw a rectangle representing the currently selected bases.
+   *  @param g The Graphics object on which to draw.
+   *  @param start_coord The start x coordinate.
+   *  @param start_coord The end x coordinate.
+   *  @param frame_id The ID of the frame line where the box should be drawn.
+   *  @param fill_colour The colour to use to draw the box(the border will be
+   *    black).
+   **/
+  private void drawBaseRange(Graphics g,
+                             int start_coord, int end_coord,
+                             int frame_id,
+                             Color fill_colour) 
+  {
+    if(start_coord < -1) 
+      start_coord = -1;
+
+    if(end_coord < -1) 
+      end_coord = -1;
+
+    if(end_coord > getCanvasWidth()) 
+      end_coord = getCanvasWidth();
+
+    if(start_coord > getCanvasWidth()) 
+      start_coord = getCanvasWidth();
+
+    final int frame_line = getFrameDisplayLine(frame_id);
+
+    final int frame_line_y_coord = getLineOffset(frame_line);
+
+    final int [] x_points = {
+      start_coord, end_coord, end_coord, start_coord
+    };
+
+    final int [] y_points = {
+      frame_line_y_coord, frame_line_y_coord,
+      frame_line_y_coord + getFeatureHeight() + 1,
+      frame_line_y_coord + getFeatureHeight() + 1
+    };
+
+    g.setColor(fill_colour);
+
+    if(start_coord > end_coord) 
+      g.fillRect(end_coord, frame_line_y_coord + 1,
+                 start_coord - end_coord + 1, getFeatureHeight());
+    else 
+      g.fillRect(start_coord, frame_line_y_coord + 1,
+                 end_coord - start_coord + 1, getFeatureHeight());
+
+    g.setColor(Color.black);
+    g.drawPolygon(x_points, y_points, 4);
+  }
+
+  /**
+   *  Return the number of button the user has down.
+   **/
+  private int buttonDownCount(final MouseEvent event) 
+  {
+    int count = 0;
+    if((event.getModifiers() & InputEvent.BUTTON1_MASK) > 0) 
+      ++count;
+    
+    if((event.getModifiers() & InputEvent.BUTTON2_MASK) > 0) 
+      ++count;
+    
+    if((event.getModifiers() & InputEvent.BUTTON3_MASK) > 0) 
+      ++count;
+    
+    return count;
+  }
+
+  /**
+   *  Add mouse and key listeners to the canvas.
+   **/
+  private void addListeners() 
+  {
+    getCanvas().addMouseListener(new MouseAdapter() 
+    {
+      /**
+       *  Listen for mouse press events so that we can do popup menus and
+       *  selection.
+       **/
+      public void mousePressed(MouseEvent event)
+      {
+        // finish dragging if the user presses any other button because
+        // we may not get a mouseReleased() call on some platforms
+        if(click_segment_marker != null) 
+        {
+          getEntryGroup().getActionController().endAction();
+          click_segment_marker = null;
+        }
+
+        //ignore
+        if(buttonDownCount(event) > 1) 
+          return;
+
+        last_mouse_press_event = event;
+        handleCanvasMousePress(event);
+
+        if(isMenuTrigger(event)) 
+        {
+          final FeaturePopup popup =
+            new FeaturePopup(FeatureDisplay.this,
+                             getEntryGroup(),
+                             getSelection(),
+                             getGotoEventSource(),
+                             getBasePlotGroup());
+          final JComponent parent = (JComponent)event.getSource();
+
+          parent.add(popup);
+          popup.show(parent, event.getX(), event.getY());
+        }
+      }
+
+      /**
+       *  Listen for mouse release events so that we can call endAction().
+       **/
+      public void mouseReleased(MouseEvent event) 
+      {
+        last_mouse_press_event = null;
+
+        if(click_segment_marker != null) 
+        {
+          getEntryGroup().getActionController().endAction();
+          click_segment_marker = null;
+        }
+      }
+    });
+
+    // Listen for mouse motion events so that we can select ranges of bases.
+    getCanvas().addMouseMotionListener(new MouseMotionAdapter() {
+      public void mouseDragged(MouseEvent event) 
+      {
+        if(last_mouse_press_event != null &&
+            !isMenuTrigger(last_mouse_press_event)) 
+          handleCanvasMouseDrag(event);
+      }
+    });
+
+    getCanvas().addKeyListener(new KeyAdapter() {
+      public void keyPressed(final KeyEvent event) 
+      {
+        handleKeyPress(FeatureDisplay.this, event);
+      }
+    });
+  }
+
+  /**
+   *  Handle a mouse press event on the drawing canvas - select on click,
+   *  select and broadcast it on double click.
+   **/
+  private void handleCanvasMousePress(MouseEvent event) 
+  {
+    if(event.getID() != MouseEvent.MOUSE_PRESSED) 
+      return;
+
+    getCanvas().requestFocus();
+
+    if(event.getClickCount() == 2) 
+      handleCanvasDoubleClick(event);
+    else
+      handleCanvasSingleClick(event);
+
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Handle a double click on the canvas.
+   **/
+  private void handleCanvasDoubleClick(MouseEvent event) 
+  {
+    if(isMenuTrigger(event)) 
+      return;
+
+    if((event.getModifiers() & InputEvent.BUTTON2_MASK) != 0 ||
+        event.isAltDown()) 
+    {
+
+      final Selectable clicked_thing = getThingAtPoint(event.getPoint());
+
+      if(clicked_thing == null) 
+      {
+        // select the ORF around the click position
+        final MarkerRange new_click_range =
+          getMarkerRangeFromPosition(event.getPoint());
+
+        if(new_click_range == null) 
+          return;
+
+        final MarkerRange orf_range =
+          Strand.getORFAroundMarker(new_click_range.getStart(), true);
+
+        // user clicked on a stop codon
+        if(orf_range == null) 
+          return;
+        else 
+          getSelection().setMarkerRange(orf_range);
+      }
+      else 
+      {
+        // edit the selected Feature
+
+        final Feature clicked_feature = getFeatureOf(clicked_thing);
+
+        getSelection().set(clicked_feature);
+
+        if(Options.readWritePossible()) 
+          new FeatureEdit(clicked_feature, getEntryGroup(),
+                           getSelection(),
+                           getGotoEventSource()).show();
+      }
+    }
+
+    makeSelectionVisible();
+  }
+
+  /**
+   *  Handle a single click on the canvas.
+   **/
+  private void handleCanvasSingleClick(MouseEvent event) 
+  {
+    click_segment_marker = null;
+
+    final Selectable clicked_thing = getThingAtPoint(event.getPoint());
+
+    // treate alt modifier like pressing button 2
+    if(clicked_thing == null ||
+       (event.getModifiers() & InputEvent.BUTTON2_MASK) != 0) 
+    {
+
+      // if the user presses the mouse button 2 on feature or segment we treat
+      // it like the feature/segment isn't there
+
+      // if the user hasn't clicked on anything then don't change the
+      // selection
+      if(isMenuTrigger(event)) 
+        return;
+
+      // something is selected but the user clicked on nothing with the
+      // shift key down - do nothing
+      if(event.isShiftDown() &&
+          getSelection().getAllFeatures().size() > 0) 
+        return;
+
+      final MarkerRange old_selected_range = getSelection().getMarkerRange();
+
+      final MarkerRange new_click_range =
+        getMarkerRangeFromPosition(event.getPoint());
+
+      if(new_click_range == null) 
+      {
+        click_range = null;
+        getSelection().clear();
+        return;
+      }
+
+      final MarkerRange new_selection_range;
+
+      if(!event.isShiftDown() || old_selected_range == null) 
+        new_selection_range = new_click_range;
+      else 
+      {
+        if(old_selected_range.getStrand() ==
+            new_click_range.getStrand()) 
+        {
+          // extend the selection
+          new_selection_range =
+            old_selected_range.combineRanges(new_click_range, true);
+        }
+        else 
+          new_selection_range = old_selected_range;
+      }
+
+      getSelection().setMarkerRange(new_selection_range);
+      click_range = new_selection_range;
+
+    }  
+    else
+    {
+      // can't select a feature and a MarkerRange
+      if(getSelection().getMarkerRange() != null &&
+         event.isShiftDown()) 
+        return;
+
+      // clear the MarkerRange because this isn't the start of a range drag
+      click_range = null;
+
+      getSelection().setMarkerRange(null);
+      final Feature clicked_feature = getFeatureOf(clicked_thing);
+      raiseFeature(clicked_feature);
+
+      if(clicked_thing instanceof Feature) 
+      {
+        // toggle the feature in or out of the selection unless this event
+        // is a popup trigger in which case we should just make sure the
+        // feature is in the selection
+        if(getSelection().contains(clicked_feature)) 
+        {
+          if(! isMenuTrigger(event)) 
+          {
+            // change the selection only if the user isn't popping up a menu
+            if(event.isShiftDown()) 
+              getSelection().remove(clicked_feature);
+            else 
+              getSelection().set(clicked_feature);
+          }
+        } 
+        else
+        {
+          if(event.isShiftDown()) 
+            getSelection().add(clicked_feature);
+          else 
+            getSelection().set(clicked_feature);
+        }
+      } 
+      else
+      {
+        // must be a FeatureSegment
+
+        // toggle the feature segment in or out of the selection unless
+        // this event is a popup trigger in which case we should just make
+        // sure the segment is in the selection
+        final FeatureSegment clicked_segment =
+         (FeatureSegment) clicked_thing;
+
+        final FeatureSegmentVector selected_feature_segments =
+          getSelection().getSelectedSegments();
+
+        if(selected_feature_segments.contains(clicked_segment)) 
+        {
+          if(! isMenuTrigger(event)) 
+          {
+            if(event.isShiftDown()) 
+              getSelection().remove(clicked_segment);
+            else 
+              getSelection().set(clicked_segment);
+          }
+        } 
+        else
+        {
+          if(event.isShiftDown()) 
+          {
+            final FeatureVector selected_features =
+              getSelection().getSelectedFeatures();
+
+            if(selected_features.contains(clicked_feature)) 
+              getSelection().remove(clicked_feature);
+            else 
+              getSelection().add(clicked_segment);
+          }
+          else 
+            getSelection().set(clicked_segment);
+        }
+      }
+
+      if(Options.getOptions().canDirectEdit() &&
+          !clicked_feature.isReadOnly()) 
+      {
+        // check to see if the click was on a marker, the second argument is
+        // false because we want the range to cover just one base
+
+        final MarkerRange new_click_range =
+          getMarkerRangeFromPosition(event.getPoint(), false);
+
+        // search the feature to find if the start Marker of any of the
+        // segments matches the start Marker of new_click_range or if an end
+        // Marker matches the end Marker of new_click_range
+
+        final FeatureSegmentVector segments = clicked_feature.getSegments();
+
+        for(int i = 0 ; i < segments.size() ; ++i) 
+        {
+          final FeatureSegment this_segment = segments.elementAt(i);
+
+          if(new_click_range.getStart().equals(this_segment.getStart()) &&
+              this_segment.canDirectEdit()) 
+          {
+            click_segment_marker = this_segment.getStart();
+            click_segment_marker_is_start_marker = true;
+            other_end_of_segment_marker = this_segment.getEnd();
+            getEntryGroup().getActionController().startAction();
+            break;
+          }
+
+          if(new_click_range.getEnd().equals(this_segment.getEnd()) &&
+              this_segment.canDirectEdit()) 
+          {
+            click_segment_marker = this_segment.getEnd();
+            click_segment_marker_is_start_marker = false;
+            other_end_of_segment_marker = this_segment.getStart();
+            getEntryGroup().getActionController().startAction();
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   *  This is a helper method for handleCanvasSingleClick() and
+   *  handleCanvasDoubleClick().
+   *  @param object The should be an instance of Feature or FeatureSegment.
+   *  @return If the the argument is a Feature it is returned, if it is a
+   *    FeatureSegment the owning Feature is returned.
+   **/
+  private Feature getFeatureOf(final Object object) 
+  {
+    if(object instanceof Feature) 
+      return(Feature) object;
+    else 
+      // object must be a FeatureSegment
+      return((FeatureSegment) object).getFeature();
+  }
+
+  /**
+   *  Handle a mouse drag that started on a Marker from a FeatureSegment.
+   **/
+  private void handleMarkerDrag(final MouseEvent event) 
+  {
+    MarkerRange drag_range =
+      getMarkerRangeFromPosition(event.getPoint(), false);
+
+    if(drag_range == null) 
+      return;
+
+    final int new_position;
+
+    if(click_segment_marker_is_start_marker) 
+    {
+      // the Marker is at the start of the segment
+      new_position = drag_range.getStart().getPosition();
+
+      // don't go past the other end of the segment
+      if(new_position > other_end_of_segment_marker.getPosition()) 
+        return;
+    }
+    else
+    {
+      new_position = drag_range.getEnd().getPosition();
+
+      // don't go past the other end of the segment
+      if(new_position < other_end_of_segment_marker.getPosition()) 
+        return;
+    }
+
+    try
+    {
+      click_segment_marker.setPosition(new_position);
+
+      if(!baseVisible(click_segment_marker)) 
+        makeBaseVisible(click_segment_marker);
+    }
+    catch(OutOfRangeException e) 
+    {
+      throw new Error("internal error - unexpected OutOfRangeException");
+    }
+  }
+
+  /**
+   *  Handle a mouse drag event on the drawing canvas.
+   **/
+  private void handleCanvasMouseDrag(final MouseEvent event)
+  {
+    if(event.isShiftDown() &&
+        getSelection().getAllFeatures().size() > 0) 
+      return;
+
+    if(click_segment_marker != null) 
+    {
+      handleMarkerDrag(event);
+      return;
+    }
+
+    MarkerRange drag_range = getMarkerRangeFromPosition(event.getPoint());
+
+    if(drag_range == null) 
+      return;
+
+    final MarkerRange selected_range = getSelection().getMarkerRange();
+
+    // if the start and current positions of the drag are not on the
+    // same Strand then ignore this event
+    if(selected_range != null &&
+        drag_range.getStrand() != selected_range.getStrand()) 
+      return;
+
+    try 
+    {
+      // user has dragged off the screen so use a marker at position 1
+      if(drag_range.getStart().getPosition() < 1) 
+        drag_range = new MarkerRange(drag_range.getStrand(), 1, 1);
+
+      // user has dragged off the screen so use a marker at position the
+      // end of the sequence
+      if(drag_range.getEnd().getPosition() > getSequenceLength()) 
+        drag_range = new MarkerRange(drag_range.getStrand(),
+                                      getSequenceLength(),
+                                      getSequenceLength());
+    }
+    catch(OutOfRangeException e) 
+    {
+      throw new Error("internal error - unexpected OutOfRangeException");
+    }
+
+    final boolean start_base_is_visible = baseVisible(drag_range.getStart());
+    final boolean end_base_is_visible = baseVisible(drag_range.getEnd());
+
+    if(!start_base_is_visible) 
+      makeBaseVisible(drag_range.getStart());
+
+    if(!end_base_is_visible) 
+      makeBaseVisible(drag_range.getEnd());
+
+    // scrolling was necessary so update the visible features vector
+    if(!start_base_is_visible || !end_base_is_visible) 
+      needVisibleFeatureVectorUpdate();
+
+    final MarkerRange new_marker_range;
+
+    if(click_range == null) 
+      new_marker_range = drag_range;
+    else 
+      new_marker_range = selected_range.combineRanges(drag_range, true);
+
+    getSelection().setMarkerRange(new_marker_range);
+
+    getCanvas().repaint();
+  }
+
+  /**
+   *  Return the reference of the Object at p on the canvas or null if there
+   *  is none.  The returned Object will be a Feature or a FeatureSegment.
+   **/
+  private Selectable getThingAtPoint(Point p) 
+  {
+    final int line_of_point = getLineFromPoint(p);
+
+    // point is not on a forward or reverse frame or strand
+    if(line_of_point == -1) 
+      return null;
+
+    // go through the features in reverse order because the feature that is
+    // drawn last will be on top
+    for(int i = getVisibleFeatures().size() - 1 ; i >= 0 ; --i) 
+    {
+      final Feature current_feature = getVisibleFeatures().elementAt(i);
+      final FeatureSegmentVector segments = current_feature.getSegments();
+
+      for(int segment_index = 0; segment_index < segments.size();
+          ++segment_index) 
+      {
+        final FeatureSegment current_segment =
+                                         segments.elementAt(segment_index);
+
+        final int line_of_segment = getSegmentDisplayLine(current_segment);
+
+        if(line_of_segment == line_of_point ||
+            show_labels && line_of_segment + 1 == line_of_point) 
+        {
+          // this segment is in the right frame so check that it is between
+          // the start and end positions of the segment
+
+          if(p.x >= getSegmentStartCoord(current_segment) &&
+              p.x <= getSegmentEndCoord(current_segment) ||
+              p.x <= getSegmentStartCoord(current_segment) &&
+              p.x >= getSegmentEndCoord(current_segment)) 
+          {
+            final Feature segment_feature = current_segment.getFeature();
+
+            // special case - if there is only one segment then select the
+            // whole feature
+            if(segment_feature.getSegments().size() == 1) 
+              return segment_feature;
+            else
+              return current_segment;
+          }
+        }
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   *  Create and add a new scroll bar to this FeatureDisplay.
+   **/
+  private void createScrollbar(final boolean scrollbar_at_top) 
+  {
+    scrollbar = new JScrollBar(Scrollbar.HORIZONTAL);
+    scrollbar.addAdjustmentListener(new AdjustmentListener() 
+    {
+      public void adjustmentValueChanged(AdjustmentEvent e) 
+      {
+        if(left_edge_base != e.getValue()) 
+        {
+          if(e.getValue() < getSequenceLength()) 
+            left_edge_base = e.getValue();
+          else
+          {
+            if(left_edge_base == getSequenceLength()) 
+              return;
+            else 
+              left_edge_base = getSequenceLength();
+          }
+
+          fireAdjustmentEvent(DisplayAdjustmentEvent.SCROLL_ADJUST_EVENT);
+
+          needVisibleFeatureVectorUpdate();
+          getCanvas().repaint();
+        }
+      }
+    });
+
+    if(scrollbar_at_top) 
+      getMidPanel().add(scrollbar, "North");
+    else 
+      getMidPanel().add(scrollbar, "South");
+    
+    fixScrollbar();
+    fireAdjustmentEvent(DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT);
+  }
+
+  /**
+   *  Send a DisplayAdjustmentEvent with the current start and end base to
+   *  each of the listeners.  This has package scope because EntryEdit
+   *  components need to send this event when a new BasePlot component is
+   *  added.  The BasePlot needs to know the first visible base, last visible
+   *  base and the maximum number of visible bases before the plot can be
+   *  drawn.
+   *  @param type the type of event: DisplayAdjustmentEvent.SCALE_ADJUST_EVENT,
+   *    DisplayAdjustmentEvent.SCROLL_ADJUST_EVENT or
+   *    DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT.
+   **/
+  void fireAdjustmentEvent(final int type) 
+  {
+    if(disable_display_events) 
+      return;
+
+    final DisplayAdjustmentEvent event =
+      new DisplayAdjustmentEvent(this,
+                                 getForwardBaseAtLeftEdge(),
+                                 getLastVisibleForwardBase(),
+                                 getMaxVisibleBases(),
+                                 getScaleValue(), getScaleFactor(),
+                                 isRevCompDisplay(), type);
+
+    fireAction(adjustment_listener_list, event);
+  }
+
+  /**
+   *  Send an event to those object listening for it.
+   *  @param listeners A Vector of the objects that the event should be sent
+   *    to.
+   *  @param event The event to send
+   **/
+  private void fireAction(Vector listeners, ChangeEvent event) 
+  {
+    final Vector targets;
+    // copied from a book - synchronizing the whole method might cause a
+    // deadlock
+    synchronized(this) 
+    {
+      targets =(Vector) listeners.clone();
+    }
+
+    for(int i = 0; i < targets.size(); ++i) 
+    {
+      ChangeListener change_listener = (ChangeListener)targets.elementAt(i);
+
+      final DisplayAdjustmentListener target =
+                               (DisplayAdjustmentListener)change_listener;
+      target.displayAdjustmentValueChanged((DisplayAdjustmentEvent)event);
+    }
+  }
+
+  /**
+   *  Create the scroll bar used for changing the scale and add it to the
+   *  FeatureDisplay.
+   **/
+  private void createScaleScrollbar() 
+  {
+    scale_changer = new JScrollBar(Scrollbar.VERTICAL);
+    // try to arrange for the scrollbar to have a maximum value big enough
+    // that the whole sequence can be visible at once
+    final int MAX_FACTOR =
+      (int)Math.round(Math.log(getSequenceLength()/20) /  Math.log(3));
+    scale_changer.setValues(getScaleFactor(), 1, 0, MAX_FACTOR);
+    scale_changer.setBlockIncrement(1);
+    scale_changer.setUnitIncrement(1);
+    scale_changer.addAdjustmentListener(new AdjustmentListener() 
+    {
+      public void adjustmentValueChanged(AdjustmentEvent e) 
+      {
+        setScaleFactor(e.getValue());
+      }
+    });
+
+    add(scale_changer, "East");
+
+    if(scale_factor >= MAX_FACTOR) 
+      setScaleFactor(MAX_FACTOR - 1);
+  }
+
+  /**
+   *  Update the parameters of the scrollbar taking changes to the entry_group
+   *  into account.
+   **/
+  private void fixScrollbar() 
+  {
+    if(scrollbar == null)
+      return;
+
+    final int sequence_length = getSequenceLength();
+
+    final int max_visible_bases;
+
+    // make sure max_visible_bases is at least 1 to stop setBlockIncrement()
+    // from complaining
+    if(getMaxVisibleBases() > 0) 
+      max_visible_bases = getMaxVisibleBases();
+    else 
+      max_visible_bases = 1;
+
+    // initial_value, visible, minimum, maximum
+    scrollbar.setValues(getForwardBaseAtLeftEdge(),
+                         max_visible_bases,
+                         hard_left_edge ? 1 : -max_visible_bases / 2,
+                         sequence_length + max_visible_bases / 2);
+
+    scrollbar.setBlockIncrement(max_visible_bases);
+
+    if(max_visible_bases >= 10 && getScaleFactor() > 0)
+      scrollbar.setUnitIncrement(max_visible_bases / 10);
+    else
+      scrollbar.setUnitIncrement(1);
+  }
+
+  /**
+   *  Set the size of the canvas, taking the value of font_height and
+   *  show_label into account.
+   **/
+  private void fixCanvasSize() 
+  {
+    final int new_width  = getCanvas().getSize().width;
+    final int new_height = getLineHeight() * getLineCount();
+
+    if(new_height != getCanvas().getSize().width ||
+        new_width != getCanvas().getSize().height) 
+    {
+      final Dimension preferred_size =
+        new Dimension(getCanvas().getSize().width, new_height);
+      getCanvas().setPreferredSize(preferred_size);
+
+      final Dimension minimum_size = new Dimension(1, new_height);
+      getCanvas().setMinimumSize(minimum_size);
+
+      getCanvas().revalidate();
+      getCanvas().repaint();
+    }
+  }
+
+  /**
+   *  Returns the base length of the sequence we are displaying in this
+   *  component.
+   **/
+  public int getSequenceLength() 
+  {
+    return getEntryGroup().getSequenceLength();
+  }
+
+  /**
+   *  Set the first visible base in the forward direction (set the base on the
+   *  forward strand that is on the left of the canvas).  This method will
+   *  scroll the display so that this base is is at the very left hand side of
+   *  the canvas.
+   **/
+  private void setFirstVisibleForwardBase(int new_position) 
+  {
+    if(left_edge_base != new_position) 
+    {
+      left_edge_base = new_position;
+      if(scrollbar != null) 
+        scrollbar.setValue(new_position);
+      
+      needVisibleFeatureVectorUpdate();
+      getCanvas().repaint();
+    }
+  }
+
+  /**
+   *  Return the height each line of the display should be.  Each frame will
+   *  be drawn into one line.
+   **/
+  private int getLineHeight() 
+  {
+    return getFontHeight();
+  }
+
+  /**
+   *  Return the current scale factor.  The scale factor is a number greater
+   *  than or equal to zero than controls the number of bases that can appear
+   *  on screen.  See getScaleValue().
+   **/
+  public int getScaleFactor() 
+  {
+    return scale_factor;
+  }
+
+  /**
+   *  Return the ID of the first(ie. top) line of the display.
+   **/
+  private int getFirstLineID() 
+  {
+    if(show_forward_lines) 
+      return FORWARD_FRAME_1;
+    else 
+      return FORWARD_STRAND;
+  }
+
+  /**
+   *  Return the ID of the last(ie. bottom) line of the display.
+   **/
+  private int getLastLineID() 
+  {  
+    if(show_reverse_lines) 
+      return REVERSE_FRAME_1;
+    else 
+      return REVERSE_STRAND;
+  }
+
+  /**
+   *  Return the display line that contains the given point, or -1 if
+   *  the point is off screen.
+   **/
+  private int getLineFromPoint(final Point point) 
+  {
+    if(point.y >= getCanvasHeight()) 
+      return -1;
+
+    final int return_value = point.y / getLineHeight();
+
+    if(return_value < 0)
+      return -1;
+    else 
+      return return_value;
+  }
+
+  /**
+   *  Return the ID of the frame at the given Point in the canvas or NO_FRAME
+   *  if the Point is not within a frame. If labels are on then points on the
+   *  label lines get returned as the frame id of the STRAND or FRAME line
+   *  above.
+   **/
+  private int getFrameFromPoint(final Point point) 
+  {
+    if(getOneLinePerEntryFlag()) 
+    {
+      // there are no frame lines so just look at the strand lines
+      final int line_point = getLineFromPoint(point);
+      if(line_point == getFrameDisplayLine(FORWARD_STRAND) ^
+         isRevCompDisplay()) 
+        return FORWARD_STRAND;
+      
+      if(line_point == getFrameDisplayLine(REVERSE_STRAND) ^
+          isRevCompDisplay()) 
+        return REVERSE_STRAND;
+      
+      // fall through
+    } 
+    else 
+    {
+      final int start_frame = getFirstLineID();
+      final int end_frame = getLastLineID();
+
+      for(int i = start_frame ; i <= end_frame ; ++i) {
+        final int top_of_line = getLineOffset(getFrameDisplayLine(i));
+        final int line_height;
+
+        if(show_labels) 
+          line_height = getLineHeight() * 2;
+        else 
+          line_height = getLineHeight();
+
+        if(point.y >= top_of_line &&
+           point.y < top_of_line + line_height) 
+          return i;
+      }
+    }
+    // the point isn't in any or the frames
+    return NO_FRAME;
+  }
+
+  /**
+   *  Return the base number of the first visible base displayed in the
+   *  forward direction, ie the base on the forward(or top strand) that is on
+   *  the left each of the canvas.  Note that the strand that is currently
+   *  being display in the forward direction will be the reverse strand if
+   *  rev_comp_display is true.  The number returned will always be > 1 and <
+   *  the sequence length.  If hard_left_edge is true this method will always
+   *  return the same as getForwardBaseAtLeftEdge*(.
+   **/
+  public int getFirstVisibleForwardBase() 
+  {
+    if(left_edge_base < 1) 
+      return 1;
+    else 
+      return left_edge_base;
+  }
+
+  /**
+   *  Return the base number of the first visible base displayed in the
+   *  forward direction, ie the base on the forward(or top strand) that is on
+   *  the left each of the canvas.  Note that the strand that is currently
+   *  being display in the forward direction will be the reverse strand if
+   *  rev_comp_display is true.
+   **/
+  public int getForwardBaseAtLeftEdge() 
+  {
+    return left_edge_base;
+  }
+
+  /**
+   *  Return a base marker of the first visible base in the forward
+   *  direction, ie the base on the forward strand that is on the left
+   *  each of the canvas.  See comments on getFirstVisibleForwardBase().
+   **/
+  private Marker getFirstVisibleForwardBaseMarker() 
+  {
+    try 
+    {
+      final Strand strand;
+      if(isRevCompDisplay()) 
+        strand = getBases().getReverseStrand();
+      else 
+        strand = getBases().getForwardStrand();
+      
+      return strand.makeMarker(getFirstVisibleForwardBase());
+    } 
+    catch(OutOfRangeException e) 
+    {
+      throw new Error("internal error - unexpected OutOfRangeException");
+    }
+  }
+
+  /**
+   *  Return the base number of the last visible base in the forward
+   *  direction, ie the base on the forward strand that is on the right
+   *  edge of the canvas.  The number returned will always be > 1 and < the
+   *  sequence length.
+   **/
+  public int getLastVisibleForwardBase() 
+  {
+    final int possible_last_base =
+     (int)(getForwardBaseAtLeftEdge() + getMaxVisibleBases());
+
+    if(possible_last_base > getSequenceLength()) 
+      return getSequenceLength();
+    else 
+    {
+      if(possible_last_base > 0) 
+        return possible_last_base;
+      else 
+        return 1;
+    }
+  }
+
+  /**
+   *  Return the base number of the first visible base in the reverse
+   *  direction , ie the base on the reverse strand that is on the left
+   *  each of the canvas.
+   **/
+  private int getFirstVisibleReverseBase() 
+  {
+    final int last_forward_base = getLastVisibleForwardBase();
+    return getBases().getComplementPosition(last_forward_base);
+  }
+
+  /**
+   *  Return the base number of the last visible base in the reverse
+   *  direction, ie the base on the reverse strand that is on the right
+   *  edge of the canvas.
+   **/
+  private int getLastVisibleReverseBase() 
+  {
+    // XXX
+    final int first_forward_base = getFirstVisibleForwardBase();
+    return getBases().getComplementPosition(first_forward_base);
+  }
+
+  /**
+   *  Return the base of the forward Strand of the sequence that is closest to
+   *  the centre of the view.
+   **/
+  private int getCentreForwardBase() 
+  {
+    int possible_return_position =
+      getForwardBaseAtLeftEdge() + getMaxVisibleBases() / 2;
+
+    int return_position;
+
+    if(possible_return_position < getSequenceLength()) 
+      return_position = possible_return_position;
+    else 
+      return_position = getSequenceLength();
+
+    return return_position;
+  }
+
+  /**
+   *  Return the amount display is scaled.  The value is 3 to the power of
+   *  -(scale_factor - 1), except for a scale_factor of zero which gives a
+   *  scale value of font_width.
+   **/
+  float getScaleValue() 
+  {
+    return scale_value;
+  }
+
+  /**
+   *  Return the height in pixels we should use for drawing features.
+   **/
+  private int getFeatureHeight() 
+  {
+    // don't use getLineHeight() because we want a nice space between
+    // frames/lines
+    return getFontAscent() + 2;
+  }
+
+  /**
+   *  Return the number of bases we can fit on screen at once, ie the number
+   *  that will fit side by side on the canvas.
+   **/
+  public int getMaxVisibleBases() 
+  {
+    return(int)(getCanvasWidth() / getScaleValue());
+  }
+
+  /**
+   *  Return the Bases object of the EntryGroup that this FeatureDisplay is
+   *  displaying.
+   **/
+  public Bases getBases() 
+  {
+    return getEntryGroup().getBases();
+  }
+
+  /**
+   *  Update the value of scale_value to reflect the current value of
+   *  scale_factor.
+   **/
+  private void setScaleValue() 
+  {
+    final int scale_factor = getScaleFactor();
+
+    if(scale_factor > 0) 
+      scale_value =(float)(1 / Math.pow(3, scale_factor - 1));
+    else 
+      scale_value = getFontWidth();
+  }
+
+  /**
+   *  Scroll and scale the display so that the given first base is at the left
+   *  edge of the screen and the given last base is at the right edge.
+   **/
+  public void setFirstAndLastBase(final int first, final int last) 
+  {
+    left_edge_base = first;
+    setScaleValue(1.0F * getCanvasWidth() /(last - first + 1));
+  }
+
+  /**
+   *  Scroll the display so that the given first base is at the left edge of
+   *  the screen.
+   **/
+  public void setFirstBase(int base_position) 
+  {
+    if(base_position > getSequenceLength()) 
+      base_position = getSequenceLength();
+
+    if(base_position < 1 && hard_left_edge) 
+      base_position = 1;
+
+    setFirstVisibleForwardBase(base_position);
+    fireAdjustmentEvent(DisplayAdjustmentEvent.SCROLL_ADJUST_EVENT);
+  }
+
+  /**
+   *  Set the scale value to use for this FeatureDisplay.  Note that it is
+   *  better to call setScaleFactor() which will also set the scale value.
+   **/
+  private void setScaleValue(final float scale_value) 
+  {
+    if(scale_value <= 0) 
+      throw new Error("internal error in FeatureDisplay.setScaleValue() - " +
+                       "scale value must be positive");
+
+    this.scale_value = scale_value;
+
+    if(scale_value > 1) 
+      scale_factor = 1;
+    else 
+      scale_factor =
+       (int) Math.round(Math.log(1/scale_value) /  Math.log(3)) + 1;
+
+    scale_changer.setValue(scale_factor);
+
+    fixScrollbar();
+    needVisibleFeatureVectorUpdate();
+    getCanvas().repaint();
+    fireAdjustmentEvent(DisplayAdjustmentEvent.SCALE_ADJUST_EVENT);
+  }
+
+  /**
+   *  Make a new Range and throw a Error is an OutOfRangeException occurs.
+   **/
+  private Range newRange(final int start, final int end) 
+  {
+    try 
+    {
+      return new Range(start, end);
+    } 
+    catch(OutOfRangeException e) 
+    {
+      throw new Error("internal error - unexpected exception: " + e);
+    }
+  }
+
+  /**
+   *  Arrange for updateVisibleFeatureVector() to be called in the next call
+   *  to paint()
+   **/
+  private void needVisibleFeatureVectorUpdate() 
+  {
+    update_visible_features = true;
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/FeatureEdit.java b/uk/ac/sanger/artemis/components/FeatureEdit.java
new file mode 100644
index 000000000..bd4988755
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeatureEdit.java
@@ -0,0 +1,1276 @@
+/* FeatureEdit.java
+ *
+ * created: Tue Dec  1 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1998,1999,2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeatureEdit.java,v 1.1 2004-06-09 09:46:38 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.io.OutOfDateException;
+import uk.ac.sanger.artemis.io.LocationParseException;
+import uk.ac.sanger.artemis.io.InvalidRelationException;
+import uk.ac.sanger.artemis.io.QualifierParseException;
+import uk.ac.sanger.artemis.io.Range;
+import uk.ac.sanger.artemis.io.RangeVector;
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.KeyVector;
+import uk.ac.sanger.artemis.io.Location;
+import uk.ac.sanger.artemis.io.Qualifier;
+import uk.ac.sanger.artemis.io.QualifierVector;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+import uk.ac.sanger.artemis.io.StreamQualifier;
+import uk.ac.sanger.artemis.io.QualifierInfo;
+import uk.ac.sanger.artemis.io.EmblStreamFeature;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.Date;
+
+import javax.swing.*;
+
+/**
+ *  FeatureEdit class
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeatureEdit.java,v 1.1 2004-06-09 09:46:38 tjc Exp $
+ **/
+
+public class FeatureEdit
+    extends JFrame
+    implements EntryChangeListener, FeatureChangeListener {
+  /**
+   *  Create a new FeatureEdit object from the given Feature.
+   *  @param entry_group The EntryGroup that contains this Feature.
+   *  @param selection The Selection operate on.  The operations are "Remove
+   *    Range" and "Grab Range"
+   *  @param goto_event_source The object the we will call gotoBase () on.
+   **/
+  public FeatureEdit (final Feature edit_feature,
+                      final EntryGroup entry_group,
+                      final Selection selection,
+                      final GotoEventSource goto_event_source) {
+    super ("Artemis Feature Edit: " + edit_feature.getIDString () +
+           (edit_feature.isReadOnly () ?
+            "  -  (read only)" :
+            ""));
+
+    this.edit_feature = edit_feature;
+    this.edit_entry = edit_feature.getEntry ();
+    this.entry_group = entry_group;
+    this.selection = selection;
+    this.goto_event_source = goto_event_source;
+
+    createComponents ();
+
+    updateFromFeature ();
+
+    orig_qualifier_text = qualifier_text_area.getText ();
+  
+    edit_feature.getEntry ().addEntryChangeListener (this);
+    edit_feature.addFeatureChangeListener (this);
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        stopListening ();
+        dispose ();
+      }
+    });
+
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+
+    qualifier_text_area.requestFocus();
+  }
+
+  /**
+   *  Remove this object as a feature and entry change listener.
+   **/
+  public void stopListening () {
+    getEntry ().removeEntryChangeListener (this);
+    getFeature ().removeFeatureChangeListener (this);
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so we can notify the user if of this component if the
+   *  feature gets deleted.
+   **/
+  public void entryChanged (EntryChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryChangeEvent.FEATURE_DELETED:
+      if (event.getFeature () == edit_feature) {
+        stopListening ();
+        dispose ();
+      }
+      break;
+    default:
+      // do nothing;
+      break;
+    }
+  }
+
+  /**
+   *  Add an ActionListener to the Cancel JButton of this FeatureEdit.
+   **/
+  public void addCancelActionListener (final ActionListener l) {
+    cancel_button.addActionListener (l);
+  }
+
+  /**
+   *  Remove an ActionListener from the Cancel JButton of this FeatureEdit.
+   **/
+  public void removeCancelActionListener (final ActionListener l) {
+    cancel_button.removeActionListener (l);
+  }
+
+  /**
+   *  Add an ActionListener to the Apply JButton of this FeatureEdit.
+   **/
+  public void addApplyActionListener (final ActionListener l) {
+    apply_button.addActionListener (l);
+  }
+
+  /**
+   *  Remove an ActionListener from the Apply JButton of this FeatureEdit.
+   **/
+  public void removeApplyActionListener (final ActionListener l) {
+    apply_button.removeActionListener (l);
+  }
+
+  /**
+   *  Implementation of the FeatureChangeListener interface.  We need to
+   *  listen to feature change events from the Features in this object so that
+   *  we can update the display.
+   *  @param event The change event.
+   **/
+  public void featureChanged (FeatureChangeEvent event) {
+    // re-read the information from the feature
+    switch (event.getType ()) {
+    case FeatureChangeEvent.LOCATION_CHANGED:
+      updateLocation ();
+      break;
+    case FeatureChangeEvent.KEY_CHANGED:
+      updateKey ();
+      break;
+    case FeatureChangeEvent.QUALIFIER_CHANGED:
+      if (qualifier_text_area.getText ().equals (orig_qualifier_text)) {
+        updateFromFeature ();
+      } else {
+        final String message =
+          "warning: the qualifiers have changed outside the editor - " +
+          "view now?";
+
+        final YesNoDialog yes_no_dialog =
+          new YesNoDialog (FeatureEdit.this, message);
+
+        if (yes_no_dialog.getResult ()) {
+          new FeatureViewer (getFeature ());
+        }
+      }
+      break;
+    default:
+      updateFromFeature ();
+      break;
+    }
+  }
+
+
+  /**
+   *  Create all the components for this FeatureEdit component.
+   **/
+  private void createComponents () {
+    qualifier_text_area = new QualifierTextArea ();
+
+    qualifier_text_area.setWrapStyleWord (true);
+
+    key_choice =
+      new KeyChoice (getEntryInformation (),getFeature ().getKey ());
+
+    final JPanel key_and_qualifier_panel = new JPanel ();
+
+    location_text.setBackground (Color.white);
+
+    final JPanel key_panel = new JPanel ();
+    key_panel.add (new JLabel ("Key:"));
+    key_panel.add (key_choice);
+
+    key_and_qualifier_panel.setLayout (new BorderLayout ());
+    key_and_qualifier_panel.add (key_panel, "West");
+
+    qualifier_choice = new QualifierChoice (getEntryInformation (),
+                                            key_choice.getSelectedItem (),
+                                            null);
+
+    final JPanel qualifier_panel = new JPanel ();
+
+    final JButton qualifier_add_button = new JButton ("Add Qualifier:");
+
+    qualifier_panel.add (qualifier_add_button);
+    qualifier_panel.add (qualifier_choice);
+
+    key_and_qualifier_panel.add (qualifier_panel, "East");
+
+    key_choice.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent _) {
+        qualifier_choice.setKey (key_choice.getSelectedItem ());
+      }
+    });
+
+    qualifier_add_button.addActionListener (new ActionListener () {
+      public void actionPerformed(ActionEvent e) {
+        final String qualifier_name =
+          (String) qualifier_choice.getSelectedItem ();
+
+        QualifierInfo qualifier_info =
+          getEntryInformation ().getQualifierInfo (qualifier_name);
+
+        if (qualifier_info == null) {
+          qualifier_info =
+            new QualifierInfo (qualifier_name,
+                               QualifierInfo.OPTIONAL_QUOTED_TEXT,
+                               null, null, false);
+        }
+
+        qualifier_text_area.append ("/" + qualifier_name);
+
+        switch (qualifier_info.getType ()) {
+        case QualifierInfo.QUOTED_TEXT:
+          if (qualifier_name.equals ("GO")) {
+            // special case for /GO
+            final java.util.Calendar calendar =
+              java.util.Calendar.getInstance ();
+            
+            final java.util.Date current_time =
+              calendar.getTime ();
+            
+            final java.text.SimpleDateFormat date_format =
+              new java.text.SimpleDateFormat("yyyyMMdd");
+            
+            final StringBuffer result_buffer = new StringBuffer ();
+            
+            date_format.format(current_time, result_buffer,
+                               new java.text.FieldPosition(java.text.DateFormat.DATE_FIELD));
+            
+            final String go_string =
+              "aspect=; term=; GOid=GO:; "+
+              "evidence=ISS; db_xref=GOC:unpublished; " +
+              "with=UNIPROT:; date=" + result_buffer;
+            qualifier_text_area.append ("=\"" + go_string + "\"");
+          } else {
+            qualifier_text_area.append ("=\"\"");
+          }
+          break;
+
+        case QualifierInfo.NO_VALUE:
+        case QualifierInfo.OPTIONAL_QUOTED_TEXT:
+          break;
+
+        default:
+          qualifier_text_area.append ("=");
+        }
+
+        qualifier_text_area.append ("\n");
+      }
+    });
+
+    final JPanel middle_panel = new JPanel ();
+    middle_panel.setLayout (new BorderLayout ());
+
+    final JPanel lower_panel = new JPanel ();
+    lower_panel.setLayout (new BorderLayout ());
+
+    final JPanel outer_location_button_panel = new JPanel ();
+    lower_panel.add (outer_location_button_panel, "North");
+    outer_location_button_panel.setLayout (new BorderLayout ());
+
+    final JPanel location_button_panel = new JPanel ();
+    outer_location_button_panel.add (location_button_panel, "West");
+
+    final JPanel location_panel = new JPanel ();
+    location_panel.setLayout (new BorderLayout ());
+    location_panel.add (new JLabel ("location: "), "West");
+    location_panel.add (location_text, "Center");
+
+    final JButton complement_button = new JButton ("Complement");
+    location_button_panel.add (complement_button);
+    complement_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        complementLocation ();
+      }
+    });
+
+    final JButton grab_button = new JButton ("Grab Range");
+    location_button_panel.add (grab_button);
+    grab_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        grabSelectedRange ();
+      }
+    });
+
+    final JButton remove_button = new JButton ("Remove Range");
+    location_button_panel.add (remove_button);
+    remove_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        removeSelectedRange ();
+      }
+    });
+
+    final JButton goto_button = new JButton ("Goto Feature");
+    location_button_panel.add (goto_button);
+    goto_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        goto_event_source.gotoBase (getFeature ().getFirstBaseMarker ());
+      }
+    });
+
+    final JButton select_button = new JButton ("Select Feature");
+    location_button_panel.add (select_button);
+    select_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        getSelection ().set (getFeature ());
+      }
+    });
+
+    final boolean sanger_options =
+      Options.getOptions ().getPropertyTruthValue ("sanger_options");
+
+    if (sanger_options) {
+      // a PSU only hack 
+      final JButton tidy_button = new JButton ("Tidy");
+      location_button_panel.add (tidy_button);
+      tidy_button.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent e) {
+          try {
+            tidy ();
+          } catch (QualifierParseException exception) {
+            final String error_string = exception.getMessage ();
+            new MessageDialog (FeatureEdit.this,
+                               "Cannot tidy - qualifier error: " +
+                               error_string);
+          }
+        }
+      });
+    }
+
+    if (Options.getOptions ().getProperty ("external_editor") != null) {
+      final JButton external_fasta_edit_button = new JButton ("MESS/FASTA");
+      location_button_panel.add (external_fasta_edit_button);
+      external_fasta_edit_button.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent e) {
+          try {
+            if (getFeature ().getQualifierByName ("fasta_file") != null) {
+              final String DEFAULT_MAX_EUK_FASTA_HITS = "10";
+
+              final String max_fasta_hits;
+
+              final String max_fasta_hits_from_options =
+                Options.getOptions ().getProperty ("mess_fasta_hits");
+
+              if (Options.getOptions ().isEukaryoticMode ()) {
+                if (max_fasta_hits_from_options == null) {
+                  max_fasta_hits = DEFAULT_MAX_EUK_FASTA_HITS;
+                } else {
+                  max_fasta_hits = max_fasta_hits_from_options;
+                }
+
+                externalEdit (new String [] {
+                  "-fasta",
+                  "-maxhits",
+                  max_fasta_hits,
+                  "-euk"
+                });
+              } else {
+                if (max_fasta_hits_from_options == null) {
+                  externalEdit (new String [] {
+                    "-fasta"
+                  });
+                } else {
+                  externalEdit (new String [] {
+                    "-fasta",
+                    "-maxhits",
+                    max_fasta_hits_from_options
+                  });
+
+                }
+              }
+              return;
+            }
+          } catch (InvalidRelationException _) {
+            // fall through
+          }
+          new MessageDialog (FeatureEdit.this,
+                             "nothing to edit - no /fasta_file qualifier");
+        }
+      });
+
+      final JButton external_blastp_edit_button = new JButton ("MESS/BLASTP");
+      location_button_panel.add (external_blastp_edit_button);
+      external_blastp_edit_button.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent e) {
+          try {
+            if (getFeature ().getQualifierByName ("blastp_file") != null) {
+              final String DEFAULT_MAX_BLASTP_HITS = "10";
+
+              final String max_blastp_hits;
+
+              final String max_blastp_hits_from_options =
+                Options.getOptions ().getProperty ("mess_blastp_hits");
+
+              if (max_blastp_hits_from_options == null) {
+                max_blastp_hits = DEFAULT_MAX_BLASTP_HITS;
+              } else {
+                max_blastp_hits = max_blastp_hits_from_options;
+              }
+
+              if (Options.getOptions ().isEukaryoticMode ()) {
+                externalEdit (new String [] {
+                  "-blastp",
+                  "-maxhits",
+                  max_blastp_hits,
+                  "-euk"
+                });
+              } else {
+                externalEdit (new String [] {
+                  "-blastp",
+                  "-maxhits",
+                  max_blastp_hits
+                });
+              }
+              return;
+            }
+          } catch (InvalidRelationException _) {
+            // fall through
+          }
+          new MessageDialog (FeatureEdit.this,
+                             "nothing to edit - no /blastp_file qualifier");
+        }
+      });
+
+      final JButton external_go_edit_button = new JButton ("MESS/GO");
+      location_button_panel.add (external_go_edit_button);
+      external_go_edit_button.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent e) {
+          try {
+            if (getFeature ().getQualifierByName ("blastp+go_file") != null) {
+              final String DEFAULT_MAX_GO_BLAST_HITS = "10";
+
+              final String max_go_blast_hits;
+
+              final String max_go_blast_hits_from_options =
+                Options.getOptions ().getProperty ("mess_blast_go_hits");
+
+              if (max_go_blast_hits_from_options == null) {
+                max_go_blast_hits = DEFAULT_MAX_GO_BLAST_HITS;
+              } else {
+                max_go_blast_hits = max_go_blast_hits_from_options;
+              }
+
+              if (Options.getOptions ().isEukaryoticMode ()) {
+                externalEdit (new String [] {
+                  "-blastp+go",
+                  "-maxhits",
+                  max_go_blast_hits,
+                  "-euk"
+                });
+              } else {
+                externalEdit (new String [] {
+                  "-blastp+go",
+                  "-maxhits",
+                  max_go_blast_hits
+                });
+              }
+              return;
+            }
+          } catch (InvalidRelationException _) {
+            // fall through
+          }
+          new MessageDialog (FeatureEdit.this,
+                             "nothing to edit - no /blastp+go_file qualifier");
+        }
+      });
+    }
+
+    middle_panel.add (location_panel, "North");
+
+    getContentPane ().add (key_and_qualifier_panel, "North");
+
+    cancel_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        stopListening ();
+        dispose ();
+      }
+    });
+
+    if (!getFeature ().isReadOnly ()) {
+      ok_button.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent e) {
+          if (setFeature ()) {
+            stopListening ();
+            dispose ();
+          }
+        }
+      });
+
+      apply_button.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent e) {
+          setFeature ();
+        }
+      });
+    }
+
+    final FlowLayout flow_layout =
+      new FlowLayout (FlowLayout.CENTER, 18, 5);
+
+    final JPanel ok_cancel_update_panel = new JPanel (flow_layout);
+
+    if (!getFeature ().isReadOnly ()) {
+      ok_cancel_update_panel.add (ok_button);
+    }
+
+    ok_cancel_update_panel.add (cancel_button);
+
+    if (!getFeature ().isReadOnly ()) {
+      ok_cancel_update_panel.add (apply_button);
+    }
+
+    getContentPane ().add (ok_cancel_update_panel, "South");
+
+    middle_panel.add (lower_panel, "Center");
+    lower_panel.add (new JScrollPane (qualifier_text_area), "Center");
+
+    getContentPane ().add (middle_panel, "Center");
+  }
+
+  /**
+   *  Read the key, location and qualifier information from the feature and
+   *  update the components.
+   **/
+  private void updateFromFeature () {
+    datestamp = getFeature ().getDatestamp ();
+
+    updateKey ();
+
+    updateLocation ();
+
+    updateQualifiers ();
+  }
+
+  /**
+   *  Read the location from the feature and update the location field.
+   **/
+  private void updateLocation () {
+    location_text.setText (getFeature ().getLocation ().toStringShort ());
+  }
+
+  /**
+   *   Complement the current location_text.
+   **/
+  private void complementLocation () {
+    if (rationalizeLocation ()) {
+      if (location_text.getText ().startsWith ("complement(")) {
+        final String new_text = location_text.getText ().substring (11);
+        if (new_text.endsWith (")")) {
+          final String new_text2 =
+            new_text.substring (0, new_text.length () - 1);
+          location_text.setText (new_text2);
+        } else {
+          location_text.setText (new_text);
+        }
+      } else {
+        final String new_text = location_text.getText ();
+
+        location_text.setText ("complement(" + new_text + ")");
+      }
+    } else {
+      new MessageDialog (this, "complement failed - " +
+                         "current location cannot be parsed");
+    }
+  }
+
+  /**
+   *  Tidy the qualifiers by removing any indication that the annotation has
+   *  been transferred from another gene.  Remove the "transferred_" part of
+   *  all qualifier names and the "[[FROM sc_001234 abc1]]" bit of each
+   *  corresponding qualifier value.
+   **/
+  private void tidy ()
+      throws QualifierParseException {
+    final StringBuffer buffer = new StringBuffer ();
+
+    final QualifierVector qualifiers =
+      qualifier_text_area.getParsedQualifiers (getEntryInformation ());
+    
+    for (int qualifier_index = 0 ;
+         qualifier_index < qualifiers.size () ;
+         ++qualifier_index) {
+      final Qualifier this_qualifier = qualifiers.elementAt (qualifier_index);
+      
+      final QualifierInfo qualifier_info =
+        getEntryInformation ().getQualifierInfo (this_qualifier.getName ());
+      
+      final StringVector qualifier_strings =
+          StreamQualifier.toStringVector (qualifier_info, this_qualifier);
+      
+      for (int value_index = 0 ;
+           value_index < qualifier_strings.size () ;
+           ++value_index) {
+
+        final String qualifier_string =
+          qualifier_strings.elementAt (value_index);
+
+        buffer.append (tidyHelper (qualifier_string) + "\n");
+      }
+    }
+
+    qualifier_text_area.setText (buffer.toString ());
+  }
+
+  /**
+   *  Perform the action of tidy() on the String version of one qualifier.
+   **/
+  private String tidyHelper (final String qualifier_string) {
+    final String temp_string;
+
+    if (qualifier_string.startsWith ("/transferred_")) {
+      temp_string = "/" + qualifier_string.substring (13);
+    } else {
+      temp_string = qualifier_string;
+    }
+
+    final int start_index = temp_string.indexOf ("<<FROM ");
+    final int end_index = temp_string.indexOf (">>");
+
+    if (start_index != -1 && end_index != -1) {
+      if (temp_string.length () > end_index + 2 &&
+          temp_string.charAt (end_index + 2) == ' ') {
+        return temp_string.substring (0, start_index) +
+          temp_string.substring (end_index + 3);
+      } else {
+        return temp_string.substring (0, start_index) +
+          temp_string.substring (end_index + 2);
+      }
+    } else {
+      return temp_string;
+    }
+  }
+
+  /**
+   *  Add the currently selected range to location_text.
+   **/
+  private void grabSelectedRange () {
+    if (!rationalizeLocation ()) {
+      new MessageDialog (this,
+                         "grab failed - current location cannot be parsed");
+      return;
+    }
+
+    final Range selected_range = getSelection ().getSelectionRange ();
+
+    if (selected_range == null) {
+      new MessageDialog (this, "grab failed - nothing is selected");
+      return;
+    }
+
+    // save it in case it gets mangled
+    final String old_location_text = location_text.getText ();
+
+    if (old_location_text.endsWith ("))")) {
+      final String new_text =
+        old_location_text.substring (0, old_location_text.length () - 2);
+
+      location_text.setText (new_text + "," + selected_range.getStart () +
+                             ".." + selected_range.getEnd () + "))");
+    } else {
+      if (old_location_text.endsWith (")")) {
+        final String new_text =
+          old_location_text.substring (0, old_location_text.length () - 1);
+
+        location_text.setText (new_text + "," + selected_range.getStart () +
+                               ".." + selected_range.getEnd () + ")");
+      } else {
+        location_text.setText (old_location_text + "," +
+                               selected_range.getStart () +
+                               ".." + selected_range.getEnd ());
+      }
+    }
+
+    if (!rationalizeLocation ()) {
+      location_text.setText (old_location_text);
+      new MessageDialog (this,
+                         "grab failed - location cannot be parsed after " +
+                         "grabbing");
+    }
+  }
+
+  /**
+   *  Remove the currently selected range of bases from location_text.
+   **/
+  private void removeSelectedRange () {
+    if (!rationalizeLocation ()) {
+      new MessageDialog (this,
+                         "grab failed - current location cannot be parsed");
+      return;
+    }
+
+    final MarkerRange selected_marker_range =
+      getSelection ().getMarkerRange ();
+
+    if (selected_marker_range == null) {
+      new MessageDialog (this, "remove range failed - no bases are selected");
+      return;
+    }
+
+    final Range selected_range = selected_marker_range.getRawRange ();
+
+    if (selected_marker_range.getStrand () != getFeature ().getStrand ()) {
+      new MessageDialog (this, "remove range failed - you need to select " +
+                         "some bases on the other strand");
+      return;
+    }
+
+    final Location location;
+
+    try {
+      location = new Location (location_text.getText ());
+    } catch (LocationParseException e) {
+      // this shouldn't happen because we called rationalizeLocation ()
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+
+    final Range location_total_range = location.getTotalRange ();
+
+    if (!selected_range.overlaps (location_total_range)) {
+      new MessageDialog (this, "remove range failed - the range you " +
+                         "selected does not overlap the feature");
+      return;
+    }
+
+    if (selected_range.contains (location_total_range)) {
+      new MessageDialog (this, "remove range failed - the range you " +
+                         "selected overlaps the whole feature");
+      return;
+    }
+
+    final RangeVector location_ranges = location.getRanges ();
+    final boolean location_is_complemented = location.isComplement ();
+
+    final RangeVector new_ranges = new RangeVector ();
+
+    // if the selected_range completely covers a range remove the
+    // range. otherwise if the selected_range is completely within one of the
+    // ranges two new ranges are created.  if the selected_range is not
+    // completely contained then the appropriate end of the range is truncated
+    for (int i = 0 ; i < location_ranges.size () ; ++i) {
+      final Range this_range = location_ranges.elementAt (i);
+
+      if (selected_range.overlaps (this_range)) {
+        try {
+          if (this_range.contains (selected_range) &&
+              this_range.getStart () != selected_range.getStart () &&
+              this_range.getEnd () != selected_range.getEnd ()) {
+            // chop a piece out of the middle and make two new ranges
+            final Range new_start_range =
+              this_range.change (selected_range.getEnd () + 1,
+                                 this_range.getEnd ());
+            new_ranges.add (new_start_range);
+            final Range new_end_range =
+              this_range.change (this_range.getStart (),
+                                 selected_range.getStart () - 1);
+            new_ranges.add (new_end_range);
+          } else {
+            if (selected_range.contains (this_range)) {
+              // delete (ie. don't copy) the range
+            } else {
+              if (this_range.getStart () < selected_range.getStart ()) {
+                // truncate the end of the range
+                final Range new_start_range =
+                  this_range.change (this_range.getStart (),
+                                     selected_range.getStart () - 1);
+                new_ranges.add (new_start_range);
+              } else {
+                if (this_range.getEnd () > selected_range.getEnd ()) {
+                  // truncate the start of the range
+                  final Range new_end_range =
+                    this_range.change (selected_range.getEnd () + 1,
+                                       this_range.getEnd ());
+                  new_ranges.add (new_end_range);
+
+                } else {
+                  throw new Error ("internal error - can't remove range");
+                }
+              }
+            }
+          }
+        } catch (OutOfRangeException e) {
+          throw new Error ("internal error - unexpected exception: " + e);
+        }
+      } else {
+        // copy it unchanged
+        new_ranges.add (this_range);
+      }
+    }
+
+    final Location new_location =
+      new Location (new_ranges, location_is_complemented);
+
+    location_text.setText (new_location.toStringShort ());
+  }
+
+  /**
+   *  Attempt to parse the current location_text as a Location.  If it can be
+   *  parsed it will be canonicalized (ie. the complement, if any, will be
+   *  outermost).  Returns true if and only if the location_text could be
+   *  parsed.
+   **/
+  private boolean rationalizeLocation () {
+    try {
+      final Location location = new Location (location_text.getText ());
+
+      location_text.setText (location.toStringShort ());
+
+      return true;
+    } catch (LocationParseException e) {
+      return false;
+    }
+  }
+
+  /**
+   *  Used for getting the current time/date in externalEdit ().
+   **/
+  private static java.util.Calendar calendar =
+    java.util.Calendar.getInstance ();
+
+  /**
+   *  Edit the qualifiers of this Feature in an external editor.  The
+   *  qualifiers will be set when the editor finishes.  This method works by
+   *  writing the qualifiers to a temporary file and the sequence of the
+   *  feature to a different file.
+   *  @param editor_extra_args Extra arguments to pass to the editor.  null
+   *    means there are no extra args.
+   **/
+  private void externalEdit (final String [] editor_extra_args) {
+    try {
+      final String pre_edit_text = qualifier_text_area.getText ();
+
+      // write to a temporary file
+      final java.util.Date current_time = calendar.getTime ();
+
+      final String temp_file_name =
+        "/tmp/artemis_temp." + current_time.getTime ();
+
+      final File temp_file = new File (temp_file_name);
+
+      final FileWriter out_writer = new FileWriter (temp_file);
+      final PrintWriter print_writer = new PrintWriter (out_writer);
+
+      print_writer.write (qualifier_text_area.getText ());
+
+      print_writer.close ();
+      out_writer.close ();
+
+
+      final File sequence_temp_file = new File (temp_file_name + ".seq");
+
+      final FileWriter sequence_out_writer =
+        new FileWriter (sequence_temp_file);
+      final PrintWriter sequence_print_writer =
+        new PrintWriter (sequence_out_writer);
+
+      getFeature ().writeBasesOfFeature (sequence_print_writer);
+
+      sequence_print_writer.close ();
+      sequence_out_writer.close ();
+
+
+      final String editor_path =
+        Options.getOptions ().getProperty ("external_editor");
+
+      final String [] process_args;
+
+      if (editor_extra_args == null) {
+        process_args = new String [1];
+        process_args[0] = temp_file.getCanonicalPath ();
+      } else {
+        process_args = new String [editor_extra_args.length + 1];
+        System.arraycopy (editor_extra_args, 0, process_args, 0,
+                          editor_extra_args.length);
+        process_args[process_args.length - 1] = temp_file.getCanonicalPath ();
+      }
+
+
+      final Process process =
+        ExternalProgram.startProgram (editor_path, process_args);
+
+      final ProcessWatcher process_watcher =
+        new ProcessWatcher (process, "editor", false);
+
+      final Thread watcher_thread = new Thread (process_watcher);
+
+      watcher_thread.start ();
+
+      final ProcessWatcherListener listener =
+        new ProcessWatcherListener () {
+          public void processFinished (final ProcessWatcherEvent event) {
+            try {
+              final FileReader file_reader = new FileReader (temp_file);
+
+              final BufferedReader buffered_reader =
+                new BufferedReader (file_reader);
+
+              final StringBuffer buffer = new StringBuffer ();
+
+              while (true) {
+                final String line = buffered_reader.readLine ();
+
+                if (line == null) {
+                  final String current_qualifier_text =
+                    qualifier_text_area.getText ();
+                  
+                  if (!current_qualifier_text.equals (pre_edit_text)) {
+                    final String message =
+                      "the qualifiers have changed - apply changes from the " +
+                      "external editor?";
+
+                    final YesNoDialog yes_no_dialog =
+                      new YesNoDialog (FeatureEdit.this, message);
+
+                    if (!yes_no_dialog.getResult ()) {
+                      return;
+                    }
+                  }
+
+                  qualifier_text_area.setText (buffer.toString ());
+
+                  temp_file.delete ();
+                  sequence_temp_file.delete ();
+
+                  return;
+                } else {
+                  buffer.append (line + "\n");
+                }
+              }
+            } catch (IOException e) {
+              new MessageDialog (FeatureEdit.this, "an error occured while " +
+                                 "reading from the editor: " + e);
+            }
+          }
+        };
+
+      process_watcher.addProcessWatcherListener (listener);
+    } catch (IOException e) {
+      new MessageDialog (this, "error while starting editor: " + e);
+    } catch (ExternalProgramException e) {
+      new MessageDialog (this, "error while starting editor: " + e);
+    }
+  }
+
+  /**
+   *  Read the qualifiers from the feature and update the qualifier JTextArea.
+   **/
+  private void updateQualifiers () {
+    qualifier_text_area.setText (getQualifierString ());
+  }
+
+  /**
+   *  Return a string containing one qualifier per line.  These are the
+   *  original qualifiers, not the qualifiers from the qualifier_text_area.
+   **/
+  private String getQualifierString () {
+    final StringBuffer buffer = new StringBuffer ();
+
+    final QualifierVector qualifiers = getFeature ().getQualifiers ();
+
+    for (int qualifier_index = 0 ;
+         qualifier_index < qualifiers.size () ;
+         ++qualifier_index) {
+      final Qualifier this_qualifier = qualifiers.elementAt (qualifier_index);
+
+      final QualifierInfo qualifier_info =
+        getEntryInformation ().getQualifierInfo (this_qualifier.getName ());
+
+      final StringVector qualifier_strings =
+        StreamQualifier.toStringVector (qualifier_info, this_qualifier);
+
+      for (int value_index = 0 ;
+           value_index < qualifier_strings.size () ;
+           ++value_index) {
+
+        final String qualifier_string =
+          qualifier_strings.elementAt (value_index);
+
+        buffer.append (qualifier_string + "\n");
+      }
+    }
+
+    return buffer.toString ();
+  }
+
+  /**
+   *  Set the key, location and qualifiers of the feature to be the same as
+   *  what values currently shown in the components.
+   *  @return true if and only if action succeeds.  It may fail because of an
+   *    illegal location or qualifier, in which case a message will be
+   *    displayed before returning.
+   **/
+  private boolean setFeature () {
+    final Key key = key_choice.getSelectedItem ();
+
+    final KeyVector possible_keys = getEntryInformation ().getValidKeys ();
+
+    if (possible_keys != null && !possible_keys.contains (key)) {
+      final YesNoDialog dialog =
+        new YesNoDialog (this, "Add this new key: " + key + "?");
+
+      if (dialog.getResult ()) {
+        // yes
+        getEntryInformation ().addKey (key);
+      } else {
+        // no
+        return false;
+      }
+    }
+
+    final Location location;
+
+    try {
+      location = new Location (location_text.getText ());
+    } catch (LocationParseException exception) {
+      final String error_string = exception.getMessage ();
+      System.out.println (error_string);
+      new MessageDialog (this,
+                         "Cannot apply changes because of location error: " +
+                         error_string);
+
+      return false;
+    }
+
+
+    final QualifierVector qualifiers;
+
+    try {
+      qualifiers =
+        qualifier_text_area.getParsedQualifiers (getEntryInformation ());
+    } catch (QualifierParseException exception) {
+      final String error_string = exception.getMessage ();
+      System.out.println (error_string);
+      new MessageDialog (this,
+                         "Cannot apply changes because of a qualifier " +
+                         "error: " + error_string);
+
+      return false;
+    }
+
+
+    try {
+      entry_group.getActionController ().startAction ();
+
+      try {
+        getFeature ().set (datestamp, key, location, qualifiers);
+      } catch (OutOfDateException e) {
+        final YesNoDialog dialog =
+          new YesNoDialog (this,
+                           "the feature has changed since the edit " +
+                           "window was opened, continue?");
+        if (dialog.getResult ()) {
+          // yes - ignore the datestamp
+          getFeature ().set (key, location, qualifiers);
+        } else {
+          // no
+          return false;
+        }
+      }
+    } catch (EntryInformationException e) {
+      final String error_string = e.getMessage ();
+      new MessageDialog (this, "Cannot apply changes: " + error_string);
+
+      return false;
+    } catch (OutOfRangeException e) {
+      new MessageDialog (this,
+                         "Cannot apply changes - the location is out of " +
+                         "range for this sequence");
+      return false;
+    } catch (ReadOnlyException e) {
+      new MessageDialog (this,
+                         "Cannot apply changes - the feature is " +
+                         "read only");
+      return false;
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+
+    dribble ();
+
+    return true;
+  }
+
+  /**
+   *  Return the Feature we are editing as passed to the constructor.
+   **/
+  public Feature getFeature () {
+    return edit_feature;
+  }
+
+  /**
+   *  On Unix machines this method will append the text of the feature to a
+   *  file in a current directory called .dribble + <the entry name>
+   **/
+  private void dribble () {
+    if (!Options.isUnixHost ()) {
+      return;
+    }
+
+    final String dribble_file_name;
+
+    if (getEntry ().getName () != null) {
+      dribble_file_name = ".dribble." + getEntry ().getName ();
+    } else {
+      dribble_file_name = ".dribble.no_name";
+    }
+
+    try {
+      final Writer writer = new FileWriter (dribble_file_name, true);
+      getFeature ().writeNative (writer);
+      writer.flush ();
+      writer.close ();
+    } catch (IOException e) {
+      System.err.println ("IO exception while accessing " + dribble_file_name +
+                          ": " + e.getMessage ());
+    }
+  }
+
+  /**
+   *  Return the Entry that contains the Feature this object is displaying.
+   **/
+  private Entry getEntry () {
+    return edit_entry;
+  }
+
+  /**
+   *  Read the key from the feature and update the key chooser.
+   **/
+  private void updateKey () {
+    final Key feature_key = getFeature ().getKey ();
+
+    key_choice.setKey (feature_key);
+  }
+
+  /**
+   *  Return the EntryInformation object of the entry containing the feature.
+   **/
+  public EntryInformation getEntryInformation () {
+    return getEntry ().getEntryInformation ();
+  }
+
+  /**
+   *  Return the Selection that was passed to the constructor.
+   **/
+  private Selection getSelection () {
+    return selection;
+  }
+
+  /**
+   *  The choice of feature keys - created in createComponents().
+   **/
+  private KeyChoice key_choice;
+
+  /**
+   *  The choice of qualifiers - created in createComponents().
+   **/
+  private QualifierChoice qualifier_choice = null;
+
+  private final static int LOCATION_TEXT_WIDTH = 80;
+
+  /**
+   *  The location text - set by updateLocation().
+   **/
+  private JTextField location_text = new JTextField (LOCATION_TEXT_WIDTH);
+
+  private JButton add_qualifier_button = new JButton ("Add qualifier");
+
+  /**
+   *  When pressed - apply changes and dispose of the component.
+   **/
+  private JButton ok_button = new JButton ("OK");
+
+  /**
+   *  When pressed - discard changes and dispose of the component.
+   **/
+  private JButton cancel_button = new JButton ("Cancel");
+
+  /**
+   *  When pressed - apply changes and keep the component open.
+   **/
+  private JButton apply_button = new JButton ("Apply");
+
+  /**
+   *  Edit area for qualifiers - created by createComponents().
+   **/
+  private QualifierTextArea qualifier_text_area;
+
+  /**
+   *  The Feature this object is displaying.
+   **/
+  private Feature edit_feature;
+
+  /**
+   *  The GotoEventSource that was passed to the constructor - used for the
+   *  "Goto Feature" button.
+   **/
+  private GotoEventSource goto_event_source;
+
+  /**
+   *  The Entry that contains the Feature this object is displaying.
+   **/
+  private Entry edit_entry;
+
+  /**
+   *  The EntryGroup that contains this Feature (passed to the constructor).
+   **/
+  private EntryGroup entry_group;
+
+  /**
+   *  The datestamp of the RWCorbaFeature when updateFromFeature () was last
+   *  called or null if this is not a RWCorbaFeature.
+   **/
+  private Date datestamp = null;
+
+  /**
+   *  The Selection that was passed to the constructor.
+   **/
+  private Selection selection;
+
+  /**
+   *  The contents of the QualifierTextArea before the user edits anything.
+   *  This is used to work out if anything has changed since the creation of
+   *  the FeatureEdit.
+   **/
+  final String orig_qualifier_text;
+}
diff --git a/uk/ac/sanger/artemis/components/FeatureInfo.java b/uk/ac/sanger/artemis/components/FeatureInfo.java
new file mode 100644
index 000000000..3d8e12574
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeatureInfo.java
@@ -0,0 +1,658 @@
+/* FeatureInfo.java
+ *
+ * created: Sat Dec 19 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1998,1999,2000,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeatureInfo.java,v 1.1 2004-06-09 09:46:39 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.plot.*;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This component displays a summary of the statistical information about one
+ *  feature (such as correlation score, codon usage and gc content).
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeatureInfo.java,v 1.1 2004-06-09 09:46:39 tjc Exp $
+ **/
+public class FeatureInfo extends JFrame
+    implements EntryChangeListener, FeatureChangeListener {
+  /**
+   *  Create a new FeatureInfo component.
+   *  @param feature The Feature that this component is showing information
+   *    about.
+   **/
+  public FeatureInfo (final Feature feature,
+                      final CodonUsageAlgorithm codon_usage_algorithm) {
+    super ("Feature infomation: " + feature.getIDString ());
+
+    this.feature = feature;
+    this.entry = feature.getEntry ();
+    this.codon_usage_algorithm = codon_usage_algorithm;
+
+    setBackground (new Color (210, 210, 210));
+    getContentPane ().setBackground (new Color (210, 210, 210));
+
+    codon_info_areas = new JTextArea [4][4];
+    base_count_info_areas = new JTextArea [4];
+
+    final JPanel centre_panel = new JPanel ();
+    centre_panel.setLayout (new BorderLayout ());
+
+    makeCountList ();
+    getContentPane ().add (aa_count_panel, "West");
+
+    makeMiscInfogrid ();
+    centre_panel.add (misc_info_panel, "South");
+
+    makeCodonInfogrid ();
+    centre_panel.add (codon_info_panel, "Center");
+
+    getContentPane ().add (centre_panel, "Center");
+
+    button_panel = new JPanel ();
+    final JButton close_button = new JButton ("Close");
+
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        stopListening ();
+        FeatureInfo.this.dispose ();
+      }
+    });
+
+    button_panel.add (close_button);
+
+    getContentPane ().add (button_panel, "South");
+
+    updateComponents ();
+
+    getFeature ().addFeatureChangeListener (this);
+    getFeature ().getEntry ().addEntryChangeListener (this);
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        stopListening ();
+        FeatureInfo.this.dispose ();
+      }
+    });
+
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+
+    setVisible (true);
+  }
+
+  /**
+   *  Remove this object as a feature change and entry change listener.
+   **/
+  private void stopListening () {
+    getEntry ().removeEntryChangeListener (this);
+    getFeature ().removeFeatureChangeListener (this);
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so we can notify the user if of this component if the
+   *  feature gets deleted.
+   **/
+  public void entryChanged (EntryChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryChangeEvent.FEATURE_DELETED:
+      if (event.getFeature () == getFeature ()) {
+        stopListening ();
+        dispose ();
+      }
+      break;
+    default:
+      // do nothing;
+      break;
+    }
+  }
+
+  /**
+   *  Implementation of the FeatureChangeListener interface.  We need to
+   *  listen to feature change events from the Features in this object.
+   *  @param event The change event.
+   **/
+  public void featureChanged (FeatureChangeEvent event) {
+    updateComponents ();
+  }
+
+  /**
+   *  Create all the labels in the misc_info_panel.
+   **/
+  private void makeMiscInfogrid () {
+    misc_info_panel = new JPanel ();
+
+    GridLayout grid = new GridLayout (0, 1);
+    misc_info_panel.setLayout (grid);
+
+    molecular_weight_label = new JLabel ();
+    misc_info_panel.add (molecular_weight_label);
+
+    correlation_scores_label = new JLabel ();
+    misc_info_panel.add (correlation_scores_label);
+
+    if (codon_usage_algorithm != null) {
+      usage_scores_label = new JLabel ();
+      misc_info_panel.add (usage_scores_label);
+    }
+  }
+
+  /**
+   *  Create a List containing a count of the occurrences of each codon in the
+   *  feature.
+   **/
+  private void makeCountList () {
+    aa_count_panel = new JPanel ();
+
+    final JPanel aa_count_sub_panel1 = new JPanel ();
+    aa_count_sub_panel1.setLayout (new GridLayout (0, 1));
+    aa_count_panel.add (aa_count_sub_panel1);
+
+    final JPanel aa_count_sub_panel2 = new JPanel ();
+    aa_count_sub_panel2.setLayout (new GridLayout (0, 1));
+    aa_count_panel.add (aa_count_sub_panel2);
+
+    aa_count_list = new JLabel [AminoAcidSequence.symbol_count];
+
+    for (int i = 0 ; i < AminoAcidSequence.symbol_count / 2 ; ++i) {
+      aa_count_list[i] = new JLabel ();
+      aa_count_sub_panel1.add (aa_count_list[i]);
+    }
+
+    // add a blank label so that the columns balance
+    aa_count_sub_panel1.add (new JLabel (""));
+
+    for (int i = AminoAcidSequence.symbol_count / 2 ;
+         i < AminoAcidSequence.symbol_count ;
+         ++i) {
+      aa_count_list[i] = new JLabel ();
+      aa_count_sub_panel2.add (aa_count_list[i]);
+    }
+  }
+
+  /**
+   *  Create all the labels in the codon_info_panel.
+   **/
+  private void makeCodonInfogrid () {
+    codon_info_panel = new JPanel ();
+
+    GridBagLayout gridbag = new GridBagLayout();
+
+    codon_info_panel.setLayout (gridbag);
+
+    GridBagConstraints c = new GridBagConstraints();
+
+    final int GRID_WIDTH = 7;
+
+    c.gridwidth = GRID_WIDTH;
+    c.anchor = GridBagConstraints.NORTH;
+    c.insets = new Insets (1, 1, 1, 1);
+    c.fill = GridBagConstraints.NONE;
+
+
+    // create the first row with some dummy labels to pad it out
+
+    // the first row has the bases T,C,A,G as column titles
+    final JLabel dummy_label1 = new JLabel ("");
+    gridbag.setConstraints (dummy_label1, c);
+    codon_info_panel.add (dummy_label1);
+
+    final JLabel dummy_label2 = new JLabel ("");
+    gridbag.setConstraints (dummy_label2, c);
+    codon_info_panel.add (dummy_label2);
+
+    for (int i = 0 ; i < Bases.letter_index.length ; ++i) {
+      final JLabel new_label =
+        new JLabel (String.valueOf (Bases.letter_index[i]).toUpperCase ());
+      gridbag.setConstraints (new_label, c);
+      codon_info_panel.add (new_label);
+    }
+
+    final JLabel dummy_label3 = new JLabel ("");
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (dummy_label3, c);
+    codon_info_panel.add (dummy_label3);
+
+    c.anchor = GridBagConstraints.WEST;
+    c.gridwidth = GRID_WIDTH;
+
+    final int ROWS = 4;
+
+    // create the main rows
+
+    for (int row = 0 ; row < ROWS ; ++row) {
+      {
+        final JLabel new_label =
+          new JLabel (String.valueOf (Bases.letter_index[row]).toUpperCase ());
+
+        new_label.setBorder (new javax.swing.border.EmptyBorder (1,1,1,1));
+        gridbag.setConstraints (new_label, c);
+        codon_info_panel.add (new_label);
+      }
+
+      {
+        base_count_info_areas[row] = new JTextArea (5, 15);
+        base_count_info_areas[row].setEditable (false);
+
+        final JScrollPane scroll_pane =
+          new JScrollPane (base_count_info_areas[row]);
+        gridbag.setConstraints (scroll_pane, c);
+        codon_info_panel.add (scroll_pane);
+
+        final Color parent_background = getBackground ();
+        base_count_info_areas[row].setBackground (parent_background);
+      }
+
+      for (int column = 2 ; column < GRID_WIDTH - 1 ; ++column) {
+
+        final JTextArea new_text = new JTextArea (5, 12);
+        new_text.setText ("r: " + row + " c: " + column);
+
+        new_text.setEditable (false);
+
+        new_text.setBackground (Color.white);
+
+        final JScrollPane scroll_pane = new JScrollPane (new_text);
+        gridbag.setConstraints (scroll_pane, c);
+        codon_info_panel.add (scroll_pane);
+
+        codon_info_areas[row][column - 2] = new_text;
+      }
+
+      c.gridwidth = GridBagConstraints.REMAINDER;
+
+      {
+        final JTextArea new_text = new JTextArea (5, 2);
+        new_text.setText (Bases.letter_index[0] + "\n" +
+                          Bases.letter_index[1] + "\n" +
+                          Bases.letter_index[2] + "\n" +
+                          Bases.letter_index[3]);
+
+        new_text.setEditable (false);
+
+        final JScrollPane scroll_pane = new JScrollPane (new_text);
+        gridbag.setConstraints (scroll_pane, c);
+        codon_info_panel.add (scroll_pane);
+      }
+
+      c.gridwidth = GRID_WIDTH;
+    }
+  }
+
+  /**
+   *  Set the List components with the information from the feature.
+   **/
+  private void updateComponents () {
+    updateAACountList ();
+
+    updateCodonInfoAreas ();
+
+    updateMolecularWeightLabel ();
+
+    updateCorrelationScoresLabel ();
+
+    updateUsageScoresLabel ();
+
+    updateBaseCounts ();
+
+    validate ();
+  }
+
+  /**
+   *  Update the aa_count_list JTextArea.
+   **/
+  private void updateAACountList () {
+    for (int i = 0 ; i < AminoAcidSequence.symbol_count ; ++i) {
+      if (i == AminoAcidSequence.symbol_count - 1 &&
+          getFeature ().getResidueCount (i) == 0) {
+        // don't include Selenocysteine in the list in the list if there are
+        // none
+        aa_count_list[i].setText ("");
+        continue;
+      }
+
+      final String three_letter_abbreviation =
+        AminoAcidSequence.getThreeLetterAbbreviation (i);
+
+      final char one_letter_abbreviation =
+        Character.toUpperCase (AminoAcidSequence.getSymbolFromIndex (i)) ;
+
+      final String label_string =
+        three_letter_abbreviation + " (" + one_letter_abbreviation +"): " +
+        getFeature ().getResidueCount (i);
+
+      aa_count_list[i].setText (label_string);
+    }
+  }
+
+  /**
+   *  Update all the JTextArea objects in codon_info_areas.
+   **/
+  private void updateCodonInfoAreas () {
+    for (int first = 0 ; first < 4 ; ++first) {
+
+      for (int second = 0 ; second < 4 ; ++second) {
+
+        codon_info_areas[first][second].setText ("");
+
+        for (int third = 0 ; third < 4 ; ++third) {
+
+          final char this_codon_symbol =
+            AminoAcidSequence.getCodonTranslation (Bases.letter_index[first],
+                                                   Bases.letter_index[second],
+                                                   Bases.letter_index[third]);
+
+          final String this_codon_string =
+            AminoAcidSequence.getThreeLetterAbbreviation (this_codon_symbol);
+
+          final int this_codon_count =
+            getFeature ().getCodonCount (first,second,third);
+
+          final int this_codon_index =
+            AminoAcidSequence.getSymbolIndex (this_codon_symbol);
+
+          final int this_amino_acid_count =
+            getFeature ().getResidueCount (this_codon_index);
+
+          final String percent_string;
+
+          if (this_amino_acid_count == this_codon_count) {
+            percent_string = "ALL";
+          } else {
+            if (this_amino_acid_count < this_codon_count) {
+              // some of the amino acids aren't coded for by the standard
+              // codon - probably because of the /transl_except qualifier
+              percent_string = "---";
+            } else {
+              final int percentage =
+                100 * this_codon_count / this_amino_acid_count;
+
+              percent_string = String.valueOf (percentage) + "%";
+            }
+          }
+
+          final String first_part_of_line =
+            this_codon_string + " " + this_codon_count;
+
+          codon_info_areas[first][second].append (first_part_of_line);
+
+          final int line_length_without_spaces =
+            first_part_of_line.length () + percent_string.length ();
+
+          // add some spaces so that everything lines up nicely
+          for (int i = 0 ; i < 11 - line_length_without_spaces ; ++i) {
+            codon_info_areas[first][second].append (" ");
+          }
+
+          codon_info_areas[first][second].append (percent_string);
+
+          if (third != 3) {
+            // insert a newline on all but the last line
+            codon_info_areas[first][second].append ("\n");
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   *  Update molecular_weight_label.
+   **/
+  private void updateMolecularWeightLabel () {
+    final String new_text =
+      "Mol weight: " +
+      getFeature ().getTranslation ().getMolecularWeight () + "  Start: " +
+      getFeature ().getFirstBase () + "  End: " +
+      getFeature ().getLastBase () + "  Bases: " +
+      getFeature ().getBaseCount () + "  AA length: " +
+      getFeature ().getTranslation ().length ();
+    molecular_weight_label.setText (new_text);
+  }
+
+
+  /**
+   *  The method updates the components in base_count_info_areas.
+   **/
+  private void updateBaseCounts () {
+    final int translation_base_count =
+      getFeature ().getTranslationBases ().length ();
+
+    final int amino_acid_count = getFeature ().getTranslation ().length ();
+
+    for (int i = 0 ; i < 4 ; ++i) {
+      base_count_info_areas[i].setText ("");
+      {
+        final String string =
+          "ALL:" + updateBaseCountsFormatter (getFeature ().getBaseCount (i),
+                                              translation_base_count);
+
+        base_count_info_areas[i].append (string);
+      }
+
+      for (int codon_base_index = 0;
+           codon_base_index < 3 ;
+           ++codon_base_index) {
+        String label = null;
+        switch (codon_base_index) {
+        case 0: label = "\n1st:"; break;
+        case 1: label = "\n2nd:"; break;
+        case 2: label = "\n3rd:"; break;
+        }
+
+        final int base_count =
+          getFeature ().getPositionalBaseCount (codon_base_index, i);
+
+        final String string =
+          label + updateBaseCountsFormatter (base_count, amino_acid_count);
+
+        base_count_info_areas[i].append (string);
+      }
+    }
+  }
+
+  /**
+   *  A helper method for updateBaseCounts ().
+   **/
+  private String updateBaseCountsFormatter (int base_count,
+                                            int total) {
+    final String count_string = "     " + base_count;
+
+    final String percent_string;
+
+    if (base_count < total) {
+      percent_string = " " + 100 * base_count / total + "%";
+    } else {
+      percent_string = "ALL";
+    }
+
+    final int count_width = 5;
+
+    final int percent_width = 3;
+
+    return
+      count_string.substring (count_string.length () - count_width) + " " +
+      percent_string.substring (percent_string.length () - percent_width);
+  }
+
+  /**
+   *  Update correlation_scores_label.
+   **/
+  private void updateCorrelationScoresLabel () {
+    final int c_total =
+      getFeature ().getBaseCount (Bases.getIndexOfBase ('c'));
+    final int g_total =
+      getFeature ().getBaseCount (Bases.getIndexOfBase ('g'));
+
+    final String c3_score;
+    final String g1_score;
+    final String g3_score;
+
+    final int c3_count =
+      getFeature ().getPositionalBaseCount (2, Bases.getIndexOfBase ('c'));
+    final int g1_count =
+      getFeature ().getPositionalBaseCount (0, Bases.getIndexOfBase ('g'));
+    final int g3_count =
+      getFeature ().getPositionalBaseCount (2, Bases.getIndexOfBase ('g'));
+
+    if (c_total == 0) {
+      c3_score = "ALL";
+    } else {
+      c3_score =
+        String.valueOf (1000 * (3 * c3_count - c_total) / c_total / 10.0);
+    }
+
+    if (g_total == 0) {
+      g1_score = "ALL";
+    } else {
+      g1_score =
+        String.valueOf (1000 * (3 * g1_count - g_total) / g_total / 10.0);
+    }
+
+    if (g_total == 0) {
+      g3_score = "ALL";
+    } else {
+      g3_score =
+        String.valueOf (1000 * (3 * g3_count - g_total) / g_total / 10.0);
+    }
+
+    final double cor1_2_score =
+      ((int) getFeature ().get12CorrelationScore () * 10) / 10.0;
+
+    final double gc_percent =
+      ((int) (getFeature ().getPercentGC () * 100.0)) / 100.0;
+
+    final String new_label =
+      "position 1/2 score = " + cor1_2_score + "  " +
+      "C3/G1/G3 (o-e)/e = " + c3_score + " " + g1_score + " " + g3_score +
+      "  " + gc_percent + "% GC";
+
+    correlation_scores_label.setText (new_label);
+  }
+
+  /**
+   *  Update usage_scores_label.
+   **/
+  private void updateUsageScoresLabel () {
+    if (codon_usage_algorithm != null) {
+      final String new_label =
+        "usage score = " + codon_usage_algorithm.getFeatureScore (feature);
+
+      usage_scores_label.setText (new_label);
+    }
+  }
+
+  /**
+   *  Return the feature this component is showing information about.
+   **/
+  private Feature getFeature () {
+    return feature;
+  }
+
+  /**
+   *  Return the Entry that contains the Feature this object is displaying.
+   **/
+  private Entry getEntry () {
+    return entry;
+  }
+
+  /**
+   *  The Feature that this component is showing information about.
+   **/
+  private Feature feature = null;
+
+  /**
+   *  The Entry that contains the Feature this object is displaying.
+   **/
+  private Entry entry;
+
+  /**
+   *  A panel containing the labels with the amino acid counts for the
+   *  feature.
+   **/
+  private JPanel aa_count_panel = null;
+
+  /**
+   *  An array Labels of showing the amino acid counts for the feature.  There
+   *  is one Label for each symbol (24).
+   **/
+  private JLabel [] aa_count_list = null;
+
+  /**
+   *  A panel containing general statistics about the whole sequence.
+   **/
+  private JPanel misc_info_panel = null;
+
+  /**
+   *  A Label containing molecular weight of the protein, start and end
+   *  positions and length.
+   **/
+  private JLabel molecular_weight_label = null;
+
+  /**
+   *  A Label containing the correlation scores for this feature.
+   **/
+  private JLabel correlation_scores_label = null;
+
+  /**
+   *  A Label containing the usage scores for this feature.
+   **/
+  private JLabel usage_scores_label = null;
+
+  /**
+   *  Statistics about the codon frequency.
+   **/
+  private JPanel codon_info_panel = null;
+
+  /**
+   *  A panel to hold the close button.
+   **/
+  private JPanel button_panel = null;
+
+  /**
+   *  These components each hold information about 4 codons each.
+   **/
+  private JTextArea [] [] codon_info_areas;
+
+  /**
+   *  These components display position information about the base counts.
+   *  See positional_base_counts and base_counts.
+   **/
+  private JTextArea [] base_count_info_areas;
+
+  /**
+   *  The CodonUsageAlgorithm reference that was passed to the constructor.
+   *  (Used by updateUsageScoresLabel ()).
+   **/
+  private final CodonUsageAlgorithm codon_usage_algorithm;
+}
diff --git a/uk/ac/sanger/artemis/components/FeatureList.java b/uk/ac/sanger/artemis/components/FeatureList.java
new file mode 100644
index 000000000..435800716
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeatureList.java
@@ -0,0 +1,971 @@
+/* FeatureList.java
+ *
+ * created: Fri Oct  9 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1998,1999,2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeatureList.java,v 1.1 2004-06-09 09:46:40 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.plot.*;
+
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.Qualifier;
+import uk.ac.sanger.artemis.io.InvalidRelationException;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.QualifierInfo;
+import uk.ac.sanger.artemis.io.QualifierVector;
+import uk.ac.sanger.artemis.io.StreamQualifier;
+import uk.ac.sanger.artemis.util.StringVector;
+
+import java.awt.event.*;
+import java.awt.*;
+import java.text.*;
+import java.util.*;
+
+import javax.swing.*;
+
+/**
+ *  This component gives the user a list containing the details the current
+ *  Features.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeatureList.java,v 1.1 2004-06-09 09:46:40 tjc Exp $
+ *
+ **/
+
+public class FeatureList extends EntryGroupPanel
+  implements EntryGroupChangeListener,
+             EntryChangeListener, FeatureChangeListener,
+             SelectionChangeListener, DisplayComponent
+{
+  /**
+   *  Create a new FeatureList with the default number of rows.
+   *  @param entry_group The EntryGroup that this component will display.
+   *  @param selection The Selection object for this component.  Selected
+   *    objects will be highlighted.
+   *  @param goto_event_source The object to use when we need to call
+   *    gotoBase ().
+   **/
+  public FeatureList (final EntryGroup entry_group,
+                      final Selection selection,
+                      final GotoEventSource goto_event_source,
+                      final BasePlotGroup base_plot_group) {
+    super (entry_group, selection, goto_event_source, base_plot_group);
+
+    getCanvas ().addMouseListener (new MouseAdapter () {
+      /**
+       *  Listen for mouse press events so that we can do popup menus and
+       *  selection.
+       **/
+      public void mousePressed (MouseEvent event) {
+        if (isMenuTrigger (event)) {
+          final FeaturePopup popup =
+            new FeaturePopup (FeatureList.this,
+                              getEntryGroup (),
+                              getSelection (),
+                              getGotoEventSource (),
+                              getBasePlotGroup ());
+          final JComponent parent = (JComponent) event.getSource ();
+
+          popup.show (parent, event.getX (), event.getY ());
+        } else {
+          handleCanvasMousePress (event);
+        }
+      }
+    });
+
+    createScrollbars ();
+
+    addComponentListener (new ComponentAdapter () {
+      public void componentShown (ComponentEvent e) {
+        repaintCanvas ();
+      }
+      public void componentResized (ComponentEvent e) {
+        repaintCanvas ();
+      }
+    });
+
+    getSelection ().addSelectionChangeListener (this);
+
+    // changes to the EntryGroup will be noticed by listening for EntryChange
+    // and FeatureChange events.
+
+    getEntryGroup ().addEntryGroupChangeListener (this);
+    getEntryGroup ().addEntryChangeListener (this);
+    getEntryGroup ().addFeatureChangeListener (this);
+
+
+    // find the maximum posible width for the high and low positions
+    final int sequence_length = getEntryGroup ().getSequenceLength ();
+    max_base_pos_width = (int)(Math.log (sequence_length)/Math.log (10)) + 1;
+
+    if (max_base_pos_width < 4) {
+      max_base_pos_width = 4;
+    }
+
+    repaintCanvas ();
+  }
+
+  /**
+   *  Remove this component from all the listener lists it is on.
+   **/
+  void stopListening () {
+    getSelection ().removeSelectionChangeListener (this);
+
+    getEntryGroup ().removeEntryGroupChangeListener (this);
+    getEntryGroup ().removeEntryChangeListener (this);
+    getEntryGroup ().removeFeatureChangeListener (this);
+  }
+
+  /**
+   *  Returns the value of a flag that indicates whether this component can be
+   *  traversed using Tab or Shift-Tab keyboard focus traversal - returns true
+   *  for FeatureDisplay components
+   **/
+// tjc - deprecated replaced by isFocusable()
+//public boolean isFocusTraversable () 
+//{
+//  return true;
+//}
+
+  /**
+   *  Set value of the show correlation scores flag.
+   *  @param show_correlation_scores Show correlation scores in the list if
+   *    and only if this argument is true.
+   **/
+  public void setCorrelationScores (final boolean show_correlation_scores) {
+    if (this.show_correlation_scores != show_correlation_scores) {
+      this.show_correlation_scores = show_correlation_scores;
+      repaintCanvas ();
+    } else {
+      // do nothing
+    }
+  }
+
+  /**
+   *  Get the value of the "show correlation scores" flag.
+   **/
+  public boolean getCorrelationScores () {
+    return show_correlation_scores;
+  }
+
+  /**
+   *  Set value of the show /gene flag.
+   *  @param show_gene_names If true this component will show the /gene (really
+   *    Feature.getIDString ()) instead of the key.
+   **/
+  public void setShowGenes (final boolean show_gene_names) {
+    if (this.show_gene_names != show_gene_names) {
+      this.show_gene_names = show_gene_names;
+      repaintCanvas ();
+    } else {
+      // do nothing
+    }
+  }
+
+  /**
+   *  Get the value of the "show genes" flag.
+   **/
+  public boolean getShowGenes () {
+    return show_gene_names;
+  }
+
+  /**
+   *  Set value of the show qualifiers flag.
+   *  @param show_quailfiers If true this component will show all the
+   *    qualifiers after the note.
+   **/
+  public void setShowQualifiers (final boolean show_qualifiers) {
+    if (this.show_qualifiers != show_qualifiers) {
+      this.show_qualifiers = show_qualifiers;
+      repaintCanvas ();
+    } else {
+      // do nothing
+    }
+  }
+
+  /**
+   *  Get the value of the "show qualifiers" flag.
+   **/
+  public boolean getShowQualifiers () {
+    return show_qualifiers;
+  }
+
+  /**
+   *  Set value of the show /product flag.
+   *  @param show_products If true this component will show the /product
+   *    qualifier instead of the /note.
+   **/
+  public void setShowProducts (final boolean show_products) {
+    if (this.show_products != show_products) {
+      this.show_products = show_products;
+      repaintCanvas ();
+    } else {
+      // do nothing
+    }
+  }
+
+  /**
+   *  Get the value of the "show products" flag.
+   **/
+  public boolean getShowProducts () {
+    return show_products;
+  }
+
+  /**
+   *  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 (EntryGroupChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryGroupChangeEvent.ENTRY_ADDED:
+    case EntryGroupChangeEvent.ENTRY_ACTIVE:
+    case EntryGroupChangeEvent.ENTRY_DELETED:
+    case EntryGroupChangeEvent.ENTRY_INACTIVE:
+      repaintCanvas ();
+      break;
+    }
+  }
+
+  /**
+   *  Implementation of the FeatureChangeListener interface.
+   **/
+  public void featureChanged (FeatureChangeEvent event) {
+    if (!isVisible ()) {
+      return;
+    }
+
+    repaintCanvas ();
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so that we can update the list if features are added
+   *  or deleted.
+   **/
+  public void entryChanged (EntryChangeEvent event) {
+    if (!isVisible ()) {
+      return;
+    }
+
+    repaintCanvas ();
+  }
+
+  /**
+   *  Implementation of the SelectionChangeListener interface.  We listen to
+   *  SelectionChange events so that we can update the list to reflect the
+   *  current selection.
+   **/
+  public void selectionChanged (SelectionChangeEvent event) {
+    if (!isVisible ()) {
+      return;
+    }
+
+    if (event.getSource () == this) {
+      // don't bother with events we sent ourself
+      return;
+    }
+
+    if (getSelection ().getMarkerRange () != null &&
+        event.getType () == SelectionChangeEvent.OBJECT_CHANGED) {
+      // if the selected range changes we don't care
+      return;
+    }
+
+    selection_changed_flag = true;
+
+    repaintCanvas ();
+  }
+
+  /**
+   *  Return a vector containing the text that is shown in the list - one
+   *  String per line.
+   **/
+  public StringVector getListStrings () {
+    final StringVector return_vector = new StringVector ();
+
+    final FeatureEnumeration test_enumerator = getEntryGroup ().features ();
+
+    while (test_enumerator.hasMoreFeatures ()) {
+      final Feature this_feature = test_enumerator.nextFeature ();
+
+      return_vector.add (makeFeatureString (this_feature, true));
+    }
+
+    return return_vector;
+  }
+
+  /**
+   *  Create the scroll bar.
+   **/
+  private void createScrollbars () {
+    scrollbar = new JScrollBar (Scrollbar.VERTICAL);
+    scrollbar.setValues (0, 1, 0,
+                         getEntryGroup ().getAllFeaturesCount ());
+    scrollbar.setUnitIncrement (1);
+    scrollbar.setBlockIncrement (1);
+    scrollbar.addAdjustmentListener (new AdjustmentListener () {
+      public void adjustmentValueChanged(AdjustmentEvent e) {
+        setFirstIndex (e.getValue ());
+      }
+    });
+
+    horiz_scrollbar = new JScrollBar (Scrollbar.HORIZONTAL);
+    horiz_scrollbar.setValues (0, 1, 0,
+                               getEntryGroup ().getAllFeaturesCount () - 1);
+    horiz_scrollbar.setUnitIncrement (getFontWidth ());
+    horiz_scrollbar.setBlockIncrement (100);
+    horiz_scrollbar.addAdjustmentListener (new AdjustmentListener () {
+      public void adjustmentValueChanged (AdjustmentEvent e) {
+        repaintCanvas ();
+      }
+    });
+
+    getMidPanel ().add (horiz_scrollbar, "South");
+
+    add (scrollbar, "East");
+  }
+
+  /**
+   *  Set the extent, max and value of the horizontal scrollbar 
+   **/
+  private void fixHorizScrollBar (final int max_width) {
+    int old_value = horiz_scrollbar.getValue ();
+
+    horiz_scrollbar.setValues (horiz_scrollbar.getValue (),
+                               getCanvas ().getSize ().width,
+                               0, max_width * getFontWidth ());
+    horiz_scrollbar.setBlockIncrement (getCanvas ().getSize ().width);
+  }
+
+  /**
+   *  Set the first visible index.
+   **/
+  public void setFirstIndex (final int first_index) {
+    this.first_index = first_index;
+    need_to_fix_horiz_scrollbar = true;
+    repaintCanvas ();
+  }
+
+  /**
+   *  Handle a mouse press event on the drawing canvas - select on click,
+   *  select and broadcast it on double click.
+   **/
+  private void handleCanvasMousePress (final MouseEvent event) {
+    if (event.getID() != MouseEvent.MOUSE_PRESSED) {
+      return;
+    }
+
+    getCanvas ().requestFocus ();
+
+    if (!event.isShiftDown ()) {
+      getSelection ().clear ();
+    }
+
+    final int clicked_feature_index =
+      scrollbar.getValue () + event.getY () / getLineHeight ();
+
+    if (clicked_feature_index < getEntryGroup ().getAllFeaturesCount ()) {
+      final FeatureVector selected_features =
+        getSelection ().getAllFeatures ();
+
+      final Feature clicked_feature =
+        getEntryGroup ().featureAt (clicked_feature_index);
+
+      if (selected_features.contains (clicked_feature)) {
+        getSelection ().remove (clicked_feature);
+        getSelection ().removeSegmentsOf (clicked_feature);
+      } else {
+        getSelection ().add (clicked_feature);
+      }
+
+      if (event.getClickCount () == 2) {
+        makeSelectionVisible ();
+
+        if ((event.getModifiers () & InputEvent.BUTTON2_MASK) != 0 ||
+            event.isAltDown ()) {
+          if (Options.readWritePossible ()) {
+            new FeatureEdit (clicked_feature, getEntryGroup (),
+                             getSelection (),
+                             getGotoEventSource ()).show ();
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   *  The main paint function for the canvas.  An off screen image used for
+   *  double buffering when drawing the canvas.
+   *  @param g The Graphics object of the canvas.
+   **/
+  protected void paintCanvas (Graphics g) {
+    refresh ();
+    
+    if (!isVisible ()) {
+      return;
+    }
+
+    if (selection_changed_flag) {
+      selection_changed_flag = false;
+
+      final FeatureVector selected_features =
+        getSelection ().getAllFeatures ();
+
+      if (selected_features.size () > 0) {
+        // set to true if any of the selected features is visible
+        boolean a_selected_feature_is_visible = false;
+
+        int first_line_in_view = scrollbar.getValue ();
+
+        if (first_line_in_view == -1) {
+          first_line_in_view = 0;
+        }
+
+        final int feature_count = getEntryGroup ().getAllFeaturesCount ();
+
+        for (int i = first_line_in_view ;
+             i < feature_count && i < first_line_in_view + linesInView () ;
+             ++i) {
+          final Feature this_feature = getEntryGroup ().featureAt (i);
+          if (selected_features.contains (this_feature)) {
+            a_selected_feature_is_visible = true;
+            break;
+          }
+        }
+
+        if (!a_selected_feature_is_visible) {
+          // make the first selected feature visible
+          final Feature first_selected_feature =
+            selected_features.elementAt (0);
+
+          final int index_of_first_selected_feature =
+            getEntryGroup ().indexOf (first_selected_feature);
+
+          if (index_of_first_selected_feature < scrollbar.getValue () ||
+              index_of_first_selected_feature >=
+              scrollbar.getValue () + linesInView ()) {
+
+            scrollbar.setValue (index_of_first_selected_feature);
+          }
+        }
+      }
+    }
+
+    g.setColor (background_colour);
+
+    g.fillRect (0, 0, getCanvasWidth (), getCanvasHeight ());
+
+    g.setColor (Color.black);
+
+    final int all_feature_count = getEntryGroup ().getAllFeaturesCount ();
+    
+    if (all_feature_count == 0) {
+      fixHorizScrollBar (0);
+    } else {
+      final int lines_in_view = linesInView ();
+      int first_index_in_view = scrollbar.getValue ();
+
+      if (first_index_in_view == -1) {
+        first_index_in_view = 0;
+      }
+
+      final int feature_count = getEntryGroup ().getAllFeaturesCount ();
+
+      /* jikes 1.15 bug  final */  int last_index_in_view;
+
+      if (lines_in_view < feature_count - first_index_in_view) {
+        last_index_in_view = first_index_in_view + lines_in_view;
+      } else {
+        last_index_in_view = feature_count - 1;
+      }
+
+      final FeatureVector features_in_view =
+        getEntryGroup ().getFeaturesInIndexRange (first_index_in_view,
+                                                  last_index_in_view);
+
+      /**
+       *  The maximum width of the strings we have seen - used to set the
+       *  horiz_scrollbar maximum.
+       **/
+      int max_width = -1;
+
+      for (int i = 0 ; i <= last_index_in_view - first_index_in_view ; ++i) {
+        final Feature this_feature = features_in_view.elementAt (i);
+        final String feature_string = makeFeatureString (this_feature, false);
+        drawFeatureLine (g, this_feature, feature_string,i);
+
+        if (feature_string.length () > max_width) {
+          max_width = feature_string.length ();
+        }
+      }
+
+      if (need_to_fix_horiz_scrollbar) {
+        fixHorizScrollBar (max_width);
+      }
+    }
+  }
+
+  /**
+   *  Return the number of visible text lines on canvas.
+   **/
+  private int linesInView () {
+    return getCanvas ().getSize ().height / getLineHeight ();
+  }
+
+  /**
+   *  Update the scrollbar.
+   **/
+  private void refresh () {
+    scrollbar.setMaximum (getEntryGroup ().getAllFeaturesCount ());
+    final int lines_in_view = linesInView ();
+    scrollbar.setBlockIncrement (lines_in_view > 0 ? lines_in_view : 1);
+    scrollbar.setUnitIncrement (1);
+    scrollbar.setVisibleAmount (linesInView ());
+  }
+
+  /**
+   *  Draw the given Feature at the given line of the list, taking the
+   *  selection into account.
+   **/
+  private void drawFeatureLine (final Graphics g,
+                                final Feature feature,
+                                final String feature_string,
+                                final int line) {
+    final int y_pos = line * getLineHeight ();
+
+    // the width of the coloured blob at the left of the text
+    final int BOX_WIDTH = getLineHeight ();
+
+    final Color feature_colour = feature.getColour ();
+
+    if (feature_colour == null) {
+      // default colour is white
+      g.setColor (Color.white);
+    } else {
+      g.setColor (feature_colour);
+    }
+    g.fillRect (1 - horiz_scrollbar.getValue (), y_pos + 1,
+                BOX_WIDTH, getLineHeight () - 1);
+
+    if (getSelection ().contains (feature)) {
+      // draw in reverse
+      g.setColor (Color.black);
+      g.fillRect (BOX_WIDTH + 4 - horiz_scrollbar.getValue (), y_pos,
+                  getCanvas ().getSize ().width + horiz_scrollbar.getValue (),
+                  getLineHeight ());
+      g.setColor (background_colour);
+    } else {
+      g.setColor (Color.black);
+    }
+
+    g.setFont (getFont ());
+
+    g.drawString (feature_string,
+                  BOX_WIDTH + 5 -
+                  horiz_scrollbar.getValue (),
+                  y_pos + getFontAscent ());
+
+    g.setPaintMode ();
+  }
+
+  /**
+   *  Return the list index of a feature.
+   **/
+  private int indexOf (Feature feature) {
+    return getEntryGroup ().indexOf (feature);
+  }
+
+  /**
+   *  Return a String object suitable for displaying in the list of features.
+   *  @param dont_truncate if true the gene name / key field won't be
+   *    truncated if it is longer than the field width
+   **/
+  private String makeFeatureString (final Feature feature,
+                                    final boolean dont_truncate) {
+    String key_string;
+
+    final int KEY_FIELD_WIDTH = 15;
+
+    if (show_gene_names) {
+      key_string = feature.getIDString ();
+
+      if (key_string.length () > KEY_FIELD_WIDTH && !dont_truncate) {
+        key_string = key_string.substring (0, KEY_FIELD_WIDTH);
+      }
+    } else {
+      key_string = feature.getKey ().toString ();
+    }
+
+    final Marker low_marker = feature.getFirstBaseMarker ();
+    final Marker high_marker = feature.getLastBaseMarker ();
+
+    final StringBuffer description_string_buffer = new StringBuffer ();
+
+    if (show_products) {
+      final String product_string = feature.getProductString ();
+
+      if (product_string == null) {
+        if (feature.isCDS ()) {
+          description_string_buffer.append ("[no /product]");
+        } else {
+          // description is blank
+        }
+      } else {
+        description_string_buffer.append (product_string);
+      }
+    } else {
+      final String note = feature.getNote ();
+
+      if (note != null && note.length () != 0) {
+        final int QUALIFIER_COLUMN = 10;
+
+        final String note_string =
+          padRightWithSpaces (feature.getNote (), QUALIFIER_COLUMN);
+
+        description_string_buffer.append (note_string);
+        description_string_buffer.append ("   ");
+      }
+
+      if (show_qualifiers) {
+        description_string_buffer.append (getQualifierString (feature));
+      }
+    }
+
+    final String low_pos;
+    final String high_pos;
+
+    if (low_marker == null || high_marker == null) {
+      low_pos = "unknown";
+      high_pos = "unknown";
+    } else {
+      if (low_marker.getRawPosition () < high_marker.getRawPosition ()) {
+        low_pos = String.valueOf (low_marker.getRawPosition ());
+        high_pos = String.valueOf (high_marker.getRawPosition ());
+      } else {
+        low_pos = String.valueOf (high_marker.getRawPosition ());
+        high_pos = String.valueOf (low_marker.getRawPosition ());
+      }
+    }
+
+    StringBuffer new_list_line = new StringBuffer ();
+
+    new_list_line.append (padRightWithSpaces (key_string, KEY_FIELD_WIDTH));
+    new_list_line.append (" ");
+
+    new_list_line.append (padLeftWithSpaces (low_pos, max_base_pos_width));
+    new_list_line.append (" ");
+    new_list_line.append (padLeftWithSpaces (high_pos, max_base_pos_width));
+    new_list_line.append (" ");
+
+    if (feature.isForwardFeature ()) {
+      new_list_line.append ("   ");
+    } else {
+      new_list_line.append ("c  ");
+    }
+
+    if (show_correlation_scores) {
+      if (feature.isCDS ()) {
+        new_list_line.append (getScoresString (feature));
+        new_list_line.append ("  ");
+      } else {
+        new_list_line.append ("                         ");
+        if (getBasePlotGroup ().getCodonUsageAlgorithm () != null) {
+          new_list_line.append ("      ");
+        }
+      }
+    }
+
+    new_list_line.append (description_string_buffer);
+
+    return new_list_line.toString ();
+  }
+
+  /**
+   *  Return the characters width of the Canvas.
+   **/
+  private int getWidthInChars () {
+    return getCanvas ().getSize ().width / getFontWidth ();
+  }
+
+  /**
+   *  Return a String containing the given Qualifier and it's values (in EMBL
+   *  format).
+   *  @param start_index ignore the values before this index
+   **/
+  private String formatQualifier (final String qualifier_name,
+                                  final Feature feature,
+                                  final int start_index) {
+    final StringBuffer buffer = new StringBuffer ();
+
+    try {
+      final Qualifier qualifier = feature.getQualifierByName (qualifier_name);
+
+      if (qualifier != null) {
+        final EntryInformation entry_information =
+          feature.getEntry ().getEntryInformation ();
+
+        final QualifierInfo qualifier_info =
+          entry_information.getQualifierInfo (qualifier_name);
+
+        final StringVector qualifier_strings =
+          StreamQualifier.toStringVector (qualifier_info,
+                                          qualifier);
+
+        for (int i = start_index ; i < qualifier_strings.size () ; ++i) {
+
+          final String qualifier_string = qualifier_strings.elementAt (i);
+
+          buffer.append (qualifier_string + " ");
+        }
+      }
+    } catch (InvalidRelationException e) {
+      // ignore
+    }
+
+    return buffer.toString ();
+  }
+
+  /**
+   *  Return a String containing all the qualifiers of the given Feature
+   *  (except /note) in EMBL format.  Any /similarity qualifier will come
+   *  first.
+   **/
+  private String getQualifierString (final Feature feature) {
+    final StringBuffer buffer = new StringBuffer ();
+
+    final QualifierVector qualifiers = feature.getQualifiers ();
+
+    // if there is a /note and it has more than one value put it next (without
+    // the first value)
+    final Qualifier note_qualifier =
+      qualifiers.getQualifierByName ("note");
+
+    if (note_qualifier != null && note_qualifier.getValues ().size () > 1) {
+      buffer.append (formatQualifier ("note", feature, 1));
+      buffer.append (" ");
+    }
+
+    // put /similarity before all but the /note qualifier
+    final Qualifier similarity_qualifier =
+      qualifiers.getQualifierByName ("similarity");
+
+    if (similarity_qualifier != null) {
+      buffer.append (formatQualifier ("similarity", feature, 0));
+      buffer.append (" ");
+    }
+
+    for (int i = 0 ; i < qualifiers.size () ; ++i) {
+      final Qualifier this_qualifier = qualifiers.elementAt (i);
+
+      final String this_qualifier_name = this_qualifier.getName ();
+
+      if (!this_qualifier_name.equals ("note") &&
+          !this_qualifier_name.equals ("similarity")) {
+        buffer.append (formatQualifier (this_qualifier_name, feature, 0));
+        buffer.append (" ");
+      }
+    }
+
+    return buffer.toString ();
+  }
+
+  /**
+   *  Return a String containing the correlation scores.
+   **/
+  public String getScoresString (final Feature feature) {
+    final int base_total = feature.getTranslationBases ().length ();
+
+    final int c_total = feature.getBaseCount (Bases.getIndexOfBase ('c'));
+    final int g_total = feature.getBaseCount (Bases.getIndexOfBase ('g'));
+
+    final int g1_count =
+      feature.getPositionalBaseCount (0, Bases.getIndexOfBase ('g'));
+
+    final int c3_count =
+      feature.getPositionalBaseCount (2, Bases.getIndexOfBase ('c'));
+    final int g3_count =
+      feature.getPositionalBaseCount (2, Bases.getIndexOfBase ('g'));
+
+    final double c3_score = 100.0 * (3 * c3_count - c_total) / c_total;
+    final double g1_score = 100.0 * (3 * g1_count - g_total) / g_total;
+    final double g3_score = 100.0 * (3 * g3_count - g_total) / g_total;
+
+    final double cor1_2_score = feature.get12CorrelationScore ();
+
+    final NumberFormat number_format = NumberFormat.getNumberInstance ();
+
+    number_format.setMaximumFractionDigits (1);
+    number_format.setMinimumFractionDigits (1);
+
+    final String cor1_2_score_string = number_format.format (cor1_2_score);
+    final String c3_score_string;
+    final String g1_score_string;
+    final String g3_score_string;
+
+
+    if (c_total == 0) {
+      c3_score_string = "ALL";
+    } else {
+      c3_score_string = number_format.format (c3_score);
+    }
+
+    if (g_total == 0) {
+      g1_score_string = "ALL";
+    } else {
+      g1_score_string = number_format.format (g1_score);
+    }
+
+    if (g_total == 0) {
+      g3_score_string = "ALL";
+    } else {
+      g3_score_string = number_format.format (g3_score);
+    }
+
+    String codon_usage_score_string = "";
+
+    final CodonUsageAlgorithm codon_usage_alg =
+      getBasePlotGroup ().getCodonUsageAlgorithm ();
+
+    if (codon_usage_alg != null) {
+      number_format.setMaximumFractionDigits (3);
+      number_format.setMinimumFractionDigits (3);
+
+      codon_usage_score_string =
+        number_format.format (codon_usage_alg.getFeatureScore (feature)) + " ";
+    }
+
+    return
+      codon_usage_score_string +
+      padRightWithSpaces (cor1_2_score_string, 5) + " " +
+      padRightWithSpaces (c3_score_string, 5) + " " +
+      padRightWithSpaces (g1_score_string, 5) + " " +
+      padRightWithSpaces (g3_score_string, 5);
+  }
+
+  /**
+   *  Return the given string padded with spaces to the given width.  The
+   *  spaces are added on the right of the string.
+   **/
+  private String padRightWithSpaces (final String string, final int width) {
+    if (string.length () == width) {
+      return string;
+    }
+
+    final StringBuffer buffer = new StringBuffer (string);
+
+    for (int i = 0 ; i < width - string.length () ; ++i) {
+      buffer.append (' ');
+    }
+
+    return buffer.toString ();
+  }
+
+  /**
+   *  Return the given string padded with spaces to the given width.  The
+   *  spaces are added on the left of the string.
+   **/
+  private String padLeftWithSpaces (final String string, final int width) {
+    if (string.length () == width) {
+      return string;
+    }
+
+    final StringBuffer buffer = new StringBuffer ();
+
+    for (int i = 0 ; i < width - string.length () ; ++i) {
+      buffer.append (' ');
+    }
+
+    buffer.append (string);
+
+    return buffer.toString ();
+  }
+
+  /**
+   *  Return the height each line of the display should be.  Each feature will
+   *  be drawn into one line.
+   **/
+  private int getLineHeight () {
+    return getFontAscent () + 2;
+  }
+
+  /**
+   *  This variable is true if correlation scores should be shown in the list.
+   **/
+  private boolean show_correlation_scores = false;
+
+  /**
+   *  The JScrollBar for this FeatureList object.
+   **/
+  private JScrollBar scrollbar = null;
+
+  /**
+   *  The JScrollBar for horizontal scrolling in this FeatureList object.
+   **/
+  private JScrollBar horiz_scrollbar = null;
+
+  /**
+   *  The index of the first visible feature in the list.
+   **/
+  private int first_index;
+
+  /**
+   *  This is set to true by selectionChanged () and used by paintCanvas ().
+   **/
+  private boolean selection_changed_flag = false;
+
+  /**
+   *  The colour used to draw the background.
+   **/
+  private Color background_colour = Color.white;
+
+  /**
+   *  If true this component will show Feature.getIDString () (ie /gene or
+   *  /label) instead of the key.
+   **/
+  private boolean show_gene_names = false;
+
+  /**
+   *  If true this component will show the /product qualifier instead of the
+   *  /note field.
+   **/
+  private boolean show_products = false;
+
+  /**
+   * If true this component will show all the qualifiers after the note.
+   **/
+  private boolean show_qualifiers = false;
+
+  /**
+   *  The is the maximum width of the strings containing the feature start and
+   *  stop positions.  Set in the constructor.
+   **/
+  private int max_base_pos_width;
+
+  /**
+   *  Set to true when paintCanvas() needs to call fixHorizScrollBar().
+   **/
+  private boolean need_to_fix_horiz_scrollbar = true;
+}
diff --git a/uk/ac/sanger/artemis/components/FeatureListFrame.java b/uk/ac/sanger/artemis/components/FeatureListFrame.java
new file mode 100644
index 000000000..ee31a12b6
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeatureListFrame.java
@@ -0,0 +1,195 @@
+/* FeatureListFrame.java
+ *
+ * created: Fri Sep  3 1999
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1999,2000,2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeatureListFrame.java,v 1.1 2004-06-09 09:46:41 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A JFrame that contains a FeatureList component and an Close button.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeatureListFrame.java,v 1.1 2004-06-09 09:46:41 tjc Exp $
+ **/
+
+public class FeatureListFrame extends JFrame
+    implements EntryGroupChangeListener {
+  /**
+   *  Create a new FeatureListFrame component.  The constructor does not call
+   *  setVisible (true).
+   *  @param title The title to use for the new JFrame.
+   *  @param feature_list The FeatureList to show.
+   *  @param selection The Selection that the commands in the menus will
+   *    operate on.
+   *  @param entry_group The EntryGroup object where new features/entries will
+   *    be added.
+   *  @param goto_event_source The object that the menu item will call
+   *    makeBaseVisible () on.
+   **/
+  public FeatureListFrame (final String title,
+                           final Selection selection,
+                           final GotoEventSource goto_event_source,
+                           final EntryGroup entry_group,
+                           final BasePlotGroup base_plot_group) {
+    super (title);
+
+    this.entry_group = entry_group;
+
+    feature_list = new FeatureList (entry_group, selection, goto_event_source,
+                                    base_plot_group);
+    
+    final Font default_font = Options.getOptions ().getFont ();
+    
+    setFont (default_font);
+
+    final JMenuBar menu_bar = new JMenuBar ();
+
+    menu_bar.setFont (default_font);
+
+    setJMenuBar (menu_bar);
+
+    final JMenu file_menu = new JMenu ("File");
+
+    final JMenuItem close = new JMenuItem ("Close");
+    close.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        setVisible (false);
+        FeatureListFrame.this.dispose ();
+        feature_list.stopListening ();        
+      }
+    });
+
+    file_menu.add (close);
+
+    menu_bar.add (file_menu);
+    
+    final JMenu select_menu =
+      new SelectMenu (this, selection, goto_event_source, entry_group,
+                      base_plot_group);
+    menu_bar.add (select_menu);
+
+    final JMenu view_menu =
+      new ViewMenu (this, selection, goto_event_source, entry_group,
+                    base_plot_group);
+    menu_bar.add (view_menu);
+
+    final JMenu goto_menu =
+      new GotoMenu (this, selection, goto_event_source, entry_group);
+    menu_bar.add (goto_menu);
+
+    if (Options.readWritePossible ()) {
+      final JMenu edit_menu =
+        new EditMenu (this, selection, goto_event_source, entry_group,
+                      base_plot_group);
+      menu_bar.add (edit_menu);
+
+      final JMenu write_menu = new WriteMenu (this, selection, entry_group);
+      menu_bar.add (write_menu);
+
+      final JMenu run_menu = new RunMenu (this, selection);
+      menu_bar.add (run_menu);
+    }
+
+    getContentPane ().add (feature_list, "Center");
+
+    final JPanel panel = new JPanel ();
+
+    final JButton close_button = new JButton ("Close");
+
+    panel.add (close_button);
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        setVisible (false);
+        FeatureListFrame.this.dispose ();
+        feature_list.stopListening ();
+      }
+    });
+
+    getContentPane ().add (panel, "South");
+    pack ();
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        setVisible (false);
+        entry_group.removeEntryGroupChangeListener (FeatureListFrame.this);
+        FeatureListFrame.this.dispose ();
+        feature_list.stopListening ();
+      }
+    });
+
+    entry_group.addEntryGroupChangeListener (this);
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+
+    int screen_height = screen.height;
+    int screen_width = screen.width;
+
+    if (screen_width <= 700 || screen_height <= 400) {
+      setSize (screen_width * 9 / 10, screen_height * 9 / 10);
+    } else {
+      setSize (700, 400);
+    }
+
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+  }
+
+  /**
+   *  Implementation of the EntryGroupChangeListener interface.  We listen to
+   *  EntryGroupChange events so that we can get rid of the Navigator when the
+   *  EntryGroup is no longer in use (for example when the EntryEdit is
+   *  closed).
+   **/
+  public void entryGroupChanged (final EntryGroupChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryGroupChangeEvent.DONE_GONE:
+      entry_group.removeEntryGroupChangeListener (this);
+      dispose ();
+      break;
+    }
+  }
+
+  /**
+   *  Return the FeatureList that this JFrame is displaying.
+   **/
+  public FeatureList getFeatureList () {
+    return feature_list;
+  }
+
+  /**
+   *  The FeatureList that this JFrame is displaying.
+   **/
+  private FeatureList feature_list;
+
+  /**
+   *  The EntryGroup that was passed to the constructor.
+   **/
+  private EntryGroup entry_group;
+}
diff --git a/uk/ac/sanger/artemis/components/FeaturePlot.java b/uk/ac/sanger/artemis/components/FeaturePlot.java
new file mode 100644
index 000000000..1707d46d8
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeaturePlot.java
@@ -0,0 +1,389 @@
+/* FeaturePlot.java
+ *
+ * created: Wed Dec 16 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeaturePlot.java,v 1.1 2004-06-09 09:46:42 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.plot.*;
+import java.awt.*;
+import java.awt.event.*;
+
+/**
+ *  The components of this class display a plot of a FeatureAlgorithm for a
+ *  particular feature.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeaturePlot.java,v 1.1 2004-06-09 09:46:42 tjc Exp $
+ **/
+
+public class FeaturePlot extends Plot
+    implements DisplayAdjustmentListener, FeatureChangeListener {
+  /**
+   *  Create a new FeatureDisplay object.
+   *  @param algorithm The object that will generate the values we plot in
+   *    this component.
+   **/
+  public FeaturePlot (FeatureAlgorithm algorithm) {
+    super (algorithm, true);    // true means draw a scale at the bottom of
+                                // the graph
+
+    getFeature ().addFeatureChangeListener (this);
+
+    recalculate_flag = true;
+  }
+
+  /**
+   *  Used by getPreferredSize () and getMinimumSize ();
+   **/
+  private final static int HEIGHT;
+
+  static {
+    final Integer feature_plot_height =
+      Options.getOptions ().getIntegerProperty ("feature_plot_height");
+
+    if (feature_plot_height == null) {
+      HEIGHT = 160;
+    } else {
+      HEIGHT = feature_plot_height.intValue ();
+    }
+  }
+
+  /**
+   *  Overridden to set the component height to 150.
+   **/
+  public Dimension getPreferredSize() {
+    return (new Dimension(getSize ().width, HEIGHT));
+  }
+  
+  /**
+   *  Overridden to set the component height to 150.
+   **/
+  public Dimension getMinimumSize() {
+    return (new Dimension(getSize ().width, HEIGHT));
+  }
+  
+  /**
+   *  Remove this object as a feature change listener.  (Called by
+   *  FeaturePlotGroup)
+   **/
+  void stopListening () {
+    getFeature ().removeFeatureChangeListener (this);
+  }
+
+  /**
+   *  Implementation of the DisplayAdjustmentListener interface.  Invoked when
+   *  a component scrolls or changes the scale.
+   **/
+  public void displayAdjustmentValueChanged (DisplayAdjustmentEvent event) {
+    start_base = event.getStart ();
+    end_base = event.getEnd ();
+    width_in_bases = event.getWidthInBases ();
+
+    recalculate_flag = true;
+
+    repaintCanvas ();
+  }
+
+  /**
+   *  Implementation of the FeatureChangeListener interface.
+   *  @param event The change event.
+   **/
+  public void featureChanged (FeatureChangeEvent event) {
+    recalculate_flag = true;
+  }
+
+  /**
+   *  Return the algorithm that was passed to the constructor.
+   **/
+  public FeatureAlgorithm getFeatureAlgorithm () {
+    return (FeatureAlgorithm) super.getAlgorithm ();
+  }
+
+  /**
+   *  Return the new start base to display, from the last event.
+   **/
+  private int getStart () {
+    return start_base;
+  }
+
+  /**
+   *  Return the new end base to display, from the last event.
+   **/
+  private int getEnd () {
+    return end_base;
+  }
+
+  /**
+   *  Return the width in bases of the display, from the last event.
+   **/
+  private int getWidthInBases () {
+    return width_in_bases;
+  }
+
+  /**
+   *  This array is used by drawGraph ().  It is reallocated when the scale
+   *  changes.
+   **/
+  private float [][] value_array_array = null;
+
+  /**
+   *  The number of bases to step before each evaluation of the algorithm.
+   *  (Set by recalculateValues ()).
+   **/
+  private int step_size = 0;
+
+  /**
+   *  The maximum of the values in value_array_array.
+   **/
+  private float min_value = Float.MAX_VALUE;
+
+  /**
+   *  The minimum of the values in value_array_array.
+   **/
+  private float max_value = Float.MIN_VALUE;
+
+  /**
+   *  Recalculate the values in value_array_array, step_size, min_value and
+   *  max_value.
+   **/
+  protected void recalculateValues () {
+    final Float algorithm_minimum = getAlgorithm ().getMinimum ();
+    final Float algorithm_maximum = getAlgorithm ().getMaximum ();
+
+    // use the Algorithm specified maximum if there is one - otherwise
+    // calculate it
+    if (algorithm_maximum == null) {
+      max_value = Float.MIN_VALUE;
+    } else {
+      max_value = algorithm_maximum.floatValue ();
+    }
+
+    // use the Algorithm specified minimum if there is one - otherwise
+    // calculate it
+    if (algorithm_minimum == null) {
+      min_value = Float.MAX_VALUE;
+    } else {
+      min_value = algorithm_minimum.floatValue ();
+    }
+
+    final int window_size = getWindowSize ();
+
+    final Integer default_step_size =
+      getAlgorithm ().getDefaultStepSize (window_size);
+
+    if (default_step_size == null) {
+      step_size = window_size;
+    } else {
+      if (default_step_size.intValue () < window_size) {
+        step_size = default_step_size.intValue ();
+      } else {
+        step_size = window_size;
+      }
+    }
+
+    final int unit_count = getEnd () - getStart ();
+
+    // the number of plot points in the graph
+    final int number_of_values =
+      (unit_count - (window_size - step_size)) / step_size;
+
+    if (number_of_values < 2) {
+      // there is nothing to plot
+      value_array_array = null;
+      return;
+    }
+
+    // the number of values that getValues () will return
+    final int get_values_return_count =
+      getFeatureAlgorithm ().getValueCount ();
+
+    if (value_array_array == null) {
+      value_array_array = new float [get_values_return_count][];
+    }
+
+    if (value_array_array[0] == null ||
+        value_array_array[0].length != number_of_values) {
+      for (int i = 0 ; i < value_array_array.length ; ++i) {
+        value_array_array[i] = new float [number_of_values];
+      }
+    } else {
+      // reuse the previous arrays
+    }
+
+    if (!isVisible ()) {
+      return;
+    }
+
+    float [] temp_values = new float [get_values_return_count];
+
+    for (int i = 0 ; i < number_of_values ; ++i) {
+      getFeatureAlgorithm ().getValues (getStart () + i * step_size,
+                                        getStart () + i * step_size +
+                                        window_size - 1,
+                                        temp_values);
+
+      for (int value_index = 0 ;
+           value_index < get_values_return_count ;
+           ++value_index) {
+        final float current_value = temp_values[value_index];
+
+        value_array_array[value_index][i] = current_value;
+
+        // use the Algorithm specified maximum if there is one - otherwise
+        // calculate it
+        if (algorithm_maximum == null) {
+          if (current_value > max_value) {
+            max_value = current_value;
+          }
+        }
+
+        // use the Algorithm specified minimum if there is one - otherwise
+        // calculate it
+        if (algorithm_minimum == null) {
+          if (current_value < min_value) {
+            min_value = current_value;
+          }
+        }
+      }
+    }
+    
+    recalculate_flag = false;
+  }
+
+  /**
+   *  Redraw the graph on the canvas using the algorithm, start_base and
+   *  end_base.  This method plots BaseWindowAlgorithm objects only.
+   *  @param g The object to draw into.
+   **/
+  protected void drawMultiValueGraph (Graphics g) {
+    if (recalculate_flag) {
+      recalculateValues ();
+    }
+    
+    if (value_array_array == null) {
+      // there is nothing to draw - probably because the sequence is too short
+      drawMinMax (g, 0, 1);
+      
+      return;
+    }
+    
+    final int window_size = getWindowSize ();
+
+    // the number of values to plot at each x position
+    final int get_values_return_count =
+      getFeatureAlgorithm ().getValueCount ();
+
+    // the number of x positions to plot points at
+    final int number_of_values = value_array_array[0].length;
+
+    if (number_of_values > 1) {
+      drawGlobalAverage (g, min_value, max_value);
+    }
+
+    for (int value_index = 0 ;
+         value_index < get_values_return_count ;
+         ++value_index) {
+      if (get_values_return_count == 1) {
+        g.setColor (Color.black);
+      } else {
+        switch (value_index) {
+        case 0:
+          g.setColor (new Color (255, 0, 0));
+          break;
+        case 1:
+          g.setColor (new Color (100, 255, 100));
+          break;
+        case 2:
+          g.setColor (new Color (0, 0, 255));
+          break;
+        default:
+          g.setColor (Color.black);
+        }
+      }
+
+      drawPoints (g, min_value, max_value, step_size, window_size,
+                  getCanvas ().getSize ().width,
+                  0, // no offset.
+                  value_array_array[value_index]);
+    }
+
+    drawMinMax (g, min_value, max_value);
+
+    drawScaleLine (g, getStart (), getEnd ());
+
+    final int cross_hair_position = getCrossHairPosition ();
+
+    if (cross_hair_position >= 0) {
+      if (cross_hair_position >= getTranslation ().length ()) {
+        cancelCrossHairs ();
+      } else {
+        drawCrossHair (g, cross_hair_position,
+                       String.valueOf (getPointPosition (cross_hair_position +
+                                                         getStart () - 1)),
+                       0);
+      }
+    }
+  }
+
+  /**
+   *  Get the position in the Feature of the given canvas x position.  This
+   *  amino acid position is the label used when the user clicks the mouse in
+   *  on the canvas (see drawCrossHair ()).
+   **/
+  protected int getPointPosition (final int canvas_x_position) {
+    return canvas_x_position + 1;
+  }
+
+  /**
+   *  Return the feature that this component is plotting.
+   **/
+  private Feature getFeature () {
+    return getFeatureAlgorithm ().getFeature ();
+  }
+
+  /**
+   *  Return the translation of the bases of the feature we are plotting.
+   **/
+  private AminoAcidSequence getTranslation () {
+    return getFeature ().getTranslation ();
+  }
+
+  /**
+   *  The start base to plot, as obtained from the DisplayAdjustmentEvent.
+   **/
+  private int start_base;
+
+  /**
+   *  The end base to plot, as obtained from the DisplayAdjustmentEvent.
+   **/
+  private int end_base;
+
+  /**
+   *  The width in bases of the display, as obtained from the
+   *  DisplayAdjustmentEvent.
+   **/
+  private int width_in_bases;
+}
diff --git a/uk/ac/sanger/artemis/components/FeaturePlotGroup.java b/uk/ac/sanger/artemis/components/FeaturePlotGroup.java
new file mode 100644
index 000000000..ca1244457
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeaturePlotGroup.java
@@ -0,0 +1,337 @@
+/* FeaturePlotGroup.java
+ *
+ * created: Wed Dec 16 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeaturePlotGroup.java,v 1.1 2004-06-09 09:46:43 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.plot.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  This is a super-component containing several FeaturePlot components, each
+ *  of which can toggled off and on.  The component contains a row of toggle
+ *  buttons and then the FeaturePlot components below.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeaturePlotGroup.java,v 1.1 2004-06-09 09:46:43 tjc Exp $
+ **/
+
+public class FeaturePlotGroup extends JFrame
+    implements EntryChangeListener, FeatureChangeListener {
+  /**
+   *  Create a new FeaturePlotGroup component for the given feature.
+   **/
+  public FeaturePlotGroup (Feature feature) {
+    super ("Graphs for: " + feature.getIDString ());
+    this.feature = feature;
+    this.entry = feature.getEntry ();
+
+    // don't repeat an algorithm in this array.
+    final FeatureAlgorithm [] plot_value_producers = {
+      new HydrophobicityAlgorithm (getFeature ()),
+      new HydrophilicityAlgorithm (getFeature ()),
+      new CoilFeatureAlgorithm (getFeature ())
+    };
+
+    final Font font = Options.getOptions ().getFont ();
+
+    setFont (font);
+
+    GridBagLayout gridbag = new GridBagLayout();
+    getContentPane ().setLayout (gridbag);
+
+    GridBagConstraints c = new GridBagConstraints();
+
+    c.anchor = GridBagConstraints.NORTH;
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    c.gridheight = 1;
+    c.weightx = 1;
+    c.fill = GridBagConstraints.BOTH;
+    c.weighty = 1;
+//    c.insets = new Insets (0,0,5,0);
+
+    for (int i = 0 ; i < plot_value_producers.length ; ++i) {
+      final FeatureAlgorithm this_algorithm = plot_value_producers[i];
+
+      final FeaturePlot new_feature_plot = new FeaturePlot (this_algorithm);
+
+      gridbag.setConstraints (new_feature_plot, c);
+      getContentPane ().add (new_feature_plot);
+      new_feature_plot.setVisible (true);
+    }
+
+    getFeature ().getEntry ().addEntryChangeListener (this);
+    getFeature ().addFeatureChangeListener (this);
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        stopListening ();
+        FeaturePlotGroup.this.dispose ();
+      }
+    });
+
+    addComponentListener (new ComponentAdapter () {
+      public void componentResized (ComponentEvent event) {
+        fixScrollbar ();
+        fireAdjustmentEvent (scrollbar.getValue ());
+      }
+    });
+
+    int new_x_size = feature.getTranslation ().length () + SCROLL_BAR_SIZE;
+
+    if (new_x_size > 1000) {
+      new_x_size = 1000;
+    }
+
+    if (new_x_size < 300) {
+      new_x_size = 300;
+    }
+
+    scrollbar = new JScrollBar (Scrollbar.HORIZONTAL);
+    c.fill = GridBagConstraints.HORIZONTAL;
+    c.weighty = 0;
+    gridbag.setConstraints (scrollbar, c);
+    scrollbar.addAdjustmentListener (new AdjustmentListener () {
+      public void adjustmentValueChanged(AdjustmentEvent e) {
+        fireAdjustmentEvent (e.getValue ());
+      }
+    });
+    getContentPane ().add (scrollbar);
+
+    final Component [] children = getContentPane ().getComponents ();
+
+    for (int i = 0 ; i < children.length ; ++i) {
+      if (children[i] instanceof FeaturePlot) {
+        addDisplayAdjustmentListener ((FeaturePlot)children[i]);
+      }
+    }
+
+
+    bottom_button_panel = new JPanel ();
+    c.fill = GridBagConstraints.HORIZONTAL;
+    c.weighty = 0;
+    gridbag.setConstraints (bottom_button_panel, c);
+    getContentPane ().add (bottom_button_panel);
+
+    close_button = new JButton ("Close");
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        stopListening ();
+        FeaturePlotGroup.this.dispose ();
+      }
+    });
+
+    bottom_button_panel.add (close_button);
+
+    pack ();
+
+    // give each FeaturePlot component a height of 200
+    setSize (new_x_size, getSize ().height);
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+
+    show ();
+
+    fixScrollbar ();
+
+    fireAdjustmentEvent (1);
+  }
+
+  /**
+   *  Fudge factor to take the scroll bar width into account when creating
+   *  windows and scrollbars.
+   **/
+  private final int SCROLL_BAR_SIZE = 100;
+
+  /**
+   *  Remove this object as a entry change listener and then call
+   *  stopListening () on each FeaturePlot component.
+   **/
+  private void stopListening () {
+    getEntry ().removeEntryChangeListener (this);
+
+    final Component [] children = getComponents ();
+
+    for (int i = 0 ; i < children.length ; ++i) {
+      if (children[i] instanceof FeaturePlot) {
+        ((FeaturePlot)children[i]).stopListening ();
+      }
+    }
+
+    getFeature ().getEntry ().removeEntryChangeListener (this);
+    getFeature ().removeFeatureChangeListener (this);
+  }
+
+  /**
+   *  Set the scrollbar maximum, minimum and value.
+   **/
+  private void fixScrollbar () {
+    final int feature_length = getFeature ().getTranslation ().length ();
+
+    int scroll_max = feature_length;
+
+    if (scroll_max < 1) {
+      scroll_max = 1;
+    }
+
+    scrollbar.setValues (1, getSize ().width,
+                         1, scroll_max + SCROLL_BAR_SIZE);
+
+    scrollbar.setBlockIncrement (getSize ().width);
+  }
+
+  /**
+   *  Adds the specified event adjustemnt listener to receive adjustment
+   *  change events from this object.
+   *  @param l the event change listener.
+   **/
+  private  void addDisplayAdjustmentListener (DisplayAdjustmentListener l) {
+    adjustment_listener_list.addElement (l);
+  }
+
+  /**
+   *  Removes the specified event listener so that it no longer receives
+   *  adjustment change events from this object.
+   *  @param l the event change listener.
+   **/
+  private  void removeDisplayAdjustmentListener (DisplayAdjustmentListener l) {
+    adjustment_listener_list.removeElement (l);
+  }
+
+  /**
+   *  Implementation of the FeatureChangeListener interface.
+   *  @param event The change event.
+   **/
+  public void featureChanged (FeatureChangeEvent event) {
+    fixScrollbar ();
+    fireAdjustmentEvent (scrollbar.getValue ());
+  }
+
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so we can delete this component if the feature gets
+   *  deleted.
+   **/
+  public void entryChanged (EntryChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryChangeEvent.FEATURE_DELETED:
+      if (event.getFeature () == getFeature ()) {
+        stopListening ();
+        dispose ();
+      }
+      break;
+    default:
+      // do nothing;
+      break;
+    }
+  }
+
+  /**
+   *  Send a DisplayAdjustmentEvent to the objects that are listening for it.
+   **/
+  private void fireAdjustmentEvent (final int scroll_value) {
+    final int feature_length = getFeature ().getTranslation ().length ();
+
+    int end_value = scroll_value + getSize ().width;
+
+    if (end_value > feature_length) {
+      end_value = feature_length;
+    }
+
+    final DisplayAdjustmentEvent event =
+      new DisplayAdjustmentEvent (this,
+                                  scroll_value,
+                                  end_value,
+                                  getSize ().width,
+                                  1,
+                                  0,// this arg will be ignored
+                                  false,
+                                  DisplayAdjustmentEvent.SCALE_ADJUST_EVENT);
+
+    final Vector targets;
+
+    // copied from a book - synchronising the whole method might cause a
+    // deadlock
+    synchronized (this) {
+      targets = (Vector) adjustment_listener_list.clone ();
+    }
+
+    for ( int i = 0 ; i < targets.size () ; ++i ) {
+      DisplayAdjustmentListener target =
+        (DisplayAdjustmentListener) targets.elementAt (i);
+
+      target.displayAdjustmentValueChanged (event);
+    }
+  }
+
+  /**
+   *  Return the feature that this component is plotting.
+   **/
+  private Feature getFeature () {
+    return feature;
+  }
+
+  /**
+   *  Return the Entry that contains the Feature this object is displaying.
+   **/
+  private Entry getEntry () {
+    return entry;
+  }
+
+  /**
+   *  The feature we are plotting.
+   **/
+  private Feature feature;
+
+  /**
+   *  The Entry that contains the Feature this object is displaying.
+   **/
+  private Entry entry;
+
+  /**
+   *  Pressing this button will distroy the JFrame.
+   **/
+  private JButton close_button;
+
+  /**
+   *  A JPanel to hold the buttons.
+   **/
+  private JPanel bottom_button_panel;
+
+  private JScrollBar scrollbar;
+
+  /**
+   *  A vector of those objects listening for adjustment events.
+   **/
+  final private Vector adjustment_listener_list = new Vector ();
+}
diff --git a/uk/ac/sanger/artemis/components/FeaturePopup.java b/uk/ac/sanger/artemis/components/FeaturePopup.java
new file mode 100644
index 000000000..f978e2a63
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeaturePopup.java
@@ -0,0 +1,758 @@
+/* FeaturePopup.java
+ *
+ * created: Wed Oct 21 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 1998,1999,2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeaturePopup.java,v 1.1 2004-06-09 09:46:45 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.util.StringVector;
+
+import java.io.*;
+import java.awt.MenuItem;
+import java.awt.CheckboxMenuItem;
+import java.awt.event.*;
+import javax.swing.*;
+
+/**
+ *  FeaturePopup class
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeaturePopup.java,v 1.1 2004-06-09 09:46:45 tjc Exp $
+ *
+ **/
+
+public class FeaturePopup extends JPopupMenu 
+{
+
+  /**
+   *  The reference of the EntryGroup object that was passed to the
+   *  constructor.
+   **/
+  private EntryGroup entry_group;
+
+  /**
+   *  This is the Selection object that was passed to the constructor.
+   **/
+  final private Selection selection;
+
+  /**
+   *  This is a reference to the GotoEventSource object that was passed to the
+   *  constructor.
+   **/
+  private GotoEventSource goto_event_source;
+
+  /**
+   *  The reference of the object that created this popup.
+   **/
+  private DisplayComponent owner;
+
+  /**
+   *  If the parent component of this popup is a FeatureDisplay then this will
+   *  contain it's reference.
+   **/
+  private FeatureDisplay feature_display = null;
+
+  /**
+   *  If the parent component of this popup is a FeatureList then this will
+   *  contain it's reference.
+   **/
+  private FeatureList feature_list = null;
+
+  /**
+   *  Set by the constructor to be the(possibly) empty vector of selected
+   *  features.
+   **/
+  private FeatureVector selection_features;
+
+  /**
+   *  Set by the constructor to be the(possibly) empty vector of selected
+   *  features.
+   **/
+  private FeatureSegmentVector selection_segments;
+
+  private JCheckBoxMenuItem show_labels_item = null;
+  private JCheckBoxMenuItem one_line_per_entry_item = null;
+  private JCheckBoxMenuItem show_forward_frame_lines_item = null;
+  private JCheckBoxMenuItem show_reverse_frame_lines_item = null;
+  private JCheckBoxMenuItem show_start_codons_item = null;
+  private JCheckBoxMenuItem show_stop_codons_item = null;
+  private JCheckBoxMenuItem show_feature_arrows_item = null;
+  private JCheckBoxMenuItem show_feature_borders_item = null;
+  private JCheckBoxMenuItem frame_features_item = null;
+  private JCheckBoxMenuItem source_features_item = null;
+  private JCheckBoxMenuItem rev_comp_display_item = null;
+  private JCheckBoxMenuItem base_colours_item = null;
+  private JCheckBoxMenuItem correlation_scores_item = null;
+  private JCheckBoxMenuItem show_genes_item = null;
+  private JCheckBoxMenuItem show_products_item = null;
+  private JCheckBoxMenuItem show_qualifiers_item = null;
+  private JMenuItem entry_group_menu_item = null;
+  private JMenuItem select_menu_item = null;
+  private JMenuItem add_menu_item = null;
+  private JMenuItem view_menu_item = null;
+  private JMenuItem edit_menu_item = null;
+  private JMenuItem goto_menu_item = null;
+  private JMenuItem write_menu_item = null;
+  private JMenuItem run_menu_item = null;
+  private JMenuItem broadcast_item = null;
+  private JMenuItem raise_feature_item = null;
+  private JMenuItem lower_feature_item = null;
+  private JMenuItem smallest_to_front_item = null;
+  private JMenuItem zoom_to_selection_item = null;
+  private JMenuItem score_cutoffs_item = null;
+  private JMenuItem select_visible_range = null;
+  private JMenuItem save_feature_list_item = null;
+  private JMenuItem select_visible_features = null;
+
+  private BasePlotGroup base_plot_group = null;
+
+  /**
+   *  Create a new FeaturePopup object.
+   *  @param owner The component where this popup was popped up from.
+   *  @param selection The selection to use for this popup.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public FeaturePopup(final DisplayComponent owner,
+                      final EntryGroup entry_group,
+                      final Selection selection,
+                      final GotoEventSource goto_event_source,
+                      final BasePlotGroup base_plot_group) 
+  {
+    super(getMenuName(owner));
+
+    this.owner = owner;
+    this.entry_group = entry_group;
+    this.selection = selection;
+    this.goto_event_source = goto_event_source;
+    this.base_plot_group = base_plot_group;
+
+    selection_features = selection.getSelectedFeatures();
+    selection_segments = selection.getSelectedSegments();
+
+    makeSubMenus();
+
+    addGenericItems();
+
+    if(owner instanceof FeatureDisplay) 
+    {
+      feature_display =(FeatureDisplay) owner;
+      addFeatureDisplayItems();
+    } 
+    else 
+    {
+      // must be a FeatureList
+      feature_list =(FeatureList) owner;
+      addFeatureListItems();
+    }
+
+    maybeAdd(raise_feature_item);
+    maybeAdd(lower_feature_item);
+    maybeAdd(smallest_to_front_item);
+    maybeAdd(zoom_to_selection_item);
+    maybeAdd(select_visible_range);
+    maybeAdd(select_visible_features);
+    maybeAdd(score_cutoffs_item);
+    maybeAdd(save_feature_list_item);
+    addSeparator();
+    maybeAdd(entry_group_menu_item);
+    maybeAdd(select_menu_item);
+    maybeAdd(goto_menu_item);
+    maybeAdd(view_menu_item);
+    maybeAdd(edit_menu_item);
+    maybeAdd(add_menu_item);
+    maybeAdd(write_menu_item);
+    maybeAdd(run_menu_item);
+    addSeparator();
+    maybeAdd(show_labels_item);
+    maybeAdd(one_line_per_entry_item);
+    maybeAdd(show_forward_frame_lines_item);
+    maybeAdd(show_reverse_frame_lines_item);
+    maybeAdd(show_start_codons_item);
+    maybeAdd(show_stop_codons_item);
+    maybeAdd(show_feature_arrows_item);
+    maybeAdd(show_feature_borders_item);
+    maybeAdd(frame_features_item);
+    maybeAdd(source_features_item);
+    maybeAdd(rev_comp_display_item);
+    maybeAdd(base_colours_item);
+    maybeAdd(correlation_scores_item);
+    maybeAdd(show_genes_item);
+    maybeAdd(show_qualifiers_item);
+    maybeAdd(show_products_item);
+    addSeparator();
+    maybeAdd(broadcast_item);
+  }
+
+  /**
+   *  Rename the name String to use for this JMenu.
+   **/
+  private static String getMenuName(final DisplayComponent owner) 
+  {
+    if(owner instanceof FeatureDisplay) 
+      return "Feature Viewer JMenu";
+    else
+      return "Feature List JMenu";
+  }
+
+  /**
+   *  Add an item only if it isn't null.
+   **/
+  private void maybeAdd(JMenuItem item) 
+  {
+    if(item != null) 
+      add(item);
+  }
+
+  /**
+   *  Create those menu items that are relevant to all components.
+   **/
+  private void addGenericItems() 
+  {
+    if(selection_features.size() > 0 || selection_segments.size() > 0) {
+
+    }
+  }
+
+  /**
+   *  Create the Edit, Add and View sub menus.
+   **/
+  public void makeSubMenus() 
+  {
+    final JFrame frame = owner.getParentFrame();
+    entry_group_menu_item = new EntryGroupMenu(frame, getEntryGroup());
+
+    select_menu_item = new SelectMenu(frame, selection,
+                                      getGotoEventSource(),
+                                      getEntryGroup(),
+                                      base_plot_group);
+
+    view_menu_item = new ViewMenu(frame, selection,
+                                  getGotoEventSource(),
+                                  getEntryGroup(),
+                                  base_plot_group);
+
+    goto_menu_item = new GotoMenu(frame, selection,
+                                  getGotoEventSource(),
+                                  getEntryGroup());
+
+    if(Options.readWritePossible()) 
+    {
+      edit_menu_item = new EditMenu(frame, selection,
+                                    getGotoEventSource(),
+                                    getEntryGroup(),
+                                    base_plot_group);
+      if(entry_group instanceof SimpleEntryGroup) 
+        add_menu_item = new AddMenu(frame, selection,
+                                    getEntryGroup(),
+                                    getGotoEventSource(),
+                                    base_plot_group);
+
+      write_menu_item = new WriteMenu(frame, selection,
+                                      getEntryGroup());
+      if(Options.isUnixHost()) 
+        run_menu_item = new RunMenu(frame, selection);
+    }
+  }
+
+  /**
+   *  Create those menu items that are relevant only to FeatureDisplay objects.
+   **/
+  private void addFeatureDisplayItems() 
+  {
+    show_start_codons_item = new JCheckBoxMenuItem("Start Codons");
+    show_start_codons_item.setState(feature_display.getShowStartCodons());
+    show_start_codons_item.addItemListener(new ItemListener()
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        feature_display.setShowStartCodons(show_start_codons_item.getState());
+      }
+    });
+
+    show_stop_codons_item = new JCheckBoxMenuItem("Stop Codons");
+    show_stop_codons_item.setState(feature_display.getShowStopCodons());
+    show_stop_codons_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        feature_display.setShowStopCodons(show_stop_codons_item.getState());
+      }
+    });
+
+    show_feature_arrows_item = new JCheckBoxMenuItem("Feature Arrows");
+    show_feature_arrows_item.setState(feature_display.getShowFeatureArrows());
+    show_feature_arrows_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        feature_display.setShowFeatureArrows(show_feature_arrows_item.getState());
+      }
+    });
+
+    show_feature_borders_item = new JCheckBoxMenuItem("Feature Borders");
+    show_feature_borders_item.setState(feature_display.getShowFeatureBorders());
+    show_feature_borders_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        feature_display.setShowFeatureBorders(show_feature_borders_item.getState());
+      }
+    });
+
+    show_labels_item = new JCheckBoxMenuItem("Feature Labels");
+    show_labels_item.setState(feature_display.getShowLabels());
+    show_labels_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_display.setShowLabels(show_labels_item.getState());
+      }
+    });
+
+    one_line_per_entry_item = new JCheckBoxMenuItem("One Line Per Entry");
+    one_line_per_entry_item.setState(feature_display.getOneLinePerEntryFlag());
+    one_line_per_entry_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        final boolean new_state = one_line_per_entry_item.getState();
+        if(new_state && getEntryGroup().size() > 8) 
+          feature_display.setShowLabels(false);
+        feature_display.setOneLinePerEntry(new_state);
+      }
+    });
+
+    show_forward_frame_lines_item =
+      new JCheckBoxMenuItem("Forward Frame Lines");
+    show_forward_frame_lines_item.setState(feature_display.getShowForwardFrameLines());
+    show_forward_frame_lines_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_display.setShowForwardFrameLines(show_forward_frame_lines_item.getState());
+      }
+    });
+
+    show_reverse_frame_lines_item =
+      new JCheckBoxMenuItem("Reverse Frame Lines");
+    show_reverse_frame_lines_item.setState(feature_display.getShowReverseFrameLines());
+    show_reverse_frame_lines_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_display.setShowReverseFrameLines(show_reverse_frame_lines_item.getState());
+      }
+    });
+
+    frame_features_item = new JCheckBoxMenuItem("All Features On Frame Lines");
+    frame_features_item.setState(feature_display.getFrameFeaturesFlag());
+    frame_features_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_display.setFrameFeaturesFlag(frame_features_item.getState());
+      }
+    });
+
+    source_features_item = new JCheckBoxMenuItem("Show Source Features");
+    source_features_item.setState(feature_display.getShowSourceFeatures());
+    source_features_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_display.setShowSourceFeatures(source_features_item.getState());
+      }
+    });
+
+    rev_comp_display_item = new JCheckBoxMenuItem("Flip Display");
+ 
+    rev_comp_display_item.setState(feature_display.isRevCompDisplay());
+    rev_comp_display_item.addItemListener(new ItemListener()
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_display.setRevCompDisplay(rev_comp_display_item.getState());
+      }
+    });
+
+    base_colours_item = new JCheckBoxMenuItem("Colourise Bases");
+    base_colours_item.setState(feature_display.getShowBaseColours());
+    base_colours_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_display.setShowBaseColours(base_colours_item.getState());
+      }
+    });
+
+    smallest_to_front_item =
+      new JMenuItem("Smallest Features In Front");
+    smallest_to_front_item.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent e) 
+      {
+        // clear the selection because selected features will always be on
+        // top - which is not usually what is wanted
+        selection.clear();
+        feature_display.smallestToFront();
+      }
+    });
+
+    score_cutoffs_item = new JMenuItem("Set Score Cutoffs ...");
+
+    score_cutoffs_item.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent e) 
+      {
+        final ScoreChangeListener minimum_listener =
+          new ScoreChangeListener() 
+          {
+            public void scoreChanged(final ScoreChangeEvent event) 
+            {
+              feature_display.setMinimumScore(event.getValue());
+            }
+          };
+
+        final ScoreChangeListener maximum_listener =
+          new ScoreChangeListener() 
+          {
+            public void scoreChanged(final ScoreChangeEvent event) 
+            {
+              feature_display.setMaximumScore(event.getValue());
+            }
+          };
+
+        final ScoreChanger score_changer =
+          new ScoreChanger("Score Cutoffs",
+                            minimum_listener, maximum_listener,
+                            0, 100);
+
+        score_changer.setVisible(true);
+      }
+    });
+
+    if(selection_features.size() > 0 || selection_segments.size() > 0) 
+    {
+      raise_feature_item = new JMenuItem("Raise Selected Features");
+      raise_feature_item.addActionListener(new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          raiseSelection();
+        }
+      });
+
+      lower_feature_item = new JMenuItem("Lower Selected Features");
+      lower_feature_item.addActionListener(new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          lowerSelection();
+        }
+      });
+    }
+
+    if(!selection.isEmpty()) 
+    {
+      zoom_to_selection_item = new JMenuItem("Zoom to Selection");
+      zoom_to_selection_item.addActionListener(new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          zoomToSelection((FeatureDisplay) owner);
+        }
+      });
+    }
+
+
+    select_visible_range =
+      new JMenuItem("Select Visible Range");
+    select_visible_range.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent e) 
+      {
+        selection.setMarkerRange(feature_display.getVisibleMarkerRange());
+      }
+    });
+
+    select_visible_features =
+      new JMenuItem("Select Visible Features");
+    select_visible_features.addActionListener(new ActionListener() {
+      public void actionPerformed(ActionEvent e) {
+        selection.set(feature_display.getCurrentVisibleFeatures());
+      }
+    });
+  }
+
+  /**
+   *  Create those menu items that are relevant only to FeatureList objects.
+   **/
+  private void addFeatureListItems() 
+  {
+    if(Options.getOptions().readWritePossible()) 
+    {
+      save_feature_list_item = new JMenuItem("Save List To File ...");
+      save_feature_list_item.addActionListener(new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent e) 
+        {
+          saveFeatureList();
+        }
+      });
+    }
+
+    correlation_scores_item = new JCheckBoxMenuItem("Show Correlation Scores");
+    correlation_scores_item.setState(feature_list.getCorrelationScores());
+    correlation_scores_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_list.setCorrelationScores(correlation_scores_item.getState());
+      }
+    });
+
+    show_genes_item = new JCheckBoxMenuItem("Show Gene Names");
+    show_genes_item.setState(feature_list.getShowGenes());
+    show_genes_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_list.setShowGenes(show_genes_item.getState());
+      }
+    });
+
+    show_products_item = new JCheckBoxMenuItem("Show Products");
+    show_products_item.setState(feature_list.getShowProducts());
+    show_products_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        if(show_products_item.getState()) 
+          feature_list.setShowQualifiers(false);
+        
+        feature_list.setShowProducts(show_products_item.getState());
+      }
+    });
+    
+    show_qualifiers_item = new JCheckBoxMenuItem("Show Qualifiers");
+    show_qualifiers_item.setState(feature_list.getShowQualifiers());
+    show_qualifiers_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent e) 
+      {
+        feature_list.setShowQualifiers(show_qualifiers_item.getState());
+        if(show_qualifiers_item.getState()) 
+          feature_list.setShowProducts(false);
+      }
+    });
+  }
+
+  /**
+   *  Save the text of the feature list to a file.
+   **/
+  private void saveFeatureList() 
+  {
+    final JFrame frame = owner.getParentFrame();
+    final StickyFileChooser file_dialog = new StickyFileChooser();
+
+    file_dialog.setDialogTitle("Choose save file ...");
+    file_dialog.setDialogType(JFileChooser.SAVE_DIALOG);
+    final int status = file_dialog.showOpenDialog(frame);
+
+    if(status != JFileChooser.APPROVE_OPTION ||
+       file_dialog.getSelectedFile() == null) 
+      return;
+
+    final File write_file =
+      new File(file_dialog.getCurrentDirectory(),
+                file_dialog.getSelectedFile().getName());
+    
+    if(write_file.exists()) 
+    {
+      final YesNoDialog yes_no_dialog =
+        new YesNoDialog(frame,
+                         "this file exists: " + write_file +
+                         " overwrite it?");
+      if(yes_no_dialog.getResult()) 
+      {
+        // yes - continue
+      }
+      else 
+        return;
+    }
+    
+    try 
+    {
+      final PrintWriter writer =
+        new PrintWriter(new FileWriter(write_file));
+      
+      final StringVector list_strings = feature_list.getListStrings();
+      
+      for(int i = 0 ; i < list_strings.size() ; ++i) 
+        writer.println(list_strings.elementAt(i));
+      
+      writer.close();
+    } 
+    catch(IOException e) 
+    {
+      new MessageDialog(frame, "error while writing: " + e.getMessage());
+    }
+  }
+
+  /**
+   *  Raise the selected features. (FeatureDisplay only.)
+   **/
+  private void raiseSelection() 
+  {
+    final FeatureVector features_to_raise = selection.getAllFeatures();
+
+    for(int i = 0 ; i < features_to_raise.size() ; ++i) 
+    {
+      final Feature selection_feature = features_to_raise.elementAt(i);
+      feature_display.raiseFeature(selection_feature);
+    }
+  }
+
+  /**
+   *  Lower the selected features. (FeatureDisplay only.)
+   **/
+  private void lowerSelection() 
+  {
+    final FeatureVector features_to_lower = selection.getAllFeatures();
+
+    for(int i = 0 ; i < features_to_lower.size() ; ++i) 
+    {
+      final Feature selection_feature = features_to_lower.elementAt(i);
+      feature_display.lowerFeature(selection_feature);
+    }
+  }
+
+  /**
+   *  Zoom the FeatureDisplay to the selection.
+   **/
+  static void zoomToSelection(final FeatureDisplay feature_display) 
+  {
+    final Selection selection = feature_display.getSelection();
+
+    if(selection.isEmpty()) 
+      return;
+
+    // why bother in this case?
+    if(feature_display.getEntryGroup().getSequenceLength() < 1000) 
+      return;
+
+    int first_base;
+    int last_base;
+
+    final FeatureSegmentVector segments = selection.getSelectedSegments();
+
+    if(segments.size() == 1) 
+    {
+      // special case - zoom to the feature instead
+      first_base = segments.elementAt(0).getFeature().getRawFirstBase();
+      last_base  = segments.elementAt(0).getFeature().getRawLastBase();
+    }
+    else
+    {
+      first_base = selection.getLowestBaseOfSelection().getRawPosition();
+      last_base  = selection.getHighestBaseOfSelection().getRawPosition();
+    }
+
+    if(first_base < 250) 
+      first_base = 250;
+    else 
+      first_base -= 250;
+
+    last_base += 250;
+
+    feature_display.setFirstAndLastBase(first_base, last_base);
+  }
+
+  /**
+   *  Return the EntryGroup object that this FeatureDisplay is displaying.
+   **/
+  private EntryGroup getEntryGroup() 
+  {
+    return entry_group;
+  }
+
+  /**
+   *  Return an object that implements the GotoEventSource interface and is
+   *  for the sequence that this DisplayComponent is displaying.
+   **/
+  public GotoEventSource getGotoEventSource() 
+  {
+    return goto_event_source;
+  }
+
+  /**
+   *  Return a new CheckboxMenuItem unless the VM is 1.2 or worse(ie. 1.3 or
+   *  1.4) on GNU/Linux, in which case return a new ArtemisCheckboxMenuItem
+   **/
+  private MenuItem makeCheckboxMenuItem(final String item_name) 
+  {
+    if(Options.getOptions().isBuggyLinuxVM() &&
+        Options.getOptions().getPropertyTruthValue("buggy_linux_vm_fix")) 
+      return new ArtemisCheckboxMenuItem(item_name);
+    else 
+      return new CheckboxMenuItem(item_name);
+  }
+
+  /**
+   *  Add a ItemListener to a ArtemisCheckboxMenuItem or a CheckboxMenuItem.
+   **/
+  private void addMenuItemListener(final MenuItem menu_item,
+                                    final ItemListener listener)
+  {
+    if(menu_item instanceof ArtemisCheckboxMenuItem) 
+     ((ArtemisCheckboxMenuItem)menu_item).addItemListener(listener);
+    else 
+     ((CheckboxMenuItem)menu_item).addItemListener(listener);
+  }
+
+  /**
+   *  Set the state of a ArtemisCheckboxMenuItem or a CheckboxMenuItem.
+   **/
+  private void setCheckboxMenuItemState(final MenuItem menu_item,
+                                         final boolean state) 
+  {
+    if(menu_item instanceof ArtemisCheckboxMenuItem) 
+     ((ArtemisCheckboxMenuItem)menu_item).setState(state);
+    else 
+     ((CheckboxMenuItem)menu_item).setState(state);
+  }
+
+  /**
+   *  Get the state of a ArtemisCheckboxMenuItem or a CheckboxMenuItem.
+   **/
+  private boolean getCheckboxMenuItemState(final MenuItem menu_item) 
+  {
+    if(menu_item instanceof ArtemisCheckboxMenuItem) 
+      return((ArtemisCheckboxMenuItem)menu_item).getState();
+    else 
+      return((CheckboxMenuItem)menu_item).getState();
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/FeatureViewer.java b/uk/ac/sanger/artemis/components/FeatureViewer.java
new file mode 100644
index 000000000..3a4afd0db
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FeatureViewer.java
@@ -0,0 +1,151 @@
+/* FeatureViewer.java
+ *
+ * created: Thu Nov 19 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FeatureViewer.java,v 1.1 2004-06-09 09:46:46 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import java.io.*;
+import java.awt.event.*;
+
+/**
+ *  A viewer for Feature objects.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FeatureViewer.java,v 1.1 2004-06-09 09:46:46 tjc Exp $
+ *
+ **/
+
+public class FeatureViewer
+    implements EntryChangeListener, FeatureChangeListener {
+  /**
+   *  Create a new FeatureViewer object from the given Feature.
+   **/
+  public FeatureViewer (Feature view_feature) {
+    this.view_feature = view_feature;
+    this.entry = view_feature.getEntry ();
+    
+    file_viewer = new FileViewer ("Artemis Feature View: " +
+                                  view_feature.getIDString ());
+    readFeature (view_feature);
+
+    view_feature.getEntry ().addEntryChangeListener (this);
+    view_feature.addFeatureChangeListener (this);
+
+    file_viewer.addWindowListener (new WindowAdapter () {
+      public void windowClosed (WindowEvent event) {
+        stopListening ();
+      }
+    });
+  }
+
+  /**
+   *  Remove this object as a feature and entry change listener.
+   **/
+  public void stopListening () {
+    getEntry ().removeEntryChangeListener (this);
+    getFeature ().removeFeatureChangeListener (this);
+  }
+  
+  /**
+   *  Implementation of the EntryChangeListener interface.  We listen to
+   *  EntryChange events so we can delete this component if the feature gets
+   *  deleted.
+   **/
+  public void entryChanged (EntryChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryChangeEvent.FEATURE_DELETED:
+      if (event.getFeature () == view_feature) {
+        stopListening ();
+        file_viewer.dispose ();
+      }
+      break;
+    default:
+      // do nothing;
+      break;
+    }
+  }
+
+  /**
+   *  Implementation of the FeatureChangeListener interface.  We need to
+   *  listen to feature change events from the Features in this object so that
+   *  we can keep the display up to date.
+   *  @param event The change event.
+   **/
+  public void featureChanged (FeatureChangeEvent event) {
+    // re-read the information from the feature
+    readFeature (view_feature);
+  }
+
+  /**
+   *  Read the given Feature into this FeatureViewer object.
+   **/
+  public void readFeature (Feature feature) {
+    try {
+      file_viewer.clear ();
+      file_viewer.appendFile (view_feature.toReader ());
+    } catch (uk.ac.sanger.artemis.io.ReadFormatException e) {
+      throw new Error ("internal error - unexpected exception: " +
+                       e.getMessage () +
+                       (e.getLineNumber () > 1 ?
+                        " at line " + e.getLineNumber () :
+                        ""));
+    } catch (IOException e) {
+      throw new Error ("internal error - unexpected exception: " +
+                       e.getMessage ());
+    }
+  }
+
+  /**
+   *  Return the Feature we are viewing.
+   **/
+  public Feature getFeature () {
+    return view_feature;
+  }
+  
+  /**
+   *  Return the Entry that contains the Feature this object is displaying.
+   **/
+  private Entry getEntry () {
+    return entry;
+  }
+
+  /**
+   *  The Feature this object is displaying.
+   **/
+  private Feature view_feature;
+
+  /**
+   *  The Entry that contains the Feature this object is displaying.
+   **/
+  private Entry entry;
+
+  /**
+   *  The FileViewer object that is displaying the feature.
+   **/
+  private FileViewer file_viewer;
+}
+
+
diff --git a/uk/ac/sanger/artemis/components/FileDialogEntrySource.java b/uk/ac/sanger/artemis/components/FileDialogEntrySource.java
new file mode 100644
index 000000000..1a8ea9fd8
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FileDialogEntrySource.java
@@ -0,0 +1,286 @@
+/* FileDialogEntrySource.java
+ *
+ * created: Thu Jun  8 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FileDialogEntrySource.java,v 1.1 2004-06-09 09:46:49 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.SimpleEntryInformation;
+
+import javax.swing.JFrame;
+
+/**
+ *  This is an EntrySource that reads Entry objects from the local filesystem.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: FileDialogEntrySource.java,v 1.1 2004-06-09 09:46:49 tjc Exp $
+ **/
+
+public class FileDialogEntrySource
+    implements EntrySource 
+{
+   
+   /** 
+   *  The component that created this FileEntrySource.  (Used for requesters.)
+   **/
+  final JFrame frame;
+
+  /** InputStreamProgressEvents are sent to this object. */
+  private final InputStreamProgressListener stream_progress_listener;
+
+  /**
+   *  Create a new FileDialogEntrySource.
+   *  @param frame The component that created this FileDialogEntrySource.
+   *    (Used for requesters.)
+   *  @param listener InputStreamProgressEvent objects will be sent to this
+   *    listener as progress on reading is made.
+   **/
+  public FileDialogEntrySource (final JFrame frame,
+                                final InputStreamProgressListener listener) 
+  {
+    this.frame = frame;
+    this.stream_progress_listener = listener;
+  }
+
+  /**
+   *  Get an Entry object from this source (by reading from a file).
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.
+   *  @return null if and only if the user cancels the read.
+   **/
+  public Entry getEntry(final Bases bases,
+                        final ProgressThread progress_thread,
+                        final boolean show_progress)
+      throws OutOfRangeException
+  {
+    try
+    {
+      return getEntryInternal(bases, progress_thread, show_progress);
+    }
+    catch (NoSequenceException e)
+    {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+  }
+
+
+  /**
+   *  Get an Entry object from this source (by reading from a file).
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.
+   *  @return null if and only if the user cancels the read.
+   **/
+  public Entry getEntry (final Bases bases,
+                         final boolean show_progress)
+      throws OutOfRangeException
+  {
+    try 
+    {
+      return getEntryInternal (bases, null, show_progress);
+    }
+    catch (NoSequenceException e) 
+    {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+  }
+
+  /**
+   *  Get an Entry object from this source (by reading from a file).
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if the entry that we read has no
+   *    sequence.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.
+   *  @return null if and only if the user cancels the read.
+   **/
+  public Entry getEntry (final boolean show_progress)
+      throws OutOfRangeException, NoSequenceException 
+  {
+    return getEntryInternal (null, null, show_progress);
+  }
+
+  /**
+   *  Get an Entry object from this source (by reading from a file).
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if the entry that we read has no
+   *    sequence.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.
+   *  @return null if and only if the user cancels the read.
+   **/
+  public Entry getEntry (final boolean show_progress,
+                         final ProgressThread progress_thread)
+      throws OutOfRangeException, NoSequenceException
+  {
+    return getEntryInternal (null, progress_thread, show_progress);
+  }
+
+
+  /**
+   *  Returns true if and only if this EntrySource always returns "full"
+   *  entries.  ie. entries that contain features and sequence.  Entries that
+   *  are read from a file may contain just features so in this class this
+   *  method returns false.
+   **/
+  public boolean isFullEntrySource () 
+  {
+    return false;
+  }
+  
+
+////  change ArtemisMain.readArgsAndOptions () to use this?:
+
+//    /**
+//     *  Get an Entry object from this source by name (by reading from a file).
+//     *  @exception OutOfRangeException Thrown if one of the features in
+//     *    embl_entry is out of range of the Bases object.
+//     *  @exception NoSequenceException Thrown if the entry that we read has no
+//     *    sequence.
+//     *  @return null if and only if there is no Entry with that name.
+//     **/
+//    public Entry getEntryByName (final String entry_file_name)
+//        throws OutOfRangeException, NoSequenceException, IOException {
+//      final Document new_document =
+//        new FileProgressDocument (new File (entry_file_name),
+//                                  getInputStreamProgressListener ());
+
+//      final EntryInformation new_entry_information =
+//        new SimpleEntryInformation (Options.getArtemisEntryInformation ());
+
+//      boolean seen_error = false;
+
+//      while (true) {
+//        try {
+//          final uk.ac.sanger.artemis.io.Entry new_entry =
+//            DocumentEntryFactory.makeDocumentEntry (new_entry_information,
+//                                                    new_document);
+
+//          return new Entry (new_entry);
+//        } catch (EntryInformationException e) {
+
+//          if (!seen_error) {
+//            final String message =
+//              "warning while reading " + entry_file_name + " - " +
+//              e.getMessage ();
+
+//            System.err.println (message);
+
+//            new MessageDialog (frame, message);
+
+//            seen_error = true;
+//          }
+
+//          EntryFileDialog.handleOpenException (new_entry_information, e);
+
+//          // go around the loop again
+//        }
+//      }
+//    }
+
+  /**
+   *  Make a new Entry.
+   *  @param bases The Bases object to pass to the Entry constructor.  If null
+   *    a new Bases object will be created.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if bases is null and the Entry that
+   *    we read has no sequence.
+   **/
+  private Entry makeEntry (final Bases bases,
+                           final uk.ac.sanger.artemis.io.Entry embl_entry)
+      throws OutOfRangeException, NoSequenceException 
+  {
+    if (bases == null) 
+      return new Entry (embl_entry);
+    else 
+      return new Entry (bases, embl_entry);
+  }
+
+  /**
+   *  Return the InputStreamProgressListener that was passed to the
+   *  constructor.
+   **/
+  public InputStreamProgressListener getInputStreamProgressListener () 
+  {
+    return stream_progress_listener;
+  }
+
+  /**
+   *  Return the name of this source (for display to the user in menus).
+   **/
+  public String getSourceName () 
+  {
+    return "Filesystem";
+  }
+
+  /**
+   *  Implementation of getEntry ().
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if bases is null and the entry that
+   *    we read has no sequence.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.
+   *  @return null if and only if the user cancels the read.
+   **/
+  private Entry getEntryInternal(final Bases bases,
+                                 final ProgressThread progress_thread,
+                                 final boolean show_progress)
+      throws OutOfRangeException, NoSequenceException 
+  {
+    final EntryInformation new_entry_information =
+      new SimpleEntryInformation(Options.getArtemisEntryInformation());
+
+    EntryFileDialog dialog;
+
+    if(bases == null) 
+      dialog = new EntryFileDialog(frame, true);
+    else 
+      dialog = new EntryFileDialog(frame, false);
+
+
+    uk.ac.sanger.artemis.io.Entry new_embl_entry =
+      dialog.getEntry(new_entry_information, stream_progress_listener,
+                      progress_thread, show_progress);
+
+    if(new_embl_entry == null) 
+      return null;
+
+    return makeEntry(bases, new_embl_entry);
+  }
+
+}
+
diff --git a/uk/ac/sanger/artemis/components/FileManager.java b/uk/ac/sanger/artemis/components/FileManager.java
new file mode 100644
index 000000000..54047d835
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FileManager.java
@@ -0,0 +1,401 @@
+/********************************************************************
+*
+*  This library is free software; you can redistribute it and/or
+*  modify it under the terms of the GNU Library General Public
+*  License as published by the Free Software Foundation; either
+*  version 2 of the License, or (at your option) any later version.
+*
+*  This library 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
+*  Library General Public License for more details.
+*
+*  You should have received a copy of the GNU Library General Public
+*  License along with this library; if not, write to the
+*  Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+*  Boston, MA  02111-1307, USA.
+*
+*  Copyright (C) Genome Research Limited
+*
+********************************************************************/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.util.StringVector;
+import uk.ac.sanger.artemis.Options;
+
+import javax.swing.*;
+import java.io.File;
+import java.io.FileFilter;
+import java.awt.event.*;
+import java.awt.geom.*;
+import java.awt.*;
+
+public class FileManager extends JFrame
+{
+
+  /** busy cursor */
+  private Cursor cbusy = new Cursor(Cursor.WAIT_CURSOR);
+  /** done cursor */
+  private Cursor cdone = new Cursor(Cursor.DEFAULT_CURSOR);
+
+  public FileManager(JFrame frame)
+  {
+    this(frame,getArtemisFilter());
+  }
+
+  /**
+  *
+  * File Manager Frame
+  * @param frame  parent frame
+  *
+  */
+  public FileManager(JFrame frame, FileFilter filter)
+  {
+    super("File Manager");
+
+    FileTree ftree  = new FileTree(new File(System.getProperty("user.dir")),
+                                   frame, filter);
+    JScrollPane jsp = new JScrollPane(ftree);
+    JPanel pane = (JPanel)getContentPane();
+    pane.setLayout(new BorderLayout());
+    pane.add(jsp, BorderLayout.CENTER);
+    setJMenuBar(makeMenuBar(pane,ftree));
+    pane.add(getFileFileterComboBox(ftree), BorderLayout.SOUTH);
+
+    Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    jsp.setPreferredSize(new Dimension(210,
+                         (int)(screen.getHeight()/2)));
+    pack();
+    
+    int yloc = (int)((screen.getHeight()-getHeight())/2);
+    setLocation(0,yloc);  
+    setVisible(true);
+  }
+
+  protected JComboBox getFileFileterComboBox(final FileTree ftree)
+  {
+    String[] filters = { "Artemis Files", "Sequence Files", 
+                         "Feature Files", "All Files" };
+    final JComboBox comboFilter = new JComboBox(filters);
+    comboFilter.addActionListener(new ActionListener()
+    {
+      public void actionPerformed(ActionEvent e)
+      {
+        String select = (String)comboFilter.getSelectedItem(); 
+        if(select.equals("Artemis Files"))
+          ftree.setFilter(getArtemisFilter());
+        else if(select.equals("Sequence Files"))
+          ftree.setFilter(getSequenceFilter());
+        else if(select.equals("Feature Files"))
+          ftree.setFilter(getFeatureFilter());
+        else if(select.equals("All Files"))
+        {
+          ftree.setFilter(new FileFilter()
+          {
+            public boolean accept(File pathname)
+            {
+              if(pathname.getName().startsWith("."))
+                return false;
+              return true;
+            }
+          });
+        }
+      }
+    });
+    return comboFilter;
+  }
+
+  /**
+  *
+  * Get a file filter for sequence and feature suffixes.
+  * @return file filter
+  */
+  protected static FileFilter getArtemisFilter()
+  {
+    final StringVector sequence_suffixes =
+      Options.getOptions().getOptionValues("sequence_file_suffixes");
+
+    final StringVector feature_suffixes =
+      Options.getOptions().getOptionValues("feature_file_suffixes");
+
+    final FileFilter artemis_filter = new FileFilter()
+    {
+      public boolean accept(File pathname)
+      {
+        if(pathname.isDirectory() &&
+           !pathname.getName().startsWith("."))
+          return true;
+          
+        for(int i = 0; i<sequence_suffixes.size(); ++i)
+        {
+          final String suffix = sequence_suffixes.elementAt(i);
+
+          if(pathname.getName().endsWith("." + suffix) ||
+             pathname.getName().endsWith("." + suffix + ".gz"))
+            return true;
+        }
+
+        for(int i = 0; i<feature_suffixes.size(); ++i)
+        {
+          final String suffix = feature_suffixes.elementAt(i);
+
+          if(pathname.getName().endsWith("." + suffix) ||
+             pathname.getName().endsWith("." + suffix + ".gz"))
+            return true;
+        }
+        return false;
+      }
+    };
+    return artemis_filter;
+  }
+
+
+  /**
+  *
+  * Get a file filter for feature suffixes.
+  * @return file filter
+  */
+  protected static FileFilter getFeatureFilter()
+  {
+    final StringVector feature_suffixes =
+      Options.getOptions().getOptionValues("feature_file_suffixes");
+
+    final FileFilter feature_filter = new FileFilter()
+    {
+      public boolean accept(File pathname)
+      {
+        if(pathname.isDirectory() &&
+           !pathname.getName().startsWith("."))
+          return true;
+
+        for(int i = 0; i<feature_suffixes.size(); ++i)
+        {
+          final String suffix = feature_suffixes.elementAt(i);
+
+          if(pathname.getName().endsWith("." + suffix) ||
+             pathname.getName().endsWith("." + suffix + ".gz"))
+            return true;
+        }
+        return false;
+      }
+    };
+    return feature_filter;
+  }
+
+  /**
+  *
+  * Get a file filter for sequence suffixes.
+  * @return file filter
+  */
+  protected static FileFilter getSequenceFilter()
+  {
+    final StringVector sequence_suffixes =
+      Options.getOptions().getOptionValues("sequence_file_suffixes");
+
+    final FileFilter seq_filter = new FileFilter()
+    {
+      public boolean accept(File pathname)
+      {
+        if(pathname.isDirectory() &&
+           !pathname.getName().startsWith("."))
+          return true;
+         
+        for(int i = 0; i<sequence_suffixes.size(); ++i)
+        {
+          final String suffix = sequence_suffixes.elementAt(i);
+
+          if(pathname.getName().endsWith("." + suffix) ||
+             pathname.getName().endsWith("." + suffix + ".gz"))
+            return true;
+        }
+
+        return false;
+      }
+    };
+    return seq_filter;
+  }
+
+  /**
+  *
+  * Set up a menu and tool bar
+  * @param pane   panel to add toolbar to
+  * @param ftree  file tree display
+  *
+  */
+  private JMenuBar makeMenuBar(JPanel pane, final FileTree ftree)
+  {
+    JMenuBar mBar = new JMenuBar();
+    JMenu fileMenu = new JMenu("File");
+    mBar.add(fileMenu);
+    
+    JMenuItem fileMenuGoto = new JMenuItem("Go to Directory ...");
+    fileMenuGoto.addActionListener(new ActionListener()
+    {
+      public void actionPerformed(ActionEvent e)
+      {
+        String dir = ftree.getRoot().getAbsolutePath();
+        String newDir = JOptionPane.showInputDialog(FileManager.this,
+                                             "Go to Directory:",dir);      
+
+        if(newDir == null)
+          return;
+       
+        newDir = newDir.trim();
+        File newDirFile = new File(newDir);
+        
+        if(newDirFile.exists() &&
+           newDirFile.canRead() &&
+           !newDir.equals(dir))
+          ftree.newRoot(newDir);
+        else
+        {
+          String error = null;
+          if(!newDirFile.exists())
+            error = new String(newDir+" doesn't exist!");
+          else if(!newDirFile.canRead())
+            error = new String(newDir+" cannot be read!");
+          else if(newDir.equals(dir))
+            error = new String("Same directory!");
+
+          if(error != null)
+            JOptionPane.showMessageDialog(FileManager.this,
+                                        error, "Warning",
+                                        JOptionPane.WARNING_MESSAGE);
+        }
+      }
+    });
+    fileMenu.add(fileMenuGoto);
+    fileMenu.add(new JSeparator());
+    
+    JMenuItem fileMenuClose = new JMenuItem("Close");
+    fileMenuClose.addActionListener(new ActionListener()
+    {
+      public void actionPerformed(ActionEvent e)
+      {
+        setVisible(false);
+      }
+    });
+    fileMenu.add(fileMenuClose);
+
+    // tool bar set up
+    JToolBar toolBar  = new JToolBar();
+    Dimension buttonSize = new Dimension(22,24);
+
+    JButton upBt = new JButton()
+    {
+      public void paintComponent(Graphics g)
+      {
+        super.paintComponent(g);
+        Graphics2D g2 = (Graphics2D)g;
+
+        g2.setColor(new Color(0,128,0));
+        float loc1[][] = { {11,18}, {7,18}, {7,14},
+                           {3,14},  {11,4} };
+                  
+        g2.fill(makeShape(loc1));
+        g2.setColor(Color.green);
+
+        float loc2[][] = { {11,18}, {15,18}, {15,14},
+                           {19,14},  {11,4} };
+        g2.fill(makeShape(loc2));
+
+        setSize(22,24);
+      }
+    };
+    upBt.setPreferredSize(buttonSize);
+    upBt.setMinimumSize(buttonSize);
+
+    upBt.addActionListener(new ActionListener()
+    {
+      public void actionPerformed(ActionEvent e)
+      {
+        FileManager.this.setCursor(cbusy);
+        File root = ftree.getRoot();
+        String parent = root.getParent();
+        if(parent != null)
+          ftree.newRoot(parent);
+        FileManager.this.setCursor(cdone);
+      }
+    });
+    toolBar.add(upBt);
+
+    JButton shortCut1 = new JButton()
+    {
+      public void paintComponent(Graphics g)
+      {
+        super.paintComponent(g);
+        Graphics2D g2 = (Graphics2D)g;
+        Font font = new Font("Monospaced", Font.BOLD, 14);
+        g2.setFont(font);
+
+        g2.setColor(Color.black);
+        g2.drawString("Y",4,18);
+        g2.setColor(Color.red);
+        g2.drawString("P",10,15);
+        setSize(22,24);
+      }
+    };
+    shortCut1.setPreferredSize(buttonSize);
+    shortCut1.setMinimumSize(buttonSize);
+    shortCut1.addActionListener(new ActionListener()
+    {
+      public void actionPerformed(ActionEvent e)
+      {
+        ftree.newRoot("/nfs/disk222/yeastpub");
+      }
+    });
+    toolBar.add(shortCut1);
+
+    JButton homeBt = new JButton()
+    {
+      public void paintComponent(Graphics g)
+      {
+        super.paintComponent(g);
+        Graphics2D g2 = (Graphics2D)g;
+                                                                                
+        g2.setColor(Color.gray);
+        float loc1[][] = { {3,14}, {11,3}, {19,14},
+                           {17,14}, {17,18}, {5,18}, {5,14} };
+        g2.fill(makeShape(loc1));
+                                                                                
+        setSize(22,24);
+      }
+    };
+    homeBt.setPreferredSize(buttonSize);
+    homeBt.setMinimumSize(buttonSize);
+    homeBt.addActionListener(new ActionListener()
+    {
+      public void actionPerformed(ActionEvent e)
+      {
+        ftree.newRoot(System.getProperty("user.home"));
+      }
+    });
+    toolBar.add(homeBt);
+
+    toolBar.add(Box.createVerticalStrut(35));
+    pane.add(toolBar, BorderLayout.NORTH);
+
+    return mBar;
+  }
+
+  /**
+  *
+  * Used to draw a Shape.
+  *
+  */
+  public static GeneralPath makeShape(float loc[][]) 
+  {
+    GeneralPath path = new GeneralPath(GeneralPath.WIND_NON_ZERO);
+
+    path.moveTo(loc[0][0],loc[0][1]);
+
+    for(int i=1; i<loc.length; i++)
+      path.lineTo(loc[i][0],loc[i][1]);
+    
+    return path;
+  }
+
+
+}
+
diff --git a/uk/ac/sanger/artemis/components/FileNode.java b/uk/ac/sanger/artemis/components/FileNode.java
new file mode 100644
index 000000000..084ef31b7
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FileNode.java
@@ -0,0 +1,159 @@
+/********************************************************************
+*
+*  This library is free software; you can redistribute it and/or
+*  modify it under the terms of the GNU Library General Public
+*  License as published by the Free Software Foundation; either
+*  version 2 of the License, or (at your option) any later version.
+*
+*  This library 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
+*  Library General Public License for more details.
+*
+*  You should have received a copy of the GNU Library General Public
+*  License along with this library; if not, write to the
+*  Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+*  Boston, MA  02111-1307, USA.
+*
+*  Copyright (C) Genome Research Limited
+*
+********************************************************************/
+
+package uk.ac.sanger.artemis.components;
+
+import java.awt.datatransfer.*;
+import javax.swing.tree.*;
+import java.io.*;
+import java.util.*;
+
+/**
+*
+* File node for local file tree manager
+*
+*/
+public class FileNode extends DefaultMutableTreeNode 
+                 implements Transferable, Serializable
+{
+    /** true if explored */
+    private boolean explored = false;
+    /** data flavour of a file node */
+    public static DataFlavor FILENODE =
+           new DataFlavor(FileNode.class, "Local file");
+    /** flavours file node and string */
+    static DataFlavor flavors[] = { FILENODE, DataFlavor.stringFlavor };
+
+    /**
+    *
+    * @param file	file node file
+    *
+    */
+    public FileNode(File file)
+    { 
+      setUserObject(file); 
+    }
+
+    /** Determine if this is a directory */
+    public boolean getAllowsChildren() { return isDirectory(); }
+    /** Determine if this is a file */
+    public boolean isLeaf() { return !isDirectory(); }
+    /** Get the File this node represents */
+    public File getFile() { return (File)getUserObject(); }
+    /** Determine if this node has been explored */
+    public boolean isExplored() { return explored; }
+    /** Determine if this is a directory */
+    public boolean isDirectory() 
+    {
+      File file = getFile();
+      return file.isDirectory();
+    }
+
+    /**
+    *
+    * Returns the name of the file 
+    *
+    */
+    public String toString() 
+    {
+      File file = (File)getUserObject();
+      String filename = file.toString();
+      int index = filename.lastIndexOf(File.separator);
+
+      return (index != -1 && index != filename.length()-1) ? 
+                          filename.substring(index+1) : 
+                                            filename;
+    }
+
+    /**
+    *
+    * Explores a directory adding a FileNode for each
+    * child
+    *
+    */
+    public void explore(FileFilter filter) 
+    {
+      if(!isDirectory())
+        return;
+
+      if(!isExplored()) 
+      {
+        File file = getFile();
+        explored = true;
+        File[] children;
+// filter files
+        children = file.listFiles(filter);
+        
+// sort into alphabetic order
+        java.util.Arrays.sort(children);
+        for(int i=0; i < children.length; ++i)
+          add(new FileNode(children[i]));
+      }
+    }
+
+    /**
+    *
+    * Forces the directory to be re-explored
+    *
+    */
+    public void reExplore(FileFilter filter)
+    {
+      explored = false;
+      removeAllChildren();
+      explore(filter);
+    }
+ 
+// Transferable
+    public DataFlavor[] getTransferDataFlavors()
+    {
+      return flavors;
+    }
+
+    public boolean isDataFlavorSupported(DataFlavor f)
+    {
+      if(f.equals(FILENODE) || f.equals(DataFlavor.stringFlavor))
+        return true;
+      return false;
+    }
+
+    public Object getTransferData(DataFlavor d)
+        throws UnsupportedFlavorException, IOException
+    {
+      if(d.equals(FILENODE))
+        return this;
+      else if(d.equals(DataFlavor.stringFlavor))
+        return getFile().getAbsolutePath();
+      else throw new UnsupportedFlavorException(d);
+    }
+
+//Serializable
+   private void writeObject(java.io.ObjectOutputStream out) throws IOException
+   {
+     out.defaultWriteObject();
+   }
+
+   private void readObject(java.io.ObjectInputStream in)
+     throws IOException, ClassNotFoundException
+   {
+     in.defaultReadObject();
+   }
+
+}
diff --git a/uk/ac/sanger/artemis/components/FileTree.java b/uk/ac/sanger/artemis/components/FileTree.java
new file mode 100644
index 000000000..34c646a19
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FileTree.java
@@ -0,0 +1,941 @@
+/********************************************************************
+*
+*  This library is free software; you can redistribute it and/or
+*  modify it under the terms of the GNU Library General Public
+*  License as published by the Free Software Foundation; either
+*  version 2 of the License, or (at your option) any later version.
+*
+*  This library 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
+*  Library General Public License for more details.
+*
+*  You should have received a copy of the GNU Library General Public
+*  License along with this library; if not, write to the
+*  Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+*  Boston, MA  02111-1307, USA.
+*
+*  Copyright (C) Genome Research Limited
+*
+********************************************************************/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.SimpleEntryInformation;
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.util.FileDocument;
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.sequence.NoSequenceException;
+import uk.ac.sanger.artemis.components.MessageDialog;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.datatransfer.*;
+import java.awt.dnd.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.tree.*;
+import java.io.*;
+import java.util.Vector;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+
+/**
+*
+* Creates a local file tree manager. This acts as a drag 
+* source and sink for files.
+*
+*/
+public class FileTree extends JTree implements DragGestureListener,
+                 DragSourceListener, DropTargetListener, ActionListener,
+                 Autoscroll 
+{
+
+  /** root directory */
+  private File root;
+  /** store of directories that are opened */
+  private Vector openNode;
+  /** file separator */
+  private String fs = new String(System.getProperty("file.separator"));
+  /** popup menu */
+  private JPopupMenu popup;
+  /** busy cursor */
+  private Cursor cbusy = new Cursor(Cursor.WAIT_CURSOR);
+  /** done cursor */
+  private Cursor cdone = new Cursor(Cursor.DEFAULT_CURSOR);
+  /** AutoScroll margin */
+  private static final int AUTOSCROLL_MARGIN = 45;
+  /** used by AutoScroll method */
+  private Insets autoscrollInsets = new Insets( 0, 0, 0, 0 );
+  /** file filter */
+  private FileFilter filter = null;
+
+  /**
+  *
+  * @param rt		root directory
+  * @param f		frame
+  *
+  */
+  public FileTree(File rt, final JFrame f,
+                  FileFilter filter)
+  {
+    this.root   = rt;
+    this.filter = filter;
+
+    DragSource dragSource = DragSource.getDefaultDragSource();
+
+    dragSource.createDefaultDragGestureRecognizer(
+       this,                             // component where drag originates
+       DnDConstants.ACTION_COPY_OR_MOVE, // actions
+       this);                            // drag gesture recognizer
+
+    setDropTarget(new DropTarget(this,this));
+    DefaultTreeModel model = createTreeModel(root);
+    setModel(model);
+    createTreeModelListener();
+
+    this.getSelectionModel().setSelectionMode
+                  (TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
+
+    // Popup menu
+    addMouseListener(new PopupListener());
+    popup = new JPopupMenu();
+    
+    JMenuItem menuItem = new JMenuItem("Refresh");
+    menuItem.addActionListener(this);
+    popup.add(menuItem);
+    popup.add(new JSeparator());
+    //open menu
+    JMenu openMenu = new JMenu("Open With");
+    popup.add(openMenu);
+    menuItem = new JMenuItem("Jemboss Alignment Editor");
+    menuItem.addActionListener(this);
+    openMenu.add(menuItem);
+    menuItem = new JMenuItem("Artemis");
+    menuItem.addActionListener(this);
+    openMenu.add(menuItem);
+
+    menuItem = new JMenuItem("Rename...");
+    menuItem.addActionListener(this);
+    popup.add(menuItem);
+    menuItem = new JMenuItem("New Folder...");
+    menuItem.addActionListener(this);
+    popup.add(menuItem);
+    menuItem = new JMenuItem("Delete...");
+    menuItem.addActionListener(this);
+    popup.add(menuItem);
+    popup.add(new JSeparator());
+    menuItem = new JMenuItem("De-select All");
+    menuItem.addActionListener(this);
+    popup.add(menuItem);
+
+    //Listen for when a file is selected
+    MouseListener mouseListener = new MouseAdapter() 
+    {
+      public void mouseClicked(MouseEvent me) 
+      {
+        if(me.getClickCount() == 2 && isFileSelection() &&
+           !me.isPopupTrigger()) 
+        {
+          f.setCursor(cbusy);
+          FileNode node = getSelectedNode();
+          String selected = node.getFile().getAbsolutePath();
+          showFilePane(selected);
+          f.setCursor(cdone);
+        }
+      }
+    };
+    this.addMouseListener(mouseListener);
+
+    addTreeExpansionListener(new TreeExpansionListener()
+    {
+      public void treeCollapsed(TreeExpansionEvent e){} 
+      public void treeExpanded(TreeExpansionEvent e) 
+      {
+        TreePath path = e.getPath();
+        if(path != null) 
+        {
+          FileNode node = (FileNode)path.getLastPathComponent();
+
+          if(!node.isExplored()) 
+          {  
+            f.setCursor(cbusy);
+            exploreNode(node);
+            f.setCursor(cdone);
+          }
+        }
+      }
+    });
+
+  }
+
+
+  /**
+  *
+  * Popup menu actions
+  * @param e	action event
+  * 
+  */
+  public void actionPerformed(ActionEvent e) 
+  {
+
+    JMenuItem source = (JMenuItem)(e.getSource());
+    final FileNode node = getSelectedNode();
+
+    if(source.getText().equals("Refresh")) 
+    {
+      if(node == null)
+        newRoot(root.getAbsolutePath());
+      else if(node.isLeaf())
+        refresh((FileNode)node.getParent());
+      else 
+        refresh(node);
+      return;
+    }
+
+    if(node == null)
+    {
+      JOptionPane.showMessageDialog(null,"No file selected.",
+                        "Warning",
+                        JOptionPane.WARNING_MESSAGE);
+      return;
+    }
+    
+    final File f = node.getFile();
+
+    if(source.getText().equals("Jemboss Alignment Editor"))
+    {
+//    org.emboss.jemboss.editor.AlignJFrame ajFrame =
+//             new org.emboss.jemboss.editor.AlignJFrame(f);
+//    ajFrame.setVisible(true);
+    }
+    else if(source.getText().equals("Text Editor"))
+      showFilePane(f.getAbsolutePath());
+    else if(source.getText().equals("New Folder..."))
+    {
+      String path = null;
+      if(node.isLeaf())
+        path = f.getParent();
+      else
+        path = f.getAbsolutePath();
+
+      String inputValue = JOptionPane.showInputDialog(null,
+                    "Folder Name","Create New Folder in "+path,
+                    JOptionPane.QUESTION_MESSAGE);
+
+      if(inputValue != null && !inputValue.equals("") )
+      {
+        String fullname = path+fs+inputValue;
+        File dir = new File(fullname);
+        
+        if(dir.exists())
+          JOptionPane.showMessageDialog(null, fullname+" alread exists!",
+                                   "Error", JOptionPane.ERROR_MESSAGE);
+        else
+        {
+          if(dir.mkdir())
+            addObject(inputValue,path,node);
+          else
+            JOptionPane.showMessageDialog(null,
+                       "Cannot make the folder\n"+fullname,
+                       "Error", JOptionPane.ERROR_MESSAGE);     
+        }
+      }
+    }
+    else if(source.getText().equals("Delete..."))
+    {
+      File fn[] = getSelectedFiles();
+      String names = "";
+      for(int i=0; i<fn.length;i++)
+         names = names.concat(fn[i].getAbsolutePath()+"\n");
+      int n = JOptionPane.showConfirmDialog(null,
+                                 "Delete \n"+names+"?",
+                                 "Delete Files",
+                                 JOptionPane.YES_NO_OPTION);
+
+      FileNode nodes[] = getSelectedNodes();
+      if(n == JOptionPane.YES_OPTION)
+        for(int i=0; i<nodes.length;i++)
+          deleteFile(nodes[i]);
+    }
+    else if(source.getText().equals("De-select All"))
+      clearSelection();
+    else if(isFileSelection() && source.getText().equals("Rename..."))
+    {
+      String inputValue = (String)JOptionPane.showInputDialog(null,
+                              "New File Name","Rename "+f.getName(), 
+                              JOptionPane.QUESTION_MESSAGE,null,null,f.getName());
+
+      if(inputValue != null && !inputValue.equals("") )
+      {
+        String path = f.getParent();
+        String fullname   = path+fs+inputValue;
+        File newFile = new File(fullname);
+
+        try
+        {
+          renameFile(f,node,newFile.getCanonicalPath());
+        }
+        catch(IOException ioe){}
+      }
+    }
+  }
+
+
+  /**
+  *
+  * Delete a file from the tree
+  * @param node		node to delete
+  *
+  */
+  public void deleteFile(final FileNode node)
+  {
+    File f = node.getFile();
+    if(f.delete())
+    {
+      Runnable deleteFileFromTree = new Runnable()
+      {
+        public void run () { deleteObject(node); };
+      };
+      SwingUtilities.invokeLater(deleteFileFromTree);
+    }
+    else
+      JOptionPane.showMessageDialog(null,"Cannot delete\n"+
+                         f.getAbsolutePath(),"Warning",
+                         JOptionPane.ERROR_MESSAGE);
+  }
+ 
+ 
+  /**
+  *
+  * Method to rename a file and update the filenode's.
+  * @param oldFile 	file to rename
+  * @param oldNode 	filenode to be removed
+  * @param newFullName 	name of the new file
+  *
+  */
+  private void renameFile(final File oldFile, final FileNode oldNode, 
+                          String newFullName)
+  {
+    final File fnew = new File(newFullName);
+    if(fnew.exists())
+      JOptionPane.showMessageDialog(null, newFullName+" alread exists!",
+                               "Warning", JOptionPane.ERROR_MESSAGE);
+    else
+    {
+      if(oldFile.renameTo(fnew))
+      {
+        Runnable renameFileInTree = new Runnable()
+        {
+          public void run ()
+          {
+            addObject(fnew.getName(),fnew.getParent(),oldNode);
+	    deleteObject(oldNode);
+          };
+        };
+        SwingUtilities.invokeLater(renameFileInTree);
+      }
+      else
+        JOptionPane.showMessageDialog(null, 
+                   "Cannot rename \n"+oldFile.getAbsolutePath()+
+                   "\nto\n"+fnew.getAbsolutePath(), "Rename Error",
+                   JOptionPane.ERROR_MESSAGE);
+    }
+    return;
+  }
+
+
+  /**
+  * 
+  * Define a directory root for the file tree
+  * @param newRoot 	directory to use as the root for
+  *        		the tree.
+  *
+  */
+  public void newRoot(String newRoot)
+  {
+    root = new File(newRoot);
+    DefaultTreeModel model = (DefaultTreeModel)getModel();
+    model = createTreeModel(root);
+    setModel(model);
+  }
+
+  /**
+  *
+  * Get the current root node.
+  * @return directory root.
+  *
+  */
+  public File getRoot()
+  {
+    return root;
+  }
+
+  /**
+  *
+  * Refresh
+  * @param FileNode node to refresh 
+  *
+  */
+  public void refresh(FileNode node)
+  {
+    node.reExplore(filter);
+    DefaultTreeModel model = (DefaultTreeModel)getModel();
+    model.nodeStructureChanged(node);
+  }
+
+  /**
+  *
+  * Set the current file filter
+  *
+  */
+  public void setFilter(FileFilter filter)
+  { 
+    this.filter = filter;
+    Enumeration en = openNode.elements();
+
+    while(en.hasMoreElements())
+      refresh((FileNode)en.nextElement());
+  }
+
+  /**
+  *
+  * Get FileNode of selected node
+  * @return     node that is currently selected
+  *
+  */
+  public FileNode getSelectedNode()
+  {
+    TreePath path = getLeadSelectionPath();
+    if(path == null)
+      return null;
+    FileNode node = (FileNode)path.getLastPathComponent();
+    return node;
+  }
+
+
+  /**
+  *
+  * Get FileNodes of selected nodes
+  * @return     node that is currently selected
+  *
+  */
+  public FileNode[] getSelectedNodes()
+  {
+    TreePath path[] = getSelectionPaths();
+    if(path == null)
+      return null;
+
+    int numberSelected = path.length;
+    FileNode nodes[] = new FileNode[numberSelected];
+    for(int i=0;i<numberSelected;i++)
+       nodes[i] = (FileNode)path[i].getLastPathComponent();
+
+    return nodes;
+  }
+
+
+  /**
+  *
+  * Get selected files
+  * @return     node that is currently selected
+  *
+  */
+  public File[] getSelectedFiles()
+  {
+    FileNode[] fn = getSelectedNodes();
+    int numberSelected = fn.length;
+    File files[] = new File[numberSelected];
+    for(int i=0;i<numberSelected;i++)
+       files[i] = fn[i].getFile();
+
+    return files;
+  }
+
+
+  /**
+  *
+  * Return true if selected node is a file
+  * @return true is a file is selected, false if
+  *         a directory is selected
+  *
+  */
+  public boolean isFileSelection()
+  {
+    TreePath path = getLeadSelectionPath();
+    if(path == null)
+      return false;
+
+    FileNode node = (FileNode)path.getLastPathComponent();
+    return node.isLeaf();
+  }
+
+
+  /**
+  *
+  * Make the given directory the root and create a new
+  * DefaultTreeModel.
+  * @param root         root directory
+  * @param              tree model with the root node set
+  *                     to the given directory
+  *
+  */
+  private DefaultTreeModel createTreeModel(File root)
+  {
+    FileNode rootNode = new FileNode(root);
+    rootNode.explore(filter);
+    openNode = new Vector();
+    openNode.add(rootNode);
+    return new DefaultTreeModel(rootNode);
+  }
+
+
+  /**
+  *
+  * Adding a file (or directory) to the file tree manager.
+  * This looks to see if the directory has already been opened
+  * and updates the filetree if it has.
+  * @param child        new child to add in
+  * @param path         path to where child is to be added
+  * @param node         node to add child to
+  *
+  */
+  public DefaultMutableTreeNode addObject(String child,
+                            String path, FileNode node)
+  {
+
+    DefaultTreeModel model = (DefaultTreeModel)getModel();
+    if(node == null)
+    {
+      node = getNode(path);
+      if(node==null)
+        return null;
+    }
+
+    FileNode parentNode = getNode(path);
+    File newleaf = new File(path + fs + child);
+    FileNode childNode = null;
+
+    if(parentNode.isExplored())
+    {
+      childNode = new FileNode(newleaf);
+      int index = getAnIndex(parentNode,child);
+      if(index > -1)
+        model.insertNodeInto(childNode, parentNode, index);
+    }
+    else if(parentNode.isDirectory())
+    {
+      exploreNode(parentNode);
+      childNode = getNode(path + fs + child);
+    }
+
+    // Make sure the user can see the new node.
+    this.scrollPathToVisible(new TreePath(childNode.getPath()));
+    return childNode;
+  }
+
+
+  /**
+  *
+  * Delete a node from the JTree
+  * @param node         node for deletion
+  *
+  */
+  public void deleteObject(FileNode node)
+  {
+    DefaultTreeModel model =(DefaultTreeModel)getModel();
+    FileNode parentNode = getNode(node.getFile().getParent());
+    model.removeNodeFromParent(node);
+  }
+
+
+  /**
+  *
+  * Explore a directory node
+  * @param dirNode      direcory node to display
+  *
+  */
+  public void exploreNode(FileNode dirNode)
+  {
+    DefaultTreeModel model = (DefaultTreeModel)getModel();
+    dirNode.explore(filter);
+    openNode.add(dirNode);
+    model.nodeStructureChanged(dirNode);
+  }
+
+  /**
+  *
+  * Gets the node from the existing explored nodes and their
+  * children.
+  * @param path         path to a file or directory
+  * @return             corresponding node if the directory or
+  *                     file is visible otherwise returns null.
+  *
+  */
+  private FileNode getNode(String path)
+  {
+    Enumeration en = openNode.elements();
+
+    while(en.hasMoreElements())
+    {
+      FileNode node = (FileNode)en.nextElement();
+      String nodeName = node.getFile().getAbsolutePath();
+      if(nodeName.equals(path))
+        return node;
+    }
+
+// check children of explored nodes
+    en = openNode.elements();
+    while(en.hasMoreElements())
+    {
+      FileNode child = getChildNode((FileNode)en.nextElement(),path);
+      if(child != null)
+        return child;
+    }
+
+    return null;
+  }
+
+
+  /**
+  *
+  * Gets the child node of a parent node
+  * @param parent       parent node
+  * @param childName    name of child
+  * @return the child node
+  *
+  */
+  private FileNode getChildNode(FileNode parent, String childName)
+  {
+    for(Enumeration children = parent.children(); children.hasMoreElements() ;)
+    {
+      FileNode childNode = (FileNode)children.nextElement();
+      String nodeName = childNode.getFile().getAbsolutePath();
+      if(childName.equals(nodeName))
+        return childNode;
+    }
+
+    return null;
+  }
+
+  /**
+  *
+  * Finds a new index for adding a new file to the file manager.
+  * @param parentNode   parent directory node
+  * @param child        new child node
+  * @return             index of the child in the directory
+  *
+  */
+  private int getAnIndex(FileNode parentNode, String child)
+  {
+    //find the index for the child
+    int num = parentNode.getChildCount();
+    int childIndex = num;
+    for(int i=0;i<num;i++)
+    {
+      String nodeName =
+            ((FileNode)parentNode.getChildAt(i)).getFile().getName();
+      if(nodeName.compareTo(child) > 0)
+      {
+        childIndex = i;
+        break;
+      }
+      else if (nodeName.compareTo(child) == 0)  //file already exists
+      {
+        childIndex = -1;
+        break;
+      }
+    }
+    return childIndex;
+  }
+
+
+  /**
+  *
+  * Read a file into a byte array.
+  * @param filename     file name
+  * @return             byte[] contents of file
+  *
+  */
+  protected static byte[] readByteFile(String filename)
+  {
+
+    File fn = new File(filename);
+    byte[] b = null;
+    try
+    {
+      long s = fn.length();
+      if(s == 0)
+        return b;
+      b = new byte[(int)s];
+      FileInputStream fi = new FileInputStream(fn);
+      fi.read(b);
+      fi.close();
+    }
+    catch (IOException ioe)
+    {
+      System.out.println("Cannot read file: " + filename);
+    }
+    return b;
+
+  }
+
+  /**
+  *
+  * Opens a JFrame with the file contents displayed.
+  * @param filename     file name to display
+  *
+  */
+  public void showFilePane(final String filename)
+  {
+    final ProgressThread progress_thread = new ProgressThread(null,
+                                                "Loading Entry...");
+
+    progress_thread.start();
+    SwingWorker entryWorker = new SwingWorker()
+    {
+      EntryEdit entry_edit;
+      public Object construct()
+      {
+        try
+        {
+          EntryInformation new_entry_information =
+             new SimpleEntryInformation(Options.getArtemisEntryInformation());
+
+          final Entry entry =  new Entry(EntryFileDialog.getEntryFromFile(
+                         null, new FileDocument(new File(filename)),
+                         new_entry_information, false));
+          if(entry == null)
+            return null;
+
+          final EntryGroup entry_group =
+              new SimpleEntryGroup(entry.getBases());
+
+          entry_group.add(entry);
+          entry_edit = new EntryEdit(entry_group);
+          return null;
+        }
+        catch(NoSequenceException e)
+        {
+          new MessageDialog(null, "read failed: entry contains no sequence");
+        }
+        catch(OutOfRangeException e)
+        {
+          new MessageDialog(null, "read failed: one of the features in " +
+                     " the entry has an out of range " +
+                     "location: " + e.getMessage());
+
+        }
+        catch(NullPointerException npe){}
+
+        return null;
+      }
+
+      public void finished()
+      {
+        if(entry_edit != null)
+          entry_edit.setVisible(true);
+        if(progress_thread !=null)
+          progress_thread.finished();
+      }
+    };
+    entryWorker.start();
+
+  }
+
+
+////////////////////
+// DRAG AND DROP
+////////////////////
+// drag source
+  public void dragGestureRecognized(DragGestureEvent e) 
+  {
+    // ignore if mouse popup trigger
+    InputEvent ie = e.getTriggerEvent();
+    if(ie instanceof MouseEvent) 
+      if(((MouseEvent)ie).isPopupTrigger()) 
+        return;
+    
+    // drag only files 
+    if(isFileSelection())
+      e.startDrag(DragSource.DefaultCopyDrop,     // cursor
+                 (Transferable)getSelectedNode(), // transferable data
+                                       this);     // drag source listener
+  }
+  public void dragDropEnd(DragSourceDropEvent e) {}
+  public void dragEnter(DragSourceDragEvent e) {}
+  public void dragExit(DragSourceEvent e) {}
+  public void dragOver(DragSourceDragEvent e) {}
+  public void dropActionChanged(DragSourceDragEvent e) {}
+
+// drop sink
+  public void dragEnter(DropTargetDragEvent e)
+  {
+    if(e.isDataFlavorSupported(FileNode.FILENODE)) 
+      e.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+  }
+
+  public void drop(DropTargetDropEvent e)
+  {
+
+    Transferable t = e.getTransferable();
+    final FileNode dropNode = getSelectedNode();
+    if(dropNode == null)
+    {
+      e.rejectDrop();
+      return;
+    }
+
+    //local drop
+    if(t.isDataFlavorSupported(FileNode.FILENODE))
+    {
+       try
+       {
+         FileNode fn = (FileNode)t.getTransferData(FileNode.FILENODE);
+         fn = getNode(fn.getFile().getAbsolutePath());
+
+         if (dropNode.isLeaf())
+         {
+           e.rejectDrop();
+           return;
+         }
+        
+         String dropDir = dropNode.getFile().getAbsolutePath();
+         String newFullName = dropDir+fs+fn.toString();
+         renameFile(fn.getFile(),fn,newFullName);  
+       }
+       catch(Exception ufe){}        
+    }
+    else
+    {
+      e.rejectDrop();
+      return;
+    }
+
+  }
+
+
+  /**
+  *
+  * When a suitable DataFlavor is offered over a remote file
+  * node the node is highlighted/selected and the drag
+  * accepted. Otherwise the drag is rejected.
+  *
+  */
+  public void dragOver(DropTargetDragEvent e)
+  {
+    if(e.isDataFlavorSupported(FileNode.FILENODE))
+    {
+      Point ploc = e.getLocation();
+      TreePath ePath = getPathForLocation(ploc.x,ploc.y);
+      if (ePath == null)
+      {
+        e.rejectDrag();
+        return;
+      }
+      FileNode node = (FileNode)ePath.getLastPathComponent();
+      if(!node.isDirectory())
+        e.rejectDrag();
+      else 
+      {
+        setSelectionPath(ePath);
+        e.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+      }
+    }
+    else
+      e.rejectDrag();
+    
+    return;
+  }
+
+  public void dropActionChanged(DropTargetDragEvent e) {}
+  public void dragExit(DropTargetEvent e){}
+
+
+////////////////////
+// AUTO SCROLLING //
+////////////////////
+  /**
+  *
+  * Handles the auto scrolling of the JTree.
+  * @param location The location of the mouse.
+  *
+  */
+  public void autoscroll( Point location )
+  {
+    int top = 0, left = 0, bottom = 0, right = 0;
+    Dimension size = getSize();
+    Rectangle rect = getVisibleRect();
+    int bottomEdge = rect.y + rect.height;
+    int rightEdge = rect.x + rect.width;
+    if( location.y - rect.y < AUTOSCROLL_MARGIN && rect.y > 0 ) 
+      top = AUTOSCROLL_MARGIN;
+    if( location.x - rect.x < AUTOSCROLL_MARGIN && rect.x > 0 )
+      left = AUTOSCROLL_MARGIN;
+    if( bottomEdge - location.y < AUTOSCROLL_MARGIN && bottomEdge < size.height )
+      bottom = AUTOSCROLL_MARGIN;
+    if( rightEdge - location.x < AUTOSCROLL_MARGIN && rightEdge < size.width ) 
+      right = AUTOSCROLL_MARGIN;
+    rect.x += right - left;
+    rect.y += bottom - top;
+    scrollRectToVisible( rect );
+  }
+
+
+  /**
+  *
+  * Gets the insets used for the autoscroll.
+  * @return The insets.
+  *
+  */
+  public Insets getAutoscrollInsets()
+  {
+    Dimension size = getSize();
+    Rectangle rect = getVisibleRect();
+    autoscrollInsets.top = rect.y + AUTOSCROLL_MARGIN;
+    autoscrollInsets.left = rect.x + AUTOSCROLL_MARGIN;
+    autoscrollInsets.bottom = size.height - (rect.y+rect.height) + AUTOSCROLL_MARGIN;
+    autoscrollInsets.right  = size.width - (rect.x+rect.width) + AUTOSCROLL_MARGIN;
+    return autoscrollInsets;
+  }
+
+  /**
+  *
+  * Popup menu listener
+  *
+  */
+  class PopupListener extends MouseAdapter
+  {
+    public void mousePressed(MouseEvent e)
+    {
+      maybeShowPopup(e);
+    }
+
+    public void mouseReleased(MouseEvent e)
+    {
+      maybeShowPopup(e);
+    }
+
+    private void maybeShowPopup(MouseEvent e)
+    {
+      if(e.isPopupTrigger())
+        popup.show(e.getComponent(),
+                e.getX(), e.getY());
+    }
+  }
+
+  public static void main(String[] args)
+  {
+    JFrame tree_frame = new JFrame("File Manager");
+    FileTree ftree    = new FileTree(new File(System.getProperty("user.home")),
+                                     tree_frame,null);
+    JScrollPane jsp   = new JScrollPane(ftree);
+    tree_frame.getContentPane().add(jsp);
+    tree_frame.pack();
+    tree_frame.setVisible(true);
+  } 
+
+}
+
diff --git a/uk/ac/sanger/artemis/components/FileViewer.java b/uk/ac/sanger/artemis/components/FileViewer.java
new file mode 100644
index 000000000..f40fd4e84
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/FileViewer.java
@@ -0,0 +1,293 @@
+/* FileViewer.java
+ *
+ * created: Thu Nov 19 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 1998,1999,2000,2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/FileViewer.java,v 1.1 2004-06-09 09:46:55 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.Reader;
+import java.io.BufferedReader;
+import java.io.IOException;
+
+import javax.swing.*;
+
+/**
+ *  This class implements a simple file viewer.  In fact any Reader object can
+ *  be viewed.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: FileViewer.java,v 1.1 2004-06-09 09:46:55 tjc Exp $
+ *
+ **/
+
+public class FileViewer
+  extends JFrame 
+{
+
+  /** Pressing this button will distroy the JFrame. */
+  private JButton close_button;
+
+  /** A JPanel to hold the close button. */
+  private JPanel button_panel;
+
+  /** The main component we use for displaying the file. */
+  private JTextArea text_area = null;
+
+  /**
+   *  The size of the last FileViewer JFrame to be resized.  When a new
+   *  FileViewer component is created it is given the current value of this
+   *  variable.  This is updated when any FileViewer frame is resized.
+   **/
+  private static Dimension saved_size = null;
+
+  /**
+   *  The position of the last FileViewer JFrame to be moved.  When a new
+   *  FileViewer component is created it is given the current value of this
+   *  variable.  This is updated when any FileViewer frame is moved.
+   **/
+  private static Point saved_position = null;
+
+  /**
+   *  Create a new FileViewer component and make it visible.
+   *  @param label The name to attach to the new JFrame.
+   **/
+  public FileViewer(final String label) 
+  {
+    this(label, true);
+  }
+
+  /**
+   *  Create a new FileViewer component.
+   *  @param label The name to attach to the new JFrame.
+   *  @param visible The new FileViewer will be made visible if and only if
+   *    this argument is true.
+   **/
+  public FileViewer(final String label, final boolean visible) 
+  {
+    super(label);
+
+    getContentPane().setLayout(new BorderLayout());
+
+    final Font font = Options.getOptions().getFont();
+
+    setFont(font);
+
+    text_area = new JTextArea(18, 90);
+    text_area.setEditable(false);
+    text_area.setFont(font);
+    text_area.setBackground(Color.white);
+
+    getContentPane().add(new JScrollPane(text_area), "Center");
+
+    button_panel = new JPanel();
+    getContentPane().add(button_panel, "South");
+
+    close_button = new JButton("Close");
+    close_button.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent e) 
+      {
+        dispose();
+      }
+    });
+
+    button_panel.add(close_button);
+
+    addWindowListener(new WindowAdapter() 
+    {
+      public void windowClosing(WindowEvent event) 
+      {
+        dispose();
+      }
+    });
+
+    addComponentListener(new ComponentAdapter() 
+    {
+      public void componentResized(ComponentEvent e) 
+      {
+        saved_size = FileViewer.this.getSize();
+        saved_position = FileViewer.this.getLocation();
+      }
+      public void componentMoved(ComponentEvent e) 
+      {
+        saved_size = FileViewer.this.getSize();
+        saved_position = FileViewer.this.getLocation();
+      }
+    });
+
+    pack();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+
+    if(saved_position == null) 
+    {
+      int screen_height = screen.height;
+      int screen_width = screen.width;
+
+      if(screen_width <= 800 || screen_height <= 700) 
+        setSize(screen_width * 9 / 10, screen_height * 9 / 10);
+      else 
+        setSize(800, 700);
+
+      setLocation(new Point((screen.width - getSize().width) / 2,
+                             (screen.height - getSize().height) / 2));
+    } 
+    else
+    {
+      if(saved_position.x < 0 || saved_position.x + 20 > screen.width) 
+        saved_position.x = 20;
+
+      if(saved_position.y < 0 || saved_position.y + 20 > screen.height) 
+        saved_position.y = 20;
+
+      if(saved_size.width < 50) 
+        saved_size.width = 50;
+
+      if(saved_size.height < 50) 
+        saved_size.height = 50;
+      
+      setLocation(saved_position);
+      setSize(saved_size);
+    }
+    
+    setVisible(visible);
+  }
+
+  /**
+   *  Clear the viewer window.
+   **/
+  public void clear()  
+  {
+    text_area.setText("");
+  }
+
+  /**
+   *  Read from the given Reader and append the text to this FileViewer.
+   *  @param read_stream The stream to read the contents of the viewer from.
+   **/
+  public void appendFile(Reader read_stream)
+      throws IOException 
+  {
+    final BufferedReader buffered_reader = new BufferedReader(read_stream);
+    String line;
+
+    while((line = buffered_reader.readLine()) != null) 
+    {
+      appendString(line + "\n");
+      Thread.yield();
+    }
+
+    buffered_reader.close();
+  }
+
+  /**
+   *  Clear the viewer window and display the lines from the given Reader.
+   *  @param read_stream The stream to read the contents of the viewer from.
+   **/
+  public void readFile(Reader read_stream)
+      throws IOException 
+  {
+    final BufferedReader buffered_reader = new BufferedReader(read_stream);
+
+    String line;
+
+    final StringBuffer line_buffer = new StringBuffer();
+
+    while((line = buffered_reader.readLine()) != null) 
+      line_buffer.append(line).append('\n');
+
+    buffered_reader.close();
+
+    final String new_text = line_buffer.toString();
+    text_area.setText(new_text);
+//  text_area.getCaret().setDot(0);
+    text_area.setCaretPosition(0);
+  }
+
+  /**
+   *  Clear the viewer window and display the lines from the given String.
+   *  @param read_string The string to read the contents of the viewer from.
+   **/
+  public void setText(String read_string) 
+  {
+    if(!read_string.equals(text_area.getText())) 
+    {
+      text_area.setText(read_string);
+//    text_area.getCaret().setDot(0);
+      text_area.setCaretPosition(0);
+    }
+  }
+
+
+  /**
+   *  Clear the viewer window and display the lines from the given String.
+   *  @param read_string The string to read the contents of the viewer from.
+   **/
+  public void appendString(String read_string) 
+  {
+    text_area.append(read_string);
+    text_area.getCaret().setDot(0);
+  }
+
+  /**
+   *  Return a String containing the text that this component is displaying.
+   **/
+  public String getText() 
+  {
+    return getTextArea().getText();
+  }
+
+  /**
+   *  Destroy this component.
+   **/
+  public void dispose() 
+  {
+    setVisible(false);
+    
+    saved_size = getSize();
+    saved_position = getLocation();
+
+    super.dispose();
+  }
+
+  /**
+   *  return the TextArea component from this FileViewer.
+   **/
+  public JTextArea getTextArea() 
+  {
+    return text_area;
+  }
+
+  /**
+   *  Return the JPanel containing the close button of this FileViewer.
+   **/
+  protected JPanel getButtonPanel() 
+  {
+    return button_panel;
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/GotoMenu.java b/uk/ac/sanger/artemis/components/GotoMenu.java
new file mode 100644
index 000000000..a753fedf8
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/GotoMenu.java
@@ -0,0 +1,510 @@
+/* GotoMenu.java
+ *
+ * created: Thu Jan  7 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/GotoMenu.java,v 1.1 2004-06-09 09:46:56 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.util.*;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A JMenu with commands for moving around the entries.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: GotoMenu.java,v 1.1 2004-06-09 09:46:56 tjc Exp $
+ **/
+
+public class GotoMenu extends SelectionMenu {
+  /**
+   *  Create a new GotoMenu object.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @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 used to create the Navigator
+   *    component.
+   *  @param menu_name The name of the new menu.
+   **/
+  public GotoMenu (final JFrame frame,
+                   final Selection selection,
+                   final GotoEventSource goto_event_source,
+                   final EntryGroup entry_group,
+                   final String menu_name) {
+    super (frame, menu_name, selection);
+
+    this.goto_event_source = goto_event_source;
+
+    navigator_item = new JMenuItem ("Navigator ...");
+    navigator_item.setAccelerator (NAVIGATOR_KEY);
+    navigator_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        new Navigator (getSelection (),
+                       GotoMenu.this.goto_event_source,
+                       entry_group);
+      }
+    });
+
+    goto_selection_start_item = new JMenuItem ("Start of Selection");
+    goto_selection_start_item.setAccelerator (START_OF_SELECTION_KEY);
+    goto_selection_start_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        makeSelectionStartVisible ();
+      }
+    });
+
+    goto_selection_end_item = new JMenuItem ("End of Selection");
+    goto_selection_end_item.setAccelerator (END_OF_SELECTION_KEY);
+    goto_selection_end_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        makeSelectionEndVisible ();
+      }
+    });
+
+    goto_feature_start_item = new JMenuItem ("Feature Start");
+    goto_feature_start_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        gotoFeatureStart ();
+      }
+    });
+
+    goto_feature_end_item = new JMenuItem ("Feature End");
+    goto_feature_end_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        gotoFeatureEnd ();
+      }
+    });
+
+    goto_feature_base = new JMenuItem ("Feature Base Position ...");
+    goto_feature_base.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        gotoFeaturePosition (false);
+      }
+    });
+
+    goto_feature_aa_position = new JMenuItem ("Feature Amino Acid ...");
+    goto_feature_aa_position.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        gotoFeaturePosition (true);
+      }
+    });
+
+
+    goto_first_item = new JMenuItem ("Start of Sequence");
+    goto_first_item.setAccelerator (START_OF_SEQUENCE_KEY);
+    goto_first_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        goto_event_source.gotoFirstBase ();
+      }
+    });
+
+    goto_last_item = new JMenuItem ("End of Sequence");
+    goto_last_item.setAccelerator (END_OF_SEQUENCE_KEY);
+    goto_last_item.addActionListener(new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        goto_event_source.gotoLastBase ();
+      }
+    });
+
+    add (navigator_item);
+    addSeparator ();
+    add (goto_selection_start_item);
+    add (goto_selection_end_item);
+    add (goto_feature_start_item);
+    add (goto_feature_end_item);
+    add (goto_first_item);
+    add (goto_last_item);
+    add (goto_feature_base);
+    add (goto_feature_aa_position);
+  }
+
+  /**
+   *  The shortcut for the Navigator.
+   **/
+  final static KeyStroke NAVIGATOR_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_G, InputEvent.CTRL_MASK);
+
+  /**
+   *  The shortcut to go to the start of the selection.
+   **/
+  final static KeyStroke START_OF_SELECTION_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_LEFT, InputEvent.CTRL_MASK);
+
+  /**
+   *  The shortcut to go to the end of the selection.
+   **/
+  final static KeyStroke END_OF_SELECTION_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_RIGHT, InputEvent.CTRL_MASK);
+
+  /**
+   *  The shortcut to go to the start of the sequence.
+   **/
+  final static KeyStroke START_OF_SEQUENCE_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_UP, InputEvent.CTRL_MASK);
+
+  /**
+   *  The shortcut to go to the end of the sequence.
+   **/
+  final static KeyStroke END_OF_SEQUENCE_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_DOWN, InputEvent.CTRL_MASK);
+
+  /**
+   *  Create a new GotoMenu object.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @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 used to create the Navigator
+   *    component.
+   **/
+  public GotoMenu (final JFrame frame,
+                   final Selection selection,
+                   final GotoEventSource goto_event_source,
+                   final EntryGroup entry_group) {
+    this (frame, selection, goto_event_source, entry_group, "Goto");
+  }
+
+  /**
+   *  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);
+  }
+
+  /**
+   *  This method sends an GotoEvent to all the GotoEvent listeners that will
+   *  make the last base of the selection visible.
+   **/
+  private void makeSelectionEndVisible () {
+    final GotoEvent new_event =
+      new GotoEvent (this, getSelection ().getEndBaseOfSelection ());
+
+    goto_event_source.sendGotoEvent (new_event);
+  }
+
+  /**
+   *  Goto the start of the (first) selected feature.  (FeatureDisplay only.)
+   **/
+  private void gotoFeatureStart () {
+    setInternalVariables ();
+    if (selection_feature == null) {
+      if (selection_segment == null) {
+        // do nothing
+      } else {
+        // go to the start of the parent feature of the first selected segment
+        final Feature segment_feature = selection_segment.getFeature ();
+        makeFeatureStartVisible (segment_feature);
+      }
+    } else {
+      makeFeatureStartVisible (selection_feature);
+    }
+  }
+
+  /**
+   *  Goto the end of the (first) selected feature.  (FeatureDisplay only.)
+   **/
+  private void gotoFeatureEnd () {
+    setInternalVariables ();
+    if (selection_feature == null) {
+      if (selection_segment == null) {
+        // do nothing
+      } else {
+        // go to the end of the parent feature of the first selected segment
+        final Feature segment_feature = selection_segment.getFeature ();
+        makeFeatureEndVisible (segment_feature);
+      }
+    } else {
+      makeFeatureEndVisible (selection_feature);
+    }
+  }
+
+  /**
+   *  Goto the start of the (first) selected segment.  (FeatureDisplay only.)
+   **/
+  private void gotoSegmentStart () {
+    setInternalVariables ();
+    if (selection_segment == null) {
+      if (selection_feature == null) {
+        // do nothing
+      } else {
+        makeFeatureStartVisible (selection_feature);
+      }
+    } else {
+      makeSegmentStartVisible (selection_segment);
+    }
+  }
+
+  /**
+   *  Goto the end of the (first) selected segment.  (FeatureDisplay only.)
+   **/
+  private void gotoSegmentEnd () {
+    setInternalVariables ();
+    if (selection_segment == null) {
+      if (selection_feature == null) {
+        // do nothing
+      } else {
+        makeFeatureEndVisible (selection_feature);
+      }
+    } else {
+      makeSegmentEndVisible (selection_segment);
+    }
+  }
+
+  /**
+   *  This method will ask the user for a base or amino acid position within
+   *  the first selected feature (using a TextRequester component) and will
+   *  then goto that position.
+   *  @param goto_aa_position If true goto an amino acid position, otherwise
+   *    goto a base position
+   **/
+  private void gotoFeaturePosition (final boolean goto_aa_position) {
+    if (!checkForSelectionFeatures ()) {
+      return;
+    }
+
+    final FeatureVector selected_features = getSelection ().getAllFeatures ();
+
+    if (selected_features.size () > 1) {
+      new MessageDialog (getParentFrame (), "select only one feature");
+      return;
+    }
+    
+    final Feature first_feature = selected_features.elementAt (0);
+
+    final TextRequester text_requester =
+      goto_aa_position ?
+      new TextRequester ("amino acid position within selected feature:",
+                         18, "") :
+      new TextRequester ("base position within selected feature:",
+                         18, "");
+
+    text_requester.addTextRequesterListener (new TextRequesterListener () {
+      public void actionPerformed (final TextRequesterEvent event) {
+        final String position_string = event.getRequesterText ().trim ();
+
+        if (position_string.length () == 0) {
+          return;
+        }
+
+        try {
+          final int feature_position =
+            Integer.valueOf (position_string).intValue ();
+
+          final Marker sequence_marker;
+
+          if (goto_aa_position) {
+            // base and aa positions are numbered from 1
+            final int aa_position = (feature_position - 1) * 3 + 1;
+
+            sequence_marker =
+              first_feature.getPositionInSequence (aa_position);
+          } else {
+            sequence_marker =
+              first_feature.getPositionInSequence (feature_position);
+          }
+
+          final MarkerRange range = new MarkerRange (sequence_marker);
+
+          goto_event_source.gotoBase (sequence_marker);
+
+          /*final*/ MarkerRange selection_range;
+          
+          if (goto_aa_position) {
+            // we want to select the whole codon if we are going to a amino
+            // acid position
+            try { 
+              final MarkerRange codon_end_marker_range =
+                new MarkerRange (sequence_marker.moveBy (2));
+              selection_range =
+                range.combineRanges (codon_end_marker_range, false);
+            } catch (OutOfRangeException e) {
+              // just select one base
+              selection_range = range;
+            }
+          } else {
+            selection_range = range;
+          }
+
+          // select that base (or range) 
+          getSelection ().setMarkerRange (selection_range);
+
+        } catch (NumberFormatException e) {
+          new MessageDialog (getParentFrame (),
+                             "this is not a number: " + position_string);
+        } catch (OutOfRangeException e) {
+          new MessageDialog (getParentFrame (), "the base position is not " +
+                             "within the selection feature: " +
+                             position_string);
+        }
+      }
+    });
+
+    text_requester.show ();
+  }
+
+  /**
+   *  This method sends an GotoEvent to all the GotoEvent listeners that will
+   *  make the given base visible.
+   **/
+  private void makeBaseVisible (final Marker base_marker) {
+    goto_event_source.gotoBase (base_marker);
+  }
+
+  /**
+   *  Scroll the display so that the start of the given feature is visible
+   *  (it's start will be in the middle of the screen after the call).
+   *  @param feature The feature to make visible.
+   **/
+  private void makeFeatureStartVisible (Feature feature) {
+    if (feature == null) {
+      return;
+    }
+
+    makeBaseVisible (feature.getFirstBaseMarker ());
+  }
+
+  /**
+   *  Scroll the display so that the start of the given feature is visible
+   *  (it's start will be in the middle of the screen after the call).
+   *  @param feature The feature to make visible.
+   **/
+  private void makeFeatureEndVisible (Feature feature) {
+    if (feature == null) {
+      return;
+    }
+
+    makeBaseVisible (feature.getLastBaseMarker ());
+
+//    System.out.println (feature.getLastBaseMarker ().getPosition ());
+//    System.out.println (feature.getLastBaseMarker ().getStrand ());
+  }
+
+  /**
+   *  Scroll the display so that the given segment is visible (the first base
+   *  in the segment will be the middle of the screen after the call).
+   *  @param segment The segment to make visible.
+   **/
+  private void makeSegmentStartVisible (FeatureSegment segment) {
+    if (segment == null) {
+      return;
+    }
+
+    final Marker first_base = segment.getStart ();
+
+    makeBaseVisible (first_base);
+  }
+
+  /**
+   *  Scroll the display so that the given segment is visible (the last base
+   *  in the segment will be the middle of the screen after the call).
+   *  @param segment The segment to make visible.
+   **/
+  private void makeSegmentEndVisible (FeatureSegment segment) {
+    if (segment == null) {
+      return;
+    }
+
+    final Marker last_base = segment.getEnd ();
+
+    makeBaseVisible (last_base);
+  }
+
+  /**
+   *  Initialise selection_feature and selection_segment.
+   **/
+  private void setInternalVariables () {
+    final FeatureVector selection_features =
+      getSelection ().getSelectedFeatures ();
+    final FeatureSegmentVector selection_segments =
+      getSelection ().getSelectedSegments ();
+
+    if (selection_features.size () > 0) {
+      selection_feature = selection_features.elementAt (0);
+    } else {
+      selection_feature = null;
+    }
+
+    if (selection_segments.size () > 0) {
+      selection_segment = selection_segments.elementAt (0);
+    } else {
+      selection_segment = null;
+    }
+  }
+
+  /**
+   *  Check that the are some Features in the current selection.  If there
+   *  there are some features then return true.  If there are no features
+   *  selected then popup a message saying so and return false.
+   **/
+  protected boolean checkForSelection () {
+    final FeatureVector features_to_check =
+      getSelection ().getAllFeatures ();
+
+    if (features_to_check.size () == 0) {
+      new MessageDialog (getParentFrame (), "No features selected");
+      return false;
+    } else {
+      return true;
+    }
+  }
+
+  /**
+   *  The GotoEventSource object that was passed to the constructor.
+   **/
+  private GotoEventSource goto_event_source;
+
+  /**
+   *  Set by the constructor to the reference of the first feature in
+   *  selection_features or null if selection_features is empty.
+   **/
+  private Feature selection_feature;
+
+  /**
+   *  Set by the constructor to the reference of the first segment in
+   *  selection_segments or null if selection_segments is empty.
+   **/
+  private FeatureSegment selection_segment;
+
+  private JMenuItem navigator_item = null;
+  private JMenuItem goto_feature_start_item = null;
+  private JMenuItem goto_feature_end_item = null;
+  private JMenuItem goto_selection_end_item = null;
+  private JMenuItem goto_selection_start_item = null;
+  private JMenuItem goto_first_item = null;
+  private JMenuItem goto_last_item = null;
+  private JMenuItem goto_feature_base = null;
+  private JMenuItem goto_feature_aa_position = null;
+}
diff --git a/uk/ac/sanger/artemis/components/GraphMenu.java b/uk/ac/sanger/artemis/components/GraphMenu.java
new file mode 100644
index 000000000..ae419651e
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/GraphMenu.java
@@ -0,0 +1,403 @@
+/* GraphMenu.java
+ *
+ * created: Tue Sep 18 2001
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/GraphMenu.java,v 1.1 2004-06-09 09:46:57 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.plot.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.IOException;
+import java.io.File;
+import java.io.FileReader;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  This menu controls one particular BasePlotGroup.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: GraphMenu.java,v 1.1 2004-06-09 09:46:57 tjc Exp $
+ **/
+
+public class GraphMenu extends JMenu {
+  /**
+   *  Create a new GraphMenu object and all it's menu items.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param entry_group The EntryGroup containing the sequence to plot.
+   *  @param base_plot_group The BasePlotGroup that this menu controls.
+   *  @param view_menu This ViewMenu is updated when a usage plot is added.
+   *  @param add_menu This AddMenu is updated when a usage plot is added.
+   *  @param menu_name The name of the new menu.
+   **/
+  public GraphMenu (final JFrame frame,
+                    final EntryGroup entry_group,
+                    final BasePlotGroup base_plot_group,
+                    final FeatureDisplay feature_display,
+                    final String menu_name) {
+    super (menu_name);
+    this.frame = frame;
+    this.entry_group = entry_group;
+    this.base_plot_group = base_plot_group;
+    this.view_menu = view_menu;
+    this.add_menu = add_menu;
+    this.feature_display = feature_display;
+
+    final BaseAlgorithm [] orig_algorithms =
+      base_plot_group.getPlotAlgorithms ();
+
+    final JMenuItem hide_all_graphs_item = new JMenuItem ("Hide All Graphs");
+    hide_all_graphs_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        final BaseAlgorithm [] current_algorithms =
+          base_plot_group.getPlotAlgorithms ();
+
+        for (int i = 0 ; i < current_algorithms.length ; ++i) {
+          final BaseAlgorithm this_algorithm = current_algorithms[i];
+
+          base_plot_group.setVisibleByAlgorithm (this_algorithm, false);
+
+          final JCheckBoxMenuItem this_menu_item =
+            (JCheckBoxMenuItem) algorithm_menu_items.elementAt (i);
+
+          this_menu_item.setState (false);
+        }
+        if (getParent () != null) {
+          // XXX change to revalidate().
+          frame.validate ();
+        }
+      }
+    });
+    add (hide_all_graphs_item);
+
+    addSeparator ();
+
+    if (Options.readWritePossible ()) {
+      final JMenuItem usage_plot_item = new JMenuItem ("Add Usage Plots ...");
+      usage_plot_item.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent event) {
+          addUsagePlot ();
+        }
+      });
+      add (usage_plot_item);
+
+      final JMenuItem user_plot_item = new JMenuItem ("Add User Plot ...");
+      user_plot_item.addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent event) {
+          addUserPlot ();
+        }
+      });
+
+      add (user_plot_item);
+
+      addSeparator ();
+    }
+
+    for (int i = 0 ; i < orig_algorithms.length ; ++i) {
+      final BaseAlgorithm this_algorithm = orig_algorithms[i];
+
+      addAlgorithm (this_algorithm, false);
+    }
+
+    if (Options.getOptions ().getProperty ("codon_usage_file") != null) {
+      final String codon_usage_file_name =
+        Options.getOptions ().getProperty ("codon_usage_file");
+
+      try {
+        addUsagePlot (new File (codon_usage_file_name), true, false);
+        addUsagePlot (new File (codon_usage_file_name), false, false);
+
+        if (getParent () != null) {
+          // XXX change to revalidate().
+          frame.validate ();
+        }
+      } catch (IOException e) {
+        new MessageDialog (frame, "error while reading usage data: " + e);
+      }
+    }
+  }
+
+  /**
+   *  Create a new GraphMenu object and all it's menu items.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param entry_group The EntryGroup containing the sequence to plot.
+   *  @param base_plot_group The BasePlotGroup that this menu controls.
+   *  @param view_menu This ViewMenu is updated when a usage plot is added.
+   *  @param add_menu This AddMenu is updated when a usage plot is added.
+   *  @param menu_name The name of the new menu.
+   **/
+  public GraphMenu (final JFrame frame,
+                    final EntryGroup entry_group,
+                    final BasePlotGroup base_plot_group,
+                    final FeatureDisplay feature_display) {
+    this (frame, entry_group, base_plot_group, feature_display, "Graph");
+  }
+
+  /**
+   *  Add a menu item for the given algorithm to the Display menu.
+   *  @param is_visible The JCheckBoxMenuItem starts in the true state if and
+   *    only if this is true.
+   *  @return The new JCheckBoxMenuItem
+   **/
+  public JCheckBoxMenuItem addAlgorithm (final BaseAlgorithm algorithm,
+                                        final boolean is_visible) {
+
+    final JCheckBoxMenuItem new_item =
+      new JCheckBoxMenuItem (algorithm.getAlgorithmName ());
+
+    new_item.setState (is_visible);
+
+    new_item.addItemListener (new ItemListener () {
+      public void itemStateChanged(ItemEvent event) {
+        base_plot_group.setVisibleByAlgorithm (algorithm,
+                                               new_item.getState ());
+      }
+    });
+
+    add (new_item);
+
+    algorithm_menu_items.addElement (new_item);
+
+    return new_item;
+  }
+
+  /**
+   *  Ask the user for a file name, then read the codon usage data from that
+   *  file, then make and add forward and a reverse BasePlot component using
+   *  the data.
+   **/
+  public void addUsagePlot () {
+    final JFrame frame = Utilities.getComponentFrame (base_plot_group);
+
+    final StickyFileChooser dialog = new StickyFileChooser ();
+
+    dialog.setDialogTitle ("Select a codon usage data file name ...");
+    dialog.setDialogType (JFileChooser.OPEN_DIALOG);
+
+    final int status = dialog.showOpenDialog (frame);
+
+    if (status != JFileChooser.APPROVE_OPTION ||
+        dialog.getSelectedFile () == null) {
+      return;
+    }
+
+    final File file =
+      new File (dialog.getCurrentDirectory (),
+                dialog.getSelectedFile ().getName ());
+
+    if (file.length () != 0) {
+      try {
+        final BasePlot new_forward_plot =
+          addUsagePlot (file, true, true);
+        final BasePlot new_reverse_plot =
+          addUsagePlot (file, false, true);
+
+        final Algorithm forward_algorithm = new_forward_plot.getAlgorithm ();
+        final Algorithm reverse_algorithm = new_reverse_plot.getAlgorithm ();
+
+        base_plot_group.setVisibleByAlgorithm (forward_algorithm, true);
+        base_plot_group.setVisibleByAlgorithm (reverse_algorithm, true);
+      } catch (IOException e) {
+        new MessageDialog (Utilities.getComponentFrame (base_plot_group),
+                           "error while reading usage data: " + e);
+      }
+    }
+  }
+
+  /**
+   *  Read the codon usage data from the given File, then make and add a
+   *  BasePlot component using the data.
+   *  @param use_forward_strand The plot will be a forward plot if and only if
+   *    this is true.
+   *  @param is_visible The plot will start off visible if and only if this is
+   *    true.
+   *  @return The BasePlot that was added.
+   **/
+  private BasePlot addUsagePlot (final File codon_usage_file,
+                                 final boolean use_forward_strand,
+                                 final boolean is_visible)
+      throws IOException {
+    final CodonUsageAlgorithm codon_usage_algorithm;
+
+    if (use_forward_strand) {
+      final Strand forward_strand =
+        entry_group.getBases ().getForwardStrand ();
+
+      final CodonUsageWeight usage_weights =
+        new CodonUsageWeight (codon_usage_file, forward_strand);
+
+      codon_usage_algorithm =
+        new CodonUsageAlgorithm (forward_strand, usage_weights);
+    } else {
+      final Strand backward_strand =
+        entry_group.getBases ().getReverseStrand ();
+
+      final CodonUsageWeight usage_weights =
+        new CodonUsageWeight (codon_usage_file, backward_strand);
+
+      codon_usage_algorithm =
+        new CodonUsageAlgorithm (backward_strand, usage_weights);
+    }
+
+    addAlgorithm (codon_usage_algorithm, is_visible);
+
+    final BasePlot new_plot =
+      base_plot_group.addAlgorithm (codon_usage_algorithm);
+
+    base_plot_group.setVisibleByAlgorithm (codon_usage_algorithm, is_visible);
+
+    // XXX hack to force the BasePlot to initialise
+    final DisplayAdjustmentEvent event =
+      new DisplayAdjustmentEvent (this,
+                                  feature_display.getFirstVisibleForwardBase (),
+                                  feature_display.getLastVisibleForwardBase (),
+                                  feature_display.getMaxVisibleBases (),
+                                  feature_display.getScaleValue (),
+                                  feature_display.getScaleFactor (),
+                                  feature_display.isRevCompDisplay (),
+                                  DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT);
+
+    base_plot_group.displayAdjustmentValueChanged (event);
+
+    return new_plot;
+  }
+
+  /**
+   *  Add a UserDataAlgorithm to the display.
+   **/
+  private void addUserPlot () {
+    final JFrame frame = Utilities.getComponentFrame (base_plot_group);
+
+    final StickyFileChooser dialog = new StickyFileChooser ();
+
+    dialog.setDialogTitle ("Select a data file name ...");
+    dialog.setDialogType (JFileChooser.OPEN_DIALOG);
+
+    final int status = dialog.showOpenDialog (frame);
+
+    if (status != JFileChooser.APPROVE_OPTION ||
+        dialog.getSelectedFile () == null) {
+      return;
+    }
+
+    final File file =
+      new File (dialog.getCurrentDirectory (),
+                dialog.getSelectedFile ().getName ());
+
+    if (file.length () != 0) {
+      final uk.ac.sanger.artemis.util.Document document =
+        new uk.ac.sanger.artemis.util.FileDocument (file);
+
+      final Strand forward_strand =
+        getEntryGroup ().getBases ().getForwardStrand ();
+
+      try {
+        final UserDataAlgorithm new_algorithm =
+          new UserDataAlgorithm (forward_strand, document);
+
+        final BasePlot new_base_plot =
+          base_plot_group.addAlgorithm (new_algorithm);
+
+        base_plot_group.setVisibleByAlgorithm (new_algorithm, true);
+
+        addAlgorithm (new_algorithm, true);
+
+        // XXX hack to force the BasePlot to initialise
+        final DisplayAdjustmentEvent event =
+          new DisplayAdjustmentEvent (this,
+                                      feature_display.getFirstVisibleForwardBase (),
+                                      feature_display.getLastVisibleForwardBase (),
+                                      feature_display.getMaxVisibleBases (),
+                                      feature_display.getScaleValue (),
+                                      feature_display.getScaleFactor (),
+                                      feature_display.isRevCompDisplay (),
+                                      DisplayAdjustmentEvent.ALL_CHANGE_ADJUST_EVENT);
+
+        base_plot_group.displayAdjustmentValueChanged (event);
+
+        if (getParent () != null) {
+          // XXX change to revalidate().
+          frame.validate ();
+        }
+      } catch (IOException e) {
+        new MessageDialog (Utilities.getComponentFrame (base_plot_group),
+                           "error while reading user data: " + e);
+      }
+    }
+  }
+
+  /**
+   *  Return the JFrame that was passed to the constructor.
+   **/
+  public JFrame getParentFrame () {
+    return frame;
+  }
+
+  /**
+   *  Return the EntryGroup that was passed to the constructor.
+   **/
+  private EntryGroup getEntryGroup () {
+    return entry_group;
+  }
+
+  /**
+   *  The JFrame reference that was passed to the constructor.
+   **/
+  private JFrame frame;
+
+  /**
+   *  The BasePlotGroup that was passed to the constructor.
+   **/
+  private BasePlotGroup base_plot_group;
+
+  /**
+   *  The EntryGroup object that was passed to the constructor.
+   **/
+  private EntryGroup entry_group = null;
+
+  /**
+   *  This ViewMenu is updated when a usage plot is added.
+   **/
+  private ViewMenu view_menu;
+
+  /**
+   *  This AddMenu is updated when a usage plot is added.
+   **/
+  private AddMenu add_menu;
+
+  /**
+   *  The FeatureDisplay that was passed to the constructor.
+   **/
+  private FeatureDisplay feature_display;
+
+  /**
+   *  This list of the CheckboxMenuItems for the graphs is stored so that we
+   *  can turn them all off with "Hide All Graphs".
+   **/
+  private Vector algorithm_menu_items = new Vector ();
+}
diff --git a/uk/ac/sanger/artemis/components/InputStreamProgressDialog.java b/uk/ac/sanger/artemis/components/InputStreamProgressDialog.java
new file mode 100644
index 000000000..87f0ab26b
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/InputStreamProgressDialog.java
@@ -0,0 +1,126 @@
+/* InputStreamProgressDialog.java
+ *
+ * created: Thu Aug  8 2002
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/InputStreamProgressDialog.java,v 1.1 2004-06-09 09:46:58 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import uk.ac.sanger.artemis.util.InputStreamProgressListener;
+import uk.ac.sanger.artemis.util.InputStreamProgressEvent;
+
+import javax.swing.*;
+
+/**
+ *  A JDialog the show the byte count of an InputStream (via
+ *  InputStreamProgressEvent objects)
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: InputStreamProgressDialog.java,v 1.1 2004-06-09 09:46:58 tjc Exp $
+ **/
+
+public class InputStreamProgressDialog extends JDialog {
+  /**
+   *  Create a blocking InputStreamProgressDialog JFrame.
+   *  @param parent The parent window.
+   *  @param message The message to display in the JDialog and to use as the
+   *    frame title.
+   **/
+  public InputStreamProgressDialog (final JFrame parent,
+                                    final String message) {
+    this (parent, message, message, true);
+  }
+
+  /**
+   *  Create a new InputStreamProgressDialog JFrame.
+   *  @param parent The parent window.
+   *  @param title The title of the new JDialog.
+   *  @param message The message to display in the JDialog.
+   *  @param modal If true, dialog blocks input to the parent window when
+   *    shown.
+   **/
+  public InputStreamProgressDialog (final JFrame parent_frame,
+                                    final String title,
+                                    final String message,
+                                    final boolean modal) {
+    super (parent_frame, title, modal);
+    getContentPane ().add (new JLabel (message), "North");
+
+    final JLabel bytes_label = new JLabel ("                               ");
+    getContentPane ().add (bytes_label, "Center");
+
+    final JPanel panel = new JPanel ();
+
+    panel.add (ok_button);
+    ok_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        InputStreamProgressDialog.this.dispose ();
+      }
+    });
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        InputStreamProgressDialog.this.dispose ();
+      }
+    });
+
+    stream_progress_listener = new InputStreamProgressListener () {
+      public void progressMade (final InputStreamProgressEvent event) {
+
+        final int char_count = event.getCharCount ();
+        if (char_count == -1) {
+          bytes_label.setText ("");
+        } else {
+          setVisible (true);
+          bytes_label.setText ("Characters read so far: " + char_count);
+        }
+      }
+    };
+
+    getContentPane ().add (panel, "South");
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+
+    setVisible (false);
+  }
+
+  /**
+   *  Return an InputStreamProgressListener to pass to FileProgressDocument.
+   **/
+  public InputStreamProgressListener getInputStreamProgressListener () {
+    return stream_progress_listener;
+  }
+
+  /**
+   *  An InputStreamProgressListener used to update the label with the
+   *  current number of chars read.
+   **/
+  private InputStreamProgressListener stream_progress_listener = null;
+
+  final private JButton ok_button = new JButton ("OK");
+}
diff --git a/uk/ac/sanger/artemis/components/KeyChoice.java b/uk/ac/sanger/artemis/components/KeyChoice.java
new file mode 100644
index 000000000..f3241bd12
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/KeyChoice.java
@@ -0,0 +1,203 @@
+/* KeyChoice.java
+ *
+ * created: Mon Sep  6 1999
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/KeyChoice.java,v 1.1 2004-06-09 09:46:59 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import uk.ac.sanger.artemis.util.StringVector;
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.KeyVector;
+import uk.ac.sanger.artemis.io.EntryInformation;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This component is a Choice component that shows the possible feature keys.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: KeyChoice.java,v 1.1 2004-06-09 09:46:59 tjc Exp $
+ **/
+public class KeyChoice extends JPanel {
+  /**
+   *  Create a new KeyChoice component with CDS as the default key.
+   *  @param entry_information The object to get the list of possible
+   *    keys from.
+   **/
+  public KeyChoice (final EntryInformation entry_information) {
+    this (entry_information, Key.CDS);
+  }
+
+  /**
+   *  Create a new KeyChoice component with the given key as the default.
+   *  @param entry_information The object to get the list of possible
+   *    keys from.
+   **/
+  public KeyChoice (final EntryInformation entry_information,
+                    final Key default_key) {
+    this.entry_information = entry_information;
+    this.default_key = default_key;
+
+    final Font font = Options.getOptions ().getFont ();
+    setFont (font);
+
+    key_chooser = new JComboBox ();
+
+    final int MAX_VISIBLE_ROWS = 30;
+    
+    key_chooser.setMaximumRowCount (MAX_VISIBLE_ROWS);
+
+    updateChoice ();
+  }
+
+  /**
+   *  Return the currently selected key.
+   **/
+  public Key getSelectedItem () {
+    final Key key;
+
+    key = new Key ((String) key_chooser.getSelectedItem ());
+
+    return key;
+  }
+
+  /**
+   *  Set the selected Key.
+   **/
+  public void setKey (final Key new_key) {
+    final int key_index = keyIndex (new_key);
+
+    if (key_index == -1) {
+      // add the key
+      key_chooser.addItem (new_key.toString ());
+      key_chooser.setSelectedItem (new_key.toString ());
+    } else {
+      key_chooser.setSelectedIndex (key_index);
+    }
+  }
+
+  /**
+   *  Adds the specified item listener to receive item events from the Choice
+   *  component of this KeyChoice.
+   *  @param l The item listener.
+   **/
+  public void addItemListener(ItemListener l) {
+    key_chooser.addItemListener (l);
+  }
+
+
+  /**
+   *  Removes the specified item listener so that it no longer receives item
+   *  events from the Choice component of this KeyChoice.
+   *  @param l The item listener.
+   **/
+  public void removeItemListener(ItemListener l) {
+    key_chooser.removeItemListener (l);
+  }
+
+  /**
+   *  Update the key_chooser.
+   **/
+  private void updateChoice () {
+    removeAll ();
+
+    final Font font = Options.getOptions ().getFont ();
+    key_chooser.setFont (font);
+
+    add (key_chooser);
+
+    KeyVector keys = getSortedKeys ();
+
+    if (keys == null) {
+      keys = new KeyVector ();
+      keys.add (Key.CDS);
+    }
+
+    for (int i = 0 ; i < keys.size () ; ++i) {
+      key_chooser.addItem (keys.elementAt (i).toString ());
+    }
+
+    if (keyIndex (default_key) != -1) {
+      setKey (default_key);
+    }
+
+    // XXX change to revalidate().
+    validate ();
+
+    if (getParent () != null) {
+      // XXX change to revalidate().
+      getParent ().validate ();
+      if (getParent ().getParent () != null) {
+        // XXX change to revalidate().
+        getParent ().getParent ().validate ();
+      }
+    }
+  }
+
+  /**
+   *  Return a alphanumerically sorted vector containing the String
+   *  representations of the common keys (those listed in the common_keys
+   *  property of the options file).  If there is no common_keys option then
+   *  all the legal keys are returned.
+   **/
+  private KeyVector getSortedKeys () {
+    return entry_information.getSortedValidKeys ();
+  }
+
+  /**
+   *  Return the index in the key_chooser component of the given Key.
+   **/
+  private int keyIndex (final Key key) {
+    for (int i = 0 ; i < key_chooser.getItemCount () ; ++i) {
+      if (key.toString ().equals ((String)key_chooser.getItemAt (i))) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   *  The Key that was passed to the constructor.
+   **/
+  private Key default_key = null;
+
+  /**
+   *  The JComboBox component that will show the feature keys.
+   **/
+  private JComboBox key_chooser = null;
+
+  /**
+   *  This toggle sets whether to show common keys or uncommon keys.
+   **/
+  private JCheckBox common_keys_checkbox = null;
+
+  /**
+   *  The EntryInformation object that was passed to the constructor.
+   **/
+  private EntryInformation entry_information = null;
+}
diff --git a/uk/ac/sanger/artemis/components/KeyChooser.java b/uk/ac/sanger/artemis/components/KeyChooser.java
new file mode 100644
index 000000000..af9d93849
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/KeyChooser.java
@@ -0,0 +1,118 @@
+/* KeyChooser.java
+ *
+ * created: Mon Sep  6 1999
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/KeyChooser.java,v 1.1 2004-06-09 09:47:00 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.EntryInformation;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This component is a KeyChoice component in a JFrame (see addItemListener ()
+ *  and removeItemListener () for details).
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: KeyChooser.java,v 1.1 2004-06-09 09:47:00 tjc Exp $
+ **/
+public class KeyChooser extends JFrame {
+  /**
+   *  Create a new KeyChooser component with CDS as the default key.
+   *  @param entry_information The object to get the list of possible
+   *    keys from.
+   **/
+  public KeyChooser (final EntryInformation entry_information) {
+    this (entry_information, Key.CDS);
+  }
+
+  /**
+   *  Create a new KeyChooser component with the given key as the default.
+   *  @param entry_information The object to get the list of possible
+   *    keys from.
+   **/
+  public KeyChooser (final EntryInformation entry_information,
+                     final Key default_key) {
+    key_choice = new KeyChoice (entry_information, default_key);
+
+    getContentPane ().add (key_choice, "Center");
+
+    final JPanel panel = new JPanel ();
+
+    panel.add (ok_button);
+    ok_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        KeyChooser.this.dispose ();
+      }
+    });
+
+    panel.add (close_button);
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        KeyChooser.this.dispose ();
+      }
+    });
+
+    getContentPane ().add (panel, "South");
+    pack ();
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        KeyChooser.this.dispose ();
+      }
+    });
+
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+  }
+
+  /**
+   *  Return the KeyChoice component that is displayed in this JFrame.
+   **/
+  public KeyChoice getKeyChoice () {
+    return key_choice;
+  }
+
+  /**
+   *  Return the reference of the OK button of this KeyChooser.
+   **/
+  public JButton getOKButton () {
+    return ok_button;
+  }
+
+  private KeyChoice key_choice;
+
+  final private JButton ok_button = new JButton ("OK");
+
+  final private JButton close_button = new JButton ("Cancel");
+}
+
diff --git a/uk/ac/sanger/artemis/components/ListDialog.java b/uk/ac/sanger/artemis/components/ListDialog.java
new file mode 100644
index 000000000..c84d08dc6
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ListDialog.java
@@ -0,0 +1,128 @@
+/* ListDialog.java
+ *
+ * created: Fri Sep  1 2000
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ListDialog.java,v 1.1 2004-06-09 09:47:01 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This component is a JDialog that contains a List.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: ListDialog.java,v 1.1 2004-06-09 09:47:01 tjc Exp $
+ **/
+
+public class ListDialog extends JDialog {
+  /**
+   *  Create a new ListDialog component.
+   **/
+  public ListDialog (final JFrame parent, final String title) {
+    super (parent, title, true);
+
+    JScrollPane scroll_pane = new JScrollPane (getList ());
+
+    getContentPane ().add (scroll_pane, "Center");
+
+    getList ().setBackground (Color.white);
+    
+    button_panel = new JPanel ();
+
+    button_panel.add (ok_button);
+
+    getContentPane ().add (button_panel, "South");
+
+    ok_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        selected_item = getList ().getSelectedValue ();
+        ListDialog.this.dispose ();
+      }
+    });
+
+    button_panel.add (cancel_button);
+    cancel_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        selected_item = null;
+        ListDialog.this.dispose ();
+      }
+    });
+    packme ();
+  }
+    
+  /**
+   *  This method will call pack () and then move the JDialog to the centre of
+   *  the screen.
+   **/
+  private void packme () {
+    pack ();
+
+    setSize (750, 400);
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+
+    final int x_position = (screen.width - getSize ().width) / 2;
+    int y_position = (screen.height - getSize ().height) / 2;
+
+    if (y_position < 10) {
+      y_position = 10;
+    }
+
+    setLocation (new Point (x_position, y_position));
+  }
+
+  /**
+   *  Show the dialog and then return the selected item, or null if the user
+   *  hits cancel.
+   **/
+  public Object getSelectedValue () {
+    show ();
+    return selected_item;
+  }
+
+  /**
+   *  Return the reference of the List.
+   **/
+  public JList getList () {
+    return list;
+  }
+
+  /**
+   *  The List.
+   **/
+  final private JList list = new JList ();
+
+  private final JButton ok_button = new JButton ("OK");
+  private final JButton cancel_button = new JButton ("Cancel");
+
+  private JPanel button_panel = null;
+
+  /**
+   *  Set to null if and only if the user cancels the dialog, otherwise
+   *  contains the selected item.
+   **/
+  private Object selected_item = null;
+}
diff --git a/uk/ac/sanger/artemis/components/LogReadListener.java b/uk/ac/sanger/artemis/components/LogReadListener.java
new file mode 100644
index 000000000..646fc23a9
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/LogReadListener.java
@@ -0,0 +1,76 @@
+/* LogReadListener.java
+ *
+ * created: Fri Nov 28 2003
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 2003  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/LogReadListener.java,v 1.1 2004-06-09 09:47:02 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.io.ReadListener;
+import uk.ac.sanger.artemis.io.ReadEvent;
+
+import javax.swing.*;
+
+/**
+ *  A class that implements ReadListener by logging all ReadEvents.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: LogReadListener.java,v 1.1 2004-06-09 09:47:02 tjc Exp $
+ **/
+
+public class LogReadListener implements ReadListener {
+  /**
+   *  Create a new DialogReadListener.
+   *  @param source The source we are reading from - generally a file name or
+   *    URL.  This file name is prepended to the log entries.
+   **/
+  public LogReadListener (final String source) {
+    this.source = source;
+  }
+  
+  /**
+   *  Implementation of ReadListener.
+   **/
+  public void notify (final ReadEvent event) {
+    if (source == null) {
+      Splash.getLogger ().log (event.getMessage () + "\n");
+    } else {
+      Splash.getLogger ().log ("while reading from " + source + ": " +
+                               event.getMessage () + "\n");
+    }
+    seen_message = true;
+  }
+  
+  /**
+   *  Return true if and only if notify() has been called at least once.
+   **/
+  public boolean seenMessage () {
+    return seen_message;
+  }
+  
+  /**
+   *  The name that was passed to the constructor.
+   **/
+  private String source;
+
+  private boolean seen_message = false;
+}
diff --git a/uk/ac/sanger/artemis/components/LogViewer.java b/uk/ac/sanger/artemis/components/LogViewer.java
new file mode 100644
index 000000000..dbbbf1940
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/LogViewer.java
@@ -0,0 +1,102 @@
+/* LogViewer.java
+ *
+ * created: Wed Aug 30 2000
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/LogViewer.java,v 1.1 2004-06-09 09:47:03 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import java.io.*;
+
+/**
+ *  A class for viewing log messages in a FileViewer component.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: LogViewer.java,v 1.1 2004-06-09 09:47:03 tjc Exp $
+ **/
+
+public class LogViewer implements Logger
+{
+
+  /** The FileViewer that is used to show the messages. */
+  private FileViewer file_viewer = null;
+
+  /**
+   *  Create a new, empty LogViewer component.
+   **/
+  public LogViewer() 
+  {
+
+  }
+
+  /**
+   *  Send the given String to the log.
+   **/
+  public void log(final String message) 
+  {
+    maybeMakeViewer();
+    file_viewer.appendString(message);
+  }
+
+  /**
+   *  Read from the given Reader and send it to the log.
+   **/
+  public void log(final Reader reader)
+      throws IOException 
+  {
+    maybeMakeViewer();
+    file_viewer.appendFile(reader);
+  }
+
+  /**
+   *  Call setVisible() on the FileViewer that this object contains.
+   **/
+  public void setVisible(final boolean flag) 
+  {
+    maybeMakeViewer();
+    file_viewer.setVisible(flag);
+  }
+
+  /**
+   *  If file_viewer is null make a FileViewer and assign it to file_viewer.
+   **/
+  private void maybeMakeViewer() 
+  {
+    if (file_viewer == null) 
+    {
+      file_viewer = new FileViewer("Log Viewer", false) 
+      {
+        public void dispose()
+        {
+          // if the FileViewer is deleted we want to know
+          file_viewer = null;
+          super.dispose();
+        }
+      };
+
+      file_viewer.pack();
+    }
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/MarkerRangeRequester.java b/uk/ac/sanger/artemis/components/MarkerRangeRequester.java
new file mode 100644
index 000000000..26b033376
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/MarkerRangeRequester.java
@@ -0,0 +1,106 @@
+/* MarkerRangeRequester.java
+ *
+ * created: Mon Jul 10 2000
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/MarkerRangeRequester.java,v 1.1 2004-06-09 09:47:04 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.util.Vector;
+
+/**
+ *  A requester that gets a MarkerRange or a Range from the user.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: MarkerRangeRequester.java,v 1.1 2004-06-09 09:47:04 tjc Exp $
+ **/
+
+public class MarkerRangeRequester extends TextRequester {
+  /**
+   *  Create a new MarkerRangeRequester component with the given prompt. Other
+   *  components can listen for MarkerRangeRequesterEvent object.
+   *  @param prompt A message that is displayed in the component beside the
+   *    TextArea that the user types into.  This String is also used as the
+   *    JFrame title.
+   *  @param width The width of the TextRequester in the new requester.
+   *  @param initial_text The initial text to put in the TextRequester.
+   **/
+  public MarkerRangeRequester (final String prompt,
+                               final int width,
+                               final String initial_text) {
+    super (prompt, width, initial_text);
+  }
+
+  /**
+   *  Add the given object as a listen for MarkerRangeRequester events from
+   *  this MarkerRangeRequester.
+   **/
+  public void
+    addMarkerRangeRequesterListener (final MarkerRangeRequesterListener l) {
+    listeners.addElement (l);
+  }
+
+  /**
+   *  Send a MarkerRangeRequesterEvent of type OK to all the listeners.
+   **/
+  protected void performOK () {
+    final MarkerRangeRequesterEvent new_event =
+      new MarkerRangeRequesterEvent (this, getText (),
+                                     MarkerRangeRequesterEvent.OK);
+
+    sendEvent (new_event);
+
+    super.performOK ();
+  }
+
+  /**
+   *  Send a MarkerRangeRequesterEvent of type CANCEL to all the listeners.
+   **/
+  protected void performCancel () {
+    final MarkerRangeRequesterEvent new_event =
+      new MarkerRangeRequesterEvent (this, getText (),
+                                     MarkerRangeRequesterEvent.CANCEL);
+
+    sendEvent (new_event);
+
+    super.performCancel ();
+  }
+
+  /**
+   *  Send the given MarkerRangeRequesterEvent to all the object that are
+   *  listening for the event.
+   **/
+  private void sendEvent (final MarkerRangeRequesterEvent event) {
+    for (int i = 0 ; i < listeners.size () ; ++i) {
+      final MarkerRangeRequesterListener listener =
+        ((MarkerRangeRequesterListener) listeners.elementAt (i));
+
+      listener.actionPerformed (event);
+    }
+  }
+
+  /**
+   *  This contains the objects that are listening for MarkerRangeRequester
+   *  events from this MarkerRangeRequester.
+   **/
+  private Vector listeners = new Vector ();
+}
diff --git a/uk/ac/sanger/artemis/components/MarkerRangeRequesterEvent.java b/uk/ac/sanger/artemis/components/MarkerRangeRequesterEvent.java
new file mode 100644
index 000000000..afa2db47c
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/MarkerRangeRequesterEvent.java
@@ -0,0 +1,199 @@
+/* MarkerRangeRequesterEvent.java
+ *
+ * created: Mon Jul 10 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/MarkerRangeRequesterEvent.java,v 1.1 2004-06-09 09:47:05 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.io.Range;
+import uk.ac.sanger.artemis.io.LocationLexer;
+import uk.ac.sanger.artemis.io.LocationLexer.*;
+import uk.ac.sanger.artemis.util.*;
+
+import javax.swing.*;
+
+/**
+ *  This event is sent when the user presses OK or Cancel in a
+ *  MarkerRangeRequester component.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: MarkerRangeRequesterEvent.java,v 1.1 2004-06-09 09:47:05 tjc Exp $
+ **/
+
+public class MarkerRangeRequesterEvent extends TextRequesterEvent {
+  /**
+   *  Create a new MarkerRangeRequesterEvent object.
+   *  @param source The MarkerRangeRequester that generated the event.
+   *  @param requester_text The contents of the TextField in the
+   *    MarkerRangeRequester.
+   *  @param type The type of event.
+   **/
+  public MarkerRangeRequesterEvent (final MarkerRangeRequester source,
+                                    final String requester_text,
+                                    final int type) {
+    super (source, requester_text, type);
+  }
+
+
+  /**
+   *  Parse a Range and return the start and end positions in an array that is
+   *  two elements long.
+   *  @return null if and only if there was a parse error.  If the range is on
+   *    the forward strand the first element will be less than or equal to the
+   *    second, otherwise the first will be greater than the second.
+   **/
+  private int [] getRangeInternal () {
+    if (getRequesterText ().length () == 0) {
+      return null;
+    }
+
+    final LocationLexer lexer = new LocationLexer (getRequesterText ());
+
+    final TokenEnumeration enum = lexer.getTokens ();
+
+    boolean complement_flag = false;
+
+    if (enum.peekElement () instanceof String &&
+        ((String)enum.peekElement ()).equals ("complement")) {
+      complement_flag = true;
+      enum.nextElement ();
+    }
+
+    enum.eatToken ('(');
+
+    if (enum.peekElement () instanceof Integer) {
+      int first_base = ((Integer) enum.nextElement ()).intValue ();
+
+      if (enum.peekElement () instanceof Integer ||
+          (enum.eatToken ("..") || enum.eatToken ('.') ||
+           enum.eatToken ("-")) &&
+          enum.peekElement () instanceof Integer) {
+        int last_base = ((Integer) enum.nextElement ()).intValue ();
+
+        enum.eatToken (')');
+
+        if (enum.peekElement () == null) {
+          if (complement_flag) {
+            final int temp = first_base;
+            first_base = last_base;
+            last_base = temp;
+          }
+
+          return new int [] {
+            first_base, last_base
+          };
+        } else {
+          new MessageDialog ((JFrame) getSource (),
+                             "garbage at the end of the range: " + enum);
+          return null;
+        }
+      }
+    }
+
+    // if we get to here then there was a parse error
+    new MessageDialog ((JFrame) getSource (),
+                       "error in range: the range should have this " +
+                       "form: 100..200 - error at: " + enum);
+
+    return null;
+  }
+
+  /**
+   *  Parse the contents of the String that was passed to the constructor as a
+   *  Range and return it.
+   *  @param bases The Bases object to use to find the Strand that is passed
+   *    to the MarkerRange constructor.
+   *  @return The Range or null if the Range can't be parsed.
+   **/
+  public MarkerRange getMarkerRange (final Bases bases) {
+    try {
+      final MarkerRange marker_range;
+
+      final int [] return_values = getRangeInternal ();
+
+      if (return_values == null) {
+        return null;
+      }
+
+      final int first_base = return_values[0];
+      final int last_base = return_values[1];
+
+      if (first_base <= last_base) {
+        // forward strand
+        final Strand strand;
+
+        strand = bases.getForwardStrand ();
+
+        marker_range =
+          strand.makeMarkerRangeFromPositions (first_base,
+                                               last_base);
+      } else {
+        // reverse strand
+        final Strand strand = bases.getReverseStrand ();
+        final int raw_first = bases.getComplementPosition (first_base);
+        final int raw_last = bases.getComplementPosition (last_base);
+
+        marker_range =
+          strand.makeMarkerRangeFromPositions (raw_last,
+                                               raw_first);
+      }
+
+      return marker_range;
+    } catch (OutOfRangeException e) {
+      new MessageDialog ((JFrame) getSource (),
+                         "the bases are out of range for this " +
+                         "sequence");
+      return null;
+    }
+  }
+
+  /**
+   *  Parse and return a raw Range object from the text.
+   *  @return The Range or null if the range could not be parsed.
+   **/
+  public Range getRawRange () {
+    final int [] return_values = getRangeInternal ();
+
+    if (return_values == null) {
+      return null;
+    }
+
+    final int first_base = return_values[0];
+    final int last_base = return_values[1];
+
+    try {
+      if (first_base < last_base) {
+        return new Range (first_base, last_base);
+      } else {
+        return new Range (last_base, first_base);
+      }
+    } catch (OutOfRangeException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+  }
+}
+
+
diff --git a/uk/ac/sanger/artemis/components/MarkerRangeRequesterListener.java b/uk/ac/sanger/artemis/components/MarkerRangeRequesterListener.java
new file mode 100644
index 000000000..732421579
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/MarkerRangeRequesterListener.java
@@ -0,0 +1,42 @@
+/* MarkerRangeRequesterListener.java
+ *
+ * created: Mon Jul 10 2000
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/MarkerRangeRequesterListener.java,v 1.1 2004-06-09 09:47:06 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  This interface is implemented by those classes that need to listen for
+ *  MarkerRangeRequesterEvents.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: MarkerRangeRequesterListener.java,v 1.1 2004-06-09 09:47:06 tjc Exp $
+ **/
+
+public interface MarkerRangeRequesterListener {
+  /** 
+   *  Invoked when the user presses the OK or Cancel button on a
+   *  MarkerRangeRequester component.
+   **/
+  void actionPerformed (final MarkerRangeRequesterEvent event);
+}
diff --git a/uk/ac/sanger/artemis/components/MessageDialog.java b/uk/ac/sanger/artemis/components/MessageDialog.java
new file mode 100644
index 000000000..18c5a0f52
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/MessageDialog.java
@@ -0,0 +1,143 @@
+/* MessageDialog.java
+ *
+ * created: Mon Dec 14 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/MessageDialog.java,v 1.1 2004-06-09 09:47:07 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Options;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A popup dialog box that displays a message and has an OK JButton.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: MessageDialog.java,v 1.1 2004-06-09 09:47:07 tjc Exp $
+ **/
+
+public class MessageDialog extends JDialog {
+  /**
+   *  Create a blocking MessageDialog component.
+   *  @param parent The parent window.
+   *  @param message The message to display in the JDialog and to use as the
+   *    frame title.
+   **/
+  public MessageDialog (final JFrame parent, final String message) {
+    this (parent, message, message, true);
+  }
+  
+  /**
+   *  Create a new MessageDialog component.
+   *  @param parent The parent window.
+   *  @param message The message to display in the JDialog and to use as the
+   *    frame title.
+   *  @param modal If true, dialog blocks input to the parent window when
+   *    shown.
+   **/
+  public MessageDialog (final JFrame parent, final String message,
+                        final boolean modal) {
+    this (parent, message, message, modal);
+  }
+  
+  /**
+   *  Create a blocking MessageDialog component.
+   *  @param parent_frame The parent window.
+   *  @param title The title of the new dialog JFrame.
+   *  @param message The message to display in the JDialog.
+   **/
+  public MessageDialog (final JFrame parent_frame,
+                        final String title,
+                        final String message) {
+    this (parent_frame, title, message, true);
+  }
+
+  /**
+   *  Create a new MessageDialog component.
+   *  @param parent_frame The parent window.
+   *  @param title The title of the new dialog JFrame.
+   *  @param message The message to display in the JDialog.
+   *  @param modal If true, dialog blocks input to the parent window when
+   *    shown.
+   **/
+  public MessageDialog (final JFrame parent_frame,
+                        final String title,
+                        final String message,
+                        final boolean modal) {
+    super (parent_frame, title, modal);
+
+    final Font font = Options.getOptions ().getFont ();
+    setFont (font);
+
+    if (message.length () < MESSAGE_SPLIT_SIZE) {
+      getContentPane ().add (new JLabel (message), "North"); 
+    } else {
+      final JTextArea text_area = new JTextArea (18, 90);
+      text_area.setText (message);
+
+      getContentPane().add (new JScrollPane (text_area), "North");
+      text_area.setEditable (false);
+    }
+
+    final JPanel panel = new JPanel ();
+
+    panel.add (ok_button);
+    ok_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        MessageDialog.this.dispose ();
+      }
+    });
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        MessageDialog.this.dispose ();
+      }
+    });
+
+    addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        MessageDialog.this.dispose ();
+      }
+    });
+    
+    getContentPane ().add (panel, "South");
+    pack ();
+    
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+
+    setVisible (true);
+  }
+
+  /**
+   *  Messages longer than this will be put in a TextArea rather than a Label.
+   **/
+  final private int MESSAGE_SPLIT_SIZE = 100;
+
+  final private JButton ok_button = new JButton ("OK");
+}
+
diff --git a/uk/ac/sanger/artemis/components/MessageFrame.java b/uk/ac/sanger/artemis/components/MessageFrame.java
new file mode 100644
index 000000000..79914703f
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/MessageFrame.java
@@ -0,0 +1,106 @@
+/* MessageFrame.java
+ *
+ * created: Mon Jan 18 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/MessageFrame.java,v 1.1 2004-06-09 09:47:08 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Options;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A popup JFrame box that displays a message and has an OK JButton.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: MessageFrame.java,v 1.1 2004-06-09 09:47:08 tjc Exp $
+ **/
+
+public class MessageFrame extends JFrame {
+  /**
+   *  Create a new MessageFrame component.
+   *  @param message The message to display in the JDialog and it's title.
+   **/
+  public MessageFrame (final String message) {
+    this (message, message);
+
+    this.message = new JLabel (message);
+  }
+
+  /**
+   *  Create a new MessageFrame component.
+   *  @param title The title of the new dialog JFrame.
+   *  @param message The message to display in the JDialog.
+   **/
+  public MessageFrame (final String title,
+                       final String message) {
+    super (title);
+
+    this.message = new JLabel (message);
+    
+    getContentPane ().add (this.message, "North");
+
+    final JPanel panel = new JPanel ();
+
+    panel.add (ok_button);
+    ok_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        MessageFrame.this.dispose ();
+      }
+    });
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        MessageFrame.this.dispose ();
+      }
+    });
+
+    addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        MessageFrame.this.dispose ();
+      }
+    });
+    
+    getContentPane ().add (panel, "South");
+    pack ();
+    
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+
+    setVisible (true);
+  }
+
+  final private JButton ok_button = new JButton ("OK");
+
+  /**
+   *  This is the message displayed above the OK button.  It can be set with
+   *  setMessage ().
+   **/
+  private JLabel message;
+}
+
+
diff --git a/uk/ac/sanger/artemis/components/MultiComparator.java b/uk/ac/sanger/artemis/components/MultiComparator.java
new file mode 100644
index 000000000..daeade376
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/MultiComparator.java
@@ -0,0 +1,1091 @@
+/* MultiComparator.java
+ *
+ * created: Tue Sep 11 2001
+ *
+ * This file is part of Artemis
+ *
+ * Copyright(C) 2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/MultiComparator.java,v 1.1 2004-06-09 09:47:09 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import uk.ac.sanger.artemis.util.FileDocument;
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.util.InputStreamProgressListener;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+import uk.ac.sanger.artemis.io.DocumentEntryFactory;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.SimpleEntryInformation;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.IOException;
+import java.io.File;
+import java.util.Vector;
+import java.net.URL;
+import javax.swing.*;
+import java.awt.dnd.*;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import javax.swing.border.Border;
+import javax.swing.border.BevelBorder;
+
+/**
+ *  This JFrame component contains an arbitrary number of AlignmentViewer
+ *  components and FeatureDisplay components along with ComparatorGlue objects
+ *  to keep them synchronized.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: MultiComparator.java,v 1.1 2004-06-09 09:47:09 tjc Exp $
+ **/
+
+public class MultiComparator extends JFrame 
+                             implements DropTargetListener
+{
+
+  /** The menu_bar is created in makeMenus(). */
+  private final JMenuBar menu_bar = new JMenuBar();
+
+  /**
+   *  An array of EntryGroup objects to display(set by the constructor).  The
+   *  array will contain at least one EntryGroup and will be exactly one
+   *  element longer than comparison_data_array.
+   **/
+  private EntryGroup[] entry_group_array = null;
+
+  /**
+   *  An array of ComparisonData objects to display(set by the constructor).
+   *  The array will be exactly one element shorter than entry_group_array.
+   **/
+  private ComparisonData[] comparison_data_array = null;
+
+  /**
+   *  An array of Selection objects - one per EntryGroup.  Created by
+   *  makeSelectionArray()
+   **/
+  private Selection[] selection_array = null;
+
+  /**
+   *  An array of GotoEventSource objects - one per EntryGroup.  Created by
+   *  makeGotoEventSourceArray()
+   **/
+  private GotoEventSource[] goto_event_source_array = null;
+
+  /**
+   *  An array of FeatureDisplay objects - one per EntryGroup.  Created in the
+   *  constructor.
+   **/
+  private FeatureDisplay[] feature_display_array = null;
+
+  /**
+   *  An array of AlignmentViewer objects - one per ComparisonData.  Created
+   *  in the constructor.
+   **/
+  private AlignmentViewer[] alignment_viewer_array = null;
+
+  /**
+   *  An array of AlignmentViewer objects - one per ComparisonData.  Created
+   *  in the constructor.
+   **/
+  private ComparatorGlue[] comparator_glue_array = null;
+
+  /**
+   *  An array of BasePlotGroup objects - one per FeatureDisplay/EntryGroup.
+   *  Created in the constructor.
+   **/
+  private BasePlotGroup[] base_plot_group_array = null;
+
+  /** This menu is populated by makeFileMenu(). */
+  private final JMenu file_menu = new JMenu("File");
+
+  /**
+   *  The EntrySourceVector reference that is created in the constructor.
+   **/
+  private EntrySourceVector entry_sources;
+
+  /** Used to show the progress of loading file. */
+  private InputStreamProgressListener progress_listener;
+
+  /** current group used for drag and drop */
+  private int current_group = -1;
+
+  /**
+   *  Initialise entry_group_array and comparison_data_array and create all
+   *  the FeatureDisplay and AlignmentViewer components.
+   *  @param entry_group_array The EntryGroup components to display.
+   *  @param comparison_data_array The ComparisonData to display.  This array
+   *    must have one element less than entry_group_array.
+   *  @param progress_listener The object to which InputStreamProgressEvents
+   *    will be send while reading.  Currently unused.
+   **/
+  public MultiComparator(final EntryGroup[] entry_group_array,
+                         final ComparisonData[] comparison_data_array,
+                         final InputStreamProgressListener
+                            progress_listener) 
+  {
+    if(entry_group_array.length != comparison_data_array.length + 1) 
+      throw new Error("internal error - " +
+                      "MultiComparator got illegal arguments");
+
+    this.entry_group_array = entry_group_array;
+    this.comparison_data_array = comparison_data_array;
+    this.progress_listener = progress_listener;
+    this.entry_sources = Utilities.getEntrySources(this, null);
+
+    setDropTarget(new DropTarget(this,this));
+    final StringBuffer title_buffer = new StringBuffer();
+
+    title_buffer.append("ACT: ");
+
+    for(int i = 0; i < entry_group_array.length - 1; ++i) 
+    {
+      final String sequence_name =
+        entry_group_array[i].getDefaultEntry().getName();
+      title_buffer.append(sequence_name).append(" vs ");
+    }
+
+    final EntryGroup last_entry_group =
+      entry_group_array[entry_group_array.length - 1];
+
+    title_buffer.append(last_entry_group.getDefaultEntry().getName());
+
+    setTitle(title_buffer.toString());
+
+    makeSelectionArray();
+    makeGotoEventSourceArray();
+
+    feature_display_array =
+      new FeatureDisplay[entry_group_array.length];
+    base_plot_group_array =
+      new BasePlotGroup[entry_group_array.length];
+    alignment_viewer_array =
+      new AlignmentViewer[comparison_data_array.length];
+    comparator_glue_array =
+      new ComparatorGlue[comparison_data_array.length];
+
+    for(int i = 0; i < getEntryGroupArray().length; ++i) 
+    {
+      final EntryGroup this_entry_group = getEntryGroupArray()[i];
+
+      final BasePlotGroup base_plot_group =
+        new BasePlotGroup(this_entry_group, this,
+                          getSelectionArray()[i],
+                          getGotoEventSourceArray()[i]);
+
+      final int scroll_bar_position;
+
+      if(i == getEntryGroupArray().length - 1 &&
+          getEntryGroupArray().length == 2) 
+      {
+        // put scrollbar at the top normally, but at the bottom of the lower
+        // sequence if there's two entries on display
+        scroll_bar_position = FeatureDisplay.SCROLLBAR_AT_BOTTOM;
+      }
+      else 
+        scroll_bar_position = FeatureDisplay.SCROLLBAR_AT_TOP;
+
+      feature_display_array[i] =
+        new FeatureDisplay(this_entry_group,
+                            getSelectionArray()[i],
+                            getGotoEventSourceArray()[i],
+                            base_plot_group,
+                            scroll_bar_position);
+
+      feature_display_array[i].setShowLabels(false);
+      feature_display_array[i].setHardLeftEdge(false);
+
+      if(getEntryGroupArray().length > 2) 
+      {
+        feature_display_array[i].setShowForwardFrameLines(false);
+        feature_display_array[i].setShowReverseFrameLines(false);
+      }
+
+      feature_display_array[i].addDisplayAdjustmentListener(base_plot_group);
+
+      base_plot_group_array[i] = base_plot_group;
+
+      this_entry_group.ref();
+    }
+
+    for(int i = 0 ; i < getComparisonDataArray().length ; ++i) 
+    {
+      alignment_viewer_array[i] =
+        new AlignmentViewer(getFeatureDisplayArray()[i] ,
+                             getFeatureDisplayArray()[i + 1],
+                             getComparisonDataArray()[i]);
+
+      comparator_glue_array[i] =
+        new ComparatorGlue(this,
+                            getFeatureDisplayArray()[i],
+                            getFeatureDisplayArray()[i + 1],
+                            getAlignmentViewerArray()[i]);
+    }
+
+    final Font default_font = getDefaultFont();
+
+    setFont(default_font);
+
+    makeMenus();
+
+    GridBagLayout gridbag = new GridBagLayout();
+    getContentPane().setLayout(gridbag);
+
+    GridBagConstraints c = new GridBagConstraints();
+
+    c.gridwidth  = GridBagConstraints.REMAINDER;
+    c.fill       = GridBagConstraints.HORIZONTAL;
+    c.anchor     = GridBagConstraints.NORTH;
+    c.gridheight = 1;
+    c.weightx    = 1;
+    c.weighty    = 0;
+
+    for(int i = 0; i < getFeatureDisplayArray().length; ++i) 
+    {
+      if(!(i == getEntryGroupArray().length - 1 &&
+            getEntryGroupArray().length == 2)) 
+      {
+        // put graph above the sequence in this case
+        c.weighty = 0;
+        gridbag.setConstraints(base_plot_group_array[i], c);
+        getContentPane().add(base_plot_group_array[i]);
+      }
+
+      c.weighty = 0;
+      gridbag.setConstraints(feature_display_array[i], c);
+      getContentPane().add(feature_display_array[i]);
+
+      if(i == getEntryGroupArray().length - 1 &&
+            getEntryGroupArray().length == 2) 
+      {
+        // put graph below the sequence in this case
+        c.weighty = 0;
+        gridbag.setConstraints(base_plot_group_array[i], c);
+        getContentPane().add(base_plot_group_array[i]);
+      }
+
+      if(i < getAlignmentViewerArray().length) 
+      {
+        c.fill = GridBagConstraints.BOTH;
+        c.weighty = 1;
+        gridbag.setConstraints(getAlignmentViewerArray()[i], c);
+        getContentPane().add(getAlignmentViewerArray()[i]);
+      }
+    }
+
+    for(int i = 0 ; i < getEntryGroupArray().length ; ++i) 
+    {
+      final EntryGroupChangeListener change_listener =
+                                 new EntryGroupChangeListener() 
+      {
+          public void entryGroupChanged(final EntryGroupChangeEvent event) 
+          {
+            switch(event.getType())
+            {
+              case EntryGroupChangeEvent.ENTRY_ADDED:
+              case EntryGroupChangeEvent.ENTRY_DELETED:
+                makeFileMenu();
+                break;
+            }
+          }
+        };
+
+      getEntryGroupArray()[i].addEntryGroupChangeListener(change_listener);
+
+      final EntryChangeListener entry_change_listener =
+                                 new EntryChangeListener() 
+      {
+          public void entryChanged(final EntryChangeEvent event)
+          {
+            switch(event.getType()) 
+            {
+              case EntryChangeEvent.NAME_CHANGED:
+                makeFileMenu();
+                break;
+            }
+          }
+        };
+
+      getEntryGroupArray()[i].addEntryChangeListener(entry_change_listener);
+    }
+
+    addWindowListener(new WindowAdapter() 
+    {
+      public void windowClosing(WindowEvent event) 
+      {
+        closeComparator();
+      }
+    });
+
+    final URL icon_url = Splash.class.getResource("/icon.gif");
+
+    if(icon_url != null) 
+    {
+      Toolkit toolkit = Toolkit.getDefaultToolkit();
+      final Image icon_image = toolkit.getImage(icon_url);
+      MediaTracker tracker = new MediaTracker(this);
+      tracker.addImage(icon_image, 0);
+
+      try 
+      {
+        tracker.waitForAll();
+        setIconImage(icon_image);
+      } 
+      catch(InterruptedException e) 
+      {
+        // ignore and continue
+      }
+    }
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+
+    packme();
+
+    int screen_height = screen.height;
+    int screen_width = screen.width;
+
+    if(getAlignmentViewerArray().length > 0) 
+    {
+      if(screen_width <= 900 || screen_height <= 700) 
+        setSize(screen_width * 9 / 10, screen_height * 9 / 10);
+      else 
+        setSize(900, 700);
+    }
+
+    setLocation(new Point((screen.width - getSize().width) / 2,
+                          (screen.height - getSize().height) / 2));
+  }
+
+  /**
+   *  Move all the sequences(and comparisons) up one place.  The first and
+   *  last EntryGroups should be the same(reference).
+   **/
+  private void rotateSequences() 
+  {
+    final EntryGroup[] new_entry_groups =
+      new EntryGroup[getEntryGroupArray().length];
+
+    final ComparisonData[] new_comparison_data =
+      new ComparisonData[getComparisonDataArray().length];
+
+    for(int i = 1 ; i < new_entry_groups.length ; ++i) 
+      new_entry_groups[i - 1] = getEntryGroupArray()[i];
+
+    new_entry_groups[new_entry_groups.length - 1] = new_entry_groups[0];
+
+    for(int i = 1; i < new_comparison_data.length; ++i) 
+      new_comparison_data[i - 1] = getComparisonDataArray()[i];
+
+    new_comparison_data[new_comparison_data.length - 1] =
+      getComparisonDataArray()[0];
+
+    final MultiComparator new_comparator =
+      new MultiComparator(new_entry_groups, new_comparison_data,
+                           progress_listener);
+
+    setVisible(false);
+    new_comparator.setVisible(true);
+    closeComparator();
+  }
+
+  /**
+   *  If there are no unsaved changes, close this EntryEdit.  Otherwise ask
+   *  the user first.
+   **/
+  private void closeComparator() 
+  {
+    for(int i = 0 ; i < getEntryGroupArray().length ; ++i)
+    {
+      final EntryGroup entry_group = getEntryGroupArray()[i];
+
+      if(entry_group.hasUnsavedChanges() &&
+          entry_group.refCount() == 1) 
+      {
+        final YesNoDialog yes_no_dialog =
+          new YesNoDialog(this,
+                           "there are unsaved changes - really close?");
+
+        if(!yes_no_dialog.getResult()) 
+          return;
+      }
+    }
+
+    for(int i = 0 ; i < getEntryGroupArray().length ; ++i) 
+    {
+      final EntryGroup entry_group = getEntryGroupArray()[i];
+      entry_group.unref();
+    }
+
+    dispose();
+  }
+
+  /**
+   *  This method will call pack() and then move the JFrame to the centre of
+   *  the screen. (Implementation of the FeatureDisplayOwner interface).
+   **/
+  public void packme() 
+  {
+    pack();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+
+    final int x_position =(screen.width - getSize().width) / 2;
+    int y_position =(screen.height - getSize().height) / 2;
+
+    if(y_position < 10) 
+      y_position = 10;
+
+    setLocation(new Point(x_position, y_position));
+  }
+
+  /**
+   *  Make one Selection object each EntryGroup.
+   **/
+  private void makeSelectionArray() 
+  {
+    selection_array = new Selection[getEntryGroupArray().length];
+    for(int i = 0 ; i < selection_array.length ; ++i) 
+      selection_array[i] = new Selection(null);
+  }
+
+  /**
+   *  Make subject_goto_event_source and query_goto_event_source and wire them
+   *  togeather so that goto events do the right thing.
+   **/
+  private void makeGotoEventSourceArray() 
+  {
+    goto_event_source_array =
+      new GotoEventSource[getEntryGroupArray().length];
+
+    for(int i = 0 ; i < goto_event_source_array.length ; ++i) 
+    {
+      goto_event_source_array[i] =
+        new SimpleGotoEventSource(getEntryGroupArray()[i]) 
+      {
+          /**
+           *  Send the given event to all the GotoListeners.
+           **/
+          public void sendGotoEvent(final GotoEvent goto_event) 
+          {
+            // temporarily disable moving so that the other FeatureDisplay
+            // doesn't start moving(because of the listeners set up in
+            // addDisplayListeners()
+            super.sendGotoEvent(goto_event);
+          }
+        };
+    }
+  }
+
+  /**
+   *  Save the given entry, prompting for a file name if necessary.
+   **/
+  private void saveEntry(final Entry entry) 
+  {
+    if(entry.getName() == null) 
+    {
+      final EntryFileDialog file_dialog = new EntryFileDialog(this, true);
+
+      file_dialog.saveEntry(entry, true, true,
+                             true, DocumentEntryFactory.ANY_FORMAT);
+
+    }
+    else 
+    {
+      try
+      {
+        entry.save(DocumentEntryFactory.ANY_FORMAT);
+      }
+      catch(IOException e)
+      {
+        new MessageDialog(this, "error while saving: " + e);
+        return;
+      } 
+      catch(EntryInformationException e) 
+      {
+        new MessageDialog(this, "error while saving: " + e);
+        return;
+      }
+    }
+  }
+
+  /**
+   *  Return the name to use for the sub JMenu for the given EntryGroup.
+   **/
+  private String makeNewSubMenuName(final EntryGroup entry_group) 
+  {
+    final Entry sequence_entry = entry_group.getSequenceEntry();
+
+    if(sequence_entry == null) 
+      return "(no name)";
+    else
+    {
+      final String sequence_name = sequence_entry.getName();
+      if(sequence_name == null) 
+        return "(no name)";
+      else 
+        return sequence_name;
+    }
+
+  }
+
+  /**
+   *  Make and add the menus for this component.
+   **/
+  private void makeMenus() 
+  {
+    final Font default_font = getDefaultFont();
+
+    setJMenuBar(menu_bar);
+
+    makeFileMenu();
+
+    menu_bar.add(file_menu);
+
+    final JMenu entries_menu = new JMenu("Entries");
+    menu_bar.add(entries_menu);
+    final JMenu select_menu = new JMenu("Select");
+    menu_bar.add(select_menu);
+    final JMenu view_menu = new JMenu("View");
+    menu_bar.add(view_menu);
+    final JMenu goto_menu = new JMenu("Goto");
+    menu_bar.add(goto_menu);
+    final JMenu edit_menu = new JMenu("Edit");
+    menu_bar.add(edit_menu);
+    final JMenu create_menu = new JMenu("Create");
+    menu_bar.add(create_menu);
+    final JMenu write_menu = new JMenu("Write");
+    menu_bar.add(write_menu);
+    JMenu run_menu = null;
+    if(Options.isUnixHost()) 
+    {
+      run_menu = new JMenu("Run");
+      menu_bar.add(run_menu);
+    }
+    final JMenu graph_menu = new JMenu("Graph");
+    menu_bar.add(graph_menu);
+
+    for(int i = 0 ; i < getEntryGroupArray().length ; ++i)
+    {
+      final EntryGroup entry_group = getEntryGroupArray()[i];
+
+      final String sub_menu_name = makeNewSubMenuName(entry_group);
+
+      final EntryGroupMenu this_entries_menu =
+        new EntryGroupMenu(this,
+                            getEntryGroupArray()[i],
+                            sub_menu_name);
+      entries_menu.add(this_entries_menu);
+
+      final SelectMenu this_select_menu =
+        new SelectMenu(this,
+                        getSelectionArray()[i],
+                        getGotoEventSourceArray()[i],
+                        getEntryGroupArray()[i],
+                        getBasePlotGroupArray()[i],
+                        sub_menu_name);
+      select_menu.add(this_select_menu);
+
+      final ViewMenu this_view_menu =
+        new ViewMenu(this,
+                      getSelectionArray()[i],
+                      getGotoEventSourceArray()[i],
+                      getEntryGroupArray()[i],
+                      getBasePlotGroupArray()[i],
+                      sub_menu_name);
+      view_menu.add(this_view_menu);
+
+      final GotoMenu this_goto_menu =
+        new GotoMenu(this,
+                      getSelectionArray()[i],
+                      getGotoEventSourceArray()[i],
+                      getEntryGroupArray()[i],
+                      sub_menu_name);
+      goto_menu.add(this_goto_menu);
+
+      AddMenu this_create_menu = null;
+
+      if(Options.readWritePossible()) 
+      {
+        final EditMenu this_edit_menu =
+          new EditMenu(this,
+                        getSelectionArray()[i],
+                        getGotoEventSourceArray()[i],
+                        getEntryGroupArray()[i],
+                        getBasePlotGroupArray()[i],
+                        sub_menu_name);
+        edit_menu.add(this_edit_menu);
+
+        this_create_menu =
+          new AddMenu(this,
+                       getSelectionArray()[i],
+                       getEntryGroupArray()[i],
+                       getGotoEventSourceArray()[i],
+                       getBasePlotGroupArray()[i],
+                       sub_menu_name);
+        create_menu.add(this_create_menu);
+
+        final WriteMenu this_write_menu =
+          new WriteMenu(this,
+                         getSelectionArray()[i],
+                         getEntryGroupArray()[i],
+                         sub_menu_name);
+        write_menu.add(this_write_menu);
+
+        if(Options.isUnixHost()) 
+        {
+          final RunMenu this_run_menu =
+            new RunMenu(this,
+                         getSelectionArray()[i],
+                         sub_menu_name);
+          run_menu.add(this_run_menu);
+        }
+      }
+
+      final GraphMenu this_graph_menu =
+        new GraphMenu(this,
+                       getEntryGroupArray()[i],
+                       getBasePlotGroupArray()[i],
+                       getFeatureDisplayArray()[i],
+                       sub_menu_name);
+      graph_menu.add(this_graph_menu);
+    }
+
+    final JMenu display_menu = new JMenu("Display");
+
+    final JMenuItem hide_on_frame_lines_item =
+      new JMenuItem("Hide Frame Lines");
+    hide_on_frame_lines_item.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event) 
+      {
+        for(int i = 0 ; i < getFeatureDisplayArray().length ; ++i) 
+        {
+          getFeatureDisplayArray()[i].setShowForwardFrameLines(false);
+          getFeatureDisplayArray()[i].setShowReverseFrameLines(false);
+        }
+      }
+    });
+
+    display_menu.add(hide_on_frame_lines_item);
+
+    final JMenuItem show_on_frame_lines_item =
+      new JMenuItem("Show Frame Lines");
+    show_on_frame_lines_item.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event) 
+      {
+        for(int i = 0 ; i < getFeatureDisplayArray().length ; ++i) 
+        {
+          getFeatureDisplayArray()[i].setShowForwardFrameLines(true);
+          getFeatureDisplayArray()[i].setShowReverseFrameLines(true);
+        }
+      }
+    });
+
+    display_menu.add(show_on_frame_lines_item);
+
+    menu_bar.add(display_menu);
+  }
+
+  /**
+   *  Make a new File menu replacing the current one(if any).
+   **/
+  private void makeFileMenu() 
+  {
+    file_menu.removeAll();
+
+    final EntryGroup[] entry_group_array = getEntryGroupArray();
+
+    JMenuItem popFileManager = new JMenuItem("Show File Manager ...");
+    popFileManager.addActionListener(new ActionListener()
+    {
+      public void actionPerformed(ActionEvent event)
+      {
+        if(ActMain.filemanager == null)
+          ActMain.filemanager = new FileManager(MultiComparator.this,null);
+        else
+          ActMain.filemanager.setVisible(true);
+      }
+    });
+    file_menu.add(popFileManager);
+
+    if(entry_group_array[0] ==
+       entry_group_array[entry_group_array.length - 1]) 
+    {
+      final JMenuItem rotate_button = new JMenuItem("Rotate Sequences");
+      rotate_button.addActionListener(new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          rotateSequences();
+        }
+      });
+
+      file_menu.add(rotate_button);
+      file_menu.addSeparator();
+    }
+
+    for(int i = 0; i < getEntryGroupArray().length; ++i) 
+    {
+      final EntryGroup entry_group = getEntryGroupArray()[i];
+      final String new_menu_name = makeNewSubMenuName(entry_group);
+      final JMenu entry_group_menu = new JMenu(new_menu_name);
+      file_menu.add(entry_group_menu);
+
+      for(int source_index = 0; source_index < entry_sources.size();
+          ++source_index) 
+      {
+        final EntrySource this_source =
+          entry_sources.elementAt(source_index);
+
+        if(this_source.isFullEntrySource()) 
+          continue;
+
+        String entry_source_name = this_source.getSourceName();
+        String menu_item_name = null;
+
+        if(entry_source_name.equals("Filesystem")) 
+          menu_item_name = "Read An Entry ...";
+        else 
+          menu_item_name = "Read An Entry From " + entry_source_name + " ...";
+
+        final JMenuItem read_entry = new JMenuItem(menu_item_name);
+
+        read_entry.addActionListener(new ActionListener() 
+        {
+          public void actionPerformed(ActionEvent event) 
+          {
+            readAnEntry(this_source,entry_group);
+          }
+        });
+
+        entry_group_menu.add(read_entry);
+      }
+
+      entry_group_menu.addSeparator();
+
+      if(entry_group == null || entry_group.size() == 0) 
+      {
+        // don't create a menu
+      }
+      else 
+      {
+        final JMenu save_entry_menu = new JMenu("Save Entry");
+
+        for(int entry_index = 0; entry_index < entry_group.size();
+            ++entry_index) 
+        {
+          final Entry this_entry = entry_group.elementAt(entry_index);
+          String entry_name = this_entry.getName();
+
+          if(entry_name == null) 
+            entry_name = "no name";
+
+          final ActionListener save_entry_listener =
+            new ActionListener() 
+          {
+            public void actionPerformed(final ActionEvent event) 
+            {
+              MultiComparator.this.saveEntry(this_entry);
+            }
+          };
+
+          final JMenuItem save_entry_item = new JMenuItem(entry_name);
+          save_entry_item.addActionListener(save_entry_listener);
+          save_entry_menu.add(save_entry_item);
+        }
+
+        entry_group_menu.add(save_entry_menu);
+
+        final JMenuItem save_all_menu = new JMenuItem("Save All");
+
+        save_all_menu.addActionListener(new ActionListener() 
+        {
+          public void actionPerformed(ActionEvent event) 
+          {
+            for(int entry_index = 0; entry_index < entry_group.size();
+                ++entry_index) 
+            {
+              final Entry this_entry = entry_group.elementAt(entry_index);
+              MultiComparator.this.saveEntry(this_entry);
+            }
+          }
+        });
+
+        entry_group_menu.add(save_all_menu);
+        entry_group_menu.addSeparator();
+      }
+
+      final JMenuItem edit_subject = new JMenuItem("Edit In Artemis");
+      edit_subject.addActionListener(new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          final EntryEdit entry_edit = new EntryEdit(entry_group);
+          entry_edit.setVisible(true);
+        }
+      });
+
+      entry_group_menu.add(edit_subject);
+    }
+
+    file_menu.addSeparator();
+
+    final JMenuItem close_button = new JMenuItem("Close");
+    close_button.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event) 
+      {
+        closeComparator();
+      }
+    });
+
+    file_menu.add(close_button);
+  }
+
+
+  /**
+   *  Read an entry
+   **/
+  private void readAnEntry(final EntrySource this_source,
+                           final EntryGroup entry_group)
+  {
+    final ProgressThread progress_thread = new ProgressThread(null,
+                                               "Loading Entry...");
+                                                                                
+    SwingWorker entryWorker = new SwingWorker()
+    {
+      public Object construct()
+      {
+        try
+        {
+          final Entry new_entry = this_source.getEntry(entry_group.getBases(),
+                                                   progress_thread, true);
+          if(new_entry != null)
+            entry_group.add(new_entry);
+        }
+        catch(final OutOfRangeException e)
+        {
+          new MessageDialog(MultiComparator.this,
+                         "read failed: one of the features " +
+                         "in the entry has an out of " +
+                         "range location: " +
+                         e.getMessage());
+        }
+        catch(final IOException e)
+        {
+          new MessageDialog(MultiComparator.this,
+                         "read failed due to an IO error: " +
+                         e.getMessage());
+        }
+        return null;
+      }
+                                                                                
+       public void finished()
+       {
+         if(progress_thread !=null)
+           progress_thread.finished();
+       }
+    };
+    entryWorker.start();
+  }
+
+  /**
+   *  Read an entry
+   **/
+  private void readAnEntryFromFile(final File file,
+                                   final EntryGroup entry_group)
+  {
+    final ProgressThread progress_thread = new ProgressThread(null,
+                                               "Loading Entry...");
+    progress_thread.start();
+                                                                                                      
+    SwingWorker entryWorker = new SwingWorker()
+    {
+      public Object construct()
+      {
+        try
+        {
+          EntryInformation new_entry_information =
+             new SimpleEntryInformation(Options.getArtemisEntryInformation());
+                                                                                                      
+          final Entry new_entry =  new Entry(entry_group.getBases(),
+                         EntryFileDialog.getEntryFromFile(null,
+                          new FileDocument(file),
+                          new_entry_information, false));
+                                                                                                      
+          if(new_entry != null)
+            entry_group.add(new_entry);
+        }
+        catch(final OutOfRangeException e)
+        {
+          new MessageDialog(MultiComparator.this,
+                         "read failed: one of the features " +
+                         "in the entry has an out of " +
+                         "range location: " +
+                         e.getMessage());
+        }
+        return null;
+      }
+                                                                                                      
+       public void finished()
+       {
+         if(progress_thread !=null)
+           progress_thread.finished();
+       }
+    };
+    entryWorker.start();
+  }
+
+
+  /**
+   *  Return the current default font(from Diana.options).
+   **/
+  private Font getDefaultFont() 
+  {
+    return Options.getOptions().getFont();
+  }
+
+  /**
+   *  Return the EntryGroup objects that were passed to the constructor.
+   **/
+  private EntryGroup[] getEntryGroupArray() 
+  {
+    return entry_group_array;
+  }
+
+  /**
+   *  Return the ComparisonData objects that were passed to the constructor.
+   **/
+  private ComparisonData[] getComparisonDataArray() 
+  {
+    return comparison_data_array;
+  }
+
+  /**
+   *  Return the AlignmentViewer objects that were created in the constructor.
+   **/
+  private AlignmentViewer[] getAlignmentViewerArray() 
+  {
+    return alignment_viewer_array;
+  }
+
+  /**
+   *  Return the FeatureDisplay objects that were created in the constructor.
+   **/
+  private FeatureDisplay[] getFeatureDisplayArray() 
+  {
+    return feature_display_array;
+  }
+
+  /**
+   *  Return the Selection objects that were created in the constructor.
+   **/
+  private Selection[] getSelectionArray() 
+  {
+    return selection_array;
+  }
+
+  /**
+   *  Return the GotoEventSource objects that were created in the constructor.
+   **/
+  private GotoEventSource[] getGotoEventSourceArray() 
+  {
+    return goto_event_source_array;
+  }
+
+  /**
+   *  Return the BasePlotGroup objects that were created in the constructor.
+   **/
+  private BasePlotGroup[] getBasePlotGroupArray() 
+  {
+    return base_plot_group_array;
+  }
+
+  // DropTargetListener methods
+
+  protected static Border dropBorder = new BevelBorder(BevelBorder.LOWERED);
+  public void drop(DropTargetDropEvent e)
+  {
+    Transferable t = e.getTransferable();
+    try
+    {
+      if(t.isDataFlavorSupported(FileNode.FILENODE))
+      {
+        FileNode fn = (FileNode)t.getTransferData(FileNode.FILENODE);
+        readAnEntryFromFile(fn.getFile(), 
+                            entry_group_array[current_group]);
+      }
+      else
+        e.rejectDrop();
+    }
+    catch(UnsupportedFlavorException ufe)
+    {
+      ufe.printStackTrace();
+    }
+    catch(IOException ioe)
+    {
+      ioe.printStackTrace();
+    }
+    finally
+    {
+      feature_display_array[current_group].setBorder(null);
+    }
+  }
+
+  public void dragExit(DropTargetEvent e)
+  {
+    for(int i=0;i<feature_display_array.length;i++)
+      feature_display_array[i].setBorder(null);
+  }
+  public void dropActionChanged(DropTargetDragEvent e) {}
+                                                                                                               
+  public void dragOver(DropTargetDragEvent e)
+  {
+    if(e.isDataFlavorSupported(FileNode.FILENODE))
+    {
+      Point ploc = e.getLocation();
+      boolean over_feature_display = false;
+
+      for(int i=0;i<feature_display_array.length;i++)
+      {
+        int top = feature_display_array[i].getY();
+        int btm = top + feature_display_array[i].getHeight();
+ 
+        if(ploc.y > top && ploc.y < btm)
+        {
+          feature_display_array[i].setBorder(dropBorder);
+          e.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+          current_group = i;
+          return;
+        }
+      }
+
+      e.rejectDrag();
+    }
+  }
+                                                                                                               
+  public void dragEnter(DropTargetDragEvent e)
+  {
+    if(e.isDataFlavorSupported(FileNode.FILENODE))
+      e.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/Navigator.java b/uk/ac/sanger/artemis/components/Navigator.java
new file mode 100644
index 000000000..199dd60e7
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/Navigator.java
@@ -0,0 +1,933 @@
+/* Navigator.java
+ *
+ * created: Sun Jan 10 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/Navigator.java,v 1.1 2004-06-09 09:47:10 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.*;
+
+import uk.ac.sanger.artemis.util.StringVector;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This component allows the user to navigate around the Entry.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: Navigator.java,v 1.1 2004-06-09 09:47:10 tjc Exp $
+ **/
+
+public class Navigator extends JFrame
+    implements EntryGroupChangeListener {
+  /**
+   *  Create a new Navigator component.
+   *  @param selection The Selection that the commands will operate on.
+   *  @param goto_event_source The object the we will call gotoBase () on.
+   *  @param entry_group The EntryGroup object used when searching for
+   *    qualifier text in features.
+   **/
+  public Navigator (final Selection selection,
+                    final GotoEventSource goto_event_source,
+                    final EntryGroup entry_group) {
+    super ("Artemis Navigator");
+
+    this.selection = selection;
+    this.entry_group = entry_group;
+    this.goto_event_source = goto_event_source;
+
+    final Font default_font = Options.getOptions ().getFont ();
+
+    GridBagLayout gridbag = new GridBagLayout();
+    getContentPane ().setLayout (gridbag);
+
+    setFont (default_font);
+
+    GridBagConstraints c = new GridBagConstraints();
+
+    c.fill = GridBagConstraints.HORIZONTAL;
+    c.anchor = GridBagConstraints.NORTH;
+    c.weighty = 0;
+
+    final int TEXT_FIELD_WIDTH = 25;
+
+    final ButtonGroup button_group = new ButtonGroup ();
+
+    goto_base_button = new JRadioButton ("Goto Base:", true);
+
+    button_group.add (goto_base_button);
+
+    final JPanel goto_base_panel = new JPanel ();
+    goto_base_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    goto_base_panel.add (goto_base_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (goto_base_panel, c);
+    getContentPane ().add (goto_base_panel);
+
+    goto_base_text = new JTextField ("", TEXT_FIELD_WIDTH);
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (goto_base_text, c);
+    getContentPane ().add (goto_base_text);
+
+    goto_base_text.addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        goto_base_button.setSelected (true);
+        if (e.getKeyChar () == '\n') {
+          doGoto ();
+        }
+      }
+    });
+
+
+    goto_gene_name_button =
+      new JRadioButton ("Goto Feature With Gene Name:", true);
+
+    button_group.add (goto_gene_name_button);
+
+    final JPanel goto_gene_name_panel = new JPanel ();
+    goto_gene_name_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    goto_gene_name_panel.add (goto_gene_name_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (goto_gene_name_panel, c);
+    getContentPane ().add (goto_gene_name_panel);
+
+    goto_gene_name_textfield = new JTextField ("", TEXT_FIELD_WIDTH);
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (goto_gene_name_textfield, c);
+    getContentPane ().add (goto_gene_name_textfield);
+
+    goto_gene_name_textfield.addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        goto_gene_name_button.setSelected (true);
+        if (e.getKeyChar () == '\n') {
+          doGoto ();
+        }
+      }
+    });
+
+
+    goto_qualifier_button =
+      new JRadioButton ("Goto Feature With This Qualifier Value:", true);
+    button_group.add (goto_qualifier_button);
+
+    final JPanel goto_qualifier_panel = new JPanel ();
+    goto_qualifier_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    goto_qualifier_panel.add (goto_qualifier_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (goto_qualifier_panel, c);
+    getContentPane ().add (goto_qualifier_panel);
+
+    goto_qualifier_textfield = new JTextField ("", TEXT_FIELD_WIDTH);
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (goto_qualifier_textfield, c);
+    getContentPane ().add (goto_qualifier_textfield);
+
+    goto_qualifier_textfield.addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        goto_qualifier_button.setSelected (true);
+        if (e.getKeyChar () == '\n') {
+          doGoto ();
+        }
+      }
+    });
+
+
+    goto_key_button =
+      new JRadioButton ("Goto Feature With This Key:", true);
+    button_group.add (goto_key_button);
+
+    final JPanel goto_key_panel = new JPanel ();
+    goto_key_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    goto_key_panel.add (goto_key_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (goto_key_panel, c);
+    getContentPane ().add (goto_key_panel);
+
+    goto_feature_key_textfield = new JTextField ("", TEXT_FIELD_WIDTH);
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (goto_feature_key_textfield, c);
+    getContentPane ().add (goto_feature_key_textfield);
+
+    goto_feature_key_textfield.addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        goto_key_button.setSelected (true);
+        if (e.getKeyChar () == '\n') {
+          doGoto ();
+        }
+      }
+    });
+
+
+    goto_base_pattern_button =
+      new JRadioButton ("Find Base Pattern:", true);
+    button_group.add (goto_base_pattern_button);
+
+    final JPanel goto_base_pattern_panel = new JPanel ();
+    goto_base_pattern_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    goto_base_pattern_panel.add (goto_base_pattern_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (goto_base_pattern_panel, c);
+    getContentPane ().add (goto_base_pattern_panel);
+
+    goto_base_pattern_text = new JTextField ("", TEXT_FIELD_WIDTH);
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (goto_base_pattern_text, c);
+    getContentPane ().add (goto_base_pattern_text);
+
+    goto_base_pattern_text.addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        goto_base_pattern_button.setSelected (true);
+        if (e.getKeyChar () == '\n') {
+          doGoto ();
+        }
+      }
+    });
+
+    goto_aa_pattern_button =
+      new JRadioButton ("Find Amino Acid String:", true);
+    button_group.add (goto_aa_pattern_button);
+
+    final JPanel goto_aa_pattern_panel = new JPanel ();
+    goto_aa_pattern_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    goto_aa_pattern_panel.add (goto_aa_pattern_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (goto_aa_pattern_panel, c);
+    getContentPane ().add (goto_aa_pattern_panel);
+
+    goto_aa_pattern_text = new JTextField ("", TEXT_FIELD_WIDTH);
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (goto_aa_pattern_text, c);
+    getContentPane ().add (goto_aa_pattern_text);
+
+    goto_aa_pattern_text.addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        goto_aa_pattern_button.setSelected (true);
+        if (e.getKeyChar () == '\n') {
+          doGoto ();
+        }
+      }
+    });
+
+
+    goto_base_button.setSelected (true);
+
+    final ButtonGroup start_position_button_group =
+      new ButtonGroup ();
+
+    final JPanel start_at_an_end_panel = new JPanel ();
+
+    start_at_an_end_button =
+      new JRadioButton ("Start search at beginning (or end)", true);
+
+    start_position_button_group.add (start_at_an_end_button);
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+
+    start_at_an_end_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    start_at_an_end_panel.add (start_at_an_end_button);
+
+    gridbag.setConstraints (start_at_an_end_panel, c);
+    getContentPane ().add (start_at_an_end_panel);
+
+
+    final JPanel start_at_selection_panel = new JPanel ();
+
+    start_at_selection_button =
+      new JRadioButton ("Start search at selection", false);
+    start_position_button_group.add (start_at_selection_button);
+
+    start_at_selection_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    start_at_selection_panel.add (start_at_selection_button);
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+
+    gridbag.setConstraints (start_at_selection_panel, c);
+    getContentPane ().add (start_at_selection_panel);
+
+
+    final JPanel option_button_panel = new JPanel ();
+
+    option_button_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+
+    search_backward_button = new JRadioButton ("Search Backward", false);
+    ignore_case_button = new JRadioButton ("Ignore Case", true);
+    partial_match_button = new JRadioButton ("Allow Substring Matches", true);
+
+    option_button_panel.add (search_backward_button);
+    option_button_panel.add (ignore_case_button);
+    option_button_panel.add (partial_match_button);
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+
+    gridbag.setConstraints (option_button_panel, c);
+    getContentPane ().add (option_button_panel);
+
+
+    final JButton goto_button = new JButton ("Goto");
+
+    goto_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        doGoto ();
+      }
+    });
+
+
+    final JButton clear_button = new JButton ("Clear");
+
+    clear_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        clear ();
+      }
+    });
+
+
+    final JButton close_button = new JButton ("Close");
+
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        Navigator.this.dispose ();
+      }
+    });
+
+
+    final FlowLayout flow_layout =
+      new FlowLayout (FlowLayout.CENTER, 15, 5);
+
+    final JPanel close_and_goto_panel = new JPanel (flow_layout);
+
+    close_and_goto_panel.add (goto_button);
+    close_and_goto_panel.add (clear_button);
+    close_and_goto_panel.add (close_button);
+
+    gridbag.setConstraints (close_and_goto_panel, c);
+    getContentPane ().add (close_and_goto_panel);
+
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        getEntryGroup ().removeEntryGroupChangeListener (Navigator.this);
+        Navigator.this.dispose ();
+      }
+    });
+
+    getEntryGroup ().addEntryGroupChangeListener (this);
+
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+
+    setVisible (true);
+  }
+
+  /**
+   *  Implementation of the EntryGroupChangeListener interface.  We listen to
+   *  EntryGroupChange events so that we can get rid of the Navigator when the
+   *  EntryGroup is no longer in use (for example when the EntryEdit is
+   *  closed).
+   **/
+  public void entryGroupChanged (final EntryGroupChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryGroupChangeEvent.DONE_GONE:
+      getEntryGroup ().removeEntryGroupChangeListener (this);
+      dispose ();
+      break;
+    }
+  }
+
+  /**
+   *  This method finds the given pattern in the Bases of the given EntryGroup
+   *  and returns a MarkerRange for it.
+   *  @param pattern This is the pattern to search for.
+   *  @param entry_group The EntryGroup to search.
+   *  @param selection The current selection.
+   *  @param start_at_end If true or if there is nothing in the Selection then
+   *    the search will start at the first or last base (depending on value of
+   *    the next parameter), otherwise the search will start at the Selection.
+   *  @param search_backwards If true the search will move from last base to
+   *    first base, otherwise first to last.
+   *  @return The range that matches the pattern or null if there is no match.
+   **/
+  public static MarkerRange findBasePattern (final BasePattern pattern,
+                                             final EntryGroup entry_group,
+                                             final Selection selection,
+                                             final boolean start_at_end,
+                                             final boolean search_backwards) {
+    // if start_at_end is false we want to start the search at the selection
+    final Marker selection_base = selection.getLowestBaseOfSelection ();
+
+    final Marker start_position;
+
+    if (start_at_end || selection_base == null) {
+      // null means start the search from the beginning
+      start_position = null;
+    } else {
+      start_position = selection_base;
+    }
+
+    final MarkerRange match_range =
+      pattern.findMatch (entry_group.getBases (),
+                         start_position,
+                         entry_group.getSequenceLength (),
+                         search_backwards);
+
+    if (match_range == null) {
+      return null;
+    } else {
+      return match_range;
+    }
+  }
+
+  /**
+   *  This method finds the given amino acid seqeunce, sets the selection to
+   *  the matching bases and then sends a GotoEvent to all the GotoEvent
+   *  listeners that will make the first base of the match visible.
+   *  @param sequence This is the pattern to search for.
+   *  @param entry_group The EntryGroup to search.
+   *  @param selection The current selection.
+   *  @param start_at_end If true or if there is nothing in the Selection then
+   *    the search will start at first or last base (depending on value of the
+   *    next parameter), otherwise the search will start at the Selection.
+   *  @param search_backwards If true the search will move from last base to
+   *    first base, otherwise first to last.
+   *  @return The range that matches the pattern or null if there is no match.
+   **/
+  public static MarkerRange findAminoAcidSequence (final AminoAcidSequence
+                                                     sequence,
+                                                   final EntryGroup
+                                                     entry_group,
+                                                   final Selection selection,
+                                                   final boolean start_at_end,
+                                                   final boolean
+                                                     search_backwards) {
+    // if start_at_end is false we want to start the search at the selection
+    final Marker selection_base = selection.getLowestBaseOfSelection ();
+
+    final Marker start_position;
+
+    if (start_at_end || selection_base == null) {
+      // null means start the search from the beginning
+      start_position = null;
+    } else {
+      start_position = selection_base;
+    }
+
+    final MarkerRange match_range;
+
+    if (search_backwards) {
+      match_range = sequence.findMatch (entry_group.getBases (),
+                                        start_position,
+                                        true);
+    } else {
+      match_range = sequence.findMatch (entry_group.getBases (),
+                                        start_position,
+                                        false);
+    }
+
+    if (match_range == null) {
+      return null;
+    } else {
+      return match_range;
+    }
+  }
+
+  /**
+   *  This method will perform the selected goto function.
+   **/
+  private void doGoto () {
+    if (goto_base_button.isSelected ()) {
+      doGotoBase ();
+      return;
+    }
+
+    if (goto_base_pattern_button.isSelected ()) {
+      doGotoBasePattern ();
+      return;
+    }
+
+    if (goto_aa_pattern_button.isSelected ()) {
+      doGotoAAPattern ();
+      return;
+    }
+
+    if (goto_gene_name_button.isSelected ()) {
+      final StringVector qualifiers_to_search = new StringVector ();
+      qualifiers_to_search.add (Options.getOptions ().getAllGeneNames ());
+
+      doGotoQualifierValue (qualifiers_to_search,
+                            goto_gene_name_textfield.getText ().trim ());
+      return;
+    }
+
+    if (goto_qualifier_button.isSelected ()) {
+      doGotoQualifierValue (null, goto_qualifier_textfield.getText ().trim ());
+      return;
+    }
+
+    if (goto_key_button.isSelected ()) {
+      doGotoKey ();
+      return;
+    }
+  }
+
+  /**
+   *  Clear all the JTextField components.
+   **/
+  private void clear () {
+    goto_base_text.setText ("");
+    goto_base_pattern_text.setText ("");
+    goto_aa_pattern_text.setText ("");
+    goto_qualifier_textfield.setText ("");
+    goto_gene_name_textfield.setText ("");
+    goto_feature_key_textfield.setText ("");
+  }
+
+  /**
+   *  Show a message dialog saying that the search text field is empty.
+   **/
+  private void searchTextEmptyError () {
+    new MessageDialog (this, "You have not entered a value to search for");
+  }
+
+  /**
+   *  Go to the base that the user typed in to the goto_base_text TextArea.
+   **/
+  private void doGotoBase () {
+    final String number_string = goto_base_text.getText ().trim ();
+
+    if (number_string.length () == 0) {
+      new MessageDialog (this, "you have not entered a number to go to");
+      return;
+    }
+
+    try {
+      final int destination_base =
+        Integer.valueOf (number_string).intValue ();
+
+      final MarkerRange destination_range =
+        goto_event_source.gotoBase (destination_base);
+
+      if (destination_range == null) {
+        new MessageDialog (this,
+                           "the base is out of range for this sequence");
+      } else {
+        // success select that base
+        getSelection ().setMarkerRange (destination_range);
+      }
+    } catch (NumberFormatException e) {
+      new MessageDialog (this, "Cannot understand this number: " +
+                         goto_base_text.getText ());
+    }
+  }
+
+  /**
+   *  Go to the base pattern that the user typed in to the
+   *  goto_base_pattern_text TextArea.
+   **/
+  private void doGotoBasePattern () {
+    try {
+      final String pattern_string =
+        goto_base_pattern_text.getText ().trim ();
+
+      if (pattern_string.length () == 0) {
+        new MessageDialog (this, "you have not entered a pattern to go to");
+        return;
+      }
+
+      final BasePattern pattern = new BasePattern (pattern_string);
+
+      final boolean start_at_an_end = start_at_an_end_button.isSelected ();
+
+      start_at_selection_button.setSelected (true);
+
+      final MarkerRange match_range =
+        findBasePattern (pattern,
+                         getEntryGroup (),
+                         getSelection (),
+                         start_at_an_end,
+                         search_backward_button.isSelected ());
+
+      if (match_range == null) {
+        new MessageDialog (this, "reached the end of sequence");
+      } else {
+        getSelection ().setMarkerRange (match_range);
+
+        final Marker first_selected_base =
+          getSelection ().getLowestBaseOfSelection ();
+
+        goto_event_source.gotoBase (first_selected_base);
+      }
+    } catch (BasePatternFormatException e) {
+      new MessageDialog (this,
+                         "Illegal base pattern: " +
+                         goto_base_pattern_text.getText ());
+    }
+  }
+
+  /**
+   *  Find the amino acid pattern that the user typed in to the
+   *  goto_aa_pattern_text TextArea.
+   **/
+  private void doGotoAAPattern () {
+    final String pattern_string =
+      goto_aa_pattern_text.getText ().trim ();
+
+    if (pattern_string.length () == 0) {
+      new MessageDialog (this, "you have not entered a pattern to go to");
+      return;
+    }
+
+    final AminoAcidSequence pattern = new AminoAcidSequence (pattern_string);
+
+    final boolean start_at_an_end = start_at_an_end_button.isSelected ();
+
+    start_at_selection_button.setSelected (true);
+
+    final boolean search_backwards = search_backward_button.isSelected ();
+
+    final MarkerRange match_range =
+      findAminoAcidSequence (pattern,
+                             getEntryGroup (),
+                             getSelection (),
+                             start_at_an_end,
+                             search_backwards);
+
+    if (match_range == null) {
+      new MessageDialog (this, "reached the end of sequence");
+    } else {
+      getSelection ().setMarkerRange (match_range);
+
+      goto_event_source.gotoBase (getSelection ().getLowestBaseOfSelection ());
+    }
+  }
+
+  /**
+   *  Select the next feature that contains the text given by the user in the
+   *  goto_qualifier_textfield TextArea.
+   **/
+  private void doGotoQualifierValue (final StringVector qualifiers_to_search,
+                                     final String search_text) {
+    if (search_text.equals ("")) {
+      searchTextEmptyError ();
+      return;
+    }
+
+    final FeatureVector selected_features =
+      getSelection ().getAllFeatures ();
+
+    final int index;
+
+    if (selected_features.size () == 0 ||
+        start_at_an_end_button.isSelected ()) {
+      index = -1;
+    } else {
+      index = getEntryGroup ().indexOf (selected_features.elementAt (0));
+    }
+
+    final int first_search_feature_index;
+
+    Feature found_feature = null;
+
+    if (search_backward_button.isSelected ()) {
+      if (index == -1) {
+        // nothing was selected so start the search at the first feature
+        first_search_feature_index =
+          getEntryGroup ().getAllFeaturesCount () - 1;
+      } else {
+        first_search_feature_index = index - 1;
+      }
+
+      for (int i = first_search_feature_index ; i >= 0 ; --i) {
+        final Feature this_feature = getEntryGroup ().featureAt (i);
+
+        if (this_feature.containsText (search_text,
+                                       ignore_case_button.isSelected (),
+                                       partial_match_button.isSelected (),
+                                       qualifiers_to_search)) {
+          found_feature = this_feature;
+          break;
+        }
+      }
+    } else {
+      if (index == -1) {
+        // nothing was selected so start the search at the first feature
+        first_search_feature_index = 0;
+      } else {
+        first_search_feature_index = index + 1;
+      }
+
+      for (int i = first_search_feature_index ;
+           i < getEntryGroup ().getAllFeaturesCount () ;
+           ++i) {
+        final Feature this_feature = getEntryGroup ().featureAt (i);
+
+        if (this_feature.containsText (search_text,
+                                       ignore_case_button.isSelected (),
+                                       partial_match_button.isSelected (),
+                                       qualifiers_to_search)) {
+
+          found_feature = this_feature;
+          break;
+        }
+      }
+    }
+
+    if (found_feature == null) {
+      getSelection ().clear ();
+      start_at_an_end_button.setSelected (true);
+
+      new MessageDialog (this, "text not found");
+    } else {
+      getSelection ().set (found_feature);
+      goto_event_source.gotoBase (getSelection ().getLowestBaseOfSelection ());
+      start_at_selection_button.setSelected (true);
+    }
+  }
+
+  /**
+   *  Select the next feature that has the key given by the user in the
+   *  goto_feature_key_textfield TextArea.
+   **/
+  private void doGotoKey () {
+    final FeatureVector selected_features =
+      getSelection ().getAllFeatures ();
+
+    final int index;
+
+    if (selected_features.size () == 0 ||
+        start_at_an_end_button.isSelected ()) {
+      index = -1;
+    } else {
+      index = getEntryGroup ().indexOf (selected_features.elementAt (0));
+    }
+
+    final int first_search_feature_index;
+
+    final String search_key_string =
+      goto_feature_key_textfield.getText ().trim ();
+
+    if (search_key_string.equals ("")) {
+      searchTextEmptyError ();
+      return;
+    }
+
+    Feature found_feature = null;
+
+    if (search_backward_button.isSelected ()) {
+      if (index == -1) {
+        // nothing was selected so start the search at the first feature
+        first_search_feature_index =
+          getEntryGroup ().getAllFeaturesCount () - 1;
+      } else {
+        first_search_feature_index = index - 1;
+      }
+
+      for (int i = first_search_feature_index ; i >= 0 ; --i) {
+        final Feature this_feature = getEntryGroup ().featureAt (i);
+
+        if (keyMatches (this_feature, search_key_string)) {
+          found_feature = this_feature;
+          break;
+        }
+      }
+    } else {
+      if (index == -1) {
+        // nothing was selected so start the search at the first feature
+        first_search_feature_index = 0;
+      } else {
+        first_search_feature_index = index + 1;
+      }
+
+      for (int i = first_search_feature_index ;
+           i < getEntryGroup ().getAllFeaturesCount () ;
+           ++i) {
+        final Feature this_feature = getEntryGroup ().featureAt (i);
+
+        if (keyMatches (this_feature, search_key_string)) {
+          found_feature = this_feature;
+          break;
+        }
+      }
+    }
+
+    if (found_feature == null) {
+      getSelection ().clear ();
+      start_at_an_end_button.setSelected (true);
+
+      new MessageDialog (this, "key not found");
+    } else {
+      getSelection ().set (found_feature);
+      goto_event_source.gotoBase (getSelection ().getLowestBaseOfSelection ());
+      start_at_selection_button.setSelected (true);
+    }
+  }
+
+  /**
+   *  Returns true if and only if the given feature has search_key_string as
+   *  it's Key.
+   **/
+  public boolean keyMatches (final Feature test_feature,
+                             final String search_key_string) {
+    final String feature_key_string;
+
+    if (ignore_case_button.isSelected ()) {
+      feature_key_string = test_feature.getKey ().toString ().toLowerCase ();
+    } else {
+      feature_key_string = test_feature.getKey ().toString ();
+    }
+
+    if (feature_key_string.equals (search_key_string.toLowerCase ())) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   *  Return the Selection object that was passed to the constructor.
+   **/
+  private Selection getSelection () {
+    return selection;
+  }
+
+  /**
+   *  Return the EntryGroup object that was passed to the constructor.
+   **/
+  private EntryGroup getEntryGroup () {
+    return entry_group;
+  }
+
+  /**
+   *  The JRadioButton that selects the goto base function.
+   **/
+  final JRadioButton goto_base_button;
+
+  /**
+   *  The JRadioButton that selects the goto base pattern function.
+   **/
+  final JRadioButton goto_base_pattern_button;
+
+  /**
+   *  The JRadioButton that selects the find amino acid sequence function.
+   **/
+  final JRadioButton goto_aa_pattern_button;
+
+  /**
+   *  The JRadioButton that selects the goto feature qualifier value function.
+   **/
+  final JRadioButton goto_qualifier_button;
+
+  /**
+   *  The JRadioButton that selects the goto gene name function.
+   **/
+  final JRadioButton goto_gene_name_button;
+
+  /**
+   *  The JRadioButton that selects the goto feature key function.
+   **/
+  final JRadioButton goto_key_button;
+
+  /**
+   *  This contains the pattern to search for if the user has selected the
+   *  goto base function.
+   **/
+  final JTextField goto_base_text;
+
+  /**
+   *  This contains the pattern to search for if the user has selected the
+   *  goto base pattern function.
+   **/
+  final JTextField goto_base_pattern_text;
+
+  /**
+   *  This contains the pattern to search for if the user has selected the
+   *  goto amino acid function.
+   **/
+  final JTextField goto_aa_pattern_text;
+
+  /**
+   *  This contains the pattern to search for if the user has selected the
+   *  goto qualifier value function.
+   **/
+  final JTextField goto_qualifier_textfield;
+
+  /**
+   *  This contains the pattern to search for if the user has selected the
+   *  goto gene name function.
+   **/
+  final JTextField goto_gene_name_textfield;
+
+  /**
+   *  This contains the key to search for if the user has selected the
+   *  goto key function.
+   **/
+  final JTextField goto_feature_key_textfield;
+
+  /**
+   *  The user selects this JRadioButton if the search should start at first/last
+   *  base or first/last feature (depending on the search type).
+   **/
+  JRadioButton start_at_an_end_button;
+
+  /**
+   *  The user selects this JRadioButton if the search should start at the
+   *  position of the current selection.
+   **/
+  final JRadioButton start_at_selection_button;
+
+  /**
+   *  If checked the search will go backwards.
+   **/
+  final JRadioButton search_backward_button;
+
+  /**
+   *  If checked the search will ignore the case of the query and subject.
+   **/
+  final JRadioButton ignore_case_button;
+
+  /**
+   *  If checked the search text is allowed to match a substring of a
+   *  qualifier value.
+   **/
+  final JRadioButton partial_match_button;
+
+  /**
+   *  The GotoEventSource object that was passed to the constructor.
+   **/
+  final GotoEventSource goto_event_source;
+
+  /**
+   *  The EntryGroup object that was passed to the constructor.
+   **/
+  final EntryGroup entry_group;
+
+  /**
+   *  This is the Selection that was passed to the constructor.
+   **/
+  final private Selection selection;
+}
diff --git a/uk/ac/sanger/artemis/components/Plot.java b/uk/ac/sanger/artemis/components/Plot.java
new file mode 100644
index 000000000..8ee213ebe
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/Plot.java
@@ -0,0 +1,723 @@
+/* Plot.java
+ *
+ * created: Thu Dec 17 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/Plot.java,v 1.1 2004-06-09 09:47:11 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.plot.*;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This class implements a simple plot component.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: Plot.java,v 1.1 2004-06-09 09:47:11 tjc Exp $
+ **/
+
+public abstract class Plot extends JPanel {
+  /**
+   *  Create a new plot component.
+   *  @param algorithm The object that will generate the values we plot in
+   *    this component.
+   *  @param draw_scale If true then a scale line will be drawn at the bottom
+   *    of the graph.
+   **/
+  public Plot (Algorithm algorithm, boolean draw_scale) {
+    this.algorithm = algorithm;
+    this.draw_scale = draw_scale;
+
+    final Font font = Options.getOptions ().getFont ();
+
+    setFont (font);
+    FontMetrics fm = getFontMetrics (font);
+    font_height = fm.getHeight ();
+
+    setLayout(new BorderLayout ());
+
+    final int MAX_WINDOW;
+
+    if (getAlgorithm ().getDefaultMaxWindowSize () != null) {
+      MAX_WINDOW = getAlgorithm ().getDefaultMaxWindowSize ().intValue ();
+    } else {
+      MAX_WINDOW = 500;
+    }
+
+    final int MIN_WINDOW;
+
+    if (getAlgorithm ().getDefaultMinWindowSize () != null) {
+      MIN_WINDOW = getAlgorithm ().getDefaultMinWindowSize ().intValue ();
+    } else {
+      MIN_WINDOW = 5;
+    }
+
+    final int START_WINDOW;
+
+    if (getAlgorithm ().getDefaultWindowSize () == null) {
+      START_WINDOW = 10;
+    } else {
+      START_WINDOW = getAlgorithm ().getDefaultWindowSize ().intValue ();
+    }
+
+    window_changer = new JScrollBar (Scrollbar.VERTICAL);
+    window_changer.setValues (START_WINDOW, SCROLL_NOB_SIZE,
+                              MIN_WINDOW, MAX_WINDOW + SCROLL_NOB_SIZE);
+    if (MAX_WINDOW >= 50) {
+      window_changer.setBlockIncrement (MAX_WINDOW/50);
+    } else {
+      window_changer.setBlockIncrement (1);
+    }
+
+    window_changer.addAdjustmentListener (new AdjustmentListener () {
+      public void adjustmentValueChanged(AdjustmentEvent e) {
+        recalculate_flag = true;
+        repaintCanvas ();
+      }
+    });
+
+    addComponentListener (new ComponentAdapter () {
+      public void componentShown(ComponentEvent e) {
+        recalculate_flag = true;
+        repaintCanvas ();
+      }
+    });
+
+    add (window_changer, "East");
+
+    createCanvas ();
+
+    getCanvas ().setBackground (Color.white);
+  }
+
+  final int SCROLL_NOB_SIZE = 10;
+
+  /**
+   *  Return the algorithm that was passed to the constructor.
+   **/
+  public Algorithm getAlgorithm () {
+    return algorithm;
+  }
+
+  /**
+   *  Return the current value of the window size, as set by the
+   *  window_changer scrollbar.
+   **/
+  public int getWindowSize () {
+    return window_changer.getValue ();
+  }
+
+  /**
+   *  Create the canvas object for this BasePlot and add it to the component.
+   **/
+  private void createCanvas () {
+    canvas = new JComponent () {
+      /**
+       *  Set the offscreen buffer to null as part of invalidation.
+       **/
+      public void invalidate() {
+        super.invalidate();
+        offscreen = null;
+      }
+
+      /**
+       *  Override update to *not* erase the background before painting
+       */
+      public void update(Graphics g) {
+        paint(g);
+      }
+
+      /**
+       *  Paint the canvas.
+       */
+      public void paint(Graphics g) {
+        paintCanvas (g);
+      }
+    };
+
+    final MouseListener mouse_listener =
+      new MouseAdapter () {
+        /**
+         *  Listen for mouse press events so that we can do a popup menu and a
+         *  crosshair.
+         **/
+        public void mousePressed (MouseEvent event) {
+          if (event.isPopupTrigger () || event.isMetaDown ()) {
+            final JComponent parent = (JComponent) event.getSource ();
+
+            final JPopupMenu popup = new JPopupMenu ("Plot Options");
+
+            final JCheckBoxMenuItem scaling_toggle =
+              new JCheckBoxMenuItem ("Scaling");
+
+            scaling_toggle.setState (getAlgorithm ().scalingFlag ());
+            scaling_toggle.addItemListener (new ItemListener () {
+              public void itemStateChanged (ItemEvent _) {
+                getAlgorithm ().setScalingFlag (scaling_toggle.getState ());
+                recalculate_flag = true;
+                repaintCanvas ();
+              }
+            });
+
+            popup.add (scaling_toggle);
+
+            popup.addSeparator ();
+
+            final JMenuItem max_window_size =
+              new JMenuItem ("Maximum Window Size:");
+
+            popup.add (max_window_size);
+
+            final int [] window_sizes = {
+              100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000,
+              200000, 500000, 1000000
+            };
+
+            for (int i = 0 ; i < window_sizes.length ; ++i) {
+              final int size = i;
+
+              JMenuItem window_size_item = new JMenuItem (" " + window_sizes[i]);
+
+              window_size_item.addActionListener (new ActionListener () {
+                public void actionPerformed (ActionEvent _) {
+                  final int new_maximum = window_sizes[size];
+                  if (new_maximum > window_changer.getMinimum ()) {
+                    window_changer.setMaximum (new_maximum + SCROLL_NOB_SIZE);
+                    recalculate_flag = true;
+                    repaintCanvas ();
+                  }
+                }
+              });
+
+              popup.add (window_size_item);
+            }
+
+            parent.add (popup);
+
+            popup.show (parent, event.getX (), event.getY ());
+          } else {
+            final int point_x = event.getPoint ().x;
+            final int point_y = event.getPoint ().y;
+
+            if (point_y > getLabelHeight ()) {
+              cross_hair_position = point_x;
+              drag_start_position = point_x;
+            } else {
+              cancelCrossHairs ();
+            }
+
+            if (event.getClickCount () == 2) {
+              fireDoubleClickEvent ();
+            } else {
+              fireClickEvent ();
+            }
+
+            repaintCanvas ();
+          }
+        }
+      };
+
+    canvas.addMouseListener (mouse_listener);
+
+    final MouseMotionListener mouse_motion_listener =
+      new MouseMotionAdapter () {
+
+      public void mouseDragged (MouseEvent event) {
+        final int point_x = event.getPoint ().x;
+        final int point_y = event.getPoint ().y;
+
+        if (point_y > getLabelHeight ()) {
+          cross_hair_position = point_x;
+        } else {
+          cancelCrossHairs ();
+        }
+
+        fireDragEvent ();
+
+        repaintCanvas ();
+      }
+    };
+
+    canvas.addMouseMotionListener (mouse_motion_listener);
+
+    add (canvas, "Center");
+  }
+
+  /**
+   *  Call mouseClick () on each of the PlotMouseListener objects in the
+   *  listener list.
+   **/
+  private void fireClickEvent () {
+    for (int i = 0 ; i < listener_list.size () ; ++i) {
+      final PlotMouseListener listener =
+        (PlotMouseListener) listener_list.elementAt (i);
+      listener.mouseClick (getPointPosition (cross_hair_position));
+    }
+  }
+
+  /**
+   *  Call mouseDragged () on each of the PlotMouseListener objects in the
+   *  listener list.
+   **/
+  private void fireDragEvent () {
+    for (int i = 0 ; i < listener_list.size () ; ++i) {
+      final PlotMouseListener listener =
+        (PlotMouseListener) listener_list.elementAt (i);
+      listener.mouseDrag (getPointPosition (drag_start_position),
+                          getPointPosition (cross_hair_position));
+    }
+  }
+
+  /**
+   *  Call mouseDoubleClick () on each of the PlotMouseListener objects in the
+   *  listener list.
+   **/
+  private void fireDoubleClickEvent () {
+    for (int i = 0 ; i < listener_list.size () ; ++i) {
+      final PlotMouseListener listener =
+        (PlotMouseListener) listener_list.elementAt (i);
+      listener.mouseDoubleClick (getPointPosition (cross_hair_position));
+    }
+  }
+
+  /**
+   *  Adds the given listener to receive mouse events from this object.
+   *  @param l the listener.
+   **/
+  public void addPlotMouseListener (final PlotMouseListener listener) {
+    listener_list.addElement (listener);
+  }
+
+  /**
+   *  Removes the given listener from the list of those objects that receive
+   *  mouse events from this object.
+   *  @param l the listener.
+   **/
+  public void removePlotMouseListener (final PlotMouseListener listener) {
+    listener_list.removeElement (listener);
+  }
+
+  /**
+   *  The main paint function for the canvas.  An off screen image used for
+   *  double buffering when drawing the canvas.
+   *  @param g The Graphics object of the canvas.
+   **/
+  private void paintCanvas (final Graphics g) {
+    if (!isVisible ()) {
+      return;
+    }
+
+    final int canvas_width = canvas.getSize ().width;
+    final int canvas_height = canvas.getSize ().height;
+
+    if (canvas_height <= 0 || canvas_width <= 0) {
+      // there is no point painting a zero width canvas
+      return;
+    }
+
+    if (offscreen == null) {
+      offscreen = createImage (canvas_width,
+                               canvas_height);
+    }
+
+    Graphics og = offscreen.getGraphics ();
+    og.setClip (0, 0, canvas_width, canvas_height);
+
+    og.setColor (new Color (240, 240, 240));
+
+    og.fillRect (0, 0, canvas.getSize ().width, canvas.getSize ().height);
+
+    // Redraw the graph on the canvas using the algorithm from the
+    // constructor.
+    drawMultiValueGraph (og);
+
+    drawLabels (og);
+
+    g.drawImage (offscreen, 0, 0, null);
+    og.dispose ();
+  }
+
+  /**
+   *  Return the canvas x position of the last click or -1 if the user hasn't
+   *  clicked anywhere yet.
+   **/
+  protected int getCrossHairPosition () {
+    if (cross_hair_position >= getCanvas ().getSize ().width) {
+      return -1;
+    } else {
+      return cross_hair_position;
+    }
+  }
+
+  /**
+   *  Force this component to stop drawing crosshairs.
+   **/
+  protected void cancelCrossHairs () {
+    cross_hair_position = -1;
+    drag_start_position = -1;
+  }
+
+  // the minimum distance in pixels between the labels
+  private final static int MINIMUM_LABEL_SPACING = 50;
+
+  /**
+   *  Draw the scale line at the bottom of the graph.
+   *  @param start The base on the left
+   *  @param end The base on the right
+   **/
+  protected void drawScaleLine (final Graphics g,
+                                final int start, final int end) {
+
+    final int canvas_width = canvas.getSize ().width;
+    final int canvas_height = canvas.getSize ().height;
+
+    final int scale_number_y_pos =  canvas_height - 1;
+
+    final float bases_per_pixel = 1.0F;
+
+    // set the spacing so that the labels are at multiples of 10
+    final int base_label_spacing = MINIMUM_LABEL_SPACING;
+
+    final int label_spacing = (int)(base_label_spacing / bases_per_pixel);
+
+    final int possible_index_of_first_label = start / base_label_spacing;
+
+    final int index_of_first_label;
+
+    if (possible_index_of_first_label == 0) {
+      index_of_first_label = 1;
+    } else {
+      index_of_first_label = possible_index_of_first_label;
+    }
+
+    final int index_of_last_label = end / base_label_spacing;
+
+    for (int i = index_of_first_label ;
+         i <= index_of_last_label ;
+         ++i) {
+      final String label_string =
+        String.valueOf ((int)(i * base_label_spacing));
+
+      final int scale_number_x_pos =
+        (int) ((i * base_label_spacing - start) / bases_per_pixel);
+
+      g.drawString (label_string,
+                    scale_number_x_pos + 2,
+                    scale_number_y_pos);
+
+      g.drawLine (scale_number_x_pos, canvas_height - getScaleHeight () / 2,
+                  scale_number_x_pos, canvas_height - getScaleHeight ());
+    }
+  }
+
+  /**
+   *  Plot the given points onto a Graphics object.
+   *  @param min_value The minimum of the plot_values.
+   *  @param max_value The maximum of the plot_values.
+   *  @param step_size The current step size for this algorithm.  This is
+   *    never greater than window_size.
+   *  @param window_size The window size used in calculating plot_values.
+   *  @param total_unit_count The maximum number of residues/bases we can
+   *    show.  This is used to draw the scale line and to calculate the
+   *    distance (in pixels) between plot points.
+   *  @param start_position The distance from the edge of the canvas (measured
+   *    in residues/bases) to start drawing the plot.
+   *  @param plot_values The values to plot.
+   **/
+  protected void drawPoints (final Graphics g,
+                             final float min_value, final float max_value,
+                             final int step_size, final int window_size,
+                             final int total_unit_count,
+                             final int start_position,
+                             final float [] plot_values) {
+    final float residues_per_pixel =
+      (float) total_unit_count / canvas.getSize ().width;
+
+    // this is the height of the graph (slightly smaller than the canvas for
+    // ease of viewing).
+    final int graph_height = canvas.getSize ().height -
+      getLabelHeight () -       // leave room for the algorithm name
+      getScaleHeight () -       // leave room for the scale
+      2;
+
+    if (graph_height < 5) {
+      // too small to draw
+      return;
+    }
+
+    final int number_of_values = plot_values.length;
+
+    for (int i = 0 ; i < number_of_values - 1 ; ++i) {
+      final int start_residue =
+        window_size / 2 + i * step_size + start_position;
+      final int end_residue =
+        start_residue + step_size;
+
+      final int start_x = (int) (start_residue / residues_per_pixel);
+      final int end_x = (int) (end_residue / residues_per_pixel);
+
+      // this is a number between 0.0 and 1.0
+      final float scaled_start_value =
+        (plot_values[i] - min_value) / (max_value - min_value);
+      final int start_y =
+        graph_height - (int) (scaled_start_value * graph_height) +
+        getLabelHeight () + 1;
+
+      final float scaled_end_value =
+        (plot_values[i+1] - min_value) / (max_value - min_value);
+      final int end_y =
+        graph_height - (int) (scaled_end_value * graph_height) +
+        getLabelHeight () + 1;
+
+      g.drawLine (start_x, start_y, end_x, end_y);
+    }
+  }
+
+  /**
+   *  Redraw the graph on the canvas using the algorithm.
+   *  @param g The object to draw into.
+   **/
+  protected abstract void drawMultiValueGraph (final Graphics g);
+
+  /**
+   *  Draw a line representing the average of the algorithm over the feature.
+   *  @param g The object to draw into.
+   *  @param min_value The minimum value of the function for the range we are
+   *    viewing
+   *  @param max_value The maximum value of the function for the range we are
+   *    viewing
+   **/
+  protected void drawGlobalAverage (final Graphics g,
+                                    final float min_value,
+                                    final float max_value) {
+    final Float average = getAlgorithm ().getAverage ();
+
+    if (average != null) {
+      g.setColor (Color.gray);
+
+      // this is the height of the graph (slightly smaller than the canvas for
+      // ease of viewing).
+      final int graph_height =
+        canvas.getSize ().height - getFontHeight ();
+
+      // this is a number between 0.0 and 1.0
+      final float scaled_average =
+        (average.floatValue () - min_value) / (max_value - min_value);
+
+      final int position =
+        graph_height -
+        (int) (scaled_average * graph_height) +
+        getFontHeight () + 1;
+
+      g.drawLine (0, position,
+                  canvas.getSize ().width, position);
+
+      final FontMetrics fm = g.getFontMetrics ();
+
+      final int canvas_width = canvas.getSize ().width;
+
+      final String average_string =
+        String.valueOf (Math.round (average.floatValue () * 100.0) / 100.0);
+
+      g.drawString (average_string,
+                    canvas_width - fm.stringWidth (average_string) - 1,
+                    position);
+    }
+  }
+
+  /**
+   *  Put the algorithm name in the top left corner of the canvas and the
+   *  window size in the bottom left.
+   *  @param g The object to draw into.
+   **/
+  private void drawLabels (final Graphics g) {
+    g.setColor (Color.black);
+    g.drawString (getAlgorithm ().getAlgorithmName () +
+                  "  Window size: " +
+                  String.valueOf (window_changer.getValue ()),
+                  2,
+                  font_height);
+  }
+
+  /**
+   *  The method converts the min_value and max_value to String objects and
+   *  then draws them onto the canvas.  The min_value is drawn at the bottom
+   *  right, max_value at the top right.
+   **/
+  protected void drawMinMax (final Graphics g,
+                             final float min_value, final float max_value) {
+    g.setColor (Color.black);
+
+    final int canvas_width = canvas.getSize ().width;
+    final int canvas_height = canvas.getSize ().height;
+
+    g.drawLine (0, canvas_height - getScaleHeight (),
+                canvas_width, canvas_height - getScaleHeight ());
+
+    g.drawLine (0, getLabelHeight (),
+                canvas_width, getLabelHeight ());
+
+    final FontMetrics fm = g.getFontMetrics ();
+
+    final String min_string =
+      String.valueOf (((int) (min_value * 100)) / 100.0);
+
+    g.drawString (min_string,
+                  canvas_width - fm.stringWidth (min_string) - 1,
+                  canvas_height - 1 - getScaleHeight ());
+
+    final String max_string =
+      String.valueOf (((int) (max_value * 100)) / 100.0);
+
+    g.drawString (max_string,
+                  canvas_width - fm.stringWidth (max_string) - 1,
+                  1 + getFontHeight () * 2);
+  }
+
+  /**
+   *  Draw a vertical line at the given position.
+   *  @param label The label to use on the crosshair
+   *  @param label_pos The position on the line at which the label should be
+   *    drawn (0 is nearest the top).
+   **/
+  protected void drawCrossHair (final Graphics g, final int x_position,
+                                final String label, final int label_pos) {
+    if (x_position >= 0) {
+      g.drawLine (x_position, getLabelHeight (),
+                  x_position, canvas.getSize ().height);
+
+      g.drawString (label, x_position + 2,
+                    getFontHeight () * (2 + label_pos) + 2);
+    }
+  }
+
+  /**
+   *  Return the JComponent of this Plot.
+   **/
+  protected JComponent getCanvas () {
+    return canvas;
+  }
+
+  /**
+   *  Call repaint () on the canvas object.
+   **/
+  protected void repaintCanvas () {
+    canvas.repaint ();
+  }
+
+  /**
+   *  Recalculate the values all the state that is used for drawing the plot
+   **/
+  protected abstract void recalculateValues ();
+
+  /**
+   *  Get the position in the Feature or Sequence of the given x canvas
+   *  position.  This is the label used when the user clicks the mouse in on
+   *  the canvas (see drawCrossHair ()).
+   **/
+  protected abstract int getPointPosition (final int canvas_x_position);
+
+  /**
+   *  Return the amount of vertical space (in pixels) to use for the scale.
+   **/
+  private int getScaleHeight () {
+    if (draw_scale) {
+      return getFontHeight () + 2;
+    } else {
+      return 0;
+    }
+  }
+
+  /**
+   *  Return the height in algorithm name and label line (returns the font
+   *  height plus a small amount).
+   **/
+  private int getLabelHeight () {
+    return getFontHeight () + 2;
+  }
+
+  /**
+   *  Return the height in pixels of the current font.
+   **/
+  private int getFontHeight () {
+    return font_height;
+  }
+
+  /**
+   *  The drawing area for this component.
+   **/
+  private JComponent canvas = null;
+
+  /**
+   *  A scroll bar for changing the window size.
+   **/
+  private JScrollBar window_changer = null;
+
+  /**
+   *  The height of the font used in this component.
+   **/
+  private int font_height;
+
+  /**
+   *  Off screen image used for double buffering when drawing the canvas.
+   **/
+  private Image offscreen;
+
+  /**
+   *  The object that will generate the value we plot in this component.
+   **/
+  private Algorithm algorithm;
+
+  /**
+   *  If true then a scale line will be drawn at the bottom of the graph when
+   *  drawScaleLine () is called.
+   **/
+  private boolean draw_scale;
+
+  /**
+   *  Set to true if drawMultiValueGraph () should call recalculateValues ().
+   *  It is reset to false by recalculateValues ().
+   **/
+  protected boolean recalculate_flag = true;
+
+  /**
+   *  The x position of the last click or -1 if the user hasn't clicked
+   *  anywhere yet or the user clicked outside the graph.
+   **/
+  private int cross_hair_position = -1;
+
+  /**
+   *  The x position of the start of the last mouse drag or -1 if the user
+   *  hasn't clicked anywhere yet or the user clicked outside the graph.
+   **/
+  private int drag_start_position = -1;
+
+  /**
+   *  A vector of those objects listening for PlotMouse events.
+   **/
+  final private java.util.Vector listener_list = new java.util.Vector ();
+}
diff --git a/uk/ac/sanger/artemis/components/PlotMouseListener.java b/uk/ac/sanger/artemis/components/PlotMouseListener.java
new file mode 100644
index 000000000..50abff74b
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/PlotMouseListener.java
@@ -0,0 +1,64 @@
+/* PlotMouseListener.java
+ *
+ * created: Wed Sep 13 2000
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/PlotMouseListener.java,v 1.1 2004-06-09 09:47:12 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  Implemented by those objects that need to mouse events from a Plot object.
+ *  The coordinates of the mouse click are translated to base/aa positions for
+ *  ease of use.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: PlotMouseListener.java,v 1.1 2004-06-09 09:47:12 tjc Exp $
+ **/
+
+public interface PlotMouseListener {
+  /**
+   *  Called when the user clicks somewhere on the plot canvas.
+   *  @param position the base/amino acid position of the click.  This is
+   *    -1 if and only if the click was outside the graph (eg. in the label at
+   *    the top)
+   **/
+  void mouseClick (final int position);
+
+  /**
+   *  Called when the user drags the mouse over the plot.
+   *  @param drag_start_position The base/amnino acid position where the drag
+   *    started or -1 if the drag was started outside the graph.
+   *  @param current_position the base/amino acid position of the click.
+   *    This is -1 if and only if the user has dragged the mouse out of
+   *    the graph (eg. in the label at the top)
+   **/
+  void mouseDrag (final int drag_start_position,
+                  final int current_position);
+
+  /**
+   *  Called when the user double-clicks somewhere on the plot.
+   *  @param position the base/amino acid position of the click.  This is
+   *    -1 if and only if the click was outside the graph (eg. in the label at
+   *    the top)
+   **/
+  void mouseDoubleClick (final int position);
+}
diff --git a/uk/ac/sanger/artemis/components/ProcessWatcher.java b/uk/ac/sanger/artemis/components/ProcessWatcher.java
new file mode 100644
index 000000000..be0136492
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ProcessWatcher.java
@@ -0,0 +1,194 @@
+/* ProcessWatcher.java
+ *
+ * created: Mon Oct  4 1999
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ProcessWatcher.java,v 1.1 2004-06-09 09:47:13 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Logger;
+
+import java.io.*;
+
+/**
+ *  Objects of this class watch a Process object and then display a
+ *  MessageFrame window when the process finishes.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: ProcessWatcher.java,v 1.1 2004-06-09 09:47:13 tjc Exp $
+ **/
+
+public class ProcessWatcher
+    implements Runnable  {
+  /**
+   *  Create a new ProcessWatcher object for the given Process.  When the
+   *  process finishes a MessageFrame will alert the user and all
+   *  ProcessWatcherListeners will be informed.
+   *  @param process The Process to watch.
+   *  @param name The name of the process to watch.
+   **/
+  public ProcessWatcher (final Process process, final String name) {
+    this (process, name, true);
+  }
+
+  /**
+   *  Create a new ProcessWatcher object for the given Process.  When the
+   *  process finishes a MessageFrame will alert the user if and only if the
+   *  alert_user argument is true and all ProcessWatcherListeners will be
+   *  informed.
+   *  @param process The Process to watch.
+   *  @param name The name of the process to watch.
+   *  @param alert_user The user will be informed when a process ends if and
+   *    only if this is true.
+   **/
+  public ProcessWatcher (final Process process, final String name,
+                         final boolean alert_user) {
+    this.process = process;
+    this.name = name;
+    this.alert_user = alert_user;
+  }
+
+  /**
+   *  This code will wait for the Process to finish then display the return
+   *  value in a MessageFrame.
+   **/
+  public void run () {
+    try {
+      final Reader reader = new InputStreamReader (process.getErrorStream ());
+      getLogger ().log (reader);
+    } catch (IOException e) {
+      final MessageFrame message_frame =
+        new MessageFrame ("error while reading output of: " + name);
+
+      message_frame.setVisible (true);
+    }
+
+    try {
+      final Reader reader = new InputStreamReader (process.getInputStream ());
+      getLogger ().log (reader);
+    } catch (IOException e) {
+      final MessageFrame message_frame =
+        new MessageFrame ("error while reading output of: " + name);
+
+      message_frame.setVisible (true);
+    }
+
+    while (true) {
+      try {
+        final int return_value = process.waitFor ();
+
+        for (int i = 0 ; i < listeners.size () ; ++i) {
+          final ProcessWatcherEvent event =
+            new ProcessWatcherEvent (process, return_value);
+          final ProcessWatcherListener listener =
+            (ProcessWatcherListener) listeners.elementAt (i);
+          listener.processFinished (event);
+        }
+
+        final boolean core_dumped = (return_value & 0x80) != 0;
+
+        getLogger ().log ("\n--------------------------------------" +
+                          "---------------------\n\n");
+
+        final String log_message;
+
+        if (core_dumped) {
+          log_message = name + " process dumped core";
+          new MessageFrame (log_message +
+                            " - check the log window").setVisible (true);
+        } else {
+          final int sig_number = return_value & 0x7f;
+
+          if (sig_number > 0) {
+            log_message = name + " process received signal: " + sig_number;
+            new MessageFrame (log_message +
+                              " - check the log window").setVisible (true);
+          } else {
+            final int exit_code = return_value >> 8;
+
+            if (exit_code == 0) {
+              log_message = name + " process completed";
+              if (alert_user) {
+                final MessageFrame message_frame =
+                  new MessageFrame (log_message);
+                message_frame.setVisible (true);
+              }
+            } else {
+              log_message =
+                name + " process finished with exit code: " + exit_code;
+              new MessageFrame (log_message +
+                                " - check the log window").setVisible (true);
+            }
+          }
+        }
+
+        getLogger ().log (log_message + "\n");
+
+        return;
+      } catch (InterruptedException e) {
+       // go around the loop again
+      }
+    }
+  }
+
+  /**
+   *  Return the global Logger object.
+   **/
+  private static Logger getLogger () {
+    return Splash.getLogger ();
+  }
+
+  /**
+   *  Add the given object as a ProcessWatcherListener for this ProcessWatcher.
+   **/
+  public void addProcessWatcherListener (final ProcessWatcherListener l) {
+    listeners.addElement (l);
+  }
+
+  /**
+   *  Remove the given object as a ProcessWatcherListener for this
+   *  ProcessWatcher.
+   **/
+  public void removeProcessWatcherListener (final ProcessWatcherListener l) {
+    listeners.removeElement (l);
+  }
+
+  /**
+   *  The Process reference that was passed to the constructor.
+   **/
+  private Process process;
+
+  /**
+   *  The program name that was passed to the constructor.
+   **/
+  private String name;
+
+  /**
+   *  The alert_user argument that was passed to the constructor.
+   **/
+  private boolean alert_user;
+
+  /**
+   *  The list of objects that are listening for ProcessWatcher events.
+   **/
+  private final java.util.Vector listeners = new java.util.Vector ();
+}
diff --git a/uk/ac/sanger/artemis/components/ProcessWatcherEvent.java b/uk/ac/sanger/artemis/components/ProcessWatcherEvent.java
new file mode 100644
index 000000000..61576c0f3
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ProcessWatcherEvent.java
@@ -0,0 +1,70 @@
+/* ProcessWatcherEvent.java
+ *
+ * created: Tue Feb 29 2000
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ProcessWatcherEvent.java,v 1.1 2004-06-09 09:47:14 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  This event is sent when the process that is watched by ProcessWatcher
+ *  finishes.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: ProcessWatcherEvent.java,v 1.1 2004-06-09 09:47:14 tjc Exp $
+ **/
+
+public class ProcessWatcherEvent {
+  /**
+   *  Create a new ProcessWatcherEvent object.
+   *  @param process The Process that has finished.
+   *  @param exit_code The exit code of the process that has finished.
+   **/
+  public ProcessWatcherEvent (final Process process, final int exit_code) {
+    this.process = process;
+    this.exit_code = exit_code;
+  }
+
+  /**
+   *  Return the process that was passed to the constructor.
+   **/
+  public Process getProcess () {
+    return process;
+  }
+
+  /**
+   *  Return the exit_code that was passed to the constructor.
+   **/
+  public int getExitCode () {
+    return exit_code;
+  }
+
+  /**
+   *  The process that was passed to the constructor.
+   **/
+  final Process process;
+
+  /**
+   *  The exit_code that was passed to the constructor.
+   **/
+  final int exit_code;
+}
diff --git a/uk/ac/sanger/artemis/components/ProcessWatcherListener.java b/uk/ac/sanger/artemis/components/ProcessWatcherListener.java
new file mode 100644
index 000000000..723155a4c
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ProcessWatcherListener.java
@@ -0,0 +1,42 @@
+/* ProcessWatcherListener.java
+ *
+ * created: Tue Feb 29 2000
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ProcessWatcherListener.java,v 1.1 2004-06-09 09:47:15 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  This interface is implemented by those classes that need to be notified
+ *  when a Process finishes.  The Process must be watched with a
+ *  ProcessWatcher object.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: ProcessWatcherListener.java,v 1.1 2004-06-09 09:47:15 tjc Exp $
+ **/
+
+public interface ProcessWatcherListener {
+  /**
+   *  Invoked by a ProcessWatcher object when a Process finishes.
+   **/
+  void processFinished (final ProcessWatcherEvent event);
+}
diff --git a/uk/ac/sanger/artemis/components/ProgressThread.java b/uk/ac/sanger/artemis/components/ProgressThread.java
new file mode 100644
index 000000000..5a83b08f5
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ProgressThread.java
@@ -0,0 +1,64 @@
+/* ArtemisMain.java
+ *
+ * created: Tue May 11 2004
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2004  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.
+ *
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import javax.swing.*;
+import java.awt.Dimension;
+import java.awt.Color;
+
+public class ProgressThread extends Thread
+{
+  private JFrame frame;
+  private JFrame progress_frame;
+  private String msg;
+  private JProgressBar progressBar = new JProgressBar();
+
+  public ProgressThread(JFrame frame, String msg)
+  {
+    this.frame = frame;
+    this.msg   = msg;
+  }
+
+  public void run()
+  {
+    progress_frame = new JFrame("Loading...");
+    Dimension d = progress_frame.getToolkit().getScreenSize();
+    progressBar.setIndeterminate(true);
+    progressBar.setBackground(Color.white);
+    progress_frame.getContentPane().add(progressBar);
+    progress_frame.pack();
+    progress_frame.setLocation(
+              ((int)d.getWidth()-progress_frame.getWidth())/2,
+              ((int)d.getHeight()-progress_frame.getHeight())/2);
+    progress_frame.setVisible(true);
+  }
+
+  public void finished()
+  {
+    if(progress_frame != null)
+      progress_frame.setVisible(false);
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/QualifierChoice.java b/uk/ac/sanger/artemis/components/QualifierChoice.java
new file mode 100644
index 000000000..4cb6c0aa0
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/QualifierChoice.java
@@ -0,0 +1,181 @@
+/* QualifierChoice.java
+ *
+ * created: Tue Sep  7 1999
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/QualifierChoice.java,v 1.1 2004-06-09 09:47:17 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import uk.ac.sanger.artemis.util.StringVector;
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.InvalidKeyException;
+import uk.ac.sanger.artemis.io.EntryInformation;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+import javax.swing.event.*;
+
+/**
+ *  This is a Choice component that shows only the qualifier names for a given
+ *  key.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: QualifierChoice.java,v 1.1 2004-06-09 09:47:17 tjc Exp $
+ **/
+
+public class QualifierChoice extends JComboBox {
+  /**
+   *  Create a new QualifierChoice component for the given Key with the given
+   *  qualifier as the default.
+   *  @param entry_information The object to get the list of possible
+   *    qualifiers from.
+   *  @param default_qualifier The name of the Qualifier that should be shown
+   *    initially.  If null the first (alphabetically) is selected.
+   **/
+  public QualifierChoice (final EntryInformation entry_information,
+                          final Key key, final String default_qualifier) {
+    this.entry_information = entry_information;
+    this.key = key;
+
+    if (default_qualifier != null &&
+        entry_information.isValidQualifier (key, default_qualifier)) {
+      this.default_qualifier = default_qualifier;
+    } else {
+      this.default_qualifier = null;
+    }
+
+    final int MAX_VISIBLE_ROWS = 30;
+    
+    setMaximumRowCount (MAX_VISIBLE_ROWS);
+
+    setEditable(true);
+
+    update ();
+  }
+
+  /**
+   *  Change the qualifiers shown in this component to be those of the given
+   *  Key.
+   **/
+  public void setKey (final Key key) {
+    if (this.key != key) {
+      this.key = key;
+
+      update ();
+    }
+  }
+
+  /**
+   *  Select the given qualifier_name.
+   **/
+  private void setSelectedQualifierByName (final String qualifier_name) {
+    final int index = indexOf (qualifier_name);
+
+    if (index == -1) {
+      // add the key
+      addItem (qualifier_name);
+      setSelectedItem (qualifier_name);
+    } else {
+      setSelectedIndex (index);
+    }
+  }
+
+  /**
+   *  Return the index in the Choice component of the given qualifier_name.
+   **/
+  private int indexOf (final String qualifier_name) {
+    for (int i = 0 ; i < getItemCount () ; ++i) {
+      if (getItemAt (i).equals (qualifier_name)) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   *  Update the Choice to refect the current Key.
+   **/
+  private void update () {
+    removeAllItems ();
+
+//    final StringVector common_qualifiers = new StringVector ();
+//    final StringVector uncommon_qualifiers = new StringVector ();
+    
+    StringVector qualifier_names =
+      entry_information.getValidQualifierNames (key);
+
+    if (qualifier_names == null) {
+      qualifier_names = new StringVector ("note");
+    }
+
+    if (default_qualifier != null &&
+        !qualifier_names.contains (default_qualifier)) {
+      qualifier_names.add (default_qualifier);
+    }
+
+    qualifier_names.sort ();
+
+//    final StringVector invisible_qualifiers =
+//      Options.getOptions ().getInvisibleQualifiers ();
+
+    for (int i = 0 ; i < qualifier_names.size () ; ++i) {
+      final String qualifier_name = qualifier_names.elementAt (i);
+//      if (!invisible_qualifiers.contains (qualifier_name)) {
+        addItem (qualifier_name);
+//      }
+    }
+
+    if (default_qualifier == null) {
+      if (indexOf ("note") != -1) {
+        setSelectedQualifierByName ("note");
+      } else {
+        if (indexOf ("locus_tag") != -1) {
+          setSelectedQualifierByName ("locus_tag");
+        } else {
+          setSelectedIndex (0);
+        }
+      }
+    } else {
+      setSelectedQualifierByName (default_qualifier);
+    }
+  }
+
+  /**
+   *  The Key that was passed to the constructor.
+   **/
+  private Key key = null;
+
+  /**
+   *  The qualifier name that was passed to the constructor.
+   **/
+  private String default_qualifier = null;
+
+  /**
+   *  The EntryInformation object that was passed to the constructor.
+   **/
+  private EntryInformation entry_information;
+}
+
diff --git a/uk/ac/sanger/artemis/components/QualifierEditor.java b/uk/ac/sanger/artemis/components/QualifierEditor.java
new file mode 100644
index 000000000..2e87f0953
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/QualifierEditor.java
@@ -0,0 +1,332 @@
+/* QualifierEditor.java
+ *
+ * created: Tue Oct 23 2001
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/QualifierEditor.java,v 1.1 2004-06-09 09:47:18 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import uk.ac.sanger.artemis.util.ReadOnlyException;
+import uk.ac.sanger.artemis.io.Qualifier;
+import uk.ac.sanger.artemis.io.QualifierInfo;
+import uk.ac.sanger.artemis.io.QualifierVector;
+import uk.ac.sanger.artemis.io.QualifierParseException;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  This component allows qualifiers to be added to or replaced in several
+ *  features at once.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: QualifierEditor.java,v 1.1 2004-06-09 09:47:18 tjc Exp $
+ **/
+
+public class QualifierEditor extends JFrame {
+  /**
+   *  Create a new QualifierEditor for the given features.
+   **/
+  public QualifierEditor (final FeatureVector features,
+                          final EntryGroup entry_group) {
+    super (getFrameTitle (features));
+
+    this.features = features;
+    this.entry_group = entry_group;
+
+    final Font font = Options.getOptions ().getFont ();
+
+    final Feature first_feature = features.elementAt (0);
+
+    final EntryInformation entry_information =
+      first_feature.getEntry ().getEntryInformation ();
+
+    setFont (font);
+
+    final QualifierChoice qualifier_choice =
+      new QualifierChoice (entry_information, first_feature.getKey (), null);
+
+    final JPanel outer_qualifier_choice_panel = new JPanel ();
+    final JPanel qualifier_choice_panel = new JPanel ();
+    outer_qualifier_choice_panel.setLayout (new BorderLayout ());
+
+    outer_qualifier_choice_panel.add (qualifier_choice_panel, "West");
+
+    final JButton qualifier_button = new JButton ("Insert qualifier:");
+
+    qualifier_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        final String qualifier_name =
+          (String) qualifier_choice.getSelectedItem ();
+        final QualifierInfo qualifier_info =
+          entry_information.getQualifierInfo (qualifier_name);
+        
+        if (qualifier_info == null) {
+          new MessageDialog (QualifierEditor.this, "internal error: no " +
+                             "qualifier info for " + qualifier_name);
+        } else {
+          qualifier_text_area.append ("/" + qualifier_name);
+          
+          switch (qualifier_info.getType ()) {
+          case QualifierInfo.QUOTED_TEXT:
+            qualifier_text_area.append ("=\"\"");
+            break;
+            
+          case QualifierInfo.NO_VALUE:
+          case QualifierInfo.OPTIONAL_QUOTED_TEXT:
+            break;
+            
+          default:
+            qualifier_text_area.append ("=");
+          }
+          
+          qualifier_text_area.append ("\n");
+        }
+      }
+      
+    });
+
+    qualifier_choice_panel.add (qualifier_button);
+
+    qualifier_choice_panel.add (qualifier_choice);
+
+    getContentPane ().add (outer_qualifier_choice_panel, "North");
+
+    qualifier_text_area = new QualifierTextArea ();
+
+    add_button.setFont (getFont ());
+    replace_button.setFont (getFont ());
+    close_button.setFont (getFont ());
+
+    add_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        addOrReplace (false);
+      }
+    });
+
+    replace_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        addOrReplace (true);
+      }
+    });
+
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        dispose ();
+      }
+    });
+
+    button_panel.setFont (getFont ());
+
+    button_panel.add (replace_button);
+    button_panel.add (add_button);
+    button_panel.add (close_button);
+
+    getContentPane ().add (qualifier_text_area, "Center");
+    getContentPane ().add (button_panel, "South");
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        dispose ();
+      }
+    });
+
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+  }
+
+  /**
+   *  Add to or replace the qualifiers in all the features that were passed to
+   *  the constructor with the qualifiers from qualifier_text_area.
+   *  @param replace If false any existing qualifiers of the same name in the
+   *    features are left unchanged.  If true existing qualifiers of the same
+   *    name in the features will be deleted.
+   **/
+  private void addOrReplace (final boolean replace) {
+    try {
+      entry_group.getActionController ().startAction ();
+
+      // this will contain one QualifierVector object for each Feature in the
+      // features vector (in the same order)
+      final Vector qualifier_vector_vector = new Vector ();
+
+      for (int i = 0 ; i < features.size () ; ++i) {
+        final Feature this_feature = features.elementAt (i);
+
+        final Entry this_feature_entry = this_feature.getEntry ();
+
+        if (this_feature_entry == null) {
+          // feature has already been deleted
+          qualifier_vector_vector.addElement (null);
+        } else {
+          final EntryInformation entry_information =
+            this_feature_entry.getEntryInformation ();
+
+          try {
+            final QualifierVector qualifiers =
+              qualifier_text_area.getParsedQualifiers (entry_information);
+
+            qualifier_vector_vector.addElement (qualifiers);
+
+          } catch (QualifierParseException e) {
+            new MessageDialog (this,
+                               "error while parsing: " + e.getMessage ());
+            return;
+          }
+        }
+      }
+
+      if (qualifier_vector_vector.size () != features.size ()) {
+        throw new Error ("Internal error in QualifierEditor.add() - " +
+                         "mismatched array sizes");
+      }
+
+      for (int feature_index = 0 ;
+           feature_index < features.size () ;
+           ++feature_index) {
+        final Feature this_feature = features.elementAt (feature_index);
+
+        if (qualifier_vector_vector.elementAt (feature_index) == null) {
+          continue;
+        }
+
+        final QualifierVector qualifier_vector =
+          (QualifierVector) qualifier_vector_vector.elementAt (feature_index);
+
+        for (int qualifier_index = 0 ;
+             qualifier_index < qualifier_vector.size () ;
+             ++qualifier_index) {
+          final Qualifier this_qualifier =
+            qualifier_vector.elementAt (qualifier_index);
+
+          if (replace) {
+            try {
+              this_feature.setQualifier (this_qualifier);
+            } catch (EntryInformationException e) {
+              new MessageDialog (this,
+                                 "failed to add qualifiers to: " +
+                                 this_feature.getIDString () + ": " +
+                                 e.getMessage ());
+            } catch (ReadOnlyException e) {
+              new MessageDialog (this,
+                                 "failed to add qualifiers to read-only " +
+                                 "feature: " + this_feature.getIDString () +
+                                 ": " + e.getMessage ());
+            }
+          } else {
+            try {
+              this_feature.addQualifierValues (this_qualifier);
+            } catch (EntryInformationException e) {
+              new MessageDialog (this,
+                                 "failed to add qualifiers to: " +
+                                 this_feature.getIDString () + ": " +
+                                 e.getMessage ());
+            } catch (ReadOnlyException e) {
+              new MessageDialog (this,
+                                 "failed to add qualifiers to read-only " +
+                                 "feature: " + this_feature.getIDString () +
+                                 ": " + e.getMessage ());
+            }
+
+          }
+        }
+      }
+    } finally {
+      entry_group.getActionController ().endAction ();
+    }
+  }
+
+  /**
+   *  Return an appropriate String to use for the title of this JFrame.
+   **/
+  static private String getFrameTitle (final FeatureVector features) {
+    boolean etc_flag = false;
+
+    final StringBuffer buffer = new StringBuffer ();
+
+    final int MAX_LENGTH = 80;
+
+    for (int i = 0 ; i < features.size () ; ++i) {
+      final Feature this_feature = features.elementAt (i);
+
+      final String feature_name = this_feature.getIDString ();
+
+      if (feature_name == null) {
+        etc_flag = true;
+        continue;
+      }
+
+      if (buffer.length () + feature_name.length () < MAX_LENGTH) {
+        if (buffer.length () == 0) {
+          buffer.append ("Add or replace qualifiers of: ");
+          buffer.append (feature_name);
+        } else {
+          buffer.append (", ").append (feature_name);
+        }
+      } else {
+        etc_flag = true;
+        break;
+      }
+    }
+
+    if (buffer.length () == 0) {
+      buffer.append ("Add or replace qualifiers");
+    } else {
+      if (etc_flag) {
+        buffer.append (", ...");
+      }
+    }
+
+    return buffer.toString ();
+  }
+
+  private QualifierTextArea qualifier_text_area;
+
+  private JButton add_button = new JButton ("Add");
+  private JButton replace_button = new JButton ("Replace");
+  private JButton close_button = new JButton ("Close");
+
+  private FlowLayout flow_layout =
+    new FlowLayout (FlowLayout.CENTER, 25, 5);
+
+  private JPanel button_panel = new JPanel (flow_layout);
+
+  /**
+   *  The Feature objects that were passed to the constructor.
+   **/
+  private FeatureVector features = new FeatureVector ();
+
+  /**
+   *  The EntryGroup that contains the Features (passed to the constructor).
+   **/
+  private EntryGroup entry_group;
+}
diff --git a/uk/ac/sanger/artemis/components/QualifierTextArea.java b/uk/ac/sanger/artemis/components/QualifierTextArea.java
new file mode 100644
index 000000000..cb9bf1849
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/QualifierTextArea.java
@@ -0,0 +1,96 @@
+/* QualifierTextArea.java
+ *
+ * created: Tue Oct 23 2001
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/QualifierTextArea.java,v 1.1 2004-06-09 09:47:19 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Options;
+
+import uk.ac.sanger.artemis.io.QualifierParseException;
+import uk.ac.sanger.artemis.io.QualifierVector;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.EmblStreamFeature;
+
+import java.awt.*;
+import java.io.*;
+import javax.swing.*;
+
+/**
+ *  This component is a TextArea that understands qualifiers.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: QualifierTextArea.java,v 1.1 2004-06-09 09:47:19 tjc Exp $
+ **/
+
+public class QualifierTextArea extends JTextArea {
+  /**
+   *  Create a new QualifierTextArea containing no text.
+   **/
+  public QualifierTextArea () {
+    super ((Options.getOptions ().getPropertyTruthValue ("alicat_mode") ||
+            Options.getOptions ().getPropertyTruthValue ("val_mode") ?
+            40 :
+            18),
+           81);
+    setLineWrap (true);
+    setBackground (Color.white);
+
+    setDragEnabled(true);
+  }
+
+  /**
+   *  Parse and return the qualifiers in this TextArea in a QualifierVector.
+   **/
+  public QualifierVector
+    getParsedQualifiers (final EntryInformation entry_information)
+      throws QualifierParseException {
+    final String qualifier_string = getText ();
+    return getQualifiersFromString (qualifier_string,
+                                    entry_information);
+  }
+
+  /**
+   *  Return a QualifierVector containing the qualifiers from a String.
+   *  @param qual_string contains the qualifiers to parse
+   */
+  private static QualifierVector
+    getQualifiersFromString (final String qual_string,
+                             final EntryInformation entry_information)
+      throws QualifierParseException {
+
+    try {
+      final StringReader string_reader = new StringReader (qual_string);
+
+      final QualifierVector embl_qualifiers =
+        EmblStreamFeature.readQualifiers (string_reader,
+                                          entry_information);
+
+      string_reader.close ();
+
+      return embl_qualifiers;
+    } catch (IOException exception) {
+      throw (new QualifierParseException (exception.getMessage ()));
+    }
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/RunMenu.java b/uk/ac/sanger/artemis/components/RunMenu.java
new file mode 100644
index 000000000..8be27bef1
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/RunMenu.java
@@ -0,0 +1,278 @@
+/* RunMenu.java
+ *
+ * created: Fri Jan 22 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/RunMenu.java,v 1.1 2004-06-09 09:47:20 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.util.ReadOnlyException;
+import uk.ac.sanger.artemis.util.StringVector;
+import uk.ac.sanger.artemis.io.EntryInformationException;
+import uk.ac.sanger.artemis.io.Qualifier;
+import uk.ac.sanger.artemis.io.InvalidKeyException;
+import uk.ac.sanger.artemis.io.InvalidRelationException;
+
+import java.io.IOException;
+import java.awt.event.*;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  A JMenu of external commands/functions.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: RunMenu.java,v 1.1 2004-06-09 09:47:20 tjc Exp $
+ **/
+
+public class RunMenu extends SelectionMenu 
+{
+  /**
+   *  Create a new RunMenu object.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param menu_name The name of the new menu.
+   **/
+  public RunMenu(final JFrame frame, final Selection selection,
+                 final String menu_name) 
+  {
+    super(frame, menu_name, selection);
+
+    final ExternalProgramVector external_programs =
+      Options.getOptions().getExternalPrograms();
+
+    for(int i = 0 ; i < external_programs.size() ; ++i) 
+      makeMenuItem(external_programs.elementAt(i));
+
+    addSeparator();
+
+    for(int i = 0 ; i < external_programs.size() ; ++i) 
+      makeOptionsMenuItem(external_programs.elementAt(i));
+
+    if(Options.getOptions().getProperty("jcon_min_jobs") != null) 
+    {
+      addSeparator();
+      final JMenuItem jcon_status = new JMenuItem("Show Job Status ...");
+
+      jcon_status.addActionListener(new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          try 
+          {
+            final int ids[] = getIds();
+            TaskViewerFrame tvf = new TaskViewerFrame(ids);
+
+            tvf.setSize(400, 600);
+            tvf.setVisible(true);
+          }
+          catch(Exception e) 
+          {
+            e.printStackTrace();
+            new MessageDialog(frame, "unable to view job status: " + e);
+          }
+        }
+      });
+
+      add(jcon_status);
+    }
+  }
+
+  /**
+   *  Create a new RunMenu object.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   **/
+  public RunMenu(final JFrame frame,
+                 final Selection selection) 
+  {
+    this(frame, selection, "Run");
+  }
+
+  /**
+   *  Make a new menu item for running the given ExternalProgram object.
+   *  @param program Create two menu items for this program.
+   **/
+  private void makeMenuItem(final ExternalProgram program) 
+  {
+    final JMenuItem new_menu;
+
+    if(program.getType() == ExternalProgram.AA_PROGRAM ||
+       program.getType() == ExternalProgram.DNA_PROGRAM &&
+       Options.getOptions().getPropertyTruthValue("sanger_options")) 
+    {
+      final String options_string = program.getProgramOptions();
+
+      if(options_string.length() > 0) 
+        new_menu = new JMenuItem("Run " + program.getName() + " (" +
+                        options_string + ") on selected features");
+      else 
+        new_menu =
+          new JMenuItem("Run " + program.getName() + " on selected features");
+    } 
+    else 
+      new_menu =
+        new JMenuItem("Run " + program.getName() + " on selected features");
+
+    new_menu.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event) 
+      {
+        if(!checkForSelectionFeatures()) 
+          return;
+
+        final FeatureVector selection_features =
+          getSelection().getAllFeatures();
+
+        try
+        {
+          final ExternalProgramMonitor monitor =
+                        program.run(selection_features, Splash.getLogger());
+
+          monitor.addExternalProgramListener(new ExternalProgramListener() 
+          {
+            public void statusChanged(final ExternalProgramEvent e) 
+            {
+              if(e.getType() == ExternalProgramEvent.FINISHED) 
+                new MessageFrame(e.getMessage()).setVisible(true);
+            }
+          });
+          new Thread(monitor).start();
+        } 
+        catch(InvalidKeyException e) 
+        {
+          new MessageDialog(getParentFrame(),
+                             "execution failed: " + e.getMessage());
+        }
+        catch(EntryInformationException e)
+        {
+          new MessageDialog(getParentFrame(),
+                             "execution of " + program.getName() +
+                             " failed because: " + e.getMessage());
+        }
+        catch(ReadOnlyException e)
+        {
+          new MessageDialog(getParentFrame(),
+                             "execution of " + program.getName() +
+                             " failed because one of the features is " +
+                             "read only");
+        }
+        catch(IOException e)
+        {
+          new MessageDialog(getParentFrame(),
+                             "execution of " + program.getName() +
+                             " failed because of an I/O error: " +
+                             e);
+        }
+        catch(ExternalProgramException e) 
+        {
+          new MessageDialog(getParentFrame(),
+                            "execution of " + program.getName() +
+                            " failed: " + e.getMessage());
+        }
+      }
+    });
+
+    add(new_menu);
+  }
+
+  /**
+   *  Make a new options menu item for the given ExternalProgram object.
+   *  @param program Create two menu items for this program.
+   **/
+  private void makeOptionsMenuItem(final ExternalProgram program)
+  {
+    if(!(program.getType() == ExternalProgram.AA_PROGRAM ||
+         program.getType() == ExternalProgram.DNA_PROGRAM)) 
+      return;
+
+    final JMenuItem new_options_menu =
+      new JMenuItem("Set " + program.getName() + " options");
+
+    new_options_menu.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event)
+      {
+        new ExternalProgramOptions(program);
+      }
+    });
+
+    add(new_options_menu);
+  }
+
+  /**
+   *  
+   **/
+  private int[] getIds() 
+  {
+    final FeatureVector selected_features = getSelection().getAllFeatures();
+    final Vector ids_vector = new Vector();
+
+    for(int feature_index = 0; feature_index < selected_features.size();
+        ++feature_index) 
+    {
+      final Feature this_feature = selected_features.elementAt(feature_index);
+
+      try
+      {
+        final Qualifier job_qualifier =
+          this_feature.getQualifierByName("job");
+        
+        final StringVector values = job_qualifier.getValues();
+
+        if(values != null && values.size() > 0) 
+        {
+          for(int value_index=0; value_index<values.size();
+              ++value_index) 
+          {
+            final String job_value = values.elementAt(value_index);
+            final StringVector bits = StringVector.getStrings(job_value);
+
+            if(bits.size() > 4 && bits.elementAt(2).equals("task:")) 
+            {
+              try 
+              {
+                final Integer task_id = Integer.valueOf(bits.elementAt(3));
+
+                if(!ids_vector.contains(task_id)) 
+                  ids_vector.add(task_id);
+              }
+              catch(NumberFormatException e) {}
+            }
+          }
+        }
+      } 
+      catch(InvalidRelationException e) {}
+    }
+
+    final int[] ids = new int[ids_vector.size()];
+
+    for(int i=0 ; i<ids.length ; ++i) 
+      ids[i] =((Integer)ids_vector.elementAt(i)).intValue();
+
+    return ids;
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/ScoreChangeEvent.java b/uk/ac/sanger/artemis/components/ScoreChangeEvent.java
new file mode 100644
index 000000000..0d567b905
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ScoreChangeEvent.java
@@ -0,0 +1,58 @@
+/* ScoreChangeEvent.java
+ *
+ * created: Thu Oct 21 1999
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ScoreChangeEvent.java,v 1.1 2004-06-09 09:47:22 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  The adjustment event emitted when a score changes.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: ScoreChangeEvent.java,v 1.1 2004-06-09 09:47:22 tjc Exp $
+ **/
+
+public class ScoreChangeEvent extends java.util.EventObject {
+  /**
+   *  Create new ScoreChangeEvent.
+   **/
+  public ScoreChangeEvent (final Object source, final int value) {
+    super (source);
+    this.value = value;
+  }
+
+  /**
+   *  Return the new score value that caused this event (as passed to the
+   *  constructor).
+   **/
+  public int getValue () {
+    return value;
+  }
+  
+
+  /**
+   *  The new score value that caused this event (as passed to the
+   *  constructor).
+   **/
+  final int value;
+}
diff --git a/uk/ac/sanger/artemis/components/ScoreChangeListener.java b/uk/ac/sanger/artemis/components/ScoreChangeListener.java
new file mode 100644
index 000000000..0cde0f0da
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ScoreChangeListener.java
@@ -0,0 +1,40 @@
+/* ScoreAdjustmentListener.java
+ *
+ * created: Thu Oct 21 1999
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ScoreChangeListener.java,v 1.1 2004-06-09 09:47:28 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  The listener interface for receiving score change events. 
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: ScoreChangeListener.java,v 1.1 2004-06-09 09:47:28 tjc Exp $
+ **/
+
+public interface ScoreChangeListener {
+  /**
+   *  Invoked when the value of the score has changed.
+   **/
+  void scoreChanged (final ScoreChangeEvent event);
+}
diff --git a/uk/ac/sanger/artemis/components/ScoreChanger.java b/uk/ac/sanger/artemis/components/ScoreChanger.java
new file mode 100644
index 000000000..1375dd314
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ScoreChanger.java
@@ -0,0 +1,222 @@
+/* ScoreChanger.java
+ *
+ * created: Thu Oct 21 1999
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ScoreChanger.java,v 1.1 2004-06-09 09:47:29 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This is a JFrame component that contains ScoreScrollbar components.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: ScoreChanger.java,v 1.1 2004-06-09 09:47:29 tjc Exp $
+ **/
+
+public class ScoreChanger extends JFrame {
+  /**
+   *  Create a new ScoreChanger.
+   *  @param minimum_value The minimum allowable value for the scroll bars.
+   *  @param maximum_value The maximum allowable value for the scroll bars.
+   **/
+  public ScoreChanger (final String name,
+                       final ScoreChangeListener minimum_listener,
+                       final ScoreChangeListener maximum_listener,
+                       final int minimum_value, final int maximum_value)
+      throws IllegalArgumentException {
+
+    super (name);
+
+    this.minimum_listener = minimum_listener;
+    this.maximum_listener = maximum_listener;
+    this.minimum_value = minimum_value;
+    this.maximum_value = maximum_value;
+
+    getContentPane ().setLayout (new GridLayout (5, 1));
+
+    minimum_label = new JLabel ();
+
+    getContentPane ().add (minimum_label);
+
+    minimum_score_scrollbar =
+      new ScoreScrollbar (minimum_value, maximum_value);
+
+    final ScoreChangeListener this_minimum_listener =
+      new ScoreChangeListener () {
+        public void scoreChanged (ScoreChangeEvent event) {
+          minimum_label.setText ("Minimum Cutoff: " + event.getValue ());
+        }
+      };
+
+    minimum_score_scrollbar.addScoreChangeListener (this_minimum_listener);
+
+    getContentPane ().add (minimum_score_scrollbar);
+
+    maximum_label = new JLabel ();
+
+    getContentPane ().add (maximum_label);
+
+    maximum_score_scrollbar =
+      new ScoreScrollbar (minimum_value, maximum_value);
+
+    final ScoreChangeListener this_maximum_listener =
+      new ScoreChangeListener () {
+        public void scoreChanged (ScoreChangeEvent event) {
+          maximum_label.setText ("Maximum Cutoff: " + event.getValue ());
+        }
+      };
+
+    maximum_score_scrollbar.addScoreChangeListener (this_maximum_listener);
+
+    getContentPane ().add (maximum_score_scrollbar);
+
+    final FlowLayout flow_layout =
+      new FlowLayout (FlowLayout.CENTER, 18, 0);
+
+    final JPanel button_panel = new JPanel (flow_layout);
+
+    final JButton reset_button = new JButton ("Reset");
+
+    reset_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        reset ();
+      }
+    });
+
+    button_panel.add (reset_button);
+
+
+    final JButton close_button = new JButton ("Close");
+
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        dispose ();
+      }
+    });
+
+
+    button_panel.add (close_button);
+
+    getContentPane ().add (button_panel);
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        dispose ();
+      }
+    });
+
+    pack ();
+
+    setSize (270, getSize ().height);
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+
+    reset ();
+    
+    minimum_score_scrollbar.addScoreChangeListener (minimum_listener);
+    maximum_score_scrollbar.addScoreChangeListener (maximum_listener);
+  }
+
+  /**
+   *  Call reset (), clean up the listeners then call super.dispose ().
+   **/
+  public void dispose () {
+    reset ();
+
+    minimum_score_scrollbar.removeScoreChangeListener (minimum_listener);
+    maximum_score_scrollbar.removeScoreChangeListener (maximum_listener);
+
+    super.dispose ();
+  }
+
+  /**
+   *  Reset the minimum_score_scrollbar to 0 and the maximum_score_scrollbar
+   *  to 100.
+   **/
+  private void reset () {
+    minimum_score_scrollbar.setValue (minimum_value);
+    maximum_score_scrollbar.setValue (maximum_value);
+
+    minimum_label.setText ("Minimum Cutoff: " + minimum_value);
+    maximum_label.setText ("Maximum Cutoff: " + maximum_value);
+
+    final ScoreChangeEvent minimum_event =
+      new ScoreChangeEvent (this, minimum_value);
+
+    minimum_listener.scoreChanged (minimum_event);
+
+    final ScoreChangeEvent maximum_event =
+      new ScoreChangeEvent (this, maximum_value);
+
+    maximum_listener.scoreChanged (maximum_event);
+  }
+
+  /**
+   *  The ScoreChangeListener for the minimum score that was passed to the
+   *  constructor.
+   **/
+  final ScoreChangeListener minimum_listener;
+
+  /**
+   *  The minimum score scrollbar the created by the constructor.
+   **/
+  final ScoreScrollbar minimum_score_scrollbar;
+
+  /**
+   *  The ScoreChangeListener for the maximum score that was passed to the
+   *  constructor.
+   **/
+  final ScoreChangeListener maximum_listener;
+
+  /**
+   *  The maximum score scrollbar the created by the constructor.
+   **/
+  final ScoreScrollbar maximum_score_scrollbar;
+
+  /**
+   *  The minimum allowable value for the scroll bars.
+   **/
+  final int minimum_value;
+
+  /**
+   *  The maximum allowable value for the scroll bars.
+   **/
+  final int maximum_value;
+
+  /**
+   *  A Label that shows something like this: "Minimum Cutoff: 0"
+   **/
+  final JLabel minimum_label;
+
+  /**
+   *  A Label that shows something like this: "Maximum Cutoff: 100"
+   **/
+  final JLabel maximum_label;
+}
+
diff --git a/uk/ac/sanger/artemis/components/ScoreScrollbar.java b/uk/ac/sanger/artemis/components/ScoreScrollbar.java
new file mode 100644
index 000000000..d234bb8cb
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ScoreScrollbar.java
@@ -0,0 +1,120 @@
+/* ScoreScrollbar.java
+ *
+ * created: Thu Oct 21 1999
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 1999  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ScoreScrollbar.java,v 1.1 2004-06-09 09:47:31 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This component is a Scrollbar that generates ScoreChange events when the
+ *  value changes.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: ScoreScrollbar.java,v 1.1 2004-06-09 09:47:31 tjc Exp $
+ **/
+
+public class ScoreScrollbar extends JScrollBar
+  implements AdjustmentListener {
+  /**
+   *  Constructs a new horizontal score scroll bar with an initial value of 0.
+   *  @param minimum_value The minimum allowable value for the scroll bar.
+   *  @param maximum_value The maximum allowable value for the scroll bar.
+   **/
+  public ScoreScrollbar (final int minimum_value, final int maximum_value) {
+    this (Scrollbar.HORIZONTAL, minimum_value, minimum_value, maximum_value);
+  }
+
+  /**
+   *  Constructs a new score scroll bar with the specified orientation.
+   *
+   *  The orientation argument must take one of the two values
+   *  java.awt.Scrollbar.HORIZONTAL, or java.awt.Scrollbar.VERTICAL,
+   *  indicating a horizontal or vertical scroll bar, respectively.
+   *  @param orientation indicates the orientation of the scroll bar.
+   *  @param value The initial value of the scrollbar.
+   *  @param minimum_value The minimum allowable value for the scroll bar.
+   *  @param maximum_value The maximum allowable value for the scroll bar.
+   *  @exception IllegalArgumentException when an illegal value for the
+   *    orientation argument is supplied or if the value parameter is less
+   *    than minimum_value or greater than maximum_value.
+   **/
+  public ScoreScrollbar (final int orientation, final int value,
+                         final int minimum_value, final int maximum_value)
+      throws IllegalArgumentException {
+    super (orientation,
+           (value < minimum_value || value > maximum_value ?
+            minimum_value :
+            value),
+           1, minimum_value, maximum_value + 1);
+  }
+
+  /**
+   *  Add the given ScoreChangeListener as a listener for ScoreChange events
+   *  from this components.
+   **/
+  public void addScoreChangeListener (final ScoreChangeListener l) {
+    score_change_listeners.addElement (l);
+
+    if (score_change_listeners.size () == 1) {
+      addAdjustmentListener (this);
+    }
+  }
+
+  /**
+   *  Remove the given ScoreChangeListener as a listener for ScoreChange
+   *  events from this components.
+   **/
+  public void removeScoreChangeListener (final ScoreChangeListener l) {
+    score_change_listeners.addElement (l);
+
+    if (score_change_listeners.size () == 0) {
+      removeAdjustmentListener (this);
+    }
+  }
+
+  /**
+   *  Implementation of the AdjustmentListener interface.
+   **/
+  public void adjustmentValueChanged (AdjustmentEvent e) {
+    for (int i = 0 ; i < score_change_listeners.size () ; ++i) {
+      final ScoreChangeEvent event =
+        new ScoreChangeEvent (this, getValue ());
+      final ScoreChangeListener this_listener =
+        (ScoreChangeListener)(score_change_listeners.elementAt (i));
+      this_listener.scoreChanged (event);
+    }
+  }
+
+  /**
+   *  The ScoreChangeListener objects that have been added with
+   *  addScoreChangeListener ().
+   **/
+  private final java.util.Vector score_change_listeners =
+    new java.util.Vector ();
+}
+
diff --git a/uk/ac/sanger/artemis/components/SearchResultViewer.java b/uk/ac/sanger/artemis/components/SearchResultViewer.java
new file mode 100644
index 000000000..17228e217
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/SearchResultViewer.java
@@ -0,0 +1,108 @@
+/* SearchResultViewer.java
+ *
+ * created: Thu Feb 24 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/SearchResultViewer.java,v 1.1 2004-06-09 09:47:34 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+
+import uk.ac.sanger.artemis.util.*;
+
+import java.io.*;
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A component that displays the results of external searches, with the
+ *  ability to send the results to a netscape process.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: SearchResultViewer.java,v 1.1 2004-06-09 09:47:34 tjc Exp $
+ **/
+
+public class SearchResultViewer extends FileViewer {
+  /**
+   *  Create a new SearchResultViewer component.
+   *  @param title The name to attach to the new JFrame.
+   *  @param file_name The file to read into the new viewer.
+   **/
+  public SearchResultViewer (final String label,
+                             final Document document)
+      throws IOException {
+
+    super (label);
+
+    try {
+      readFile (document.getReader ());
+    } catch (IOException e) {
+      System.out.println ("error while reading results: " + e);
+      dispose ();
+      throw e;
+    }
+
+    if (!Options.getOptions ().getPropertyTruthValue ("sanger_options")) {
+      return;
+    }
+
+    final JButton to_browser = new JButton ("Send to browser");
+
+    getButtonPanel ().add (to_browser);
+
+    to_browser.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        try {
+          sendToBrowser (document.toString ());
+        } catch (IOException e) {
+          System.out.println ("error while reading results: " + e);
+          new MessageDialog (SearchResultViewer.this,
+                             "Message",
+                             "Send to browser failed: " + e);
+        } catch (ExternalProgramException e) {
+          System.out.println ("error while reading results: " + e);
+          new MessageDialog (SearchResultViewer.this,
+                             "Message",
+                             "Send to browser failed: " + e);
+        }
+      }
+    });
+  }
+
+  /**
+   *  Mark up the contents of the given file (which should contain blast or
+   *  fasta output) and send it to a web browser (with netscape -remote).
+   **/
+  public static void sendToBrowser (final String file_name)
+      throws IOException, ExternalProgramException {
+    final String [] arguments = {
+      file_name
+    };
+
+    final Process process =
+      ExternalProgram.startProgram ("../etc/results_to_netscape", arguments);
+
+    new ProcessWatcher (process, "results_to_netscape", false);
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/SelectMenu.java b/uk/ac/sanger/artemis/components/SelectMenu.java
new file mode 100644
index 000000000..371c54185
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/SelectMenu.java
@@ -0,0 +1,827 @@
+/* SelectMenu.java
+ *
+ * created: Thu Jan 14 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/SelectMenu.java,v 1.1 2004-06-09 09:47:37 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+import uk.ac.sanger.artemis.util.StringVector;
+import uk.ac.sanger.artemis.io.Range;
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.KeyVector;
+import uk.ac.sanger.artemis.io.Qualifier;
+import uk.ac.sanger.artemis.io.InvalidKeyException;
+import uk.ac.sanger.artemis.io.InvalidRelationException;
+import uk.ac.sanger.artemis.io.EntryInformation;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This menu has contains items such a "Select all", "Select none" and
+ *  "Select by key".
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: SelectMenu.java,v 1.1 2004-06-09 09:47:37 tjc Exp $
+ **/
+
+public class SelectMenu extends SelectionMenu {
+  /**
+   *  Create a new SelectMenu object and all it's menu items.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @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 features and exons are
+   *    selected from.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   *  @param menu_name The name of the new menu.
+   **/
+  public SelectMenu (final JFrame frame,
+                     final Selection selection,
+                     final GotoEventSource goto_event_source,
+                     final EntryGroup entry_group,
+                     final BasePlotGroup base_plot_group,
+                     final String menu_name) {
+    super (frame, menu_name, selection);
+
+    this.entry_group = entry_group;
+    this.goto_event_source = goto_event_source;
+
+    selector_item = new JMenuItem ("Feature Selector ...");
+    selector_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        new Selector (selection, goto_event_source, getEntryGroup (),
+                      base_plot_group);
+      }
+    });
+
+    add (selector_item);
+
+    addSeparator ();
+
+    all_item = new JMenuItem ("All");
+    all_item.setAccelerator (SELECT_ALL_KEY);
+    all_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        selectAll ();
+      }
+    });
+
+    add (all_item);
+
+    all_bases_item = new JMenuItem ("All Bases");
+    all_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        selectAllBases ();
+      }
+    });
+
+    add (all_bases_item);
+
+    none_item = new JMenuItem ("None");
+    none_item.setAccelerator (SELECT_NONE_KEY);
+    none_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        clearSelection ();
+      }
+    });
+
+    add (none_item);
+
+    select_by_key_item = new JMenuItem ("By Key");
+    select_by_key_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        selectByKeyPopup ();
+      }
+    });
+
+    add (select_by_key_item);
+
+    select_non_pseudo_cds_item =
+      new JMenuItem ("CDS Features without /pseudo");
+    select_non_pseudo_cds_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        // select all CDS features that do not have the /pseudo qualifier
+        final FeatureKeyQualifierPredicate predicate =
+          new FeatureKeyQualifierPredicate (Key.CDS, "pseudo", false);
+
+        clearSelection ();
+
+        selectFeaturesByPredicate (predicate);
+      }
+    });
+
+    add (select_non_pseudo_cds_item);
+
+    select_all_cds_item = new JMenuItem ("All CDS Features");
+    select_all_cds_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        final FeatureKeyPredicate predicate =
+          new FeatureKeyPredicate (Key.CDS);
+
+        clearSelection ();
+
+        selectFeaturesByPredicate (predicate);
+      }
+    });
+
+    add (select_all_cds_item);
+
+    select_same_type_item = new JMenuItem ("Same Key");
+    select_same_type_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        selectSameKey ();
+      }
+    });
+
+    add (select_same_type_item);
+
+    select_matching_qualifiers_item =
+      new JMenuItem ("Features Matching Qualifier");
+    select_matching_qualifiers_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        selectMatchingQualifiers ();
+      }
+    });
+
+    add (select_matching_qualifiers_item);
+
+    select_orf_item = new JMenuItem ("Open Reading Frame");
+    select_orf_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        selectOrf ();
+      }
+    });
+
+    add (select_orf_item);
+
+    select_features_in_range_item =
+      new JMenuItem ("Features Overlapping Selection");
+    select_features_in_range_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        selectOverlappingFeatures ();
+      }
+    });
+
+    add (select_features_in_range_item);
+
+    select_base_range_item = new JMenuItem ("Base Range ...");
+    select_base_range_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        selectBaseRange ();
+      }
+    });
+
+    add (select_base_range_item);
+
+    select_aa_range_in_feature_item = new JMenuItem ("Feature AA Range ...");
+    select_aa_range_in_feature_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        selectFeatureAARange ();
+      }
+    });
+
+    add (select_aa_range_in_feature_item);
+
+    addSeparator ();
+
+    selection_toggle_item = new JMenuItem ("Toggle Selection");
+    selection_toggle_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        toggleSelection ();
+      }
+    });
+
+    add (selection_toggle_item);
+  }
+
+  /**
+   *  The shortcut for Select All
+   **/
+  final static KeyStroke SELECT_ALL_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_A, InputEvent.CTRL_MASK);
+
+  /**
+   *  The shortcut for Select None
+   **/
+  final static KeyStroke SELECT_NONE_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_N, InputEvent.CTRL_MASK);
+
+  /**
+   *  Create a new SelectMenu object and all it's menu items.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @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 features and exons are
+   *    selected from.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public SelectMenu (final JFrame frame,
+                     final Selection selection,
+                     final GotoEventSource goto_event_source,
+                     final EntryGroup entry_group,
+                     final BasePlotGroup base_plot_group) {
+    this (frame, selection, goto_event_source, entry_group,
+          base_plot_group, "Select");
+  }
+
+  /**
+   *  Warn if the user tries to select more than this number of features.
+   **/
+  final private int MAX_FEATURE_TO_SELECT_COUNT = 10000;
+
+  /**
+   *  Select all the features in the active entries.  If there are more than
+   *  1000 features the user is asked for confirmation.
+   **/
+  private void selectAll () {
+    if (getEntryGroup ().getAllFeaturesCount () >
+        MAX_FEATURE_TO_SELECT_COUNT) {
+      final YesNoDialog dialog =
+        new YesNoDialog (getParentFrame (),
+                         "Are you sure you want to select " +
+                          getEntryGroup ().getAllFeaturesCount () +
+                         " features?");
+
+      if (!dialog.getResult ()) {
+        return;
+      }
+    }
+
+    final FeatureVector all_features = getEntryGroup ().getAllFeatures ();
+
+    if (getEntryGroup () instanceof SimpleEntryGroup) {
+      // special case for speed
+      getSelection ().set (all_features);
+    } else {
+      clearSelection ();
+
+      getSelection ().add (all_features);
+    }
+  }
+
+  /**
+   *  Select all the bases.
+   **/
+  private void selectAllBases () {
+    try {
+      final Strand strand = getEntryGroup ().getBases ().getForwardStrand ();
+
+      final MarkerRange new_range =
+        strand.makeMarkerRangeFromPositions (1, strand.getSequenceLength ());
+
+      getSelection ().setMarkerRange (new_range);
+    } catch (OutOfRangeException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+  }
+
+  /**
+   *  Remove the all Features and FeatureSegments in the EntryGroup from the
+   *  Selection.  If the current EntryGroup is a SimpleEntryGroup then this
+   *  method just calls getSelection ().clear ();
+   **/
+  private void clearSelection () {
+    if (getEntryGroup () instanceof SimpleEntryGroup) {
+      // special case for speed
+      getSelection ().clear ();
+    } else {
+      getSelection ().setMarkerRange (null);
+
+      final FeatureEnumeration test_enumerator = getEntryGroup ().features ();
+
+      while (test_enumerator.hasMoreFeatures ()) {
+        final Feature this_feature = test_enumerator.nextFeature ();
+
+        getSelection ().remove (this_feature);
+        getSelection ().removeSegmentsOf (this_feature);
+      }
+    }
+  }
+
+  /**
+   *  Invert the selection - after calling this method the selection will
+   *  contain only those features that were not in the selection before the
+   *  call.
+   **/
+  private void toggleSelection () {
+    final FeatureVector selected_features = getSelection ().getAllFeatures ();
+
+    final FeatureVector all_features = getEntryGroup ().getAllFeatures ();
+
+    final FeatureVector new_selection = new FeatureVector ();
+
+    for (int i = 0 ; i < all_features.size () ; ++i) {
+      final Feature this_feature = all_features.elementAt (i);
+
+      if (!selected_features.contains (this_feature)) {
+        new_selection.add (this_feature);
+      }
+    }
+
+    clearSelection ();
+
+    getSelection ().add (new_selection);
+  }
+
+
+  /**
+   *  Popup a TextRequester component and then select all the features that
+   *  have the same key as the user types.
+   **/
+  private void selectByKeyPopup () {
+    final KeyChooser key_chooser;
+
+    final EntryInformation default_entry_information =
+      Options.getArtemisEntryInformation ();
+
+    key_chooser = new KeyChooser (default_entry_information,
+                                  new Key ("misc_feature"));
+
+    key_chooser.getKeyChoice ().addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent _) {
+        selectByKey (key_chooser.getKeyChoice ().getSelectedItem ());
+        key_chooser.setVisible (false);
+        key_chooser.dispose ();
+      }
+    });
+
+    key_chooser.getOKButton ().addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        selectByKey (key_chooser.getKeyChoice ().getSelectedItem ());
+        key_chooser.setVisible (false);
+        key_chooser.dispose ();
+      }
+    });
+
+    key_chooser.setVisible (true);
+  }
+
+  /**
+   *  Select all the features that have the given key.
+   **/
+  private void selectByKey (final Key key) {
+    final FeaturePredicate predicate = new FeatureKeyPredicate (key);
+
+    selectFeaturesByPredicate (predicate);
+  }
+
+  /**
+   *  Select all the features that have the given key and contains a qualifier
+   *  with the given name and value.
+   *  If key is null select any feature with the contains a qualifier with the
+   *  given name and value.
+   **/
+  private void selectByQualifier (final Key key,
+                                  final String name, final String value) {
+    final FeaturePredicate predicate =
+      new FeatureKeyQualifierPredicate (key, name, value);
+
+    selectFeaturesByPredicate (predicate);
+  }
+
+  /**
+   *  Select all the features that have the same key as the currently selected
+   *  features.
+   **/
+  private void selectSameKey () {
+    if (! checkForSelectionFeatures ()) {
+      return;
+    }
+
+    final KeyVector seen_keys = new KeyVector ();
+
+    final FeatureVector selected_features = getSelection ().getAllFeatures ();
+
+    for (int i = 0 ; i < selected_features.size () ; ++i) {
+      final Feature current_feature = selected_features.elementAt (i);
+
+      if (!seen_keys.contains (current_feature.getKey ())) {
+        seen_keys.add (current_feature.getKey ());
+      }
+    }
+
+    clearSelection ();
+
+    for (int i = 0 ; i < seen_keys.size () ; ++i) {
+      selectByKey (seen_keys.elementAt (i));
+    }
+  }
+
+  /**
+   *  Ask the user for a qualifier name, list all of the qualifier with that
+   *  name in the currently selected feature then select all features that
+   *  have that name and qualifier.
+   **/
+  private void selectMatchingQualifiers () {
+    if (!checkForSelectionFeatures (getParentFrame (), getSelection ())) {
+      return;
+    }
+
+    final FeatureVector selected_features = getSelection ().getAllFeatures ();
+
+    if (selected_features.size () > 1) {
+      new MessageDialog (getParentFrame (), "select only one feature");
+      return;
+    }
+
+    final Feature selected_feature = selected_features.elementAt (0);
+
+    final StringVector qualifier_names =
+      Feature.getAllQualifierNames (selected_features);
+
+    if (qualifier_names.size () == 0) {
+      new MessageDialog (getParentFrame (), "feature has no qualifiers");
+      return;
+    }
+
+    final ChoiceFrame name_choice_frame =
+      new ChoiceFrame ("Select a qualifier name ...", qualifier_names);
+
+    final JComboBox name_choice = name_choice_frame.getChoice ();
+
+    class SelectListener implements ActionListener, ItemListener {
+      public void doStuff () {
+        selectMatchingQualifiersHelper (selected_feature,
+                                        (String) name_choice.getSelectedItem ());
+        name_choice_frame.setVisible (false);
+        name_choice_frame.dispose ();
+      }
+
+      public void itemStateChanged (ItemEvent _) {
+        doStuff ();
+        // need to remove() because itemStateChanged() is called twice on the
+        // Tru64 1.4.1 JVM
+        name_choice.removeItemListener (this);
+      }
+
+      public void actionPerformed (ActionEvent _) {
+        doStuff ();
+        name_choice_frame.getOKButton ().removeActionListener (this);
+      }
+    };
+
+    final SelectListener listener = new SelectListener ();
+
+    name_choice.addItemListener (listener);
+    name_choice_frame.getOKButton ().addActionListener (listener);
+
+    name_choice_frame.setVisible (true);
+  }
+
+  /**
+   *  Pop up a list of the values of the qualifiers with the given name in the
+   *  given feature then select all features that have a qualifier with the
+   *  same and value.
+   **/
+  private void selectMatchingQualifiersHelper (final Feature feature,
+                                               final String name) {
+    final Qualifier qualifier;
+
+    try {
+      qualifier = feature.getQualifierByName (name);
+    } catch (InvalidRelationException e) {
+      throw new Error ("internal error - unexpected exception: " + e);
+    }
+
+    if (qualifier == null) {
+      throw new Error ("internal error - unexpected null reference");
+    }
+
+    final StringVector qualifier_values = qualifier.getValues ();
+
+    if (qualifier_values.size () == 0) {
+      new MessageDialog (getParentFrame (),
+                         "qualifier " + name + " has no values in ");
+      return;
+    }
+
+    if (qualifier_values.size () == 1) {
+      selectByQualifier (null, name, qualifier_values.elementAt (0));
+    } else {
+      final ChoiceFrame value_choice_frame =
+        new ChoiceFrame ("Select a qualifier value", qualifier_values);
+
+      final JComboBox value_choice = value_choice_frame.getChoice ();
+
+      value_choice.addItemListener (new ItemListener () {
+        public void itemStateChanged (ItemEvent _) {
+          selectByQualifier (null, name,
+                             (String) value_choice.getSelectedItem ());
+          value_choice_frame.setVisible (false);
+          value_choice_frame.dispose ();
+        }
+      });
+
+      value_choice_frame.getOKButton ().addActionListener (new ActionListener () {
+        public void actionPerformed (ActionEvent _) {
+          selectByQualifier (null, name,
+                             (String) value_choice.getSelectedItem ());
+          value_choice_frame.setVisible (false);
+          value_choice_frame.dispose ();
+        }
+      });
+
+      value_choice_frame.setVisible (true);
+    }    
+  }
+
+  /**
+   *  Select all the features that match the given predicate.
+   **/
+  private void selectFeaturesByPredicate (final FeaturePredicate predicate) {
+    final FeatureVector new_selection_features = new FeatureVector ();
+
+    final FeatureEnumeration feature_enum = getEntryGroup ().features ();
+
+    while (feature_enum.hasMoreFeatures ()) {
+      final Feature current_feature = feature_enum.nextFeature ();
+
+      if (predicate.testPredicate (current_feature)) {
+        new_selection_features.add (current_feature);
+      }
+    }
+
+    clearSelection ();
+
+    getSelection ().add (new_selection_features);
+  }
+
+  /**
+   *  If a range of bases is selected, then select the ORF around those
+   *  bases.
+   **/
+  private void selectOrf () {
+    if (!checkForSelectionRange ()) {
+      return;
+    }
+
+    final MarkerRange range = getSelection ().getMarkerRange ();
+
+    final Strand strand = getEntryGroup ().getBases ().getForwardStrand ();
+
+    final MarkerRange start_orf_range =
+      strand.getORFAroundMarker (range.getStart (), true);
+
+    /*final*/ Marker end_marker;
+
+    // get the marker of the first base of the last codon in the range (we
+    // want to keep in the same frame)
+    try {
+      final int start_end_diff = range.getCount () - 1;
+
+      final int mod_length = start_end_diff - start_end_diff % 3;
+
+      end_marker = range.getStart ().moveBy (mod_length);
+    } catch (OutOfRangeException e) {
+      end_marker = range.getStart ();
+    }
+
+    final MarkerRange end_orf_range =
+      strand.getORFAroundMarker (end_marker, true);
+
+    MarkerRange new_range = range;
+
+    if (start_orf_range != null) {
+      new_range = new_range.extendRange (start_orf_range);
+    }
+
+    if (end_orf_range != null) {
+      new_range = new_range.extendRange (end_orf_range);
+    }
+
+    if (start_orf_range == null && end_orf_range == null &&
+        range.getCount () <= 6) {   // 6 == two codons
+      new MessageDialog (getParentFrame (),
+                         "there is no open reading frame at the selected " +
+                         "bases");
+      return;
+    }
+
+    getSelection ().setMarkerRange (new_range);
+  }
+
+  /**
+   *  This method will ask the user for a range of bases (such as 100..200),
+   *  using a MarkerRangeRequester component, then selects that range.
+   **/
+  private void selectBaseRange () {
+    final MarkerRangeRequester range_requester =
+      new MarkerRangeRequester ("enter a range of bases (eg. 100..200):",
+                                18, "");
+
+    final MarkerRangeRequesterListener listener =
+      new MarkerRangeRequesterListener () {
+        public void actionPerformed (final MarkerRangeRequesterEvent event) {
+          final MarkerRange marker_range =
+            event.getMarkerRange (getEntryGroup ().getBases ());
+
+          getSelection ().setMarkerRange (marker_range);
+
+          makeBaseVisible (getSelection ().getStartBaseOfSelection ());
+        }
+      };
+
+    range_requester.addMarkerRangeRequesterListener (listener);
+
+    range_requester.setVisible (true);
+  }
+
+  /**
+   *  This method will ask the user for a range of amino acids in a feature
+   *  (such as 100..200), using a Requester component, then selects that
+   *  range on the sequence.
+   **/
+  private void selectFeatureAARange () {
+    if (!checkForSelectionFeatures (getParentFrame (), getSelection ())) {
+      return;
+    }
+
+    final FeatureVector selected_features = getSelection ().getAllFeatures ();
+
+    if (selected_features.size () > 1) {
+      new MessageDialog (getParentFrame (), "select only one feature");
+      return;
+    }
+
+    final Feature selected_feature = selected_features.elementAt (0);
+
+    final MarkerRangeRequester range_requester =
+      new MarkerRangeRequester ("enter a range of amino acids (eg. 100..200):",
+                                18, "");
+
+    final MarkerRangeRequesterListener listener =
+      new MarkerRangeRequesterListener () {
+        public void actionPerformed (final MarkerRangeRequesterEvent event) {
+          final Range range = event.getRawRange ();
+
+          if (range == null) {
+            return;
+          }
+
+          final int start_position = range.getStart ();
+          final int end_position = range.getEnd ();
+
+          final int start_pos_in_feature = (start_position - 1) * 3 + 1;
+          final int end_pos_in_feature = (end_position - 1) * 3 + 3;
+
+          try {
+            final Marker start_marker =
+              selected_feature.getPositionInSequence (start_pos_in_feature);
+
+            final Marker end_marker =
+              selected_feature.getPositionInSequence (end_pos_in_feature);
+
+            final MarkerRange marker_range =
+              new MarkerRange (start_marker.getStrand (),
+                               start_marker.getPosition (),
+                               end_marker.getPosition ());
+
+            getSelection ().setMarkerRange (marker_range);
+
+            makeBaseVisible (getSelection ().getStartBaseOfSelection ());
+          } catch (OutOfRangeException e) {
+            new MessageDialog (getParentFrame (),
+                               "amino acid range is out of range " +
+                               "for this feature");
+          }
+        }
+      };
+
+    range_requester.addMarkerRangeRequesterListener (listener);
+
+    range_requester.setVisible (true);
+  }
+
+  /**
+   *  If there are some selected bases, select the feature in that range
+   *  (replacing the orginal selection).  If some features are selected,
+   *  select those features that overlap the selected features (and unselect
+   *  the original features).
+   **/
+  private void selectOverlappingFeatures () {
+    final MarkerRange selected_marker_range =
+      getSelection ().getMarkerRange ();
+
+    if (selected_marker_range == null) {
+      final FeatureVector selected_features =
+        getSelection ().getAllFeatures ();
+
+      if (selected_features.size () == 0) {
+        new MessageDialog (getParentFrame (), "nothing selected");
+        return;
+      } else {
+        final FeatureVector new_selection = new FeatureVector ();
+
+        for (int i = 0 ; i < selected_features.size () ; ++i) {
+          final Feature this_feature = selected_features.elementAt (i);
+
+          final Range this_feature_raw_range = this_feature.getMaxRawRange ();
+
+          try {
+            final FeatureVector overlapping_features =
+              getEntryGroup ().getFeaturesInRange (this_feature_raw_range);
+
+            for (int overlapping_feature_index = 0 ;
+                 overlapping_feature_index < overlapping_features.size () ;
+                 ++overlapping_feature_index) {
+              final Feature overlapping_feature =
+                overlapping_features.elementAt (overlapping_feature_index);
+
+              if (!new_selection.contains (overlapping_feature)) {
+                new_selection.add (overlapping_feature);
+              }
+            }
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          }
+        }
+
+        for (int i = 0 ; i < selected_features.size () ; ++i) {
+          new_selection.remove (selected_features.elementAt (i));
+        }
+
+        getSelection ().set (new_selection);
+      }
+    } else {
+      try {
+        final Range raw_range = selected_marker_range.getRawRange ();
+        getSelection ().set (getEntryGroup ().getFeaturesInRange (raw_range));
+      } catch (OutOfRangeException e) {
+        throw new Error ("internal error - unexpected exception: " + e);
+      }
+    }
+  }
+
+  /**
+   *  This method sends an GotoEvent to all the GotoEvent listeners that will
+   *  make the given base visible.
+   **/
+  private void makeBaseVisible (final Marker base_marker) {
+    goto_event_source.gotoBase (base_marker);
+  }
+
+  /**
+   *  Return the EntryGroup that was passed to the constructor.
+   **/
+  private EntryGroup getEntryGroup () {
+    return entry_group;
+  }
+
+  /**
+   *  The EntryGroup object that was passed to the constructor.
+   **/
+  private EntryGroup entry_group;
+
+  /**
+   *  The GotoEventSource object that was passed to the constructor.
+   **/
+  private GotoEventSource goto_event_source;
+
+  private JMenuItem selector_item = null;
+  private JMenuItem all_item = null;
+  private JMenuItem all_bases_item = null;
+  private JMenuItem none_item = null;
+  private JMenuItem selection_toggle_item = null;
+  private JMenuItem select_same_type_item = null;
+  private JMenuItem select_matching_qualifiers_item = null;
+  private JMenuItem select_features_in_range_item = null;
+  private JMenuItem select_base_range_item = null;
+  private JMenuItem select_aa_range_in_feature_item = null;
+  private JMenuItem select_orf_item = null;
+  private JMenuItem select_by_key_item = null;
+  private JMenuItem select_non_pseudo_cds_item = null;
+  private JMenuItem select_all_cds_item = null;
+}
diff --git a/uk/ac/sanger/artemis/components/SelectionInfoDisplay.java b/uk/ac/sanger/artemis/components/SelectionInfoDisplay.java
new file mode 100644
index 000000000..d21cde661
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/SelectionInfoDisplay.java
@@ -0,0 +1,435 @@
+/* SelectionInfoDisplay.java
+ *
+ * created: Tue Dec 15 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1998,1999,2000,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/SelectionInfoDisplay.java,v 1.1 2004-06-09 09:47:40 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.io.StreamQualifier;
+import uk.ac.sanger.artemis.io.QualifierInfo;
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.io.Qualifier;
+import uk.ac.sanger.artemis.io.QualifierVector;
+import uk.ac.sanger.artemis.io.Range;
+import uk.ac.sanger.artemis.util.OutOfRangeException;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  This class displays information about the selection in a Label.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: SelectionInfoDisplay.java,v 1.1 2004-06-09 09:47:40 tjc Exp $
+ **/
+public class SelectionInfoDisplay extends CanvasPanel
+    implements SelectionChangeListener {
+  /**
+   *  Create a new SelectionInfoDisplay component.
+   *  @param entry_group The EntryGroup that this component will display.
+   *  @param selection The Selection that this component will display.
+   **/
+  public SelectionInfoDisplay (final EntryGroup entry_group,
+                               final Selection selection) {
+    this.entry_group = entry_group;
+    this.selection = selection;
+
+    getSelection ().addSelectionChangeListener (this);
+
+    getCanvas ().addComponentListener (new ComponentAdapter () {
+      public void componentResized(ComponentEvent e) {
+        getCanvas ().setSize (getCanvas ().getSize ().width,
+                              getFontHeight () + 1);
+        getCanvas ().repaint ();
+      }
+      public void componentShown (ComponentEvent e) {
+        getCanvas ().setSize (getCanvas ().getSize ().width,
+                              getFontHeight () + 1);
+        getCanvas ().repaint ();
+      }
+    });
+
+    int canvas_width = getCanvas ().getSize ().width;
+
+    setBackground (Color.white);
+
+    setSize (100, getFontHeight ());
+  }
+
+  /**
+   *
+   **/
+  public Dimension getPreferredSize () {
+    return new Dimension (10, getFontHeight ());
+  }
+
+  /**
+   *
+   **/
+  public Dimension getMinimumSize () {
+    return new Dimension (10, getFontHeight ());
+  }
+
+  /**
+   *  Implementation of the SelectionChangeListener interface.  We listen to
+   *  SelectionChange events so that we can update the list to reflect the
+   *  current selection.
+   **/
+  public void selectionChanged (SelectionChangeEvent event) {
+    repaintCanvas ();
+  }
+
+  /**
+   *  Draw the label.
+   **/
+  public void paintCanvas (final Graphics g) {
+    if (!isVisible ()) {
+      return;
+    }
+
+    final FeatureVector features = getSelection ().getAllFeatures ();
+
+    final StringBuffer new_text = new StringBuffer ();
+
+    new_text.append (markerRangeText (getSelection (), entry_group));
+
+    int base_total = 0;
+    int aa_total = 0;
+    boolean saw_a_non_cds = false;
+
+    if (features.size () > 0) {
+      final StringBuffer feature_names = new StringBuffer ();
+
+      if (features.size () < 100) {
+        if (features.size () > 1) {
+          feature_names.append ("  (");
+        }
+
+        // show up to 10 features
+        for (int i = 0 ; i < features.size () ; ++i) {
+          final Feature current_feature = features.elementAt (i);
+
+          base_total += current_feature.getBaseCount ();
+
+          aa_total += current_feature.getAACount ();
+
+          if (!current_feature.getKey ().equals ("CDS")) {
+            saw_a_non_cds = true;
+          }
+
+          if (i < 10) {
+            if (i != 0) {
+              feature_names.append (' ');
+            }
+            feature_names.append (current_feature.getIDString ());
+          }
+        }
+
+        if (features.size () > 10) {
+          feature_names.append ("...");
+        }
+
+        if (features.size () == 1) {
+          // only one feature so append some qualifiers
+          feature_names.append ("  (");
+          feature_names.append (getQualifierString (features.elementAt (0)));
+        }
+        feature_names.append (")");
+
+      }
+
+      if (features.size () == 1) {
+        new_text.append ("Selected feature:  ");
+      } else {
+        new_text.append (features.size () + " selected features  ");
+      }
+
+      if (features.size () < 100) {
+        // show a count of the number of bases and amino acids in the selected
+        // feature
+        if (features.size () > 1) {
+          new_text.append ("total bases " + base_total);
+
+          if (!saw_a_non_cds) {
+            new_text.append ("  total amino acids " + aa_total);
+          }
+        } else {
+          if (features.size () == 1) {
+            new_text.append ("bases " + base_total);
+
+            if (!saw_a_non_cds) {
+              new_text.append ("  amino acids " + aa_total);
+            }
+          }
+        }
+      }
+
+      new_text.append ("  ");
+
+      new_text.append (feature_names);
+    }
+
+
+    String text = new_text.toString ();
+
+    if (text.length () > 0) {
+      if (text.length () > 150) {
+        // call substring () just to keep the String a sensible length
+        text = text.substring (0, 150);
+      }
+    } else {
+      text = "Nothing selected";
+    }
+
+    g.setColor (new Color (230, 230, 230));
+
+    g.fillRect (0, 0, getCanvasWidth (), getCanvasHeight ());
+
+    g.setColor (Color.black);
+
+    g.drawString (text, 2, getFontMaxAscent () + 1);
+  }
+
+  /**
+   *  Return a String containing the qualifiers (except the /note) of given
+   *  feature.
+   **/
+  private String getQualifierString (final Feature feature) {
+    final QualifierVector qualifiers = feature.getQualifiers ();
+
+    // if we see a /note or /fasta_file it gets saved for later
+    final Vector saved_qualifiers = new Vector ();
+
+    final StringBuffer string_buffer = new StringBuffer ();
+
+    final EntryInformation entry_information =
+      feature.getEntry ().getEntryInformation ();
+
+    for (int i = 0 ; i < qualifiers.size () ; ++i) {
+      final Qualifier this_qualifier = qualifiers.elementAt (i);
+
+      if (this_qualifier.getName ().equals ("note") ||
+          this_qualifier.getName ().endsWith ("_file")) {
+        // save the qualifier and put it last
+        saved_qualifiers.addElement (this_qualifier);
+        continue;
+      }
+
+      if (string_buffer.length () > 0) {
+        // put spaces between the qualifiers
+        string_buffer.append (' ');
+      }
+
+      final QualifierInfo qualifier_info =
+        entry_information.getQualifierInfo (this_qualifier.getName ());
+
+      string_buffer.append (StreamQualifier.toString (qualifier_info,
+                                                      this_qualifier));
+    }
+
+    for (int i = 0 ; i < saved_qualifiers.size () ; ++i) {
+      if (string_buffer.length () > 0) {
+        // put spaces between the qualifiers
+        string_buffer.append (' ');
+      }
+
+      final Qualifier this_qualifier =
+        (Qualifier) saved_qualifiers.elementAt (i);
+
+      final QualifierInfo qualifier_info =
+        entry_information.getQualifierInfo (this_qualifier.getName ());
+
+      string_buffer.append (StreamQualifier.toString (qualifier_info,
+                                                      this_qualifier));
+    }
+
+    return string_buffer.toString ();
+  }
+
+  /**
+   *  If the selection contains a MarkerRange (a range of bases) then return a
+   *  String in this form: "Selected Bases on forward strand: <start>..<end> ",
+   *  otherwise return "".  This method is also used by the SelectionViewer
+   *  class.
+   *  @param current_selection The selection to summarize.
+   *  @param entry_group The EntryGroup that the selection refers to.
+   **/
+  static String markerRangeText (final Selection current_selection,
+                                 final EntryGroup entry_group) {
+    final MarkerRange marker_range = current_selection.getMarkerRange ();
+
+    if (marker_range == null) {
+      return "";
+    } else {
+      final StringBuffer buffer = new StringBuffer ();
+
+      final int start_pos = marker_range.getStart ().getPosition ();
+      final int end_pos = marker_range.getEnd ().getPosition ();
+
+      if (marker_range.getStrand ().isForwardStrand ()) {
+        if (start_pos == end_pos) {
+          buffer.append ("One selected base on forward strand: " +
+                         start_pos + "  ");
+        } else {
+          buffer.append ((end_pos - start_pos + 1) +
+                         " selected bases on forward strand: " +
+                         start_pos + ".." + end_pos + " ");
+        }
+      } else {
+        if (start_pos == end_pos) {
+          buffer.append ("One selected base on reverse strand: " +
+                         marker_range.getStart ().getPosition () +
+                         "  = complement (" +
+                         marker_range.getEnd ().getRawPosition () + ") ");
+        } else {
+          buffer.append ((end_pos - start_pos + 1) +
+                         " selected bases on reverse strand: " +
+                         marker_range.getStart ().getPosition () + ".." +
+                         marker_range.getEnd ().getPosition () +
+                         "  = complement (" +
+                         marker_range.getEnd ().getRawPosition () + ".." +
+                         marker_range.getStart ().getRawPosition () + ") ");
+        }
+      }
+
+      if (marker_range.getCount () >= 3) {
+        // check if this codon is in a feature
+
+        final Range raw_range = marker_range.getRawRange ();
+
+        final FeatureVector features;
+
+        try {
+          features = entry_group.getFeaturesInRange (raw_range);
+        } catch (OutOfRangeException e) {
+          return "illegal marker range";
+        }
+
+        for (int i = 0 ; i < features.size () ; ++i) {
+          final Feature this_feature = features.elementAt (i);
+
+          if (!this_feature.isProteinFeature ()) {
+            // only display protein features
+            continue;
+          }
+
+          if (marker_range.isForwardMarker () !=
+              this_feature.isForwardFeature ()) {
+            // only display feature positions in features on the same strand
+            // as the selected bases
+            continue;
+          }
+
+          final String this_feature_id = this_feature.getIDString ();
+
+          final Marker start_marker = marker_range.getStart ();
+
+          final Marker end_marker = marker_range.getEnd ();
+
+          final int start_position_in_feature =
+            this_feature.getFeaturePositionFromMarker (start_marker);
+
+          final int start_codon_position =
+            start_position_in_feature - this_feature.getCodonStart () + 1;
+
+          String start_string = null;
+
+          if (start_position_in_feature != -1) {
+            if (start_codon_position % 3 == 0) {
+              start_string = "codon " + (start_codon_position / 3 + 1);
+            }
+          }
+
+          String end_string = null;
+
+          final int end_position_in_feature =
+            this_feature.getFeaturePositionFromMarker (end_marker);
+
+          final int end_codon_position =
+            end_position_in_feature - this_feature.getCodonStart () - 1;
+
+          if (end_position_in_feature != -1) {
+            if (start_codon_position != end_codon_position &&
+                end_codon_position % 3 == 0) {
+              end_string = "codon " + (end_codon_position / 3 + 1);
+            }
+          }
+
+          if (!(start_string == null && end_string == null)) {
+            buffer.append (" (");
+
+            if (start_string != null) {
+              buffer.append (start_string);
+            }
+
+            if (start_string != null && end_string != null) {
+              buffer.append (" to ");
+            }
+
+            if (end_string != null) {
+              buffer.append (end_string);
+            }
+
+            buffer.append (" in feature " + this_feature.getIDString () +
+                           ") ");
+          }
+        }
+      }
+
+      return buffer.append (" ").toString ();
+    }
+  }
+
+  /**
+   *  Return the Selection reference that was passed to the constructor.
+   **/
+  private Selection getSelection () {
+    return selection;
+  }
+
+  /**
+   *  Return the Selection object that was passed to the constructor.
+   **/
+  private EntryGroup getEntryGroup () {
+    return entry_group;
+  }
+
+  /**
+   *  The reference of the EntryGroup object that was passed to the
+   *  constructor.
+   **/
+  private EntryGroup entry_group;
+
+  /**
+   *  This is a reference to the Selection object that created this
+   *  component.
+   **/
+  private final Selection selection;
+}
+
diff --git a/uk/ac/sanger/artemis/components/SelectionMenu.java b/uk/ac/sanger/artemis/components/SelectionMenu.java
new file mode 100644
index 000000000..252bd30e3
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/SelectionMenu.java
@@ -0,0 +1,294 @@
+/* SelectionMenu.java
+ *
+ * created: Sun Jan 10 1999
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright(C) 1998,1999,2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/SelectionMenu.java,v 1.1 2004-06-09 09:47:44 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.sequence.MarkerRange;
+import uk.ac.sanger.artemis.*;
+
+import java.awt.event.*;
+import javax.swing.*;
+
+/**
+ *  This a super class for EditMenu, ViewMenu and GotoMenu.  It is a JMenu that
+ *  knows how to get hold of the Selection.  It also has the method
+ *  getParentFrame() to find the owning JFrame of the menu.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: SelectionMenu.java,v 1.1 2004-06-09 09:47:44 tjc Exp $
+ **/
+
+public class SelectionMenu extends JMenu 
+{
+
+  /** The Selection that was passed to the constructor. */
+  /* final */ private Selection selection;
+
+  /** The JFrame reference that was passed to the constructor. */
+  private JFrame frame = null;
+
+  /**
+   *  Create a new SelectionMenu object.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param name The string to use as the menu title.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   **/
+  public SelectionMenu(final JFrame frame,
+                        final String menu_name,
+                        final Selection selection) 
+  {
+    super(menu_name);
+    this.frame = frame;
+    this.selection = selection;
+  }
+
+  /**
+   *  Return the JFrame that was passed to the constructor.
+   **/
+  public JFrame getParentFrame() 
+  {
+    return frame;
+  }
+
+  /**
+   *  Check that the are some Features in the current selection.  If there are
+   *  some features then return true.  If there are no features selected then
+   *  popup a message saying so and return false.
+   **/
+  protected boolean checkForSelectionFeatures() 
+  {
+    return checkForSelectionFeatures(getParentFrame(), getSelection());
+  }
+
+  /**
+   *  Check that the are some Features in the given selection.  If there are
+   *  some features then return true.  If there are no features selected then
+   *  popup a message saying so and return false.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The selected features to check.
+   **/
+  static boolean checkForSelectionFeatures(final JFrame frame,
+                                           final Selection selection) 
+  {
+    final FeatureVector features_to_check = selection.getAllFeatures();
+
+    if(features_to_check.size() == 0) 
+    {
+      new MessageDialog(frame, "No features selected");
+      return false;
+    } 
+    else 
+      return true;
+  }
+
+  /**
+   *  Check that the are some Features in the current selection and that the
+   *  selection isn't too big.  If there is more than one feature in the
+   *  selection and less than the given maximum then return true.  If there
+   *  are no features selected then popup a message saying so and return
+   *  false.  If there are more selected features than the given maximum
+   *  than display the message in a YesNoDialog component and return the
+   *  result of the dialog.
+   **/
+  protected boolean checkForSelectionFeatures(final int maximum_size,
+                                              final String message) 
+  {
+    return checkForSelectionFeatures(getParentFrame(), getSelection(),
+                                      maximum_size, message);
+  }
+
+  /**
+   *  Check that the are some Features in the given selection and that the
+   *  selection isn't too big.  If there is more than one feature in the
+   *  selection and less than the given maximum then return true.  If there
+   *  are no features selected then popup a message saying so and return
+   *  false.  If there are more selected features than the given maximum
+   *  than display the message in a YesNoDialog component and return the
+   *  result of the dialog.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The selected features to check.
+   **/
+  static boolean checkForSelectionFeatures(final JFrame frame,
+                                           final Selection selection,
+                                           final int maximum_size,
+                                           final String message) 
+  {
+    final FeatureVector features_to_check = selection.getAllFeatures();
+
+    if(features_to_check.size() == 0) 
+    {
+      new MessageDialog(frame, "No features selected");
+      return false;
+    }
+    else 
+    {
+      if(features_to_check.size() > maximum_size) 
+      {
+        final YesNoDialog dialog =
+          new YesNoDialog(frame, message);
+
+        return dialog.getResult();
+      }
+      else 
+        return true;
+    }
+  }
+
+  /**
+
+   *  Check that there are only CDS features in the given selection, that there
+   *  are some features selected and that the selection isn't too big.  If
+   *  there is more than one feature in the selection and less than the given
+   *  maximum then return true.  If there are no features selected then popup
+   *  a message saying so and return false.  If there are more selected
+   *  features than the given maximum than display the message in a
+   *  YesNoDialog component and return the result of the dialog.
+   *  @param frame The Frame to use for MessageDialog components.
+   *  @param selection The selected features to check.
+   **/
+  static boolean checkForSelectionCDSFeatures(final JFrame frame,
+                                               final Selection selection,
+                                               final int maximum_size,
+                                               final String max_message) 
+  {
+    final FeatureVector features_to_check = selection.getAllFeatures();
+
+    if(features_to_check.size() == 0) 
+    {
+      new MessageDialog(frame, "No CDS features selected");
+      return false;
+    }
+    else
+    {
+      for(int i = 0 ; i < features_to_check.size() ; ++i) 
+      {
+        final Feature this_feature = features_to_check.elementAt(i);
+        if(!this_feature.isCDS()) 
+        {
+          final String message =
+            "a non-CDS feature(" + this_feature.getIDString() +
+            ") is selected - can't continue";
+          final MessageDialog dialog =
+            new MessageDialog(frame, message);
+          return false;
+        }
+      }
+
+      if(features_to_check.size() > maximum_size) 
+      {
+        final YesNoDialog dialog =
+          new YesNoDialog(frame, max_message);
+
+        return dialog.getResult();
+      }
+      else 
+        return true;
+    }
+  }
+
+  /**
+   *  Check that the are some FeatureSegments in the current selection and
+   *  that the selection isn't too big.  If there is more than one
+   *  FeatureSegments in the selection and less than the given maximum then
+   *  return true.  If there are no FeatureSegments selected then popup a
+   *  message saying so and return false.  If there are more selected
+   *  FeatureSegments than the given maximum than display the message in a
+   *  YesNoDialog component and return the result of the dialog.
+   **/
+  protected boolean checkForSelectionFeatureSegments(final int maximum_size,
+                                                     final String message) 
+  {
+    final FeatureSegmentVector segments_to_check =
+      getSelection().getSelectedSegments();
+
+    if(segments_to_check.size() == 0) 
+    {
+      new MessageDialog(getParentFrame(), "No exons selected");
+      return false;
+    } 
+    else 
+    {
+      if(segments_to_check.size() > maximum_size) 
+      {
+        final YesNoDialog dialog =
+          new YesNoDialog(getParentFrame(), message);
+
+        return dialog.getResult();
+      } 
+      else 
+        return true;
+    }
+  }
+
+  /**
+   *  Check that the current selection contains a MarkerRange.  If it does
+   *  then return true.  If not then popup a message saying so and return
+   *  false.
+   **/
+  protected boolean checkForSelectionRange() 
+  {
+    return checkForSelectionRange(getParentFrame(), getSelection());
+  }
+
+  /**
+   *  Check that the current selection contains a MarkerRange.  If it does
+   *  then return true.  If not then popup a message saying so and return
+   *  false.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The selected features to check.
+   **/
+  static boolean checkForSelectionRange(final JFrame frame,
+                                        final Selection selection) 
+  {
+    final MarkerRange marker_range = selection.getMarkerRange();
+
+    if(marker_range == null) 
+    {
+      new MessageDialog(frame, "No bases selected");
+      return false;
+    } 
+    else 
+      return true;
+  }
+
+  /**
+   *  Return the Selection object that was passed to the constructor.
+   **/
+  protected Selection getSelection() 
+  {
+    return selection;
+  }
+  
+  /**
+   *
+   **/
+  protected static KeyStroke makeMenuKeyStroke(final int key_code)
+  {
+    return KeyStroke.getKeyStroke(key_code, InputEvent.CTRL_MASK);
+  }
+
+}
+
diff --git a/uk/ac/sanger/artemis/components/SelectionViewer.java b/uk/ac/sanger/artemis/components/SelectionViewer.java
new file mode 100644
index 000000000..4db4a3ca0
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/SelectionViewer.java
@@ -0,0 +1,352 @@
+/* SelectionViewer.java
+ *
+ * created: Thu Mar  4 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/SelectionViewer.java,v 1.1 2004-06-09 09:47:45 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.util.StringVector;
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+
+/**
+ *  This component displays the current selection in a FileViewer component.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: SelectionViewer.java,v 1.1 2004-06-09 09:47:45 tjc Exp $
+ **/
+
+public class SelectionViewer
+    implements SelectionChangeListener, EntryGroupChangeListener {
+  /**
+   *  Create a new SelectionViewer object from the given Selection.
+   **/
+  public SelectionViewer (final Selection selection,
+                          final EntryGroup entry_group) {
+    file_viewer = new FileViewer ("Artemis Selection View");
+
+    this.selection = selection;
+    this.entry_group = entry_group;
+
+    readSelection ();
+
+    selection.addSelectionChangeListener (this);
+
+    file_viewer.addWindowListener (new WindowAdapter () {
+      public void windowClosed (WindowEvent event) {
+        stopListening ();
+      }
+    });
+
+    entry_group.addEntryGroupChangeListener (this);
+  }
+
+  /**
+   *  Remove this object as a selection change listener.
+   **/
+  public void stopListening () {
+    selection.removeSelectionChangeListener (this);
+    entry_group.removeEntryGroupChangeListener (this);
+  }
+
+ /**
+   *  Implementation of the SelectionChangeListener interface.  We listen to
+   *  SelectionChange events so that we can update the view to reflect the
+   *  current selection.
+   **/
+  public void selectionChanged (SelectionChangeEvent event) {
+    readSelection ();
+  }
+
+  /**
+   *  Implementation of the EntryGroupChangeListener interface.  We listen to
+   *  EntryGroupChange events so that we can get rid of the JFrame when the
+   *  EntryGroup is no longer in use (for example when the EntryEdit is
+   *  closed).
+   **/
+  public void entryGroupChanged (final EntryGroupChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryGroupChangeEvent.DONE_GONE:
+      entry_group.removeEntryGroupChangeListener (this);
+      file_viewer.dispose ();
+      break;
+    }
+  }
+
+  /**
+   *  Read the selection into this SelectionViewer object.
+   **/
+  public void readSelection () {
+    final String base_selection_text =
+      SelectionInfoDisplay.markerRangeText (selection, entry_group);
+
+    final FeatureVector selection_features = selection.getAllFeatures ();
+
+    final StringBuffer buffer = new StringBuffer ();
+
+    final int MAX_FEATURES = 50;
+
+    if (selection_features.size () > MAX_FEATURES) {
+      buffer.append ("first " + MAX_FEATURES + " features:\n\n");
+    }
+
+    for (int i = 0 ; i < selection_features.size () && i < 50 ; ++i) {
+      buffer.append (selection_features.elementAt (i).toString ());
+    }
+
+    if (selection_features.size () > 0) {
+      buffer.append ("\n");
+    }
+
+    if (base_selection_text != null && base_selection_text.length () > 0) {
+      buffer.append (base_selection_text).append ("\n\n");
+    }
+
+
+    // this is the maximum number of bases to show
+    final int display_base_count;
+
+    // this is the maximum number of residues to show
+    final int display_residues_count;
+
+    if (selection_features.size () == 0) {
+      display_base_count = 2000;
+      display_residues_count = 1000;
+    } else {
+      display_base_count = 600;
+      display_residues_count = 300;
+    }
+
+
+    final String selection_bases = selection.getSelectedBases ();
+
+    if (selection_bases != null && selection_bases.length () > 0) {
+      final StringVector base_summary = getBaseSummary (selection_bases);
+
+      for (int i = 0 ; i < base_summary.size () ; ++i) {
+        buffer.append (base_summary.elementAt (i)).append ("\n");
+      }
+    }
+
+    if (selection_features.size () == 1) {
+      double score =
+        selection_features.elementAt (0).get12CorrelationScore ();
+
+      score = Math.round (score * 100) / 100.0;
+
+      buffer.append ("\nCorrelation score of the selected feature: " +
+                     score + "\n");
+    }
+
+    buffer.append ("\n");
+
+    if (selection_features.size () > 1) {
+      double correlation_score_total = 0;
+
+      double max_gc_content = -999;
+      double min_gc_content =  999;
+
+      double max_correlation_score = -999999;
+      double min_correlation_score =  999999;
+
+      for (int i = 0; i < selection_features.size () ; ++i) {
+        final Feature this_feature = selection_features.elementAt (i);
+
+        final double correlation_score = this_feature.get12CorrelationScore ();
+        final double gc_content = this_feature.getPercentGC ();
+
+        correlation_score_total += correlation_score;
+
+        if (min_correlation_score > correlation_score) {
+          min_correlation_score = correlation_score;
+        }
+
+        if (max_correlation_score < correlation_score) {
+          max_correlation_score = correlation_score;
+        }
+
+        if (min_gc_content > gc_content) {
+          min_gc_content = gc_content;
+        }
+
+        if (max_gc_content < gc_content) {
+          max_gc_content = gc_content;
+        }
+      }
+
+      min_gc_content = Math.round (min_gc_content * 100) / 100.0;
+      max_gc_content = Math.round (max_gc_content * 100) / 100.0;
+      min_correlation_score = Math.round (min_correlation_score * 100) / 100.0;
+      max_correlation_score = Math.round (max_correlation_score * 100) / 100.0;
+
+      final double correlation_score_average =
+        Math.round (correlation_score_total / selection_features.size () *
+                    100) / 100.0;
+
+      buffer.append ("Average correlation score of the selected features: " +
+                     correlation_score_average + "\n\n");
+
+      buffer.append ("Min GC percentage of the selected features: " +
+                     min_gc_content + "\n");
+      buffer.append ("Max GC percentage of the selected features: " +
+                     max_gc_content + "\n");
+      buffer.append ("\n");
+      buffer.append ("Min correlation score of the selected features: " +
+                     min_correlation_score + "\n");
+      buffer.append ("Max correlation score of the selected features: " +
+                     max_correlation_score + "\n");
+      buffer.append ("\n");
+    }
+
+
+    if (selection_bases.length () > 0 && selection_features.size () <= 1) {
+      if (selection_bases.length () > display_base_count) {
+        // display the first display_base_count/2 bases and the last
+        // display_base_count/2 bases
+        buffer.append ("first " + display_base_count / 2 +
+                                     " bases of selection:\n");
+        final String start_sub_string =
+          selection_bases.substring (0, display_base_count / 2);
+        buffer.append (start_sub_string).append ("\n\n");
+        buffer.append ("last " + display_base_count / 2 +
+                       " bases of selection:\n");
+        final String sub_string =
+          selection_bases.substring (selection_bases.length () -
+                                     display_base_count / 2);
+        buffer.append (sub_string).append ("\n\n");
+      } else {
+        buffer.append ("bases of selection:\n");
+        buffer.append (selection_bases).append ("\n\n");
+      }
+
+      if (selection_bases.length () >= 3) {
+        final int residues_to_display;
+
+        if (selection_bases.length () / 3 < display_residues_count) {
+          residues_to_display = selection_bases.length () / 3;
+
+          buffer.append ("translation of the selected bases:\n");
+        } else {
+          residues_to_display = display_residues_count;
+
+          buffer.append ("translation of the first " +
+                         display_residues_count * 3 +
+                         " selected bases:\n");
+        }
+
+        final String residue_bases =
+          selection_bases.substring (0, residues_to_display * 3);
+
+        final String residues =
+          AminoAcidSequence.getTranslation (residue_bases, true).toString ();
+
+        buffer.append (residues.toUpperCase ()).append ("\n\n");
+      }
+    }
+
+    file_viewer.setText (buffer.toString ());
+  }
+
+  /**
+   *  Return a summary of the base content of the given String.  The return
+   *  value is a StringVector containing each line to be output.  We return a
+   *  vector so that the caller can do things like indenting the lines.
+   *  @return null if the bases String is empty or null
+   **/
+  public static StringVector getBaseSummary (final String bases) {
+    if (bases == null || bases.length () == 0) {
+      return null;
+    }
+
+    final StringVector return_vector = new StringVector ();
+
+    long counts [] = new long [257];
+
+    for (int i = 0 ; i < bases.length () ; ++i) {
+      final char this_char = bases.charAt (i);
+
+      if (this_char > 255) {
+        // don't know what this is, but it isn't a base
+        ++counts[256];
+      } else {
+        ++counts[this_char];
+      }
+    }
+
+    for (int i = 0 ; i < 256 ; ++i) {
+      if (counts[i] > 0) {
+        return_vector.add (new String (Character.toUpperCase ((char)i) +
+                                       " content: " + counts[i] +
+                                       "  (" + 10000 * counts[i] /
+                                       bases.length () / 100.0 + "%)"));
+      }
+    }
+    
+    final long non_ambiguous_base_count =
+      counts['g'] + counts['c'] + counts['a'] + counts['t'];
+
+    if (non_ambiguous_base_count == bases.length ()) {
+      return_vector.add ("(no ambiguous bases)");
+    }
+
+    return_vector.add ("");
+
+    return_vector.add (new String ("GC percentage: " +
+                                   (Math.round (100.0 *
+                                                (counts['g'] + counts['c']) /
+                                                bases.length () * 100) /
+                                    100.0)));
+
+    if (non_ambiguous_base_count > 0 &&
+        non_ambiguous_base_count < bases.length ()) {
+      return_vector.add (new String ("GC percentage of non-ambiguous bases: " +
+                                     (Math.round (100.0 * (counts['g'] +
+                                                           counts['c']) /
+                                                  non_ambiguous_base_count *
+                                                  100) / 100.0)));
+    }
+
+    return return_vector;
+  }
+
+  /**
+   *  This is the selection object that we are viewing.
+   **/
+  final private Selection selection;
+
+  /**
+   *  The FileViewer object that is displaying the selection.
+   **/
+  final private FileViewer file_viewer;
+
+  /**
+   *  The EntryGroup that was passed to the constructor.
+   **/
+  final EntryGroup entry_group;
+}
+
+
diff --git a/uk/ac/sanger/artemis/components/Selector.java b/uk/ac/sanger/artemis/components/Selector.java
new file mode 100644
index 000000000..d9dcd942b
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/Selector.java
@@ -0,0 +1,885 @@
+/* Selector.java
+ *
+ * created: Tue Apr 11 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2000,2001,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/Selector.java,v 1.1 2004-06-09 09:47:46 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.AminoAcidSequence;
+
+import uk.ac.sanger.artemis.io.EntryInformation;
+import uk.ac.sanger.artemis.util.StringVector;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  This component allows the user to set the selection by filtering the
+ *  features in an EntryGroup on key and contents.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: Selector.java,v 1.1 2004-06-09 09:47:46 tjc Exp $
+ **/
+
+public class Selector extends JFrame
+    implements EntryGroupChangeListener {
+  /**
+   *  Create a new Selector that van set the given Selection.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param entry_group The component will choose features from this
+   *    EntryGroup.
+   *  @param goto_event_source The object the we will call gotoBase () on.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public Selector (final Selection selection,
+                   final GotoEventSource goto_event_source,
+                   final EntryGroup entry_group,
+                   final BasePlotGroup base_plot_group) {
+    super ("Artemis Feature Selector");
+
+    this.selection = selection;
+    this.entry_group = entry_group;
+
+    final Font default_font = Options.getOptions ().getFont ();
+
+    setFont (default_font);
+
+    GridBagLayout gridbag = new GridBagLayout();
+    getContentPane ().setLayout (gridbag);
+
+    GridBagConstraints c = new GridBagConstraints();
+
+    c.fill = GridBagConstraints.HORIZONTAL;
+    c.anchor = GridBagConstraints.WEST;
+    c.weighty = 0;
+    c.gridwidth = GridBagConstraints.REMAINDER;
+
+    final JLabel top_label = new JLabel ("Select by:");
+    gridbag.setConstraints (top_label, c);
+    getContentPane ().add (top_label);
+
+
+    by_key_button = new JCheckBox ("Key: ", false);
+    final JPanel by_key_panel = new JPanel ();
+    by_key_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    by_key_panel.add (by_key_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (by_key_panel, c);
+    getContentPane ().add (by_key_panel);
+
+    final EntryInformation default_entry_information =
+      Options.getArtemisEntryInformation ();
+
+    key_selector = new KeyChoice (default_entry_information);
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (key_selector, c);
+    getContentPane ().add (key_selector);
+
+    key_selector.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent _) {
+        by_key_button.setSelected (true);
+      }
+    });
+
+    by_key_button.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent e) {
+        if (!by_key_button.isSelected ()) {
+          by_qualifier_button.setSelected (false);
+        }
+      }
+    });
+
+
+    by_qualifier_button = new JCheckBox ("Qualifier: ", false);
+    final JPanel by_qualifier_panel = new JPanel ();
+    by_qualifier_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    by_qualifier_panel.add (by_qualifier_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (by_qualifier_panel, c);
+    getContentPane ().add (by_qualifier_panel);
+
+    qualifier_selector = new QualifierChoice (default_entry_information,
+                                              key_selector.getSelectedItem (),
+                                              null);
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (qualifier_selector, c);
+    getContentPane ().add (qualifier_selector);
+
+    qualifier_selector.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent _) {
+        by_qualifier_button.setSelected (true);
+        by_key_button.setSelected (true);
+      }
+    });
+
+    key_selector.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent _) {
+        qualifier_selector.setKey (key_selector.getSelectedItem ());
+      }
+    });
+
+    by_qualifier_button.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent e) {
+        if (by_qualifier_button.isSelected ()) {
+          if (!by_key_button.isSelected ()) {
+            by_key_button.setSelected (true);
+          }
+        }
+      }
+    });
+
+
+    final JLabel qualifier_text_label =
+      new JLabel ("   Containing this text: ");
+    c.gridwidth = 2;
+    gridbag.setConstraints (qualifier_text_label, c);
+    getContentPane ().add (qualifier_text_label);
+
+    qualifier_text = new JTextField ("", 18);
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (qualifier_text, c);
+    getContentPane ().add (qualifier_text);
+
+
+    ignore_case_checkbox = new JCheckBox ("Ignore Case", true);
+
+    final JPanel ignore_case_panel = new JPanel ();
+
+    ignore_case_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    ignore_case_panel.add (ignore_case_checkbox);
+
+    c.gridwidth = 2;
+    gridbag.setConstraints (ignore_case_panel, c);
+    getContentPane ().add (ignore_case_panel);
+
+
+    partial_match_checkbox = new JCheckBox ("Allow Partial Match", true);
+
+    final JPanel partial_match_panel = new JPanel ();
+
+    partial_match_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    partial_match_panel.add (partial_match_checkbox);
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (partial_match_panel, c);
+    getContentPane ().add (partial_match_panel);
+
+
+    match_any_word_checkbox = new JCheckBox ("Match Any Word", false);
+
+    final JPanel match_any_word_panel = new JPanel ();
+
+    match_any_word_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    match_any_word_panel.add (match_any_word_checkbox);
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (match_any_word_panel, c);
+    getContentPane ().add (match_any_word_panel);
+
+
+    {
+      // leave a blank line to give the user a visual clue
+      final JLabel blank_label = new JLabel ("");
+
+      gridbag.setConstraints (blank_label, c);
+      getContentPane ().add (blank_label);
+
+      final JLabel and_label = new JLabel ("And:");
+
+      gridbag.setConstraints (and_label, c);
+      getContentPane ().add (and_label);
+    }
+
+
+    less_than_bases_button = new JCheckBox ("Up to: ", false);
+    final JPanel less_than_bases_panel = new JPanel ();
+    less_than_bases_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    less_than_bases_panel.add (less_than_bases_button);
+    c.gridwidth = 3;
+    gridbag.setConstraints (less_than_bases_panel, c);
+    getContentPane ().add (less_than_bases_panel);
+
+    less_than_bases_text = new JTextField ("", 18);
+
+    gridbag.setConstraints (less_than_bases_text, c);
+    getContentPane ().add (less_than_bases_text);
+
+    final JLabel less_than_bases_label = new JLabel (" bases long");
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (less_than_bases_label, c);
+    getContentPane ().add (less_than_bases_label);
+
+    {
+      // leave a blank line to give the user a visual clue
+      final JLabel blank_label = new JLabel ("");
+
+      gridbag.setConstraints (blank_label, c);
+      getContentPane ().add (blank_label);
+
+      final JLabel and_label = new JLabel ("And:");
+
+      gridbag.setConstraints (and_label, c);
+      getContentPane ().add (and_label);
+    }
+
+
+    greater_than_bases_button = new JCheckBox ("At least: ", false);
+    final JPanel greater_than_bases_panel = new JPanel ();
+    greater_than_bases_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    greater_than_bases_panel.add (greater_than_bases_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (greater_than_bases_panel, c);
+    getContentPane ().add (greater_than_bases_panel);
+
+    greater_than_bases_text = new JTextField ("", 18);
+
+    gridbag.setConstraints (greater_than_bases_text, c);
+    getContentPane ().add (greater_than_bases_text);
+
+    final JLabel greater_than_bases_label = new JLabel (" bases long");
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (greater_than_bases_label, c);
+    getContentPane ().add (greater_than_bases_label);
+
+    {
+      // leave a blank line to give the user a visual clue
+      final JLabel blank_label = new JLabel ("");
+
+      gridbag.setConstraints (blank_label, c);
+      getContentPane ().add (blank_label);
+
+      final JLabel and_label = new JLabel ("And:");
+
+      gridbag.setConstraints (and_label, c);
+      getContentPane ().add (and_label);
+    }
+
+    less_than_exons_button = new JCheckBox ("Up to: ", false);
+    final JPanel less_than_exons_panel = new JPanel ();
+    less_than_exons_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    less_than_exons_panel.add (less_than_exons_button);
+    c.gridwidth = 3;
+    gridbag.setConstraints (less_than_exons_panel, c);
+    getContentPane ().add (less_than_exons_panel);
+
+    less_than_exons_text = new JTextField ("", 18);
+
+    gridbag.setConstraints (less_than_exons_text, c);
+    getContentPane ().add (less_than_exons_text);
+
+    final JLabel less_than_exons_label = new JLabel (" exons long");
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (less_than_exons_label, c);
+    getContentPane ().add (less_than_exons_label);
+
+    {
+      // leave a blank line to give the user a visual clue
+      final JLabel blank_label = new JLabel ("");
+
+      gridbag.setConstraints (blank_label, c);
+      getContentPane ().add (blank_label);
+
+      final JLabel and_label = new JLabel ("And:");
+      
+      gridbag.setConstraints (and_label, c);
+      getContentPane ().add (and_label);
+    }
+
+
+    greater_than_exons_button = new JCheckBox ("At least: ", false);
+    final JPanel greater_than_exons_panel = new JPanel ();
+    greater_than_exons_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    greater_than_exons_panel.add (greater_than_exons_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (greater_than_exons_panel, c);
+    getContentPane ().add (greater_than_exons_panel);
+
+    greater_than_exons_text = new JTextField ("", 18);
+
+    gridbag.setConstraints (greater_than_exons_text, c);
+    getContentPane ().add (greater_than_exons_text);
+
+    final JLabel greater_than_exons_label = new JLabel (" exons long");
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (greater_than_exons_label, c);
+    getContentPane ().add (greater_than_exons_label);
+
+    {
+      // leave a blank line to give the user a visual clue
+      final JLabel blank_label = new JLabel ("");
+
+      gridbag.setConstraints (blank_label, c);
+      getContentPane ().add (blank_label);
+
+      final JLabel and_label = new JLabel ("And by:");
+
+      gridbag.setConstraints (and_label, c);
+      getContentPane ().add (and_label);
+    }
+
+
+    by_motif_button = new JCheckBox ("Amino acid motif: ", false);
+    final JPanel by_motif_panel = new JPanel ();
+    by_motif_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+    by_motif_panel.add (by_motif_button);
+    c.gridwidth = 2;
+    gridbag.setConstraints (by_motif_panel, c);
+    getContentPane ().add (by_motif_panel);
+
+    motif_text = new JTextField ("", 18);
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (motif_text, c);
+    getContentPane ().add (motif_text);
+
+    {
+      // leave a blank line to give the user a visual clue
+      final JLabel blank_label = new JLabel ("");
+
+      gridbag.setConstraints (blank_label, c);
+      getContentPane ().add (blank_label);
+    }
+
+    final JPanel strand_panel = new JPanel ();
+
+    strand_panel.setLayout (new FlowLayout (FlowLayout.LEFT));
+
+    forward_strand_checkbox = new JCheckBox ("Forward Strand Features", true);
+    strand_panel.add (forward_strand_checkbox);
+    forward_strand_checkbox.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent _) {
+        if (!forward_strand_checkbox.isSelected () &&
+            !reverse_strand_checkbox.isSelected ()) {
+          // make sure one of the strand is always selected
+          reverse_strand_checkbox.setSelected (true);
+        }
+      }
+    });
+
+
+    reverse_strand_checkbox = new JCheckBox ("Reverse Strand Features", true);
+    strand_panel.add (reverse_strand_checkbox);
+    reverse_strand_checkbox.addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent _) {
+        if (!reverse_strand_checkbox.isSelected () &&
+            !forward_strand_checkbox.isSelected ()) {
+          // make sure one of the strand is always selected
+          forward_strand_checkbox.setSelected (true);
+        }
+      }
+    });
+
+
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    gridbag.setConstraints (strand_panel, c);
+    getContentPane ().add (strand_panel);
+
+
+
+
+
+    final JButton select_button = new JButton ("Select");
+
+    select_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        selection.set (getSelected ());
+      }
+    });
+
+    final JButton view_button = new JButton ("View");
+
+    view_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        final FeaturePredicate predicate =
+          new FeatureFromVectorPredicate (getSelected ());
+
+        String title = "All features";
+
+        if (by_key_button.isSelected ()) {
+          title += " with key \"" + key_selector.getSelectedItem () + "\"";
+        }
+
+        if (by_qualifier_button.isSelected ()) {
+          if (qualifier_text.getText ().trim ().length () > 0) {
+            title +=
+              " with qualifier \"" + qualifier_selector.getSelectedItem () +
+              "\" containing text \"" + qualifier_text.getText () + "\"";
+          } else {
+            title +=
+              " with qualifier \"" +
+              qualifier_selector.getSelectedItem () + "\"";
+          }
+        }
+
+        if (forward_strand_checkbox.isSelected () &&
+            !reverse_strand_checkbox.isSelected ()) {
+          title += " on the forward strand";
+        }
+
+        if (!forward_strand_checkbox.isSelected () &&
+            reverse_strand_checkbox.isSelected ()) {
+          title += " on the reverse strand";
+        }
+
+        if (by_motif_button.isSelected ()) {
+          title +=
+            " with motif: " + motif_text.getText ().trim ().toUpperCase ();
+        }
+        
+        if (less_than_bases_button.isSelected ()) {
+          title +=
+            " at most " + less_than_bases_text.getText ().trim () +
+            " bases long";
+        }
+        
+        if (greater_than_bases_button.isSelected ()) {
+          title +=
+            " at least " + greater_than_bases_text.getText ().trim () +
+            " bases long";
+        }
+        
+        if (less_than_exons_button.isSelected ()) {
+          title +=
+            " at most " + less_than_exons_text.getText ().trim () +
+            " exons long";
+        }
+        
+        if (greater_than_exons_button.isSelected ()) {
+          title +=
+            " at least " + greater_than_exons_text.getText ().trim () +
+            " exons long";
+        }
+        
+        final FilteredEntryGroup filtered_entry_group =
+          new FilteredEntryGroup (entry_group, predicate, title);
+
+        final FeatureListFrame feature_list_frame =
+          new FeatureListFrame (title,
+                                getSelection (),
+                                goto_event_source, filtered_entry_group,
+                                base_plot_group);
+
+        feature_list_frame.setVisible (true);
+      }
+    });
+
+
+    final JButton close_button = new JButton ("Close");
+
+    close_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        Selector.this.dispose ();
+      }
+    });
+
+    final FlowLayout flow_layout =
+      new FlowLayout (FlowLayout.CENTER, 15, 5);
+
+    final JPanel bottom_button_panel = new JPanel (flow_layout);
+
+    bottom_button_panel.add (select_button);
+    bottom_button_panel.add (view_button);
+    bottom_button_panel.add (close_button);
+
+    gridbag.setConstraints (bottom_button_panel, c);
+    getContentPane ().add (bottom_button_panel);
+
+    addWindowListener (new WindowAdapter () {
+      public void windowClosing (WindowEvent event) {
+        getEntryGroup ().removeEntryGroupChangeListener (Selector.this);
+        Selector.this.dispose ();
+      }
+    });
+
+    getEntryGroup ().addEntryGroupChangeListener (this);
+
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+
+    setVisible (true);
+  }
+
+  /**
+   *  Implementation of the EntryGroupChangeListener interface.  We listen to
+   *  EntryGroupChange events so that we can get rid of the Navigator when the
+   *  EntryGroup is no longer in use (for example when the EntryEdit is
+   *  closed).
+   **/
+  public void entryGroupChanged (final EntryGroupChangeEvent event) {
+    switch (event.getType ()) {
+    case EntryGroupChangeEvent.DONE_GONE:
+      getEntryGroup ().removeEntryGroupChangeListener (this);
+      dispose ();
+      break;
+    }
+  }
+
+  /**
+   *  Return those features that match the current setting of the Selector.
+   **/
+  private FeatureVector getSelected () {
+    final FeaturePredicate key_and_qualifier_predicate;
+    final FeaturePredicate motif_predicate;
+
+    if (by_key_button.isSelected ()) {
+      if (by_qualifier_button.isSelected ()) {
+        final String search_text = qualifier_text.getText ().trim ();
+        final String qualifier_name =
+          (String) qualifier_selector.getSelectedItem ();
+
+        if (search_text.length () == 0) {
+          key_and_qualifier_predicate =
+            new FeatureKeyQualifierPredicate (key_selector.getSelectedItem (),
+                                              qualifier_name,
+                                              true);
+        } else {
+          if (match_any_word_checkbox.isSelected ()) {
+            final FeaturePredicateVector temp_predicates =
+              new FeaturePredicateVector ();
+
+            final StringVector words =
+              StringVector.getStrings (search_text, " \t\n");
+
+            for (int i = 0 ; i < words.size () ; ++i) {
+              final String this_word = words.elementAt (i);
+              final FeaturePredicate new_predicate =
+                new FeatureKeyQualifierPredicate (key_selector.getSelectedItem (),
+                                                  qualifier_name,
+                                                  this_word,
+                                                  partial_match_checkbox.isSelected (),
+                                                  ignore_case_checkbox.isSelected ());
+
+              temp_predicates.add (new_predicate);
+            }
+
+            key_and_qualifier_predicate =
+              new FeaturePredicateConjunction (temp_predicates,
+                                               FeaturePredicateConjunction.OR);
+          } else {
+            key_and_qualifier_predicate =
+              new FeatureKeyQualifierPredicate (key_selector.getSelectedItem (),
+                                                qualifier_name,
+                                                search_text,
+                                                partial_match_checkbox.isSelected (),
+                                                ignore_case_checkbox.isSelected ());
+            
+          }
+        }
+      } else {
+        final String search_text = qualifier_text.getText ().trim ();
+        if (search_text.length () == 0) {
+          key_and_qualifier_predicate =
+            new FeatureKeyPredicate (key_selector.getSelectedItem ());
+        } else {
+          if (match_any_word_checkbox.isSelected ()) {
+            final FeaturePredicateVector temp_predicates =
+              new FeaturePredicateVector ();
+            
+            final StringVector words = 
+              StringVector.getStrings (search_text, " \t\n");
+            
+
+            for (int i = 0 ; i < words.size () ; ++i) {
+              final String this_word = words.elementAt (i);
+              final FeaturePredicate new_predicate =
+                new FeatureKeyQualifierPredicate (key_selector.getSelectedItem (),
+                                                  null, // match any qialifier
+                                                  this_word,
+                                                  partial_match_checkbox.isSelected (),
+                                                  ignore_case_checkbox.isSelected ());
+              temp_predicates.add (new_predicate);
+            }
+
+            key_and_qualifier_predicate =
+              new FeaturePredicateConjunction (temp_predicates,
+                                               FeaturePredicateConjunction.OR);
+          } else {
+            key_and_qualifier_predicate =
+              new FeatureKeyQualifierPredicate (key_selector.getSelectedItem (),
+                                                null, // match any qialifier
+                                                search_text,
+                                                partial_match_checkbox.isSelected (),
+                                                ignore_case_checkbox.isSelected ());
+          }
+        }
+      }
+    } else {
+      key_and_qualifier_predicate = null;
+    }
+
+    if (by_motif_button.isSelected ()) {
+      final AminoAcidSequence amino_acid_sequence =
+        new AminoAcidSequence (motif_text.getText ().trim ());
+
+      motif_predicate =
+        new FeaturePatternPredicate (amino_acid_sequence);
+    } else {
+      motif_predicate = null;
+    }
+
+    FeaturePredicate less_than_bases_predicate = null;
+
+    if (less_than_bases_button.isSelected () &&
+        less_than_bases_text.getText ().trim ().length () > 0) {
+      try {
+        final int less_than_bases_int =
+          Integer.valueOf (less_than_bases_text.getText ().trim ()).intValue ();
+
+        less_than_bases_predicate = new FeaturePredicate () {
+          public boolean testPredicate (final Feature feature) {
+            if (feature.getBaseCount () <= less_than_bases_int) {
+              return true;
+            } else {
+              return false;
+            }
+          }
+        };
+      } catch (NumberFormatException e) {
+        new MessageDialog (this,
+                           "warning this is not a number: " +
+                           less_than_bases_text.getText ());
+        less_than_bases_predicate = null;
+      }
+    } else {
+      less_than_bases_predicate = null;
+    }
+
+    FeaturePredicate greater_than_bases_predicate = null;
+
+    if (greater_than_bases_button.isSelected () &&
+        greater_than_bases_text.getText ().trim ().length () > 0) {
+      try {
+        final int greater_than_bases_int =
+          Integer.valueOf (greater_than_bases_text.getText ().trim ()).intValue ();
+
+        greater_than_bases_predicate = new FeaturePredicate () {
+          public boolean testPredicate (final Feature feature) {
+            if (feature.getBaseCount () >= greater_than_bases_int) {
+              return true;
+            } else {
+              return false;
+            }
+          }
+        };
+      } catch (NumberFormatException e) {
+        new MessageDialog (this,
+                           "warning this is not a number: " +
+                           greater_than_bases_text.getText ());
+        greater_than_bases_predicate = null;
+      }
+    } else {
+      greater_than_bases_predicate = null;
+    }
+
+    FeaturePredicate less_than_exons_predicate = null;
+
+    if (less_than_exons_button.isSelected () &&
+        less_than_exons_text.getText ().trim ().length () > 0) {
+      try {
+        final int less_than_exons_int =
+          Integer.valueOf (less_than_exons_text.getText ().trim ()).intValue ();
+
+        less_than_exons_predicate = new FeaturePredicate () {
+          public boolean testPredicate (final Feature feature) {
+            if (feature.getSegments ().size () <= less_than_exons_int) {
+              return true;
+            } else {
+              return false;
+            }
+          }
+        };
+      } catch (NumberFormatException e) {
+        new MessageDialog (this,
+                           "warning this is not a number: " +
+                           less_than_exons_text.getText ());
+        less_than_exons_predicate = null;
+      }
+    } else {
+      less_than_exons_predicate = null;
+    }
+
+    FeaturePredicate greater_than_exons_predicate = null;
+
+    if (greater_than_exons_button.isSelected () &&
+        greater_than_exons_text.getText ().trim ().length () > 0) {
+      try {
+        final int greater_than_exons_int =
+          Integer.valueOf (greater_than_exons_text.getText ().trim ()).intValue ();
+
+        greater_than_exons_predicate = new FeaturePredicate () {
+          public boolean testPredicate (final Feature feature) {
+            if (feature.getSegments ().size () >= greater_than_exons_int) {
+              return true;
+            } else {
+              return false;
+            }
+          }
+        };
+      } catch (NumberFormatException e) {
+        new MessageDialog (this,
+                           "warning this is not a number: " +
+                           greater_than_exons_text.getText ());
+        greater_than_exons_predicate = null;
+      }
+    } else {
+      greater_than_exons_predicate = null;
+    }
+
+    FeaturePredicate predicate = null;
+
+    if (!by_key_button.isSelected () && !by_motif_button.isSelected ()) {
+      // default to selecting all features
+      predicate = new FeaturePredicate () {
+        public boolean testPredicate (final Feature feature) {
+          return true;
+        }
+      };
+    } else {
+      if (motif_predicate != null && key_and_qualifier_predicate != null) {
+        predicate =
+          new FeaturePredicateConjunction (key_and_qualifier_predicate,
+                                           motif_predicate,
+                                           FeaturePredicateConjunction.AND);
+      } else {
+        if (motif_predicate != null) {
+          predicate = motif_predicate;
+        } else {
+          predicate = key_and_qualifier_predicate;
+        }
+      }
+    }
+
+    if (less_than_bases_predicate != null) {
+      predicate =
+        new FeaturePredicateConjunction (predicate,
+                                         less_than_bases_predicate,
+                                         FeaturePredicateConjunction.AND);
+    }
+
+    if (greater_than_bases_predicate != null) {
+      predicate =
+        new FeaturePredicateConjunction (predicate,
+                                         greater_than_bases_predicate,
+                                         FeaturePredicateConjunction.AND);
+    }
+
+    if (less_than_exons_predicate != null) {
+      predicate =
+        new FeaturePredicateConjunction (predicate,
+                                         less_than_exons_predicate,
+                                         FeaturePredicateConjunction.AND);
+    }
+
+    if (greater_than_exons_predicate != null) {
+      predicate =
+        new FeaturePredicateConjunction (predicate,
+                                         greater_than_exons_predicate,
+                                         FeaturePredicateConjunction.AND);
+    }
+
+    final FeatureEnumeration test_enumerator = entry_group.features ();
+
+    final FeatureVector return_features = new FeatureVector ();
+
+    while (test_enumerator.hasMoreFeatures ()) {
+      final Feature this_feature = test_enumerator.nextFeature ();
+
+      if (predicate.testPredicate (this_feature)) {
+        if (this_feature.isForwardFeature ()) {
+          if (forward_strand_checkbox.isSelected ()) {
+            return_features.add (this_feature);
+          }
+        } else {
+          if (reverse_strand_checkbox.isSelected ()) {
+            return_features.add (this_feature);
+          }
+        }
+      }
+    }
+
+    return return_features;
+  }
+
+  /**
+   *  Return the Selection object that was passed to the constructor.
+   **/
+  private Selection getSelection () {
+    return selection;
+  }
+
+  /**
+   *  Return the EntryGroup object that was passed to the constructor.
+   **/
+  private EntryGroup getEntryGroup () {
+    return entry_group;
+  }
+
+  private JCheckBox by_key_button;
+  private JCheckBox by_qualifier_button;
+  private JCheckBox by_motif_button;
+  private JCheckBox less_than_bases_button;
+  private JCheckBox greater_than_bases_button;
+  private JCheckBox less_than_exons_button;
+  private JCheckBox greater_than_exons_button;
+  private JCheckBox ignore_case_checkbox;
+  private JCheckBox match_any_word_checkbox;
+  private JCheckBox forward_strand_checkbox;
+  private JCheckBox reverse_strand_checkbox;
+  private KeyChoice key_selector;
+  private QualifierChoice qualifier_selector;
+  private JTextField qualifier_text;
+  private JTextField motif_text;
+  private JTextField less_than_bases_text;
+  private JTextField greater_than_bases_text;
+  private JTextField less_than_exons_text;
+  private JTextField greater_than_exons_text;
+
+  /**
+   *  If checked the search text is allowed to match a substring of a
+   *  qualifier value.
+   **/
+  final JCheckBox partial_match_checkbox;
+
+  /**
+   *  The EntryGroup object that was passed to the constructor.
+   **/
+  final EntryGroup entry_group;
+
+  /**
+   *  This is the Selection that was passed to the constructor.
+   **/
+  final private Selection selection;
+}
diff --git a/uk/ac/sanger/artemis/components/SequenceViewer.java b/uk/ac/sanger/artemis/components/SequenceViewer.java
new file mode 100644
index 000000000..7015f7e98
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/SequenceViewer.java
@@ -0,0 +1,165 @@
+/* SequenceViewer.java
+ *
+ * created: Sat Dec 19 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/SequenceViewer.java,v 1.1 2004-06-09 09:47:47 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import java.io.StringWriter;
+import java.io.PrintWriter;
+
+/**
+ *  This component provides a viewer for dna or amino acid sequences.  The
+ *  units are numbered automatically, given a view like this:
+ *  <pre>
+ *  ATGATGATGATGATATGCATGATCG
+ *           10        20
+ *  </pre>
+ *  @author Kim Rutherford
+ *  @version $Id: SequenceViewer.java,v 1.1 2004-06-09 09:47:47 tjc Exp $
+ **/
+
+public class SequenceViewer extends FileViewer {
+  /**
+   *  Create a new SequenceViewer component.
+   *  @param title The name to attach to the new JFrame.
+   *  @param include_numbers If true then the sequence will be numbered
+   *    (every second line of the display will be numbers rather than
+   *    sequence).
+   **/
+  public SequenceViewer (final String title,
+                         final boolean include_numbers) {
+    super (title);
+
+    this.include_numbers = include_numbers;
+  }
+
+  /**
+   *  Set the sequence to display.
+   *  @param comment_line A comment to put on the first line.  This may be
+   *    null in which case there is no comment line.  This String should not
+   *    be "\n" terminated.
+   *  @param sequence The sequence to display.  This may be null in which case
+   *    there is no comment line.  This String should not be "\n" terminated.
+   **/
+  public void setSequence (final String comment_line, final String sequence) {
+    this.comment_line = comment_line;
+    this.sequence = sequence;
+
+    setView ();
+  }
+
+  /**
+   *  Set the sequence to display with no comment line.
+   *  @param sequence The sequence to display.  This may be null in which case
+   *    there is no comment line.  This String should not be "\n" terminated.
+   **/
+  public void setSequence (final String sequence) {
+    this.comment_line = null;
+    this.sequence = sequence;
+
+    setView ();
+  }
+
+  /**
+   *  Write the sequence and comment_line in the view.
+   **/
+  private void setView () {
+    final StringWriter string_writer = new StringWriter ();
+
+    final PrintWriter writer = new PrintWriter (string_writer);
+
+    if (comment_line != null) {
+      writer.println (comment_line);
+    }
+
+    String rest_of_sequence = sequence;
+
+    final int UNITS_PER_LINE = 60;
+
+    int line_count = 0;
+
+    while (rest_of_sequence != null) {
+      final String this_line;
+
+      if (rest_of_sequence.length () < UNITS_PER_LINE) {
+        this_line = rest_of_sequence;
+        rest_of_sequence = null;
+      } else {
+        this_line = rest_of_sequence.substring (0, UNITS_PER_LINE);
+        rest_of_sequence = rest_of_sequence.substring (UNITS_PER_LINE);
+      }
+
+
+      writer.println (this_line);
+
+      if (include_numbers) {
+        final int COUNT_INTERVAL = 10;
+
+        for (int i = 1 ;
+             i <= this_line.length () / COUNT_INTERVAL;
+             ++i) {
+          final int this_unit_count =
+            i * COUNT_INTERVAL + line_count * UNITS_PER_LINE;
+
+          final int number_of_spaces =
+            COUNT_INTERVAL - String.valueOf (this_unit_count).length ();
+
+          for (int space_index = 0 ;
+               space_index < number_of_spaces ;
+               ++space_index) {
+            writer.print (' ');
+          }
+
+          writer.print (this_unit_count);
+        }
+
+        writer.println ();
+      }
+
+      ++line_count;
+    }
+
+    writer.flush ();
+
+    setText (string_writer.toString ());
+  }
+
+  /**
+   *  The sequence to display.
+   **/
+  private String sequence = "";
+
+  /**
+   *  A comment to put on the first line.  If null don't display any comment.
+   **/
+  private String comment_line = null;
+
+  /**
+   *  If true then the amino acids will be numbered (every second line of the
+   *  display will be numbers rather than sequence).
+   **/
+  private final boolean include_numbers;
+}
+
+
diff --git a/uk/ac/sanger/artemis/components/Splash.java b/uk/ac/sanger/artemis/components/Splash.java
new file mode 100644
index 000000000..7e67f4194
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/Splash.java
@@ -0,0 +1,526 @@
+/* Splash.java
+ *
+ * created: Wed May 10 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2000,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/Splash.java,v 1.1 2004-06-09 09:47:48 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Options;
+import uk.ac.sanger.artemis.EntrySourceVector;
+import uk.ac.sanger.artemis.Logger;
+import uk.ac.sanger.artemis.util.InputStreamProgressListener;
+import uk.ac.sanger.artemis.util.InputStreamProgressEvent;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.border.Border;
+
+/**
+ *  Base class that creates a generic "Splash Screen"
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: Splash.java,v 1.1 2004-06-09 09:47:48 tjc Exp $
+ **/
+
+abstract public class Splash extends JFrame 
+{
+
+  /**
+   *  Do any necessary cleanup then exit.
+   **/
+  abstract protected void exit();
+
+  /**
+   *  A label for status and error messages.
+   **/
+  final private JLabel status_line = new JLabel("");
+
+  /**
+   *  The program name that was passed to the constructor.
+   **/
+  private String program_name;
+
+  /**
+   *  The program version that was passed to the constructor.
+   **/
+  private String program_version;
+
+  /**
+   *  The JComponent to draw the main splash screen into
+   **/
+  private JComponent helix_canvas;
+
+  /**
+   *  JMenu bar for the main window.
+   **/
+  private JMenuBar menu_bar;
+
+  protected JMenu file_menu;
+
+  protected JMenu options_menu;
+
+  /**
+   *  Create a new JFrame for a Splash screen.
+   *  @param program_name The full name of the program.
+   *  @param program_title The name to use in the title.
+   *  @param program_version The version string.
+   **/
+  public Splash(final String program_name,
+                final String program_title,
+                final String program_version) 
+  {
+    super(program_title + " " + program_version);
+
+    this.program_name    = program_name;
+    this.program_version = program_version;
+
+    addWindowListener(new WindowAdapter() 
+    {
+      public void windowClosing(WindowEvent event) 
+      {
+        exit();
+      }
+    });
+
+    final javax.swing.LookAndFeel look_and_feel =
+      javax.swing.UIManager.getLookAndFeel();
+
+    final javax.swing.plaf.FontUIResource font_ui_resource =
+      Options.getOptions().getFontUIResource();
+
+    java.util.Enumeration keys = UIManager.getDefaults().keys();
+    while(keys.hasMoreElements()) 
+    {
+      Object key = keys.nextElement();
+      Object value = UIManager.get(key);
+      if(value instanceof javax.swing.plaf.FontUIResource) 
+        UIManager.put(key, font_ui_resource);
+    }
+
+    getContentPane().setLayout(new BorderLayout());
+
+    makeAllMenus();
+
+    helix_canvas = makeHelixCanvas();
+
+    status_line.setFont(Options.getOptions().getFont());
+
+    final FontMetrics fm =
+      this.getFontMetrics(status_line.getFont());
+
+    final int font_height = fm.getHeight()+10;
+
+    status_line.setMinimumSize(new Dimension(100, font_height));
+    status_line.setPreferredSize(new Dimension(100, font_height));
+
+    Border loweredbevel = BorderFactory.createLoweredBevelBorder();
+    Border raisedbevel = BorderFactory.createRaisedBevelBorder();
+    Border compound = BorderFactory.createCompoundBorder(raisedbevel,loweredbevel);
+    status_line.setBorder(compound);
+
+    getContentPane().add(helix_canvas, "Center");
+    getContentPane().add(status_line, "South");
+
+    ClassLoader cl = this.getClass().getClassLoader();
+    ImageIcon icon = new ImageIcon(cl.getResource("images/icon.gif"));
+
+    if(icon != null) 
+    {
+      final Image icon_image = icon.getImage();
+      MediaTracker tracker = new MediaTracker(this);
+      tracker.addImage(icon_image, 0);
+
+      try
+      {
+        tracker.waitForAll();
+        setIconImage(icon_image);
+      }
+      catch(InterruptedException e) 
+      {
+        // ignore and continue
+      }
+    }
+
+    pack();
+
+    final int x = 460;
+    final int y = 250;
+
+    setSize(x, y);
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation(new Point((screen.width - getSize().width) / 2,
+                          (screen.height - getSize().height) / 2));
+  }
+
+  /**
+   *  Return a JComponent object that will display a helix and a short
+   *  copyright notice.
+   **/
+  private JComponent makeHelixCanvas() 
+  {
+    return new JPanel() {
+      public void update(Graphics g) 
+      {
+        paint(g);
+      }
+
+      // return the program name and the program mode in one String
+      private String getNameString() 
+      {
+        if(Options.getOptions().isEukaryoticMode())
+          return program_name + "  [Eukaryotic mode]";
+        else
+          return program_name + "  [Prokaryotic mode]";
+      }
+
+      /**
+       *  Draws the splash screen text.
+       **/
+      public int textPaint(final Graphics g)
+      {
+        FontMetrics fm = this.getFontMetrics(g.getFont());
+        final int font_height = fm.getHeight() + 3;
+        g.setColor(Color.black);
+        final int left_margin = 5;
+
+        g.drawString(program_name,
+                     helix_width + left_margin, font_height);
+        g.drawString(program_version,
+                      helix_width + left_margin, font_height * 2);
+        if(Options.getOptions().isEukaryoticMode()) 
+          g.drawString("[Eukaryotic mode]",
+                        helix_width + left_margin, font_height * 3);
+        else
+          g.drawString("[Prokaryotic mode]",
+                        helix_width + left_margin, font_height * 3);
+        g.drawString("Copyright 1998 - 2003",
+                      helix_width + left_margin, font_height * 9 / 2);
+        g.drawString("Genome Research Limited",
+                      helix_width + left_margin, font_height * 11 / 2);
+
+        return font_height;
+      }
+
+      public void paint(final Graphics g) 
+      {
+        final boolean simple_splash_screen =
+          Options.getOptions().getPropertyTruthValue("simple_splash_screen");
+
+        g.setColor(Color.white);
+
+        g.fillRect(0, 0, this.getSize().width, this.getSize().height);
+
+        if(simple_splash_screen) {
+          // java SIGILL bug work-around
+          textPaint(g);
+          return;
+        }
+
+        if(helix == null) 
+        {
+//        Toolkit toolkit = Toolkit.getDefaultToolkit();
+//        final URL helix_url = Splash.class.getResource("/uk.ac.sanger.artemis/helix.gif");
+//        helix = toolkit.getImage(helix_url);
+
+          ClassLoader cl = this.getClass().getClassLoader();
+          ImageIcon helix_icon = new ImageIcon(cl.getResource("images/helix.gif"));
+          helix = helix_icon.getImage();
+
+//        final URL sanger_url =
+//          Splash.class.getResource("/uk.ac.sanger.artemis/sanger-centre.gif");
+//        sanger = toolkit.getImage(sanger_url);
+          ImageIcon sanger_icon = new ImageIcon(cl.getResource("images/sanger-centre.gif"));
+          sanger = sanger_icon.getImage();
+
+          tracker = new MediaTracker(this);
+          tracker.addImage(helix, 0);
+          tracker.addImage(sanger, 1);
+
+          try 
+          {
+            tracker.waitForAll();
+            helix_height = helix.getHeight(this);
+            helix_width = helix.getWidth(this);
+            sanger_height = sanger.getHeight(this);
+          }
+          catch(InterruptedException e) 
+          {
+            return;
+          }
+        }
+
+        for(int i=0; i*helix_height<=this.getSize().height; ++i) 
+          g.drawImage(helix,
+                      0,
+                      i * helix_height, this);
+
+        final int font_height = textPaint(g);
+
+        int sanger_position = this.getSize().height - sanger_height;
+
+        if(sanger_position > font_height * 5.5) 
+          g.drawImage(sanger,
+                       helix_width + 5,
+                       sanger_position, this);
+      }
+
+      MediaTracker tracker = null;
+
+      /**
+       *  The image of the Sanger DNA logo.  This is set in paint().
+       **/
+      private Image helix = null;
+
+      /**
+       *  The image of the Sanger logo.  This is set in paint().
+       **/
+      private Image sanger = null;
+
+      /**
+       *  The height of the Sanger logo.  This is set in paint().
+       **/
+      private int sanger_height;
+
+      /**
+       *  The height of the Sanger DNA logo.  This is set in paint().
+       **/
+      private int helix_height;
+
+      /**
+       *  The width of the Sanger DNA logo.  This is set in paint().
+       **/
+      private int helix_width;
+    };
+  }
+
+  /**
+   *  Return the reference of the Label used as a status line.
+   **/
+  public JLabel getStatusLabel() 
+  {
+    return status_line;
+  }
+
+  /**
+   *  The possible sources for reading Entry objects.
+   **/
+  public EntrySourceVector getEntrySources(final JFrame frame) 
+  {
+    return Utilities.getEntrySources(frame, stream_progress_listener);
+  }
+
+  /**
+   *  Return an InputStreamProgressListener which updates the error label with
+   *  the current number of chars read while reading
+   **/
+  public InputStreamProgressListener getInputStreamProgressListener() 
+  {
+    return stream_progress_listener;
+  }
+
+  /**
+   *  Force the options files to be re-read and the EntryEdit components to be
+   *  redisplayed.
+   **/
+  private void resetOptions() 
+  {
+    Options.getOptions().reset();
+  }
+
+  /**
+   *  Make all the menus and menu items for the main window.  Also sets up
+   *  suitable ActionListener objects for each item.
+   */
+  private void makeAllMenus() 
+  {
+    menu_bar = new JMenuBar();
+    file_menu = new JMenu("File");
+    options_menu = new JMenu("Options");
+
+    menu_bar.add(file_menu);
+    menu_bar.add(options_menu);
+
+    setJMenuBar(menu_bar);
+
+    ActionListener menu_listener = null;
+
+    menu_listener = new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent event) 
+      {
+        resetOptions();
+      }
+    };
+    makeMenuItem(options_menu, "Re-read Options", menu_listener);
+
+    final JCheckBoxMenuItem enable_direct_edit_item =
+      new JCheckBoxMenuItem("Enable Direct Editing");
+    enable_direct_edit_item.setState(Options.getOptions().canDirectEdit());
+    enable_direct_edit_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        final boolean item_state = enable_direct_edit_item.getState();
+        Options.getOptions().setDirectEdit(item_state);
+      }
+    });
+    options_menu.add(enable_direct_edit_item);
+
+    final JCheckBoxMenuItem enable_euk_mode_item = new JCheckBoxMenuItem(
+                                                      "Eukaryotic Mode");
+    enable_euk_mode_item.setState(Options.getOptions().isEukaryoticMode());
+    enable_euk_mode_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        final boolean item_state = enable_euk_mode_item.getState();
+        Options.getOptions().setEukaryoticMode(item_state);
+
+        helix_canvas.repaint();
+      }
+    });
+    options_menu.add(enable_euk_mode_item);
+
+    final JCheckBoxMenuItem highlight_active_entry_item =
+      new JCheckBoxMenuItem("Highlight Active Entry");
+    final boolean highlight_active_entry_state =
+      Options.getOptions().highlightActiveEntryFlag();
+    highlight_active_entry_item.setState(highlight_active_entry_state);
+    highlight_active_entry_item.addItemListener(new ItemListener() 
+    {
+      public void itemStateChanged(ItemEvent event) 
+      {
+        final boolean item_state = highlight_active_entry_item.getState();
+        Options.getOptions().setHighlightActiveEntryFlag(item_state);
+      }
+    });
+    options_menu.add(highlight_active_entry_item);
+
+    if(Options.getOptions().getPropertyTruthValue("sanger_options") &&
+        Options.getOptions().getProperty("black_belt_mode") != null) 
+    {
+      final JCheckBoxMenuItem black_belt_mode_item =
+        new JCheckBoxMenuItem("Black Belt Mode");
+      final boolean state =
+        Options.getOptions().isBlackBeltMode();
+      black_belt_mode_item.setState(state);
+      black_belt_mode_item.addItemListener(new ItemListener() 
+      {
+        public void itemStateChanged(ItemEvent event) 
+        {
+          final boolean item_state = black_belt_mode_item.getState();
+          if(item_state) 
+            Options.getOptions().put("black_belt_mode", "true");
+          else 
+            Options.getOptions().put("black_belt_mode", "false");
+        }
+      });
+      options_menu.add(black_belt_mode_item);
+    }
+
+    if(Options.isUnixHost()) 
+    {
+      options_menu.addSeparator();
+
+      menu_listener = new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          showLog();
+        }
+      };
+      makeMenuItem(options_menu, "Show Log Window", menu_listener);
+
+      menu_listener = new ActionListener() 
+      {
+        public void actionPerformed(ActionEvent event) 
+        {
+          logger.setVisible(false);
+        }
+      };
+      makeMenuItem(options_menu, "Hide Log Window", menu_listener);
+    }
+  }
+
+  /**
+   *  Make a new menu item in the given menu, with its label given the
+   *  String and add the given ActionListener to it.
+   */
+  protected static void makeMenuItem(JMenu menu, String name,
+                                     ActionListener listener) 
+  {
+    JMenuItem new_item = new JMenuItem(name);
+    menu.add(new_item);
+    new_item.addActionListener(listener);
+    if(name.equals("Open ..."))
+      new_item.setAccelerator(KeyStroke.getKeyStroke
+             (KeyEvent.VK_O, InputEvent.CTRL_MASK));
+  }
+
+  /**
+   *  Return a Logger for warnings/errors/messages.
+   **/
+  public static Logger getLogger() 
+  {
+    return logger;
+  }
+
+  /**
+   *  Return the JComponent that the Splash screen is drawing on.
+   **/
+  public JComponent getCanvas() 
+  {
+    return helix_canvas;
+  }
+
+  /**
+   *  Make the LogViewer visible.
+   **/
+  public static void showLog() 
+  {
+    logger.setVisible(true);
+  }
+
+  /**
+   *  The Logger that is returned by getLogger().
+   **/
+  private final static LogViewer logger = new LogViewer();
+
+  /**
+   *  An InputStreamProgressListener used to update the error label with the
+   *  current number of chars read.
+   **/
+  private final InputStreamProgressListener stream_progress_listener =
+    new InputStreamProgressListener() {
+      public void progressMade(final InputStreamProgressEvent event) 
+      {
+        final int char_count = event.getCharCount();
+        if(char_count == -1) 
+          getStatusLabel().setText("");
+        else 
+          getStatusLabel().setText("chars read so far: " + char_count);
+      }
+    };
+}
diff --git a/uk/ac/sanger/artemis/components/StickyFileChooser.java b/uk/ac/sanger/artemis/components/StickyFileChooser.java
new file mode 100644
index 000000000..ed43f18a7
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/StickyFileChooser.java
@@ -0,0 +1,99 @@
+/* StickyFileChooser.java
+ *
+ * created: Mon Sep  1 2003
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright (C) 2003  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/StickyFileChooser.java,v 1.1 2004-06-09 09:47:49 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Options;
+
+import java.io.File;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.filechooser.FileSystemView;
+
+/**
+ *  A JFileChooser that remembers which directory it is in for next time.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: StickyFileChooser.java,v 1.1 2004-06-09 09:47:49 tjc Exp $
+ **/
+
+public class StickyFileChooser extends JFileChooser 
+{
+
+  /**
+   *  Used to remember the directory the JFileChooser was in when the user
+   *  pressed OK.  This is used as the starting directory next time.
+   **/
+  private static File last_directory = null;
+
+  /**
+   *  Create a new StickyFileChooser and set the current directory to what is
+   *  was after the last call to StickyFileChooser.showOpenDialog() or
+   *  StickyFileChooser.showSaveDialog().
+   **/
+  public StickyFileChooser() 
+  {
+    super();
+
+    if (last_directory == null) 
+    {
+      if (Options.getOptions ().getProperty ("default_directory") != null) 
+      {
+        final String default_directory =
+          Options.getOptions().getProperty("default_directory");
+        setCurrentDirectory (new File (default_directory));
+      } 
+      else 
+      {
+        if (last_directory == null) 
+          last_directory = new File (System.getProperty ("user.dir"));
+      }
+    }
+
+    setCurrentDirectory(last_directory);
+    setSize(620, 460);
+  }
+
+  /**
+   *  Calls super.showOpenDialog() then remembers the current directory.
+   **/
+  public int showOpenDialog(JFrame owner) 
+  {
+    int status = super.showOpenDialog(owner);
+    last_directory = getCurrentDirectory ();
+    return status;
+  }
+
+  /**
+   *  Calls super.showSaveDialog() then remembers the current directory.
+   **/
+  public int showSaveDialog(JFrame owner) 
+  {
+    int status = super.showSaveDialog (owner);
+    last_directory = getCurrentDirectory ();
+    return status;
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/SwingWorker.java b/uk/ac/sanger/artemis/components/SwingWorker.java
new file mode 100644
index 000000000..6d03d4dcb
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/SwingWorker.java
@@ -0,0 +1,150 @@
+/********************************************************************
+*
+*  This library is free software; you can redistribute it and/or
+*  modify it under the terms of the GNU Library General Public
+*  License as published by the Free Software Foundation; either
+*  version 2 of the License, or (at your option) any later version.
+* 
+*  This library 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
+*  Library General Public License for more details.
+* 
+*  You should have received a copy of the GNU Library General Public
+*  License along with this library; if not, write to the
+*  Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+*  Boston, MA  02111-1307, USA.
+********************************************************************/
+
+package uk.ac.sanger.artemis.components;
+
+import javax.swing.SwingUtilities;
+
+/**
+* This is the 3rd version of SwingWorker (also known as
+* SwingWorker 3), an abstract class that you subclass to
+* perform GUI-related work in a dedicated thread.  For
+* instructions on using this class, see:
+* 
+* http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html
+*
+* Note that the API changed slightly in the 3rd version:
+* You must now invoke start() on the SwingWorker after
+* creating it.
+*/
+public abstract class SwingWorker {
+    private Object value;  // see getValue(), setValue()
+    private Thread thread;
+
+    /** 
+    * Class to maintain reference to current worker thread
+    * under separate synchronization control.
+    */
+    private static class ThreadVar {
+        private Thread thread;
+        ThreadVar(Thread t) { thread = t; }
+        synchronized Thread get() { return thread; }
+        synchronized void clear() { thread = null; }
+    }
+
+    private ThreadVar threadVar;
+
+    /** 
+    * Get the value produced by the worker thread, or null if it 
+    * hasn't been constructed yet.
+    */
+    protected synchronized Object getValue() { 
+        return value; 
+    }
+
+    /** 
+    * Set the value produced by worker thread 
+    */
+    private synchronized void setValue(Object x) { 
+        value = x; 
+    }
+
+    /** 
+    * Compute the value to be returned by the <code>get</code> method. 
+    */
+    public abstract Object construct();
+
+    /**
+    * Called on the event dispatching thread (not on the worker thread)
+    * after the <code>construct</code> method has returned.
+    */
+    public void finished() {
+    }
+
+    /**
+    * A new method that interrupts the worker thread.  Call this method
+    * to force the worker to stop what it's doing.
+    */
+    public void interrupt() {
+        Thread t = threadVar.get();
+        if (t != null) {
+            t.interrupt();
+        }
+        threadVar.clear();
+    }
+
+    /**
+    * Return the value created by the <code>construct</code> method.  
+    * Returns null if either the constructing thread or the current
+    * thread was interrupted before a value was produced.
+    * 
+    * @return the value created by the <code>construct</code> method
+    */
+    public Object get() {
+        while (true) {  
+            Thread t = threadVar.get();
+            if (t == null) {
+                return getValue();
+            }
+            try {
+                t.join();
+            }
+            catch (InterruptedException e) {
+                Thread.currentThread().interrupt(); // propagate
+                return null;
+            }
+        }
+    }
+
+
+    /**
+    * Start a thread that will call the <code>construct</code> method
+    * and then exit.
+    */
+    public SwingWorker() {
+        final Runnable doFinished = new Runnable() {
+           public void run() { finished(); }
+        };
+
+        Runnable doConstruct = new Runnable() { 
+            public void run() {
+                try {
+                    setValue(construct());
+                }
+                finally {
+                    threadVar.clear();
+                }
+
+                SwingUtilities.invokeLater(doFinished);
+            }
+        };
+
+        Thread t = new Thread(doConstruct);
+        threadVar = new ThreadVar(t);
+    }
+
+    /**
+    * Start the worker thread.
+    */
+    public void start() {
+        Thread t = threadVar.get();
+        if (t != null) {
+            t.start();
+        }
+    }
+}
diff --git a/uk/ac/sanger/artemis/components/TaskViewerFrame.java b/uk/ac/sanger/artemis/components/TaskViewerFrame.java
new file mode 100644
index 000000000..ecf04bce2
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/TaskViewerFrame.java
@@ -0,0 +1,88 @@
+/* TaskViewerFrame.java
+ *
+ * created: Mon Aug 18 2003
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright(C) 2003  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/TaskViewerFrame.java,v 1.1 2004-06-09 09:47:51 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.jcon.gui.TaskViewer;
+
+import java.awt.*;
+import java.awt.event.*;
+import javax.swing.*;
+
+/**
+ *  TaskViewerFrame class
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: TaskViewerFrame.java,v 1.1 2004-06-09 09:47:51 tjc Exp $
+ **/
+
+public class TaskViewerFrame extends JFrame 
+{
+
+  /** TaskViewer that was created in the constructor. */
+  private TaskViewer tv = null;
+
+
+  /**
+   *  Create a new TaskViewerFrame to monitor the tasks with the given ids.
+   **/
+  public TaskViewerFrame(final int [] ids) 
+  {
+    try 
+    {
+      tv = new TaskViewer(ids);
+    }
+    catch(Exception e) 
+    {
+      dispose();
+      new MessageDialog(this, "Exception when viewing tasks: " + 
+                         e.getMessage());
+      return;
+    }
+
+    final JPanel panel = new JPanel();
+    getContentPane().add(panel);
+    panel.setLayout(new BorderLayout());
+    panel.add(tv, "Center");
+
+    final JButton close_button = new JButton("Close");
+    close_button.addActionListener(new ActionListener() 
+    {
+      public void actionPerformed(ActionEvent e) 
+      {
+        dispose();
+      }
+    });
+
+    panel.add(close_button, "South");
+    pack();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+
+    setLocation(new Point((screen.width - getSize().width) / 2,
+                           (screen.height - getSize().height) / 2));
+  }
+
+}
diff --git a/uk/ac/sanger/artemis/components/TextDialog.java b/uk/ac/sanger/artemis/components/TextDialog.java
new file mode 100644
index 000000000..f263cb425
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/TextDialog.java
@@ -0,0 +1,119 @@
+/* TextDialog.java
+ *
+ * created: Mon Jan 11 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/TextDialog.java,v 1.1 2004-06-09 09:47:52 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  A popup TextField JDialog with an OK and a Cancel button.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: TextDialog.java,v 1.1 2004-06-09 09:47:52 tjc Exp $
+ **/
+
+public class TextDialog extends JDialog {
+  /**
+   *  Create a new TextDialog component with the given prompt. Other
+   *  components can listen for TextDialogEvent object.
+   *  @param parent The parent window.
+   *  @param prompt A message that is displayed in the component beside the
+   *    TextArea that the user types into.  This String is also used as the
+   *    JFrame title.
+   *  @param width The width of the new TextField.
+   *  @param initial_text The initial text to put in the TextField.
+   **/
+  public TextDialog (final JFrame parent,
+                     final String prompt,
+                     final int width,
+                     final String initial_text) {
+    super (parent, prompt, true);
+
+    getContentPane ().add (new JLabel (prompt), "North");
+
+    final JPanel panel = new JPanel ();
+
+    panel.add (ok_button);
+    ok_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        text = text_field.getText ();
+        TextDialog.this.dispose ();
+      }
+    });
+
+    panel.add (cancel_button);
+    cancel_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        text = null;
+        TextDialog.this.dispose ();
+      }
+    });
+
+    getContentPane ().add (panel, "South");
+
+    text_field = new JTextField (initial_text, width);
+
+    text_field.addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        if (e.getKeyChar () == '\n') {
+          text = text_field.getText ();
+          TextDialog.this.dispose ();
+        }
+      }
+    });
+
+    getContentPane ().add (text_field, "Center");
+
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+  }
+
+  /**
+   *  Wait for a user action then return the text if the user hit OK or null
+   *  if the user hit Cancel.
+   **/
+  public String getText () {
+    setVisible (true);
+
+    return text;
+  }
+
+  private final JButton ok_button = new JButton ("OK");
+  private final JButton cancel_button = new JButton ("Cancel");
+  private JTextField text_field = null;
+
+  /**
+   *  Set to null if and only if the user cancels the dialog, otherwise
+   *  contains the text that the user entered.
+   **/
+  private String text = null;
+}
diff --git a/uk/ac/sanger/artemis/components/TextFieldSink.java b/uk/ac/sanger/artemis/components/TextFieldSink.java
new file mode 100644
index 000000000..61ba9d4b8
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/TextFieldSink.java
@@ -0,0 +1,142 @@
+/********************************************************************
+*
+*  This library is free software; you can redistribute it and/or
+*  modify it under the terms of the GNU Library General Public
+*  License as published by the Free Software Foundation; either
+*  version 2 of the License, or (at your option) any later version.
+*
+*  This library 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
+*  Library General Public License for more details.
+*
+*  You should have received a copy of the GNU Library General Public
+*  License along with this library; if not, write to the
+*  Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+*  Boston, MA  02111-1307, USA.
+*
+*  Copyright (C) Genome Research Limited
+*
+********************************************************************/
+
+package uk.ac.sanger.artemis.components;
+
+import java.awt.datatransfer.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import java.awt.event.*;
+import javax.swing.border.*;
+import java.awt.dnd.*;
+import java.awt.*;
+import java.io.*;
+
+
+/**
+*
+* Extends JTextField to enable pasting & drag and drop into it.
+*
+*/
+public class TextFieldSink extends JTextField implements DropTargetListener
+{
+
+  public TextFieldSink(String text, int columns)
+  {
+    super(text, columns);
+    //pasting
+    addMouseListener(new MouseAdapter()
+    {
+      public void mouseClicked(MouseEvent e)
+      {
+        if(e.getClickCount() == 2)
+        {
+          pasteText();
+          e.consume();
+        };
+      }
+    });
+
+    setDropTarget(new DropTarget(this,this));
+  }
+
+  public TextFieldSink()
+  {
+    this("",0);
+  }
+
+  public void pasteText()
+  {
+    Clipboard c = this.getToolkit().getSystemClipboard();
+   
+    Transferable t = c.getContents(this);
+    if(t==null)
+    {
+      this.getToolkit().beep();
+      return;
+    }
+    try
+    {
+      if(t.isDataFlavorSupported(DataFlavor.stringFlavor))
+      {
+        String s = (String) t.getTransferData(DataFlavor.stringFlavor);
+        this.replaceSelection(s);
+      }
+      else
+        this.getToolkit().beep();
+    }
+    catch (UnsupportedFlavorException ex) { this.getToolkit().beep(); }
+    catch (IOException ex) { this.getToolkit().beep(); }
+
+  }
+
+  protected static Border dropBorder = new BevelBorder(BevelBorder.LOWERED);
+  protected static Border endBorder =
+                               BorderFactory.createLineBorder(Color.black);
+
+  public void dragEnter(DropTargetDragEvent e)
+  {
+    if(e.isDataFlavorSupported(DataFlavor.stringFlavor))
+    {
+      e.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+      this.setBorder(dropBorder);
+    }
+  }
+
+  public void dragExit(DropTargetEvent e)
+  {
+    this.setBorder(null);
+  }
+
+  public void drop(DropTargetDropEvent e)
+  {
+    this.setBorder(endBorder);
+    Transferable t = e.getTransferable();
+    if(t.isDataFlavorSupported(DataFlavor.stringFlavor))
+    {
+      e.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
+      try
+      {
+        String dropS = (String)t.getTransferData(DataFlavor.stringFlavor);
+        this.replaceSelection(dropS);
+        e.dropComplete(true);
+      }
+      catch (Exception ex) {}
+    }
+    else
+    {
+      e.rejectDrop();
+      return;
+    }
+    return;
+  }
+
+  public void dragOver(DropTargetDragEvent e) 
+  {
+    if(e.isDataFlavorSupported(DataFlavor.stringFlavor))
+      e.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
+  }
+  public void dropActionChanged(DropTargetDragEvent e) {}
+}
+
+
+
+
diff --git a/uk/ac/sanger/artemis/components/TextRequester.java b/uk/ac/sanger/artemis/components/TextRequester.java
new file mode 100644
index 000000000..ddf3bc6e8
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/TextRequester.java
@@ -0,0 +1,158 @@
+/* TextRequester.java
+ *
+ * created: Mon Jan 11 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/TextRequester.java,v 1.1 2004-06-09 09:47:54 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Options;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.Vector;
+
+import javax.swing.*;
+
+/**
+ *  A popup JTextField with an OK and a Cancel button.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: TextRequester.java,v 1.1 2004-06-09 09:47:54 tjc Exp $
+ **/
+
+public class TextRequester extends JFrame {
+  /**
+   *  Create a new TextRequester component with the given prompt. Other
+   *  components can listen for TextRequesterEvent object.
+   *  @param prompt A message that is displayed in the component beside the
+   *    TextArea that the user types into.  This String is also used as the
+   *    JFrame title.
+   *  @param width The width of the JTextField in the new requester.
+   *  @param initial_text The initial text to put in the JTextField.
+   **/
+  public TextRequester (final String prompt,
+                        final int width,
+                        final String initial_text) {
+    super (prompt);
+
+    getContentPane ().add (new JLabel (prompt), "North");
+
+    final JPanel panel = new JPanel ();
+
+    panel.add (ok_button);
+    ok_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        performOK ();
+      }
+    });
+
+    panel.add (cancel_button);
+    cancel_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        performCancel ();
+      }
+    });
+
+    getContentPane ().add (panel, "South");
+
+    text_field = new JTextField (initial_text, width);
+
+    text_field.addKeyListener (new KeyAdapter () {
+      public void keyTyped(final KeyEvent e) {
+        if (e.getKeyChar () == '\n') {
+          performOK ();
+        }
+      }
+    });
+
+    getContentPane ().add (text_field, "Center");
+
+    pack ();
+
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+  }
+
+  /**
+   *  Add the given object as a listen for TextRequester events from this
+   *  TextRequester.
+   **/
+  public void addTextRequesterListener (final TextRequesterListener l) {
+    listeners.addElement (l);
+  }
+
+  /**
+   *  Return the text the is currently displayed in this requester.
+   **/
+  protected String getText () {
+    return text_field.getText ();
+  }
+
+  /**
+   *  Send a TextRequesterEvent of type OK to all the listeners.
+   **/
+  protected void performOK () {
+    final TextRequesterEvent new_event =
+      new TextRequesterEvent (this, getText (), TextRequesterEvent.OK);
+
+    sendEvent (new_event);
+
+    TextRequester.this.dispose ();
+  }
+
+  /**
+   *  Send a TextRequesterEvent of type CANCEL to all the listeners.
+   **/
+  protected void performCancel () {
+    final TextRequesterEvent new_event =
+      new TextRequesterEvent (this, getText (), TextRequesterEvent.CANCEL);
+
+    sendEvent (new_event);
+
+    TextRequester.this.dispose ();
+  }
+
+  /**
+   *  Send the given TextRequesterEvent to all the object that are listening
+   *  for the event.
+   **/
+  private void sendEvent (final TextRequesterEvent event) {
+    for (int i = 0 ; i < listeners.size () ; ++i) {
+      final TextRequesterListener listener =
+        ((TextRequesterListener) listeners.elementAt (i));
+
+      listener.actionPerformed (event);
+    }
+  }
+
+  private final JButton ok_button = new JButton ("OK");
+  private final JButton cancel_button = new JButton ("Cancel");
+  private JTextField text_field = null;
+
+  /**
+   *  This contains the objects that are listening for TextRequester events
+   *  from this TextRequester.
+   **/
+  private Vector listeners = new Vector ();
+}
diff --git a/uk/ac/sanger/artemis/components/TextRequesterEvent.java b/uk/ac/sanger/artemis/components/TextRequesterEvent.java
new file mode 100644
index 000000000..b1e3951d0
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/TextRequesterEvent.java
@@ -0,0 +1,87 @@
+/* TextRequesterEvent.java
+ *
+ * created: Wed Jan 13 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/TextRequesterEvent.java,v 1.1 2004-06-09 09:47:55 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  This event is sent when the user presses OK or Cancel in a TextRequester
+ *  component.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: TextRequesterEvent.java,v 1.1 2004-06-09 09:47:55 tjc Exp $
+ **/
+
+public class TextRequesterEvent extends java.util.EventObject {
+  /**
+   *  Event type - This event was generated by pressing the OK button.
+   **/
+  final public static int OK = 1;
+
+  /**
+   *  Event type - This event was generated by pressing the CANCEL button.
+   **/
+  final public static int CANCEL = 2;
+
+  /**
+   *  Create a new TextRequesterEvent object.
+   *  @param source The TextRequester that generated the event.
+   *  @param requester_text The contents of the TextField in the TextRequester.
+   *  @param type The type of event.
+   **/
+  public TextRequesterEvent (final TextRequester source,
+                             final String requester_text,
+                             final int type) {
+    super (source);
+    this.requester_text = requester_text;
+    this.type           = type;
+  }
+
+  /**
+   *  Return the type of this event as passed to the constructor.
+   **/
+  public int getType () {
+    return type;
+  }
+
+  /**
+   *  Return the TextRequester contents String that was passed to the
+   *  constructor.
+   **/
+  public String getRequesterText () {
+    return requester_text;
+  }
+
+  /**
+   *  This is the type of this event, as passed to the constructor
+   **/
+  private int type;
+
+  /**
+   *  The TextRequester contents String that was passed to the constructor.
+   **/
+  private String requester_text;
+}
+
+
diff --git a/uk/ac/sanger/artemis/components/TextRequesterListener.java b/uk/ac/sanger/artemis/components/TextRequesterListener.java
new file mode 100644
index 000000000..0b70582f9
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/TextRequesterListener.java
@@ -0,0 +1,44 @@
+/* TextRequesterListener.java
+ *
+ * created: Sun Jan 17 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/TextRequesterListener.java,v 1.1 2004-06-09 09:47:56 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+/**
+ *  This interface is implemented by those classes that need to listen for
+ *  TextRequesterEvents.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: TextRequesterListener.java,v 1.1 2004-06-09 09:47:56 tjc Exp $
+ **/
+
+public interface TextRequesterListener {
+  /**
+   *  Invoked when the user presses the OK or Cancel button on a TextRequester
+   *  component.
+   **/
+  void actionPerformed (final TextRequesterEvent event);
+}
+
+
diff --git a/uk/ac/sanger/artemis/components/Utilities.java b/uk/ac/sanger/artemis/components/Utilities.java
new file mode 100644
index 000000000..b5d90710c
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/Utilities.java
@@ -0,0 +1,144 @@
+/* Utilities.java
+ *
+ * created: Tue Sep 18 2001
+ *
+ * This file is part of Artemis
+ * 
+ * Copyright(C) 2001  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/Utilities.java,v 1.1 2004-06-09 09:47:57 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.util.InputStreamProgressListener;
+import uk.ac.sanger.artemis.EntrySourceVector;
+import uk.ac.sanger.artemis.Options;
+
+import java.awt.*;
+import java.net.MalformedURLException;
+import javax.swing.*;
+
+/**
+ *  Utilities methods used by the uk.ac.sanger.artemis.components package.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: Utilities.java,v 1.1 2004-06-09 09:47:57 tjc Exp $
+ **/
+
+public class Utilities 
+{
+  /**
+   *  Return the JFrame that contains the given component.
+   **/
+  public static JFrame getComponentFrame(final JComponent component) 
+  {
+    if(component.getTopLevelAncestor() instanceof JFrame) 
+      return(JFrame) component.getTopLevelAncestor();
+    else 
+      return null;
+  }
+
+  /**
+   *  Move the given JFrame to the centre of the screen.
+   **/
+  public static void centreFrame(final JFrame frame) 
+  {
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+
+    final int x_position =(screen.width - frame.getSize().width) / 2;
+    int y_position =(screen.height - frame.getSize().height) / 2;
+
+    if(y_position < 10) 
+      y_position = 10;
+
+    frame.setLocation(new Point(x_position, y_position));
+  }
+
+  /**
+   *  Find the parent Frame of the given component and re-centre it on the
+   *  screen.
+   **/
+  public static void centreOwningFrame(final JComponent component) 
+  {
+    final JFrame frame = getComponentFrame(component);
+    centreFrame(frame);
+  }
+
+  /**
+   *  Returns a Vector containing the possible Entry sources for this
+   *  application.
+   *  @param frame The component that is creating the EntrySource objects.
+   *   (Used for requesters.)
+   *  @param listener InputStreamProgressEvent objects will be sent to this
+   *    listener as progress on reading is made.
+   **/
+  public static EntrySourceVector getEntrySources(final JFrame frame,
+                     final InputStreamProgressListener listener) 
+  {
+    final EntrySourceVector return_vector = new EntrySourceVector();
+    
+    return_vector.add(new FileDialogEntrySource(frame, listener));
+    return_vector.add(new DbfetchEntrySource(frame));
+
+    // return_vector.add(new BioJavaEntrySource());
+    
+    // this doesn't work on a v1.2 system so it is taken out with perl when
+    // necessary 
+    // CORBA_START_MARKER
+
+    // The location of the IOR for the corba server at EMBL.  Can be
+    // overridden using the options file. 
+    final String embl_ior_url =
+      Options.getOptions().getProperty("embl_ior_url");
+
+    if(embl_ior_url != null) 
+    {
+      try 
+      {
+        return_vector.add(new EMBLCorbaEntrySource(frame, embl_ior_url));
+      } 
+      catch(MalformedURLException e) 
+      {
+        new MessageDialog(frame, "the url given for the embl database is " +
+                          "badly formatted: " + e.getMessage());
+      }
+    }
+
+    // The location of the IOR for the pathogens group read-write corba
+    // server. 
+    final String db_ior_url =
+      Options.getOptions().getProperty("db_ior_url");
+    
+    if(db_ior_url != null) 
+    {
+      try 
+      {
+        return_vector.add(new WritableEMBLCorbaEntrySource(frame,
+                                                           db_ior_url));
+      }
+      catch(MalformedURLException e)
+      {
+        new MessageDialog(frame, "the url given for the embl database is " +
+                           "badly formatted: " + e.getMessage());
+      }
+    }
+    // CORBA_END_MARKER
+
+    return return_vector;
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/ViewMenu.java b/uk/ac/sanger/artemis/components/ViewMenu.java
new file mode 100644
index 000000000..f39c5c918
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/ViewMenu.java
@@ -0,0 +1,1430 @@
+/* ViewMenu.java
+ *
+ * created: Tue Dec 29 1998
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 1998,1999,2000,2002  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/ViewMenu.java,v 1.1 2004-06-09 09:47:58 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+import uk.ac.sanger.artemis.plot.*;
+
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.io.InvalidRelationException;
+import uk.ac.sanger.artemis.io.Key;
+import uk.ac.sanger.artemis.io.InvalidKeyException;
+import uk.ac.sanger.artemis.io.Range;
+
+import java.io.*;
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A popup menu with viewing commands.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: ViewMenu.java,v 1.1 2004-06-09 09:47:58 tjc Exp $
+ **/
+
+public class ViewMenu extends SelectionMenu {
+  /**
+   *  Create a new ViewMenu object.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param entry_group The EntryGroup object where new features/entries will
+   *    be added.
+   *  @param goto_event_source The object that we will call makeBaseVisible ()
+   *    on.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   *  @param menu_name The name of the new menu.
+   **/
+  public ViewMenu (final JFrame frame,
+                   final Selection selection,
+                   final GotoEventSource goto_event_source,
+                   final EntryGroup entry_group,
+                   final BasePlotGroup base_plot_group,
+                   final String menu_name) {
+    super (frame, menu_name, selection);
+
+    this.entry_group = entry_group;
+    this.selection = selection;
+    this.goto_event_source = goto_event_source;
+
+    this.base_plot_group = base_plot_group;
+
+    plot_features_item = new JMenuItem ("Show Feature Plots");
+    plot_features_item.setAccelerator (PLOT_FEATURES_KEY);
+    plot_features_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        plotSelectedFeatures (getParentFrame (), getSelection ());
+      }
+    });
+
+    view_feature_item = new JMenuItem ("View Selected Features");
+    view_feature_item.setAccelerator (VIEW_FEATURES_KEY);
+    view_feature_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        viewSelectedFeatures (getParentFrame (), getSelection ());
+      }
+    });
+
+    view_selection_item = new JMenuItem ("View Selection");
+    view_selection_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        new SelectionViewer (getSelection (), entry_group);
+      }
+    });
+
+    feature_info_item = new JMenuItem ("Show Feature Statistics");
+    feature_info_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        viewSelectedFeatureInfo ();
+      }
+    });
+
+    view_bases_item = new JMenuItem ("View Bases Of Selection");
+    view_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        viewSelectedBases (true);
+      }
+    });
+
+    view_bases_as_fasta_item =
+      new JMenuItem ("View Bases Of Selection As FASTA");
+    view_bases_as_fasta_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        viewSelectedBases (false);
+      }
+    });
+
+    view_aa_item = new JMenuItem ("View Amino Acids Of Selection");
+    view_aa_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        viewSelectedAminoAcids (true);
+      }
+    });
+
+    view_aa_as_fasta_item =
+      new JMenuItem ("View Amino Acids Of Selection As FASTA");
+    view_aa_as_fasta_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        viewSelectedAminoAcids (false);
+      }
+    });
+
+    overview_item = new JMenuItem ("Show Overview");
+    overview_item.setAccelerator (OVERVIEW_KEY);
+    overview_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        new EntryGroupInfoDisplay (getParentFrame (), entry_group);
+      }
+    });
+
+    forward_overview_item = new JMenuItem ("Show Forward Strand Overview");
+    forward_overview_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        new EntryGroupInfoDisplay (getParentFrame (), entry_group,
+                                   EntryGroupInfoDisplay.FORWARD);
+      }
+    });
+
+    reverse_overview_item = new JMenuItem ("Show Reverse Strand Overview");
+    reverse_overview_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        new EntryGroupInfoDisplay (getParentFrame (), entry_group,
+                                   EntryGroupInfoDisplay.REVERSE);
+      }
+    });
+
+    view_cds_item = new JMenuItem ("Show CDS Genes And Products");
+    view_cds_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        final FeaturePredicate feature_predicate =
+          new FeatureKeyPredicate (Key.CDS);
+
+        final String filter_name =
+          "CDS features (filtered from: " +
+          getParentFrame ().getTitle () + ")";
+
+        final FilteredEntryGroup filtered_entry_group =
+          new FilteredEntryGroup (entry_group, feature_predicate, filter_name);
+
+        final FeatureListFrame feature_list_frame =
+          new FeatureListFrame (filter_name,
+                                selection, goto_event_source,
+                                filtered_entry_group,
+                                base_plot_group);
+
+        feature_list_frame.getFeatureList ().setShowGenes (true);
+        feature_list_frame.getFeatureList ().setShowProducts (true);
+
+        feature_list_frame.setVisible (true);
+      }
+    });
+
+    JMenu search_results_menu = null;
+
+    search_results_menu = new JMenu ("Search Results");
+
+    final boolean sanger_options =
+      Options.getOptions ().getPropertyTruthValue ("sanger_options");
+
+    final ExternalProgramVector external_programs =
+      Options.getOptions ().getExternalPrograms ();
+
+    final StringVector external_program_names = new StringVector ();
+
+    for (int i = 0 ; i < external_programs.size () ; ++i) {
+      final ExternalProgram external_program =
+        external_programs.elementAt (i);
+
+      final String new_name = external_program.getName ();
+
+      if (!external_program_names.contains (new_name)) {
+        external_program_names.add (new_name);
+      }
+    }
+
+    for (int i = 0 ; i < external_program_names.size () ; ++i) {
+      final String external_program_name =
+        external_program_names.elementAt (i);
+
+      final JMenuItem new_menu =
+        makeSearchResultsMenu (external_program_name, false, sanger_options);
+      search_results_menu.add (new_menu);
+    }
+
+    if (sanger_options) {
+      search_results_menu.addSeparator ();
+
+      for (int i = 0 ; i < external_program_names.size () ; ++i) {
+        final String external_program_name =
+          external_program_names.elementAt (i);
+
+        final JMenuItem new_menu =
+          makeSearchResultsMenu (external_program_name, true, sanger_options);
+        search_results_menu.add (new_menu);
+      }
+    }
+
+    final int MAX_FILTER_FEATURE_COUNT = 10000;
+
+    final JMenu feature_filters_menu = new JMenu ("Feature Filters");
+
+    final JMenuItem bad_start_codons_item =
+      new JMenuItem ("Suspicious Start Codons ...");
+    bad_start_codons_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        if (checkEntryGroupSize (MAX_FILTER_FEATURE_COUNT)) {
+          showBadStartCodons (getParentFrame (),
+                              selection, entry_group, goto_event_source,
+                              base_plot_group);
+        }
+      }
+    });
+
+    final JMenuItem bad_stop_codons_item =
+      new JMenuItem ("Suspicious Stop Codons ...");
+    bad_stop_codons_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        if (checkEntryGroupSize (MAX_FILTER_FEATURE_COUNT)) {
+          showBadStopCodons (getParentFrame (),
+                             selection, entry_group, goto_event_source,
+                             base_plot_group);
+        }
+      }
+    });
+
+    final JMenuItem stop_codons_in_translation =
+      new JMenuItem ("Stop Codons In Translation ...");
+    stop_codons_in_translation.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        if (checkEntryGroupSize (MAX_FILTER_FEATURE_COUNT)) {
+          showStopsInTranslation (getParentFrame (),
+                                  selection, entry_group, goto_event_source,
+                                  base_plot_group);
+        }
+      }
+    });
+
+    final JMenuItem bad_feature_keys_item =
+      new JMenuItem ("Non EMBL Keys ...");
+    bad_feature_keys_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        if (checkEntryGroupSize (MAX_FILTER_FEATURE_COUNT)) {
+          showNonEMBLKeys (getParentFrame (),
+                           selection, entry_group, goto_event_source,
+                           base_plot_group);
+        }
+      }
+    });
+
+    final JMenuItem duplicated_keys_item =
+      new JMenuItem ("Duplicated Features ...");
+    duplicated_keys_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        if (checkEntryGroupSize (MAX_FILTER_FEATURE_COUNT)) {
+          showDuplicatedFeatures (getParentFrame (),
+                                  selection, entry_group, goto_event_source,
+                                  base_plot_group);
+        }
+      }
+    });
+
+    final JMenuItem overlapping_cds_features_item =
+      new JMenuItem ("Overlapping CDS Features ...");
+    overlapping_cds_features_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        if (checkEntryGroupSize (MAX_FILTER_FEATURE_COUNT)) {
+          showOverlappingCDSs (getParentFrame (),
+                               selection, entry_group, goto_event_source,
+                               base_plot_group);
+        }
+      }
+    });
+
+    final JMenuItem same_stop_cds_features_item =
+      new JMenuItem ("CDSs Sharing Stop Codons ...");
+    same_stop_cds_features_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        if (checkEntryGroupSize (MAX_FILTER_FEATURE_COUNT)) {
+          showFeaturesWithSameStopCodons (getParentFrame (),
+                                          selection, entry_group,
+                                          goto_event_source,
+                                          base_plot_group);
+        }
+      }
+    });
+
+    final JMenuItem missing_qualifier_features_item =
+      new JMenuItem ("Features Missing Required Qualifiers ...");
+    missing_qualifier_features_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        if (checkEntryGroupSize (MAX_FILTER_FEATURE_COUNT)) {
+          showMissingQualifierFeatures (getParentFrame (),
+                                        selection,
+                                        entry_group, goto_event_source,
+                                        base_plot_group);
+        }
+      }
+    });
+
+    final JMenuItem filter_by_key_item =
+      new JMenuItem ("Filter By Key ...");
+    filter_by_key_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        if (checkEntryGroupSize (MAX_FILTER_FEATURE_COUNT)) {
+          showFilterByKey (getParentFrame (),
+                           selection, entry_group, goto_event_source,
+                           base_plot_group);
+        }
+      }
+    });
+
+    final JMenuItem filter_by_selection_item =
+      new JMenuItem ("Selected Features ...");
+    filter_by_selection_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        showFilterBySelection (getParentFrame (),
+                               selection, entry_group, goto_event_source,
+                               base_plot_group);
+      }
+    });
+
+    feature_filters_menu.add (bad_start_codons_item);
+    feature_filters_menu.add (bad_stop_codons_item);
+    feature_filters_menu.add (stop_codons_in_translation);
+    feature_filters_menu.add (bad_feature_keys_item);
+    feature_filters_menu.add (duplicated_keys_item);
+    feature_filters_menu.add (overlapping_cds_features_item);
+    feature_filters_menu.add (same_stop_cds_features_item);
+    feature_filters_menu.add (missing_qualifier_features_item);
+    feature_filters_menu.addSeparator ();
+    feature_filters_menu.add (filter_by_key_item);
+    feature_filters_menu.add (filter_by_selection_item);
+
+    add (view_feature_item);
+    add (view_selection_item);
+    addSeparator ();
+    if (search_results_menu != null) {
+      add (search_results_menu);
+    }
+    add (view_cds_item);
+    add (feature_filters_menu);
+    addSeparator ();
+    add (overview_item);
+    add (forward_overview_item);
+    add (reverse_overview_item);
+    addSeparator ();
+    add (view_bases_item);
+    add (view_bases_as_fasta_item);
+    add (view_aa_item);
+    add (view_aa_as_fasta_item);
+    addSeparator ();
+    add (feature_info_item);
+    add (plot_features_item);
+  }
+
+  /**
+   *  Create a new ViewMenu object.
+   *  @param entry_edit The EntryEdit that owns this JMenu.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param entry_group The EntryGroup object where new features/entries will
+   *    be added.
+   *  @param goto_event_source The object that we will call makeBaseVisible ()
+   *    on.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public ViewMenu (final JFrame frame,
+                   final Selection selection,
+                   final GotoEventSource goto_event_source,
+                   final EntryGroup entry_group,
+                   final BasePlotGroup base_plot_group) {
+    this (frame, selection, goto_event_source, entry_group,
+          base_plot_group, "View");
+  }
+
+  /**
+   *  The shortcut for Show Feature Plots.
+   **/
+  final static KeyStroke PLOT_FEATURES_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_W, InputEvent.CTRL_MASK);
+
+  final static public int PLOT_FEATURES_KEY_CODE = KeyEvent.VK_W;
+
+  /**
+   *  The shortcut for View Selected Features.
+   **/
+  final static KeyStroke VIEW_FEATURES_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_V, InputEvent.CTRL_MASK);
+
+  final static public int VIEW_FEATURES_KEY_CODE = KeyEvent.VK_V;
+
+  /**
+   *  The shortcut for Show Overview.
+   **/
+  final static KeyStroke OVERVIEW_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_O, InputEvent.CTRL_MASK);
+
+  final static public int OVERVIEW_KEY_CODE = KeyEvent.VK_O;
+
+  /**
+   *  The shortcut for View FASTA in browser.
+   **/
+  final static KeyStroke FASTA_IN_BROWSER_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_F, InputEvent.CTRL_MASK);
+
+  final static public int FASTA_IN_BROWSER_KEY_CODE = KeyEvent.VK_F;
+
+  /**
+   *  The shortcut for View FASTA.
+   **/
+  final static KeyStroke VIEW_FASTA_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_R, InputEvent.CTRL_MASK);
+
+  final static public int VIEW_FASTA_KEY_CODE = KeyEvent.VK_R;
+
+  /**
+   *  The shortcut for View BLASTP in browser.
+   **/
+  final static KeyStroke BLASTP_IN_BROWSER_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_B, InputEvent.CTRL_MASK);
+
+  final static public int BLASTP_IN_BROWSER_KEY_CODE = KeyEvent.VK_B;
+
+  /**
+   *  The shortcut for View BLASTP.
+   **/
+  final static KeyStroke VIEW_BLASTP_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_BACK_QUOTE , InputEvent.CTRL_MASK);
+
+  final static public int VIEW_BLASTP_KEY_CODE = KeyEvent.VK_BACK_QUOTE;
+
+  /**
+   *  The shortcut for View HTH.
+   **/
+  final static KeyStroke VIEW_HTH_KEY =
+    KeyStroke.getKeyStroke (KeyEvent.VK_H, InputEvent.CTRL_MASK);
+
+  final static public int VIEW_HTH_KEY_CODE = KeyEvent.VK_H;
+
+  /**
+   *  Make a JMenuItem for viewing the results of running the given program.
+   *  @param send_to_browser if true the results should be sent straight to
+   *    the web browser rather than using a SearchResultViewer object.
+   *  @param sanger_options true if the sanger_options is set to true in the
+   *    options file.
+   **/
+  private JMenuItem makeSearchResultsMenu (final String program_name,
+                                          final boolean send_to_browser,
+                                          final boolean sanger_options) {
+    final String new_menu_name;
+
+    if (send_to_browser) {
+      new_menu_name = program_name + " results (in browser)";
+    } else {
+      new_menu_name = program_name + " results";
+    }
+
+    final JMenuItem new_menu;
+
+    if ((sanger_options && send_to_browser || !sanger_options)
+        && program_name.equals ("fasta")) {
+      new_menu = new JMenuItem (new_menu_name);
+      new_menu.setAccelerator (FASTA_IN_BROWSER_KEY);
+    } else {
+      if ((sanger_options && send_to_browser || !sanger_options)
+          && program_name.equals ("blastp")) {
+        new_menu = new JMenuItem (new_menu_name);
+        new_menu.setAccelerator (BLASTP_IN_BROWSER_KEY);
+      } else {
+        if (program_name.equals ("fasta")) {
+          new_menu = new JMenuItem (new_menu_name);
+          new_menu.setAccelerator (VIEW_FASTA_KEY);
+        } else {
+          if (program_name.equals ("blastp")) {
+            new_menu = new JMenuItem (new_menu_name);
+            new_menu.setAccelerator (VIEW_BLASTP_KEY);
+          } else {
+            if (program_name.equals ("hth")) {
+              new_menu = new JMenuItem (new_menu_name);
+              new_menu.setAccelerator (VIEW_HTH_KEY);
+            } else {
+              new_menu = new JMenuItem (new_menu_name);
+            }
+          }
+        }
+      }
+    }
+
+    new_menu.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        viewExternalResults (getParentFrame (), getSelection (),
+                             program_name, send_to_browser);
+      }
+    });
+
+    return new_menu;
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing the non-pseudo CDS features that
+   *  have invalid start codons.
+   *  @param parent_frame The parent JFrame.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showBadStartCodons (final JFrame parent_frame,
+                                         final Selection selection,
+                                         final EntryGroup entry_group,
+                                         final GotoEventSource goto_source,
+                                         final BasePlotGroup
+                                           base_plot_group) {
+    final FeatureKeyQualifierPredicate cds_predicate =
+      new FeatureKeyQualifierPredicate (Key.CDS, "pseudo", false);
+
+    final FeaturePredicate feature_predicate =
+      new FeaturePredicate () {
+        public boolean testPredicate (final Feature feature) {
+          if (!cds_predicate.testPredicate (feature)) {
+            return false;
+          }
+
+          if (feature.hasValidStartCodon ()) {
+            return false;
+          } else {
+            return true;
+          }
+        }
+      };
+
+    final String filter_name =
+      "CDS features with suspicious start codons (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group, feature_predicate, filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing the non-pseudo CDS features that
+   *  have invalid stop codons.
+   *  @param parent_frame The parent JFrame.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showBadStopCodons (final JFrame parent_frame,
+                                        final Selection selection,
+                                        final EntryGroup entry_group,
+                                        final GotoEventSource goto_source,
+                                        final BasePlotGroup
+                                          base_plot_group) {
+    final FeatureKeyQualifierPredicate cds_predicate =
+      new FeatureKeyQualifierPredicate (Key.CDS, "pseudo", false);
+
+    final FeaturePredicate feature_predicate =
+      new FeaturePredicate () {
+        public boolean testPredicate (final Feature feature) {
+          if (!cds_predicate.testPredicate (feature)) {
+            return false;
+          }
+
+          if (feature.hasValidStopCodon ()) {
+            return false;
+          } else {
+            return true;
+          }
+        }
+      };
+
+    final String filter_name =
+      "CDS features with suspicious stop codons (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group, feature_predicate, filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing the non-pseudo CDS features that
+   *  contain a stop codon in the translation.
+   *  @param parent_frame The parent Frame.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this Menu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showStopsInTranslation (final Frame parent_frame,
+                                             final Selection selection,
+                                             final EntryGroup entry_group,
+                                             final GotoEventSource goto_source,
+                                             final BasePlotGroup
+                                               base_plot_group) {
+    final FeatureKeyQualifierPredicate cds_predicate =
+      new FeatureKeyQualifierPredicate (Key.CDS, "pseudo", false);
+
+    final FeaturePredicate feature_predicate =
+      new FeaturePredicate () {
+        public boolean testPredicate (final Feature feature) {
+          if (!cds_predicate.testPredicate (feature)) {
+            return false;
+          }
+
+          final AminoAcidSequence amino_acids = feature.getTranslation ();
+
+          if (amino_acids.containsStopCodon ()) {
+            return true;
+          } else {
+            return false;
+          }
+        }
+      };
+
+    final String filter_name =
+      "CDS features with stop codon(s) in translation (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group, feature_predicate, filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing the features that have non-EMBL keys.
+   *  @param parent_frame The parent JFrame.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showNonEMBLKeys (final JFrame parent_frame,
+                                      final Selection selection,
+                                      final EntryGroup entry_group,
+                                      final GotoEventSource goto_source,
+                                      final BasePlotGroup
+                                        base_plot_group) {
+    final FeaturePredicate feature_predicate =
+      new FeaturePredicate () {
+        public boolean testPredicate (final Feature feature) {
+          if (feature.hasValidEMBLKey ()) {
+            return false;
+          } else {
+            return true;
+          }
+        }
+      };
+
+    final String filter_name =
+      "features with a non-EMBL key (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group, feature_predicate, filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+
+  /**
+   *  Popup a FeatureListFrame containing the features that have the same key
+   *  and location as another features (ie. duplicates).
+   *  @param parent_frame The parent JFrame.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showDuplicatedFeatures (final JFrame parent_frame,
+                                             final Selection selection,
+                                             final EntryGroup entry_group,
+                                             final GotoEventSource goto_source,
+                                             final BasePlotGroup
+                                               base_plot_group) {
+    final FeaturePredicate feature_predicate =
+      new FeaturePredicate () {
+        public boolean testPredicate (final Feature feature) {
+          final Entry feature_entry = feature.getEntry ();
+
+          final int feature_index = feature_entry.indexOf (feature);
+
+          if (feature_index + 1 == feature_entry.getFeatureCount ()) {
+            // last in the Entry
+            return false;
+          }
+
+          final Feature next_feature =
+            feature_entry.getFeature (feature_index + 1);
+
+          if (feature.getKey ().equals (next_feature.getKey ()) &&
+              feature.getLocation ().equals (next_feature.getLocation ())) {
+            return true;
+          } else {
+            return false;
+          }
+        }
+      };
+
+    final String filter_name =
+      "duplicated Features (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group, feature_predicate, filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing those CDS features that overlap with
+   *  the next feature.
+   *  @param parent_frame The parent JFrame.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showOverlappingCDSs (final JFrame parent_frame,
+                                          final Selection selection,
+                                          final EntryGroup entry_group,
+                                          final GotoEventSource goto_source,
+                                          final BasePlotGroup
+                                            base_plot_group) {
+    final FeatureKeyPredicate cds_predicate =
+      new FeatureKeyPredicate (Key.CDS);
+
+    final FeaturePredicate feature_predicate =
+      new FeaturePredicate () {
+        public boolean testPredicate (final Feature test_feature) {
+          if (!cds_predicate.testPredicate (test_feature)) {
+            return false;
+          }
+
+          final Range feature_range = test_feature.getMaxRawRange ();
+
+          final FeatureVector overlapping_features;
+
+          try {
+            overlapping_features =
+              entry_group.getFeaturesInRange (feature_range);
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          }
+
+          for (int i = 0 ; i < overlapping_features.size () ; ++i) {
+            final Feature current_feature = overlapping_features.elementAt (i);
+
+            if (current_feature != test_feature &&
+                cds_predicate.testPredicate (current_feature)) {
+              return true;
+            }
+          }
+
+          return false;
+        }
+      };
+
+    final String filter_name =
+      "overlapping CDS features (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group, feature_predicate, filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing those CDS features that overlap with
+   *  the next feature.
+   *  @param parent_frame The parent JFrame.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void
+    showFeaturesWithSameStopCodons (final JFrame parent_frame,
+                                    final Selection selection,
+                                    final EntryGroup entry_group,
+                                    final GotoEventSource goto_source,
+                                    final BasePlotGroup base_plot_group) {
+    final FeatureKeyPredicate cds_predicate =
+      new FeatureKeyPredicate (Key.CDS);
+
+    final FeaturePredicate feature_predicate =
+      new FeaturePredicate () {
+        public boolean testPredicate (final Feature test_feature) {
+          if (!cds_predicate.testPredicate (test_feature)) {
+            return false;
+          }
+
+          final Range feature_range = test_feature.getMaxRawRange ();
+
+          final FeatureVector overlapping_features;
+
+          try {
+            overlapping_features =
+              entry_group.getFeaturesInRange (feature_range);
+          } catch (OutOfRangeException e) {
+            throw new Error ("internal error - unexpected exception: " + e);
+          }
+
+          for (int i = 0 ; i < overlapping_features.size () ; ++i) {
+            final Feature current_feature = overlapping_features.elementAt (i);
+
+            if (current_feature == test_feature) {
+              continue;
+            }
+
+            if (current_feature != test_feature &&
+                cds_predicate.testPredicate (current_feature) &&
+                (current_feature.getLastBase () ==
+                 test_feature.getLastBase ()) &&
+                (current_feature.getSegments ().lastElement ().getFrameID () ==
+                 test_feature.getSegments ().lastElement ().getFrameID ())) {
+              return true;
+            }
+          }
+
+          return false;
+        }
+      };
+
+    final String filter_name =
+      "CDS features with the same stop codon as another (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group, feature_predicate, filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing the features that are missing
+   *  required EMBL qualifiers.
+   *  @param parent_frame The parent JFrame.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showMissingQualifierFeatures (final JFrame parent_frame,
+                                                   final Selection selection,
+                                                   final EntryGroup
+                                                     entry_group,
+                                                   final GotoEventSource
+                                                     goto_source,
+                                                   final BasePlotGroup
+                                                     base_plot_group) {
+    final FeaturePredicate feature_predicate =
+      new FeaturePredicate () {
+        public boolean testPredicate (final Feature feature) {
+          if (feature.hasRequiredQualifiers ()) {
+            return false;
+          } else {
+            return true;
+          }
+        }
+      };
+
+    final String filter_name =
+      "features that are missing a required EMBL " +
+      "qualifier (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group, feature_predicate, filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing only those features that have the
+   *  key choosen by the user.
+   *  @param parent_frame The parent JFrame.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showFilterByKey (final JFrame parent_frame,
+                                      final Selection selection,
+                                      final EntryGroup entry_group,
+                                      final GotoEventSource goto_source,
+                                      final BasePlotGroup base_plot_group) {
+    final KeyChooser key_chooser;
+
+    key_chooser = new KeyChooser (Options.getArtemisEntryInformation (),
+                                  new Key ("misc_feature"));
+
+    key_chooser.getKeyChoice ().addItemListener (new ItemListener () {
+      public void itemStateChanged (ItemEvent e) {
+        if (e.getStateChange () == ItemEvent.SELECTED) {
+          showFilterByKeyHelper (parent_frame,
+                                 key_chooser.getKeyChoice ().getSelectedItem (),
+                                 selection, entry_group, goto_source,
+                                 base_plot_group);
+          key_chooser.setVisible (false);
+          key_chooser.dispose ();
+        }
+      }
+    });
+
+    key_chooser.getOKButton ().addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent _) {
+        showFilterByKeyHelper (parent_frame,
+                               key_chooser.getKeyChoice ().getSelectedItem (),
+                               selection, entry_group, goto_source,
+                               base_plot_group);
+        key_chooser.setVisible (false);
+        key_chooser.dispose ();
+      }
+    });
+
+    key_chooser.setVisible (true);
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing only those features that have the
+   *  key choosen by the user.
+   *  @param parent_frame The parent JFrame.
+   *  @param key The key to use in the filter.
+   *  @param selection The Selection to pass to the FeatureList.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showFilterByKeyHelper (final JFrame parent_frame,
+                                            final Key key,
+                                            final Selection selection,
+                                            final EntryGroup entry_group,
+                                            final GotoEventSource
+                                              goto_source,
+                                            final BasePlotGroup
+                                              base_plot_group) {
+    final String filter_name =
+      "features with key: " + key + " (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group,
+                              new FeatureKeyPredicate (key), filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+  /**
+   *  Popup a FeatureListFrame containing only those features that are
+   *  currently in the Selection.
+   *  @param parent_frame The parent JFrame.
+   *  @param selection The Selection to pass to the FeatureList and to use to
+   *    filter the entry_group.
+   *  @param entry_group The EntryGroup to pass to the FilteredEntryGroup.
+   *  @param goto_source The GotoEventSource to pass to the FeatureList.
+   *  @param base_plot_group The BasePlotGroup associated with this JMenu -
+   *    needed to call getCodonUsageAlgorithm()
+   **/
+  public static void showFilterBySelection (final JFrame parent_frame,
+                                            final Selection selection,
+                                            final EntryGroup entry_group,
+                                            final GotoEventSource
+                                              goto_source,
+                                            final BasePlotGroup
+                                              base_plot_group) {
+    final FeaturePredicate predicate =
+      new FeatureFromVectorPredicate (selection.getAllFeatures ());
+
+    final String filter_name =
+      "features from the selection (filtered from: " +
+      parent_frame.getTitle () + ")";
+
+    final FilteredEntryGroup filtered_entry_group =
+      new FilteredEntryGroup (entry_group, predicate, filter_name);
+
+    final FeatureListFrame feature_list_frame =
+      new FeatureListFrame (filter_name,
+                            selection, goto_source, filtered_entry_group,
+                            base_plot_group);
+
+    feature_list_frame.setVisible (true);
+  }
+
+  /**
+   *  viewSelectedFeatures(), plotSelectedFeatures() etc. will only show this
+   *  many features.
+   **/
+  private static final int MAXIMUM_SELECTED_FEATURES = 25;
+
+  /**
+   *  Open a view window for each of the selected features.  The viewer will
+   *  listen for feature change events and update itself.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The Selection containing the features to merge.
+   **/
+  static void viewSelectedFeatures (final JFrame frame,
+                                    final Selection selection) {
+    final FeatureVector features_to_view = selection.getAllFeatures ();
+
+    if (features_to_view.size () > MAXIMUM_SELECTED_FEATURES) {
+      new MessageDialog (frame, "warning: only viewing the first " +
+                         MAXIMUM_SELECTED_FEATURES + " selected features");
+    }
+
+    for (int i = 0 ;
+         i < features_to_view.size () && i < MAXIMUM_SELECTED_FEATURES ;
+         ++i) {
+      final Feature selection_feature = features_to_view.elementAt (i);
+
+      new FeatureViewer (selection_feature);
+    }
+  }
+
+  /**
+   *  Open an plot viewing window for each of the selected features.  The plot
+   *  viewer will listen for feature change events and update itself.
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The Selection containing the features to plot.
+   **/
+  static void plotSelectedFeatures (final JFrame frame,
+                                    final Selection selection) {
+    final FeatureVector features_to_plot = selection.getAllFeatures ();
+
+    if (features_to_plot.size () > MAXIMUM_SELECTED_FEATURES) {
+      new MessageDialog (frame, "warning: only showing plots for the first " +
+                         MAXIMUM_SELECTED_FEATURES + " selected features");
+    }
+
+    for (int i = 0 ;
+         i < features_to_plot.size () && i < MAXIMUM_SELECTED_FEATURES ;
+         ++i) {
+      final Feature selection_feature = features_to_plot.elementAt (i);
+
+      new FeaturePlotGroup (selection_feature);
+    }
+  }
+
+  /**
+   *  Show the output file from an external program (like fasta) for the
+   *  selected Feature objects.  The name of the file to read is stored in a
+   *  feature qualifier.  The qualifier used is the program name plus "_file".
+   *  @param frame The JFrame to use for MessageDialog components.
+   *  @param selection The Selection containing the features to merge.
+   *  @param send_to_browser if true the results should be sent straight to
+   *    the web browser rather than using a SearchResultViewer object.
+   **/
+  static void viewExternalResults (final JFrame frame,
+                                   final Selection selection,
+                                   final String program_name,
+                                   final boolean send_to_browser) {
+    final FeatureVector features_to_view = selection.getAllFeatures ();
+
+    if (features_to_view.size () > MAXIMUM_SELECTED_FEATURES) {
+      new MessageDialog (frame, "warning: only viewing results from " +
+                         "the first " + MAXIMUM_SELECTED_FEATURES +
+                         " selected features");
+    }
+
+    for (int i = 0 ;
+         i < features_to_view.size () && i < MAXIMUM_SELECTED_FEATURES ;
+         ++i) {
+      final Feature this_feature = features_to_view.elementAt (i);
+
+      String qualifier_value;
+
+      try {
+        qualifier_value =
+          this_feature.getValueOfQualifier (program_name + "_file");
+      } catch (InvalidRelationException e) {
+        qualifier_value = null;
+      }
+
+      String file_name = qualifier_value;
+
+      if (file_name == null || file_name.length () == 0) {
+        new MessageDialog (frame,
+                           "Message",
+                           "No " + program_name + " results for " +
+                           this_feature.getIDString ());
+
+        continue;
+      }
+
+      // remove the program name string (if any) from the qualifier, so that
+      // we can try to read the file with and without the prefix.
+      if (file_name.startsWith (program_name + File.separatorChar)) {
+        file_name = file_name.substring (program_name.length () + 1);
+      }
+
+      Document root_document = this_feature.getEntry ().getRootDocument ();
+
+      if (root_document == null) {
+        root_document = new FileDocument (new File ("."));
+      }
+
+      try {
+        Document document = null;
+
+        // try the current directory with the program name added:
+        // ./fasta/abc.seq.00001.out
+        final File dir_name = new File (program_name);
+
+        final Document [] possible_documents =
+          new Document [] {
+            root_document.append (program_name).append (file_name),
+            root_document.append (file_name),
+            new FileDocument (new File (file_name)),
+            new FileDocument (dir_name).append (file_name),
+          };
+
+        for (int possible_document_index = 0 ;
+             possible_document_index < possible_documents.length ;
+             ++possible_document_index) {
+
+          final Document this_document =
+            possible_documents[possible_document_index];
+
+          if (this_document.readable ()) {
+            document = this_document;
+            break;
+          } else {
+            final File gzip_file =
+              new File (this_document.toString () + ".gz");
+            final Document gzip_document =
+              new FileDocument (gzip_file);
+
+            if (gzip_document.readable ()) {
+              document = gzip_document;
+              break;
+            }
+          }
+        }
+
+        if (document == null) {
+          final String message_string =
+            "No " + program_name + " results for " +
+            this_feature.getIDString () + " (file not found: " +
+            qualifier_value + ")";
+
+          new MessageDialog (frame, message_string);
+
+          continue;
+        }
+
+        if (send_to_browser) {
+          SearchResultViewer.sendToBrowser (document.toString ());
+        } else {
+          new SearchResultViewer (program_name + " results for " +
+                                  this_feature.getIDString () + " from " +
+                                  document,
+                                  document);
+        }
+      } catch (ExternalProgramException e) {
+        new MessageDialog (frame,
+                           "error while open results file: " + e);
+      } catch (IOException e) {
+        new MessageDialog (frame,
+                           "error while open results file: " + e);
+      }
+    }
+  }
+
+  /**
+   *  Open a FeatureInfo component for each of the selected features.  The
+   *  new component will listen for feature change events and update itself.
+   **/
+  private void viewSelectedFeatureInfo () {
+    final FeatureVector features_to_view = getSelection ().getAllFeatures ();
+
+    if (features_to_view.size () > MAXIMUM_SELECTED_FEATURES) {
+      new MessageDialog (getParentFrame (),
+                         "warning: only viewing the statistics for " +
+                         "the first " + MAXIMUM_SELECTED_FEATURES +
+                         " selected features");
+    }
+
+    for (int i = 0 ;
+         i < features_to_view.size () && i < MAXIMUM_SELECTED_FEATURES ;
+         ++i) {
+      final Feature selection_feature = features_to_view.elementAt (i);
+
+      new FeatureInfo (selection_feature,
+                       base_plot_group.getCodonUsageAlgorithm ());
+    }
+  }
+
+  /**
+   *  View the bases of the selected features.  This creates a
+   *  FeatureBaseViewer for each feature.
+   *  @param include_numbers If true then the sequence will be numbered
+   *    (every second line of the display will be numbers rather than
+   *    sequence).
+   **/
+  private void viewSelectedBases (final boolean include_numbers) {
+    if (getSelection ().isEmpty ()) {
+      new MessageDialog (getParentFrame (), "Nothing selected");
+      return;
+    }
+
+    final MarkerRange range = selection.getMarkerRange ();
+
+    if (range == null) {
+      final FeatureVector features_to_view = getSelection ().getAllFeatures ();
+
+      if (features_to_view.size () > MAXIMUM_SELECTED_FEATURES) {
+        new MessageDialog (getParentFrame (),
+                           "waning: only viewing bases for " +
+                           "the first " + MAXIMUM_SELECTED_FEATURES +
+                           " selected features");
+      }
+
+      for (int i = 0 ;
+           i < features_to_view.size () && i < MAXIMUM_SELECTED_FEATURES ;
+           ++i) {
+        final Feature this_feature = features_to_view.elementAt (i);
+
+        new FeatureBaseViewer (this_feature, include_numbers);
+      }
+    } else {
+      final SequenceViewer sequence_viewer =
+        new SequenceViewer ("Selected bases", include_numbers);
+
+      final String bases = getSelection ().getSelectionText ();
+      sequence_viewer.setSequence (null, bases);
+    }
+  }
+
+  /**
+   *  View the bases of the selected CDS features.  This creates a
+   *  FeatureBaseViewer for each CDS feature.
+   *  @param include_numbers If true then the amino acids will be numbered
+   *    (every second line of the display will be numbers rather than
+   *    sequence).
+   **/
+  private void viewSelectedAminoAcids (final boolean include_numbers) {
+    if (getSelection ().isEmpty ()) {
+      new MessageDialog (getParentFrame (), "Nothing selected");
+      return;
+    }
+
+    final MarkerRange range = selection.getMarkerRange ();
+
+    if (range == null) {
+      final FeatureVector features_to_view = getSelection ().getAllFeatures ();
+
+      if (features_to_view.size () > MAXIMUM_SELECTED_FEATURES) {
+        new MessageDialog (getParentFrame (),
+                           "warning: only viewing amino acids for " +
+                           "the first " + MAXIMUM_SELECTED_FEATURES +
+                           " selected features");
+      }
+
+      for (int i = 0 ;
+           i < features_to_view.size () && i < MAXIMUM_SELECTED_FEATURES ;
+           ++i) {
+        final Feature this_feature = features_to_view.elementAt (i);
+
+        new FeatureAminoAcidViewer (this_feature, include_numbers);
+      }
+    } else {
+      final SequenceViewer sequence_viewer =
+        new SequenceViewer ("Selected bases (translated)", include_numbers);
+
+      final String bases = getSelection ().getSelectionText ();
+
+      final AminoAcidSequence amino_acids =
+        AminoAcidSequence.getTranslation (bases, true);
+
+      sequence_viewer.setSequence (null, amino_acids.toString ());
+    }
+  }
+
+  /**
+   *  Check that the number of features in the EntryGroup is less than the
+   *  given number.  If it is less return true.  If not then popup a
+   *  YesNoDialog asking for confirmation and return true if and only if the
+   *  user chooses yes.
+   **/
+  private boolean checkEntryGroupSize (final int max_size) {
+    final int feature_count = getEntryGroup ().getAllFeaturesCount ();
+
+    if (feature_count < max_size) {
+      return true;
+    } else {
+      final YesNoDialog dialog =
+        new YesNoDialog (getParentFrame (),
+                         "there are " + feature_count + " features in the " +
+                         "active entries - continue?");
+
+      return dialog.getResult ();
+    }
+  }
+
+  /**
+   *  Return the EntryGroup that was passed to the constructor.
+   **/
+  private EntryGroup getEntryGroup () {
+    return entry_group;
+  }
+
+  private JMenuItem feature_info_item = null;
+  private JMenuItem plot_features_item = null;
+  private JMenuItem view_feature_item = null;
+  private JMenuItem view_selection_item = null;
+  private JMenuItem view_fasta_item = null;
+  private JMenuItem view_bases_item = null;
+  private JMenuItem view_bases_as_fasta_item = null;
+  private JMenuItem view_aa_item = null;
+  private JMenuItem view_aa_as_fasta_item = null;
+  private JMenuItem overview_item = null;
+  private JMenuItem forward_overview_item = null;
+  private JMenuItem reverse_overview_item = null;
+  private JMenuItem view_cds_item = null;
+
+  /**
+   *  The EntryGroup that was passed to the constructor.
+   **/
+  private EntryGroup entry_group = null;
+
+  /**
+   *  The Selection that was passed to the constructor.
+   **/
+  private Selection selection = null;
+
+  /**
+   *  The GotoEventSource that was passed to the constructor.
+   **/
+  private GotoEventSource goto_event_source = null;
+
+  private BasePlotGroup base_plot_group;
+}
diff --git a/uk/ac/sanger/artemis/components/WritableEMBLCorbaEntrySource.java b/uk/ac/sanger/artemis/components/WritableEMBLCorbaEntrySource.java
new file mode 100644
index 000000000..87ff712d3
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/WritableEMBLCorbaEntrySource.java
@@ -0,0 +1,226 @@
+/* WritableEMBLCorbaEntrySource.java
+ *
+ * created: Mon Jun 12 2000
+ *
+ * This file is part of Artemis
+ *
+ * Copyright (C) 2000,2002x  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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/WritableEMBLCorbaEntrySource.java,v 1.1 2004-06-09 09:47:59 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.util.*;
+import uk.ac.sanger.artemis.io.LocationParseException;
+import uk.ac.sanger.artemis.io.InvalidKeyException;
+
+import nsdb.ServerInfo;
+import nsdb.EntryStats;
+import nsdb.EmblSeq;
+import nsdb.EmblPackage.Superceded;
+import type.NoResult;
+
+import java.io.*;
+import java.awt.*;
+import java.net.*;
+
+import javax.swing.*;
+
+/**
+ *  This is an EntrySource that can get writable Entry objects from an EMBL
+ *  CORBA server.
+ *
+ *  @author Kim Rutherford <kmr@sanger.ac.uk>
+ *  @version $Id: WritableEMBLCorbaEntrySource.java,v 1.1 2004-06-09 09:47:59 tjc Exp $
+ **/
+
+public class WritableEMBLCorbaEntrySource extends EMBLCorbaEntrySource {
+  /**
+   *  Create a new WritableEMBLCorbaEntrySource from the given String.
+   *  @param frame The component that created this EntrySource.  (Used for
+   *    requesters.)
+   *  @param ior_url_string A String containing the URL of the IOR for the
+   *    server.
+   **/
+  public WritableEMBLCorbaEntrySource (final JFrame frame,
+                                       final String ior_url_string)
+      throws MalformedURLException {
+    super (frame, ior_url_string);
+  }
+
+  /**
+   *  Get an Entry object from the Ensembl CORBA server.
+   *  @param bases The Bases object to pass to the Entry constructor.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final Bases bases, final boolean show_progress)
+      throws OutOfRangeException, IOException {
+    return makeCorbaDialog (bases, false);
+  }
+
+  /**
+   *  Get an Entry object from the Ensembl CORBA server.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   *  @exception NoSequenceException Thrown if the entry that we read has no
+   *    sequence.
+   *  @param show_progress If true a InputStreamProgressDialog will be shown
+   *    while loading.
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   **/
+  public Entry getEntry (final boolean show_progress)
+      throws OutOfRangeException, NoSequenceException, IOException {
+    return makeCorbaDialog (null, false);
+  }
+
+  /**
+   *  Create a TextRequester, wait for the user to type an accession number
+   *  and then read that entry from the EMBL CORBA server.
+   *  @param bases If this is null a new Bases object will be created for the
+   *    Entry once it has been read from the server.  If not null then it will
+   *    be passed to the Entry constructor.
+   *  @param read_only true if and only if a read-only Entry should be created
+   *    (some are always read only).
+   *  @return null if and only if the user cancels the read or if the read
+   *    fails.
+   *  @exception OutOfRangeException Thrown if one of the features in
+   *    the Entry is out of range of the Bases object.
+   **/
+  protected Entry makeCorbaDialog (final Bases bases,
+                                   final boolean read_only)
+      throws OutOfRangeException, IOException {
+    final org.omg.CORBA.ORB orb =
+      org.omg.CORBA.ORB.init (new String [0], new java.util.Properties());
+
+    final nsdb.Embl corba_handle = getServerHandle ();
+
+    final nsdb.EmblWriter embl_writer =
+      nsdb.EmblWriterHelper.narrow (corba_handle);
+
+    if (embl_writer == null) {
+      final String message = "Server reference is not an EmblWriter: " +
+        corba_handle;
+      new MessageDialog (getFrame (), message);
+      return null;
+    }
+
+    final ListDialog list_dialog =
+      new ListDialog (getFrame (), "Select an entry");
+
+    final ServerInfo stats = embl_writer.getServerInfo ();
+
+    final EntryStats [] entry_stats_list = stats.entry_stats_list;
+    final EntryStats [] file_stats_list = stats.file_stats_list;
+
+    list_dialog.getList ().setModel (new AbstractListModel () {
+      public int getSize () {
+        return entry_stats_list.length + file_stats_list.length;
+      }
+      public Object getElementAt (int i) {
+        if (i < entry_stats_list.length) {
+          return makeListString (entry_stats_list[i], false);
+        } else {
+          return makeListString (file_stats_list[i-entry_stats_list.length],
+                                 true);
+        }
+      }
+    });
+
+    // returns when user hits ok or cancel
+    final String selected_entry_string =
+      (String) list_dialog.getSelectedValue ();
+
+    if (selected_entry_string == null) {
+      // user hit cancel
+      return null;
+    }
+
+    final int end_of_entry_name = selected_entry_string.indexOf ("   ");
+
+    final String corba_id =
+      selected_entry_string.substring (0, end_of_entry_name);
+
+    if (corba_id.length () > 0) {
+      final MessageDialog message_frame =
+        new MessageDialog (getFrame (),
+                           "reading entry - please wait", false);
+
+      try {
+        return makeFromCorbaID (bases, corba_id, read_only);
+      } finally {
+        message_frame.dispose ();
+      }
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   *  Make a ListDialog then let the user choose an entry.
+   **/
+  private String makeListString (final EntryStats this_entry_stats,
+                                 final boolean is_file_entry) {
+    final StringBuffer buffer = new StringBuffer ();
+
+    buffer.append (this_entry_stats.name).append ("   ");
+
+    for (int j = 0 ; j < (30 - this_entry_stats.name.length ()) ; ++j) {
+      buffer.append (" ");
+    }
+
+    final long last_change_time = this_entry_stats.last_change_time;
+
+    if (is_file_entry) {
+      buffer.append ("(file)");
+    } else {
+      if (last_change_time > 0) {
+        buffer.append (new java.util.Date (last_change_time * 1000L));
+      } else {
+        buffer.append ("(not saved)");
+      }
+    }
+
+    return buffer.toString ();
+  }
+
+  /**
+   *  Return the name of this source (for display to the user in menus).
+   **/
+  public String getSourceName () {
+    return "Database";
+  }
+
+  /**
+   *  Returns true if and only if this EntrySource always returns "full"
+   *  entries.  ie. entries that contain features and sequence.  Entries that
+   *  are read from a file may contain just features so in this class this
+   *  method returns false.
+   **/
+  public boolean isFullEntrySource () {
+    return false;
+  }
+}
diff --git a/uk/ac/sanger/artemis/components/WriteMenu.java b/uk/ac/sanger/artemis/components/WriteMenu.java
new file mode 100644
index 000000000..32c5a7402
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/WriteMenu.java
@@ -0,0 +1,852 @@
+/* WriteMenu.java
+ *
+ * created: Mon Jan 11 1999
+ *
+ * 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/WriteMenu.java,v 1.1 2004-06-09 09:48:00 tjc Exp $
+ **/
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.*;
+import uk.ac.sanger.artemis.sequence.*;
+
+import uk.ac.sanger.artemis.io.Sequence;
+import uk.ac.sanger.artemis.io.StreamSequence;
+import uk.ac.sanger.artemis.io.FastaStreamSequence;
+import uk.ac.sanger.artemis.io.RawStreamSequence;
+import uk.ac.sanger.artemis.io.EmblStreamSequence;
+import uk.ac.sanger.artemis.io.GenbankStreamSequence;
+import uk.ac.sanger.artemis.io.StreamSequenceFactory;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+
+import javax.swing.*;
+
+/**
+ *  A menu of commands for writing out protein and bases.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: WriteMenu.java,v 1.1 2004-06-09 09:48:00 tjc Exp $
+ **/
+
+public class WriteMenu extends SelectionMenu {
+  /**
+   *  Create a new WriteMenu component.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param entry_group The EntryGroup object to use when writing a sequence.
+   *  @param menu_name The name of the new menu.
+   **/
+  public WriteMenu (final JFrame frame,
+                    final Selection selection,
+                    final EntryGroup entry_group,
+                    final String menu_name) {
+    super (frame, menu_name, selection);
+
+    this.entry_group = entry_group;
+
+    final JMenuItem aa_item = new JMenuItem ("Amino Acids Of Selected Features");
+    aa_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeAminoAcids ();
+      }
+    });
+
+    add (aa_item);
+
+    final JMenuItem pir_item =
+      new JMenuItem ("PIR Database Of Selected Features");
+    pir_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writePIRDataBase ();
+      }
+    });
+
+    add (pir_item);
+
+    addSeparator ();
+
+    final JMenu bases_menu = new JMenu ("Bases Of Selection");
+
+    final JMenuItem raw_bases_item = new JMenuItem ("Raw Format");
+    raw_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeBasesOfSelection (StreamSequenceFactory.RAW_FORMAT);
+      }
+    });
+
+    bases_menu.add (raw_bases_item);
+
+    final JMenuItem fasta_bases_item = new JMenuItem ("FASTA Format");
+    fasta_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeBasesOfSelection (StreamSequenceFactory.FASTA_FORMAT);
+      }
+    });
+
+    bases_menu.add (fasta_bases_item);
+
+    final JMenuItem embl_bases_item = new JMenuItem ("EMBL Format");
+    embl_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeBasesOfSelection (StreamSequenceFactory.EMBL_FORMAT);
+      }
+    });
+
+    bases_menu.add (embl_bases_item);
+
+    final JMenuItem genbank_bases_item = new JMenuItem ("Genbank Format");
+    genbank_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeBasesOfSelection (StreamSequenceFactory.GENBANK_FORMAT);
+      }
+    });
+
+    bases_menu.add (genbank_bases_item);
+
+    add (bases_menu);
+
+    final JMenu upstream_bases_menu =
+      new JMenu ("Upstream Bases Of Selected Features");
+
+    final JMenuItem raw_upstream_bases_item = new JMenuItem ("Raw Format");
+    raw_upstream_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeUpstreamBases (StreamSequenceFactory.RAW_FORMAT);
+      }
+    });
+
+    upstream_bases_menu.add (raw_upstream_bases_item);
+
+    final JMenuItem fasta_upstream_bases_item = new JMenuItem ("FASTA Format");
+    fasta_upstream_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeUpstreamBases (StreamSequenceFactory.FASTA_FORMAT);
+      }
+    });
+
+    upstream_bases_menu.add (fasta_upstream_bases_item);
+
+    final JMenuItem embl_upstream_bases_item = new JMenuItem ("EMBL Format");
+    embl_upstream_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeUpstreamBases (StreamSequenceFactory.EMBL_FORMAT);
+      }
+    });
+
+    upstream_bases_menu.add (embl_upstream_bases_item);
+
+    final JMenuItem genbank_upstream_bases_item = new JMenuItem ("Genbank Format");
+    genbank_upstream_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeUpstreamBases (StreamSequenceFactory.GENBANK_FORMAT);
+      }
+    });
+
+    upstream_bases_menu.add (genbank_upstream_bases_item);
+
+    add (upstream_bases_menu);
+
+    final JMenu downstream_bases_menu =
+      new JMenu ("Downstream Bases Of Selected Features");
+
+    final JMenuItem raw_downstream_bases_item = new JMenuItem ("Raw Format");
+    raw_downstream_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeDownstreamBases (StreamSequenceFactory.RAW_FORMAT);
+      }
+    });
+
+    downstream_bases_menu.add (raw_downstream_bases_item);
+
+    final JMenuItem fasta_downstream_bases_item = new JMenuItem ("FASTA Format");
+    fasta_downstream_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeDownstreamBases (StreamSequenceFactory.FASTA_FORMAT);
+      }
+    });
+
+    downstream_bases_menu.add (fasta_downstream_bases_item);
+
+    final JMenuItem embl_downstream_bases_item = new JMenuItem ("EMBL Format");
+    embl_downstream_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeDownstreamBases (StreamSequenceFactory.EMBL_FORMAT);
+      }
+    });
+
+    downstream_bases_menu.add (embl_downstream_bases_item);
+
+    final JMenuItem genbank_downstream_bases_item =
+      new JMenuItem ("Genbank Format");
+    genbank_downstream_bases_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeDownstreamBases (StreamSequenceFactory.GENBANK_FORMAT);
+      }
+    });
+
+    downstream_bases_menu.add (genbank_downstream_bases_item);
+
+    add (downstream_bases_menu);
+
+    addSeparator ();
+
+    final JMenu write_all_bases_menu = new JMenu ("Write All Bases");
+
+    final JMenuItem write_raw_item = new JMenuItem ("Raw Format");
+    write_raw_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeAllSequence (StreamSequenceFactory.RAW_FORMAT);
+      }
+    });
+
+    write_all_bases_menu.add (write_raw_item);
+
+    final JMenuItem write_fasta_item = new JMenuItem ("FASTA Format");
+    write_fasta_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeAllSequence (StreamSequenceFactory.FASTA_FORMAT);
+      }
+    });
+
+    write_all_bases_menu.add (write_fasta_item);
+
+    final JMenuItem write_embl_item = new JMenuItem ("EMBL Format");
+    write_embl_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeAllSequence (StreamSequenceFactory.EMBL_FORMAT);
+      }
+    });
+
+    write_all_bases_menu.add (write_embl_item);
+
+    final JMenuItem write_genbank_item = new JMenuItem ("Genbank Format");
+    write_genbank_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeAllSequence (StreamSequenceFactory.GENBANK_FORMAT);
+      }
+    });
+
+    write_all_bases_menu.add (write_genbank_item);
+
+    add (write_all_bases_menu);
+
+    addSeparator ();
+
+    final JMenuItem codon_usage_item =
+      new JMenuItem ("Write Codon Usage of Selected Features");
+    codon_usage_item.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent event) {
+        writeCodonUsage ();
+      }
+    });
+
+    add (codon_usage_item);
+  }
+
+  /**
+   *  Create a new WriteMenu component.
+   *  @param frame The JFrame that owns this JMenu.
+   *  @param selection The Selection that the commands in the menu will
+   *    operate on.
+   *  @param entry_group The EntryGroup object to use when writing a sequence.
+   **/
+  public WriteMenu (final JFrame frame,
+                    final Selection selection,
+                    final EntryGroup entry_group) {
+    this (frame, selection, entry_group, "Write");
+  }
+  
+  /**
+   *  Write a PIR database of the selected features to a file choosen by the
+   *  user.
+   **/
+  private void writePIRDataBase () {
+    if (! checkForSelectionFeatures ()) {
+      return;
+    }
+
+    final File write_file =
+      getWriteFile ("Select a PIR output file name ...", "cosmid.pir");
+
+    if (write_file == null) {
+      return;
+    }
+
+    try {
+      final FileWriter writer = new FileWriter (write_file);
+
+      final FeatureVector features_to_write =
+        getSelection ().getAllFeatures ();
+
+      for (int i = 0 ; i < features_to_write.size () ; ++i) {
+        final Feature selection_feature = features_to_write.elementAt (i);
+
+        selection_feature.writePIROfFeature (writer);
+      }
+
+      writer.close ();
+    } catch (IOException e) {
+      new MessageDialog (getParentFrame (),
+                         "error while writing: " + e.getMessage ());
+    }
+  }
+
+  /**
+   *  Write the amino acid symbols of the selected features to a file choosen
+   *  by the user.
+   **/
+  private void writeAminoAcids () {
+    if (! checkForSelectionFeatures ()) {
+      return;
+    }
+
+    final File write_file =
+      getWriteFile ("Select an output file name..", "amino_acids");
+
+    if (write_file == null) {
+      return;
+    }
+
+    try {
+      final FileWriter writer = new FileWriter (write_file);
+
+      final FeatureVector features_to_write =
+        getSelection ().getAllFeatures ();
+
+      for (int i = 0 ; i < features_to_write.size () ; ++i) {
+        final Feature selection_feature = features_to_write.elementAt (i);
+
+        selection_feature.writeAminoAcidsOfFeature (writer);
+      }
+
+      writer.close ();
+    } catch (IOException e) {
+      new MessageDialog (getParentFrame (),
+                         "error while writing: " + e.getMessage ());
+    }
+  }
+
+  /**
+   *  Write the bases of the selection to a file choosen by the user.
+   *  @param output_type One of EMBL_FORMAT, RAW_FORMAT etc.
+   **/
+  private void writeBasesOfSelection (final int output_type) {
+    final MarkerRange marker_range = getSelection ().getMarkerRange ();
+
+    if (marker_range == null) {
+      writeFeatureBases (output_type);
+    } else {
+      final String selection_bases = Strand.markerRangeBases (marker_range);
+
+      writeBases (selection_bases,
+                  "selected bases",
+                  getSequenceFileName (output_type),
+                  output_type);
+    }
+  }
+
+  /**
+   *  Write the bases of the selected features to a file choosen by the user
+   *  or show a message and return immediately if there are no selected
+   *  features.
+   *  @param sequence The sequence to be written.
+   *  @param header The header line that will be used on those output formats
+   *    that need it.
+   *  @param default_output_filename The filename that is passed to
+   *    getWriteFile ()
+   *  @param output_type One of EMBL_FORMAT, RAW_FORMAT etc.
+   **/
+  private void writeBases (final String sequence,
+                           final String header,
+                           final String default_output_filename,
+                           final int output_type) {
+    final File write_file =
+      getWriteFile ("Select an output file name ...", default_output_filename);
+
+    if (write_file == null) {
+      return;
+    }
+
+    try {
+      final FileWriter writer = new FileWriter (write_file);
+
+      final StreamSequence stream_sequence;
+
+      switch (output_type) {
+      case StreamSequenceFactory.FASTA_FORMAT:
+        stream_sequence =
+          new FastaStreamSequence (sequence, header);
+        break;
+
+      case StreamSequenceFactory.EMBL_FORMAT:
+        stream_sequence =
+          new EmblStreamSequence (sequence);
+        break;
+
+      case StreamSequenceFactory.GENBANK_FORMAT:
+        stream_sequence =
+          new GenbankStreamSequence (sequence);
+        break;
+
+      case StreamSequenceFactory.RAW_FORMAT:
+      default:
+        stream_sequence =
+          new RawStreamSequence (sequence);
+        break;
+      }
+
+      stream_sequence.writeToStream (writer);
+
+      writer.close ();
+    } catch (IOException e) {
+      new MessageDialog (getParentFrame (),
+                         "error while writing: " + e.getMessage ());
+    }
+  }
+
+  /**
+   *  Write the bases of the selected features to a file choosen by the user
+   *  or show a message and return immediately if there are no selected
+   *  features.
+   *  @param output_type One of EMBL_FORMAT, RAW_FORMAT etc.
+   **/
+  private void writeFeatureBases (final int output_type) {
+    if (! checkForSelectionFeatures ()) {
+      return;
+    }
+
+    final File write_file =
+      getWriteFile ("Select an output file name ...", "bases");
+
+    if (write_file == null) {
+      return;
+    }
+
+    try {
+      final FileWriter writer = new FileWriter (write_file);
+
+      final FeatureVector features_to_write =
+        getSelection ().getAllFeatures ();
+
+      for (int i = 0 ; i < features_to_write.size () ; ++i) {
+        final Feature selection_feature = features_to_write.elementAt (i);
+
+        final StringBuffer header_buffer = new StringBuffer ();
+
+        header_buffer.append (selection_feature.getSystematicName ());
+        header_buffer.append (" ");
+        header_buffer.append (selection_feature.getIDString ());
+        header_buffer.append (" ");
+
+        final String product = selection_feature.getProductString ();
+
+        if (product == null) {
+          header_buffer.append ("undefined product");
+        } else {
+          header_buffer.append (product);
+        }
+        
+        header_buffer.append (" ").append (selection_feature.getWriteRange ());
+
+        final StreamSequence stream_sequence =
+          getStreamSequence (selection_feature.getBases (),
+                             header_buffer.toString (),
+                             output_type);
+
+        stream_sequence.writeToStream (writer);
+      }
+
+      writer.close ();
+    } catch (IOException e) {
+      new MessageDialog (getParentFrame (),
+                         "error while writing: " + e.getMessage ());
+    }
+  }
+
+  /**
+   *  Return a StreamSequence object for the given sequence string and the
+   *  given type.
+   *  @param sequence A String containing the sequence.
+   *  @param header The header line that will be used on those output formats
+   *    that need it.
+   *  @param output_type One of EMBL_FORMAT, RAW_FORMAT etc.
+   **/
+  private StreamSequence getStreamSequence (final String sequence,
+                                            final String header,
+                                            final int output_type) {
+
+    switch (output_type) {
+    case StreamSequenceFactory.FASTA_FORMAT:
+      return new FastaStreamSequence (sequence, header);
+
+    case StreamSequenceFactory.EMBL_FORMAT:
+      return new EmblStreamSequence (sequence);
+
+    case StreamSequenceFactory.GENBANK_FORMAT:
+      return new GenbankStreamSequence (sequence);
+
+    case StreamSequenceFactory.RAW_FORMAT:
+    default:
+      return new RawStreamSequence (sequence);
+    }
+  }
+
+  /**
+   *  Write the upstream bases of the selected features to a file choosen by
+   *  the user.  The user can also choose the number of upstream base to
+   *  write.
+   *  @param output_type One of EMBL_FORMAT, RAW_FORMAT etc.
+   **/
+  private void writeUpstreamBases (final int output_type) {
+    if (! checkForSelectionFeatures ()) {
+      return;
+    }
+
+    final TextRequester text_requester =
+      new TextRequester ("write how many bases upstream of each feature?",
+                         18, "");
+
+    text_requester.addTextRequesterListener (new TextRequesterListener () {
+      public void actionPerformed (final TextRequesterEvent event) {
+        if (event.getType () == TextRequesterEvent.CANCEL) {
+          return;
+        }
+
+        final String base_count_string = event.getRequesterText ().trim ();
+
+        if (base_count_string.length () == 0) {
+          new MessageDialog (getParentFrame (), "no bases written");
+          return;
+        }
+
+        try {
+          final int base_count =
+            Integer.valueOf (base_count_string).intValue ();
+
+
+          final File write_file =
+            getWriteFile ("Select an output file name ...",
+                          "upstream_" + getSequenceFileName (output_type));
+
+          if (write_file == null) {
+            return;
+          }
+
+          try {
+            final FileWriter writer = new FileWriter (write_file);
+
+            final FeatureVector features_to_write =
+              getSelection ().getAllFeatures ();
+
+            for (int i = 0 ; i < features_to_write.size () ; ++i) {
+              final Feature selection_feature =
+                features_to_write.elementAt (i);
+
+              final String sequence_string =
+                selection_feature.getUpstreamBases (base_count);
+
+
+              final String header_line =
+                selection_feature.getIDString () + " - " +
+                sequence_string.length () + " bases upstream";
+
+              final StreamSequence stream_sequence =
+                getStreamSequence (sequence_string,
+                                   header_line,
+                                   output_type);
+              
+              stream_sequence.writeToStream (writer);
+            }
+
+            writer.close ();
+          } catch (IOException e) {
+            new MessageDialog (getParentFrame (),
+                               "error while writing: " + e.getMessage ());
+          }
+        } catch (NumberFormatException e) {
+          new MessageDialog (getParentFrame (),
+                             "this is not a number: " + base_count_string);
+        }
+      }
+    });
+
+    text_requester.show ();
+  }
+
+  /**
+   *  Write the downstream bases of the selected features to a file choosen by
+   *  the user.  The user can also choose the number of downstream base to
+   *  write.
+   *  @param output_type One of EMBL_FORMAT, RAW_FORMAT etc.
+   **/
+  private void writeDownstreamBases (final int output_type) {
+    if (! checkForSelectionFeatures ()) {
+      return;
+    }
+
+    final TextRequester text_requester =
+      new TextRequester ("write how many bases downstream of each feature?",
+                         18, "");
+
+    text_requester.addTextRequesterListener (new TextRequesterListener () {
+      public void actionPerformed (final TextRequesterEvent event) {
+        if (event.getType () == TextRequesterEvent.CANCEL) {
+          return;
+        }
+
+        final String base_count_string = event.getRequesterText ().trim ();
+
+        if (base_count_string.length () == 0) {
+          new MessageDialog (getParentFrame (), "no bases written");
+          return;
+        }
+
+        try {
+          final int base_count =
+            Integer.valueOf (base_count_string).intValue ();
+
+          final File write_file =
+            getWriteFile ("Select an output file name ...",
+                          "downstream_" + getSequenceFileName (output_type));
+
+          if (write_file == null) {
+            return;
+          }
+
+          try {
+            final FileWriter writer = new FileWriter (write_file);
+
+            final FeatureVector features_to_write =
+              getSelection ().getAllFeatures ();
+
+            for (int i = 0 ; i < features_to_write.size () ; ++i) {
+              final Feature selection_feature =
+                features_to_write.elementAt (i);
+
+              final String sequence_string =
+                selection_feature.getDownstreamBases (base_count);
+
+
+              final String header_line =
+                selection_feature.getIDString () + " - " +
+                sequence_string.length () + " bases downstream ";
+
+              final StreamSequence stream_sequence =
+                getStreamSequence (sequence_string,
+                                   header_line,
+                                   output_type);
+              
+              stream_sequence.writeToStream (writer);
+            }
+
+            writer.close ();
+          } catch (IOException e) {
+            new MessageDialog (getParentFrame (),
+                               "error while writing: " + e.getMessage ());
+          }
+        } catch (NumberFormatException e) {
+          new MessageDialog (getParentFrame (),
+                             "this is not a number: " + base_count_string);
+        }
+      }
+    });
+
+    text_requester.show ();
+  }
+
+  /**
+   *  Helper method for writeFeatureBases (), writeUpstreamBases () and
+   *  writeDownstreamBases (). 
+   *  @return A string of the form "100:200 reverse" or "1:2222 forward".
+   **/
+  private static String getWriteRange (final Feature feature) {
+    return (feature.isForwardFeature () ?
+            feature.getFirstCodingBaseMarker ().getRawPosition () + ":" +
+            feature.getLastBaseMarker ().getRawPosition () +
+            " forward" :
+            feature.getLastBaseMarker ().getRawPosition () + ":" +
+            feature.getFirstCodingBaseMarker ().getRawPosition () +
+            " reverse");
+  }
+
+  
+  /**
+   *  Return a sensible file name to write sequence of the given type to.
+   *  @param output_type One of EMBL_FORMAT, RAW_FORMAT etc.
+   **/
+  private String getSequenceFileName (final int output_type) {
+    switch (output_type) {
+      case StreamSequenceFactory.FASTA_FORMAT:
+        return "sequence.dna";
+
+      case StreamSequenceFactory.EMBL_FORMAT:
+        return "sequence.embl";
+
+      case StreamSequenceFactory.GENBANK_FORMAT:
+        return "sequence.genbank";
+
+      case StreamSequenceFactory.RAW_FORMAT:
+      default:
+        return "sequence.seq";
+    }
+  }
+  
+  /**
+   *  Write the bases from entry_group as raw sequence.
+   *  @param output_type One of EMBL_FORMAT, RAW_FORMAT etc.
+   **/
+  private void writeAllSequence (final int output_type) {
+    final String file_name = getSequenceFileName (output_type);
+
+    writeBases (entry_group.getBases ().toString (),
+                "all_bases", file_name, output_type);
+  }
+
+  /**
+   *  Popup a requester and ask for a file name to write to.  If the file
+   *  exists ask the user whether to overwrite.
+   *  @param title The title of the new FileDialog JFrame.
+   *  @param default_name The name to put in the FileDialog as a default.
+   **/
+  private File getWriteFile (final String title, final String default_name) {
+    final JFileChooser dialog = new StickyFileChooser ();
+
+    dialog.setDialogTitle (title);
+
+    dialog.setFileSelectionMode (JFileChooser.FILES_ONLY);
+
+    dialog.setSelectedFile (new File (default_name));
+    dialog.setDialogType (JFileChooser.SAVE_DIALOG);
+    final int status = dialog.showSaveDialog (getParentFrame ());
+
+    if (status != JFileChooser.APPROVE_OPTION) {
+      return null;
+    }
+
+    if (dialog.getSelectedFile () == null) {
+      return null;
+    } else {
+      final File write_file = dialog.getSelectedFile ();
+
+      if (write_file.exists ()) {
+        final YesNoDialog yes_no_dialog =
+          new YesNoDialog (getParentFrame (),
+                           "this file exists: " + write_file +
+                           " overwrite it?");
+        
+        if (yes_no_dialog.getResult ()) {
+          // yes - continue
+        } else {
+          // no
+          return null;
+        }
+      }
+
+      return write_file;
+    }
+  }
+
+  /**
+   *  Write a table of codon usage for the selected features to a file choosen
+   *  by the user.
+   **/
+  private void writeCodonUsage () {
+    if (! checkForSelectionFeatures ()) {
+      return;
+    }
+
+    final File write_file =
+      getWriteFile ("Select a codon usage output file ...", "usage");
+
+    if (write_file == null) {
+      return;
+    }
+
+    try {
+      final PrintWriter writer = new PrintWriter (new FileWriter (write_file));
+
+      final int [] [] [] codon_counts = new int [4][4][4];
+
+      final FeatureVector features_to_write =
+        getSelection ().getAllFeatures ();
+
+      int codon_total = 0;
+
+      for (int i = 0 ; i < features_to_write.size () ; ++i) {
+        final Feature selection_feature = features_to_write.elementAt (i);
+
+        for (int base1 = 0 ; base1 < 4 ; ++base1) {
+          for (int base2 = 0 ; base2 < 4 ; ++base2) {
+            for (int base3 = 0 ; base3 < 4 ; ++base3) {
+              codon_counts[base1][base2][base3] +=
+                selection_feature.getCodonCount (base1, base2, base3);
+            }
+          }
+        }
+
+        codon_total += selection_feature.getTranslationBasesLength () / 3;
+      }
+
+      for (int base1 = 0 ; base1 < 4 ; ++base1) {
+        for (int base3 = 0 ; base3 < 4 ; ++base3) {
+          final StringBuffer buffer = new StringBuffer ();
+
+          for (int base2 = 0 ; base2 < 4 ; ++base2) {
+            buffer.append (Bases.letter_index[base1]);
+            buffer.append (Bases.letter_index[base2]);
+            buffer.append (Bases.letter_index[base3]);
+            buffer.append (' ');
+
+            final float per_thousand;
+
+            if (codon_total > 0) {
+              per_thousand =
+                10000 * codon_counts[base1][base2][base3] / codon_total *
+                1.0F / 10;
+            } else {
+              per_thousand = 0.0F;
+            }
+
+            buffer.append (per_thousand);
+            buffer.append ("( ").append (codon_counts[base1][base2][base3]);
+            buffer.append (")  ");
+          }
+
+          writer.println (buffer.toString ());
+        }
+      }
+
+      writer.close ();
+    } catch (IOException e) {
+      new MessageDialog (getParentFrame (),
+                         "error while writing: " + e.getMessage ());
+    }
+  }
+
+  /**
+   *  The EntryGroup object that was passed to the constructor.
+   **/
+  private EntryGroup entry_group;
+}
diff --git a/uk/ac/sanger/artemis/components/YesNoDialog.java b/uk/ac/sanger/artemis/components/YesNoDialog.java
new file mode 100644
index 000000000..6b7386c88
--- /dev/null
+++ b/uk/ac/sanger/artemis/components/YesNoDialog.java
@@ -0,0 +1,115 @@
+/* YesNoDialog.java
+ *
+ * created: Sat Dec 12 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: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/components/YesNoDialog.java,v 1.1 2004-06-09 09:48:01 tjc Exp $
+ */
+
+package uk.ac.sanger.artemis.components;
+
+import uk.ac.sanger.artemis.Options;
+
+import java.awt.*;
+import java.awt.event.*;
+
+import javax.swing.*;
+
+/**
+ *  A popup dialog box that displays a message and then waits for the to press
+ *  yes or no.
+ *
+ *  @author Kim Rutherford
+ *  @version $Id: YesNoDialog.java,v 1.1 2004-06-09 09:48:01 tjc Exp $
+ **/
+
+public class YesNoDialog extends JDialog {
+  /**
+   *  Create a new YesNoDialog component.  The constructor does not show ()
+   *  the dialog, call getResult () to do that.
+   *  @param parent The parent window.
+   *  @param title The title of the new dialog JFrame.
+   *  @param message The message to display in the JDialog.
+   **/
+  public YesNoDialog (JFrame parent, String title, String message) {
+    super (parent, message, true);
+    
+    getContentPane ().add (new JLabel (message), "North");
+
+    final JPanel panel = new JPanel ();
+
+    panel.add (yes_button);
+    yes_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        button_result = true;
+        setVisible (false);
+        YesNoDialog.this.dispose ();
+      }
+    });
+
+    panel.add (no_button);
+    no_button.addActionListener (new ActionListener () {
+      public void actionPerformed (ActionEvent e) {
+        button_result = false;
+        setVisible (false);
+        YesNoDialog.this.dispose ();
+      }
+    });
+
+    getContentPane ().add (panel, "South");
+    pack ();
+        
+    final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
+    setLocation (new Point ((screen.width - getSize ().width) / 2,
+                            (screen.height - getSize ().height) / 2));
+  }
+
+  /**
+   *  Create a new YesNoDialog component.  The constructor does not show ()
+   *  the dialog, call getResult () to do that.
+   *  @param parent The parent window.
+   *  @param message The message to display in the JDialog and to uise as the
+   *    title string.
+   **/
+  public YesNoDialog (JFrame parent, String message) {
+    this (parent, message, message);
+  }
+  
+  /**
+   *  This method calls show () on this object, and then waits for the user to
+   *  press the Yes button or the No button.
+   *  @return true is the user pressed Yes, false otherwise.
+   **/
+  public boolean getResult () {
+    setVisible (true);
+    
+    return button_result;
+  }
+
+  final JButton yes_button = new JButton ("Yes");
+
+  final JButton no_button = new JButton ("No");
+
+  /**
+   *  Set by the yes and no button action listeners and read by getResult ().
+   **/
+  boolean button_result;
+}
+
-- 
GitLab