Merge branch 'bug/JAL-3997_NPE_in_wrapped_mode' into develop
[jalview.git] / src / jalview / gui / SeqPanel.java
index 92f221c..111b4c0 100644 (file)
@@ -25,6 +25,8 @@ import java.awt.Color;
 import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
@@ -34,12 +36,15 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import javax.swing.JLabel;
 import javax.swing.JPanel;
+import javax.swing.JToolTip;
 import javax.swing.SwingUtilities;
+import javax.swing.Timer;
 import javax.swing.ToolTipManager;
 
 import jalview.api.AlignViewportI;
-import jalview.bin.Cache;
+import jalview.bin.Console;
 import jalview.commands.EditCommand;
 import jalview.commands.EditCommand.Action;
 import jalview.commands.EditCommand.Edit;
@@ -209,9 +214,19 @@ public class SeqPanel extends JPanel
 
   private final SequenceAnnotationReport seqARep;
 
-  StringBuilder tooltipText = new StringBuilder();
+  /*
+   * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
+   * - the tooltip is not set again if unchanged
+   * - this is the tooltip text _before_ formatting as html
+   */
+  private String lastTooltip;
 
-  String tmpString;
+  /*
+   * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
+   * - used to decide where to place the tooltip in getTooltipLocation() 
+   * - this is the tooltip text _after_ formatting as html
+   */
+  private String lastFormattedTooltip;
 
   EditCommand editCommand;
 
@@ -231,6 +246,7 @@ public class SeqPanel extends JPanel
     ToolTipManager.sharedInstance().registerComponent(this);
     ToolTipManager.sharedInstance().setInitialDelay(0);
     ToolTipManager.sharedInstance().setDismissDelay(10000);
+
     this.av = viewport;
     setBackground(Color.white);
 
@@ -258,6 +274,9 @@ public class SeqPanel extends JPanel
   /**
    * Computes the column and sequence row (and possibly annotation row when in
    * wrapped mode) for the given mouse position
+   * <p>
+   * Mouse position is not set if in wrapped mode with the cursor either between
+   * sequences, or over the left or right vertical scale.
    * 
    * @param evt
    * @return
@@ -322,9 +341,13 @@ public class SeqPanel extends JPanel
 
     return new MousePos(col, seqIndex, annIndex);
   }
+
   /**
    * Returns the aligned sequence position (base 0) at the mouse position, or
    * the closest visible one
+   * <p>
+   * Returns -1 if in wrapped mode with the mouse over either left or right
+   * vertical scale.
    * 
    * @param evt
    * @return
@@ -460,8 +483,9 @@ public class SeqPanel extends JPanel
 
   void moveCursor(int dx, int dy)
   {
-    moveCursor(dx, dy,false);
+    moveCursor(dx, dy, false);
   }
+
   void moveCursor(int dx, int dy, boolean nextWord)
   {
     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
@@ -469,11 +493,13 @@ public class SeqPanel extends JPanel
     if (nextWord)
     {
       int maxWidth = av.getAlignment().getWidth();
-      int maxHeight=av.getAlignment().getHeight();
-      SequenceI seqAtRow = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
+      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;
+      boolean isGap = Comparison
+              .isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
+      int p = seqCanvas.cursorX, lastP, r = seqCanvas.cursorY, lastR;
       do
       {
         lastP = p;
@@ -494,19 +520,23 @@ public class SeqPanel extends JPanel
         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 {
+      seqCanvas.cursorX = p;
+      seqCanvas.cursorY = r;
+    }
+    else
+    {
       int maxWidth = av.getAlignment().getWidth();
-      seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX, dx);
+      seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX,
+              dx);
       seqCanvas.cursorY += dy;
     }
     scrollToVisible(false);
   }
 
-  private int nextVisible(HiddenColumns hidden,int maxWidth, int original, int dx)
+  private int nextVisible(HiddenColumns hidden, int maxWidth, int original,
+          int dx)
   {
-    int newCursorX=original+dx;
+    int newCursorX = original + dx;
     if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
     {
       int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
@@ -527,13 +557,13 @@ public class SeqPanel extends JPanel
       }
     }
     newCursorX = (newCursorX < 0) ? 0 : newCursorX;
-    if (newCursorX >= maxWidth
-            || !hidden.isVisible(newCursorX))
+    if (newCursorX >= maxWidth || !hidden.isVisible(newCursorX))
     {
       newCursorX = original;
     }
     return newCursorX;
   }
+
   /**
    * Scroll to make the cursor visible in the viewport.
    * 
@@ -589,7 +619,7 @@ public class SeqPanel extends JPanel
     if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
     {
       setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
-            seqCanvas.cursorX, seqCanvas.cursorY);
+              seqCanvas.cursorX, seqCanvas.cursorY);
     }
 
     if (repaintNeeded)
@@ -598,7 +628,6 @@ public class SeqPanel extends JPanel
     }
   }
 
-
   void setSelectionAreaAtCursor(boolean topLeft)
   {
     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
@@ -890,13 +919,12 @@ public class SeqPanel extends JPanel
       ap.setToScrollComplementPanel(true);
     }
 
-    boolean noFastPaint = wasScrolled && av.getWrapAlignment();
-    if (seqCanvas.highlightSearchResults(results, noFastPaint))
+    boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
+    if (seqCanvas.highlightSearchResults(results, fastPaint))
     {
       setStatusMessage(results);
     }
-    // JAL-3303 feature suppressed for now pending review
-    return null; // results.isEmpty() ? null : getHighlightInfo(results);
+    return results.isEmpty() ? null : getHighlightInfo(results);
   }
 
   /**
@@ -926,19 +954,19 @@ public class SeqPanel extends JPanel
     AlignFrame af = Desktop.getAlignFrameFor(complement);
     FeatureRendererModel fr2 = af.getFeatureRenderer();
 
-    int j = results.getSize();
+    List<SearchResultMatchI> matches = results.getResults();
+    int j = matches.size();
     List<String> infos = new ArrayList<>();
     for (int i = 0; i < j; i++)
     {
-      SearchResultMatchI match = results.getResults().get(i);
+      SearchResultMatchI match = matches.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);
+        MappedFeatures mf = fr2.findComplementFeaturesAtResidue(ds, pos);
         if (mf != null)
         {
           for (SequenceFeature sf : mf.features)
@@ -1006,8 +1034,10 @@ public class SeqPanel extends JPanel
       /*
        * just a pixel move without change of 'cell'
        */
