Merge branch 'develop' into bug/JAL-2837
[jalview.git] / src / jalview / gui / SeqPanel.java
index d3ed93a..2cdb1d8 100644 (file)
@@ -73,9 +73,9 @@ import javax.swing.ToolTipManager;
  * @author $author$
  * @version $Revision: 1.130 $
  */
-public class SeqPanel extends JPanel implements MouseListener,
-        MouseMotionListener, MouseWheelListener, SequenceListener,
-        SelectionListener
+public class SeqPanel extends JPanel
+        implements MouseListener, MouseMotionListener, MouseWheelListener,
+        SequenceListener, SelectionListener
 
 {
   /** DOCUMENT ME!! */
@@ -84,6 +84,16 @@ public class SeqPanel extends JPanel implements MouseListener,
   /** DOCUMENT ME!! */
   public AlignmentPanel ap;
 
+  /*
+   * last column position for mouseMoved event
+   */
+  private int lastMouseColumn;
+
+  /*
+   * last sequence offset for mouseMoved event
+   */
+  private int lastMouseSeq;
+
   protected int lastres;
 
   protected int startseq;
@@ -170,6 +180,9 @@ public class SeqPanel extends JPanel implements MouseListener,
       ssm.addStructureViewerListener(this);
       ssm.addSelectionListener(this);
     }
+
+    lastMouseColumn = -1;
+    lastMouseSeq = -1;
   }
 
   int startWrapBlock = -1;
@@ -183,11 +196,12 @@ public class SeqPanel extends JPanel implements MouseListener,
    * @param evt
    * @return
    */
-  int findRes(MouseEvent evt)
+  int findColumn(MouseEvent evt)
   {
     int res = 0;
     int x = evt.getX();
 
+    int startRes = av.getRanges().getStartRes();
     if (av.getWrapAlignment())
     {
 
@@ -201,8 +215,8 @@ public class SeqPanel extends JPanel implements MouseListener,
               + hgap + seqCanvas.getAnnotationHeight();
 
       int y = evt.getY();
-      y -= hgap;
-      x -= seqCanvas.LABEL_WEST;
+      y = Math.max(0, y - hgap);
+      x = Math.max(0, x - seqCanvas.getLabelWidthWest());
 
       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
       if (cwidth < 1)
@@ -211,10 +225,11 @@ public class SeqPanel extends JPanel implements MouseListener,
       }
 
       wrappedBlock = y / cHeight;
-      wrappedBlock += av.getRanges().getStartRes() / cwidth;
-
-      res = wrappedBlock * cwidth + x / av.getCharWidth();
-
+      wrappedBlock += startRes / cwidth;
+      // allow for wrapped view scrolled right (possible from Overview)
+      int startOffset = startRes % cwidth;
+      res = wrappedBlock * cwidth + startOffset
+              + +Math.min(cwidth - 1, x / av.getCharWidth());
     }
     else
     {
@@ -224,7 +239,7 @@ public class SeqPanel extends JPanel implements MouseListener,
         // right-hand gutter
         x = seqCanvas.getX() + seqCanvas.getWidth();
       }
-      res = (x / av.getCharWidth()) + av.getRanges().getStartRes();
+      res = (x / av.getCharWidth()) + startRes;
       if (res > av.getRanges().getEndRes())
       {
         // moused off right
@@ -260,15 +275,14 @@ public class SeqPanel extends JPanel implements MouseListener,
 
       y -= hgap;
 
-      seq = Math.min((y % cHeight) / av.getCharHeight(), av.getAlignment()
-              .getHeight() - 1);
+      seq = Math.min((y % cHeight) / av.getCharHeight(),
+              av.getAlignment().getHeight() - 1);
     }
     else
     {
-      seq = Math.min((y / av.getCharHeight())
-              + av.getRanges().getStartSeq(),
-              av
-              .getAlignment().getHeight() - 1);
+      seq = Math.min(
+              (y / av.getCharHeight()) + av.getRanges().getStartSeq(),
+              av.getAlignment().getHeight() - 1);
     }
 
     return seq;
