}
}
- // 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;
}
/*
- * 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;
}
}
- 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++)
{
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);
}
/**
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;
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);
+ }
}