Merge branch 'feature/JAL-3093wrappedModeTooltips' into
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 1 Mar 2019 14:30:03 +0000 (14:30 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 1 Mar 2019 14:30:03 +0000 (14:30 +0000)
merge/JAL-3093_JAL-3132

Conflicts:
src/jalview/gui/SeqPanel.java
test/jalview/gui/SeqPanelTest.java

15 files changed:
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/AnnotationPanel.java
src/jalview/gui/CutAndPasteTransfer.java
src/jalview/gui/IdPanel.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/SequenceFetcher.java
src/jalview/io/FileLoader.java
src/jalview/jbgui/GAlignFrame.java
test/jalview/gui/AnnotationLabelsTest.java [new file with mode: 0644]
test/jalview/gui/AnnotationPanelTest.java [new file with mode: 0644]
test/jalview/gui/SeqCanvasTest.java
test/jalview/gui/SeqPanelTest.java

index 03daeb4..606e2e7 100644 (file)
@@ -964,6 +964,11 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     return progressBar.operationInProgress();
   }
 
+  /**
+   * Sets the text of the status bar. Note that setting a null or empty value
+   * will cause the status bar to be hidden, with possibly undesirable flicker
+   * of the screen layout.
+   */
   @Override
   public void setStatus(String text)
   {
index cc533ce..f94b74d 100644 (file)
@@ -795,7 +795,7 @@ public class AlignViewport extends AlignmentViewport
     AlignFrame newAlignFrame = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH,
             AlignFrame.DEFAULT_HEIGHT);
     newAlignFrame.setTitle(title);
-    newAlignFrame.statusBar.setText(MessageManager
+    newAlignFrame.setStatus(MessageManager
             .formatMessage("label.successfully_loaded_file", new Object[]
             { title }));
 
index 6f8b225..6da6cc3 100755 (executable)
@@ -51,12 +51,9 @@ import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.awt.geom.AffineTransform;
-import java.awt.image.BufferedImage;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
-import java.util.regex.Pattern;
 
 import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JMenuItem;
@@ -73,6 +70,10 @@ import javax.swing.ToolTipManager;
 public class AnnotationLabels extends JPanel
         implements MouseListener, MouseMotionListener, ActionListener
 {
+  private static final String HTML_END_TAG = "</html>";
+
+  private static final String HTML_START_TAG = "<html>";
+
   /**
    * width in pixels within which height adjuster arrows are shown and active
    */
@@ -83,9 +84,6 @@ public class AnnotationLabels extends JPanel
    */
   private static int HEIGHT_ADJUSTER_HEIGHT = 10;
 
-  private static final Pattern LEFT_ANGLE_BRACKET_PATTERN = Pattern
-          .compile("<");
-
   private static final Font font = new Font("Arial", Font.PLAIN, 11);
 
   private static final String TOGGLE_LABELSCALE = MessageManager
@@ -378,15 +376,6 @@ public class AnnotationLabels extends JPanel
             AlignmentUtils.showOrHideSequenceAnnotations(
                     ap.av.getAlignment(), Collections.singleton(label),
                     null, false, false);
-            // for (AlignmentAnnotation ann : ap.av.getAlignment()
-            // .getAlignmentAnnotation())
-            // {
-            // if (ann.sequenceRef != null && ann.label != null
-            // && ann.label.equals(label))
-            // {
-            // ann.visible = false;
-            // }
-            // }
             ap.refresh(true);
           }
         });
@@ -425,174 +414,154 @@ public class AnnotationLabels extends JPanel
       }
       else if (label.indexOf("Consensus") > -1)
       {
-        pop.addSeparator();
-        // av and sequencegroup need to implement same interface for
-        final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
-                MessageManager.getString("label.ignore_gaps_consensus"),
-                (aa[selectedRow].groupRef != null)
-                        ? aa[selectedRow].groupRef.getIgnoreGapsConsensus()
-                        : ap.av.isIgnoreGapsConsensus());
-        final AlignmentAnnotation aaa = aa[selectedRow];
-        cbmi.addActionListener(new ActionListener()
-        {
-          @Override
-          public void actionPerformed(ActionEvent e)
-          {
-            if (aaa.groupRef != null)
-            {
-              // TODO: pass on reference to ap so the view can be updated.
-              aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
-              ap.getAnnotationPanel()
-                      .paint(ap.getAnnotationPanel().getGraphics());
-            }
-            else
-            {
-              ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
-            }
-            ap.alignmentChanged();
-          }
-        });
-        pop.add(cbmi);
-        // av and sequencegroup need to implement same interface for
+        addConsensusMenuOptions(ap, aa[selectedRow], pop);
+
+        final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
+        consclipbrd.addActionListener(this);
+        pop.add(consclipbrd);
+      }
+    }
+    pop.show(this, evt.getX(), evt.getY());
+  }
+
+  /**
+   * A helper method that adds menu options for calculation and visualisation of
+   * group and/or alignment consensus annotation to a popup menu. This is
+   * designed to be reusable for either unwrapped mode (popup menu is shown on
+   * component AnnotationLabels), or wrapped mode (popup menu is shown on
+   * IdPanel when the mouse is over an annotation label).
+   * 
+   * @param ap
+   * @param ann
+   * @param pop
+   */
+  static void addConsensusMenuOptions(AlignmentPanel ap,
+          AlignmentAnnotation ann,
+          JPopupMenu pop)
+  {
+    pop.addSeparator();
+
+    final JCheckBoxMenuItem cbmi = new JCheckBoxMenuItem(
+            MessageManager.getString("label.ignore_gaps_consensus"),
+            (ann.groupRef != null) ? ann.groupRef.getIgnoreGapsConsensus()
+                    : ap.av.isIgnoreGapsConsensus());
+    final AlignmentAnnotation aaa = ann;
+    cbmi.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
         if (aaa.groupRef != null)
         {
-          final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
-                  MessageManager.getString("label.show_group_histogram"),
-                  aa[selectedRow].groupRef.isShowConsensusHistogram());
-          chist.addActionListener(new ActionListener()
-          {
-            @Override
-            public void actionPerformed(ActionEvent e)
-            {
-              // TODO: pass on reference
-              // to ap
-              // so the
-              // view
-              // can be
-              // updated.
-              aaa.groupRef.setShowConsensusHistogram(chist.getState());
-              ap.repaint();
-              // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-            }
-          });
-          pop.add(chist);
-          final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
-                  MessageManager.getString("label.show_group_logo"),
-                  aa[selectedRow].groupRef.isShowSequenceLogo());
-          cprofl.addActionListener(new ActionListener()
-          {
-            @Override
-            public void actionPerformed(ActionEvent e)
-            {
-              // TODO: pass on reference
-              // to ap
-              // so the
-              // view
-              // can be
-              // updated.
-              aaa.groupRef.setshowSequenceLogo(cprofl.getState());
-              ap.repaint();
-              // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-            }
-          });
-          pop.add(cprofl);
-          final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
-                  MessageManager.getString("label.normalise_group_logo"),
-                  aa[selectedRow].groupRef.isNormaliseSequenceLogo());
-          cproflnorm.addActionListener(new ActionListener()
-          {
-            @Override
-            public void actionPerformed(ActionEvent e)
-            {
-
-              // TODO: pass on reference
-              // to ap
-              // so the
-              // view
-              // can be
-              // updated.
-              aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
-              // automatically enable logo display if we're clicked
-              aaa.groupRef.setshowSequenceLogo(true);
-              ap.repaint();
-              // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-            }
-          });
-          pop.add(cproflnorm);
+          aaa.groupRef.setIgnoreGapsConsensus(cbmi.getState());
+          ap.getAnnotationPanel()
+                  .paint(ap.getAnnotationPanel().getGraphics());
         }
         else
         {
-          final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
-                  MessageManager.getString("label.show_histogram"),
-                  av.isShowConsensusHistogram());
-          chist.addActionListener(new ActionListener()
-          {
-            @Override
-            public void actionPerformed(ActionEvent e)
-            {
-              // TODO: pass on reference
-              // to ap
-              // so the
-              // view
-              // can be
-              // updated.
-              av.setShowConsensusHistogram(chist.getState());
-              ap.alignFrame.setMenusForViewport();
-              ap.repaint();
-              // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-            }
-          });
-          pop.add(chist);
-          final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
-                  MessageManager.getString("label.show_logo"),
-                  av.isShowSequenceLogo());
-          cprof.addActionListener(new ActionListener()
-          {
-            @Override
-            public void actionPerformed(ActionEvent e)
-            {
-              // TODO: pass on reference
-              // to ap
-              // so the
-              // view
-              // can be
-              // updated.
-              av.setShowSequenceLogo(cprof.getState());
-              ap.alignFrame.setMenusForViewport();
-              ap.repaint();
-              // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-            }
-          });
-          pop.add(cprof);
-          final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
-                  MessageManager.getString("label.normalise_logo"),
-                  av.isNormaliseSequenceLogo());
-          cprofnorm.addActionListener(new ActionListener()
-          {
-            @Override
-            public void actionPerformed(ActionEvent e)
-            {
-              // TODO: pass on reference
-              // to ap
-              // so the
-              // view
-              // can be
-              // updated.
-              av.setShowSequenceLogo(true);
-              av.setNormaliseSequenceLogo(cprofnorm.getState());
-              ap.alignFrame.setMenusForViewport();
-              ap.repaint();
-              // ap.annotationPanel.paint(ap.annotationPanel.getGraphics());
-            }
-          });
-          pop.add(cprofnorm);
+          ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
         }
-        final JMenuItem consclipbrd = new JMenuItem(COPYCONS_SEQ);
-        consclipbrd.addActionListener(this);
-        pop.add(consclipbrd);
+        ap.alignmentChanged();
       }
