JAL-1244 comments, javadoc, method signature tweaks
[jalview.git] / src / jalview / gui / SeqPanel.java
index dbbf510..87e655b 100644 (file)
@@ -59,7 +59,6 @@ import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.awt.event.MouseWheelEvent;
 import java.awt.event.MouseWheelListener;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -76,12 +75,11 @@ import javax.swing.ToolTipManager;
 public class SeqPanel extends JPanel
         implements MouseListener, MouseMotionListener, MouseWheelListener,
         SequenceListener, SelectionListener
-
 {
-  /** DOCUMENT ME!! */
+  private static final int MAX_TOOLTIP_LENGTH = 300;
+
   public SeqCanvas seqCanvas;
 
-  /** DOCUMENT ME!! */
   public AlignmentPanel ap;
 
   /*
@@ -94,9 +92,9 @@ public class SeqPanel extends JPanel
    */
   private int lastMouseSeq;
 
-  protected int lastres;
+  protected int editLastRes;
 
-  protected int startseq;
+  protected int editStartSeq;
 
   protected AlignViewport av;
 
@@ -148,35 +146,33 @@ public class SeqPanel extends JPanel
   SearchResultsI lastSearchResults;
 
   /**
-   * Creates a new SeqPanel object.
+   * Creates a new SeqPanel object
    * 
-   * @param avp
-   *          DOCUMENT ME!
-   * @param p
-   *          DOCUMENT ME!
+   * @param viewport
+   * @param alignPanel
    */
-  public SeqPanel(AlignViewport av, AlignmentPanel ap)
+  public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
   {
     linkImageURL = getClass().getResource("/images/link.gif");
     seqARep = new SequenceAnnotationReport(linkImageURL.toString());
     ToolTipManager.sharedInstance().registerComponent(this);
     ToolTipManager.sharedInstance().setInitialDelay(0);
     ToolTipManager.sharedInstance().setDismissDelay(10000);
-    this.av = av;
+    this.av = viewport;
     setBackground(Color.white);
 
-    seqCanvas = new SeqCanvas(ap);
+    seqCanvas = new SeqCanvas(alignPanel);
     setLayout(new BorderLayout());
     add(seqCanvas, BorderLayout.CENTER);
 
-    this.ap = ap;
+    this.ap = alignPanel;
 
-    if (!av.isDataset())
+    if (!viewport.isDataset())
     {
       addMouseMotionListener(this);
       addMouseListener(this);
       addMouseWheelListener(this);
-      ssm = av.getStructureSelectionManager();
+      ssm = viewport.getStructureSelectionManager();
       ssm.addStructureViewerListener(this);
       ssm.addSelectionListener(this);
     }
@@ -250,7 +246,7 @@ public class SeqPanel extends JPanel
     if (av.hasHiddenColumns())
     {
       res = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(res);
+              .visibleToAbsoluteColumn(res);
     }
 
     return res;
@@ -307,8 +303,8 @@ public class SeqPanel extends JPanel
       /*
        * Tidy up come what may...
        */
-      startseq = -1;
-      lastres = -1;
+      editStartSeq = -1;
+      editLastRes = -1;
       editingSeqs = false;
       groupEditing = false;
       keyboardNo1 = null;
@@ -320,13 +316,13 @@ public class SeqPanel extends JPanel
   void setCursorRow()
   {
     seqCanvas.cursorY = getKeyboardNo1() - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void setCursorColumn()
   {
     seqCanvas.cursorX = getKeyboardNo1() - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void setCursorRowAndColumn()
@@ -339,7 +335,7 @@ public class SeqPanel extends JPanel
     {
       seqCanvas.cursorX = getKeyboardNo1() - 1;
       seqCanvas.cursorY = getKeyboardNo2() - 1;
-      scrollToVisible();
+      scrollToVisible(true);
     }
   }
 
@@ -348,7 +344,7 @@ public class SeqPanel extends JPanel
     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
 
     seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void moveCursor(int dx, int dy)
@@ -363,13 +359,25 @@ public class SeqPanel extends JPanel
       int original = seqCanvas.cursorX - dx;
       int maxWidth = av.getAlignment().getWidth();
 
-      // TODO: once JAL-2759 is ready, change this loop to something more
-      // efficient
-      while (!hidden.isVisible(seqCanvas.cursorX)
-              && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0
-              && dx != 0)
+      if (!hidden.isVisible(seqCanvas.cursorX))
       {
-        seqCanvas.cursorX += dx;
+        int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
+        int[] region = hidden.getRegionWithEdgeAtRes(visx);
+
+        if (region != null) // just in case
+        {
+          if (dx == 1)
+          {
+            // moving right
+            seqCanvas.cursorX = region[1] + 1;
+          }
+          else if (dx == -1)
+          {
+            // moving left
+            seqCanvas.cursorX = region[0] - 1;
+          }
+        }
+        seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
       }
 
       if (seqCanvas.cursorX >= maxWidth
@@ -379,10 +387,16 @@ public class SeqPanel extends JPanel
       }
     }
 
-    scrollToVisible();
+    scrollToVisible(false);
   }
 
-  void scrollToVisible()
+  /**
+   * Scroll to make the cursor visible in the viewport.
+   * 
+   * @param jump
+   *          just jump to the location rather than scrolling
+   */
+  void scrollToVisible(boolean jump)
   {
     if (seqCanvas.cursorX < 0)
     {
@@ -403,20 +417,44 @@ public class SeqPanel extends JPanel
     }
 
     endEditing();
-    if (av.getWrapAlignment())
+
+    boolean repaintNeeded = true;
+    if (jump)
     {
-      av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
+      // only need to repaint if the viewport did not move, as otherwise it will
+      // get a repaint
+      repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
+              seqCanvas.cursorY);
     }
     else
     {
-      av.getRanges().scrollToVisible(seqCanvas.cursorX, seqCanvas.cursorY);
+      if (av.getWrapAlignment())
+      {
+        // scrollToWrappedVisible expects x-value to have hidden cols subtracted
+        int x = av.getAlignment().getHiddenColumns()
+                .absoluteToVisibleColumn(seqCanvas.cursorX);
+        av.getRanges().scrollToWrappedVisible(x);
+      }
+      else
+      {
+        av.getRanges().scrollToVisible(seqCanvas.cursorX,
+                seqCanvas.cursorY);
+      }
     }
-    setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
+
+    if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
+    {
+      setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
             seqCanvas.cursorX, seqCanvas.cursorY);
+    }
 
