Merge branch 'features/r2_11_2_alphafold/JAL-2349_JAL-3855' into develop
[jalview.git] / test / jalview / gui / SeqPanelTest.java
index a5d244d..f905ef2 100644 (file)
 package jalview.gui;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
 
+import java.awt.EventQueue;
+import java.awt.FontMetrics;
+import java.awt.event.MouseEvent;
+import java.lang.reflect.InvocationTargetException;
+
+import javax.swing.JLabel;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+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;
 import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
-
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
+import jalview.gui.SeqPanel.MousePos;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.util.MessageManager;
+import jalview.viewmodel.ViewportRanges;
+import junit.extensions.PA;
 
 public class SeqPanelTest
 {
@@ -40,6 +65,7 @@ public class SeqPanelTest
     JvOptionPane.setInteractiveMode(false);
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
+
   @Test(groups = "Functional")
   public void testSetStatusReturnsNearestResiduePosition()
   {
@@ -51,25 +77,23 @@ public class SeqPanelTest
     AlignmentI visAl = alignFrame.getViewport().getAlignment();
 
     // Test either side of gap
-    assertEquals(
-            alignFrame.alignPanel.getSeqPanel().setStatusMessage(
-                    visAl.getSequenceAt(1), 1, 1), 2);
-    assertEquals(alignFrame.statusBar.getText(),
+    assertEquals(alignFrame.alignPanel.getSeqPanel()
+            .setStatusMessage(visAl.getSequenceAt(1), 1, 1), 2);
+    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(alignFrame.alignPanel.getSeqPanel()
+            .setStatusMessage(visAl.getSequenceAt(1), 4, 1), 3);
+    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(
-            alignFrame.alignPanel.getSeqPanel().setStatusMessage(
-                    visAl.getSequenceAt(1), 3, 1), 3);
-    assertEquals(alignFrame.statusBar.getText(), "Sequence 2 ID: Seq2");
+    assertEquals(alignFrame.alignPanel.getSeqPanel()
+            .setStatusMessage(visAl.getSequenceAt(1), 2, 1), 3);
+    assertEquals(((JLabel) PA.getValue(alignFrame, "statusBar")).getText(),
+            "Sequence 2 ID: Seq2");
+    assertEquals(alignFrame.alignPanel.getSeqPanel()
+            .setStatusMessage(visAl.getSequenceAt(1), 3, 1), 3);
+    assertEquals(((JLabel) PA.getValue(alignFrame, "statusBar")).getText(),
+            "Sequence 2 ID: Seq2");
   }
 
   @Test(groups = "Functional")
@@ -82,10 +106,955 @@ public class SeqPanelTest
             al.getHeight());
     AlignmentI visAl = alignFrame.getViewport().getAlignment();
 
-    assertEquals(
-            alignFrame.alignPanel.getSeqPanel().setStatusMessage(
-                    visAl.getSequenceAt(1), 1, 1), 2);
-    assertEquals(alignFrame.statusBar.getText(),
+    assertEquals(alignFrame.alignPanel.getSeqPanel()
+            .setStatusMessage(visAl.getSequenceAt(1), 1, 1), 2);
+    assertEquals(((JLabel) PA.getValue(alignFrame, "statusBar")).getText(),
             "Sequence 2 ID: Seq2 Residue: B (2)");
   }
+
+  @Test(groups = "Functional")
+  public void testGetEditStatusMessage()
+  {
+    assertNull(SeqPanel.getEditStatusMessage(null));
+
+    EditCommand edit = new EditCommand(); // empty
+    assertNull(SeqPanel.getEditStatusMessage(edit));
+
+    SequenceI[] seqs = new SequenceI[] { new Sequence("a", "b") };
+
+    // 1 gap
+    edit.addEdit(edit.new Edit(Action.INSERT_GAP, seqs, 1, 1, '-'));
+    String expected = MessageManager.formatMessage("label.insert_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 3 more gaps makes +4
+    edit.addEdit(edit.new Edit(Action.INSERT_GAP, seqs, 1, 3, '-'));
+    expected = MessageManager.formatMessage("label.insert_gaps", "4");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 2 deletes makes + 2
+    edit.addEdit(edit.new Edit(Action.DELETE_GAP, seqs, 1, 2, '-'));
+    expected = MessageManager.formatMessage("label.insert_gaps", "2");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 2 more deletes makes 0 - no text
+    edit.addEdit(edit.new Edit(Action.DELETE_GAP, seqs, 1, 2, '-'));
+    assertNull(SeqPanel.getEditStatusMessage(edit));
+
+    // 1 more delete makes 1 delete
+    edit.addEdit(edit.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-'));
+    expected = MessageManager.formatMessage("label.delete_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 1 more delete makes 2 deletes
+    edit.addEdit(edit.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-'));
+    expected = MessageManager.formatMessage("label.delete_gaps", "2");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+  }
+
+  /**
+   * Tests that simulate 'locked editing', where an inserted gap is balanced by
+   * a gap deletion in the selection group, and vice versa
+   */
+  @Test(groups = "Functional")
+  public void testGetEditStatusMessage_lockedEditing()
+  {
+    EditCommand edit = new EditCommand(); // empty
+    SequenceI[] seqs = new SequenceI[] { new Sequence("a", "b") };
+
+    // 1 gap inserted, balanced by 1 delete
+    Edit e1 = edit.new Edit(Action.INSERT_GAP, seqs, 1, 1, '-');
+    edit.addEdit(e1);
+    Edit e2 = edit.new Edit(Action.DELETE_GAP, seqs, 5, 1, '-');
+    e2.setSystemGenerated(true);
+    edit.addEdit(e2);
+    String expected = MessageManager.formatMessage("label.insert_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 2 more gaps makes +3
+    Edit e3 = edit.new Edit(Action.INSERT_GAP, seqs, 1, 2, '-');
+    edit.addEdit(e3);
+    Edit e4 = edit.new Edit(Action.DELETE_GAP, seqs, 5, 2, '-');
+    e4.setSystemGenerated(true);
+    edit.addEdit(e4);
+    expected = MessageManager.formatMessage("label.insert_gaps", "3");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 2 deletes makes + 1
+    Edit e5 = edit.new Edit(Action.DELETE_GAP, seqs, 1, 2, '-');
+    edit.addEdit(e5);
+    Edit e6 = edit.new Edit(Action.INSERT_GAP, seqs, 5, 2, '-');
+    e6.setSystemGenerated(true);
+    edit.addEdit(e6);
+    expected = MessageManager.formatMessage("label.insert_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 1 more delete makes 0 - no text
+    Edit e7 = edit.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-');
+    edit.addEdit(e7);
+    Edit e8 = edit.new Edit(Action.INSERT_GAP, seqs, 5, 1, '-');
+    e8.setSystemGenerated(true);
+    edit.addEdit(e8);
+    expected = MessageManager.formatMessage("label.insert_gaps", "2");
+    assertNull(SeqPanel.getEditStatusMessage(edit));
+
+    // 1 more delete makes 1 delete
+    Edit e9 = edit.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-');
+    edit.addEdit(e9);
+    Edit e10 = edit.new Edit(Action.INSERT_GAP, seqs, 5, 1, '-');
+    e10.setSystemGenerated(true);
+    edit.addEdit(e10);
+    expected = MessageManager.formatMessage("label.delete_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 2 more deletes makes 3 deletes
+    Edit e11 = edit.new Edit(Action.DELETE_GAP, seqs, 1, 2, '-');
+    edit.addEdit(e11);
+    Edit e12 = edit.new Edit(Action.INSERT_GAP, seqs, 5, 2, '-');
+    e12.setSystemGenerated(true);
+    edit.addEdit(e12);
+    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, MouseEvent.MOUSE_MOVED, 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.updateLayout();
+
+    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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, -1);
+
+    AlignmentAnnotation[] annotationRows = av.getAlignment()
+            .getAlignmentAnnotation();
+    for (int n = 0; n < annotationRows.length; n++)
+    {
+      /*
+       * cursor at the top of the n'th annotation  
+       */
+      y += 1;
+      evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0,
+              0, 0, false, 0);
+      pos = testee.findMousePosition(evt);
+      assertEquals(pos.seqIndex, alignmentHeight - 1);
+      assertEquals(pos.annotationIndex, n); // over n'th annotation
+
+      /*
+       * cursor at the bottom of the n'th annotation  
+       */
+      y += annotationRows[n].height - 1;
+      evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0,
+              0, 0, false, 0);
+      pos = testee.findMousePosition(evt);
+      assertEquals(pos.seqIndex, alignmentHeight - 1);
+      assertEquals(pos.annotationIndex, n);
+    }
+
+    /*
+     * cursor in gap between wrapped widths  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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.updateLayout();
+
+    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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, alignmentHeight - 1);
+    assertEquals(pos.annotationIndex, -1);
+
+    AlignmentAnnotation[] annotationRows = av.getAlignment()
+            .getAlignmentAnnotation();
+    for (int n = 0; n < annotationRows.length; n++)
+    {
+      /*
+       * cursor at the top of the n'th annotation  
+       */
+      y += 1;
+      evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0,
+              0, 0, false, 0);
+      pos = testee.findMousePosition(evt);
+      assertEquals(pos.seqIndex, alignmentHeight - 1);
+      assertEquals(pos.annotationIndex, n); // over n'th annotation
+
+      /*
+       * cursor at the bottom of the n'th annotation  
+       */
+      y += annotationRows[n].height - 1;
+      evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0,
+              0, 0, false, 0);
+      pos = testee.findMousePosition(evt);
+      SeqCanvas sc = testee.seqCanvas;
+      assertEquals(pos.seqIndex, alignmentHeight - 1,
+              String.format("%s n=%d y=%d %d, %d, %d, %d",
+                      annotationRows[n].label, n, y, sc.getWidth(),
+                      sc.getHeight(), sc.wrappedRepeatHeightPx,
+                      sc.wrappedSpaceAboveAlignment));
+      assertEquals(pos.annotationIndex, n);
+    }
+
+    /*
+     * cursor in gap between wrapped widths  
+     */
+    y += 1;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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");
+    Cache.applicationProperties.setProperty("FONT_SIZE", "10");
+    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.updateLayout();
+
+    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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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
+    ViewportRanges ranges = alignFrame.getViewport().getRanges();
+    assertEquals(ranges.getStartRes(), 0);
+
+    /*
+     * mouse at top left of unwrapped panel
+     */
+    MouseEvent evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 0L, 0, x, 0, 0, 0,
+            0, false, 0);
+    assertEquals(testee.findColumn(evt), 0);
+
+    /*
+     * one charWidth across
+     */
+    x = charWidth;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, 0, 0, 0,
+            0, false, 0);
+    assertEquals(testee.findColumn(evt), 1);
+
+    /*
+     * two charWidths across
+     */
+    x = 2 * charWidth;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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,
+    // and to end of visible range
+    int expected = w / charWidth;
+    expected = Math.min(expected, ranges.getEndRes());
+    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, MouseEvent.MOUSE_MOVED, 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.updateLayout();
+    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, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 0L, 0, x, 0, 0, 0,
+            0, false, 0);
+    assertEquals(testee.findColumn(evt), 0);
+
+    /*
+     * one charWidth across
+     */
+    x = charWidth;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 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.updateLayout();
+    SeqCanvas seqCanvas = testee.seqCanvas;
+    int labelWidth = (int) PA.getValue(seqCanvas, "labelWidthWest");
+    assertTrue(labelWidth > 0);
+    x = labelWidth - 1;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, 0, 0, 0,
+            0, false, 0);
+    assertEquals(testee.findColumn(evt), -1);
+
+    x = labelWidth;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 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, MouseEvent.MOUSE_MOVED, 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.updateLayout();
+    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, MouseEvent.MOUSE_MOVED, 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 for Swing event dispatch queue to empty
+   */
+  synchronized void waitForSwing()
+  {
+    try
+    {
+      EventQueue.invokeAndWait(new Runnable()
+      {
+        @Override
+        public void run()
+        {
+        }
+      });
+    } catch (InterruptedException | InvocationTargetException e)
+    {
+      e.printStackTrace();
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testFindMousePosition_wrapped_scales_longSequence()
+  {
+    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS", "false");
+    Cache.applicationProperties.setProperty("WRAP_ALIGNMENT", "true");
+    Cache.applicationProperties.setProperty("FONT_SIZE", "14");
+    Cache.applicationProperties.setProperty("FONT_NAME", "SansSerif");
+    Cache.applicationProperties.setProperty("FONT_STYLE", "0");
+    // sequence of 50 bases, doubled 10 times, = 51200 bases
+    String dna = "ATGGCCATTGGGCCCAAATTTCCCAAAGGGTTTCCCTGAGGTCAGTCAGA";
+    for (int i = 0; i < 10; i++)
+    {
+      dna += dna;
+    }
+    assertEquals(dna.length(), 51200);
+    AlignFrame alignFrame = new FileLoader()
+            .LoadFileWaitTillLoaded("dna "+dna, DataSourceType.PASTE);
+    SeqPanel testee = alignFrame.alignPanel.getSeqPanel();
+    AlignViewport av = alignFrame.getViewport();
+    av.setScaleAboveWrapped(true);
+    av.setScaleLeftWrapped(true);
+    av.setScaleRightWrapped(true);
+    alignFrame.alignPanel.updateLayout();
+
+    try
+    {
+      Thread.sleep(200);
+    } catch (InterruptedException e)
+    {
+    }
+
+    final int charHeight = av.getCharHeight();
+    final int charWidth = av.getCharWidth();
+    assertEquals(charHeight, 17);
+    assertEquals(charWidth, 12);
+
+    FontMetrics fm = testee.getFontMetrics(av.getFont());
+    int labelWidth = fm.stringWidth("00000") + charWidth;
+    assertEquals(labelWidth, 57); // 5 x 9 + charWidth
+    assertEquals(testee.seqCanvas.getLabelWidthWest(), labelWidth);
+
+    int x = 0;
+    int y = 0;
+
+    /*
+     * mouse at top left of wrapped panel; there is a gap of 2 * charHeight
+     * above the alignment
+     */
+    MouseEvent evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0,
+            x, y, 0, 0, 0, false, 0);
+    MousePos pos = testee.findMousePosition(evt);
+    assertEquals(pos.column, -1); // over scale left, not an alignment column
+    assertEquals(pos.seqIndex, -1); // above sequences
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor over scale above first sequence
+     */
+    y += charHeight;
+    x = labelWidth;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.column, 0);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor over scale left of first sequence
+     */
+    y += charHeight;
+    x = 0;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.column, -1);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * cursor over start of first sequence
+     */
+    x = labelWidth;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.column, 0);
+    assertEquals(pos.annotationIndex, -1);
+
+    /*
+     * move one character right, to bottom pixel of same row
+     */
+    x += charWidth;
+    y += charHeight - 1;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.column, 1);
+
+    /*
+     * move down one pixel - now in the no man's land between rows
+     */
+    y += 1;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.column, 1);
+
+    /*
+     * move down two char heights less one pixel - still in the no man's land
+     * (scale above + spacer line)
+     */
+    y += (2 * charHeight - 1);
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, -1);
+    assertEquals(pos.column, 1);
+
+    /*
+     * move down one more pixel - now on the next row of the sequence
+     */
+    y += 1;
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    assertEquals(pos.column, 1 + av.getWrappedWidth());
+
+    /*
+     * scroll to near the end of the sequence
+     */
+    SearchResultsI sr = new SearchResults();
+    int scrollTo = dna.length() - 1000;
+    sr.addResult(av.getAlignment().getSequenceAt(0), scrollTo, scrollTo);
+    alignFrame.alignPanel.scrollToPosition(sr);
+
+    /*
+     * place the mouse on the first column of the 6th sequence, and
+     * verify that (computed) findMousePosition matches (actual) ViewportRanges
+     */
+    x = labelWidth;
+    y = 17 * charHeight; // 17 = 6 times two header rows and 5 sequence rows
+    evt = new MouseEvent(testee, MouseEvent.MOUSE_MOVED, 0L, 0, x, y, 0, 0,
+            0, false, 0);
+    pos = testee.findMousePosition(evt);
+    assertEquals(pos.seqIndex, 0);
+    int expected = av.getRanges().getStartRes() + 5 * av.getWrappedWidth();
+    assertEquals(pos.column, expected);
+  }
 }