@@ -285,8 +299,8 @@ public class SeqPanel extends JPanel implements MouseListener,
       if (editCommand != null && editCommand.getSize() > 0)
       {
         ap.alignFrame.addHistoryItem(editCommand);
-        av.firePropertyChange("alignment", null, av.getAlignment()
-                .getSequences());
+        av.firePropertyChange("alignment", null,
+                av.getAlignment().getSequences());
       }
     } finally
     {
@@ -306,13 +320,13 @@ public class SeqPanel extends JPanel implements MouseListener,
   void setCursorRow()
   {
     seqCanvas.cursorY = getKeyboardNo1() - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void setCursorColumn()
   {
     seqCanvas.cursorX = getKeyboardNo1() - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void setCursorRowAndColumn()
@@ -325,7 +339,7 @@ public class SeqPanel extends JPanel implements MouseListener,
     {
       seqCanvas.cursorX = getKeyboardNo1() - 1;
       seqCanvas.cursorY = getKeyboardNo2() - 1;
-      scrollToVisible();
+      scrollToVisible(true);
     }
   }
 
@@ -334,7 +348,7 @@ public class SeqPanel extends JPanel implements MouseListener,
     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
 
     seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void moveCursor(int dx, int dy)
@@ -344,8 +358,7 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
 
-    if (av.hasHiddenColumns()
- && !hidden.isVisible(seqCanvas.cursorX))
+    if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
     {
       int original = seqCanvas.cursorX - dx;
       int maxWidth = av.getAlignment().getWidth();
@@ -363,10 +376,16 @@ public class SeqPanel extends JPanel implements MouseListener,
       }
     }
 
-    scrollToVisible();
+    scrollToVisible(false);
   }
 
-  void scrollToVisible()
+  /**
+   * Scroll to make the cursor visible in the viewport.
+   * 
+   * @param jump
+   *          just jump to the location rather than scrolling
+   */
+  void scrollToVisible(boolean jump)
   {
     if (seqCanvas.cursorX < 0)
     {
@@ -387,21 +406,44 @@ public class SeqPanel extends JPanel implements MouseListener,
     }
 
     endEditing();
-    if (av.getWrapAlignment())
+
+    boolean repaintNeeded = true;
+    if (jump)
     {
-      av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
+      // only need to repaint if the viewport did not move, as otherwise it will
+      // get a repaint
+      repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
+              seqCanvas.cursorY);
     }
     else
     {
-      av.getRanges().scrollToVisible(seqCanvas.cursorX, seqCanvas.cursorY,
-              av);
+      if (av.getWrapAlignment())
+      {
+        // scrollToWrappedVisible expects x-value to have hidden cols subtracted
+        int x = av.getAlignment().getHiddenColumns()
+                .findColumnPosition(seqCanvas.cursorX);
+        av.getRanges().scrollToWrappedVisible(x);
+      }
+      else
+      {
+        av.getRanges().scrollToVisible(seqCanvas.cursorX,
+                seqCanvas.cursorY);
+      }
     }
-    setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
+
+    if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
+    {
+      setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
             seqCanvas.cursorX, seqCanvas.cursorY);
+    }
 
-    seqCanvas.repaint();
+    if (repaintNeeded)
+    {
+      seqCanvas.repaint();
+    }
   }
 
+
   void setSelectionAreaAtCursor(boolean topLeft)
   {
     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
@@ -472,7 +514,7 @@ public class SeqPanel extends JPanel implements MouseListener,
       av.setSelectionGroup(sg);
     }
 
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     av.sendSelection();
   }
 
@@ -564,6 +606,7 @@ public class SeqPanel extends JPanel implements MouseListener,
   @Override
   public void mouseReleased(MouseEvent evt)
   {
+    boolean didDrag = mouseDragging; // did we come here after a drag
     mouseDragging = false;
     mouseWheelPressed = false;
 
@@ -576,7 +619,7 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     if (!editingSeqs)
     {
-      doMouseReleasedDefineMode(evt);
+      doMouseReleasedDefineMode(evt, didDrag);
       return;
     }
 
@@ -616,7 +659,7 @@ public class SeqPanel extends JPanel implements MouseListener,
     }
 
     int seq = findSeq(evt);
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (seq < 0 || res < 0)
     {
@@ -668,6 +711,8 @@ public class SeqPanel extends JPanel implements MouseListener,
     }
     lastSearchResults = results;
 
