JAL-1244 improved status message for insert/delete gap
[jalview.git] / src / jalview / gui / SeqPanel.java
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);
   }
 
   /**