-    seqCanvas.repaint();
+    if (repaintNeeded)
+    {
+      seqCanvas.repaint();
+    }
   }
 
+
   void setSelectionAreaAtCursor(boolean topLeft)
   {
     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
@@ -494,8 +532,8 @@ public class SeqPanel extends JPanel
   void insertGapAtCursor(boolean group)
   {
     groupEditing = group;
-    startseq = seqCanvas.cursorY;
-    lastres = seqCanvas.cursorX;
+    editStartSeq = seqCanvas.cursorY;
+    editLastRes = seqCanvas.cursorX;
     editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
     endEditing();
   }
@@ -503,8 +541,8 @@ public class SeqPanel extends JPanel
   void deleteGapAtCursor(boolean group)
   {
     groupEditing = group;
-    startseq = seqCanvas.cursorY;
-    lastres = seqCanvas.cursorX + getKeyboardNo1();
+    editStartSeq = seqCanvas.cursorY;
+    editLastRes = seqCanvas.cursorX + getKeyboardNo1();
     editSequence(false, false, seqCanvas.cursorX);
     endEditing();
   }
@@ -513,8 +551,8 @@ public class SeqPanel extends JPanel
   {
     // TODO not called - delete?
     groupEditing = group;
-    startseq = seqCanvas.cursorY;
-    lastres = seqCanvas.cursorX;
+    editStartSeq = seqCanvas.cursorY;
+    editLastRes = seqCanvas.cursorX;
     editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
     endEditing();
   }
@@ -642,13 +680,13 @@ public class SeqPanel extends JPanel
     if ((seq < av.getAlignment().getHeight())
             && (res < av.getAlignment().getSequenceAt(seq).getLength()))
     {
-      startseq = seq;
-      lastres = res;
+      editStartSeq = seq;
+      editLastRes = res;
     }
     else
     {
-      startseq = -1;
-      lastres = -1;
+      editStartSeq = -1;
+      editLastRes = -1;
     }
 
     return;
@@ -808,7 +846,7 @@ public class SeqPanel extends JPanel
       List<SequenceFeature> features = ap.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
       seqARep.appendFeatures(tooltipText, pos, features,
-              this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
+              this.ap.getSeqPanel().seqCanvas.fr);
     }
     if (tooltipText.length() == 6) // <html>
     {
@@ -817,6 +855,11 @@ public class SeqPanel extends JPanel
     }
     else
     {
+      if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
+      {
+        tooltipText.setLength(MAX_TOOLTIP_LENGTH);
+        tooltipText.append("...");
+      }
       String textString = tooltipText.toString();
       if (lastTooltip == null || !lastTooltip.equals(textString))
       {
@@ -1102,12 +1145,12 @@ public class SeqPanel extends JPanel
       res = 0;
     }
 
-    if ((lastres == -1) || (lastres == res))
+    if ((editLastRes == -1) || (editLastRes == res))
     {
       return;
     }
 
-    if ((res < av.getAlignment().getWidth()) && (res < lastres))
+    if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
     {
       // dragLeft, delete gap
       editSequence(false, false, res);
@@ -1124,16 +1167,40 @@ public class SeqPanel extends JPanel
     }
   }
 