+      moveTooltip = false;
       return;
     }
+    moveTooltip = true;
     lastMousePosition = mousePos;
 
     if (mousePos.isOverAnnotation())
@@ -1023,6 +1053,7 @@ public class SeqPanel extends JPanel
       lastMousePosition = null;
       setToolTipText(null);
       lastTooltip = null;
+      lastFormattedTooltip = null;
       ap.alignFrame.setStatus("");
       return;
     }
@@ -1044,7 +1075,7 @@ public class SeqPanel extends JPanel
       mouseOverSequence(sequence, column, pos);
     }
 
-    tooltipText.setLength(6); // Cuts the buffer back to <html>
+    StringBuilder tooltipText = new StringBuilder(64);
 
     SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
     if (groups != null)
@@ -1078,9 +1109,8 @@ public class SeqPanel extends JPanel
     {
       List<SequenceFeature> features = ap.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
-      unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
-              features, this.ap.getSeqPanel().seqCanvas.fr,
-              MAX_TOOLTIP_LENGTH);
+      unshownFeatures = seqARep.appendFeatures(tooltipText, pos, features,
+              this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
 
       /*
        * add features in CDS/protein complement at the corresponding
@@ -1098,13 +1128,13 @@ public class SeqPanel extends JPanel
                   pos);
           if (mf != null)
           {
-            unshownFeatures += seqARep.appendFeatures(tooltipText,
-                    pos, mf, fr2, MAX_TOOLTIP_LENGTH);
+            unshownFeatures += seqARep.appendFeatures(tooltipText, pos, mf,
+                    fr2, MAX_TOOLTIP_LENGTH);
           }
         }
       }
     }
-    if (tooltipText.length() == 6) // "<html>"
+    if (tooltipText.length() == 0) // nothing added
     {
       setToolTipText(null);
       lastTooltip = null;
@@ -1124,12 +1154,11 @@ public class SeqPanel extends JPanel
                 .append("</i>");
       }
       String textString = tooltipText.toString();
-      if (lastTooltip == null || !lastTooltip.equals(textString))
+      if (!textString.equals(lastTooltip))
       {
-        String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
-                textString);
-        setToolTipText(formattedTooltipText);
         lastTooltip = textString;
+        lastFormattedTooltip = JvSwingUtils.wrapTooltip(true, textString);
+        setToolTipText(lastFormattedTooltip);
       }
     }
   }
@@ -1155,15 +1184,36 @@ public class SeqPanel extends JPanel
 
     String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
             anns);
-    setToolTipText(tooltip);
-    lastTooltip = tooltip;
+    if (tooltip == null ? tooltip != lastTooltip
+            : !tooltip.equals(lastTooltip))
+    {
+      lastTooltip = tooltip;
+      lastFormattedTooltip = tooltip == null ? null
+              : JvSwingUtils.wrapTooltip(true, tooltip);
+      setToolTipText(lastFormattedTooltip);
+    }
 
     String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
             anns[rowIndex]);
     ap.alignFrame.setStatus(msg);
   }
 
-  private Point lastp = null;
+  /*
+   * if Shift key is held down while moving the mouse, 
+   * the tooltip location is not changed once shown
+   */
+  private Point lastTooltipLocation = null;
+
+  /*
+   * this flag is false for pixel moves within a residue,
+   * to reduce tooltip flicker
+   */
+  private boolean moveTooltip = true;
+
+  /*
+   * a dummy tooltip used to estimate where to position tooltips
+   */
+  private JToolTip tempTip = new JLabel().createToolTip();
 
   /*
    * (non-Javadoc)
@@ -1173,29 +1223,31 @@ public class SeqPanel extends JPanel
   @Override
   public Point getToolTipLocation(MouseEvent event)
   {
-    if (tooltipText == null || tooltipText.length() <= 6)
+    // BH 2018
+
+    if (lastTooltip == null || !moveTooltip)
     {
-      lastp = null;
       return null;
     }
 
-    int x = event.getX();
-    int w = getWidth();
-    // switch sides when tooltip is too close to edge
-    int wdth = (w - x < 200) ? -(w / 2) : 5;
-    Point p = lastp;
-    if (!event.isShiftDown() || p == null)
+    if (lastTooltipLocation != null && event.isShiftDown())
     {
-      p = new Point(event.getX() + wdth, event.getY() - 20);
-      lastp = p;
+      return lastTooltipLocation;
     }
-    /*
-     * TODO: try to set position so region is not obscured by tooltip
-     */
-    return p;
-  }
 