+    boolean wasScrolled = false;
+
     if (av.isFollowHighlight())
     {
       // don't allow highlight of protein/cDNA to also scroll a complementary
@@ -675,14 +720,19 @@ public class SeqPanel extends JPanel implements MouseListener,
       // 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, false))
+      wasScrolled = ap.scrollToPosition(results, false);
+      if (wasScrolled)
       {
         seqCanvas.revalidate();
       }
       ap.setToScrollComplementPanel(true);
     }
-    setStatusMessage(results);
-    seqCanvas.highlightSearchResults(results);
+
+    boolean noFastPaint = wasScrolled && av.getWrapAlignment();
+    if (seqCanvas.highlightSearchResults(results, noFastPaint))
+    {
+      setStatusMessage(results);
+    }
   }
 
   @Override
@@ -699,10 +749,12 @@ public class SeqPanel extends JPanel implements MouseListener,
   }
 
   /**
-   * DOCUMENT ME!
+   * Action on mouse movement is to update the status bar to show the current
+   * sequence position, and (if features are shown) to show any features at the
+   * position in a tooltip. Does nothing if the mouse move does not change
+   * residue position.
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseMoved(MouseEvent evt)
@@ -714,25 +766,39 @@ public class SeqPanel extends JPanel implements MouseListener,
       mouseDragged(evt);
     }
 
-    int res = findRes(evt);
-    int seq = findSeq(evt);
-    int pos;
-    if (res < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
+    final int column = findColumn(evt);
+    final int seq = findSeq(evt);
+
+    if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
+    {
+      lastMouseSeq = -1;
+      return;
+    }
+    if (column == lastMouseColumn && seq == lastMouseSeq)
     {
+      /*
+       * just a pixel move without change of residue
+       */
       return;
     }
+    lastMouseColumn = column;
+    lastMouseSeq = seq;
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
-    if (res >= sequence.getLength())
+    if (column >= sequence.getLength())
     {
       return;
     }
 
-    pos = setStatusMessage(sequence, res, seq);
-    if (ssm != null && pos > -1)
+    /*
+     * set status bar message, returning residue position in sequence
+     */
+    boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
+    final int pos = setStatusMessage(sequence, column, seq);
+    if (ssm != null && !isGapped)
     {
-      mouseOverSequence(sequence, res, pos);
+      mouseOverSequence(sequence, column, pos);
     }
 
     tooltipText.setLength(6); // Cuts the buffer back to <html>
@@ -742,7 +808,8 @@ public class SeqPanel extends JPanel implements MouseListener,
     {
       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"))
@@ -758,14 +825,16 @@ public class SeqPanel extends JPanel implements MouseListener,
       }
     }
 
-    // use aa to see if the mouse pointer is on a
+    /*
+     * add any features at the position to the tooltip; if over a gap, only
+     * add features that straddle the gap (pos may be the residue before or
+     * after the gap)
+     */
     if (av.isShowSequenceFeatures())
     {
-      int rpos;
       List<SequenceFeature> features = ap.getFeatureRenderer()
-              .findFeaturesAtRes(sequence.getDatasetSequence(),
-                      rpos = sequence.findPosition(res));
-      seqARep.appendFeatures(tooltipText, rpos, features,
+              .findFeaturesAtColumn(sequence, column + 1);
+      seqARep.appendFeatures(tooltipText, pos, features,
               this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
     }
     if (tooltipText.length() == 6) // <html>
@@ -775,18 +844,15 @@ public class SeqPanel extends JPanel implements MouseListener,
     }
     else
     {
-      if (lastTooltip == null
-              || !lastTooltip.equals(tooltipText.toString()))
+      String textString = tooltipText.toString();
+      if (lastTooltip == null || !lastTooltip.equals(textString))
       {
-        String formatedTooltipText = JvSwingUtils.wrapTooltip(true,
-                tooltipText.toString());
-        // String formatedTooltipText = tooltipText.toString();
-        setToolTipText(formatedTooltipText);
-        lastTooltip = tooltipText.toString();
+        String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
+                textString);
+        setToolTipText(formattedTooltipText);
+        lastTooltip = textString;
       }
-
     }
-
   }
 
   private Point lastp = null;
