Merge branch 'bug/JAL-3072scrollThread' into merge/JAL-3072_3073
[jalview.git] / src / jalview / gui / SeqPanel.java
index 401bd0a..4a1a9ee 100644 (file)
@@ -25,6 +25,7 @@ import jalview.bin.Cache;
 import jalview.commands.EditCommand;
 import jalview.commands.EditCommand.Action;
 import jalview.commands.EditCommand.Edit;
+import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.HiddenColumns;
@@ -77,6 +78,82 @@ public class SeqPanel extends JPanel
         implements MouseListener, MouseMotionListener, MouseWheelListener,
         SequenceListener, SelectionListener
 {
+  /*
+   * a class that holds computed mouse position
+   * - column of the alignment (0...)
+   * - sequence offset (0...)
+   * - annotation row offset (0...)
+   * where annotation offset is -1 unless the alignment is shown
+   * in wrapped mode, annotations are shown, and the mouse is
+   * over an annnotation row
+   */
+  static class MousePos
+  {
+    /*
+     * alignment column position of cursor (0...)
+     */
+    final int column;
+
+    /*
+     * index in alignment of sequence under cursor,
+     * or nearest above if cursor is not over a sequence
+     */
+    final int seqIndex;
+
+    /*
+     * index in annotations array of annotation under the cursor
+     * (only possible in wrapped mode with annotations shown),
+     * or -1 if cursor is not over an annotation row
+     */
+    final int annotationIndex;
+
+    MousePos(int col, int seq, int ann)
+    {
+      column = col;
+      seqIndex = seq;
+      annotationIndex = ann;
+    }
+
+    boolean isOverAnnotation()
+    {
+      return annotationIndex != -1;
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+      if (obj == null || !(obj instanceof MousePos))
+      {
+        return false;
+      }
+      MousePos o = (MousePos) obj;
+      boolean b = (column == o.column && seqIndex == o.seqIndex
+              && annotationIndex == o.annotationIndex);
+      // System.out.println(obj + (b ? "= " : "!= ") + this);
+      return b;
+    }
+
+    /**
+     * A simple hashCode that ensures that instances that satisfy equals() have
+     * the same hashCode
+     */
+    @Override
+    public int hashCode()
+    {
+      return column + seqIndex + annotationIndex;
+    }
+
+    /**
+     * toString method for debug output purposes only
+     */
+    @Override
+    public String toString()
+    {
+      return String.format("c%d:s%d:a%d", column, seqIndex,
+              annotationIndex);
+    }
+  }
+
   private static final int MAX_TOOLTIP_LENGTH = 300;
 
   public SeqCanvas seqCanvas;
@@ -84,18 +161,13 @@ public class SeqPanel extends JPanel
   public AlignmentPanel ap;
 
   /*
-   * last column position for mouseMoved event
+   * last position for mouseMoved event
    */
-  private int lastMouseColumn;
+  private MousePos lastMousePosition;
 
-  /*
-   * last sequence offset for mouseMoved event
-   */
-  private int lastMouseSeq;
+  protected int editLastRes;
 
-  protected int lastres;
-
-  protected int startseq;
+  protected int editStartSeq;
 
   protected AlignViewport av;
 
@@ -177,9 +249,6 @@ public class SeqPanel extends JPanel
       ssm.addStructureViewerListener(this);
       ssm.addSelectionListener(this);
     }
-
-    lastMouseColumn = -1;
-    lastMouseSeq = -1;
   }
 
   int startWrapBlock = -1;