+    });
+    pop.add(cbmi);
+
+    if (aaa.groupRef != null)
+    {
+      /*
+       * group consensus options
+       */
+      final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
+              MessageManager.getString("label.show_group_histogram"),
+              ann.groupRef.isShowConsensusHistogram());
+      chist.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          aaa.groupRef.setShowConsensusHistogram(chist.getState());
+          ap.repaint();
+        }
+      });
+      pop.add(chist);
+      final JCheckBoxMenuItem cprofl = new JCheckBoxMenuItem(
+              MessageManager.getString("label.show_group_logo"),
+              ann.groupRef.isShowSequenceLogo());
+      cprofl.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          aaa.groupRef.setshowSequenceLogo(cprofl.getState());
+          ap.repaint();
+        }
+      });
+      pop.add(cprofl);
+      final JCheckBoxMenuItem cproflnorm = new JCheckBoxMenuItem(
+              MessageManager.getString("label.normalise_group_logo"),
+              ann.groupRef.isNormaliseSequenceLogo());
+      cproflnorm.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          aaa.groupRef.setNormaliseSequenceLogo(cproflnorm.getState());
+          // automatically enable logo display if we're clicked
+          aaa.groupRef.setshowSequenceLogo(true);
+          ap.repaint();
+        }
+      });
+      pop.add(cproflnorm);
+    }
+    else
+    {
+      /*
+       * alignment consensus options
+       */
+      final JCheckBoxMenuItem chist = new JCheckBoxMenuItem(
+              MessageManager.getString("label.show_histogram"),
+              ap.av.isShowConsensusHistogram());
+      chist.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          ap.av.setShowConsensusHistogram(chist.getState());
+          ap.alignFrame.setMenusForViewport();
+          ap.repaint();
+        }
+      });
+      pop.add(chist);
+      final JCheckBoxMenuItem cprof = new JCheckBoxMenuItem(
+              MessageManager.getString("label.show_logo"),
+              ap.av.isShowSequenceLogo());
+      cprof.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          ap.av.setShowSequenceLogo(cprof.getState());
+          ap.alignFrame.setMenusForViewport();
+          ap.repaint();
+        }
+      });
+      pop.add(cprof);
+      final JCheckBoxMenuItem cprofnorm = new JCheckBoxMenuItem(
+              MessageManager.getString("label.normalise_logo"),
+              ap.av.isNormaliseSequenceLogo());
+      cprofnorm.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          ap.av.setShowSequenceLogo(true);
+          ap.av.setNormaliseSequenceLogo(cprofnorm.getState());
+          ap.alignFrame.setMenusForViewport();
+          ap.repaint();
+        }
+      });
+      pop.add(cprofnorm);
     }
-    pop.show(this, evt.getX(), evt.getY());
   }
 
   /**
@@ -705,73 +674,123 @@ public class AnnotationLabels extends JPanel
     if (selectedRow > -1 && ap.av.getAlignment()
             .getAlignmentAnnotation().length > selectedRow)
     {
-      AlignmentAnnotation aa = ap.av.getAlignment()
-              .getAlignmentAnnotation()[selectedRow];
+      AlignmentAnnotation[] anns = ap.av.getAlignment()
+              .getAlignmentAnnotation();
+      AlignmentAnnotation aa = anns[selectedRow];
+
+      String desc = getTooltip(aa);
+      this.setToolTipText(desc);
+      String msg = getStatusMessage(aa, anns);
+      ap.alignFrame.setStatus(msg);
+    }
+  }
+
+  /**
+   * Constructs suitable text to show in the status bar when over an annotation
+   * label, containing the associated sequence name (if any), and the annotation
+   * labels (or all labels for a graph group annotation)
+   * 
+   * @param aa
+   * @param anns
+   * @return
+   */
+  static String getStatusMessage(AlignmentAnnotation aa,
+          AlignmentAnnotation[] anns)
+  {
+    if (aa == null)
+    {
+      return null;
+    }
 
-      StringBuffer desc = new StringBuffer();
-      if (aa.description != null
-              && !aa.description.equals("New description"))
+    StringBuilder msg = new StringBuilder(32);
+    if (aa.sequenceRef != null)
+    {
+      msg.append(aa.sequenceRef.getName()).append(" : ");
+    }
+
+    if (aa.graphGroup == -1)
+    {
+      msg.append(aa.label);
+    }
+    else if (anns != null)
+    {
+      boolean first = true;
+      for (int i = anns.length - 1; i >= 0; i--)
       {
-        // TODO: we could refactor and merge this code with the code in
-        // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature
-        // tooltips
-        desc.append(aa.getDescription(true).trim());
-        // check to see if the description is an html fragment.
-        if (desc.length() < 6 || (desc.substring(0, 6).toLowerCase()
-                .indexOf("<html>") < 0))
+        if (anns[i].graphGroup == aa.graphGroup)
         {
-          // clean the description ready for embedding in html
-          desc = new StringBuffer(LEFT_ANGLE_BRACKET_PATTERN.matcher(desc)
-                  .replaceAll("&lt;"));
-          desc.insert(0, "<html>");
-        }
-        else
-        {
-          // remove terminating html if any
-          int i = desc.substring(desc.length() - 7).toLowerCase()
-                  .lastIndexOf("</html>");
-          if (i > -1)
+          if (!first)
           {
-            desc.setLength(desc.length() - 7 + i);
+            msg.append(", ");
           }
+          msg.append(anns[i].label);
+          first = false;
         }
-        if (aa.hasScore())
-        {
-          desc.append("<br/>");
-        }
-        // if (aa.hasProperties())
-        // {
-        // desc.append("<table>");
-        // for (String prop : aa.getProperties())
-        // {
-        // desc.append("<tr><td>" + prop + "</td><td>"
-        // + aa.getProperty(prop) + "</td><tr>");
-        // }
-        // desc.append("</table>");
-        // }
       }
-      else
+    }
+
+    return msg.toString();
+  }
+
+  /**
+   * Answers a tooltip, formatted as html, containing the annotation description
+   * (prefixed by associated sequence id if applicable), and the annotation
+   * (non-positional) score if it has one. Answers null if neither description
+   * nor score is found.
+   * 
+   * @param aa
+   * @return
+   */
+  static String getTooltip(AlignmentAnnotation aa)
+  {
+    if (aa == null)
+    {
+      return null;
+    }
+    StringBuilder tooltip = new StringBuilder();
+    if (aa.description != null && !aa.description.equals("New description"))
+    {
+      // TODO: we could refactor and merge this code with the code in
+      // jalview.gui.SeqPanel.mouseMoved(..) that formats sequence feature
+      // tooltips
+      String desc = aa.getDescription(true).trim();
+      if (!desc.toLowerCase().startsWith(HTML_START_TAG))
       {
-        // begin the tooltip's html fragment
-        desc.append("<html>");
-        if (aa.hasScore())
-        {
-          // TODO: limit precision of score to avoid noise from imprecise
-          // doubles
-          // (64.7 becomes 64.7+/some tiny value).
-          desc.append(" Score: " + aa.score);
-        }
+        tooltip.append(HTML_START_TAG);
+        desc = desc.replace("<", "&lt;");
       }
-      if (desc.length() > 6)
+      else if (desc.toLowerCase().endsWith(HTML_END_TAG))
       {
-        desc.append("</html>");
-        this.setToolTipText(desc.toString());
+        desc = desc.substring(0, desc.length() - HTML_END_TAG.length());
       }
-      else
+      tooltip.append(desc);
+    }
+    else
+    {
+      // begin the tooltip's html fragment
+      tooltip.append(HTML_START_TAG);
+    }
+    if (aa.hasScore())
+    {
+      if (tooltip.length() > HTML_START_TAG.length())
       {
-        this.setToolTipText(null);
+        tooltip.append("<br/>");
       }
+      // TODO: limit precision of score to avoid noise from imprecise
+      // doubles
+      // (64.7 becomes 64.7+/some tiny value).
+      tooltip.append(" Score: ").append(String.valueOf(aa.score));
+    }
+
+    if (tooltip.length() > HTML_START_TAG.length())
+    {
+      return tooltip.append(HTML_END_TAG).toString();
     }
+
+    /*
+     * nothing in the tooltip (except "<html>")
+     */
+    return null;
   }
 
   /**
index db531a0..ab86707 100755 (executable)
@@ -21,6 +21,7 @@
 package jalview.gui;
 
 import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.HiddenColumns;
@@ -690,30 +691,10 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
   @Override
   public void mouseMoved(MouseEvent evt)
   {
+    int yPos = evt.getY();
     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
 
-    if (aa == null)
-    {
-      this.setToolTipText(null);
-      return;
-    }
-
-    int row = -1;
-    int height = 0;
-
-    for (int i = 0; i < aa.length; i++)
-    {
-      if (aa[i].visible)
-      {
-        height += aa[i].height;
-      }
-
-      if (evt.getY() < height)
-      {
-        row = i;
-        break;
-      }
-    }
+    int row = getRowIndex(yPos, aa);
 
     if (row == -1)
     {
@@ -735,26 +716,63 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     if (row > -1 && ann.annotations != null
             && column < ann.annotations.length)
     {
-      buildToolTip(ann, column, aa);
-      setStatusMessage(column, ann);
+      setToolTipText(buildToolTip(ann, column, aa));
+      String msg = getStatusMessage(av.getAlignment(), column, ann);
+      ap.alignFrame.setStatus(msg);
     }
     else
     {
       this.setToolTipText(null);
-      ap.alignFrame.statusBar.setText(" ");
+      ap.alignFrame.setStatus(" ");
     }
   }
 
   /**
-   * Builds a tooltip for the annotation at the current mouse position.
+   * Answers the index in the annotations array of the visible annotation at the
+   * given y position. This is done by adding the heights of visible annotations
+   * until the y position has been exceeded. Answers -1 if no annotations are
+   * visible, or the y position is below all annotations.
+   * 
+   * @param yPos
+   * @param aa
+   * @return
+   */
+  static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
+  {
+    if (aa == null)
+    {
+      return -1;
+    }
+    int row = -1;
+    int height = 0;
+
+    for (int i = 0; i < aa.length; i++)
+    {
+      if (aa[i].visible)
+      {
+        height += aa[i].height;
+      }
+
+      if (height > yPos)
+      {
+        row = i;
+        break;
+      }
+    }
+    return row;
+  }
+
+  /**
+   * Answers a tooltip for the annotation at the current mouse position
    * 
    * @param ann
    * @param column
    * @param anns
    */
