JAL-3638 shift+arrow keys in cursor mode jumps cursor in a gapped region to next...
[jalview.git] / src / jalview / gui / SeqPanel.java
index 7abbd7d..407acd6 100644 (file)
  */
 package jalview.gui;
 
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Point;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+import javax.swing.ToolTipManager;
+
 import jalview.api.AlignViewportI;
 import jalview.bin.Cache;
 import jalview.commands.EditCommand;
@@ -29,6 +47,7 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
@@ -49,23 +68,8 @@ import jalview.util.MappingUtils;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.viewmodel.AlignmentViewport;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Font;
-import java.awt.FontMetrics;
-import java.awt.Point;
-import java.awt.event.MouseEvent;
-import java.awt.event.MouseListener;
-import java.awt.event.MouseMotionListener;
-import java.awt.event.MouseWheelEvent;
-import java.awt.event.MouseWheelListener;
-import java.util.Collections;
-import java.util.List;
-
-import javax.swing.JPanel;
-import javax.swing.SwingUtilities;
-import javax.swing.ToolTipManager;
+import jalview.viewmodel.ViewportRanges;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 /**
  * DOCUMENT ME!
@@ -203,8 +207,6 @@ public class SeqPanel extends JPanel
 
   StringBuffer keyboardNo2;
 
-  java.net.URL linkImageURL;
-
   private final SequenceAnnotationReport seqARep;
 
   StringBuilder tooltipText = new StringBuilder();
@@ -225,8 +227,7 @@ public class SeqPanel extends JPanel
    */
   public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
   {
-    linkImageURL = getClass().getResource("/images/link.gif");
-    seqARep = new SequenceAnnotationReport(linkImageURL.toString());
+    seqARep = new SequenceAnnotationReport(true);
     ToolTipManager.sharedInstance().registerComponent(this);
     ToolTipManager.sharedInstance().setInitialDelay(0);
     ToolTipManager.sharedInstance().setDismissDelay(10000);
@@ -313,8 +314,10 @@ public class SeqPanel extends JPanel
     }
     else
     {
-      seqIndex = Math.min((y / charHeight) + av.getRanges().getStartSeq(),
+      ViewportRanges ranges = av.getRanges();
+      seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
               alignmentHeight - 1);
+      seqIndex = Math.min(seqIndex, ranges.getEndSeq());
     }
 
     return new MousePos(col, seqIndex, annIndex);
@@ -457,47 +460,80 @@ public class SeqPanel extends JPanel
 
   void moveCursor(int dx, int dy)
   {
-    seqCanvas.cursorX += dx;
-    seqCanvas.cursorY += dy;
-
+    moveCursor(dx, dy,false);
+  }
+  void moveCursor(int dx, int dy, boolean nextWord)
+  {
     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
 
-    if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
+    if (nextWord)
     {
-      int original = seqCanvas.cursorX - dx;
       int maxWidth = av.getAlignment().getWidth();
-
-      if (!hidden.isVisible(seqCanvas.cursorX))
-      {
-        int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
-        int[] region = hidden.getRegionWithEdgeAtRes(visx);
-
-        if (region != null) // just in case
+      int maxHeight=av.getAlignment().getHeight();
+      SequenceI seqAtRow = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
+      // look for next gap or residue
+      boolean isGap = Comparison.isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
+      int p = seqCanvas.cursorX,lastP,r=seqCanvas.cursorY,lastR;
+      do
+      {
+        lastP = p;
+        lastR = r;
+        if (dy != 0)
         {
-          if (dx == 1)
+          r += dy;
+          if (r < 0)
           {
-            // moving right
-            seqCanvas.cursorX = region[1] + 1;
+            r = 0;
           }
-          else if (dx == -1)
+          if (r >= maxHeight)
           {
-            // moving left
-            seqCanvas.cursorX = region[0] - 1;
+            r = maxHeight - 1;
           }
+          seqAtRow = av.getAlignment().getSequenceAt(r);
         }
-        seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
-      }
+        p = nextVisible(hidden, maxWidth, p, dx);
+      } while ((dx != 0 ? p != lastP : r != lastR)
+              && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
+      seqCanvas.cursorX=p;
+      seqCanvas.cursorY=r;
+    } else {
+      int maxWidth = av.getAlignment().getWidth();
+      seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX, dx);
+      seqCanvas.cursorY += dy;
+    }
+    scrollToVisible(false);
+  }
 