@@ -187,6 +256,71 @@ public class SeqPanel extends JPanel
   int wrappedBlock = -1;
 
   /**
+   * Computes the column and sequence row (and possibly annotation row when in
+   * wrapped mode) for the given mouse position
+   * 
+   * @param evt
+   * @return
+   */
+  MousePos findMousePosition(MouseEvent evt)
+  {
+    int col = findColumn(evt);
+    int seqIndex = -1;
+    int annIndex = -1;
+    int y = evt.getY();
+
+    int charHeight = av.getCharHeight();
+    int alignmentHeight = av.getAlignment().getHeight();
+    if (av.getWrapAlignment())
+    {
+      seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
+              seqCanvas.getHeight());
+
+      /*
+       * yPos modulo height of repeating width
+       */
+      int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
+
+      /*
+       * height of sequences plus space / scale above,
+       * plus gap between sequences and annotations
+       */
+      int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
+              + alignmentHeight * charHeight
+              + SeqCanvas.SEQS_ANNOTATION_GAP;
+      if (yOffsetPx >= alignmentHeightPixels)
+      {
+        /*
+         * mouse is over annotations; find annotation index, also set
+         * last sequence above (for backwards compatible behaviour)
+         */
+        AlignmentAnnotation[] anns = av.getAlignment()
+                .getAlignmentAnnotation();
+        int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
+        annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
+        seqIndex = alignmentHeight - 1;
+      }
+      else
+      {
+        /*
+         * mouse is over sequence (or the space above sequences)
+         */
+        yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
+        if (yOffsetPx >= 0)
+        {
+          seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
+        }
+      }
+    }
+    else
+    {
+      seqIndex = Math.min((y / charHeight) + av.getRanges().getStartSeq(),
+              alignmentHeight - 1);
+    }
+
+    return new MousePos(col, seqIndex, annIndex);
+  }
+  /**
    * Returns the aligned sequence position (base 0) at the mouse position, or
    * the closest visible one
    * 
@@ -198,10 +332,11 @@ public class SeqPanel extends JPanel
     int res = 0;
     int x = evt.getX();
 
-    int startRes = av.getRanges().getStartRes();
+    final int startRes = av.getRanges().getStartRes();
+    final int charWidth = av.getCharWidth();
+
     if (av.getWrapAlignment())
     {
-
       int hgap = av.getCharHeight();
       if (av.getScaleAboveWrapped())
       {
@@ -213,35 +348,40 @@ public class SeqPanel extends JPanel
 
       int y = evt.getY();
       y = Math.max(0, y - hgap);
-      x = Math.max(0, x - seqCanvas.getLabelWidthWest());
+      x -= seqCanvas.getLabelWidthWest();
+      if (x < 0)
+      {
+        // mouse is over left scale
+        return -1;
+      }
 
       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
       if (cwidth < 1)
       {
         return 0;
       }
+      if (x >= cwidth * charWidth)
+      {
+        // mouse is over right scale
+        return -1;
+      }
 
       wrappedBlock = y / cHeight;
       wrappedBlock += startRes / cwidth;
       // allow for wrapped view scrolled right (possible from Overview)
       int startOffset = startRes % cwidth;
       res = wrappedBlock * cwidth + startOffset
-              + +Math.min(cwidth - 1, x / av.getCharWidth());
+              + Math.min(cwidth - 1, x / charWidth);
     }
     else
     {
-      if (x > seqCanvas.getX() + seqCanvas.getWidth())
-      {
-        // make sure we calculate relative to visible alignment, rather than
-        // right-hand gutter
-        x = seqCanvas.getX() + seqCanvas.getWidth();
-      }
-      res = (x / av.getCharWidth()) + startRes;
-      if (res > av.getRanges().getEndRes())
-      {
-        // moused off right
-        res = av.getRanges().getEndRes();
-      }
+      /*
+       * make sure we calculate relative to visible alignment, 
+       * rather than right-hand gutter
+       */
+      x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
+      res = (x / charWidth) + startRes;
+      res = Math.min(res, av.getRanges().getEndRes());
     }
 
     if (av.hasHiddenColumns())
@@ -251,38 +391,6 @@ public class SeqPanel extends JPanel
     }
 
     return res;