-  void buildToolTip(AlignmentAnnotation ann, int column,
+  static String buildToolTip(AlignmentAnnotation ann, int column,
           AlignmentAnnotation[] anns)
   {
+    String tooltip = null;
     if (ann.graphGroup > -1)
     {
       StringBuilder tip = new StringBuilder(32);
@@ -776,35 +794,39 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       if (tip.length() != 6)
       {
         tip.setLength(tip.length() - 4);
-        this.setToolTipText(tip.toString() + "</html>");
+        tooltip = tip.toString() + "</html>";
       }
     }
-    else if (ann.annotations[column] != null)
+    else if (column < ann.annotations.length
+            && ann.annotations[column] != null)
     {
       String description = ann.annotations[column].description;
       if (description != null && description.length() > 0)
       {
-        this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
+        tooltip = JvSwingUtils.wrapTooltip(true, description);
       }
       else
       {
-        this.setToolTipText(null); // no tooltip if null or empty description
+        tooltip = null; // no tooltip if null or empty description
       }
     }
     else
     {
       // clear the tooltip.
-      this.setToolTipText(null);
+      tooltip = null;
     }
+    return tooltip;
   }
 
   /**
-   * Constructs and displays the status bar message
+   * Constructs and returns the status bar message
    * 
+   * @param al
    * @param column
    * @param ann
    */
-  void setStatusMessage(int column, AlignmentAnnotation ann)
+  static String getStatusMessage(AlignmentI al, int column,
+          AlignmentAnnotation ann)
   {
     /*
      * show alignment column and annotation description if any
@@ -813,7 +835,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     text.append(MessageManager.getString("label.column")).append(" ")
             .append(column + 1);
 
-    if (ann.annotations[column] != null)
+    if (column < ann.annotations.length && ann.annotations[column] != null)
     {
       String description = ann.annotations[column].description;
       if (description != null && description.trim().length() > 0)
@@ -829,7 +851,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     SequenceI seqref = ann.sequenceRef;
     if (seqref != null)
     {
-      int seqIndex = av.getAlignment().findIndex(seqref);
+      int seqIndex = al.findIndex(seqref);
       if (seqIndex != -1)
       {
         text.append(", ").append(MessageManager.getString("label.sequence"))
@@ -839,7 +861,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
         {
           text.append(" ");
           String name;
-          if (av.getAlignment().isNucleotide())
+          if (al.isNucleotide())
           {
             name = ResidueProperties.nucleotideName
                     .get(String.valueOf(residue));
@@ -860,7 +882,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       }
     }
 
-    ap.alignFrame.statusBar.setText(text.toString());
+    return text.toString();
   }
 
   /**
index 2a96daf..b3bff0d 100644 (file)
@@ -322,7 +322,7 @@ public class CutAndPasteTransfer extends GCutAndPasteTransfer
         af.currentFileFormat = format;
         Desktop.addInternalFrame(af, title, AlignFrame.DEFAULT_WIDTH,
                 AlignFrame.DEFAULT_HEIGHT);
-        af.statusBar.setText(MessageManager
+        af.setStatus(MessageManager
                 .getString("label.successfully_pasted_alignment_file"));
 
         try
index 3888a74..1fd9e49 100755 (executable)
  */
 package jalview.gui;
 
+import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.gui.SeqPanel.MousePos;
 import jalview.io.SequenceAnnotationReport;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
@@ -38,6 +40,7 @@ import java.awt.event.MouseWheelListener;
 import java.util.List;
 
 import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
 import javax.swing.SwingUtilities;
 import javax.swing.ToolTipManager;
 
@@ -92,25 +95,46 @@ public class IdPanel extends JPanel
   }
 
   /**
-   * Respond to mouse movement by constructing tooltip text for the sequence id
-   * under the mouse.
+   * Responds to mouse movement by setting tooltip text for the sequence id
+   * under the mouse (or possibly annotation label, when in wrapped mode)
    * 
    * @param e
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseMoved(MouseEvent e)
   {
     SeqPanel sp = alignPanel.getSeqPanel();
-    int seq = Math.max(0, sp.findSeq(e));
-    if (seq > -1 && seq < av.getAlignment().getHeight())
+    MousePos pos = sp.findMousePosition(e);
+    if (pos.isOverAnnotation())
     {
-      SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-      StringBuilder tip = new StringBuilder(64);
-      seqAnnotReport.createTooltipAnnotationReport(tip, sequence,
-              av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr);
-      setToolTipText(JvSwingUtils.wrapTooltip(true,
-              sequence.getDisplayId(true) + " " + tip.toString()));
+      /*
+       * mouse is over an annotation label in wrapped mode
+       */
+      AlignmentAnnotation[] anns = av.getAlignment()
+              .getAlignmentAnnotation();
+      AlignmentAnnotation annotation = anns[pos.annotationIndex];
+      setToolTipText(AnnotationLabels.getTooltip(annotation));
+      alignPanel.alignFrame.setStatus(
+              AnnotationLabels.getStatusMessage(annotation, anns));
+    }
+    else
+    {
+      int seq = Math.max(0, pos.seqIndex);
+      if (seq < av.getAlignment().getHeight())
+      {
+        SequenceI sequence = av.getAlignment().getSequenceAt(seq);
+        StringBuilder tip = new StringBuilder(64);
+        tip.append(sequence.getDisplayId(true)).append(" ");
+        seqAnnotReport.createTooltipAnnotationReport(tip, sequence,
+                av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr);
+        setToolTipText(JvSwingUtils.wrapTooltip(true, tip.toString()));
+
+        StringBuilder text = new StringBuilder();
+        text.append("Sequence ").append(String.valueOf(seq + 1))
+                .append(" ID: ")
+                .append(sequence.getName());
+        alignPanel.alignFrame.setStatus(text.toString());
+      }
     }
   }
 
@@ -125,7 +149,14 @@ public class IdPanel extends JPanel
   {
     mouseDragging = true;
 
-    int seq = Math.max(0, alignPanel.getSeqPanel().findSeq(e));
+    MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
+    if (pos.isOverAnnotation())
+    {
+      // mouse is over annotation label in wrapped mode
+      return;
+    }
+
+    int seq = Math.max(0, pos.seqIndex);
 
     if (seq < lastid)
     {
@@ -196,7 +227,13 @@ public class IdPanel extends JPanel
       return;
     }
 
-    int seq = alignPanel.getSeqPanel().findSeq(e);
+    MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
+    int seq = pos.seqIndex;
+    if (pos.isOverAnnotation() || seq < 0)
+    {
+      return;
+    }
+
     String id = av.getAlignment().getSequenceAt(seq).getName();
     String url = Preferences.sequenceUrlLinks.getPrimaryUrl(id);
 
@@ -276,9 +313,11 @@ public class IdPanel extends JPanel
       return;
     }
 
+    MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
+    
     if (e.isPopupTrigger()) // Mac reports this in mousePressed
     {
-      showPopupMenu(e);
+      showPopupMenu(e, pos);
       return;
     }
 
@@ -301,14 +340,13 @@ public class IdPanel extends JPanel
       av.getSelectionGroup().setEndRes(av.getAlignment().getWidth() - 1);
     }
 
-    int seq = alignPanel.getSeqPanel().findSeq(e);
     if (e.isShiftDown() && (lastid != -1))
     {
-      selectSeqs(lastid, seq);
+      selectSeqs(lastid, pos.seqIndex);
     }
     else
     {
-      selectSeq(seq);
+      selectSeq(pos.seqIndex);
     }
 
     av.isSelectionGroupChanged(true);
@@ -321,10 +359,15 @@ public class IdPanel extends JPanel
    * 
    * @param e
    */