-  // TODO: Make it more clever than many booleans
+  /**
+   * Edits the sequence to insert or delete one or more gaps, in response to a
+   * mouse drag or cursor mode command. The number of inserts/deletes may be
+   * specified with the cursor command, or else depends on the mouse event
+   * (normally one column, but potentially more for a fast mouse drag).
+   * <p>
+   * Delete gaps is limited to the number of gaps left of the cursor position
+   * (mouse drag), or at or right of the cursor position (cursor mode).
+   * <p>
+   * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
+   * the current selection group.
+   * <p>
+   * In locked editing mode (with a selection group present), inserts/deletions
+   * within the selection group are limited to its boundaries (and edits outside
+   * the group stop at its border).
+   * 
+   * @param insertGap
+   *          true to insert gaps, false to delete gaps
+   * @param editSeq
+   *          (unused parameter)
+   * @param startres
+   *          the column at which to perform the action; the number of columns
+   *          affected depends on <code>this.editLastRes</code> (cursor column
+   *          position)
+   */
   synchronized void editSequence(boolean insertGap, boolean editSeq,
-          int startres)
+          final int startres)
   {
     int fixedLeft = -1;
     int fixedRight = -1;
     boolean fixedColumns = false;
     SequenceGroup sg = av.getSelectionGroup();
 
-    SequenceI seq = av.getAlignment().getSequenceAt(startseq);
+    final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
 
     // No group, but the sequence may represent a group
     if (!groupEditing && av.hasHiddenRows())
@@ -1145,47 +1212,44 @@ public class SeqPanel extends JPanel
       }
     }
 
-    StringBuilder message = new StringBuilder(64);
+    /*
+     * 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"));
-      }
+      label = MessageManager.getString("action.edit_group");
     }
     else
     {
-      message.append("Edit sequence: " + seq.getName());
-      String label = 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 });
     }
 
-    if (insertGap)
-    {
-      message.append(" insert ");
-    }
-    else
+    /*
+     * initialise the edit command if there is not
+     * already one being extended
+     */
+    if (editCommand == null)
     {
-      message.append(" delete ");
+      editCommand = new EditCommand(label);
     }
 
-    message.append(Math.abs(startres - lastres) + " gaps.");
-    ap.alignFrame.statusBar.setText(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;
 
@@ -1204,10 +1268,10 @@ public class SeqPanel extends JPanel
       fixedLeft = sg.getStartRes();
       fixedRight = sg.getEndRes();
 
-      if ((startres < fixedLeft && lastres >= fixedLeft)
-              || (startres >= fixedLeft && lastres < fixedLeft)
-              || (startres > fixedRight && lastres <= fixedRight)
-              || (startres <= fixedRight && lastres > fixedRight))
+      if ((startres < fixedLeft && editLastRes >= fixedLeft)
+              || (startres >= fixedLeft && editLastRes < fixedLeft)
+              || (startres > fixedRight && editLastRes <= fixedRight)
+              || (startres <= fixedRight && editLastRes > fixedRight))
       {
         endEditing();
         return;
@@ -1229,12 +1293,12 @@ public class SeqPanel extends JPanel
     {
       fixedColumns = true;
       int y1 = av.getAlignment().getHiddenColumns()
-              .getHiddenBoundaryLeft(startres);
+              .getNextHiddenBoundary(true, startres);
       int y2 = av.getAlignment().getHiddenColumns()
-              .getHiddenBoundaryRight(startres);
+              .getNextHiddenBoundary(false, startres);
 
-      if ((insertGap && startres > y1 && lastres < y1)
-              || (!insertGap && startres < y2 && lastres > y2))
+      if ((insertGap && startres > y1 && editLastRes < y1)
+              || (!insertGap && startres < y2 && editLastRes > y2))
       {
         endEditing();
         return;
@@ -1255,6 +1319,54 @@ public class SeqPanel extends JPanel
       }
     }
 
+    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);
+    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());
@@ -1273,7 +1385,8 @@ public class SeqPanel extends JPanel
         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
         {
-          sg.setEndRes(av.getAlignment().getWidth() + startres - lastres);
+          sg.setEndRes(
+                  av.getAlignment().getWidth() + startres - editLastRes);
           fixedRight = sg.getEndRes();
         }
 
