JAL-2547 tooltip/AmendFeatures on gap straddled by feature, not for
[jalview.git] / src / jalview / appletgui / SeqPanel.java
index 89289c0..f0ec51c 100644 (file)
@@ -1,36 +1,67 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
- * Copyright (C) 2012 J Procter, AM Waterhouse, LM Lui, J Engelhardt, G Barton, M Clamp, S Searle
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
  * Jalview is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
  *  
  * Jalview 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 Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
 package jalview.appletgui;
 
-import java.util.*;
-
-import java.awt.*;
-import java.awt.event.*;
-
-import jalview.commands.*;
-import jalview.datamodel.*;
-import jalview.schemes.*;
+import jalview.api.AlignViewportI;
+import jalview.commands.EditCommand;
+import jalview.commands.EditCommand.Action;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.SearchResultMatchI;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.SearchResultsI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+import jalview.schemes.ResidueProperties;
+import jalview.structure.SelectionListener;
 import jalview.structure.SelectionSource;
 import jalview.structure.SequenceListener;
 import jalview.structure.StructureSelectionManager;
+import jalview.structure.VamsasSource;
+import jalview.util.Comparison;
+import jalview.util.MappingUtils;
+import jalview.util.MessageManager;
+import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.ViewportRanges;
+
+import java.awt.BorderLayout;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Panel;
+import java.awt.Point;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Vector;
 
 public class SeqPanel extends Panel implements MouseMotionListener,
-        MouseListener, SequenceListener
+        MouseListener, SequenceListener, SelectionListener
 {
 
   public SeqCanvas seqCanvas;
@@ -93,6 +124,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     seqCanvas.addMouseListener(this);
     ssm = StructureSelectionManager.getStructureSelectionManager(av.applet);
     ssm.addStructureViewerListener(this);
+    ssm.addSelectionListener(this);
 
     seqCanvas.repaint();
   }