-
-  }
-
-  int findSeq(MouseEvent evt)
-  {
-    int seq = 0;
-    int y = evt.getY();
-
-    if (av.getWrapAlignment())
-    {
-      int hgap = av.getCharHeight();
-      if (av.getScaleAboveWrapped())
-      {
-        hgap += av.getCharHeight();
-      }
-
-      int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
-              + hgap + seqCanvas.getAnnotationHeight();
-
-      y -= hgap;
-
-      seq = Math.min((y % cHeight) / av.getCharHeight(),
-              av.getAlignment().getHeight() - 1);
-    }
-    else
-    {
-      seq = Math.min(
-              (y / av.getCharHeight()) + av.getRanges().getStartSeq(),
-              av.getAlignment().getHeight() - 1);
-    }
-
-    return seq;
   }
 
   /**
@@ -304,8 +412,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;
@@ -533,8 +641,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();
   }
@@ -542,8 +650,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();
   }
@@ -552,8 +660,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();
   }
@@ -618,13 +726,19 @@ public class SeqPanel extends JPanel
   @Override
   public void mouseReleased(MouseEvent evt)
   {
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+    {
+      return;
+    }
+
     boolean didDrag = mouseDragging; // did we come here after a drag
     mouseDragging = false;
     mouseWheelPressed = false;
 
     if (evt.isPopupTrigger()) // Windows: mouseReleased
     {
-      showPopupMenu(evt);
+      showPopupMenu(evt, pos);
       evt.consume();
       return;
     }
@@ -649,6 +763,11 @@ public class SeqPanel extends JPanel
   public void mousePressed(MouseEvent evt)
   {
     lastMousePress = evt.getPoint();
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+    {
+      return;
+    }
 
     if (SwingUtilities.isMiddleMouseButton(evt))
     {
@@ -667,28 +786,23 @@ public class SeqPanel extends JPanel
     }
     else
     {
-      doMousePressedDefineMode(evt);
+      doMousePressedDefineMode(evt, pos);
       return;
     }
 
-    int seq = findSeq(evt);
-    int res = findColumn(evt);
-
-    if (seq < 0 || res < 0)
-    {
-      return;
-    }
+    int seq = pos.seqIndex;
+    int res = pos.column;
 
     if ((seq < av.getAlignment().getHeight())
             && (res < av.getAlignment().getSequenceAt(seq).getLength()))
     {
-      startseq = seq;
-      lastres = res;
+      editStartSeq = seq;
+      editLastRes = res;
     }
     else
     {
-      startseq = -1;
-      lastres = -1;
+      editStartSeq = -1;
+      editLastRes = -1;
     }
 
     return;
@@ -733,7 +847,7 @@ public class SeqPanel extends JPanel
       // over residue to change abruptly, causing highlighted residue in panel 2
       // to change, causing a scroll in panel 1 etc)
       ap.setToScrollComplementPanel(false);
-      wasScrolled = ap.scrollToPosition(results, false);
+      wasScrolled = ap.scrollToPosition(results);
       if (wasScrolled)
       {
         seqCanvas.revalidate();
@@ -779,23 +893,32 @@ public class SeqPanel extends JPanel
       mouseDragged(evt);
     }
 
-    final int column = findColumn(evt);
-    final int seq = findSeq(evt);
+    final MousePos mousePos = findMousePosition(evt);
+    if (mousePos.equals(lastMousePosition))
+    {
+      /*
+       * just a pixel move without change of 'cell'
+       */
+      return;
+    }
+    lastMousePosition = mousePos;
 
-    if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
+    if (mousePos.isOverAnnotation())
     {
-      lastMouseSeq = -1;
+      mouseMovedOverAnnotation(mousePos);
       return;
     }
-    if (column == lastMouseColumn && seq == lastMouseSeq)
+    final int seq = mousePos.seqIndex;
+
+    final int column = mousePos.column;
+    if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
     {
-      /*
-       * just a pixel move without change of residue
-       */
+      lastMousePosition = null;
+      setToolTipText(null);
+      lastTooltip = null;
+      ap.alignFrame.setStatus("");
       return;
     }