-      if (seqCanvas.cursorX >= maxWidth
-              || !hidden.isVisible(seqCanvas.cursorX))
+  private int nextVisible(HiddenColumns hidden,int maxWidth, int original, int dx)
+  {
+    int newCursorX=original+dx;
+    if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
+    {
+      int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
+      int[] region = hidden.getRegionWithEdgeAtRes(visx);
+
+      if (region != null) // just in case
       {
-        seqCanvas.cursorX = original;
+        if (dx == 1)
+        {
+          // moving right
+          newCursorX = region[1] + 1;
+        }
+        else if (dx == -1)
+        {
+          // moving left
+          newCursorX = region[0] - 1;
+        }
       }
     }
-
-    scrollToVisible(false);
+    newCursorX = (newCursorX < 0) ? 0 : newCursorX;
+    if (newCursorX >= maxWidth
+            || !hidden.isVisible(newCursorX))
+    {
+      newCursorX = original;
+    }
+    return newCursorX;
   }
-
   /**
    * Scroll to make the cursor visible in the viewport.
    * 
@@ -742,13 +778,14 @@ public class SeqPanel extends JPanel
       return;
     }
 
-    if (!editingSeqs)
+    if (editingSeqs)
+    {
+      endEditing();
+    }
+    else
     {
       doMouseReleasedDefineMode(evt, didDrag);
-      return;
     }
-
-    endEditing();
   }
 
   /**
@@ -828,11 +865,11 @@ public class SeqPanel extends JPanel
    * the start of the highlighted region.
    */
   @Override
-  public void highlightSequence(SearchResultsI results)
+  public String highlightSequence(SearchResultsI results)
   {
     if (results == null || results.equals(lastSearchResults))
     {
-      return;
+      return null;
     }
     lastSearchResults = results;
 
@@ -858,6 +895,78 @@ public class SeqPanel extends JPanel
     {
       setStatusMessage(results);
     }
+    // JAL-3303 feature suppressed for now pending review
+    return null; // results.isEmpty() ? null : getHighlightInfo(results);
+  }
+
+  /**
+   * temporary hack: answers a message suitable to show on structure hover
+   * label. This is normally null. It is a peptide variation description if
+   * <ul>
+   * <li>results are a single residue in a protein alignment</li>
+   * <li>there is a mapping to a coding sequence (codon)</li>
+   * <li>there are one or more SNP variant features on the codon</li>
+   * </ul>
+   * in which case the answer is of the format (e.g.) "p.Glu388Asp"
+   * 
+   * @param results
+   * @return
+   */
+  private String getHighlightInfo(SearchResultsI results)
+  {
+    /*
+     * ideally, just find mapped CDS (as we don't care about render style here);
+     * for now, go via split frame complement's FeatureRenderer
+     */
+    AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
+    if (complement == null)
+    {
+      return null;
+    }
+    AlignFrame af = Desktop.getAlignFrameFor(complement);
+    FeatureRendererModel fr2 = af.getFeatureRenderer();
+
+    int j = results.getSize();
+    List<String> infos = new ArrayList<>();
+    for (int i = 0; i < j; i++)
+    {
+      SearchResultMatchI match = results.getResults().get(i);
+      int pos = match.getStart();
+      if (pos == match.getEnd())
+      {
+        SequenceI seq = match.getSequence();
+        SequenceI ds = seq.getDatasetSequence() == null ? seq
+                : seq.getDatasetSequence();
+        MappedFeatures mf = fr2
+                .findComplementFeaturesAtResidue(ds, pos);
+        if (mf != null)
+        {
+          for (SequenceFeature sf : mf.features)
+          {
+            String pv = mf.findProteinVariants(sf);
+            if (pv.length() > 0 && !infos.contains(pv))
+            {
+              infos.add(pv);
+            }
+          }
+        }
+      }
+    }
+
+    if (infos.isEmpty())
+    {
+      return null;
+    }
+    StringBuilder sb = new StringBuilder();
+    for (String info : infos)
+    {
+      if (sb.length() > 0)
+      {
+        sb.append("|");
+      }
+      sb.append(info);
+    }
+    return sb.toString();
   }
 
   @Override