@@ -1281,15 +1394,16 @@ public class SeqPanel extends JPanel
         // Find the next gap before the end
         // of the visible region boundary
         boolean blank = false;
-        for (; fixedRight > lastres; fixedRight--)
+        for (; fixedRight > editLastRes; fixedRight--)
         {
           blank = true;
 
           for (g = 0; g < groupSize; g++)
           {
-            for (int j = 0; j < startres - lastres; j++)
+            for (int j = 0; j < startres - editLastRes; j++)
             {
-              if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
+              if (!Comparison
+                      .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
               {
                 blank = false;
                 break;
@@ -1306,11 +1420,11 @@ public class SeqPanel extends JPanel
         {
           if (sg.getSize() == av.getAlignment().getHeight())
           {
-            if ((av.hasHiddenColumns() && startres < av.getAlignment()
-                    .getHiddenColumns().getHiddenBoundaryRight(startres)))
+            if ((av.hasHiddenColumns()
+                    && startres < av.getAlignment().getHiddenColumns()
+                            .getNextHiddenBoundary(false, startres)))
             {
-              endEditing();
-              return;
+              return false;
             }
 
             int alWidth = av.getAlignment().getWidth();
@@ -1325,13 +1439,12 @@ public class SeqPanel extends JPanel
             }
             // We can still insert gaps if the selectionGroup
             // contains all the sequences
-            sg.setEndRes(sg.getEndRes() + startres - lastres);
-            fixedRight = alWidth + startres - lastres;
+            sg.setEndRes(sg.getEndRes() + startres - editLastRes);
+            fixedRight = alWidth + startres - editLastRes;
           }
           else
           {
-            endEditing();
-            return;
+            return false;
           }
         }
       }
@@ -1344,7 +1457,7 @@ public class SeqPanel extends JPanel
 
         for (g = 0; g < groupSize; g++)
         {
-          for (int j = startres; j < lastres; j++)
+          for (int j = startres; j < editLastRes; j++)
           {
             if (groupSeqs[g].getLength() <= j)
             {
@@ -1354,8 +1467,7 @@ public class SeqPanel extends JPanel
             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
             {
               // Not a gap, block edit not valid
-              endEditing();
-              return;
+              return false;
             }
           }
         }
@@ -1366,15 +1478,15 @@ public class SeqPanel extends JPanel
         // dragging to the right
         if (fixedColumns && fixedRight != -1)
         {
-          for (int j = lastres; j < startres; j++)
+          for (int j = editLastRes; j < startres; j++)
           {
-            insertChar(j, groupSeqs, fixedRight);
+            insertGap(j, groupSeqs, fixedRight);
           }
         }
         else
         {
           appendEdit(Action.INSERT_GAP, groupSeqs, startres,
-                  startres - lastres);
+                  startres - editLastRes, false);
         }
       }
       else
@@ -1382,7 +1494,7 @@ public class SeqPanel extends JPanel
         // dragging to the left
         if (fixedColumns && fixedRight != -1)
         {
-          for (int j = lastres; j > startres; j--)
+          for (int j = editLastRes; j > startres; j--)
           {
             deleteChar(startres, groupSeqs, fixedRight);
           }
@@ -1390,28 +1502,36 @@ public class SeqPanel extends JPanel
         else
         {
           appendEdit(Action.DELETE_GAP, groupSeqs, startres,
-                  lastres - startres);
+                  editLastRes - startres, false);
         }
-
       }
     }
     else