-  String lastTooltip;
+    int x = event.getX();
+    int y = event.getY();
+    int w = getWidth();
+
+    tempTip.setTipText(lastFormattedTooltip);
+    int tipWidth = (int) tempTip.getPreferredSize().getWidth();
+
+    // was x += (w - x < 200) ? -(w / 2) : 5;
+    x = (x + tipWidth < w ? x + 10 : w - tipWidth);
+    Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
+
+    return lastTooltipLocation = p;
+  }
 
   /**
    * set when the current UI interaction has resulted in a change that requires
@@ -1204,7 +1256,8 @@ public class SeqPanel extends JPanel
    * changed, so selective redraws can be applied (ie. only structures, only
    * overview, etc)
    */
-  private boolean updateOverviewAndStructs = 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
@@ -1266,8 +1319,7 @@ public class SeqPanel extends JPanel
      * Sequence number (if known), and sequence name.
      */
     String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
-    text.append("Sequence").append(seqno).append(" ID: ")
-            .append(seqName);
+    text.append("Sequence").append(seqno).append(" ID: ").append(seqName);
 
     String residue = null;
 
@@ -1526,12 +1578,12 @@ public class SeqPanel extends JPanel
     String label = null;
     if (groupEditing)
     {
-        message.append("Edit group:");
+      message.append("Edit group:");
       label = MessageManager.getString("action.edit_group");
     }
     else
     {
-        message.append("Edit sequence: " + seq.getName());
+      message.append("Edit sequence: " + seq.getName());
       label = seq.getName();
       if (label.length() > 10)
       {
@@ -1723,8 +1775,7 @@ public class SeqPanel extends JPanel
           {
             for (int j = 0; j < startres - editLastRes; j++)
             {
-              if (!Comparison
-                      .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
+              if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
               {
                 blank = false;
                 break;
@@ -2072,7 +2123,7 @@ public class SeqPanel extends JPanel
 
     if (mouseDragging && scrollThread == null)
     {
-      scrollThread = new ScrollThread();
+      startScrolling(e.getPoint());
     }
   }
 
@@ -2116,19 +2167,15 @@ public class SeqPanel extends JPanel
          * highlight the first feature at the position on the alignment
          */
         SearchResultsI highlight = new SearchResults();
-        highlight.addResult(sequence, features.get(0).getBegin(), features
-                .get(0).getEnd());
-        seqCanvas.highlightSearchResults(highlight, false);
+        highlight.addResult(sequence, features.get(0).getBegin(),
+                features.get(0).getEnd());
+        seqCanvas.highlightSearchResults(highlight, true);
 
         /*
-         * open the Amend Features dialog; clear highlighting afterwards,
-         * whether changes were made or not
+         * open the Amend Features dialog
          */
-        List<SequenceI> seqs = Collections.singletonList(sequence);
-        seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
-                ap);
-        av.setSearchResults(null); // clear highlighting
-        seqCanvas.repaint(); // draw new/amended features
+        new FeatureEditor(ap, Collections.singletonList(sequence), features,
+                false).showDialog();
       }
     }
   }
@@ -2210,22 +2257,22 @@ public class SeqPanel extends JPanel
       }
     }
 
