JAL-1244 improved status message for insert/delete gap
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 5 Oct 2018 12:56:33 +0000 (13:56 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 5 Oct 2018 12:56:33 +0000 (13:56 +0100)
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 ae5b0e7..3718a83 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 555977d..8e887b1 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 cac843f..81076e5 100644 (file)
@@ -114,7 +114,7 @@ public class EditCommand implements CommandI
     public abstract Action getUndoAction();
   };
 
-  private List<Edit> edits = new ArrayList<Edit>();
+  private List<Edit> edits = new ArrayList<>();
 
   String description;
 
@@ -789,7 +789,7 @@ public class EditCommand implements CommandI
     if (modifyVisibility && !insert)
     {
       // only occurs if a sequence was added or deleted.
-      command.deletedAnnotationRows = new Hashtable<SequenceI, AlignmentAnnotation[]>();
+      command.deletedAnnotationRows = new Hashtable<>();
     }
     if (command.fullAlignmentHeight)
     {
@@ -948,7 +948,7 @@ public class EditCommand implements CommandI
 
     if (!insert)
     {
-      command.deletedAnnotations = new Hashtable<String, Annotation[]>();
+      command.deletedAnnotations = new Hashtable<>();
     }
 
     int aSize;
@@ -1138,7 +1138,7 @@ public class EditCommand implements CommandI
       return;
     }
 
-    List<SequenceFeature> oldsf = new ArrayList<SequenceFeature>();
+    List<SequenceFeature> oldsf = new ArrayList<>();
 
     int cSize = j - i;
 
@@ -1196,7 +1196,7 @@ public class EditCommand implements CommandI
 
     if (command.editedFeatures == null)
     {
-      command.editedFeatures = new Hashtable<SequenceI, List<SequenceFeature>>();
+      command.editedFeatures = new Hashtable<>();
     }
 
     command.editedFeatures.put(seq, oldsf);
@@ -1233,7 +1233,7 @@ public class EditCommand implements CommandI
    */
   public Map<SequenceI, SequenceI> priorState(boolean forUndo)
   {
-    Map<SequenceI, SequenceI> result = new HashMap<SequenceI, SequenceI>();
+    Map<SequenceI, SequenceI> result = new HashMap<>();
     if (getEdits() == null)
     {
       return result;
@@ -1266,7 +1266,7 @@ public class EditCommand implements CommandI
      * Work backwards through the edit list, deriving the sequences before each
      * was applied. The final result is the sequence set before any edits.
      */
-    Iterator<Edit> editList = new ReverseListIterator<Edit>(getEdits());
+    Iterator<Edit> editList = new ReverseListIterator<>(getEdits());
     while (editList.hasNext())
     {
       Edit oldEdit = editList.next();
@@ -1339,6 +1339,12 @@ public class EditCommand implements CommandI
 
     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)
     {
@@ -1410,6 +1416,16 @@ public class EditCommand implements CommandI
     {
       return gapChar;
     }
+
+    public void setSystemGenerated(boolean b)
+    {
+      systemGenerated = b;
+    }
+
+    public boolean isSystemGenerated()
+    {
+      return systemGenerated;
+    }
   }
 
   /**
@@ -1427,7 +1443,7 @@ public class EditCommand implements CommandI
     }
     else
     {
-      return new ReverseListIterator<Edit>(getEdits());
+      return new ReverseListIterator<>(getEdits());
     }
   }
 }
index 0135e9d..92a5030 100644 (file)
@@ -1167,9 +1167,32 @@ 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.lastres</code> (cursor position)
+   */
   synchronized void editSequence(boolean insertGap, boolean editSeq,
-          int startres)
+          final int startres)
   {
     int fixedLeft = -1;
     int fixedRight = -1;
@@ -1189,33 +1212,39 @@ public class SeqPanel extends JPanel
     }
 
     /*
-     * initialise the edit command if there is not
-     * already one being extended
+     * make a name for the edit action, for
+     * status bar message and Undo/Redo menu
      */
-    if (editCommand == null)
+    String label = null;
+    if (groupEditing)
     {
-      if (groupEditing)
-      {
-        editCommand = new EditCommand(
-                MessageManager.getString("action.edit_group"));
-      }
-      else
+      label = MessageManager.getString("action.edit_group");
+    }
+    else
+    {
+      label = seq.getName();
+      if (label.length() > 10)
       {
-        String label = seq.getName();
-        if (label.length() > 10)
-        {
-          label = label.substring(0, 10);
-        }
-        editCommand = new EditCommand(MessageManager
-                .formatMessage("label.edit_params", new String[]
-                { label }));
+        label = label.substring(0, 10);
       }
+      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);
+    }
 
     // Are we editing within a selection group?