@@ -143,8 +175,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
   void setCursorPosition()
   {
-    SequenceI sequence = (Sequence) av.getAlignment().getSequenceAt(
-            seqCanvas.cursorY);
+    SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
 
     seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
     scrollToVisible();
@@ -155,19 +186,22 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     seqCanvas.cursorX += dx;
     seqCanvas.cursorY += dy;
     if (av.hasHiddenColumns()
-            && !av.getColumnSelection().isVisible(seqCanvas.cursorX))
+            && !av.getAlignment().getHiddenColumns()
+                    .isVisible(seqCanvas.cursorX))
     {
       int original = seqCanvas.cursorX - dx;
       int maxWidth = av.getAlignment().getWidth();
 
-      while (!av.getColumnSelection().isVisible(seqCanvas.cursorX)
+      while (!av.getAlignment().getHiddenColumns()
+              .isVisible(seqCanvas.cursorX)
               && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0)
       {
         seqCanvas.cursorX += dx;
       }
 
       if (seqCanvas.cursorX >= maxWidth
-              || !av.getColumnSelection().isVisible(seqCanvas.cursorX))
+              || !av.getAlignment().getHiddenColumns()
+                      .isVisible(seqCanvas.cursorX))
       {
         seqCanvas.cursorX = original;
       }
@@ -196,33 +230,35 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
 
     endEditing();
-    if (av.wrapAlignment)
+    if (av.getWrapAlignment())
     {
-      ap.scrollToWrappedVisible(seqCanvas.cursorX);
+      av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
     }
     else
     {
-      while (seqCanvas.cursorY < av.startSeq)
+      ViewportRanges ranges = av.getRanges();
+      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+      while (seqCanvas.cursorY < ranges.getStartSeq())
       {
-        ap.scrollUp(true);
+        ranges.scrollUp(true);
       }
-      while (seqCanvas.cursorY + 1 > av.endSeq)
+      while (seqCanvas.cursorY > ranges.getEndSeq())
       {
-        ap.scrollUp(false);
+        ranges.scrollUp(false);
       }
-      while (seqCanvas.cursorX < av.getColumnSelection()
-              .adjustForHiddenColumns(av.startRes))
+      while (seqCanvas.cursorX < hidden.adjustForHiddenColumns(ranges
+              .getStartRes()))
       {
 
-        if (!ap.scrollRight(false))
+        if (!ranges.scrollRight(false))
         {
           break;
         }
       }
-      while (seqCanvas.cursorX > av.getColumnSelection()
-              .adjustForHiddenColumns(av.endRes))
+      while (seqCanvas.cursorX > hidden.adjustForHiddenColumns(ranges
+              .getEndRes()))
       {
-        if (!ap.scrollRight(true))
+        if (!ranges.scrollRight(true))
         {
           break;
         }
@@ -236,8 +272,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
   void setSelectionAreaAtCursor(boolean topLeft)
   {
-    SequenceI sequence = (Sequence) av.getAlignment().getSequenceAt(
-            seqCanvas.cursorY);
+    SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
 
     if (av.getSelectionGroup() != null)
     {
@@ -342,72 +377,132 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       keyboardNo1.append(value);
     }
   }
+
   int getKeyboardNo1()
   {
-    try {
-    if (keyboardNo1 != null) 
+    try
     {
-      int value = Integer.parseInt(keyboardNo1.toString());
-      keyboardNo1 = null;
-      return value;
-    }
+      if (keyboardNo1 != null)
+      {
+        int value = Integer.parseInt(keyboardNo1.toString());
+        keyboardNo1 = null;
+        return value;
+      }
     } catch (Exception x)
-    {}
+    {
+    }
     keyboardNo1 = null;
     return 1;
   }
 
   int getKeyboardNo2()
   {
-    try {
-    if (keyboardNo2!=null){
-      int value = Integer.parseInt(keyboardNo2.toString());
-      keyboardNo2 = null;
-      return value;
-    }
+    try
+    {
+      if (keyboardNo2 != null)
+      {
+        int value = Integer.parseInt(keyboardNo2.toString());
+        keyboardNo2 = null;
+        return value;
+      }
     } catch (Exception x)
-    {}
+    {
+    }
     keyboardNo2 = null;
     return 1;
   }
-  
-  void setStatusMessage(SequenceI sequence, int res, int seq)
-  {
-    StringBuffer text = new StringBuffer("Sequence " + (seq + 1) + " ID: "
-            + sequence.getName());
 
-    Object obj = null;
+  /**
+   * Set status message in alignment panel
+   * 
+   * @param sequence
+   *          aligned sequence object
+   * @param column
+   *          alignment column
+   * @param seq
+   *          index of sequence in alignment
+   * @return position of column in sequence or -1 if at gap
+   */
+  void setStatusMessage(SequenceI sequence, int column, int seq)
+  {
+    // TODO remove duplication of identical gui method
+    StringBuilder text = new StringBuilder(32);
+    String seqno = seq == -1 ? "" : " " + (seq + 1);
+    text.append("Sequence" + seqno + " ID: " + sequence.getName());
+
+    String residue = null;
+    /*
+     * Try to translate the display character to residue name (null for gap).
+     */
+    final String displayChar = String.valueOf(sequence.getCharAt(column));
     if (av.getAlignment().isNucleotide())
     {
-      obj = ResidueProperties.nucleotideName.get(sequence.getCharAt(res)
-              + "");
-      if (obj != null)
+      residue = ResidueProperties.nucleotideName.get(displayChar);
+      if (residue != null)
       {
-        text.append(" Nucleotide: ");
+        text.append(" Nucleotide: ").append(residue);
       }
     }
     else
     {
-      obj = ResidueProperties.aa2Triplet.get(sequence.getCharAt(res) + "");
-      if (obj != null)
+      residue = "X".equalsIgnoreCase(displayChar) ? "X" : ("*"
+              .equals(displayChar) ? "STOP" : ResidueProperties.aa2Triplet
+              .get(displayChar));
+      if (residue != null)
       {
-        text.append("  Residue: ");
+        text.append(" Residue: ").append(residue);
       }
     }
 
-    if (obj != null)
+    int pos = -1;
+    if (residue != null)
     {
-
-      if (obj != "")
-      {
-        text.append(obj + " (" + sequence.findPosition(res) + ")");
-      }
+      pos = sequence.findPosition(column);
+      text.append(" (").append(Integer.toString(pos)).append(")");
     }
 
     ap.alignFrame.statusBar.setText(text.toString());
+  }
+
+  /**
+   * Set the status bar message to highlight the first matched position in
+   * search results.
+   * 
+   * @param results
+   * @return true if results were matched, false if not
+   */
+  private boolean setStatusMessage(SearchResultsI results)
+  {
+    AlignmentI al = this.av.getAlignment();
+    int sequenceIndex = al.findIndex(results);
+    if (sequenceIndex == -1)
+    {
+      return false;
+    }
+    SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
+    for (SearchResultMatchI m : results.getResults())
+    {
+      SequenceI seq = m.getSequence();
+      if (seq.getDatasetSequence() != null)
+      {
+        seq = seq.getDatasetSequence();
+      }
 
+      if (seq == ds)
+      {
+        /*
+         * Convert position in sequence (base 1) to sequence character array
+         * index (base 0)
+         */
+        int start = m.getStart() - m.getSequence().getStart();
+        setStatusMessage(seq, start, sequenceIndex);
+        return true;
+      }
+    }
+    return false;
   }
 
+  @Override
   public void mousePressed(MouseEvent evt)
   {
     lastMousePress = evt.getPoint();
@@ -458,6 +553,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     return;
   }
 
+  @Override
   public void mouseClicked(MouseEvent evt)
   {
     SequenceI sequence = av.getAlignment().getSequenceAt(findSeq(evt));
@@ -471,35 +567,39 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         av.setSelectionGroup(null);
       }
 
-      SequenceFeature[] features = findFeaturesAtRes(sequence,
-              sequence.findPosition(findRes(evt)));
-
-      if (features != null && features.length > 0)
+      int column = findRes(evt);
+      boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
+      List<SequenceFeature> features = findFeaturesAtRes(sequence,
+              sequence.findPosition(column));
+      if (isGapped)
       {
-        SearchResults highlight = new SearchResults();
-        highlight.addResult(sequence, features[0].getBegin(),
-                features[0].getEnd());
-        seqCanvas.highlightSearchResults(highlight);
+        removeAdjacentFeatures(features, column + 1, sequence);
       }
-      if (features != null && features.length > 0)
+
+      if (!features.isEmpty())
       {
-        seqCanvas.getFeatureRenderer().amendFeatures(new SequenceI[]
-        { sequence }, features, false, ap);
+        SearchResultsI highlight = new SearchResults();
+        highlight.addResult(sequence, features.get(0).getBegin(), features
+                .get(0).getEnd());
+        seqCanvas.highlightSearchResults(highlight);
+        seqCanvas.getFeatureRenderer().amendFeatures(
+                Collections.singletonList(sequence), features, false, ap);
 
         seqCanvas.highlightSearchResults(null);
       }
     }
   }
 
+  @Override
   public void mouseReleased(MouseEvent evt)
   {
+    boolean didDrag = mouseDragging; // did we come here after a drag
     mouseDragging = false;
     mouseWheelPressed = false;
-    ap.paintAlignment(true);
 
     if (!editingSeqs)
     {
-      doMouseReleasedDefineMode(evt);
+      doMouseReleasedDefineMode(evt, didDrag);
       return;
     }
 
@@ -516,17 +616,17 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     int res = 0;
     int x = evt.getX();
 
-    if (av.wrapAlignment)
+    if (av.getWrapAlignment())
     {
 
-      int hgap = av.charHeight;
-      if (av.scaleAboveWrapped)
+      int hgap = av.getCharHeight();
+      if (av.getScaleAboveWrapped())
       {
-        hgap += av.charHeight;
+        hgap += av.getCharHeight();
       }
 
-      int cHeight = av.getAlignment().getHeight() * av.charHeight + hgap
-              + seqCanvas.getAnnotationHeight();
+      int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
+              + hgap + seqCanvas.getAnnotationHeight();
 
       int y = evt.getY();
       y -= hgap;
@@ -539,19 +639,20 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       }
 
       wrappedBlock = y / cHeight;
-      wrappedBlock += av.getStartRes() / cwidth;
+      wrappedBlock += av.getRanges().getStartRes() / cwidth;
 
       res = wrappedBlock * cwidth + x / av.getCharWidth();
 
     }
     else
     {
-      res = (x / av.getCharWidth()) + av.getStartRes();
+      res = (x / av.getCharWidth()) + av.getRanges().getStartRes();
     }
 
     if (av.hasHiddenColumns())
     {
-      res = av.getColumnSelection().adjustForHiddenColumns(res);
+      res = av.getAlignment().getHiddenColumns()
+              .adjustForHiddenColumns(res);
     }
 
     return res;
@@ -574,16 +675,16 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     int seq = 0;
     int y = evt.getY();
 
-    if (av.wrapAlignment)
+    if (av.getWrapAlignment())
     {
-      int hgap = av.charHeight;
-      if (av.scaleAboveWrapped)
+      int hgap = av.getCharHeight();
+      if (av.getScaleAboveWrapped())
       {
-        hgap += av.charHeight;
+        hgap += av.getCharHeight();
       }
 
-      int cHeight = av.getAlignment().getHeight() * av.charHeight + hgap
-              + seqCanvas.getAnnotationHeight();
+      int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
+              + hgap + seqCanvas.getAnnotationHeight();
 
       y -= hgap;
 
@@ -596,7 +697,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
     else
     {
-      seq = Math.min((y / av.getCharHeight()) + av.getStartSeq(), av
+      seq = Math.min((y / av.getCharHeight())
+              + av.getRanges().getStartSeq(),
+              av
               .getAlignment().getHeight() - 1);
       if (seq < 0)
       {
@@ -634,40 +737,59 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
   String lastMessage;
 
+  @Override
   public void mouseOverSequence(SequenceI sequence, int index, int pos)
   {
     String tmp = sequence.hashCode() + index + "";
     if (lastMessage == null || !lastMessage.equals(tmp))
+    {
       ssm.mouseOverSequence(sequence, index, pos, av);
+    }
 
     lastMessage = tmp;
   }
 
-  public void highlightSequence(SearchResults results)
+  @Override
+  public void highlightSequence(SearchResultsI results)
   {
-    if (av.followHighlight)
+    if (av.isFollowHighlight())
     {
+      // don't allow highlight of protein/cDNA to also scroll a complementary
+      // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
+      // over residue to change abruptly, causing highlighted residue in panel 2
+      // to change, causing a scroll in panel 1 etc)
+      ap.setToScrollComplementPanel(false);
       if (ap.scrollToPosition(results, true))
       {
         ap.alignFrame.repaint();
       }
+      ap.setToScrollComplementPanel(true);
     }
+    setStatusMessage(results);
     seqCanvas.highlightSearchResults(results);
 
   }
 
+  @Override
+  public VamsasSource getVamsasSource()
+  {
+    return this.ap == null ? null : this.ap.av;
+  }
+
+  @Override
   public void updateColours(SequenceI seq, int index)
   {
     System.out.println("update the seqPanel colours");
     // repaint();
   }
 
+  @Override
   public void mouseMoved(MouseEvent evt)
   {
-    int res = findRes(evt);
+    final int column = findRes(evt);
     int seq = findSeq(evt);
 
-    if (seq >= av.getAlignment().getHeight() || seq < 0 || res < 0)
+    if (seq >= av.getAlignment().getHeight() || seq < 0 || column < 0)
     {
       if (tooltip != null)
       {
@@ -677,7 +799,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    if (res > sequence.getLength())
+    if (column > sequence.getLength())
     {
       if (tooltip != null)
       {
@@ -686,54 +808,51 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       return;
     }
 
-    int respos = sequence.findPosition(res);
-    if (ssm != null)
-      mouseOverSequence(sequence, res, respos);
-
-    StringBuffer text = new StringBuffer("Sequence " + (seq + 1) + " ID: "
-            + sequence.getName());
+    final char ch = sequence.getCharAt(column);
+    boolean isGapped = Comparison.isGap(ch);
+    // find residue at column (or nearest if at a gap)
+    int respos = sequence.findPosition(column);
 
-    Object obj = null;
-    if (av.getAlignment().isNucleotide())
+    if (ssm != null && !isGapped)
     {
-      obj = ResidueProperties.nucleotideName.get(sequence.getCharAt(res)
-              + "");
-      if (obj != null)
-      {
-        text.append(" Nucleotide: ");
-      }
+      mouseOverSequence(sequence, column, respos);
     }
-    else
+
+    StringBuilder text = new StringBuilder();
+    text.append("Sequence ").append(Integer.toString(seq + 1))
+            .append(" ID: ").append(sequence.getName());
+
+    if (!isGapped)
     {
-      obj = ResidueProperties.aa2Triplet.get(sequence.getCharAt(res) + "");
-      if (obj != null)
+      if (av.getAlignment().isNucleotide())
       {
-        text.append("  Residue: ");
+        String base = ResidueProperties.nucleotideName.get(ch);
+        text.append(" Nucleotide: ").append(base == null ? ch : base);
       }
-    }
-
-    if (obj != null)
-    {
-      if (obj != "")
+      else
       {
-        text.append(obj + " (" + respos + ")");
+        String residue = (ch == 'x' || ch == 'X') ? "X"
+                : ResidueProperties.aa2Triplet
+                .get(String.valueOf(ch));
+        text.append(" Residue: ").append(residue == null ? ch : residue);
       }
+      text.append(" (").append(Integer.toString(respos)).append(")");
     }
 
     ap.alignFrame.statusBar.setText(text.toString());
 
-    StringBuffer tooltipText = new StringBuffer();
+    StringBuilder tooltipText = new StringBuilder();
     SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
     if (groups != null)
     {
       for (int g = 0; g < groups.length; g++)
       {
-        if (groups[g].getStartRes() <= res && groups[g].getEndRes() >= res)
+        if (groups[g].getStartRes() <= column && groups[g].getEndRes() >= column)
         {
           if (!groups[g].getName().startsWith("JTreeGroup")
                   && !groups[g].getName().startsWith("JGroup"))
           {
-            tooltipText.append(groups[g].getName() + " ");
+            tooltipText.append(groups[g].getName()).append(" ");
           }
           if (groups[g].getDescription() != null)
           {
@@ -744,33 +863,37 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       }
     }
 
-    // use aa to see if the mouse pointer is on a
-    SequenceFeature[] allFeatures = findFeaturesAtRes(sequence,
-            sequence.findPosition(res));
-
-    int index = 0;
-    while (index < allFeatures.length)
+    /*
+     * add feature details to tooltip, including any that straddle
+     * a gapped position
+     */
+    if (av.isShowSequenceFeatures())
     {
-      SequenceFeature sf = allFeatures[index];
-
-      tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end);
-
-      if (sf.getDescription() != null)
+      List<SequenceFeature> allFeatures = findFeaturesAtRes(sequence,
+              sequence.findPosition(column));
+      if (isGapped)
       {
-        tooltipText.append(" " + sf.getDescription());
+        removeAdjacentFeatures(allFeatures, column + 1, sequence);
       }
-
-      if (sf.getValue("status") != null)
+      for (SequenceFeature sf : allFeatures)
       {
-        String status = sf.getValue("status").toString();
-        if (status.length() > 0)
+        tooltipText.append(sf.getType() + " " + sf.begin + ":" + sf.end);
+
+        if (sf.getDescription() != null)
         {
-          tooltipText.append(" (" + sf.getValue("status") + ")");
+          tooltipText.append(" " + sf.getDescription());
         }
-      }
-      tooltipText.append("\n");
 
-      index++;
+        if (sf.getValue("status") != null)
+        {
+          String status = sf.getValue("status").toString();
+          if (status.length() > 0)
+          {
+            tooltipText.append(" (" + sf.getValue("status") + ")");
+          }
+        }
+        tooltipText.append("\n");
+      }
     }
 
     if (tooltip == null)
@@ -783,49 +906,81 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
   }
 
-  SequenceFeature[] findFeaturesAtRes(SequenceI sequence, int res)
+  /**
+   * Removes from the list of features any that start after, or end before, the
+   * given column position. This allows us to retain only those features
+   * adjacent to a gapped position that straddle the position.
+   * 
+   * @param features
+   * @param column
+   *          alignment column (1..)
+   * @param sequence
+   */
+  protected void removeAdjacentFeatures(List<SequenceFeature> features,
+          int column, SequenceI sequence)
+  {
+    // TODO should this be an AlignViewController method (shared by gui)?
+    ListIterator<SequenceFeature> it = features.listIterator();
+    while (it.hasNext())
+    {
+      SequenceFeature sf = it.next();
+      if (sequence.findIndex(sf.getBegin()) > column
+              || sequence.findIndex(sf.getEnd()) < column)
+      {
+        it.remove();
+      }
+    }
+  }
+
+  List<SequenceFeature> findFeaturesAtRes(SequenceI sequence, int res)
   {
-    Vector tmp = new Vector();
+    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
     SequenceFeature[] features = sequence.getSequenceFeatures();
     if (features != null)
     {
       for (int i = 0; i < features.length; i++)
       {
-        if (av.featuresDisplayed == null
-                || !av.featuresDisplayed.containsKey(features[i].getType()))
+        if (av.getFeaturesDisplayed() == null
+                || !av.getFeaturesDisplayed().isVisible(
+                        features[i].getType()))
         {
           continue;
         }
 
         if (features[i].featureGroup != null
-                && seqCanvas.fr.featureGroups != null
-                && seqCanvas.fr.featureGroups
-                        .containsKey(features[i].featureGroup)
-                && !((Boolean) seqCanvas.fr.featureGroups
-                        .get(features[i].featureGroup)).booleanValue())
+                && !seqCanvas.fr.checkGroupVisibility(
+                        features[i].featureGroup, false))
+        {
           continue;
+        }
 
         if ((features[i].getBegin() <= res)
                 && (features[i].getEnd() >= res))
         {
-          tmp.addElement(features[i]);
+          result.add(features[i]);
         }
       }
     }
 
-    features = new SequenceFeature[tmp.size()];
-    tmp.copyInto(features);
-
-    return features;
+    return result;
   }
 
   Tooltip tooltip;
 
+  /**
+   * set when the current UI interaction has resulted in a change that requires
+   * overview shading to be recalculated. this could be changed to something
+   * more expressive that indicates what actually has changed, so selective
+   * redraws can be applied
+   */
+  private boolean needOverviewUpdate; // TODO: refactor to avcontroller
+
+  @Override
   public void mouseDragged(MouseEvent evt)
   {
     if (mouseWheelPressed)
     {
-      int oldWidth = av.charWidth;
+      int oldWidth = av.getCharWidth();
 
       // Which is bigger, left-right or up-down?
       if (Math.abs(evt.getY() - lastMousePress.y) > Math.abs(evt.getX()
@@ -833,7 +988,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       {
         int fontSize = av.font.getSize();
 
-        if (evt.getY() < lastMousePress.y && av.charHeight > 1)
+        if (evt.getY() < lastMousePress.y && av.getCharHeight() > 1)
         {
           fontSize--;
         }
@@ -847,30 +1002,32 @@ public class SeqPanel extends Panel implements MouseMotionListener,
           fontSize = 1;
         }
 
-        av.setFont(new Font(av.font.getName(), av.font.getStyle(), fontSize));
-        av.charWidth = oldWidth;
+        av.setFont(
+                new Font(av.font.getName(), av.font.getStyle(), fontSize),
+                true);
+        av.setCharWidth(oldWidth);
       }
       else
       {
-        if (evt.getX() < lastMousePress.x && av.charWidth > 1)
+        if (evt.getX() < lastMousePress.x && av.getCharWidth() > 1)
         {
-          av.charWidth--;
+          av.setCharWidth(av.getCharWidth() - 1);
         }
         else if (evt.getX() > lastMousePress.x)
         {
-          av.charWidth++;
+          av.setCharWidth(av.getCharWidth() + 1);
         }
 
-        if (av.charWidth < 1)
+        if (av.getCharWidth() < 1)
         {
-          av.charWidth = 1;
+          av.setCharWidth(1);
         }
       }
 
       ap.fontChanged();
 
       FontMetrics fm = getFontMetrics(av.getFont());
-      av.validCharWidth = fm.charWidth('M') <= av.charWidth;
+      av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
 
       lastMousePress = evt.getPoint();
 
@@ -928,7 +1085,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     {
       if (av.isHiddenRepSequence(seq))
       {
-        sg = (SequenceGroup) av.getRepresentedSequences(seq);
+        sg = av.getRepresentedSequences(seq);
         groupEditing = true;
       }
     }
@@ -936,15 +1093,18 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     StringBuffer message = new StringBuffer();
     if (groupEditing)
     {
-      message.append("Edit group:");
+      message.append(MessageManager.getString("action.edit_group")).append(
+              ":");
       if (editCommand == null)
       {
-        editCommand = new EditCommand("Edit Group");
+        editCommand = new EditCommand(
+                MessageManager.getString("action.edit_group"));
       }
     }
     else
     {
-      message.append("Edit sequence: " + seq.getName());
+      message.append(MessageManager.getString("label.edit_sequence"))
+              .append(" " + seq.getName());
       String label = seq.getName();
       if (label.length() > 10)
       {
@@ -952,7 +1112,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       }
       if (editCommand == null)
       {
-        editCommand = new EditCommand("Edit " + label);
+        editCommand = new EditCommand(MessageManager.formatMessage(
+                "label.edit_params", new String[] { label }));
       }
     }
 
@@ -1015,8 +1176,10 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     if (av.hasHiddenColumns())
     {
       fixedColumns = true;
-      int y1 = av.getColumnSelection().getHiddenBoundaryLeft(startres);
-      int y2 = av.getColumnSelection().getHiddenBoundaryRight(startres);
+      int y1 = av.getAlignment().getHiddenColumns()
+              .getHiddenBoundaryLeft(startres);
+      int y2 = av.getAlignment().getHiddenColumns()
+              .getHiddenBoundaryRight(startres);
 
       if ((insertGap && startres > y1 && lastres < y1)
               || (!insertGap && startres < y2 && lastres > y2))
@@ -1087,8 +1250,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         {
           if (sg.getSize() == av.getAlignment().getHeight())
           {
-            if ((av.hasHiddenColumns() && startres < av
-                    .getColumnSelection().getHiddenBoundaryRight(startres)))
+            if ((av.hasHiddenColumns() && startres < av.getAlignment()
+                    .getHiddenColumns().getHiddenBoundaryRight(startres)))
             {
               endEditing();
               return;
@@ -1154,8 +1317,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         }
         else
         {
-          editCommand.appendEdit(EditCommand.INSERT_GAP, groupSeqs,
-                  startres, startres - lastres, av.getAlignment(), true);
+          editCommand.appendEdit(Action.INSERT_GAP, groupSeqs, startres,
+                  startres - lastres, av.getAlignment(), true);
         }
       }
       else
@@ -1170,8 +1333,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         }
         else
         {
-          editCommand.appendEdit(EditCommand.DELETE_GAP, groupSeqs,
-                  startres, lastres - startres, av.getAlignment(), true);
+          editCommand.appendEdit(Action.DELETE_GAP, groupSeqs, startres,
+                  lastres - startres, av.getAlignment(), true);
         }
 
       }
@@ -1186,14 +1349,14 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         {
           for (int j = lastres; j < startres; j++)
           {
-            insertChar(j, new SequenceI[]
-            { seq }, fixedRight);
+            insertChar(j, new SequenceI[] { seq }, fixedRight);
           }
         }
         else
         {
-          editCommand.appendEdit(EditCommand.INSERT_GAP, new SequenceI[]
-          { seq }, lastres, startres - lastres, av.getAlignment(), true);
+          editCommand.appendEdit(Action.INSERT_GAP,
+                  new SequenceI[] { seq }, lastres, startres - lastres,
+                  av.getAlignment(), true);
         }
       }
       else
@@ -1208,8 +1371,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
               endEditing();
               break;
             }
-            deleteChar(startres, new SequenceI[]
-            { seq }, fixedRight);
+            deleteChar(startres, new SequenceI[] { seq }, fixedRight);
           }
         }
         else
@@ -1227,8 +1389,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
           if (max > 0)
           {
-            editCommand.appendEdit(EditCommand.DELETE_GAP, new SequenceI[]
-            { seq }, startres, max, av.getAlignment(), true);
+            editCommand.appendEdit(Action.DELETE_GAP,
+                    new SequenceI[] { seq }, startres, max,
+                    av.getAlignment(), true);
           }
         }
       }
@@ -1263,21 +1426,21 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       }
     }
 
-    editCommand.appendEdit(EditCommand.DELETE_GAP, seq, blankColumn, 1,
+    editCommand.appendEdit(Action.DELETE_GAP, seq, blankColumn, 1,
             av.getAlignment(), true);
 
-    editCommand.appendEdit(EditCommand.INSERT_GAP, seq, j, 1,
-            av.getAlignment(), true);
+    editCommand.appendEdit(Action.INSERT_GAP, seq, j, 1, av.getAlignment(),
+            true);
 
   }
 
   void deleteChar(int j, SequenceI[] seq, int fixedColumn)
   {
 
-    editCommand.appendEdit(EditCommand.DELETE_GAP, seq, j, 1,
-            av.getAlignment(), true);
+    editCommand.appendEdit(Action.DELETE_GAP, seq, j, 1, av.getAlignment(),
+            true);
 
-    editCommand.appendEdit(EditCommand.INSERT_GAP, seq, fixedColumn, 1,
+    editCommand.appendEdit(Action.INSERT_GAP, seq, fixedColumn, 1,
             av.getAlignment(), true);
   }
 
@@ -1302,7 +1465,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       return;
     }
 
-    SequenceI sequence = (Sequence) av.getAlignment().getSequenceAt(seq);
+    SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
     if (sequence == null || res > sequence.getLength())
     {
@@ -1311,64 +1474,35 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
     stretchGroup = av.getSelectionGroup();
 
-    if (stretchGroup == null)
+    if (stretchGroup == null || !stretchGroup.contains(sequence, res))
     {
-      stretchGroup = av.getAlignment().findGroup(sequence);
-      if (stretchGroup != null && res > stretchGroup.getStartRes()
-              && res < stretchGroup.getEndRes())
+      stretchGroup = av.getAlignment().findGroup(sequence, res);
+      if (stretchGroup != null)
       {
+        // only update the current selection if the popup menu has a group to
+        // focus on
         av.setSelectionGroup(stretchGroup);
       }
-      else
-      {
-        stretchGroup = null;
-      }
-    }
-
-    else if (!stretchGroup.getSequences(null).contains(sequence)
-            || stretchGroup.getStartRes() > res
-            || stretchGroup.getEndRes() < res)
-    {
-      stretchGroup = null;
-
-      SequenceGroup[] allGroups = av.getAlignment().findAllGroups(sequence);
-
-      if (allGroups != null)
-      {
-        for (int i = 0; i < allGroups.length; i++)
-        {
-          if (allGroups[i].getStartRes() <= res
-                  && allGroups[i].getEndRes() >= res)
-          {
-            stretchGroup = allGroups[i];
-            break;
-          }
-        }
-      }
-      av.setSelectionGroup(stretchGroup);
     }
 
     // DETECT RIGHT MOUSE BUTTON IN AWT
     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK)
     {
-      SequenceFeature[] allFeatures = findFeaturesAtRes(sequence,
+      List<SequenceFeature> allFeatures = findFeaturesAtRes(sequence,
               sequence.findPosition(res));
 
-      Vector links = null;
-      if (allFeatures != null)
+      Vector<String> links = null;
+      for (SequenceFeature sf : allFeatures)
       {
-        for (int i = 0; i < allFeatures.length; i++)
+        if (sf.links != null)
         {
-          if (allFeatures[i].links != null)
+          if (links == null)
           {
-            if (links == null)
-            {
-              links = new Vector();
-            }
-            for (int j = 0; j < allFeatures[i].links.size(); j++)
-            {
-              links.addElement(allFeatures[i].links.elementAt(j));
-            }
+            links = new Vector<String>();
+          }
+          for (int j = 0; j < sf.links.size(); j++)
+          {
+            links.addElement(sf.links.elementAt(j));
           }
         }
       }
@@ -1400,27 +1534,30 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
       if (av.getConservationSelected())
       {
-        SliderPanel.setConservationSlider(ap, av.getGlobalColourScheme(),
-                "Background");
+        SliderPanel.setConservationSlider(ap, av.getResidueShading(),
+                ap.getViewName());
       }
       if (av.getAbovePIDThreshold())
       {
-        SliderPanel.setPIDSliderSource(ap, av.getGlobalColourScheme(),
-                "Background");
+        SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
+                ap.getViewName());
       }
 
     }
   }
 
-  public void doMouseReleasedDefineMode(MouseEvent evt)
+  public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
   {
     if (stretchGroup == null)
     {
       return;
     }
-
-    stretchGroup.recalcConservation(); // always do this - annotation has own
-                                       // state
+    // always do this - annotation has own state
+    // but defer colourscheme update until hidden sequences are passed in
+    boolean vischange = stretchGroup.recalcConservation(true);
+    // here we rely on stretchGroup == av.getSelection()
+    needOverviewUpdate |= vischange && av.isSelectionDefinedGroup()
+            && afterDrag;
     if (stretchGroup.cs != null)
     {
       stretchGroup.cs.alignmentChanged(stretchGroup,
@@ -1431,17 +1568,18 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         SliderPanel.setConservationSlider(ap, stretchGroup.cs,
                 stretchGroup.getName());
       }
-      else
+      if (stretchGroup.cs.getThreshold() > 0)
       {
         SliderPanel.setPIDSliderSource(ap, stretchGroup.cs,
                 stretchGroup.getName());
       }
     }
+    PaintRefresher.Refresh(ap, av.getSequenceSetId());
+    ap.paintAlignment(needOverviewUpdate);
+    needOverviewUpdate = false;
     changeEndRes = false;
     changeStartRes = false;
     stretchGroup = null;
-    PaintRefresher.Refresh(ap, av.getSequenceSetId());
-    ap.paintAlignment(true);
     av.sendSelection();
   }
 
@@ -1493,6 +1631,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       if (res > (stretchGroup.getStartRes() - 1))
       {
         stretchGroup.setEndRes(res);
+        needOverviewUpdate |= av.isSelectionDefinedGroup();
       }
     }
     else if (changeStartRes)
@@ -1500,6 +1639,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       if (res < (stretchGroup.getEndRes() + 1))
       {
         stretchGroup.setStartRes(res);
+        needOverviewUpdate |= av.isSelectionDefinedGroup();
       }
     }
 
@@ -1533,6 +1673,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       if (stretchGroup.getSequences(null).contains(nextSeq))
       {
         stretchGroup.deleteSequence(seq, false);
+        needOverviewUpdate |= av.isSelectionDefinedGroup();
       }
       else
       {
@@ -1542,6 +1683,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         }
 
         stretchGroup.addSequence(nextSeq, false);
+        needOverviewUpdate |= av.isSelectionDefinedGroup();
       }
     }
 