-  void showPopupMenu(MouseEvent e)
+  void showPopupMenu(MouseEvent e, MousePos pos)
   {
-    int seq2 = alignPanel.getSeqPanel().findSeq(e);
-    Sequence sq = (Sequence) av.getAlignment().getSequenceAt(seq2);
+    if (pos.isOverAnnotation())
+    {
+      showAnnotationMenu(e, pos);
+      return;
+    }
+
+    Sequence sq = (Sequence) av.getAlignment().getSequenceAt(pos.seqIndex);
 
     /*
      *  build a new links menu based on the current links
@@ -339,10 +382,7 @@ public class IdPanel extends JPanel
     {
       if (sf.links != null)
       {
-        for (String link : sf.links)
-        {
-          nlinks.add(link);
-        }
+        nlinks.addAll(sf.links);
       }
     }
     }
@@ -353,6 +393,38 @@ public class IdPanel extends JPanel
   }
 
   /**
+   * On right mouse click on a Consensus annotation label, shows a limited popup
+   * menu, with options to configure the consensus calculation and rendering.
+   * 
+   * @param e
+   * @param pos
+   * @see AnnotationLabels#showPopupMenu(MouseEvent)
+   */
+  void showAnnotationMenu(MouseEvent e, MousePos pos)
+  {
+    if (pos.annotationIndex == -1)
+    {
+      return;
+    }
+    AlignmentAnnotation[] anns = this.av.getAlignment()
+            .getAlignmentAnnotation();
+    if (anns == null || pos.annotationIndex >= anns.length)
+    {
+      return;
+    }
+    AlignmentAnnotation ann = anns[pos.annotationIndex];
+    if (!ann.label.contains("Consensus"))
+    {
+      return;
+    }
+
+    JPopupMenu pop = new JPopupMenu(
+            MessageManager.getString("label.annotations"));
+    AnnotationLabels.addConsensusMenuOptions(this.alignPanel, ann, pop);
+    pop.show(this, e.getX(), e.getY());
+  }
+
+  /**
    * Toggle whether the sequence is part of the current selection group.
    * 
    * @param seq
@@ -414,6 +486,7 @@ public class IdPanel extends JPanel
     {
       scrollThread.running = false;
     }
+    MousePos pos = alignPanel.getSeqPanel().findMousePosition(e);
 
     mouseDragging = false;
     PaintRefresher.Refresh(this, av.getSequenceSetId());
@@ -422,7 +495,7 @@ public class IdPanel extends JPanel
 
     if (e.isPopupTrigger()) // Windows reports this in mouseReleased
     {
-      showPopupMenu(e);
+      showPopupMenu(e, pos);
     }
   }
 
index 5c404f0..dc87eba 100755 (executable)
@@ -55,6 +55,11 @@ import javax.swing.JComponent;
  */
 public class SeqCanvas extends JComponent implements ViewportListenerI
 {
+  /*
+   * pixels gap between sequences and annotations when in wrapped mode
+   */
+  static final int SEQS_ANNOTATION_GAP = 3;
+
   private static final String ZEROS = "0000000000";
 
   final FeatureRenderer fr;
@@ -82,9 +87,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
   private int labelWidthWest; // label left width in pixels if shown
 
-  private int wrappedSpaceAboveAlignment; // gap between widths
+  int wrappedSpaceAboveAlignment; // gap between widths
 
-  private int wrappedRepeatHeightPx; // height in pixels of wrapped width
+  int wrappedRepeatHeightPx; // height in pixels of wrapped width
 
   private int wrappedVisibleWidths; // number of wrapped widths displayed
 
@@ -559,7 +564,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     calculateWrappedGeometry(canvasWidth, canvasHeight);
 
     /*
-     * draw one width at a time (excluding any scales or annotation shown),
+     * draw one width at a time (excluding any scales shown),
      * until we have run out of either alignment or vertical space available
      */
     int ypos = wrappedSpaceAboveAlignment;
@@ -606,14 +611,22 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
             * (av.getScaleAboveWrapped() ? 2 : 1);
 
     /*
-     * height in pixels of the wrapped widths
+     * compute height in pixels of the wrapped widths
+     * - start with space above plus sequences
      */
     wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
-    // add sequences
     wrappedRepeatHeightPx += av.getAlignment().getHeight()
             * charHeight;
-    // add annotations panel height if shown
-    wrappedRepeatHeightPx += getAnnotationHeight();
+
+    /*
+     * add annotations panel height if shown
+     * also gap between sequences and annotations
+     */
+    if (av.isShowAnnotation())
+    {
+      wrappedRepeatHeightPx += getAnnotationHeight();
+      wrappedRepeatHeightPx += SEQS_ANNOTATION_GAP; // 3px
+    }
 
     /*
      * number of visible widths (the last one may be part height),
@@ -657,8 +670,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
    * @param endColumn
    * @param canvasHeight
    */
-  protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
-          int endColumn, int canvasHeight)
+  protected void drawWrappedWidth(Graphics g, final int ypos,
+          final int startColumn, final int endColumn,
+          final int canvasHeight)
   {
     ViewportRanges ranges = av.getRanges();
     int viewportWidth = ranges.getViewportWidth();
@@ -705,7 +719,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
     if (av.isShowAnnotation())
     {
-      g.translate(0, cHeight + ypos + 3);
+      final int yShift = cHeight + ypos + SEQS_ANNOTATION_GAP;
+      g.translate(0, yShift);
       if (annotations == null)
       {
         annotations = new AnnotationPanel(av);
@@ -713,7 +728,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
       annotations.renderer.drawComponent(annotations, av, g, -1,
               startColumn, endx + 1);
-      g.translate(0, -cHeight - ypos - 3);
+      g.translate(0, -yShift);
     }
     g.setClip(clip);
     g.translate(-xOffset, 0);
index 7292499..7abbd7d 100644 (file)
@@ -25,6 +25,7 @@ import jalview.bin.Cache;
 import jalview.commands.EditCommand;
 import jalview.commands.EditCommand.Action;
 import jalview.commands.EditCommand.Edit;
+import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.HiddenColumns;
@@ -76,6 +77,82 @@ public class SeqPanel extends JPanel
         implements MouseListener, MouseMotionListener, MouseWheelListener,
         SequenceListener, SelectionListener
 {
+  /*
+   * a class that holds computed mouse position
+   * - column of the alignment (0...)
+   * - sequence offset (0...)
+   * - annotation row offset (0...)
+   * where annotation offset is -1 unless the alignment is shown
+   * in wrapped mode, annotations are shown, and the mouse is
+   * over an annnotation row
+   */
+  static class MousePos
+  {
+    /*
+     * alignment column position of cursor (0...)
+     */
+    final int column;
+
+    /*
+     * index in alignment of sequence under cursor,
+     * or nearest above if cursor is not over a sequence
+     */
+    final int seqIndex;
+
+    /*
+     * index in annotations array of annotation under the cursor
+     * (only possible in wrapped mode with annotations shown),
+     * or -1 if cursor is not over an annotation row
+     */
+    final int annotationIndex;
+
+    MousePos(int col, int seq, int ann)
+    {
+      column = col;
+      seqIndex = seq;
+      annotationIndex = ann;
+    }
+
+    boolean isOverAnnotation()
+    {
+      return annotationIndex != -1;
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+      if (obj == null || !(obj instanceof MousePos))
+      {
+        return false;
+      }
+      MousePos o = (MousePos) obj;
+      boolean b = (column == o.column && seqIndex == o.seqIndex
+              && annotationIndex == o.annotationIndex);
+      // System.out.println(obj + (b ? "= " : "!= ") + this);
+      return b;
+    }
+
+    /**
+     * A simple hashCode that ensures that instances that satisfy equals() have
+     * the same hashCode
+     */
+    @Override
+    public int hashCode()
+    {
+      return column + seqIndex + annotationIndex;
+    }
+
+    /**
+     * toString method for debug output purposes only
+     */
+    @Override
+    public String toString()
+    {
+      return String.format("c%d:s%d:a%d", column, seqIndex,
+              annotationIndex);
+    }
+  }
+
   private static final int MAX_TOOLTIP_LENGTH = 300;
 
   public SeqCanvas seqCanvas;
@@ -83,14 +160,9 @@ public class SeqPanel extends JPanel
   public AlignmentPanel ap;
 
   /*
-   * last column position for mouseMoved event
+   * last position for mouseMoved event
    */
-  private int lastMouseColumn;
-
-  /*
-   * last sequence offset for mouseMoved event
-   */
-  private int lastMouseSeq;
+  private MousePos lastMousePosition;
 
   protected int editLastRes;
 
@@ -176,9 +248,6 @@ public class SeqPanel extends JPanel
       ssm.addStructureViewerListener(this);
       ssm.addSelectionListener(this);
     }
-
-    lastMouseColumn = -1;
-    lastMouseSeq = -1;
   }
 
   int startWrapBlock = -1;
@@ -186,6 +255,71 @@ public class SeqPanel extends JPanel
   int wrappedBlock = -1;
 
   /**
+   * Computes the column and sequence row (and possibly annotation row when in
+   * wrapped mode) for the given mouse position
+   * 
+   * @param evt
+   * @return
+   */
+  MousePos findMousePosition(MouseEvent evt)
+  {
+    int col = findColumn(evt);
+    int seqIndex = -1;
+    int annIndex = -1;
+    int y = evt.getY();
+
+    int charHeight = av.getCharHeight();
+    int alignmentHeight = av.getAlignment().getHeight();
+    if (av.getWrapAlignment())
+    {
+      seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
+              seqCanvas.getHeight());
+
+      /*
+       * yPos modulo height of repeating width
+       */
+      int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
+
+      /*
+       * height of sequences plus space / scale above,
+       * plus gap between sequences and annotations
+       */
+      int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
+              + alignmentHeight * charHeight
+              + SeqCanvas.SEQS_ANNOTATION_GAP;
+      if (yOffsetPx >= alignmentHeightPixels)
+      {
+        /*
+         * mouse is over annotations; find annotation index, also set
+         * last sequence above (for backwards compatible behaviour)
+         */
+        AlignmentAnnotation[] anns = av.getAlignment()
+                .getAlignmentAnnotation();
+        int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
+        annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
+        seqIndex = alignmentHeight - 1;
+      }
+      else
+      {
+        /*
+         * mouse is over sequence (or the space above sequences)
+         */
+        yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
+        if (yOffsetPx >= 0)
+        {
+          seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
+        }
+      }
+    }
+    else
+    {
+      seqIndex = Math.min((y / charHeight) + av.getRanges().getStartSeq(),
+              alignmentHeight - 1);
+    }
+
+    return new MousePos(col, seqIndex, annIndex);
+  }
+  /**
    * Returns the aligned sequence position (base 0) at the mouse position, or
    * the closest visible one
    * 
@@ -197,10 +331,11 @@ public class SeqPanel extends JPanel
     int res = 0;
     int x = evt.getX();
 
-    int startRes = av.getRanges().getStartRes();
+    final int startRes = av.getRanges().getStartRes();
+    final int charWidth = av.getCharWidth();
+
     if (av.getWrapAlignment())
     {
-
       int hgap = av.getCharHeight();
       if (av.getScaleAboveWrapped())
       {
@@ -212,35 +347,40 @@ public class SeqPanel extends JPanel
 
       int y = evt.getY();
       y = Math.max(0, y - hgap);
-      x = Math.max(0, x - seqCanvas.getLabelWidthWest());
+      x -= seqCanvas.getLabelWidthWest();
+      if (x < 0)
+      {
+        // mouse is over left scale
+        return -1;
+      }
 
       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
       if (cwidth < 1)
       {
         return 0;
       }
+      if (x >= cwidth * charWidth)
+      {
+        // mouse is over right scale
+        return -1;
+      }
 
       wrappedBlock = y / cHeight;
       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());
+              + Math.min(cwidth - 1, x / charWidth);
     }
     else
     {
-      if (x > seqCanvas.getX() + seqCanvas.getWidth())
-      {
-        // make sure we calculate relative to visible alignment, rather than
-        // right-hand gutter
-        x = seqCanvas.getX() + seqCanvas.getWidth();
-      }
-      res = (x / av.getCharWidth()) + startRes;
-      if (res > av.getRanges().getEndRes())
-      {
-        // moused off right
-        res = av.getRanges().getEndRes();
-      }
+      /*
+       * make sure we calculate relative to visible alignment, 
+       * rather than right-hand gutter
+       */
+      x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
+      res = (x / charWidth) + startRes;
+      res = Math.min(res, av.getRanges().getEndRes());
     }
 
     if (av.hasHiddenColumns())
@@ -250,38 +390,6 @@ public class SeqPanel extends JPanel
     }
 
     return res;