-    // ///Editing a single sequence///////////
     {
+      /*
+       * editing a single sequence
+       */
       if (insertGap)
       {
         // dragging to the right
         if (fixedColumns && fixedRight != -1)
         {
-          for (int j = lastres; j < startres; j++)
+          for (int j = editLastRes; j < startres; j++)
           {
-            insertChar(j, new SequenceI[] { seq }, fixedRight);
+            if (!insertGap(j, seqs, fixedRight))
+            {
+              /*
+               * e.g. cursor mode command specified 
+               * more inserts than are possible
+               */
+              return false;
+            }
           }
         }
         else
         {
-          appendEdit(Action.INSERT_GAP, new SequenceI[] { seq }, lastres,
-                  startres - lastres);
+          appendEdit(Action.INSERT_GAP, seqs, editLastRes,
+                  startres - editLastRes, false);
         }
       }
       else
@@ -1421,21 +1541,20 @@ public class SeqPanel extends JPanel
           // dragging to the left
           if (fixedColumns && fixedRight != -1)
           {
-            for (int j = lastres; j > startres; j--)
+            for (int j = editLastRes; j > startres; j--)
             {
               if (!Comparison.isGap(seq.getCharAt(startres)))
               {
-                endEditing();
-                break;
+                return false;
               }
-              deleteChar(startres, new SequenceI[] { seq }, fixedRight);
+              deleteChar(startres, seqs, fixedRight);
             }
           }
           else
           {
             // could be a keyboard edit trying to delete none gaps
             int max = 0;
-            for (int m = startres; m < lastres; m++)
+            for (int m = startres; m < editLastRes; m++)
             {
               if (!Comparison.isGap(seq.getCharAt(m)))
               {
@@ -1443,11 +1562,9 @@ public class SeqPanel extends JPanel
               }
               max++;
             }
-
             if (max > 0)
             {
-              appendEdit(Action.DELETE_GAP, new SequenceI[] { seq },
-                      startres, max);
+              appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
             }
           }
         }
@@ -1455,25 +1572,82 @@ public class SeqPanel extends JPanel
         {// insertGap==false AND editSeq==TRUE;
           if (fixedColumns && fixedRight != -1)
           {
-            for (int j = lastres; j < startres; j++)
+            for (int j = editLastRes; j < startres; j++)
             {
-              insertChar(j, new SequenceI[] { seq }, fixedRight);
+              insertGap(j, seqs, fixedRight);
             }
           }
           else
           {
-            appendEdit(Action.INSERT_NUC, new SequenceI[] { seq }, lastres,
-                    startres - lastres);
+            appendEdit(Action.INSERT_NUC, seqs, editLastRes,
+                    startres - editLastRes, false);
           }
         }
       }
     }
 
-    lastres = startres;
-    seqCanvas.repaint();
+    return true;
   }
 
-  void insertChar(int j, SequenceI[] seq, int fixedColumn)
+  /**
+   * Constructs an informative status bar message while dragging to insert or
+   * delete gaps. Answers null if inserts and deletes cancel out.
+   * 
+   * @param editCommand
+   *          a command containing the list of individual edits
+   * @return
+   */
+  protected static String getEditStatusMessage(EditCommand editCommand)
+  {
+    if (editCommand == null)
+    {
+      return null;
+    }
+
+    /*
+     * add any inserts, and subtract any deletes,  
+     * not counting those auto-inserted when doing a 'locked edit'
+     * (so only counting edits 'under the cursor')
+     */
+    int count = 0;
+    for (Edit cmd : editCommand.getEdits())
+    {
+      if (!cmd.isSystemGenerated())
+      {
+        count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
+                : -cmd.getNumber();
+      }
+    }
+
+    if (count == 0)
+    {
+      /*
+       * inserts and deletes cancel out
+       */
+      return null;
+    }
+
+    String msgKey = count > 1 ? "label.insert_gaps"
+            : (count == 1 ? "label.insert_gap"
+                    : (count == -1 ? "label.delete_gap"
+                            : "label.delete_gaps"));
+    count = Math.abs(count);
+
+    return MessageManager.formatMessage(msgKey, String.valueOf(count));
+  }
+
+  /**
+   * Inserts one gap at column j, deleting the right-most gapped column up to
+   * (and including) fixedColumn. Returns true if the edit is successful, false
+   * if no blank column is available to allow the insertion to be balanced by a
+   * deletion.
+   * 
+   * @param j
+   * @param seq
+   * @param fixedColumn
+   * @return
+   */
+  boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
   {
     int blankColumn = fixedColumn;
     for (int s = 0; s < seq.length; s++)
@@ -1494,40 +1668,53 @@ public class SeqPanel extends JPanel
       {
         blankColumn = fixedColumn;
         endEditing();
-        return;
+        return false;
       }
     }
 