-    lastMouseColumn = column;
-    lastMouseSeq = seq;
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
@@ -873,6 +996,35 @@ public class SeqPanel extends JPanel
     }
   }
 
+  /**
+   * When the view is in wrapped mode, and the mouse is over an annotation row,
+   * shows the corresponding tooltip and status message (if any)
+   * 
+   * @param pos
+   * @param column
+   */
+  protected void mouseMovedOverAnnotation(MousePos pos)
+  {
+    final int column = pos.column;
+    final int rowIndex = pos.annotationIndex;
+
+    if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
+            || rowIndex < 0)
+    {
+      return;
+    }
+    AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
+
+    String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
+            anns);
+    setToolTipText(tooltip);
+    lastTooltip = tooltip;
+
+    String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
+            anns[rowIndex]);
+    ap.alignFrame.setStatus(msg);
+  }
+
   private Point lastp = null;
 
   /*
@@ -883,20 +1035,26 @@ public class SeqPanel extends JPanel
   @Override
   public Point getToolTipLocation(MouseEvent event)
   {
-    int x = event.getX(), w = getWidth();
-    int wdth = (w - x < 200) ? -(w / 2) : 5; // switch sides when tooltip is too
-    // close to edge
+    if (tooltipText == null || tooltipText.length() <= 6)
+    {
+      lastp = null;
+      return null;
+    }
+
+    int x = event.getX();
+    int w = getWidth();
+    // switch sides when tooltip is too close to edge
+    int wdth = (w - x < 200) ? -(w / 2) : 5;
     Point p = lastp;
     if (!event.isShiftDown() || p == null)
     {
-      p = (tooltipText != null && tooltipText.length() > 6)
-              ? new Point(event.getX() + wdth, event.getY() - 20)
-              : null;
+      p = new Point(event.getX() + wdth, event.getY() - 20);
+      lastp = p;
     }
     /*
-     * TODO: try to modify position region is not obcured by tooltip
+     * TODO: try to set position so region is not obscured by tooltip
      */
-    return lastp = p;
+    return p;
   }
 
   String lastTooltip;
@@ -999,7 +1157,7 @@ public class SeqPanel extends JPanel
 
       text.append(" (").append(Integer.toString(residuePos)).append(")");
     }
