import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SeqPanel extends JPanel
implements MouseListener, MouseMotionListener, MouseWheelListener,
SequenceListener, SelectionListener
-
{
- /** DOCUMENT ME!! */
+ private static final int MAX_TOOLTIP_LENGTH = 300;
+
public SeqCanvas seqCanvas;
- /** DOCUMENT ME!! */
public AlignmentPanel ap;
/*
*/
private int lastMouseSeq;
- protected int lastres;
+ protected int editLastRes;
- protected int startseq;
+ protected int editStartSeq;
protected AlignViewport av;
SearchResultsI lastSearchResults;
/**
- * Creates a new SeqPanel object.
+ * Creates a new SeqPanel object
*
- * @param avp
- * DOCUMENT ME!
- * @param p
- * DOCUMENT ME!
+ * @param viewport
+ * @param alignPanel
*/
- public SeqPanel(AlignViewport av, AlignmentPanel ap)
+ public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
{
linkImageURL = getClass().getResource("/images/link.gif");
seqARep = new SequenceAnnotationReport(linkImageURL.toString());
ToolTipManager.sharedInstance().registerComponent(this);
ToolTipManager.sharedInstance().setInitialDelay(0);
ToolTipManager.sharedInstance().setDismissDelay(10000);
- this.av = av;
+ this.av = viewport;
setBackground(Color.white);
- seqCanvas = new SeqCanvas(ap);
+ seqCanvas = new SeqCanvas(alignPanel);
setLayout(new BorderLayout());
add(seqCanvas, BorderLayout.CENTER);
- this.ap = ap;
+ this.ap = alignPanel;
- if (!av.isDataset())
+ if (!viewport.isDataset())
{
addMouseMotionListener(this);
addMouseListener(this);
addMouseWheelListener(this);
- ssm = av.getStructureSelectionManager();
+ ssm = viewport.getStructureSelectionManager();
ssm.addStructureViewerListener(this);
ssm.addSelectionListener(this);
}
/*
* Tidy up come what may...
*/
- startseq = -1;
- lastres = -1;
+ editStartSeq = -1;
+ editLastRes = -1;
editingSeqs = false;
groupEditing = false;
keyboardNo1 = null;
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();
}
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();
}
{
// 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();
}
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;
List<SequenceFeature> features = ap.getFeatureRenderer()
.findFeaturesAtColumn(sequence, column + 1);
seqARep.appendFeatures(tooltipText, pos, features,
- this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
+ this.ap.getSeqPanel().seqCanvas.fr);
}
if (tooltipText.length() == 6) // <html>
{
}
else
{
+ if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
+ {
+ tooltipText.setLength(MAX_TOOLTIP_LENGTH);
+ tooltipText.append("...");
+ }
String textString = tooltipText.toString();
if (lastTooltip == null || !lastTooltip.equals(textString))
{
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);
}
}
- // 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())
}
}
- 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;
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;
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;
}
}
+ 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());
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();
}
// 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;
{
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();
}
// 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;
}
}
}
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)
{
if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
{
// Not a gap, block edit not valid
- endEditing();
- return;
+ return false;
}
}
}
// 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
// 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);
}
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
// 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)))
{
}
max++;
}
-
if (max > 0)
{
- appendEdit(Action.DELETE_GAP, new SequenceI[] { seq },
- startres, max);
+ appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
}
}
}
{// 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++)
{
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);
}
/**
final int column = findColumn(evt);
final int seq = findSeq(evt);
SequenceI sequence = av.getAlignment().getSequenceAt(seq);
- List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
+ List<SequenceFeature> features = ap.getFeatureRenderer()
.findFeaturesAtColumn(sequence, column + 1);
- List<String> links = new ArrayList<>();
- for (SequenceFeature sf : allFeatures)
- {
- if (sf.links != null)
- {
- for (String link : sf.links)
- {
- links.add(link);
- }
- }
- }
- PopupMenu pop = new PopupMenu(ap, null, links);
+ PopupMenu pop = new PopupMenu(ap, null, features);
pop.show(this, evt.getX(), evt.getY());
}
{
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)