@@ -805,8 +871,9 @@ public class SeqPanel extends JPanel implements MouseListener,
     Point p = lastp;
     if (!event.isShiftDown() || p == null)
     {
-      p = (tooltipText != null && tooltipText.length() > 6) ? new Point(
-              event.getX() + wdth, event.getY() - 20) : null;
+      p = (tooltipText != null && tooltipText.length() > 6)
+              ? new Point(event.getX() + wdth, event.getY() - 20)
+              : null;
     }
     /*
      * TODO: try to modify position region is not obcured by tooltip
@@ -818,11 +885,12 @@ public class SeqPanel extends JPanel implements MouseListener,
 
   /**
    * 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
+   * shading in overviews and structures to be recalculated. this could be
+   * changed to a something more expressive that indicates what actually has
+   * changed, so selective redraws can be applied (ie. only structures, only
+   * overview, etc)
    */
-  private boolean needOverviewUpdate = false; // TODO: refactor to avcontroller
+  private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
 
   /**
    * set if av.getSelectionGroup() refers to a group that is defined on the
@@ -832,59 +900,88 @@ public class SeqPanel extends JPanel implements MouseListener,
   // avcontroller or viewModel
 
   /**
-   * Set status message in alignment panel
+   * Sets the status message in alignment panel, showing the sequence number
+   * (index) and id, and residue and residue position if not at a gap, for the
+   * given sequence and column position. Returns the residue position returned
+   * by Sequence.findPosition. Note this may be for the nearest adjacent residue
+   * if at a gapped position.
    * 
    * @param sequence
    *          aligned sequence object
-   * @param res
+   * @param column
    *          alignment column
-   * @param seq
+   * @param seqIndex
    *          index of sequence in alignment
-   * @return position of res in sequence
+   * @return sequence position of residue at column, or adjacent residue if at a
+   *         gap
+   */
+  int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
+  {
+    char sequenceChar = sequence.getCharAt(column);
+    int pos = sequence.findPosition(column);
+    setStatusMessage(sequence, seqIndex, sequenceChar, pos);
+
+    return pos;
+  }
+
+  /**
+   * Builds the status message for the current cursor location and writes it to
+   * the status bar, for example
+   * 
+   * <pre>
+   * Sequence 3 ID: FER1_SOLLC
+   * Sequence 5 ID: FER1_PEA Residue: THR (4)
+   * Sequence 5 ID: FER1_PEA Residue: B (3)
+   * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
+   * </pre>
+   * 
+   * @param sequence
+   * @param seqIndex
+   *          sequence position in the alignment (1..)
+   * @param sequenceChar
+   *          the character under the cursor
+   * @param residuePos
+   *          the sequence residue position (if not over a gap)
    */
