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

1  2 
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AnnotationPanel.java
src/jalview/gui/IdPanel.java
src/jalview/gui/SeqPanel.java
src/jalview/io/FileLoader.java
test/jalview/gui/SeqPanelTest.java

Simple merge
Simple merge
Simple merge
@@@ -339,13 -379,9 +382,10 @@@ public class IdPanel extends JPane
      {
        if (sf.links != null)
        {
-         for (String link : sf.links)
-         {
-           nlinks.add(link);
-         }
+         nlinks.addAll(sf.links);
        }
      }
 +    }
  
      PopupMenu pop = new PopupMenu(alignPanel, sq, features,
              Preferences.getGroupURLLinks());
@@@ -83,18 -160,13 +160,13 @@@ public class SeqPanel extends JPane
    public AlignmentPanel ap;
  
    /*
-    * last column position for mouseMoved event
-    */
-   private int lastMouseColumn;
-   /*
-    * last sequence offset for mouseMoved event
+    * last position for mouseMoved event
     */
-   private int lastMouseSeq;
+   private MousePos lastMousePosition;
  
 -  protected int lastres;
 +  protected int editLastRes;
  
 -  protected int startseq;
 +  protected int editStartSeq;
  
    protected AlignViewport av;
  
        }
      }
  
 -    StringBuilder message = new StringBuilder(64);
++    StringBuilder message = new StringBuilder(64); // for status bar
++
 +    /*
 +     * make a name for the edit action, for
 +     * status bar message and Undo/Redo menu
 +     */
 +    String label = null;
      if (groupEditing)
      {
 -      message.append("Edit group:");
 -      if (editCommand == null)
 -      {
 -        editCommand = new EditCommand(
 -                MessageManager.getString("action.edit_group"));
 -      }
++        message.append("Edit group:");
 +      label = MessageManager.getString("action.edit_group");
      }
      else
      {
 -      message.append("Edit sequence: " + seq.getName());
 -      String label = seq.getName();
++        message.append("Edit sequence: " + seq.getName());
 +      label = seq.getName();
        if (label.length() > 10)
        {
          label = label.substring(0, 10);
        }
 -      if (editCommand == null)
 -      {
 -        editCommand = new EditCommand(MessageManager
 -                .formatMessage("label.edit_params", new String[]
 -                { label }));
 -      }
 +      label = MessageManager.formatMessage("label.edit_params",
 +              new String[]
 +              { label });
 +    }
 +
 +    /*
 +     * initialise the edit command if there is not
 +     * already one being extended
 +     */
 +    if (editCommand == null)
 +    {
 +      editCommand = new EditCommand(label);
      }
  
+     if (insertGap)
+     {
+       message.append(" insert ");
+     }
+     else
+     {
+       message.append(" delete ");
+     }
 -    message.append(Math.abs(startres - lastres) + " gaps.");
++    message.append(Math.abs(startres - editLastRes) + " gaps.");
+     ap.alignFrame.setStatus(message.toString());
 -    // Are we editing within a selection group?
 -    if (groupEditing || (sg != null
 -            && sg.getSequences(av.getHiddenRepSequences()).contains(seq)))
 +    /*
 +     * is there a selection group containing the sequence being edited?
 +     * if so the boundary of the group is the limit of the edit
 +     * (but the edit may be inside or outside the selection group)
 +     */
 +    boolean inSelectionGroup = sg != null
 +            && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
 +    if (groupEditing || inSelectionGroup)
      {
        fixedColumns = true;
  
        }
      }
  
 +    boolean success = doEditSequence(insertGap, editSeq, startres,
 +            fixedRight, fixedColumns, sg);
 +
 +    /*
 +     * report what actually happened (might be less than
 +     * 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();
 +    }
 +
 +    editLastRes = startres;
 +    seqCanvas.repaint();
 +  }
 +
 +  /**
 +   * A helper method that performs the requested editing to insert or delete
 +   * gaps (if possible). Answers true if the edit was successful, false if could
 +   * only be performed in part or not at all. Failure may occur in 'locked edit'
 +   * mode, when an insertion requires a matching gapped position (or column) to
 +   * delete, and deletion requires an adjacent gapped position (or column) to
 +   * remove.
 +   * 
 +   * @param insertGap
 +   *          true if inserting gap(s), false if deleting
 +   * @param editSeq
 +   *          (unused parameter, currently always false)
 +   * @param startres
 +   *          the column at which to perform the edit
 +   * @param fixedRight
 +   *          fixed right boundary column of a locked edit (within or to the
 +   *          left of a selection group)
 +   * @param fixedColumns
 +   *          true if this is a locked edit
 +   * @param sg
 +   *          the sequence group (if group edit is being performed)
 +   * @return
 +   */
 +  protected boolean doEditSequence(final boolean insertGap,
 +          final boolean editSeq, final int startres, int fixedRight,
 +          final boolean fixedColumns, final SequenceGroup sg)
 +  {
 +    final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
 +    SequenceI[] seqs = new SequenceI[] { seq };
 +
      if (groupEditing)
      {
        List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
Simple merge
  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;
  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;
  
@@@ -95,107 -107,750 +112,854 @@@ public class SeqPanelTes
    }
  
    @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, 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();
+     }
+   }
  }