-    ap.alignFrame.statusBar.setText(text.toString());
+    ap.alignFrame.setStatus(text.toString());
   }
 
   /**
@@ -1041,6 +1199,12 @@ public class SeqPanel extends JPanel
   @Override
   public void mouseDragged(MouseEvent evt)
   {
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.column == -1)
+    {
+      return;
+    }
+
     if (mouseWheelPressed)
     {
       boolean inSplitFrame = ap.av.getCodingComplement() != null;
@@ -1136,23 +1300,23 @@ public class SeqPanel extends JPanel
 
     if (!editingSeqs)
     {
-      doMouseDraggedDefineMode(evt);
+      dragStretchGroup(evt);
       return;
     }
 
-    int res = findColumn(evt);
+    int res = pos.column;
 
     if (res < 0)
     {
       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);
@@ -1169,16 +1333,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())
@@ -1190,30 +1378,38 @@ public class SeqPanel extends JPanel
       }
     }
 
-    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)
@@ -1225,12 +1421,17 @@ public class SeqPanel extends JPanel
       message.append(" delete ");
     }
 
-    message.append(Math.abs(startres - lastres) + " gaps.");
-    ap.alignFrame.statusBar.setText(message.toString());
+    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;
 
@@ -1249,10 +1450,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;
@@ -1278,8 +1479,8 @@ public class SeqPanel extends JPanel
       int y2 = av.getAlignment().getHiddenColumns()
               .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;
@@ -1300,6 +1501,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.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());
@@ -1318,7 +1567,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();
         }
 
@@ -1326,15 +1576,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;
@@ -1351,12 +1602,11 @@ public class SeqPanel extends JPanel
         {
           if (sg.getSize() == av.getAlignment().getHeight())
           {
-            if ((av.hasHiddenColumns() && startres < av.getAlignment()
-                    .getHiddenColumns()
-                    .getNextHiddenBoundary(false, startres)))
+            if ((av.hasHiddenColumns()
+                    && startres < av.getAlignment().getHiddenColumns()
+                            .getNextHiddenBoundary(false, startres)))
             {
-              endEditing();
-              return;
+              return false;
             }
 
             int alWidth = av.getAlignment().getWidth();
@@ -1371,13 +1621,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;
           }
         }
       }
@@ -1390,7 +1639,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)
             {
@@ -1400,8 +1649,7 @@ public class SeqPanel extends JPanel
             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
             {
               // Not a gap, block edit not valid
-              endEditing();
-              return;
+              return false;
             }
           }
         }
@@ -1412,15 +1660,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
@@ -1428,7 +1676,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);
           }
@@ -1436,28 +1684,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
@@ -1467,21 +1723,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)))
               {
@@ -1489,11 +1744,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);
             }
           }
         }
@@ -1501,25 +1754,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;
+  }
+
+  /**
+   * 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));
   }
 
-  void insertChar(int j, SequenceI[] seq, int fixedColumn)
+  /**
+   * 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++)
@@ -1540,40 +1850,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);
   }
 
   /**
@@ -1601,7 +1924,13 @@ public class SeqPanel extends JPanel
   @Override
   public void mouseExited(MouseEvent e)
   {
-    if (!av.getWrapAlignment() && mouseDragging && scrollThread == null)
+    ap.alignFrame.setStatus(" ");
+    if (av.getWrapAlignment())
+    {
+      return;
+    }
+
+    if (mouseDragging && scrollThread == null)
     {
       scrollThread = new ScrollThread();
     }
@@ -1616,7 +1945,12 @@ public class SeqPanel extends JPanel
   public void mouseClicked(MouseEvent evt)
   {
     SequenceGroup sg = null;
-    SequenceI sequence = av.getAlignment().getSequenceAt(findSeq(evt));
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+    {
+      return;
+    }
+
     if (evt.getClickCount() > 1)
     {
       sg = av.getSelectionGroup();
@@ -1626,12 +1960,13 @@ public class SeqPanel extends JPanel
         av.setSelectionGroup(null);
       }
 
-      int column = findColumn(evt);
+      int column = pos.column;
 
       /*
        * find features at the position (if not gapped), or straddling
        * the position (if at a gap)
        */
+      SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
 
@@ -1698,32 +2033,22 @@ public class SeqPanel extends JPanel
   /**
    * DOCUMENT ME!
    * 
-   * @param evt
+   * @param pos
    *          DOCUMENT ME!
    */
-  public void doMousePressedDefineMode(MouseEvent evt)
+  protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
   {
-    final int res = findColumn(evt);
-    final int seq = findSeq(evt);
-    oldSeq = seq;
-    updateOverviewAndStructs = false;
-
-    startWrapBlock = wrappedBlock;
-
-    if (av.getWrapAlignment() && seq > av.getAlignment().getHeight())
+    if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
     {
-      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-              MessageManager.getString(
-                      "label.cannot_edit_annotations_in_wrapped_view"),
-              MessageManager.getString("label.wrapped_view_no_edit"),
-              JvOptionPane.WARNING_MESSAGE);
       return;
     }
 
-    if (seq < 0 || res < 0)
-    {
-      return;
-    }
+    final int res = pos.column;
+    final int seq = pos.seqIndex;
+    oldSeq = seq;
+    updateOverviewAndStructs = false;
+
+    startWrapBlock = wrappedBlock;
 
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
 
