X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fgui%2FSeqPanel.java;h=7abbd7d440a289204aa576e6ec579f0e9caeade5;hb=bc18effe68ba80213a6d03ca7e6175adc6be71d6;hp=0135e9d84096377b116e7a9125ec11c71def4006;hpb=793e6d46ae572461a030e2751c0d748f2521b71e;p=jalview.git diff --git a/src/jalview/gui/SeqPanel.java b/src/jalview/gui/SeqPanel.java index 0135e9d..7abbd7d 100644 --- a/src/jalview/gui/SeqPanel.java +++ b/src/jalview/gui/SeqPanel.java @@ -25,6 +25,7 @@ import jalview.bin.Cache; import jalview.commands.EditCommand; import jalview.commands.EditCommand.Action; import jalview.commands.EditCommand.Edit; +import jalview.datamodel.AlignmentAnnotation; import jalview.datamodel.AlignmentI; import jalview.datamodel.ColumnSelection; import jalview.datamodel.HiddenColumns; @@ -76,6 +77,82 @@ public class SeqPanel extends JPanel implements MouseListener, MouseMotionListener, MouseWheelListener, SequenceListener, SelectionListener { + /* + * a class that holds computed mouse position + * - column of the alignment (0...) + * - sequence offset (0...) + * - annotation row offset (0...) + * where annotation offset is -1 unless the alignment is shown + * in wrapped mode, annotations are shown, and the mouse is + * over an annnotation row + */ + static class MousePos + { + /* + * alignment column position of cursor (0...) + */ + final int column; + + /* + * index in alignment of sequence under cursor, + * or nearest above if cursor is not over a sequence + */ + final int seqIndex; + + /* + * index in annotations array of annotation under the cursor + * (only possible in wrapped mode with annotations shown), + * or -1 if cursor is not over an annotation row + */ + final int annotationIndex; + + MousePos(int col, int seq, int ann) + { + column = col; + seqIndex = seq; + annotationIndex = ann; + } + + boolean isOverAnnotation() + { + return annotationIndex != -1; + } + + @Override + public boolean equals(Object obj) + { + if (obj == null || !(obj instanceof MousePos)) + { + return false; + } + MousePos o = (MousePos) obj; + boolean b = (column == o.column && seqIndex == o.seqIndex + && annotationIndex == o.annotationIndex); + // System.out.println(obj + (b ? "= " : "!= ") + this); + return b; + } + + /** + * A simple hashCode that ensures that instances that satisfy equals() have + * the same hashCode + */ + @Override + public int hashCode() + { + return column + seqIndex + annotationIndex; + } + + /** + * toString method for debug output purposes only + */ + @Override + public String toString() + { + return String.format("c%d:s%d:a%d", column, seqIndex, + annotationIndex); + } + } + private static final int MAX_TOOLTIP_LENGTH = 300; public SeqCanvas seqCanvas; @@ -83,18 +160,13 @@ public class SeqPanel extends JPanel public AlignmentPanel ap; /* - * last column position for mouseMoved event + * last position for mouseMoved event */ - private int lastMouseColumn; + private MousePos lastMousePosition; - /* - * last sequence offset for mouseMoved event - */ - private int lastMouseSeq; + protected int editLastRes; - protected int lastres; - - protected int startseq; + protected int editStartSeq; protected AlignViewport av; @@ -176,9 +248,6 @@ public class SeqPanel extends JPanel ssm.addStructureViewerListener(this); ssm.addSelectionListener(this); } - - lastMouseColumn = -1; - lastMouseSeq = -1; } int startWrapBlock = -1; @@ -186,6 +255,71 @@ public class SeqPanel extends JPanel int wrappedBlock = -1; /** + * Computes the column and sequence row (and possibly annotation row when in + * wrapped mode) for the given mouse position + * + * @param evt + * @return + */ + MousePos findMousePosition(MouseEvent evt) + { + int col = findColumn(evt); + int seqIndex = -1; + int annIndex = -1; + int y = evt.getY(); + + int charHeight = av.getCharHeight(); + int alignmentHeight = av.getAlignment().getHeight(); + if (av.getWrapAlignment()) + { + seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(), + seqCanvas.getHeight()); + + /* + * yPos modulo height of repeating width + */ + int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx; + + /* + * height of sequences plus space / scale above, + * plus gap between sequences and annotations + */ + int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment + + alignmentHeight * charHeight + + SeqCanvas.SEQS_ANNOTATION_GAP; + if (yOffsetPx >= alignmentHeightPixels) + { + /* + * mouse is over annotations; find annotation index, also set + * last sequence above (for backwards compatible behaviour) + */ + AlignmentAnnotation[] anns = av.getAlignment() + .getAlignmentAnnotation(); + int rowOffsetPx = yOffsetPx - alignmentHeightPixels; + annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns); + seqIndex = alignmentHeight - 1; + } + else + { + /* + * mouse is over sequence (or the space above sequences) + */ + yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment; + if (yOffsetPx >= 0) + { + seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1); + } + } + } + else + { + seqIndex = Math.min((y / charHeight) + av.getRanges().getStartSeq(), + alignmentHeight - 1); + } + + return new MousePos(col, seqIndex, annIndex); + } + /** * Returns the aligned sequence position (base 0) at the mouse position, or * the closest visible one * @@ -197,10 +331,11 @@ public class SeqPanel extends JPanel int res = 0; int x = evt.getX(); - int startRes = av.getRanges().getStartRes(); + final int startRes = av.getRanges().getStartRes(); + final int charWidth = av.getCharWidth(); + if (av.getWrapAlignment()) { - int hgap = av.getCharHeight(); if (av.getScaleAboveWrapped()) { @@ -212,35 +347,40 @@ public class SeqPanel extends JPanel int y = evt.getY(); y = Math.max(0, y - hgap); - x = Math.max(0, x - seqCanvas.getLabelWidthWest()); + x -= seqCanvas.getLabelWidthWest(); + if (x < 0) + { + // mouse is over left scale + return -1; + } int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth()); if (cwidth < 1) { return 0; } + if (x >= cwidth * charWidth) + { + // mouse is over right scale + return -1; + } wrappedBlock = y / cHeight; wrappedBlock += startRes / cwidth; // allow for wrapped view scrolled right (possible from Overview) int startOffset = startRes % cwidth; res = wrappedBlock * cwidth + startOffset - + +Math.min(cwidth - 1, x / av.getCharWidth()); + + Math.min(cwidth - 1, x / charWidth); } else { - if (x > seqCanvas.getX() + seqCanvas.getWidth()) - { - // make sure we calculate relative to visible alignment, rather than - // right-hand gutter - x = seqCanvas.getX() + seqCanvas.getWidth(); - } - res = (x / av.getCharWidth()) + startRes; - if (res > av.getRanges().getEndRes()) - { - // moused off right - res = av.getRanges().getEndRes(); - } + /* + * make sure we calculate relative to visible alignment, + * rather than right-hand gutter + */ + x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth()); + res = (x / charWidth) + startRes; + res = Math.min(res, av.getRanges().getEndRes()); } if (av.hasHiddenColumns()) @@ -250,38 +390,6 @@ public class SeqPanel extends JPanel } return res; - - } - - int findSeq(MouseEvent evt) - { - int seq = 0; - int y = evt.getY(); - - if (av.getWrapAlignment()) - { - int hgap = av.getCharHeight(); - if (av.getScaleAboveWrapped()) - { - hgap += av.getCharHeight(); - } - - int cHeight = av.getAlignment().getHeight() * av.getCharHeight() - + hgap + seqCanvas.getAnnotationHeight(); - - y -= hgap; - - seq = Math.min((y % cHeight) / av.getCharHeight(), - av.getAlignment().getHeight() - 1); - } - else - { - seq = Math.min( - (y / av.getCharHeight()) + av.getRanges().getStartSeq(), - av.getAlignment().getHeight() - 1); - } - - return seq; } /** @@ -303,8 +411,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 +640,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 +649,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 +659,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(); } @@ -617,13 +725,19 @@ public class SeqPanel extends JPanel @Override public void mouseReleased(MouseEvent evt) { + MousePos pos = findMousePosition(evt); + if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1) + { + return; + } + boolean didDrag = mouseDragging; // did we come here after a drag mouseDragging = false; mouseWheelPressed = false; if (evt.isPopupTrigger()) // Windows: mouseReleased { - showPopupMenu(evt); + showPopupMenu(evt, pos); evt.consume(); return; } @@ -647,6 +761,11 @@ public class SeqPanel extends JPanel public void mousePressed(MouseEvent evt) { lastMousePress = evt.getPoint(); + MousePos pos = findMousePosition(evt); + if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1) + { + return; + } if (SwingUtilities.isMiddleMouseButton(evt)) { @@ -665,28 +784,23 @@ public class SeqPanel extends JPanel } else { - doMousePressedDefineMode(evt); + doMousePressedDefineMode(evt, pos); return; } - int seq = findSeq(evt); - int res = findColumn(evt); - - if (seq < 0 || res < 0) - { - return; - } + int seq = pos.seqIndex; + int res = pos.column; 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; @@ -731,7 +845,7 @@ public class SeqPanel extends JPanel // over residue to change abruptly, causing highlighted residue in panel 2 // to change, causing a scroll in panel 1 etc) ap.setToScrollComplementPanel(false); - wasScrolled = ap.scrollToPosition(results, false); + wasScrolled = ap.scrollToPosition(results); if (wasScrolled) { seqCanvas.revalidate(); @@ -777,23 +891,32 @@ public class SeqPanel extends JPanel mouseDragged(evt); } - final int column = findColumn(evt); - final int seq = findSeq(evt); + final MousePos mousePos = findMousePosition(evt); + if (mousePos.equals(lastMousePosition)) + { + /* + * just a pixel move without change of 'cell' + */ + return; + } + lastMousePosition = mousePos; - if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight()) + if (mousePos.isOverAnnotation()) { - lastMouseSeq = -1; + mouseMovedOverAnnotation(mousePos); return; } - if (column == lastMouseColumn && seq == lastMouseSeq) + final int seq = mousePos.seqIndex; + + final int column = mousePos.column; + if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight()) { - /* - * just a pixel move without change of residue - */ + lastMousePosition = null; + setToolTipText(null); + lastTooltip = null; + ap.alignFrame.setStatus(""); return; } - lastMouseColumn = column; - lastMouseSeq = seq; SequenceI sequence = av.getAlignment().getSequenceAt(seq); @@ -871,6 +994,35 @@ public class SeqPanel extends JPanel } } + /** + * When the view is in wrapped mode, and the mouse is over an annotation row, + * shows the corresponding tooltip and status message (if any) + * + * @param pos + * @param column + */ + protected void mouseMovedOverAnnotation(MousePos pos) + { + final int column = pos.column; + final int rowIndex = pos.annotationIndex; + + if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation() + || rowIndex < 0) + { + return; + } + AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation(); + + String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column, + anns); + setToolTipText(tooltip); + lastTooltip = tooltip; + + String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column, + anns[rowIndex]); + ap.alignFrame.setStatus(msg); + } + private Point lastp = null; /* @@ -881,20 +1033,26 @@ public class SeqPanel extends JPanel @Override public Point getToolTipLocation(MouseEvent event) { - int x = event.getX(), w = getWidth(); - int wdth = (w - x < 200) ? -(w / 2) : 5; // switch sides when tooltip is too - // close to edge + if (tooltipText == null || tooltipText.length() <= 6) + { + lastp = null; + return null; + } + + int x = event.getX(); + int w = getWidth(); + // switch sides when tooltip is too close to edge + int wdth = (w - x < 200) ? -(w / 2) : 5; Point p = lastp; if (!event.isShiftDown() || p == null) { - p = (tooltipText != null && tooltipText.length() > 6) - ? new Point(event.getX() + wdth, event.getY() - 20) - : null; + p = new Point(event.getX() + wdth, event.getY() - 20); + lastp = p; } /* - * TODO: try to modify position region is not obcured by tooltip + * TODO: try to set position so region is not obscured by tooltip */ - return lastp = p; + return p; } String lastTooltip; @@ -997,7 +1155,7 @@ public class SeqPanel extends JPanel text.append(" (").append(Integer.toString(residuePos)).append(")"); } - ap.alignFrame.statusBar.setText(text.toString()); + ap.alignFrame.setStatus(text.toString()); } /** @@ -1039,6 +1197,12 @@ public class SeqPanel extends JPanel @Override public void mouseDragged(MouseEvent evt) { + MousePos pos = findMousePosition(evt); + if (pos.isOverAnnotation() || pos.column == -1) + { + return; + } + if (mouseWheelPressed) { boolean inSplitFrame = ap.av.getCodingComplement() != null; @@ -1134,23 +1298,23 @@ public class SeqPanel extends JPanel if (!editingSeqs) { - doMouseDraggedDefineMode(evt); + dragStretchGroup(evt); return; } - int res = findColumn(evt); + int res = pos.column; if (res < 0) { 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 +1331,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). + *
+ * 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). + *
+ * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in + * the current selection group. + *
+ * 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 this.editLastRes
(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,34 +1376,60 @@ public class SeqPanel extends JPanel
}
}
+ StringBuilder message = new StringBuilder(64); // for status bar
+
+ /*
+ * make a name for the edit action, for
+ * status bar message and Undo/Redo menu
+ */
+ String label = null;
+ if (groupEditing)
+ {
+ message.append("Edit group:");
+ label = MessageManager.getString("action.edit_group");
+ }
+ else
+ {
+ message.append("Edit sequence: " + seq.getName());
+ label = seq.getName();
+ if (label.length() > 10)
+ {
+ 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)
{
- if (groupEditing)
- {
- editCommand = new EditCommand(
- MessageManager.getString("action.edit_group"));
- }
- else
- {
- String label = seq.getName();
- if (label.length() > 10)
- {
- label = label.substring(0, 10);
- }
- editCommand = new EditCommand(MessageManager
- .formatMessage("label.edit_params", new String[]
- { label }));
- }
+ editCommand = new EditCommand(label);
+ }
+
+ if (insertGap)
+ {
+ message.append(" insert ");
+ }
+ else
+ {
+ message.append(" delete ");
}
+ message.append(Math.abs(startres - editLastRes) + " gaps.");
+ ap.alignFrame.setStatus(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;
@@ -1234,10 +1448,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;
@@ -1263,8 +1477,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;
@@ -1285,9 +1499,56 @@ 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.setStatus(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)
{
- ap.alignFrame.statusBar.setText(" "); // defer this as difficult!
List