Merge branch 'feature/JAL-1244stretchStatusMsg' into merge/JAL-1244
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 1 Mar 2019 12:26:45 +0000 (12:26 +0000)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 1 Mar 2019 12:26:45 +0000 (12:26 +0000)
Conflicts:
src/jalview/commands/EditCommand.java

resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/commands/EditCommand.java
src/jalview/gui/SeqPanel.java
test/jalview/gui/SeqPanelTest.java

index 4ad76a0..c430417 100644 (file)
@@ -502,6 +502,10 @@ label.edit_name_description = Edit Name/Description...
 label.create_sequence_feature = Create Sequence Feature...
 label.edit_sequence = Edit Sequence
 label.edit_sequences = Edit Sequences
+label.insert_gap = Insert 1 gap
+label.insert_gaps = Insert {0} gaps
+label.delete_gap = Delete 1 gap
+label.delete_gaps = Delete {0} gaps
 label.sequence_details = Sequence Details
 label.jmol_help = Jmol Help
 label.chimera_help = Chimera Help
index 86f60f9..e7b9694 100644 (file)
@@ -468,6 +468,10 @@ label.edit_name_description = Editar nombre/descripci
 label.create_sequence_feature = Crear funciĆ³n de secuencia
 label.edit_sequence = Editar secuencia
 label.edit_sequences = Editar secuencias
+label.insert_gap = Insertar 1 hueco
+label.insert_gaps = Insertar {0} huecos
+label.delete_gap = Borrar 1 hueco
+label.delete_gaps = Borrar {0} huecos
 label.sequence_details = Detalles de la secuencia
 label.jmol_help = Ayuda de Jmol
 # Todos/Todas is gender-sensitive, but currently only used for feminine (cadena / anotaciĆ³n)! 
index f80bd4a..b9d32f7 100644 (file)
@@ -1484,6 +1484,12 @@ public class EditCommand implements CommandI
 
     private char gapChar;
 