-    appendEdit(Action.DELETE_GAP, seq, blankColumn, 1);
+    appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
 
-    appendEdit(Action.INSERT_GAP, seq, j, 1);
+    appendEdit(Action.INSERT_GAP, seq, j, 1, false);
 
+    return true;
   }
 
   /**
-   * Helper method to add and perform one edit action.
+   * Helper method to add and perform one edit action
    * 
    * @param action
    * @param seq
    * @param pos
    * @param count
+   * @param systemGenerated
+   *          true if the edit is a 'balancing' delete (or insert) to match a
+   *          user's insert (or delete) in a locked editing region
    */
   protected void appendEdit(Action action, SequenceI[] seq, int pos,
-          int count)
+          int count, boolean systemGenerated)
   {
 
     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
             av.getAlignment().getGapCharacter());
+    edit.setSystemGenerated(systemGenerated);
 
     editCommand.appendEdit(edit, av.getAlignment(), true, null);
   }
 
-  void deleteChar(int j, SequenceI[] seq, int fixedColumn)
+  /**
+   * Deletes the character at column j, and inserts a gap at fixedColumn, in
+   * each of the given sequences. The caller should ensure that all sequences
+   * are gapped in column j.
+   * 
+   * @param j
+   * @param seqs
+   * @param fixedColumn
+   */
+  void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
   {
+    appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
 
-    appendEdit(Action.DELETE_GAP, seq, j, 1);
-
-    appendEdit(Action.INSERT_GAP, seq, fixedColumn, 1);
+    appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
   }
 
   /**
@@ -1626,7 +1813,8 @@ public class SeqPanel extends JPanel
   public void mouseWheelMoved(MouseWheelEvent e)
   {
     e.consume();
-    if (e.getWheelRotation() > 0)
+    double wheelRotation = e.getPreciseWheelRotation();
+    if (wheelRotation > 0)
     {
       if (e.isShiftDown())
       {
@@ -1638,7 +1826,7 @@ public class SeqPanel extends JPanel
         av.getRanges().scrollUp(false);
       }
     }
-    else
+    else if (wheelRotation < 0)
     {
       if (e.isShiftDown())
       {
@@ -1795,21 +1983,10 @@ public class SeqPanel extends JPanel
     final int column = findColumn(evt);
     final int seq = findSeq(evt);
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
+    List<SequenceFeature> features = ap.getFeatureRenderer()
             .findFeaturesAtColumn(sequence, column + 1);
-    List<String> links = new ArrayList<>();
-    for (SequenceFeature sf : allFeatures)
-    {
-      if (sf.links != null)
-      {
-        for (String link : sf.links)
-        {
-          links.add(link);
-        }
-      }
-    }
 
-    PopupMenu pop = new PopupMenu(ap, null, links);
+    PopupMenu pop = new PopupMenu(ap, null, features);
     pop.show(this, evt.getX(), evt.getY());
   }
 
@@ -1978,6 +2155,32 @@ public class SeqPanel extends JPanel
     {
       scrollThread.setEvent(evt);
     }
+
+    /*
+     * construct a status message showing the range of the selection
+     */
+    StringBuilder status = new StringBuilder(64);
+    List<SequenceI> seqs = stretchGroup.getSequences();
+    String name = seqs.get(0).getName();
+    if (name.length() > 20)
+    {
+      name = name.substring(0, 20);
+    }
+    status.append(name).append(" - ");
+    name = seqs.get(seqs.size() - 1).getName();
+    if (name.length() > 20)
+    {
+      name = name.substring(0, 20);
+    }
+    status.append(name).append(" ");
+    int startRes = stretchGroup.getStartRes();
+    status.append(" cols ").append(String.valueOf(startRes + 1))
+            .append("-");
+    int endRes = stretchGroup.getEndRes();
+    status.append(String.valueOf(endRes + 1));
+    status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
+            .append(String.valueOf(endRes - startRes + 1)).append(")");
+    ap.alignFrame.setStatus(status.toString());
   }
 
   void scrollCanvas(MouseEvent evt)
@@ -2258,4 +2461,13 @@ public class SeqPanel extends JPanel
 
     return true;
   }
+
+  /**
+   * 
+   * @return null or last search results handled by this panel
+   */
+  public SearchResultsI getLastSearchResults()
+  {
+    return lastSearchResults;
+  }
 }