@@ -964,25 +1073,56 @@ public class SeqPanel extends JPanel
      * add features that straddle the gap (pos may be the residue before or
      * after the gap)
      */
+    int unshownFeatures = 0;
     if (av.isShowSequenceFeatures())
     {
       List<SequenceFeature> features = ap.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
-      seqARep.appendFeatures(tooltipText, pos, features,
-              this.ap.getSeqPanel().seqCanvas.fr);
+      unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
+              features, this.ap.getSeqPanel().seqCanvas.fr,
+              MAX_TOOLTIP_LENGTH);
+
+      /*
+       * add features in CDS/protein complement at the corresponding
+       * position if configured to do so
+       */
+      if (av.isShowComplementFeatures())
+      {
+        if (!Comparison.isGap(sequence.getCharAt(column)))
+        {
+          AlignViewportI complement = ap.getAlignViewport()
+                  .getCodingComplement();
+          AlignFrame af = Desktop.getAlignFrameFor(complement);
+          FeatureRendererModel fr2 = af.getFeatureRenderer();
+          MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
+                  pos);
+          if (mf != null)
+          {
+            unshownFeatures = seqARep.appendFeatures(tooltipText,
+                    pos, mf, fr2, MAX_TOOLTIP_LENGTH);
+          }
+        }
+      }
     }