-    if (groupEditing || (sg != null
-            && sg.getSequences(av.getHiddenRepSequences()).contains(seq)))
+    boolean inSelectionGroup = sg != null
+            && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
+    if (groupEditing || inSelectionGroup)
     {
       fixedColumns = true;
 
@@ -1285,286 +1314,312 @@ public class SeqPanel extends JPanel
       }
     }
 
-    if (groupEditing)
-    {
-      ap.alignFrame.statusBar.setText(" "); // defer this as difficult!
-      List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
-      int g, groupSize = vseqs.size();
-      SequenceI[] groupSeqs = new SequenceI[groupSize];
-      for (g = 0; g < groupSeqs.length; g++)
-      {
-        groupSeqs[g] = vseqs.get(g);
-      }
+    SequenceI[] seqs = new SequenceI[] { seq };
+
+    boolean endEditing = false;
 
-      // drag to right
-      if (insertGap)
+    try
+    {
+      if (groupEditing)
       {
-        // If the user has selected the whole sequence, and is dragging to
-        // the right, we can still extend the alignment and selectionGroup
-        if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
-                && sg.getEndRes() == av.getAlignment().getWidth() - 1)
+        List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
+        int g, groupSize = vseqs.size();
+        SequenceI[] groupSeqs = new SequenceI[groupSize];
+        for (g = 0; g < groupSeqs.length; g++)
         {
-          sg.setEndRes(av.getAlignment().getWidth() + startres - lastres);
-          fixedRight = sg.getEndRes();
+          groupSeqs[g] = vseqs.get(g);
         }
 
-        // Is it valid with fixed columns??
-        // Find the next gap before the end
-        // of the visible region boundary
-        boolean blank = false;
-        for (; fixedRight > lastres; fixedRight--)
+        // drag to right
+        if (insertGap)
         {
-          blank = true;
+          // If the user has selected the whole sequence, and is dragging to
+          // the right, we can still extend the alignment and selectionGroup
+          if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
+                  && sg.getEndRes() == av.getAlignment().getWidth() - 1)
+          {
+            sg.setEndRes(av.getAlignment().getWidth() + startres - lastres);
+            fixedRight = sg.getEndRes();
+          }
 
-          for (g = 0; g < groupSize; g++)
+          // Is it valid with fixed columns??
+          // Find the next gap before the end
+          // of the visible region boundary
+          boolean blank = false;
+          for (; fixedRight > lastres; fixedRight--)
           {
-            for (int j = 0; j < startres - lastres; j++)
+            blank = true;
+
+            for (g = 0; g < groupSize; g++)
             {
-              if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
+              for (int j = 0; j < startres - lastres; j++)
               {
-                blank = false;
-                break;
+                if (!Comparison
+                        .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
+                {
+                  blank = false;
+                  break;
+                }
               }
             }
+            if (blank)
+            {
+              break;
+            }
           }
-          if (blank)
-          {
-            break;
-          }
-        }
 
-        if (!blank)
-        {
-          if (sg.getSize() == av.getAlignment().getHeight())
+          if (!blank)
           {
-            if ((av.hasHiddenColumns() && startres < av.getAlignment()
-                    .getHiddenColumns()
-                    .getNextHiddenBoundary(false, startres)))
+            if (sg.getSize() == av.getAlignment().getHeight())
             {
-              endEditing();
-              return;
-            }
+              if ((av.hasHiddenColumns()
+                      && startres < av.getAlignment().getHiddenColumns()
+                              .getNextHiddenBoundary(false, startres)))
+              {
+                endEditing = true;
+                return;
+              }
 
-            int alWidth = av.getAlignment().getWidth();
-            if (av.hasHiddenRows())
-            {
-              int hwidth = av.getAlignment().getHiddenSequences()
-                      .getWidth();
-              if (hwidth > alWidth)
+              int alWidth = av.getAlignment().getWidth();
+              if (av.hasHiddenRows())
               {
-                alWidth = hwidth;
+                int hwidth = av.getAlignment().getHiddenSequences()
+                        .getWidth();
+                if (hwidth > alWidth)
+                {
+                  alWidth = hwidth;
+                }
               }
+              // We can still insert gaps if the selectionGroup
+              // contains all the sequences
+              sg.setEndRes(sg.getEndRes() + startres - lastres);
+              fixedRight = alWidth + startres - lastres;
+            }
+            else
+            {
+              endEditing = true;
+              return;
             }
-            // We can still insert gaps if the selectionGroup
-            // contains all the sequences
-            sg.setEndRes(sg.getEndRes() + startres - lastres);
-            fixedRight = alWidth + startres - lastres;
-          }
-          else
-          {
-            endEditing();
-            return;
           }
         }
-      }
 
-      // drag to left
-      else if (!insertGap)
-      {
-        // / Are we able to delete?
-        // ie are all columns blank?
-
-        for (g = 0; g < groupSize; g++)
+        // drag to left
+        else if (!insertGap)
         {
-          for (int j = startres; j < lastres; j++)
+          // / Are we able to delete?
+          // ie are all columns blank?
+
+          for (g = 0; g < groupSize; g++)
           {
-            if (groupSeqs[g].getLength() <= j)
+            for (int j = startres; j < lastres; j++)
             {
-              continue;
-            }
+              if (groupSeqs[g].getLength() <= j)
+              {
+                continue;
+              }
 
-            if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
-            {
-              // Not a gap, block edit not valid
-              endEditing();
-              return;
+              if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
+              {
+                // Not a gap, block edit not valid
+                endEditing = true;
+                return;
+              }
             }
           }
         }
-      }
 
-      if (insertGap)
-      {
-        // dragging to the right
-        if (fixedColumns && fixedRight != -1)
+        if (insertGap)
         {
-          for (int j = lastres; j < startres; j++)
+          // dragging to the right
+          if (fixedColumns && fixedRight != -1)
           {
-            insertChar(j, groupSeqs, fixedRight);
+            for (int j = lastres; j < startres; j++)
+            {
+              insertGap(j, groupSeqs, fixedRight);
+            }
           }
-        }
-        else
-        {
-          appendEdit(Action.INSERT_GAP, groupSeqs, startres,
-                  startres - lastres);
-        }
-      }
-      else
-      {
-        // dragging to the left
-        if (fixedColumns && fixedRight != -1)
-        {
-          for (int j = lastres; j > startres; j--)
+          else
           {
-            deleteChar(startres, groupSeqs, fixedRight);
+            appendEdit(Action.INSERT_GAP, groupSeqs, startres,
+                    startres - lastres, false);
           }
         }
         else
         {
-          appendEdit(Action.DELETE_GAP, groupSeqs, startres,
-                  lastres - startres);
-        }
-
-      }
-    }
-    else
-    // ///Editing a single sequence///////////
-    {
-      if (fixedRight == -1)
-      {
-        String msg = getEditStatusMessage(insertGap, seq.getName());
-        ap.alignFrame.statusBar.setText(msg);
-      }
-      else
-      {
-        ap.alignFrame.statusBar.setText(" ");
-      }
-      if (insertGap)
-      {
-        // dragging to the right
-        if (fixedColumns && fixedRight != -1)
-        {
-          for (int j = lastres; j < startres; j++)
+          // dragging to the left
+          if (fixedColumns && fixedRight != -1)
+          {
+            for (int j = lastres; j > startres; j--)
+            {
+              deleteChar(startres, groupSeqs, fixedRight);
+            }
+          }
+          else
           {
-            insertChar(j, new SequenceI[] { seq }, fixedRight);
+            appendEdit(Action.DELETE_GAP, groupSeqs, startres,
+                    lastres - startres, false);
           }
         }
-        else
-        {
-          appendEdit(Action.INSERT_GAP, new SequenceI[] { seq }, lastres,
-                  startres - lastres);
-        }
       }
       else
       {
-        if (!editSeq)
+        /*
+         * editing a single sequence
+         */
+        if (insertGap)
         {
-          // dragging to the left
+          // dragging to the right
           if (fixedColumns && fixedRight != -1)
           {
-            for (int j = lastres; j > startres; j--)
+            for (int j = lastres; j < startres; j++)
             {
-              if (!Comparison.isGap(seq.getCharAt(startres)))
+              if (!insertGap(j, seqs, fixedRight))
               {
-                endEditing();
+                /*
+                 * e.g. cursor mode command asked for 
+                 * more inserts than are possible
+                 */
+                endEditing = true;
                 break;
               }
-              deleteChar(startres, new SequenceI[] { seq }, fixedRight);
             }
           }
           else
           {
-            // could be a keyboard edit trying to delete none gaps
-            int max = 0;
-            for (int m = startres; m < lastres; m++)
+            appendEdit(Action.INSERT_GAP, seqs, lastres, startres - lastres,
+                    false);
+          }
+        }
+        else
+        {
+          if (!editSeq)
+          {
+            // dragging to the left
+            if (fixedColumns && fixedRight != -1)
             {
-              if (!Comparison.isGap(seq.getCharAt(m)))
+              for (int j = lastres; j > startres; j--)
               {
-                break;
+                if (!Comparison.isGap(seq.getCharAt(startres)))
+                {
+                  endEditing = true;
+                  break;
+                }
+                deleteChar(startres, seqs, fixedRight);
               }
-              max++;
             }
-
-            if (max > 0)
+            else
             {
-              appendEdit(Action.DELETE_GAP, new SequenceI[] { seq },
-                      startres, max);
+              // could be a keyboard edit trying to delete none gaps
+              int max = 0;
+              for (int m = startres; m < lastres; m++)
+              {
+                if (!Comparison.isGap(seq.getCharAt(m)))
+                {
+                  break;
+                }
+                max++;
+              }
+              if (max > 0)
+              {
+                appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
+              }
             }
           }
-        }
-        else
-        {// insertGap==false AND editSeq==TRUE;
-          if (fixedColumns && fixedRight != -1)
-          {
-            for (int j = lastres; j < startres; j++)
+          else
+          {// insertGap==false AND editSeq==TRUE;
+            if (fixedColumns && fixedRight != -1)
             {
-              insertChar(j, new SequenceI[] { seq }, fixedRight);
+              for (int j = lastres; j < startres; j++)
+              {
+                insertGap(j, seqs, fixedRight);
+              }
+            }
+            else
+            {
+              appendEdit(Action.INSERT_NUC, seqs, lastres,
+                      startres - lastres, false);
             }
-          }
-          else
-          {
-            appendEdit(Action.INSERT_NUC, new SequenceI[] { seq }, lastres,
-                    startres - lastres);
           }
         }
       }
-    }
+    } finally
+    {
+      /*
+       * report what actually happened (might be less than
+       * what was requested)
+       */
+      String msg = getEditStatusMessage(editCommand);
+      ap.alignFrame.statusBar.setText(msg == null ? " " : msg);
 
-    lastres = startres;
-    seqCanvas.repaint();
+      if (endEditing)
+      {
+        endEditing();
+      }
+
+      lastres = startres;
+      seqCanvas.repaint();
+    }
   }
 
   /**
    * Constructs an informative status bar message while dragging to insert or
-   * delete gaps
+   * delete gaps. Answers null if inserts and deletes cancel out.
    * 
-   * @param insert
-   * @param seqName
+   * @param editCommand
+   *          a command containing the list of individual edits
    * @return
    */