-    if (evt.isPopupTrigger()) // Mac: mousePressed
-    {
-      showPopupMenu(evt, pos);
-      return;
-    }
-
     /*
      * defer right-mouse click handling to mouseReleased on Windows
      * (where isPopupTrigger() will answer true)
      * NB isRightMouseButton is also true for Cmd-click on Mac
      */
-    if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
+    if (Platform.isWinRightButton(evt))
     {
       return;
     }
 
+    if (evt.isPopupTrigger()) // Mac: mousePressed
+    {
+      showPopupMenu(evt, pos);
+      return;
+    }
+
     if (av.cursorMode)
     {
       seqCanvas.cursorX = res;
@@ -2385,7 +2432,7 @@ public class SeqPanel extends JPanel
       return;
     }
 
-    res = Math.min(res, av.getAlignment().getWidth()-1);
+    res = Math.min(res, av.getAlignment().getWidth() - 1);
 
     if (stretchGroup.getEndRes() == res)
     {
@@ -2518,29 +2565,73 @@ public class SeqPanel extends JPanel
 
   /**
    * Starts a thread to scroll the alignment, towards a given mouse position
-   * outside the panel bounds
+   * outside the panel bounds, unless the alignment is in wrapped mode
    * 
    * @param mousePos
    */
   void startScrolling(Point mousePos)
   {
-    if (scrollThread == null)
+    /*
+     * set this.mouseDragging in case this was called from 
+     * a drag in ScalePanel or AnnotationPanel
+     */
+    mouseDragging = true;
+    if (!av.getWrapAlignment() && scrollThread == null)
     {
       scrollThread = new ScrollThread();
+      scrollThread.setMousePosition(mousePos);
+      if (Platform.isJS())
+      {
+        /*
+         * Javascript - run every 20ms until scrolling stopped
+         * or reaches the limit of scrollable alignment
+         */
+        Timer t = new Timer(20, new ActionListener()
+        {
+          @Override
+          public void actionPerformed(ActionEvent e)
+          {
+            if (scrollThread != null)
+            {
+              // if (!scrollOnce() {t.stop();}) gives compiler error :-(
+              scrollThread.scrollOnce();
+            }
+          }
+        });
+        t.addActionListener(new ActionListener()
+        {
+          @Override
+          public void actionPerformed(ActionEvent e)
+          {
+            if (scrollThread == null)
+            {
+              // SeqPanel.stopScrolling called
+              t.stop();
+            }
+          }
+        });
+        t.start();
+      }
+      else
+      {
+        /*
+         * Java - run in a new thread
+         */
+        scrollThread.start();
+      }
     }
-
-    mouseDragging = true;
-    scrollThread.setMousePosition(mousePos);
   }
 
   /**
-   * Performs scrolling of the visible alignment left, right, up or down
+   * Performs scrolling of the visible alignment left, right, up or down, until
+   * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
+   * limit of the alignment is reached
    */
   class ScrollThread extends Thread
   {
     private Point mousePos;
 
-    private volatile boolean threadRunning = true;
+    private volatile boolean keepRunning = true;
 
     /**
      * Constructor
@@ -2548,12 +2639,14 @@ public class SeqPanel extends JPanel
     public ScrollThread()
     {
       setName("SeqPanel$ScrollThread");
-      start();
     }
 
     /**
      * Sets the position of the mouse that determines the direction of the
-     * scroll to perform
+     * scroll to perform. If this is called as the mouse moves, scrolling should
+     * respond accordingly. For example, if the mouse is dragged right, scroll
+     * right should start; if the drag continues down, scroll down should also
+     * happen.
      * 
      * @param p
      */
@@ -2567,7 +2660,7 @@ public class SeqPanel extends JPanel
      */
     public void stopScrolling()
     {
-      threadRunning = false;
+      keepRunning = false;
     }
 
     /**
@@ -2578,48 +2671,12 @@ public class SeqPanel extends JPanel
     @Override
     public void run()
     {
-      while (threadRunning && mouseDragging)
+      while (keepRunning)
       {
         if (mousePos != null)
         {
-          boolean scrolled = false;
-          ViewportRanges ranges = SeqPanel.this.av.getRanges();
-
-          /*
-           * scroll up or down
-           */
-          if (mousePos.y < 0)
-          {
-            // mouse is above this panel - try scroll up
-            scrolled = ranges.scrollUp(true);
-          }
-          else if (mousePos.y >= getHeight())
-          {
-            // mouse is below this panel - try scroll down
-            scrolled = ranges.scrollUp(false);
-          }
-
-          /*
-           * scroll left or right
-           */
-          if (mousePos.x < 0)
-          {
-            scrolled |= ranges.scrollRight(false);
-          }
-          else if (mousePos.x >= getWidth())
-          {
-            scrolled |= ranges.scrollRight(true);
-          }
-          if (!scrolled)
-          {
-            /*
-             * we have reached the limit of the visible alignment - quit
-             */
-            threadRunning = false;
-            SeqPanel.this.ap.repaint();
-          }
+          keepRunning = scrollOnce();
         }
-
         try
         {
           Thread.sleep(20);
@@ -2627,6 +2684,60 @@ public class SeqPanel extends JPanel
         {
         }
       }
+      SeqPanel.this.scrollThread = null;
+    }
+
+    /**
+     * Scrolls
+     * <ul>
+     * <li>one row up, if the mouse is above the panel</li>
+     * <li>one row down, if the mouse is below the panel</li>
+     * <li>one column left, if the mouse is left of the panel</li>
+     * <li>one column right, if the mouse is right of the panel</li>
+     * </ul>
+     * Answers true if a scroll was performed, false if not - meaning either
+     * that the mouse position is within the panel, or the edge of the alignment
+     * has been reached.
+     */
+    boolean scrollOnce()
+    {
+      /*
+       * quit after mouseUp ensures interrupt in JalviewJS
+       */
+      if (!mouseDragging)
+      {
+        return false;
+      }
+
+      boolean scrolled = false;
+      ViewportRanges ranges = SeqPanel.this.av.getRanges();
+
+      /*
+       * scroll up or down
+       */
+      if (mousePos.y < 0)
+      {
+        // mouse is above this panel - try scroll up
+        scrolled = ranges.scrollUp(true);
+      }
+      else if (mousePos.y >= getHeight())
+      {
+        // mouse is below this panel - try scroll down
+        scrolled = ranges.scrollUp(false);
+      }
+
+      /*
+       * scroll left or right
+       */
+      if (mousePos.x < 0)
+      {
+        scrolled |= ranges.scrollRight(false);
+      }
+      else if (mousePos.x >= getWidth())
+      {
+        scrolled |= ranges.scrollRight(true);
+      }
+      return scrolled;
     }
   }
 
@@ -2694,7 +2805,7 @@ public class SeqPanel extends JPanel
     {
       if (av.getAlignment() == null)
       {
-        Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
+        Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
                 + " ViewId=" + av.getViewId()
                 + " 's alignment is NULL! returning immediately.");
         return;