-  int setStatusMessage(SequenceI sequence, int res, int seq)
+  protected void setStatusMessage(SequenceI sequence, int seqIndex,
+          char sequenceChar, int residuePos)
   {
     StringBuilder text = new StringBuilder(32);
 
     /*
      * Sequence number (if known), and sequence name.
      */
-    String seqno = seq == -1 ? "" : " " + (seq + 1);
+    String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
     text.append("Sequence").append(seqno).append(" ID: ")
             .append(sequence.getName());
 
     String residue = null;
+
     /*
      * Try to translate the display character to residue name (null for gap).
      */
-    final String displayChar = String.valueOf(sequence.getCharAt(res));
-    if (av.getAlignment().isNucleotide())
+    boolean isGapped = Comparison.isGap(sequenceChar);
+
+    if (!isGapped)
     {
-      residue = ResidueProperties.nucleotideName.get(displayChar);
-      if (residue != null)
+      boolean nucleotide = av.getAlignment().isNucleotide();
+      String displayChar = String.valueOf(sequenceChar);
+      if (nucleotide)
       {
-        text.append(" Nucleotide: ").append(residue);
+        residue = ResidueProperties.nucleotideName.get(displayChar);
       }
-    }
-    else
-    {
-      residue = "X".equalsIgnoreCase(displayChar) ? "X" : ("*"
-              .equals(displayChar) ? "STOP" : ResidueProperties.aa2Triplet
-              .get(displayChar));
-      if (residue != null)
+      else
       {
-        text.append(" Residue: ").append(residue);
+        residue = "X".equalsIgnoreCase(displayChar) ? "X"
+                : ("*".equals(displayChar) ? "STOP"
+                        : ResidueProperties.aa2Triplet.get(displayChar));
       }
-    }
+      text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
+              .append(": ").append(residue == null ? displayChar : residue);
 
-    int pos = -1;
-    if (residue != null)
-    {
-      pos = sequence.findPosition(res);
-      text.append(" (").append(Integer.toString(pos)).append(")");
+      text.append(" (").append(Integer.toString(residuePos)).append(")");
     }
     ap.alignFrame.statusBar.setText(text.toString());
-    return pos;
   }
 
   /**
@@ -912,12 +1009,9 @@ public class SeqPanel extends JPanel implements MouseListener,
 
       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);
+        int start = m.getStart();
+        setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
+                start);
         return;
       }
     }
@@ -937,8 +1031,8 @@ public class SeqPanel extends JPanel implements MouseListener,
       int oldWidth = av.getCharWidth();
 
       // Which is bigger, left-right or up-down?
-      if (Math.abs(evt.getY() - lastMousePress.getY()) > Math.abs(evt
-              .getX() - lastMousePress.getX()))
+      if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
+              .abs(evt.getX() - lastMousePress.getX()))
       {
         /*
          * on drag up or down, decrement or increment font size
@@ -997,7 +1091,7 @@ public class SeqPanel extends JPanel implements MouseListener,
         }
         if (newWidth > 0)
         {
-          ap.paintAlignment(false);
+          ap.paintAlignment(false, false);
           if (copyChanges)
           {
             /*
@@ -1028,7 +1122,7 @@ public class SeqPanel extends JPanel implements MouseListener,
       return;
     }
 
-    int res = findRes(evt);
+    int res = findColumn(evt);
 
     if (res < 0)
     {
@@ -1051,7 +1145,7 @@ public class SeqPanel extends JPanel implements MouseListener,
     }
 
     mouseDragging = true;
-    if (scrollThread != null)
+    if ((scrollThread != null) && (scrollThread.isRunning()))
     {
       scrollThread.setEvent(evt);
     }
@@ -1098,8 +1192,9 @@ public class SeqPanel extends JPanel implements MouseListener,
       }
       if (editCommand == null)
       {
-        editCommand = new EditCommand(MessageManager.formatMessage(
-                "label.edit_params", new String[] { label }));
+        editCommand = new EditCommand(MessageManager
+                .formatMessage("label.edit_params", new String[]
+                { label }));
       }
     }
 
@@ -1116,9 +1211,8 @@ public class SeqPanel extends JPanel implements MouseListener,
     ap.alignFrame.statusBar.setText(message.toString());
 
     // Are we editing within a selection group?
-    if (groupEditing
-            || (sg != null && sg.getSequences(av.getHiddenRepSequences())
-                    .contains(seq)))
+    if (groupEditing || (sg != null
+            && sg.getSequences(av.getHiddenRepSequences()).contains(seq)))
     {
       fixedColumns = true;
 
@@ -1214,7 +1308,7 @@ public class SeqPanel extends JPanel implements MouseListener,
         // Find the next gap before the end
         // of the visible region boundary
         boolean blank = false;
-        for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--)
+        for (; fixedRight > lastres; fixedRight--)
         {
           blank = true;
 
@@ -1306,8 +1400,8 @@ public class SeqPanel extends JPanel implements MouseListener,
         }
         else
         {
-          appendEdit(Action.INSERT_GAP, groupSeqs, startres, startres
-                  - lastres);
+          appendEdit(Action.INSERT_GAP, groupSeqs, startres,
+                  startres - lastres);
         }
       }
       else
@@ -1322,8 +1416,8 @@ public class SeqPanel extends JPanel implements MouseListener,
         }
         else
         {
-          appendEdit(Action.DELETE_GAP, groupSeqs, startres, lastres
-                  - startres);
+          appendEdit(Action.DELETE_GAP, groupSeqs, startres,
+                  lastres - startres);
         }
 
       }
@@ -1477,9 +1571,9 @@ public class SeqPanel extends JPanel implements MouseListener,
       oldSeq = 0;
     }
 
-    if (scrollThread != null)
+    if ((scrollThread != null) && (scrollThread.isRunning()))
     {
-      scrollThread.running = false;
+      scrollThread.stopScrolling();
       scrollThread = null;
     }
   }
@@ -1498,7 +1592,7 @@ public class SeqPanel extends JPanel implements MouseListener,
       return;
     }
 
-    if (mouseDragging)
+    if (mouseDragging && scrollThread == null)
     {
       scrollThread = new ScrollThread();
     }
@@ -1523,9 +1617,14 @@ public class SeqPanel extends JPanel implements MouseListener,
         av.setSelectionGroup(null);
       }
 
+      int column = findColumn(evt);
+
+      /*
+       * find features at the position (if not gapped), or straddling
+       * the position (if at a gap)
+       */
       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