+    /*
+     * flag that identifies edits inserted to balance 
+     * user edits in a 'locked editing' region
+     */
+    private boolean systemGenerated;
+
     public Edit(Action cmd, SequenceI[] sqs, int pos, int count,
             char gap)
     {
@@ -1556,6 +1562,16 @@ public class EditCommand implements CommandI
     {
       return gapChar;
     }
+
+    public void setSystemGenerated(boolean b)
+    {
+      systemGenerated = b;
+    }
+
+    public boolean isSystemGenerated()
+    {
+      return systemGenerated;
+    }
   }
 
   /**
index f8e09a4..7292499 100644 (file)
@@ -92,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;
 
@@ -303,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;
@@ -532,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();
   }
@@ -541,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();
   }
@@ -551,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();
   }
@@ -680,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;
@@ -1145,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);
@@ -1167,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())
@@ -1188,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;
 
@@ -1247,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;
@@ -1276,8 +1297,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;
@@ -1298,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());
@@ -1316,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();
         }
 
@@ -1324,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;
@@ -1349,12 +1420,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();
@@ -1369,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;
           }
         }
       }
@@ -1388,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)
             {
@@ -1398,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;
             }
           }
         }
@@ -1410,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
@@ -1426,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);
           }
@@ -1434,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
@@ -1465,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)))
               {
@@ -1487,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);
             }
           }
         }
@@ -1499,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++)
@@ -1538,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);
   }
 
   /**
@@ -2015,6 +2158,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)
index a5d244d..7f3aef1 100644 (file)
 package jalview.gui;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
 
+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.util.MessageManager;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -88,4 +93,109 @@ public class SeqPanelTest
     assertEquals(alignFrame.statusBar.getText(),
             "Sequence 2 ID: Seq2 Residue: B (2)");
   }
+
+  @Test(groups = "Functional")
+  public void testGetEditStatusMessage()
+  {
+    assertNull(SeqPanel.getEditStatusMessage(null));
+
+    EditCommand edit = new EditCommand(); // empty
+    assertNull(SeqPanel.getEditStatusMessage(edit));
+
+    SequenceI[] seqs = new SequenceI[] { new Sequence("a", "b") };
+    
+    // 1 gap
+    edit.addEdit(edit.new Edit(Action.INSERT_GAP, seqs, 1, 1, '-'));
+    String expected = MessageManager.formatMessage("label.insert_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 3 more gaps makes +4
+    edit.addEdit(edit.new Edit(Action.INSERT_GAP, seqs, 1, 3, '-'));
+    expected = MessageManager.formatMessage("label.insert_gaps", "4");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 2 deletes makes + 2
+    edit.addEdit(edit.new Edit(Action.DELETE_GAP, seqs, 1, 2, '-'));
+    expected = MessageManager.formatMessage("label.insert_gaps", "2");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 2 more deletes makes 0 - no text
+    edit.addEdit(edit.new Edit(Action.DELETE_GAP, seqs, 1, 2, '-'));
+    assertNull(SeqPanel.getEditStatusMessage(edit));
+
+    // 1 more delete makes 1 delete
+    edit.addEdit(edit.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-'));
+    expected = MessageManager.formatMessage("label.delete_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+
+    // 1 more delete makes 2 deletes
+    edit.addEdit(edit.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-'));
+    expected = MessageManager.formatMessage("label.delete_gaps", "2");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+  }
+
+  /**
+   * Tests that simulate 'locked editing', where an inserted gap is balanced by
+   * a gap deletion in the selection group, and vice versa
+   */
+  @Test(groups = "Functional")
+  public void testGetEditStatusMessage_lockedEditing()
+  {
+    EditCommand edit = new EditCommand(); // empty
+    SequenceI[] seqs = new SequenceI[] { new Sequence("a", "b") };
+    
+    // 1 gap inserted, balanced by 1 delete
+    Edit e1 = edit.new Edit(Action.INSERT_GAP, seqs, 1, 1, '-');
+    edit.addEdit(e1);
+    Edit e2 = edit.new Edit(Action.DELETE_GAP, seqs, 5, 1, '-');
+    e2.setSystemGenerated(true);
+    edit.addEdit(e2);
+    String expected = MessageManager.formatMessage("label.insert_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+  
+    // 2 more gaps makes +3
+    Edit e3 = edit.new Edit(Action.INSERT_GAP, seqs, 1, 2, '-');
+    edit.addEdit(e3);
+    Edit e4 = edit.new Edit(Action.DELETE_GAP, seqs, 5, 2, '-');
+    e4.setSystemGenerated(true);
+    edit.addEdit(e4);
+    expected = MessageManager.formatMessage("label.insert_gaps", "3");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+  
+    // 2 deletes makes + 1
+    Edit e5 = edit.new Edit(Action.DELETE_GAP, seqs, 1, 2, '-');
+    edit.addEdit(e5);
+    Edit e6 = edit.new Edit(Action.INSERT_GAP, seqs, 5, 2, '-');
+    e6.setSystemGenerated(true);
+    edit.addEdit(e6);
+    expected = MessageManager.formatMessage("label.insert_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+  
+    // 1 more delete makes 0 - no text
+    Edit e7 = edit.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-');
+    edit.addEdit(e7);
+    Edit e8 = edit.new Edit(Action.INSERT_GAP, seqs, 5, 1, '-');
+    e8.setSystemGenerated(true);
+    edit.addEdit(e8);
+    expected = MessageManager.formatMessage("label.insert_gaps", "2");
+    assertNull(SeqPanel.getEditStatusMessage(edit));
+  
+    // 1 more delete makes 1 delete
+    Edit e9 = edit.new Edit(Action.DELETE_GAP, seqs, 1, 1, '-');
+    edit.addEdit(e9);
+    Edit e10 = edit.new Edit(Action.INSERT_GAP, seqs, 5, 1, '-');
+    e10.setSystemGenerated(true);
+    edit.addEdit(e10);
+    expected = MessageManager.formatMessage("label.delete_gap", "1");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+  
+    // 2 more deletes makes 3 deletes
+    Edit e11 = edit.new Edit(Action.DELETE_GAP, seqs, 1, 2, '-');
+    edit.addEdit(e11);
+    Edit e12 = edit.new Edit(Action.INSERT_GAP, seqs, 5, 2, '-');
+    e12.setSystemGenerated(true);
+    edit.addEdit(e12);
+    expected = MessageManager.formatMessage("label.delete_gaps", "3");
+    assertEquals(SeqPanel.getEditStatusMessage(edit), expected);
+  }
 }