-
-  }
-
-  int findSeq(MouseEvent evt)
-  {
-    int seq = 0;
-    int y = evt.getY();
-
-    if (av.getWrapAlignment())
-    {
-      int hgap = av.getCharHeight();
-      if (av.getScaleAboveWrapped())
-      {
-        hgap += av.getCharHeight();
-      }
-
-      int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
-              + hgap + seqCanvas.getAnnotationHeight();
-
-      y -= hgap;
-
-      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);
-    }
-
-    return seq;
   }
 
   /**
@@ -617,13 +725,19 @@ public class SeqPanel extends JPanel
   @Override
   public void mouseReleased(MouseEvent evt)
   {
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+    {
+      return;
+    }
+
     boolean didDrag = mouseDragging; // did we come here after a drag
     mouseDragging = false;
     mouseWheelPressed = false;
 
     if (evt.isPopupTrigger()) // Windows: mouseReleased
     {
-      showPopupMenu(evt);
+      showPopupMenu(evt, pos);
       evt.consume();
       return;
     }
@@ -647,6 +761,11 @@ public class SeqPanel extends JPanel
   public void mousePressed(MouseEvent evt)
   {
     lastMousePress = evt.getPoint();
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+    {
+      return;
+    }
 
     if (SwingUtilities.isMiddleMouseButton(evt))
     {
@@ -665,17 +784,12 @@ public class SeqPanel extends JPanel
     }
     else
     {
-      doMousePressedDefineMode(evt);
+      doMousePressedDefineMode(evt, pos);
       return;
     }
 
-    int seq = findSeq(evt);
-    int res = findColumn(evt);
-
-    if (seq < 0 || res < 0)
-    {
-      return;
-    }
+    int seq = pos.seqIndex;
+    int res = pos.column;
 
     if ((seq < av.getAlignment().getHeight())
             && (res < av.getAlignment().getSequenceAt(seq).getLength()))
@@ -777,23 +891,32 @@ public class SeqPanel extends JPanel
       mouseDragged(evt);
     }
 
-    final int column = findColumn(evt);
-    final int seq = findSeq(evt);
+    final MousePos mousePos = findMousePosition(evt);
+    if (mousePos.equals(lastMousePosition))
+    {
+      /*
+       * just a pixel move without change of 'cell'
+       */
+      return;
+    }
+    lastMousePosition = mousePos;
 
-    if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
+    if (mousePos.isOverAnnotation())
     {
-      lastMouseSeq = -1;
+      mouseMovedOverAnnotation(mousePos);
       return;
     }
-    if (column == lastMouseColumn && seq == lastMouseSeq)
+    final int seq = mousePos.seqIndex;
+
+    final int column = mousePos.column;
+    if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
     {
-      /*
-       * just a pixel move without change of residue
-       */
+      lastMousePosition = null;
+      setToolTipText(null);
+      lastTooltip = null;
+      ap.alignFrame.setStatus("");
       return;
     }
-    lastMouseColumn = column;
-    lastMouseSeq = seq;
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
@@ -871,6 +994,35 @@ public class SeqPanel extends JPanel
     }
   }
 
+  /**
+   * When the view is in wrapped mode, and the mouse is over an annotation row,
+   * shows the corresponding tooltip and status message (if any)
+   * 
+   * @param pos
+   * @param column
+   */
+  protected void mouseMovedOverAnnotation(MousePos pos)
+  {
+    final int column = pos.column;
+    final int rowIndex = pos.annotationIndex;
+
+    if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
+            || rowIndex < 0)
+    {
+      return;
+    }
+    AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
+
+    String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
+            anns);
+    setToolTipText(tooltip);
+    lastTooltip = tooltip;
+
+    String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
+            anns[rowIndex]);
+    ap.alignFrame.setStatus(msg);
+  }
+
   private Point lastp = null;
 
   /*
@@ -881,20 +1033,26 @@ public class SeqPanel extends JPanel
   @Override
   public Point getToolTipLocation(MouseEvent event)
   {
-    int x = event.getX(), w = getWidth();
-    int wdth = (w - x < 200) ? -(w / 2) : 5; // switch sides when tooltip is too
-    // close to edge
+    if (tooltipText == null || tooltipText.length() <= 6)
+    {
+      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)
     {
-      p = (tooltipText != null && tooltipText.length() > 6)
-              ? new Point(event.getX() + wdth, event.getY() - 20)
-              : null;
+      p = new Point(event.getX() + wdth, event.getY() - 20);
+      lastp = p;
     }
     /*
-     * TODO: try to modify position region is not obcured by tooltip
+     * TODO: try to set position so region is not obscured by tooltip
      */
-    return lastp = p;
+    return p;
   }
 
   String lastTooltip;
@@ -997,7 +1155,7 @@ public class SeqPanel extends JPanel
 
       text.append(" (").append(Integer.toString(residuePos)).append(")");
     }
-    ap.alignFrame.statusBar.setText(text.toString());
+    ap.alignFrame.setStatus(text.toString());
   }
 
   /**
@@ -1039,6 +1197,12 @@ public class SeqPanel extends JPanel
   @Override
   public void mouseDragged(MouseEvent evt)
   {
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.column == -1)
+    {
+      return;
+    }
+
     if (mouseWheelPressed)
     {
       boolean inSplitFrame = ap.av.getCodingComplement() != null;
@@ -1134,11 +1298,11 @@ public class SeqPanel extends JPanel
 
     if (!editingSeqs)
     {
-      doMouseDraggedDefineMode(evt);
+      dragStretchGroup(evt);
       return;
     }
 
-    int res = findColumn(evt);
+    int res = pos.column;
 
     if (res < 0)
     {
@@ -1212,6 +1376,8 @@ public class SeqPanel extends JPanel
       }
     }
 
+    StringBuilder message = new StringBuilder(64); // for status bar
+
     /*
      * make a name for the edit action, for
      * status bar message and Undo/Redo menu
@@ -1219,10 +1385,12 @@ public class SeqPanel extends JPanel
     String label = null;
     if (groupEditing)
     {
+        message.append("Edit group:");
       label = MessageManager.getString("action.edit_group");
     }
     else
     {
+        message.append("Edit sequence: " + seq.getName());
       label = seq.getName();
       if (label.length() > 10)
       {
@@ -1242,6 +1410,18 @@ public class SeqPanel extends JPanel
       editCommand = new EditCommand(label);
     }
 
+    if (insertGap)
+    {
+      message.append(" insert ");
+    }
+    else
+    {
+      message.append(" delete ");
+    }
+
+    message.append(Math.abs(startres - editLastRes) + " gaps.");
+    ap.alignFrame.setStatus(message.toString());
+
     /*
      * is there a selection group containing the sequence being edited?
      * if so the boundary of the group is the limit of the edit
@@ -1327,7 +1507,7 @@ public class SeqPanel extends JPanel
      * what was requested), by inspecting the edit commands added
      */
     String msg = getEditStatusMessage(editCommand);