-              .findFeaturesAtRes(sequence.getDatasetSequence(),
-                      sequence.findPosition(findRes(evt)));
+              .findFeaturesAtColumn(sequence, column + 1);
 
       if (!features.isEmpty())
       {
@@ -1535,7 +1634,7 @@ public class SeqPanel extends JPanel implements MouseListener,
         SearchResultsI highlight = new SearchResults();
         highlight.addResult(sequence, features.get(0).getBegin(), features
                 .get(0).getEnd());
-        seqCanvas.highlightSearchResults(highlight);
+        seqCanvas.highlightSearchResults(highlight, false);
 
         /*
          * open the Amend Features dialog; clear highlighting afterwards,
@@ -1543,8 +1642,9 @@ public class SeqPanel extends JPanel implements MouseListener,
          */
         List<SequenceI> seqs = Collections.singletonList(sequence);
         seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
-                ap, null);
-        seqCanvas.highlightSearchResults(null);
+                ap);
+        av.setSearchResults(null); // clear highlighting
+        seqCanvas.repaint(); // draw new/amended features
       }
     }
   }
@@ -1576,7 +1676,13 @@ public class SeqPanel extends JPanel implements MouseListener,
         av.getRanges().scrollUp(true);
       }
     }
-    // TODO Update tooltip for new position.
+
+    /*
+     * update status bar and tooltip for new position
+     * (need to synthesize a mouse movement to refresh tooltip)
+     */
+    mouseMoved(e);
+    ToolTipManager.sharedInstance().mouseMoved(e);
   }
 
   /**
@@ -1587,17 +1693,18 @@ public class SeqPanel extends JPanel implements MouseListener,
    */
   public void doMousePressedDefineMode(MouseEvent evt)
   {
-    final int res = findRes(evt);
+    final int res = findColumn(evt);
     final int seq = findSeq(evt);
     oldSeq = seq;
-    needOverviewUpdate = false;
+    updateOverviewAndStructs = false;
 
     startWrapBlock = wrappedBlock;
 
     if (av.getWrapAlignment() && seq > av.getAlignment().getHeight())
     {
-      JvOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
-              .getString("label.cannot_edit_annotations_in_wrapped_view"),
+      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
+              MessageManager.getString(
+                      "label.cannot_edit_annotations_in_wrapped_view"),
               MessageManager.getString("label.wrapped_view_no_edit"),
               JvOptionPane.WARNING_MESSAGE);
       return;
@@ -1646,7 +1753,7 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     if (av.cursorMode)
     {
-      seqCanvas.cursorX = findRes(evt);
+      seqCanvas.cursorX = findColumn(evt);
       seqCanvas.cursorY = findSeq(evt);
       seqCanvas.repaint();
       return;
@@ -1654,43 +1761,53 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     if (stretchGroup == null)
     {
-      // Only if left mouse button do we want to change group sizes
+      createStretchGroup(res, sequence);
+    }
 
-      // define a new group here
-      SequenceGroup sg = new SequenceGroup();
-      sg.setStartRes(res);
-      sg.setEndRes(res);
-      sg.addSequence(sequence, false);
-      av.setSelectionGroup(sg);
-      stretchGroup = sg;
+    if (stretchGroup != null)
+    {
+      stretchGroup.addPropertyChangeListener(seqCanvas);
+    }
 
-      if (av.getConservationSelected())
-      {
-        SliderPanel.setConservationSlider(ap, av.getResidueShading(),
-                ap.getViewName());
-      }
+    seqCanvas.repaint();
+  }
 
-      if (av.getAbovePIDThreshold())
-      {
-        SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
-                ap.getViewName());
-      }
-      // TODO: stretchGroup will always be not null. Is this a merge error ?
-      if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
-      {
-        // Edit end res position of selected group
-        changeEndRes = true;
-      }
-      else if ((stretchGroup != null)
-              && (stretchGroup.getStartRes() == res))
-      {
-        // Edit end res position of selected group
-        changeStartRes = true;
-      }
-      stretchGroup.getWidth();
+  private void createStretchGroup(int res, SequenceI sequence)
+  {
+    // Only if left mouse button do we want to change group sizes
+    // define a new group here
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(res);
+    sg.setEndRes(res);
+    sg.addSequence(sequence, false);
+    av.setSelectionGroup(sg);
+    stretchGroup = sg;
+
+    if (av.getConservationSelected())
+    {
+      SliderPanel.setConservationSlider(ap, av.getResidueShading(),
+              ap.getViewName());
     }
 
-    seqCanvas.repaint();
+    if (av.getAbovePIDThreshold())
+    {
+      SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
+              ap.getViewName());
+    }
+    // TODO: stretchGroup will always be not null. Is this a merge error ?
+    // or is there a threading issue here?
+    if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
+    {
+      // Edit end res position of selected group
+      changeEndRes = true;
+    }
+    else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
+    {
+      // Edit end res position of selected group
+      changeStartRes = true;
+    }
+    stretchGroup.getWidth();
+
   }
 
   /**
@@ -1702,13 +1819,12 @@ public class SeqPanel extends JPanel implements MouseListener,
    */
   void showPopupMenu(MouseEvent evt)
   {
-    final int res = findRes(evt);
+    final int column = findColumn(evt);
     final int seq = findSeq(evt);
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
     List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
-            .findFeaturesAtRes(sequence.getDatasetSequence(),
-                    sequence.findPosition(res));
-    List<String> links = new ArrayList<String>();
+            .findFeaturesAtColumn(sequence, column + 1);
+    List<String> links = new ArrayList<>();
     for (SequenceFeature sf : allFeatures)
     {
       if (sf.links != null)
@@ -1725,27 +1841,35 @@ public class SeqPanel extends JPanel implements MouseListener,
   }
 
   /**
-   * DOCUMENT ME!
+   * Update the display after mouse up on a selection or group
    * 
    * @param evt
-   *          DOCUMENT ME!
+   *          mouse released event details
+   * @param afterDrag
+   *          true if this event is happening after a mouse drag (rather than a
+   *          mouse down)
    */
-  public void doMouseReleasedDefineMode(MouseEvent evt)
+  public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
   {
     if (stretchGroup == null)
     {
       return;
     }
+
+    stretchGroup.removePropertyChangeListener(seqCanvas);
+
     // always do this - annotation has own state
     // but defer colourscheme update until hidden sequences are passed in
     boolean vischange = stretchGroup.recalcConservation(true);
-    needOverviewUpdate |= vischange && av.isSelectionDefinedGroup();
+    updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
+            && afterDrag;
     if (stretchGroup.cs != null)
     {
       stretchGroup.cs.alignmentChanged(stretchGroup,
               av.getHiddenRepSequences());
 
-      ResidueShaderI groupColourScheme = stretchGroup.getGroupColourScheme();
+      ResidueShaderI groupColourScheme = stretchGroup
+              .getGroupColourScheme();
       String name = stretchGroup.getName();
       if (stretchGroup.cs.conservationApplied())
       {
@@ -1757,8 +1881,10 @@ public class SeqPanel extends JPanel implements MouseListener,
       }
     }
     PaintRefresher.Refresh(this, av.getSequenceSetId());
-    ap.paintAlignment(needOverviewUpdate);
-    needOverviewUpdate = false;
+    // TODO: structure colours only need updating if stretchGroup used to or now
+    // does contain sequences with structure views
+    ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
+    updateOverviewAndStructs = false;
     changeEndRes = false;
     changeStartRes = false;
     stretchGroup = null;
@@ -1773,7 +1899,7 @@ public class SeqPanel extends JPanel implements MouseListener,
    */
   public void doMouseDraggedDefineMode(MouseEvent evt)
   {
-    int res = findRes(evt);
+    int res = findColumn(evt);
     int y = findSeq(evt);
 
     if (wrappedBlock != startWrapBlock)
@@ -1812,7 +1938,7 @@ public class SeqPanel extends JPanel implements MouseListener,
       if (res > (stretchGroup.getStartRes() - 1))
       {
         stretchGroup.setEndRes(res);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
     }
     else if (changeStartRes)
@@ -1820,7 +1946,7 @@ public class SeqPanel extends JPanel implements MouseListener,
       if (res < (stretchGroup.getEndRes() + 1))
       {
         stretchGroup.setStartRes(res);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
     }
 
@@ -1854,7 +1980,7 @@ public class SeqPanel extends JPanel implements MouseListener,
       if (stretchGroup.getSequences(null).contains(nextSeq))
       {
         stretchGroup.deleteSequence(seq, false);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
       else
       {
@@ -1864,7 +1990,7 @@ public class SeqPanel extends JPanel implements MouseListener,
         }
 
         stretchGroup.addSequence(nextSeq, false);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
     }
 
@@ -1875,21 +2001,19 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     mouseDragging = true;
 
-    if (scrollThread != null)
+    if ((scrollThread != null) && (scrollThread.isRunning()))
     {
       scrollThread.setEvent(evt);
     }
-
-    seqCanvas.repaint();
   }
 
   void scrollCanvas(MouseEvent evt)
   {
     if (evt == null)
     {
-      if (scrollThread != null)
+      if ((scrollThread != null) && (scrollThread.isRunning()))
       {
-        scrollThread.running = false;
+        scrollThread.stopScrolling();
         scrollThread = null;
       }
       mouseDragging = false;
@@ -1912,7 +2036,7 @@ public class SeqPanel extends JPanel implements MouseListener,
   {
     MouseEvent evt;
 
-    boolean running = false;
+    private volatile boolean threadRunning = true;
 
     public ScrollThread()
     {
@@ -1926,38 +2050,40 @@ public class SeqPanel extends JPanel implements MouseListener,
 
     public void stopScrolling()
     {
-      running = false;
+      threadRunning = false;
+    }
+
+    public boolean isRunning()
+    {
+      return threadRunning;
     }
 
     @Override
     public void run()
     {
-      running = true;
-
-      while (running)
+      while (threadRunning)
       {
         if (evt != null)
         {
           if (mouseDragging && (evt.getY() < 0)
                   && (av.getRanges().getStartSeq() > 0))
           {
-            running = av.getRanges().scrollUp(true);
+            av.getRanges().scrollUp(true);
           }
 
-          if (mouseDragging && (evt.getY() >= getHeight())
-                  && (av.getAlignment().getHeight() > av.getRanges()
-                          .getEndSeq()))
+          if (mouseDragging && (evt.getY() >= getHeight()) && (av
+                  .getAlignment().getHeight() > av.getRanges().getEndSeq()))
           {
-            running = av.getRanges().scrollUp(false);
+            av.getRanges().scrollUp(false);
           }
 
           if (mouseDragging && (evt.getX() < 0))
           {
-            running = av.getRanges().scrollRight(false);
+            av.getRanges().scrollRight(false);
           }
           else if (mouseDragging && (evt.getX() >= getWidth()))
           {
-            running = av.getRanges().scrollRight(true);
+            av.getRanges().scrollRight(true);
           }
         }
 
@@ -1982,8 +2108,10 @@ public class SeqPanel extends JPanel implements MouseListener,
     // handles selection messages...
     // TODO: extend config options to allow user to control if selections may be
     // shared between viewports.
-    boolean iSentTheSelection = (av == source || (source instanceof AlignViewport && ((AlignmentViewport) source)
-            .getSequenceSetId().equals(av.getSequenceSetId())));
+    boolean iSentTheSelection = (av == source
+            || (source instanceof AlignViewport
+                    && ((AlignmentViewport) source).getSequenceSetId()
+                            .equals(av.getSequenceSetId())));
 
     if (iSentTheSelection)
     {
@@ -1993,11 +2121,13 @@ public class SeqPanel extends JPanel implements MouseListener,
         ap.getCalculationDialog().validateCalcTypes();
       }
 
-      // process further ?
-      if (!av.followSelection)
-      {
-        return;
-      }
+      return;
+    }
+
+    // process further ?
+    if (!av.followSelection)
+    {
+      return;
     }
 
     /*
@@ -2083,10 +2213,8 @@ public class SeqPanel extends JPanel implements MouseListener,
       repaint = true;
     }
 
-    if (copycolsel
-            && av.hasHiddenColumns()
-            && (av.getAlignment().getHiddenColumns() == null || av
-                    .getAlignment().getHiddenColumns().getHiddenRegions() == null))
+    if (copycolsel && av.hasHiddenColumns()
+            && (av.getAlignment().getHiddenColumns() == null))
     {
       System.err.println("Bad things");
     }