-    if (tooltipText.length() == 6) // <html>
+    if (tooltipText.length() == 6) // "<html>"
     {
       setToolTipText(null);
       lastTooltip = null;
     }
     else
     {
-      if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
+      if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
       {
         tooltipText.setLength(MAX_TOOLTIP_LENGTH);
         tooltipText.append("...");
       }
+      if (unshownFeatures > 0)
+      {
+        tooltipText.append("<br/>").append("... ").append("<i>")
+                .append(MessageManager.formatMessage(
+                        "label.features_not_shown", unshownFeatures))
+                .append("</i>");
+      }
       String textString = tooltipText.toString();
       if (lastTooltip == null || !lastTooltip.equals(textString))
       {
@@ -1093,7 +1233,7 @@ public class SeqPanel extends JPanel
   {
     char sequenceChar = sequence.getCharAt(column);
     int pos = sequence.findPosition(column);
-    setStatusMessage(sequence, seqIndex, sequenceChar, pos);
+    setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
 
     return pos;
   }
@@ -1109,7 +1249,7 @@ public class SeqPanel extends JPanel
    * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
    * </pre>
    * 
-   * @param sequence
+   * @param seqName
    * @param seqIndex
    *          sequence position in the alignment (1..)
    * @param sequenceChar
@@ -1117,7 +1257,7 @@ public class SeqPanel extends JPanel
    * @param residuePos
    *          the sequence residue position (if not over a gap)
    */
-  protected void setStatusMessage(SequenceI sequence, int seqIndex,
+  protected void setStatusMessage(String seqName, int seqIndex,
           char sequenceChar, int residuePos)
   {
     StringBuilder text = new StringBuilder(32);
@@ -1127,7 +1267,7 @@ public class SeqPanel extends JPanel
      */
     String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
     text.append("Sequence").append(seqno).append(" ID: ")
-            .append(sequence.getName());
+            .append(seqName);
 
     String residue = null;
 
@@ -1172,7 +1312,8 @@ public class SeqPanel extends JPanel
     {
       return;
     }
-    SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
+    SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
+    SequenceI ds = alignedSeq.getDatasetSequence();
     for (SearchResultMatchI m : results.getResults())
     {
       SequenceI seq = m.getSequence();
@@ -1184,8 +1325,8 @@ public class SeqPanel extends JPanel
       if (seq == ds)
       {
         int start = m.getStart();
-        setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
-                start);
+        setStatusMessage(alignedSeq.getName(), sequenceIndex,
+                seq.getCharAt(start - 1), start);
         return;
       }
     }
@@ -1325,9 +1466,9 @@ public class SeqPanel extends JPanel
     }
 
     mouseDragging = true;
-    if ((scrollThread != null) && (scrollThread.isRunning()))
+    if (scrollThread != null)
     {
-      scrollThread.setEvent(evt);
+      scrollThread.setMousePosition(evt.getPoint());
     }
   }
 
@@ -1898,10 +2039,10 @@ public class SeqPanel extends JPanel
   }
 
   /**
-   * DOCUMENT ME!
+   * On reentering the panel, stops any scrolling that was started on dragging
+   * out of the panel
    * 
    * @param e
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseEntered(MouseEvent e)
@@ -1910,23 +2051,19 @@ public class SeqPanel extends JPanel
     {
       oldSeq = 0;
     }
-
-    if ((scrollThread != null) && (scrollThread.isRunning()))
-    {
-      scrollThread.stopScrolling();
-      scrollThread = null;
-    }
+    stopScrolling();
   }
 
   /**
-   * DOCUMENT ME!
+   * On leaving the panel, if the mouse is being dragged, starts a thread to
+   * scroll it until the mouse is released (in unwrapped mode only)
    * 
    * @param e
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseExited(MouseEvent e)
   {
+    lastMousePosition = null;
     ap.alignFrame.setStatus(" ");
     if (av.getWrapAlignment())
     {
@@ -1954,7 +2091,7 @@ public class SeqPanel extends JPanel
       return;
     }
 
-    if (evt.getClickCount() > 1)
+    if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
     {
       sg = av.getSelectionGroup();
       if (sg != null && sg.getSize() == 1
@@ -2159,11 +2296,11 @@ public class SeqPanel extends JPanel
     final int column = pos.column;
     final int seq = pos.seqIndex;
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    List<SequenceFeature> features = ap.getFeatureRenderer()
-            .findFeaturesAtColumn(sequence, column + 1);
-
-    PopupMenu pop = new PopupMenu(ap, null, features);
-    pop.show(this, evt.getX(), evt.getY());
+    if (sequence != null)
+    {
+      PopupMenu pop = new PopupMenu(ap, sequence, column);
+      pop.show(this, evt.getX(), evt.getY());
+    }
   }
 
   /**
@@ -2248,10 +2385,7 @@ public class SeqPanel extends JPanel
       return;
     }
 
-    if (res >= av.getAlignment().getWidth())
-    {
-      res = av.getAlignment().getWidth() - 1;
-    }
+    res = Math.min(res, av.getAlignment().getWidth()-1);
 
     if (stretchGroup.getEndRes() == res)
     {
@@ -2337,9 +2471,9 @@ public class SeqPanel extends JPanel
 
     mouseDragging = true;
 
-    if ((scrollThread != null) && (scrollThread.isRunning()))
+    if (scrollThread != null)
     {
-      scrollThread.setEvent(evt);
+      scrollThread.setMousePosition(evt.getPoint());
     }
 
     /*
@@ -2369,83 +2503,120 @@ public class SeqPanel extends JPanel
     ap.alignFrame.setStatus(status.toString());
   }
 
-  void scrollCanvas(MouseEvent evt)
+  /**
+   * Stops the scroll thread if it is running
+   */
+  void stopScrolling()
   {
-    if (evt == null)
+    if (scrollThread != null)
     {
-      if ((scrollThread != null) && (scrollThread.isRunning()))
-      {
-        scrollThread.stopScrolling();
-        scrollThread = null;
-      }
-      mouseDragging = false;
+      scrollThread.stopScrolling();
+      scrollThread = null;
     }
-    else
-    {
-      if (scrollThread == null)
-      {
-        scrollThread = new ScrollThread();
-      }
+    mouseDragging = false;
+  }
 
-      mouseDragging = true;
-      scrollThread.setEvent(evt);
+  /**
+   * Starts a thread to scroll the alignment, towards a given mouse position
+   * outside the panel bounds
+   * 
+   * @param mousePos
+   */
+  void startScrolling(Point mousePos)
+  {
+    if (scrollThread == null)
+    {
+      scrollThread = new ScrollThread();
     }
 
+    mouseDragging = true;
+    scrollThread.setMousePosition(mousePos);
   }
 
-  // this class allows scrolling off the bottom of the visible alignment
+  /**
+   * Performs scrolling of the visible alignment left, right, up or down
+   */
   class ScrollThread extends Thread
   {
-    MouseEvent evt;
+    private Point mousePos;
 
     private volatile boolean threadRunning = true;
 
+    /**
+     * Constructor
+     */
     public ScrollThread()
     {
+      setName("SeqPanel$ScrollThread");
       start();
     }
 
-    public void setEvent(MouseEvent e)
+    /**
+     * Sets the position of the mouse that determines the direction of the
+     * scroll to perform
+     * 
+     * @param p
+     */
+    public void setMousePosition(Point p)
     {
-      evt = e;
+      mousePos = p;
     }
 
+    /**
+     * Sets a flag that will cause the thread to exit
+     */
     public void stopScrolling()
     {
       threadRunning = false;
     }
 
-    public boolean isRunning()
-    {
-      return threadRunning;
-    }
-
+    /**
+     * Scrolls the alignment left or right, and/or up or down, depending on the
+     * last notified mouse position, until the limit of the alignment is
+     * reached, or a flag is set to stop the scroll
+     */
     @Override
     public void run()
     {
-      while (threadRunning)
+      while (threadRunning && mouseDragging)
       {
-        if (evt != null)
+        if (mousePos != null)
         {
-          if (mouseDragging && (evt.getY() < 0)
-                  && (av.getRanges().getStartSeq() > 0))
+          boolean scrolled = false;
+          ViewportRanges ranges = SeqPanel.this.av.getRanges();
+
+          /*
+           * scroll up or down
+           */
+          if (mousePos.y < 0)
           {
-            av.getRanges().scrollUp(true);
+            // mouse is above this panel - try scroll up
+            scrolled = ranges.scrollUp(true);
           }
-
-          if (mouseDragging && (evt.getY() >= getHeight()) && (av
-                  .getAlignment().getHeight() > av.getRanges().getEndSeq()))
+          else if (mousePos.y >= getHeight())
           {
-            av.getRanges().scrollUp(false);
+            // mouse is below this panel - try scroll down
+            scrolled = ranges.scrollUp(false);
           }
 
-          if (mouseDragging && (evt.getX() < 0))
+          /*
+           * scroll left or right
+           */
+          if (mousePos.x < 0)
           {
-            av.getRanges().scrollRight(false);
+            scrolled |= ranges.scrollRight(false);
           }
-          else if (mouseDragging && (evt.getX() >= getWidth()))
+          else if (mousePos.x >= getWidth())
           {
-            av.getRanges().scrollRight(true);
+            scrolled |= ranges.scrollRight(true);
+          }
+          if (!scrolled)
+          {
+            /*
+             * we have reached the limit of the visible alignment - quit
+             */
+            threadRunning = false;
+            SeqPanel.this.ap.repaint();
           }
         }
 
@@ -2623,7 +2794,7 @@ public class SeqPanel extends JPanel
      * Map sequence selection
      */
     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
-    av.setSelectionGroup(sg);
+    av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
     av.isSelectionGroupChanged(true);
 
     /*