@@ -1747,7 +2072,7 @@ public class SeqPanel extends JPanel
 
     if (evt.isPopupTrigger()) // Mac: mousePressed
     {
-      showPopupMenu(evt);
+      showPopupMenu(evt, pos);
       return;
     }
 
@@ -1763,8 +2088,8 @@ public class SeqPanel extends JPanel
 
     if (av.cursorMode)
     {
-      seqCanvas.cursorX = findColumn(evt);
-      seqCanvas.cursorY = findSeq(evt);
+      seqCanvas.cursorX = res;
+      seqCanvas.cursorY = seq;
       seqCanvas.repaint();
       return;
     }
@@ -1822,15 +2147,14 @@ public class SeqPanel extends JPanel
 
   /**
    * Build and show a pop-up menu at the right-click mouse position
-   * 
+   *
    * @param evt
-   * @param res
-   * @param sequences
+   * @param pos
    */
-  void showPopupMenu(MouseEvent evt)
+  void showPopupMenu(MouseEvent evt, MousePos pos)
   {
-    final int column = findColumn(evt);
-    final int seq = findSeq(evt);
+    final int column = pos.column;
+    final int seq = pos.seqIndex;
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
     List<SequenceFeature> features = ap.getFeatureRenderer()
             .findFeaturesAtColumn(sequence, column + 1);
@@ -1848,7 +2172,8 @@ public class SeqPanel extends JPanel
    *          true if this event is happening after a mouse drag (rather than a
    *          mouse down)
    */
-  public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
+  protected void doMouseReleasedDefineMode(MouseEvent evt,
+          boolean afterDrag)
   {
     if (stretchGroup == null)
     {
@@ -1864,8 +2189,11 @@ public class SeqPanel extends JPanel
             && afterDrag;
     if (stretchGroup.cs != null)
     {
-      stretchGroup.cs.alignmentChanged(stretchGroup,
-              av.getHiddenRepSequences());
+      if (afterDrag)
+      {
+        stretchGroup.cs.alignmentChanged(stretchGroup,
+                av.getHiddenRepSequences());
+      }
 
       ResidueShaderI groupColourScheme = stretchGroup
               .getGroupColourScheme();
@@ -1891,22 +2219,28 @@ public class SeqPanel extends JPanel
   }
 
   /**
-   * DOCUMENT ME!
+   * Resizes the borders of a selection group depending on the direction of
+   * mouse drag
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
-  public void doMouseDraggedDefineMode(MouseEvent evt)
+  protected void dragStretchGroup(MouseEvent evt)
   {
-    int res = findColumn(evt);
-    int y = findSeq(evt);
+    if (stretchGroup == null)
+    {
+      return;
+    }
 
-    if (wrappedBlock != startWrapBlock)
+    MousePos pos = findMousePosition(evt);
+    if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
     {
       return;
     }
 
-    if (stretchGroup == null)
+    int res = pos.column;
+    int y = pos.seqIndex;
+
+    if (wrappedBlock != startWrapBlock)
     {
       return;
     }
@@ -2001,6 +2335,32 @@ public class SeqPanel extends JPanel
     {
       scrollThread.setMousePosition(evt.getPoint());
     }
+
+    /*
+     * 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());
   }
 
   /**
@@ -2306,7 +2666,7 @@ public class SeqPanel extends JPanel
     HiddenColumns hs = new HiddenColumns();
     MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
     av.setColumnSelection(cs);
-    av.getAlignment().setHiddenColumns(hs);
+    boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
 
     // lastly, update any dependent dialogs
     if (ap.getCalculationDialog() != null)
@@ -2314,7 +2674,11 @@ public class SeqPanel extends JPanel
       ap.getCalculationDialog().validateCalcTypes();
     }
 
-    PaintRefresher.Refresh(this, av.getSequenceSetId());
+    /*
+     * repaint alignment, and also Overview or Structure
+     * if hidden column selection has changed
+     */
+    ap.paintAlignment(hiddenChanged, hiddenChanged);
 
     return true;
   }