@@ -1550,8 +1692,10 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       oldSeq = -1;
     }
 
-    if (res > av.endRes || res < av.startRes || y < av.startSeq
-            || y > av.endSeq)
+    if (res > av.getRanges().getEndRes()
+            || res < av.getRanges().getStartRes()
+            || y < av.getRanges().getStartSeq()
+            || y > av.getRanges().getEndSeq())
     {
       mouseExited(evt);
     }
@@ -1564,6 +1708,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     seqCanvas.repaint();
   }
 
+  @Override
   public void mouseEntered(MouseEvent e)
   {
     if (oldSeq < 0)
@@ -1578,6 +1723,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
   }
 
+  @Override
   public void mouseExited(MouseEvent e)
   {
     if (av.getWrapAlignment())
@@ -1637,6 +1783,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       running = false;
     }
 
+    @Override
     public void run()
     {
       running = true;
@@ -1646,25 +1793,27 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         if (evt != null)
         {
 
-          if (mouseDragging && evt.getY() < 0 && av.getStartSeq() > 0)
+          if (mouseDragging && evt.getY() < 0
+                  && av.getRanges().getStartSeq() > 0)
           {
-            running = ap.scrollUp(true);
+            running = av.getRanges().scrollUp(true);
           }
 
           if (mouseDragging && evt.getY() >= getSize().height
-                  && av.getAlignment().getHeight() > av.getEndSeq())
+                  && av.getAlignment().getHeight() > av.getRanges()
+                          .getEndSeq())
           {
-            running = ap.scrollUp(false);
+            running = av.getRanges().scrollUp(false);
           }
 
           if (mouseDragging && evt.getX() < 0)
           {
-            running = ap.scrollRight(false);
+            running = av.getRanges().scrollRight(false);
           }
 
           else if (mouseDragging && evt.getX() >= getSize().width)
           {
-            running = ap.scrollRight(true);
+            running = av.getRanges().scrollRight(true);
           }
         }
 
