JAL-3697 show total number of features not shown
[jalview.git] / src / jalview / gui / SeqPanel.java
index 11ee700..8a49092 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.ActionEvent;
+import java.awt.event.ActionListener;
+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.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.commands.EditCommand;
@@ -29,6 +52,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;
@@ -50,23 +74,7 @@ import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.ViewportRanges;
-
-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.seqfeatures.FeatureRendererModel;
 
 /**
  * DOCUMENT ME!
@@ -204,13 +212,21 @@ public class SeqPanel extends JPanel
 
   StringBuffer keyboardNo2;
 
-  java.net.URL linkImageURL;
-
   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;
 
@@ -226,11 +242,12 @@ 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);
+    
+    
     this.av = viewport;
     setBackground(Color.white);
 
@@ -255,11 +272,6 @@ public class SeqPanel extends JPanel
 
   int wrappedBlock = -1;
 
-  MousePos findMousePosition(MouseEvent evt)
-  {
-    return findMousePosition(evt, null);
-  }
-
   /**
    * Computes the column and sequence row (and possibly annotation row when in
    * wrapped mode) for the given mouse position
@@ -267,22 +279,15 @@ public class SeqPanel extends JPanel
    * @param evt
    * @return
    */
-  MousePos findMousePosition(MouseEvent evt, String debug)
+  MousePos findMousePosition(MouseEvent evt)
   {
-    int col = findColumn(evt, debug);
+    int col = findColumn(evt);
     int seqIndex = -1;
     int annIndex = -1;
     int y = evt.getY();
 
     int charHeight = av.getCharHeight();
     int alignmentHeight = av.getAlignment().getHeight();
-    if (debug != null)
-    {
-      System.out.println(String.format(
-              "%s: charHeight %d alHeight %d canvasWidth %d canvasHeight %d",
-              debug, charHeight, alignmentHeight, seqCanvas.getWidth(),
-              seqCanvas.getHeight()));
-    }
     if (av.getWrapAlignment())
     {
       seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
@@ -343,11 +348,6 @@ public class SeqPanel extends JPanel
    */
   int findColumn(MouseEvent evt)
   {
-    return findColumn(evt, null);
-  }
-
-  int findColumn(MouseEvent evt, String debug)
-  {
     int res = 0;
     int x = evt.getX();
 
@@ -368,12 +368,6 @@ public class SeqPanel extends JPanel
       int y = evt.getY();
       y = Math.max(0, y - hgap);
       x -= seqCanvas.getLabelWidthWest();
-      if (debug != null)
-      {
-        System.out.println(
-                String.format("%s: x %d labelWest %d charWidth %d ", debug,
-                x, seqCanvas.getLabelWidthWest(), charWidth));
-      }
       if (x < 0)
       {
         // mouse is over left scale
@@ -387,10 +381,6 @@ public class SeqPanel extends JPanel
       }
       if (x >= cwidth * charWidth)
       {
-        if (debug != null)
-        {
-          System.out.println(debug + ": cwidth = " + cwidth);
-        }
         // mouse is over right scale
         return -1;
       }
@@ -859,11 +849,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;
 
@@ -884,11 +874,82 @@ 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);
     }
+    return 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
@@ -928,8 +989,10 @@ public class SeqPanel extends JPanel
       /*
        * just a pixel move without change of 'cell'
        */
+      moveTooltip = false;
       return;
     }
+    moveTooltip = true;
     lastMousePosition = mousePos;
 
     if (mousePos.isOverAnnotation())
@@ -945,6 +1008,7 @@ public class SeqPanel extends JPanel
       lastMousePosition = null;
       setToolTipText(null);
       lastTooltip = null;
+      lastFormattedTooltip = null;
       ap.alignFrame.setStatus("");
       return;
     }
@@ -966,7 +1030,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)
@@ -995,32 +1059,63 @@ 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() == 0) // nothing added
     {
       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))
+      if (!textString.equals(lastTooltip))
       {
-        String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
-                textString);
-        setToolTipText(formattedTooltipText);
         lastTooltip = textString;
+        lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
+                textString);
+        setToolTipText(lastFormattedTooltip);
       }
     }
   }
@@ -1046,15 +1141,35 @@ public class SeqPanel extends JPanel
 
     String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
             anns);
-    setToolTipText(tooltip);
-    lastTooltip = tooltip;
+    if (!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)
@@ -1064,29 +1179,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
@@ -1124,7 +1241,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;
   }
@@ -1140,7 +1257,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
@@ -1148,7 +1265,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);
@@ -1158,7 +1275,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;
 
@@ -1203,7 +1320,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();
@@ -1215,8 +1333,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;
       }
     }
@@ -1962,7 +2080,7 @@ public class SeqPanel extends JPanel
 
     if (mouseDragging && scrollThread == null)
     {
-      scrollThread = new ScrollThread();
+      startScrolling(e.getPoint());
     }
   }
 
@@ -1981,7 +2099,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
@@ -2008,17 +2126,13 @@ public class SeqPanel extends JPanel
         SearchResultsI highlight = new SearchResults();
         highlight.addResult(sequence, features.get(0).getBegin(), features
                 .get(0).getEnd());
-        seqCanvas.highlightSearchResults(highlight, false);
+        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();
       }
     }
   }
@@ -2100,22 +2214,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;
@@ -2186,11 +2300,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());
+    }
   }
 
   /**
@@ -2408,29 +2522,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
@@ -2438,12 +2596,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
      */
@@ -2457,7 +2617,7 @@ public class SeqPanel extends JPanel
      */
     public void stopScrolling()
     {
-      threadRunning = false;
+      keepRunning = false;
     }
 
     /**
@@ -2468,48 +2628,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);
@@ -2517,6 +2641,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;
     }
   }
 
@@ -2684,7 +2862,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);
 
     /*