-  protected String getEditStatusMessage(boolean insert, String seqName)
+  protected static String getEditStatusMessage(EditCommand editCommand)
   {
+    if (editCommand == null)
+    {
+      return null;
+    }
+
     /*
-     * add any inserts, and subtract any deletes, so far
+     * 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())
     {
-      count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
-              : -cmd.getNumber();
+      if (!cmd.isSystemGenerated())
+      {
+        count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
+                : -cmd.getNumber();
+      }
     }
 
-    /*
-     * add the current action
-     */
-    count += insert ? 1 : -1;
-
     if (count == 0)
     {
       /*
        * inserts and deletes cancel out
        */
-      return " ";
-    }
-    StringBuilder message = new StringBuilder(64);
-    if (groupEditing)
-    {
-      message.append("Edit group:");
-    }
-    else
-    {
-      message.append("Edit sequence: ").append(seqName);
+      return null;
     }
 
-    message.append(count > 0 ? " insert " : " delete ");
+    String msgKey = count > 1 ? "label.insert_gaps"
+            : (count == 1 ? "label.insert_gap"
+                    : (count == -1 ? "label.delete_gap"
+                            : "label.delete_gaps"));
     count = Math.abs(count);
-    message.append(String.valueOf(count));
-    message.append(count > 1 ? " gaps" : " gap");
-    String msg = message.toString();
-    return msg;
+
+    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++)
@@ -1585,40 +1640,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);
   }
 
   /**
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);
+  }
 }