@@ -1681,24 +1830,38 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   /**
    * modify current selection according to a received message.
    */
+  @Override
   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
-          SelectionSource source)
+          HiddenColumns hidden, SelectionSource source)
   {
     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
     // handles selection messages...
     // TODO: extend config options to allow user to control if selections may be
     // shared between viewports.
     if (av != null
-            && (av == source || !av.followSelection || (source instanceof AlignViewport && ((AlignViewport) source)
+            && (av == source || !av.followSelection || (source instanceof AlignViewport && ((AlignmentViewport) source)
                     .getSequenceSetId().equals(av.getSequenceSetId()))))
     {
       return;
     }
+
+    /*
+     * Check for selection in a view of which this one is a dna/protein
+     * complement.
+     */
+    if (selectionFromTranslation(seqsel, colsel, hidden, source))
+    {
+      return;
+    }
+
     // do we want to thread this ? (contention with seqsel and colsel locks, I
     // suspect)
-    // rules are: colsel is copied if there is a real intersection between
-    // sequence selection
-    boolean repaint = false, copycolsel = true;
+    /*
+     * only copy colsel if there is a real intersection between
+     * sequence selection and this panel's alignment
+     */
+    boolean repaint = false;
+    boolean copycolsel = false;
     if (av.getSelectionGroup() == null || !av.isSelectionGroupChanged(true))
     {
       SequenceGroup sgroup = null;
@@ -1715,11 +1878,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         }
         sgroup = seqsel.intersect(av.getAlignment(),
                 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
-        if ((sgroup == null || sgroup.getSize() == 0)
-                && (colsel == null || colsel.size() == 0))
+        if ((sgroup != null && sgroup.getSize() > 0))
         {
-          // don't copy columns if the region didn't intersect.
-          copycolsel = false;
+          copycolsel = true;
         }
       }
       if (sgroup != null && sgroup.getSize() > 0)
@@ -1738,7 +1899,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     {
       // the current selection is unset or from a previous message
       // so import the new colsel.
-      if (colsel == null || colsel.size() == 0)
+      if (colsel == null || colsel.isEmpty())
       {
         if (av.getColumnSelection() != null)
         {
@@ -1754,15 +1915,16 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         }
         else
         {
-          av.getColumnSelection().setElementsFrom(colsel);
+          av.getColumnSelection().setElementsFrom(colsel,
+                  av.getAlignment().getHiddenColumns());
         }
       }
       repaint |= av.isColSelChanged(true);
     }
     if (copycolsel
             && av.hasHiddenColumns()
-            && (av.getColumnSelection() == null || av.getColumnSelection()
-                    .getHiddenColumns() == null))
+            && (av.getColumnSelection() == null || av.getAlignment()
+                    .getHiddenColumns().getHiddenRegions() == null))
     {
       System.err.println("Bad things");
     }
@@ -1782,8 +1944,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   public void scrollTo(int row, int column)
   {
 
-    row = row < 0 ? ap.av.startSeq : row;
-    column = column < 0 ? ap.av.startRes : column;
+    row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
+    column = column < 0 ? ap.av.getRanges().getStartRes() : column;
     ap.scrollTo(column, column, row, true, true);
   }
 
@@ -1795,8 +1957,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   public void scrollToRow(int row)
   {
 
-    row = row < 0 ? ap.av.startSeq : row;
-    ap.scrollTo(ap.av.startRes, ap.av.startRes, row, true, true);
+    row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
+    ap.scrollTo(ap.av.getRanges().getStartRes(), ap.av.getRanges()
+            .getStartRes(), row, true, true);
   }
 
   /**
@@ -1807,8 +1970,56 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   public void scrollToColumn(int column)
   {
 
-    column = column < 0 ? ap.av.startRes : column;
-    ap.scrollTo(column, column, ap.av.startSeq, true, true);
+    column = column < 0 ? ap.av.getRanges().getStartRes() : column;
+    ap.scrollTo(column, column, ap.av.getRanges().getStartSeq(), true, true);
+  }
+
+  /**
+   * If this panel is a cdna/protein translation view of the selection source,
+   * tries to map the source selection to a local one, and returns true. Else
+   * returns false.
+   * 
+   * @param seqsel
+   * @param colsel
+   * @param source
+   */
+  protected boolean selectionFromTranslation(SequenceGroup seqsel,
+          ColumnSelection colsel, HiddenColumns hidden,
+          SelectionSource source)
+  {
+    if (!(source instanceof AlignViewportI))
+    {
+      return false;
+    }
+    final AlignViewportI sourceAv = (AlignViewportI) source;
+    if (sourceAv.getCodingComplement() != av
+            && av.getCodingComplement() != sourceAv)
+    {
+      return false;
+    }
+
+    /*
+     * Map sequence selection
+     */
+    SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
+    av.setSelectionGroup(sg);
+    av.isSelectionGroupChanged(true);
+
+    /*
+     * Map column selection
+     */
+    // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
+    // av);
+    ColumnSelection cs = new ColumnSelection();
+    HiddenColumns hs = new HiddenColumns();
+    MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
+    av.setColumnSelection(cs);
+    av.getAlignment().setHiddenColumns(hs);
+
+    ap.scalePanelHolder.repaint();
+    ap.repaint();
+
+    return true;
   }
 
 }