-    ap.alignFrame.statusBar.setText(msg == null ? " " : msg);
+    ap.alignFrame.setStatus(msg == null ? " " : msg);
     if (!success)
     {
       endEditing();
@@ -1747,6 +1927,7 @@ public class SeqPanel extends JPanel
   @Override
   public void mouseExited(MouseEvent e)
   {
+    ap.alignFrame.setStatus(" ");
     if (av.getWrapAlignment())
     {
       return;
@@ -1767,7 +1948,12 @@ public class SeqPanel extends JPanel
   public void mouseClicked(MouseEvent evt)
   {
     SequenceGroup sg = null;
-    SequenceI sequence = av.getAlignment().getSequenceAt(findSeq(evt));
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+    {
+      return;
+    }
+
     if (evt.getClickCount() > 1)
     {
       sg = av.getSelectionGroup();
@@ -1777,12 +1963,13 @@ public class SeqPanel extends JPanel
         av.setSelectionGroup(null);
       }
 
-      int column = findColumn(evt);
+      int column = pos.column;
 
       /*
        * find features at the position (if not gapped), or straddling
        * the position (if at a gap)
        */
+      SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
 
@@ -1849,32 +2036,22 @@ public class SeqPanel extends JPanel
   /**
    * DOCUMENT ME!
    * 
-   * @param evt
+   * @param pos
    *          DOCUMENT ME!
    */
-  public void doMousePressedDefineMode(MouseEvent evt)
+  protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
   {
-    final int res = findColumn(evt);
-    final int seq = findSeq(evt);
-    oldSeq = seq;
-    updateOverviewAndStructs = false;
-
-    startWrapBlock = wrappedBlock;
-
-    if (av.getWrapAlignment() && seq > av.getAlignment().getHeight())
+    if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
     {
-      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-              MessageManager.getString(
-                      "label.cannot_edit_annotations_in_wrapped_view"),
-              MessageManager.getString("label.wrapped_view_no_edit"),
-              JvOptionPane.WARNING_MESSAGE);
       return;
     }
 
-    if (seq < 0 || res < 0)
-    {
-      return;
-    }
+    final int res = pos.column;
+    final int seq = pos.seqIndex;
+    oldSeq = seq;
+    updateOverviewAndStructs = false;
+
+    startWrapBlock = wrappedBlock;
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
@@ -1898,7 +2075,7 @@ public class SeqPanel extends JPanel
 
     if (evt.isPopupTrigger()) // Mac: mousePressed
     {
-      showPopupMenu(evt);
+      showPopupMenu(evt, pos);
       return;
     }
 
@@ -1914,8 +2091,8 @@ public class SeqPanel extends JPanel
 
     if (av.cursorMode)
     {
-      seqCanvas.cursorX = findColumn(evt);
-      seqCanvas.cursorY = findSeq(evt);
+      seqCanvas.cursorX = res;
+      seqCanvas.cursorY = seq;
       seqCanvas.repaint();
       return;
     }
@@ -1973,15 +2150,14 @@ public class SeqPanel extends JPanel
 
   /**
    * Build and show a pop-up menu at the right-click mouse position
-   * 
+   *
    * @param evt
-   * @param res
-   * @param sequences
+   * @param pos
    */
-  void showPopupMenu(MouseEvent evt)
+  void showPopupMenu(MouseEvent evt, MousePos pos)
   {
-    final int column = findColumn(evt);
-    final int seq = findSeq(evt);
+    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);
@@ -1999,7 +2175,8 @@ public class SeqPanel extends JPanel
    *          true if this event is happening after a mouse drag (rather than a
    *          mouse down)
    */
-  public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
+  protected void doMouseReleasedDefineMode(MouseEvent evt,
+          boolean afterDrag)
   {
     if (stretchGroup == null)
     {
@@ -2045,22 +2222,28 @@ public class SeqPanel extends JPanel
   }
 
   /**
-   * DOCUMENT ME!
+   * Resizes the borders of a selection group depending on the direction of
+   * mouse drag
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
-  public void doMouseDraggedDefineMode(MouseEvent evt)
+  protected void dragStretchGroup(MouseEvent evt)
   {
-    int res = findColumn(evt);
-    int y = findSeq(evt);
+    if (stretchGroup == null)
+    {
+      return;
+    }
 
-    if (wrappedBlock != startWrapBlock)
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
     {
       return;
     }
 
-    if (stretchGroup == null)
+    int res = pos.column;
+    int y = pos.seqIndex;
+
+    if (wrappedBlock != startWrapBlock)
     {
       return;
     }
index 8754fbb..1c4e6a6 100755 (executable)
@@ -1038,7 +1038,7 @@ public class SequenceFetcher extends JPanel implements Runnable
         Desktop.addInternalFrame(af, title, AlignFrame.DEFAULT_WIDTH,
                 AlignFrame.DEFAULT_HEIGHT);
 
-        af.statusBar.setText(MessageManager
+        af.setStatus(MessageManager
                 .getString("label.successfully_pasted_alignment_file"));
 
         try
index 2a18b0b..791f881 100755 (executable)
@@ -440,7 +440,7 @@ public class FileLoader implements Runnable
               alignFrame.getViewport()
                       .applyFeaturesStyle(proxyColourScheme);
             }
-            alignFrame.statusBar.setText(MessageManager.formatMessage(
+            alignFrame.setStatus(MessageManager.formatMessage(
                     "label.successfully_loaded_file", new String[]
                     { title }));
 
index 1cf482d..d5c8c01 100755 (executable)
@@ -75,7 +75,7 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenu sortByAnnotScore = new JMenu();
 
-  public JLabel statusBar = new JLabel();
+  protected JLabel statusBar = new JLabel();
 
   protected JMenu outputTextboxMenu = new JMenu();
 
@@ -201,7 +201,7 @@ public class GAlignFrame extends JInternalFrame
 
   private boolean showAutoCalculatedAbove = false;
 
-  private Map<KeyStroke, JMenuItem> accelerators = new HashMap<KeyStroke, JMenuItem>();
+  private Map<KeyStroke, JMenuItem> accelerators = new HashMap<>();
 
   private SplitContainerI splitFrame;
 
diff --git a/test/jalview/gui/AnnotationLabelsTest.java b/test/jalview/gui/AnnotationLabelsTest.java
new file mode 100644 (file)
index 0000000..616a1a6
--- /dev/null
@@ -0,0 +1,153 @@
+package jalview.gui;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Sequence;
+
+import org.testng.annotations.Test;
+
+public class AnnotationLabelsTest
+{
+  @Test(groups = "Functional")
+  public void testGetTooltip()
+  {
+    assertNull(AnnotationLabels.getTooltip(null));
+
+    /*
+     * simple description only
+     */
+    AlignmentAnnotation ann = new AlignmentAnnotation("thelabel", "thedesc",
+            null);
+    String expected = "<html>thedesc</html>";
+    assertEquals(AnnotationLabels.getTooltip(ann), expected);
+
+    /*
+     * description needing html encoding
+     * (no idea why '<' is encoded but '>' is not)
+     */
+    ann.description = "TCoffee scores < 56 and > 28";
+    expected = "<html>TCoffee scores &lt; 56 and > 28</html>";
+    assertEquals(AnnotationLabels.getTooltip(ann), expected);
+
+    /*
+     * description already html formatted
+     */
+    ann.description = "<html>hello world</html>";
+    assertEquals(AnnotationLabels.getTooltip(ann), ann.description);
+
+    /*
+     * simple description and score
+     */
+    ann.description = "hello world";
+    ann.setScore(2.34d);
+    expected = "<html>hello world<br/> Score: 2.34</html>";
+    assertEquals(AnnotationLabels.getTooltip(ann), expected);
+
+    /*
+     * html description and score
+     */
+    ann.description = "<html>hello world</html>";
+    ann.setScore(2.34d);
+    expected = "<html>hello world<br/> Score: 2.34</html>";
+    assertEquals(AnnotationLabels.getTooltip(ann), expected);
+
+    /*
+     * score, no description
+     */
+    ann.description = " ";
+    assertEquals(AnnotationLabels.getTooltip(ann),
+            "<html> Score: 2.34</html>");
+    ann.description = null;
+    assertEquals(AnnotationLabels.getTooltip(ann),
+            "<html> Score: 2.34</html>");
+    
+    /*
+     * sequenceref, simple description
+     */
+    ann.description = "Count < 12";
+    ann.sequenceRef = new Sequence("Seq1", "MLRIQST");
+    ann.hasScore = false;
+    ann.score = Double.NaN;
+    expected = "<html>Seq1 : Count &lt; 12</html>";
+    assertEquals(AnnotationLabels.getTooltip(ann), expected);
+
+    /*
+     * sequenceref, html description, score
+     */
+    ann.description = "<html>Score < 4.8</html>";
+    ann.sequenceRef = new Sequence("Seq1", "MLRIQST");
+    ann.setScore(-2.1D);
+    expected = "<html>Seq1 : Score < 4.8<br/> Score: -2.1</html>";
+    assertEquals(AnnotationLabels.getTooltip(ann), expected);
+
+    /*
+     * no score, null description
+     */
+    ann.description = null;
+    ann.hasScore = false;
+    ann.score = Double.NaN;
+    assertNull(AnnotationLabels.getTooltip(ann));
+
+    /*
+     * no score, empty description, sequenceRef
+     */
+    ann.description = "";
+    assertEquals(AnnotationLabels.getTooltip(ann), "<html>Seq1 :</html>");
+
+    /*
+     * no score, empty description, no sequenceRef
+     */
+    ann.sequenceRef = null;
+    assertNull(AnnotationLabels.getTooltip(ann));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetStatusMessage()
+  {
+    assertNull(AnnotationLabels.getStatusMessage(null, null));
+
+    /*
+     * simple label
+     */
+    AlignmentAnnotation aa = new AlignmentAnnotation("IUPredWS Short",
+            "Protein disorder", null);
+    assertEquals(AnnotationLabels.getStatusMessage(aa, null),
+            "IUPredWS Short");
+
+    /*
+     * with sequence ref
+     */
+    aa.setSequenceRef(new Sequence("FER_CAPAA", "MIGRKQL"));
+    assertEquals(AnnotationLabels.getStatusMessage(aa, null),
+            "FER_CAPAA : IUPredWS Short");
+
+    /*
+     * with graph group (degenerate, one annotation only)
+     */
+    aa.graphGroup = 1;
+    AlignmentAnnotation aa2 = new AlignmentAnnotation("IUPredWS Long",
+            "Protein disorder", null);
+    assertEquals(
+            AnnotationLabels.getStatusMessage(aa, new AlignmentAnnotation[]
+            { aa, aa2 }), "FER_CAPAA : IUPredWS Short");
+
+    /*
+     * graph group with two members; note labels are appended in
+     * reverse order (matching rendering order on screen)
+     */
+    aa2.graphGroup = 1;
+    assertEquals(
+            AnnotationLabels.getStatusMessage(aa, new AlignmentAnnotation[]
+            { aa, aa2 }), "FER_CAPAA : IUPredWS Long, IUPredWS Short");
+
+    /*
+     * graph group with no sequence ref
+     */
+    aa.sequenceRef = null;
+    assertEquals(
+            AnnotationLabels.getStatusMessage(aa, new AlignmentAnnotation[]
+            { aa, aa2 }), "IUPredWS Long, IUPredWS Short");
+  }
+}
diff --git a/test/jalview/gui/AnnotationPanelTest.java b/test/jalview/gui/AnnotationPanelTest.java
new file mode 100644 (file)
index 0000000..5f7d5a7
--- /dev/null
@@ -0,0 +1,51 @@
+package jalview.gui;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.AlignmentAnnotation;
+
+import org.testng.annotations.Test;
+
+public class AnnotationPanelTest
+{
+
+  @Test(groups = "Functional")
+  public void testGetRowIndex()
+  {
+    assertEquals(AnnotationPanel.getRowIndex(0, null), -1);
+
+    AlignmentAnnotation[] anns = new AlignmentAnnotation[] {};
+    assertEquals(AnnotationPanel.getRowIndex(0, anns), -1);
+
+    AlignmentAnnotation ann1 = new AlignmentAnnotation(null, null, null);
+    AlignmentAnnotation ann2 = new AlignmentAnnotation(null, null, null);
+    AlignmentAnnotation ann3 = new AlignmentAnnotation(null, null, null);
+    ann1.visible = true;
+    ann2.visible = true;
+    ann3.visible = true;
+    ann1.height = 10;
+    ann2.height = 20;
+    ann3.height = 30;
+    anns = new AlignmentAnnotation[] { ann1, ann2, ann3 };
+
+    assertEquals(AnnotationPanel.getRowIndex(0, anns), 0);
+    assertEquals(AnnotationPanel.getRowIndex(9, anns), 0);
+    assertEquals(AnnotationPanel.getRowIndex(10, anns), 1);
+    assertEquals(AnnotationPanel.getRowIndex(29, anns), 1);
+    assertEquals(AnnotationPanel.getRowIndex(30, anns), 2);
+    assertEquals(AnnotationPanel.getRowIndex(59, anns), 2);
+    assertEquals(AnnotationPanel.getRowIndex(60, anns), -1);
+
+    ann2.visible = false;
+    assertEquals(AnnotationPanel.getRowIndex(0, anns), 0);
+    assertEquals(AnnotationPanel.getRowIndex(9, anns), 0);
+    assertEquals(AnnotationPanel.getRowIndex(10, anns), 2);
+    assertEquals(AnnotationPanel.getRowIndex(39, anns), 2);
+    assertEquals(AnnotationPanel.getRowIndex(40, anns), -1);
+
+    ann1.visible = false;
+    assertEquals(AnnotationPanel.getRowIndex(0, anns), 2);
+    assertEquals(AnnotationPanel.getRowIndex(29, anns), 2);
+    assertEquals(AnnotationPanel.getRowIndex(30, anns), -1);
+  }
+}
index 5298680..73aeb79 100644 (file)
@@ -29,10 +29,10 @@ import jalview.io.FileLoader;
 import java.awt.Font;
 import java.awt.FontMetrics;
 
-import junit.extensions.PA;
-
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class SeqCanvasTest
 {
   /**
@@ -97,8 +97,8 @@ public class SeqCanvasTest
     assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 3);
 
     /*
-     * reduce canvas height by 1 pixel - should not be enough height
-     * to draw 3 widths
+     * reduce canvas height by 1 pixel 
+     * - should not be enough height to draw 3 widths
      */
     canvasHeight -= 1;
     testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
@@ -170,11 +170,11 @@ public class SeqCanvasTest
     canvasWidth += 8;
     wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
             canvasHeight);
-    assertEquals(wrappedWidth, 27);
+    assertEquals(wrappedWidth, 27); // 8px not enough
     canvasWidth += 1;
     wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
             canvasHeight);
-    assertEquals(wrappedWidth, 28);
+    assertEquals(wrappedWidth, 28); // 9px is enough
 
     /*
      * now West but not East scale - lose 39 pixels or 4 columns
@@ -190,11 +190,11 @@ public class SeqCanvasTest
     canvasWidth += 2;
     wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
             canvasHeight);
-    assertEquals(wrappedWidth, 24);
+    assertEquals(wrappedWidth, 24); // 2px not enough
     canvasWidth += 1;
     wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
             canvasHeight);
-    assertEquals(wrappedWidth, 25);
+    assertEquals(wrappedWidth, 25); // 3px is enough
 
     /*
      * turn off scales left and right, make width exactly 157 columns
@@ -256,15 +256,16 @@ public class SeqCanvasTest
             2 * charHeight);
     int repeatingHeight = (int) PA.getValue(testee, "wrappedRepeatHeightPx");
     assertEquals(repeatingHeight, charHeight * (2 + al.getHeight())
-            + annotationHeight);
+            + SeqCanvas.SEQS_ANNOTATION_GAP + annotationHeight);
     assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 1);
   
     /*
-     * repeat height is 17 * (2 + 15) = 289 + annotationHeight = 507
-     * make canvas height 2 * 289 + 3 * charHeight so just enough to
-     * draw 2 widths and the first sequence of a third
+     * repeat height is 17 * (2 + 15) = 289 + 3 + annotationHeight = 510
+     * make canvas height 2 of these plus 3 charHeights 
+     * so just enough to draw 2 widths, gap + scale + the first sequence of a third
      */
-    canvasHeight = charHeight * (17 * 2 + 3) + 2 * annotationHeight;
+    canvasHeight = charHeight * (17 * 2 + 3)
+            + 2 * (annotationHeight + SeqCanvas.SEQS_ANNOTATION_GAP);
     testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
     assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 3);
   
@@ -287,7 +288,8 @@ public class SeqCanvasTest
      * reduce height to enough for 2 widths and not quite a third
      * i.e. two repeating heights + spacer + sequence - 1 pixel
      */
-    canvasHeight = charHeight * (16 * 2 + 2) + 2 * annotationHeight - 1;
+    canvasHeight = charHeight * (16 * 2 + 2)
+            + 2 * (annotationHeight + SeqCanvas.SEQS_ANNOTATION_GAP) - 1;
     testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
     assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 2);
 
index 7f3aef1..f163299 100644 (file)
@@ -22,7 +22,11 @@ package jalview.gui;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
 
+import jalview.api.AlignViewportI;
+import jalview.bin.Cache;
+import jalview.bin.Jalview;
 import jalview.commands.EditCommand;
 import jalview.commands.EditCommand.Action;
 import jalview.commands.EditCommand.Edit;
@@ -30,11 +34,22 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
+import jalview.gui.SeqPanel.MousePos;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
 import jalview.util.MessageManager;
 
+import java.awt.Event;
+import java.awt.event.MouseEvent;
+
+import javax.swing.JLabel;
+
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class SeqPanelTest
 {
   AlignFrame af;
@@ -59,22 +74,24 @@ public class SeqPanelTest
     assertEquals(
             alignFrame.alignPanel.getSeqPanel().setStatusMessage(
                     visAl.getSequenceAt(1), 1, 1), 2);
-    assertEquals(alignFrame.statusBar.getText(),
+    assertEquals(((JLabel) PA.getValue(alignFrame, "statusBar")).getText(),
             "Sequence 2 ID: Seq2 Residue: ALA (2)");
     assertEquals(
             alignFrame.alignPanel.getSeqPanel().setStatusMessage(
                     visAl.getSequenceAt(1), 4, 1), 3);
-    assertEquals(alignFrame.statusBar.getText(),
+    assertEquals(((JLabel) PA.getValue(alignFrame, "statusBar")).getText(),
             "Sequence 2 ID: Seq2 Residue: GLU (3)");
     // no status message at a gap, returns next residue position to the right
     assertEquals(
             alignFrame.alignPanel.getSeqPanel().setStatusMessage(
                     visAl.getSequenceAt(1), 2, 1), 3);
-    assertEquals(alignFrame.statusBar.getText(), "Sequence 2 ID: Seq2");
+    assertEquals(((JLabel) PA.getValue(alignFrame, "statusBar")).getText(),
+            "Sequence 2 ID: Seq2");
     assertEquals(
             alignFrame.alignPanel.getSeqPanel().setStatusMessage(
                     visAl.getSequenceAt(1), 3, 1), 3);
-    assertEquals(alignFrame.statusBar.getText(), "Sequence 2 ID: Seq2");
+    assertEquals(((JLabel) PA.getValue(alignFrame, "statusBar")).getText(),
+            "Sequence 2 ID: Seq2");
   }
 
   @Test(groups = "Functional")
@@ -90,7 +107,7 @@ public class SeqPanelTest
     assertEquals(
             alignFrame.alignPanel.getSeqPanel().setStatusMessage(
                     visAl.getSequenceAt(1), 1, 1), 2);
-    assertEquals(alignFrame.statusBar.getText(),
+    assertEquals(((JLabel) PA.getValue(alignFrame, "statusBar")).getText(),
             "Sequence 2 ID: Seq2 Residue: B (2)");
   }
 
@@ -198,4 +215,751 @@ public class SeqPanelTest
     expected = MessageManager.formatMessage("label.delete_gaps", "3");
     assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
   }
+
+  public void testFindMousePosition_unwrapped()
+  {
+    String seqData = ">Seq1\nAACDE\n>Seq2\nAA--E\n";
+    AlignFrame alignFrame = new FileLoader().LoadFileWaitTillLoaded(seqData,
+            DataSourceType.PASTE);
+    AlignViewportI av = alignFrame.getViewport();
+    av.setShowAnnotation(true);
+    av.setWrapAlignment(false);
+    final int charHeight = av.getCharHeight();
+    final int charWidth = av.getCharWidth();
+    // sanity checks:
+    assertTrue(charHeight > 0);
+    assertTrue(charWidth > 0);
+    assertTrue(alignFrame.alignPanel.getSeqPanel().getWidth() > 0);
+
+    SeqPanel testee = alignFrame.alignPanel.getSeqPanel();
+    int x = 0;
+    int y = 0;
+
+    /*
+     * mouse at top left of unwrapped panel
+     */
+    MouseEvent evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y,
+            0, 0, 0, false, 0);
+    MousePos pos = testee.findMousePosition(evt);
+    assertEquals(pos.column, 0);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.annotationIndex, -1);
+  }
+
+  @AfterMethod(alwaysRun = true)
+  public void tearDown()
+  {
+    Desktop.instance.closeAll_actionPerformed(null);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindMousePosition_wrapped_annotations()
+  {
+    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", "true");
+    Cache.applicationProperties.setProperty("WRAP_ALIGNMENT", "true");
+    AlignFrame alignFrame = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    AlignViewportI av = alignFrame.getViewport();
+    av.setScaleAboveWrapped(false);
+    av.setScaleLeftWrapped(false);
+    av.setScaleRightWrapped(false);
+    alignFrame.alignPanel.paintAlignment(false, false);
+    waitForSwing(); // for Swing thread
+
+    final int charHeight = av.getCharHeight();
+    final int charWidth = av.getCharWidth();
+    final int alignmentHeight = av.getAlignment().getHeight();
+    
+    // sanity checks:
+    assertTrue(charHeight > 0);
+    assertTrue(charWidth > 0);
+    assertTrue(alignFrame.alignPanel.getSeqPanel().getWidth() > 0);
+  
+    SeqPanel testee = alignFrame.alignPanel.getSeqPanel();
+    int x = 0;
+    int y = 0;
+  
+    /*
+     * mouse at top left of wrapped panel; there is a gap of charHeight
+     * above the alignment
+     */
+    MouseEvent evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y,
+            0, 0, 0, false, 0);
+    MousePos pos = testee.findMousePosition(evt);
+    assertEquals(pos.column, 0);
+    assertEquals(pos.seqIndex, -1); // above sequences
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at bottom of gap above
+     */
+    y = charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor over top of first sequence
+     */
+    y = charHeight;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at bottom of first sequence
+     */
+    y = 2 * charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at top of second sequence
+     */
+    y = 2 * charHeight;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at bottom of second sequence
+     */
+    y = 3 * charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at bottom of last sequence
+     */
+    y = charHeight * (1 + alignmentHeight) - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor below sequences, in 3-pixel gap above annotations
+     * method reports index of nearest sequence above
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor still in the gap above annotations, now at the bottom of it
+     */
+    y += SeqCanvas.SEQS_ANNOTATION_GAP - 1; // 3-1 = 2
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at the top of the first annotation  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 0); // over first annotation
+
+    /*
+     * cursor at the bottom of the first annotation  
+     */
+    y += av.getAlignment().getAlignmentAnnotation()[0].height - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 0);
+
+    /*
+     * cursor at the top of the second annotation  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 1);
+
+    /*
+     * cursor at the bottom of the second annotation  
+     */
+    y += av.getAlignment().getAlignmentAnnotation()[1].height - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 1);
+
+    /*
+     * cursor at the top of the third annotation  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 2);
+
+    /*
+     * cursor at the bottom of the third annotation  
+     */
+    y += av.getAlignment().getAlignmentAnnotation()[2].height - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 2);
+
+    /*
+     * cursor in gap between wrapped widths  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at bottom of gap between wrapped widths  
+     */
+    y += charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at top of first sequence, second wrapped width  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.annotationIndex, -1);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindMousePosition_wrapped_scaleAbove()
+  {
+    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", "true");
+    Cache.applicationProperties.setProperty("WRAP_ALIGNMENT", "true");
+    AlignFrame alignFrame = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    AlignViewportI av = alignFrame.getViewport();
+    av.setScaleAboveWrapped(true);
+    av.setScaleLeftWrapped(false);
+    av.setScaleRightWrapped(false);
+    alignFrame.alignPanel.paintAlignment(false, false);
+    waitForSwing();
+
+    final int charHeight = av.getCharHeight();
+    final int charWidth = av.getCharWidth();
+    final int alignmentHeight = av.getAlignment().getHeight();
+    
+    // sanity checks:
+    assertTrue(charHeight > 0);
+    assertTrue(charWidth > 0);
+    assertTrue(alignFrame.alignPanel.getSeqPanel().getWidth() > 0);
+  
+    SeqPanel testee = alignFrame.alignPanel.getSeqPanel();
+    int x = 0;
+    int y = 0;
+  
+    /*
+     * mouse at top left of wrapped panel; there is a gap of charHeight
+     * above the alignment
+     */
+    MouseEvent evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y,
+            0, 0, 0, false, 0);
+    MousePos pos = testee.findMousePosition(evt);
+    assertEquals(pos.column, 0);
+    assertEquals(pos.seqIndex, -1); // above sequences
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor at bottom of gap above
+     * two charHeights including scale panel
+     */
+    y = 2 * charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor over top of first sequence
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor at bottom of first sequence
+     */
+    y += charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor at top of second sequence
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor at bottom of second sequence
+     */
+    y += charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor at bottom of last sequence
+     * (scale + gap + sequences)
+     */
+    y = charHeight * (2 + alignmentHeight) - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor below sequences, in 3-pixel gap above annotations
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor still in the gap above annotations, now at the bottom of it
+     * method reports index of nearest sequence above  
+     */
+    y += SeqCanvas.SEQS_ANNOTATION_GAP - 1; // 3-1 = 2
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor at the top of the first annotation  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 0); // over first annotation
+  
+    /*
+     * cursor at the bottom of the first annotation  
+     */
+    y += av.getAlignment().getAlignmentAnnotation()[0].height - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 0);
+  
+    /*
+     * cursor at the top of the second annotation  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 1);
+  
+    /*
+     * cursor at the bottom of the second annotation  
+     */
+    y += av.getAlignment().getAlignmentAnnotation()[1].height - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 1);
+  
+    /*
+     * cursor at the top of the third annotation  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 2);
+  
+    /*
+     * cursor at the bottom of the third annotation  
+     */
+    y += av.getAlignment().getAlignmentAnnotation()[2].height - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, 2);
+  
+    /*
+     * cursor in gap between wrapped widths  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor at bottom of gap between wrapped widths  
+     */
+    y += charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor at top of scale, second wrapped width  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at bottom of scale, second wrapped width  
+     */
+    y += charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at top of first sequence, second wrapped width  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.annotationIndex, -1);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindMousePosition_wrapped_noAnnotations()
+  {
+    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", "false");
+    Cache.applicationProperties.setProperty("WRAP_ALIGNMENT", "true");
+    AlignFrame alignFrame = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    AlignViewportI av = alignFrame.getViewport();
+    av.setScaleAboveWrapped(false);
+    av.setScaleLeftWrapped(false);
+    av.setScaleRightWrapped(false);
+    alignFrame.alignPanel.paintAlignment(false, false);
+    waitForSwing();
+
+    final int charHeight = av.getCharHeight();
+    final int charWidth = av.getCharWidth();
+    final int alignmentHeight = av.getAlignment().getHeight();
+    
+    // sanity checks:
+    assertTrue(charHeight > 0);
+    assertTrue(charWidth > 0);
+    assertTrue(alignFrame.alignPanel.getSeqPanel().getWidth() > 0);
+  
+    SeqPanel testee = alignFrame.alignPanel.getSeqPanel();
+    int x = 0;
+    int y = 0;
+  
+    /*
+     * mouse at top left of wrapped panel; there is a gap of charHeight
+     * above the alignment
+     */
+    MouseEvent evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y,
+            0, 0, 0, false, 0);
+    MousePos pos = testee.findMousePosition(evt);
+    assertEquals(pos.column, 0);
+    assertEquals(pos.seqIndex, -1); // above sequences
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor over top of first sequence
+     */
+    y = charHeight;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor at bottom of last sequence
+     */
+    y = charHeight * (1 + alignmentHeight) - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor below sequences, at top of charHeight gap between widths
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor below sequences, at top of charHeight gap between widths
+     */
+    y += charHeight - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.annotationIndex, -1);
+  
+    /*
+     * cursor at the top of the first sequence, second width  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, y, 0, 0, 0,
+            false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.annotationIndex, -1);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindColumn_unwrapped()
+  {
+    Cache.applicationProperties.setProperty("WRAP_ALIGNMENT", "false");
+    AlignFrame alignFrame = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    SeqPanel testee = alignFrame.alignPanel.getSeqPanel();
+    int x = 0;
+    final int charWidth = alignFrame.getViewport().getCharWidth();
+    assertTrue(charWidth > 0); // sanity check
+    assertEquals(alignFrame.getViewport().getRanges().getStartRes(), 0);
+
+    /*
+     * mouse at top left of unwrapped panel
+     */
+    MouseEvent evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0,
+            0, 0, 0, false, 0);
+    assertEquals(testee.findColumn(evt), 0);
+    
+    /*
+     * not quite one charWidth across
+     */
+    x = charWidth-1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0,
+            0, 0, 0, false, 0);
+    assertEquals(testee.findColumn(evt), 0);
+
+    /*
+     * one charWidth across
+     */
+    x = charWidth;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0, 0, 0, 0,
+            false, 0);
+    assertEquals(testee.findColumn(evt), 1);
+
+    /*
+     * two charWidths across
+     */
+    x = 2 * charWidth;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0, 0, 0, 0,
+            false, 0);
+    assertEquals(testee.findColumn(evt), 2);
+
+    /*
+     * limited to last column of seqcanvas
+     */
+    x = 20000;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0, 0, 0, 0,
+            false, 0);
+    SeqCanvas seqCanvas = alignFrame.alignPanel.getSeqPanel().seqCanvas;
+    int w = seqCanvas.getWidth();
+    // limited to number of whole columns, base 0
+    int expected = w / charWidth - 1;
+    assertEquals(testee.findColumn(evt), expected);
+
+    /*
+     * hide columns 5-10 (base 1)
+     */
+    alignFrame.getViewport().hideColumns(4, 9);
+    x = 5 * charWidth + 2;
+    // x is in 6th visible column, absolute column 12, or 11 base 0
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0, 0, 0, 0,
+            false, 0);
+    assertEquals(testee.findColumn(evt), 11);
+  }
+
+  @Test(groups = "Functional")
+  public void testFindColumn_wrapped()
+  {
+    Cache.applicationProperties.setProperty("WRAP_ALIGNMENT", "true");
+    AlignFrame alignFrame = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    AlignViewport av = alignFrame.getViewport();
+    av.setScaleAboveWrapped(false);
+    av.setScaleLeftWrapped(false);
+    av.setScaleRightWrapped(false);
+    alignFrame.alignPanel.paintAlignment(false, false);
+    // need to wait for repaint to finish!
+    waitForSwing();
+    SeqPanel testee = alignFrame.alignPanel.getSeqPanel();
+    int x = 0;
+    final int charWidth = av.getCharWidth();
+    assertTrue(charWidth > 0); // sanity check
+    assertEquals(av.getRanges().getStartRes(), 0);
+  
+    /*
+     * mouse at top left of wrapped panel, no West (left) scale
+     */
+    MouseEvent evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0,
+            0, 0, 0, false, 0);
+    assertEquals(testee.findColumn(evt), 0);
+    
+    /*
+     * not quite one charWidth across
+     */
+    x = charWidth-1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0,
+            0, 0, 0, false, 0);
+    assertEquals(testee.findColumn(evt), 0);
+  
+    /*
+     * one charWidth across
+     */
+    x = charWidth;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0, 0, 0, 0,
+            false, 0);
+    assertEquals(testee.findColumn(evt), 1);
+
+    /*
+     * x over scale left (before drawn columns) results in -1
+     */
+    av.setScaleLeftWrapped(true);
+    alignFrame.alignPanel.paintAlignment(false, false);
+    waitForSwing();
+    SeqCanvas seqCanvas = testee.seqCanvas;
+    int labelWidth = (int) PA.getValue(seqCanvas, "labelWidthWest");
+    assertTrue(labelWidth > 0);
+    x = labelWidth - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0, 0, 0, 0,
+            false, 0);
+    assertEquals(testee.findColumn(evt), -1);
+
+    x = labelWidth;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0, 0, 0, 0,
+            false, 0);
+    assertEquals(testee.findColumn(evt), 0);
+
+    /*
+     * x over right edge of last residue (including scale left)
+     */
+    int residuesWide = av.getRanges().getViewportWidth();
+    assertTrue(residuesWide > 0);
+    x = labelWidth + charWidth * residuesWide - 1;
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0, 0, 0, 0,
+            false, 0);
+    assertEquals(testee.findColumn(evt), residuesWide - 1);
+
+    /*
+     * x over scale right (beyond drawn columns) results in -1
+     */
+    av.setScaleRightWrapped(true);
+    alignFrame.alignPanel.paintAlignment(false, false);
+    waitForSwing();
+    labelWidth = (int) PA.getValue(seqCanvas, "labelWidthEast");
+    assertTrue(labelWidth > 0);
+    int residuesWide2 = av.getRanges().getViewportWidth();
+    assertTrue(residuesWide2 > 0);
+    assertTrue(residuesWide2 < residuesWide); // available width reduced
+    x += 1; // just over left edge of scale right
+    evt = new MouseEvent(testee, Event.MOUSE_MOVE, 0L, 0, x, 0, 0, 0, 0,
+            false, 0);
+    assertEquals(testee.findColumn(evt), -1);
+    
+    // todo add startRes offset, hidden columns
+
+  }
+  @BeforeClass(alwaysRun = true)
+  public static void setUpBeforeClass() throws Exception
+  {
+    /*
+     * use read-only test properties file
+     */
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    Jalview.main(new String[] { "-nonews" });
+  }
+
+  /**
+   * waits a few ms for Swing to do something
+   */
+  synchronized void waitForSwing()
+  {
+    try
+    {
+      super.wait(10);
+    } catch (InterruptedException e)
+    {
+      e.printStackTrace();
+    }
+  }
 }