*/
package jalview.gui;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+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;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JToolTip;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+import javax.swing.ToolTipManager;
+
import jalview.api.AlignViewportI;
-import jalview.bin.Cache;
+import jalview.bin.Console;
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;
+import jalview.datamodel.MappedFeatures;
import jalview.datamodel.SearchResultMatchI;
import jalview.datamodel.SearchResults;
import jalview.datamodel.SearchResultsI;
import jalview.util.MessageManager;
import jalview.util.Platform;
import jalview.viewmodel.AlignmentViewport;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Font;
-import java.awt.FontMetrics;
-import java.awt.Point;
-import java.awt.event.MouseEvent;
-import java.awt.event.MouseListener;
-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;
-
-import javax.swing.JPanel;
-import javax.swing.SwingUtilities;
-import javax.swing.ToolTipManager;
+import jalview.viewmodel.ViewportRanges;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
/**
* DOCUMENT ME!
public class SeqPanel extends JPanel
implements MouseListener, MouseMotionListener, MouseWheelListener,
SequenceListener, SelectionListener
-
{
- /** DOCUMENT ME!! */
+ /*
+ * 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);
+ // jalview.bin.Console.outPrintln(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;
- /** DOCUMENT ME!! */
public AlignmentPanel ap;
/*
- * last column position for mouseMoved event
- */
- private int lastMouseColumn;
-
- /*
- * last sequence offset for mouseMoved event
+ * last position for mouseMoved event
*/
- private int lastMouseSeq;
+ private MousePos lastMousePosition;
- protected int lastres;
+ protected int editLastRes;
- protected int startseq;
+ protected int editStartSeq;
protected AlignViewport av;
StringBuffer keyboardNo2;
- java.net.URL linkImageURL;
-
private final SequenceAnnotationReport seqARep;
- StringBuilder tooltipText = new StringBuilder();
+ /*
+ * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
+ * - the tooltip is not set again if unchanged
+ * - this is the tooltip text _before_ formatting as html
+ */
+ private String lastTooltip;
- String tmpString;
+ /*
+ * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
+ * - used to decide where to place the tooltip in getTooltipLocation()
+ * - this is the tooltip text _after_ formatting as html
+ */
+ private String lastFormattedTooltip;
EditCommand editCommand;
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());
+ seqARep = new SequenceAnnotationReport(true);
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);
}
-
- lastMouseColumn = -1;
- lastMouseSeq = -1;
}
int startWrapBlock = -1;
int wrappedBlock = -1;
/**
+ * Computes the column and sequence row (and possibly annotation row when in
+ * wrapped mode) for the given mouse position
+ * <p>
+ * Mouse position is not set if in wrapped mode with the cursor either between
+ * sequences, or over the left or right vertical scale.
+ *
+ * @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
+ {
+ ViewportRanges ranges = av.getRanges();
+ seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
+ alignmentHeight - 1);
+ seqIndex = Math.min(seqIndex, ranges.getEndSeq());
+ }
+
+ return new MousePos(col, seqIndex, annIndex);
+ }
+
+ /**
* Returns the aligned sequence position (base 0) at the mouse position, or
* the closest visible one
+ * <p>
+ * Returns -1 if in wrapped mode with the mouse over either left or right
+ * vertical scale.
*
* @param evt
* @return
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())
{
+ hgap + seqCanvas.getAnnotationHeight();
int y = evt.getY();
- y -= hgap;
- x = Math.max(0, x - seqCanvas.labelWidthWest);
+ y = Math.max(0, y - hgap);
+ 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())
{
res = av.getAlignment().getHiddenColumns()
- .adjustForHiddenColumns(res);
+ .visibleToAbsoluteColumn(res);
}
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;
}
/**
/*
* Tidy up come what may...
*/
- startseq = -1;
- lastres = -1;
+ editStartSeq = -1;
+ editLastRes = -1;
editingSeqs = false;
groupEditing = false;
keyboardNo1 = null;
void setCursorRow()
{
seqCanvas.cursorY = getKeyboardNo1() - 1;
- scrollToVisible();
+ scrollToVisible(true);
}
void setCursorColumn()
{
seqCanvas.cursorX = getKeyboardNo1() - 1;
- scrollToVisible();
+ scrollToVisible(true);
}
void setCursorRowAndColumn()
{
seqCanvas.cursorX = getKeyboardNo1() - 1;
seqCanvas.cursorY = getKeyboardNo2() - 1;
- scrollToVisible();
+ scrollToVisible(true);
}
}
SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
- scrollToVisible();
+ scrollToVisible(true);
}
void moveCursor(int dx, int dy)
{
- seqCanvas.cursorX += dx;
- seqCanvas.cursorY += dy;
+ moveCursor(dx, dy, false);
+ }
+ void moveCursor(int dx, int dy, boolean nextWord)
+ {
HiddenColumns hidden = av.getAlignment().getHiddenColumns();
- if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
+ if (nextWord)
{
- int original = seqCanvas.cursorX - dx;
int maxWidth = av.getAlignment().getWidth();
+ int maxHeight = av.getAlignment().getHeight();
+ SequenceI seqAtRow = av.getAlignment()
+ .getSequenceAt(seqCanvas.cursorY);
+ // look for next gap or residue
+ boolean isGap = Comparison
+ .isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
+ int p = seqCanvas.cursorX, lastP, r = seqCanvas.cursorY, lastR;
+ do
+ {
+ lastP = p;
+ lastR = r;
+ if (dy != 0)
+ {
+ r += dy;
+ if (r < 0)
+ {
+ r = 0;
+ }
+ if (r >= maxHeight)
+ {
+ r = maxHeight - 1;
+ }
+ seqAtRow = av.getAlignment().getSequenceAt(r);
+ }
+ p = nextVisible(hidden, maxWidth, p, dx);
+ } while ((dx != 0 ? p != lastP : r != lastR)
+ && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
+ seqCanvas.cursorX = p;
+ seqCanvas.cursorY = r;
+ }
+ else
+ {
+ int maxWidth = av.getAlignment().getWidth();
+ seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX,
+ dx);
+ seqCanvas.cursorY += dy;
+ }
+ scrollToVisible(false);
+ }
- while (!hidden.isVisible(seqCanvas.cursorX)
- && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0)
- {
- seqCanvas.cursorX += dx;
- }
+ private int nextVisible(HiddenColumns hidden, int maxWidth, int original,
+ int dx)
+ {
+ int newCursorX = original + dx;
+ if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
+ {
+ int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
+ int[] region = hidden.getRegionWithEdgeAtRes(visx);
- if (seqCanvas.cursorX >= maxWidth
- || !hidden.isVisible(seqCanvas.cursorX))
+ if (region != null) // just in case
{
- seqCanvas.cursorX = original;
+ if (dx == 1)
+ {
+ // moving right
+ newCursorX = region[1] + 1;
+ }
+ else if (dx == -1)
+ {
+ // moving left
+ newCursorX = region[0] - 1;
+ }
}
}
-
- scrollToVisible();
+ newCursorX = (newCursorX < 0) ? 0 : newCursorX;
+ if (newCursorX >= maxWidth || !hidden.isVisible(newCursorX))
+ {
+ newCursorX = original;
+ }
+ return newCursorX;
}
- void scrollToVisible()
+ /**
+ * Scroll to make the cursor visible in the viewport.
+ *
+ * @param jump
+ * just jump to the location rather than scrolling
+ */
+ void scrollToVisible(boolean jump)
{
if (seqCanvas.cursorX < 0)
{
}
endEditing();
- if (av.getWrapAlignment())
+
+ boolean repaintNeeded = true;
+ if (jump)
{
- av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
+ // only need to repaint if the viewport did not move, as otherwise it will
+ // get a repaint
+ repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
+ seqCanvas.cursorY);
}
else
{
- av.getRanges().scrollToVisible(seqCanvas.cursorX, seqCanvas.cursorY);
+ if (av.getWrapAlignment())
+ {
+ // scrollToWrappedVisible expects x-value to have hidden cols subtracted
+ int x = av.getAlignment().getHiddenColumns()
+ .absoluteToVisibleColumn(seqCanvas.cursorX);
+ av.getRanges().scrollToWrappedVisible(x);
+ }
+ else
+ {
+ av.getRanges().scrollToVisible(seqCanvas.cursorX,
+ seqCanvas.cursorY);
+ }
}
- setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
- seqCanvas.cursorX, seqCanvas.cursorY);
- seqCanvas.repaint();
+ if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
+ {
+ setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
+ seqCanvas.cursorX, seqCanvas.cursorY);
+ }
+
+ if (repaintNeeded)
+ {
+ seqCanvas.repaint();
+ }
}
void setSelectionAreaAtCursor(boolean topLeft)
av.setSelectionGroup(sg);
}
- ap.paintAlignment(false);
+ ap.paintAlignment(false, false);
av.sendSelection();
}
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();
}
@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;
}
- if (!editingSeqs)
+ if (editingSeqs)
+ {
+ endEditing();
+ }
+ else
{
doMouseReleasedDefineMode(evt, didDrag);
- return;
}
-
- endEditing();
}
/**
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))
{
}
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;
if (lastMessage == null || !lastMessage.equals(tmp))
{
- // System.err.println("mouseOver Sequence: "+tmp);
+ // jalview.bin.Console.errPrintln("mouseOver Sequence: "+tmp);
ssm.mouseOverSequence(sequence, index, pos, av);
}
lastMessage = tmp;
* the start of the highlighted region.
*/
@Override
- public void highlightSequence(SearchResultsI results)
+ public String highlightSequence(SearchResultsI results)
{
if (results == null || results.equals(lastSearchResults))
{
- return;
+ return null;
}
lastSearchResults = results;
// 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();
ap.setToScrollComplementPanel(true);
}
- boolean noFastPaint = wasScrolled && av.getWrapAlignment();
- if (seqCanvas.highlightSearchResults(results, noFastPaint))
+ boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
+ if (seqCanvas.highlightSearchResults(results, fastPaint))
{
setStatusMessage(results);
}
+ return results.isEmpty() ? null : getHighlightInfo(results);
+ }
+
+ /**
+ * temporary hack: answers a message suitable to show on structure hover
+ * label. This is normally null. It is a peptide variation description if
+ * <ul>
+ * <li>results are a single residue in a protein alignment</li>
+ * <li>there is a mapping to a coding sequence (codon)</li>
+ * <li>there are one or more SNP variant features on the codon</li>
+ * </ul>
+ * in which case the answer is of the format (e.g.) "p.Glu388Asp"
+ *
+ * @param results
+ * @return
+ */
+ private String getHighlightInfo(SearchResultsI results)
+ {
+ /*
+ * ideally, just find mapped CDS (as we don't care about render style here);
+ * for now, go via split frame complement's FeatureRenderer
+ */
+ AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
+ if (complement == null)
+ {
+ return null;
+ }
+ AlignFrame af = Desktop.getAlignFrameFor(complement);
+ FeatureRendererModel fr2 = af.getFeatureRenderer();
+
+ List<SearchResultMatchI> matches = results.getResults();
+ int j = matches.size();
+ List<String> infos = new ArrayList<>();
+ for (int i = 0; i < j; i++)
+ {
+ SearchResultMatchI match = matches.get(i);
+ int pos = match.getStart();
+ if (pos == match.getEnd())
+ {
+ SequenceI seq = match.getSequence();
+ SequenceI ds = seq.getDatasetSequence() == null ? seq
+ : seq.getDatasetSequence();
+ MappedFeatures mf = fr2.findComplementFeaturesAtResidue(ds, pos);
+ if (mf != null)
+ {
+ for (SequenceFeature sf : mf.features)
+ {
+ String pv = mf.findProteinVariants(sf);
+ if (pv.length() > 0 && !infos.contains(pv))
+ {
+ infos.add(pv);
+ }
+ }
+ }
+ }
+ }
+
+ if (infos.isEmpty())
+ {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (String info : infos)
+ {
+ if (sb.length() > 0)
+ {
+ sb.append("|");
+ }
+ sb.append(info);
+ }
+ return sb.toString();
}
@Override
@Override
public void updateColours(SequenceI seq, int index)
{
- System.out.println("update the seqPanel colours");
+ jalview.bin.Console.outPrintln("update the seqPanel colours");
// repaint();
}
/**
- * DOCUMENT ME!
+ * Action on mouse movement is to update the status bar to show the current
+ * sequence position, and (if features are shown) to show any features at the
+ * position in a tooltip. Does nothing if the mouse move does not change
+ * residue position.
*
* @param evt
- * DOCUMENT ME!
*/
@Override
public void mouseMoved(MouseEvent evt)
mouseDragged(evt);
}
- final int column = findColumn(evt);
- int seq = findSeq(evt);
- if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
+ final MousePos mousePos = findMousePosition(evt);
+ if (mousePos.equals(lastMousePosition))
+ {
+ /*
+ * just a pixel move without change of 'cell'
+ */
+ moveTooltip = false;
+ return;
+ }
+ moveTooltip = true;
+ lastMousePosition = mousePos;
+
+ 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;
+ lastFormattedTooltip = null;
+ ap.alignFrame.setStatus("");
return;
}
- lastMouseColumn = column;
- lastMouseSeq = seq;
SequenceI sequence = av.getAlignment().getSequenceAt(seq);
mouseOverSequence(sequence, column, pos);
}
- tooltipText.setLength(6); // Cuts the buffer back to <html>
+ StringBuilder tooltipText = new StringBuilder(64);
SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
if (groups != null)
* add features that straddle the gap (pos may be the residue before or
* after the gap)
*/
+ int unshownFeatures = 0;
if (av.isShowSequenceFeatures())
{
List<SequenceFeature> features = ap.getFeatureRenderer()
.findFeaturesAtColumn(sequence, column + 1);
- seqARep.appendFeatures(tooltipText, pos, features,
- this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
+ unshownFeatures = seqARep.appendFeatures(tooltipText, pos, features,
+ this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
+
+ /*
+ * add features in CDS/protein complement at the corresponding
+ * position if configured to do so
+ */
+ if (av.isShowComplementFeatures())
+ {
+ if (!Comparison.isGap(sequence.getCharAt(column)))
+ {
+ AlignViewportI complement = ap.getAlignViewport()
+ .getCodingComplement();
+ AlignFrame af = Desktop.getAlignFrameFor(complement);
+ FeatureRendererModel fr2 = af.getFeatureRenderer();
+ MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
+ pos);
+ if (mf != null)
+ {
+ unshownFeatures += seqARep.appendFeatures(tooltipText, pos, mf,
+ fr2, MAX_TOOLTIP_LENGTH);
+ }
+ }
+ }
}
- if (tooltipText.length() == 6) // <html>
+ if (tooltipText.length() == 0) // nothing added
{
setToolTipText(null);
lastTooltip = null;
}
else
{
+ if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
+ {
+ tooltipText.setLength(MAX_TOOLTIP_LENGTH);
+ tooltipText.append("...");
+ }
+ if (unshownFeatures > 0)
+ {
+ tooltipText.append("<br/>").append("... ").append("<i>")
+ .append(MessageManager.formatMessage(
+ "label.features_not_shown", unshownFeatures))
+ .append("</i>");
+ }
String textString = tooltipText.toString();
- if (lastTooltip == null || !lastTooltip.equals(textString))
+ if (!textString.equals(lastTooltip))
{
- String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
- textString);
- setToolTipText(formattedTooltipText);
lastTooltip = textString;
+ lastFormattedTooltip = JvSwingUtils.wrapTooltip(true, textString);
+ setToolTipText(lastFormattedTooltip);
}
}
}
- private Point lastp = null;
+ /**
+ * 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;
+
+ // TODO - get yOffset for annotation, too
+ if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
+ || rowIndex < 0)
+ {
+ return;
+ }
+ AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
+
+ String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
+ anns, 0, av, ap);
+ if (tooltip == null ? tooltip != lastTooltip
+ : !tooltip.equals(lastTooltip))
+ {
+ lastTooltip = tooltip;
+ lastFormattedTooltip = tooltip == null ? null
+ : JvSwingUtils.wrapTooltip(true, tooltip);
+ setToolTipText(lastFormattedTooltip);
+ }
+
+ String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
+ anns[rowIndex], 0, av);
+ ap.alignFrame.setStatus(msg);
+ }
+
+ /*
+ * if Shift key is held down while moving the mouse,
+ * the tooltip location is not changed once shown
+ */
+ private Point lastTooltipLocation = null;
+
+ /*
+ * this flag is false for pixel moves within a residue,
+ * to reduce tooltip flicker
+ */
+ private boolean moveTooltip = true;
+
+ /*
+ * a dummy tooltip used to estimate where to position tooltips
+ */
+ private JToolTip tempTip = new JLabel().createToolTip();
/*
* (non-Javadoc)
@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
- Point p = lastp;
- if (!event.isShiftDown() || p == null)
+ // BH 2018
+
+ if (lastTooltip == null || !moveTooltip)
{
- p = (tooltipText != null && tooltipText.length() > 6)
- ? new Point(event.getX() + wdth, event.getY() - 20)
- : null;
+ return null;
+ }
+
+ if (lastTooltipLocation != null && event.isShiftDown())
+ {
+ return lastTooltipLocation;
}
- /*
- * TODO: try to modify position region is not obcured by tooltip
- */
- return lastp = p;
- }
- String lastTooltip;
+ int x = event.getX();
+ int y = event.getY();
+ int w = getWidth();
+
+ tempTip.setTipText(lastFormattedTooltip);
+ int tipWidth = (int) tempTip.getPreferredSize().getWidth();
+
+ // was x += (w - x < 200) ? -(w / 2) : 5;
+ x = (x + tipWidth < w ? x + 10 : w - tipWidth);
+ Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
+
+ return lastTooltipLocation = p;
+ }
/**
* set when the current UI interaction has resulted in a change that requires
- * overview shading to be recalculated. this could be changed to something
- * more expressive that indicates what actually has changed, so selective
- * redraws can be applied
+ * shading in overviews and structures to be recalculated. this could be
+ * changed to a something more expressive that indicates what actually has
+ * changed, so selective redraws can be applied (ie. only structures, only
+ * overview, etc)
*/
- private boolean needOverviewUpdate = false; // TODO: refactor to avcontroller
+ private boolean updateOverviewAndStructs = false; // TODO: refactor to
+ // avcontroller
/**
* set if av.getSelectionGroup() refers to a group that is defined on the
{
char sequenceChar = sequence.getCharAt(column);
int pos = sequence.findPosition(column);
- setStatusMessage(sequence, seqIndex, sequenceChar, pos);
+ setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
return pos;
}
* Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
* </pre>
*
- * @param sequence
+ * @param seqName
* @param seqIndex
* sequence position in the alignment (1..)
* @param sequenceChar
* @param residuePos
* the sequence residue position (if not over a gap)
*/
- protected void setStatusMessage(SequenceI sequence, int seqIndex,
+ protected void setStatusMessage(String seqName, int seqIndex,
char sequenceChar, int residuePos)
{
StringBuilder text = new StringBuilder(32);
* Sequence number (if known), and sequence name.
*/
String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
- text.append("Sequence").append(seqno).append(" ID: ")
- .append(sequence.getName());
+ text.append("Sequence").append(seqno).append(" ID: ").append(seqName);
String residue = null;
text.append(" (").append(Integer.toString(residuePos)).append(")");
}
- ap.alignFrame.statusBar.setText(text.toString());
+ ap.alignFrame.setStatus(text.toString());
}
/**
{
return;
}
- SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
+ SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
+ SequenceI ds = alignedSeq.getDatasetSequence();
for (SearchResultMatchI m : results.getResults())
{
SequenceI seq = m.getSequence();
if (seq == ds)
{
int start = m.getStart();
- setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
- start);
+ setStatusMessage(alignedSeq.getName(), sequenceIndex,
+ seq.getCharAt(start - 1), start);
return;
}
}
@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;
}
if (newWidth > 0)
{
- ap.paintAlignment(false);
+ ap.paintAlignment(false, false);
if (copyChanges)
{
/*
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);
mouseDragging = true;
if (scrollThread != null)
{
- scrollThread.setEvent(evt);
+ scrollThread.setMousePosition(evt.getPoint());
}
}
- // 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);
+ 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:");
- 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 });
+ }
+
+ /*
+ * initialise the edit command if there is not
+ * already one being extended
+ */
+ if (editCommand == null)
+ {
+ editCommand = new EditCommand(label);
}
if (insertGap)
message.append(" delete ");
}
- message.append(Math.abs(startres - lastres) + " gaps.");
- ap.alignFrame.statusBar.setText(message.toString());
+ 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;
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;
{
fixedColumns = true;
int y1 = av.getAlignment().getHiddenColumns()
- .getHiddenBoundaryLeft(startres);
+ .getNextHiddenBoundary(true, startres);
int y2 = av.getAlignment().getHiddenColumns()
- .getHiddenBoundaryRight(startres);
+ .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.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)
{
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 (sg.getSize() == av.getAlignment().getHeight())
{
- if ((av.hasHiddenColumns() && startres < av.getAlignment()
- .getHiddenColumns().getHiddenBoundaryRight(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;
+ }
+
+ /**
+ * 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));
}
- 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);
}
/**
- * DOCUMENT ME!
+ * On reentering the panel, stops any scrolling that was started on dragging
+ * out of the panel
*
* @param e
- * DOCUMENT ME!
*/
@Override
public void mouseEntered(MouseEvent e)
{
oldSeq = 0;
}
-
- if (scrollThread != null)
- {
- scrollThread.running = false;
- scrollThread = null;
- }
+ stopScrolling();
}
/**
- * DOCUMENT ME!
+ * On leaving the panel, if the mouse is being dragged, starts a thread to
+ * scroll it until the mouse is released (in unwrapped mode only)
*
* @param e
- * DOCUMENT ME!
*/
@Override
public void mouseExited(MouseEvent e)
{
+ lastMousePosition = null;
+ ap.alignFrame.setStatus(" ");
if (av.getWrapAlignment())
{
return;
}
- if (mouseDragging)
+ if (mouseDragging && scrollThread == null)
{
- scrollThread = new ScrollThread();
+ startScrolling(e.getPoint());
}
}
public void mouseClicked(MouseEvent evt)
{
SequenceGroup sg = null;
- SequenceI sequence = av.getAlignment().getSequenceAt(findSeq(evt));
- if (evt.getClickCount() > 1)
+ MousePos pos = findMousePosition(evt);
+ if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
+ {
+ return;
+ }
+
+ if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
{
sg = av.getSelectionGroup();
if (sg != null && sg.getSize() == 1
av.setSelectionGroup(null);
}
- int column = findColumn(evt);
+ int column = pos.column;
/*
* find features at the position (if not gapped), or straddling
* the position (if at a gap)
*/
+ SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
.findFeaturesAtColumn(sequence, column + 1);
* highlight the first feature at the position on the alignment
*/
SearchResultsI highlight = new SearchResults();
- highlight.addResult(sequence, features.get(0).getBegin(), features
- .get(0).getEnd());
- seqCanvas.highlightSearchResults(highlight, false);
+ highlight.addResult(sequence, features.get(0).getBegin(),
+ features.get(0).getEnd());
+ seqCanvas.highlightSearchResults(highlight, true);
/*
- * open the Amend Features dialog; clear highlighting afterwards,
- * whether changes were made or not
+ * open the Amend Features dialog
*/
- List<SequenceI> seqs = Collections.singletonList(sequence);
- seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
- ap);
- av.setSearchResults(null); // clear highlighting
- seqCanvas.repaint(); // draw new/amended features
+ new FeatureEditor(ap, Collections.singletonList(sequence), features,
+ false).showDialog();
}
}
}
public void mouseWheelMoved(MouseWheelEvent e)
{
e.consume();
- if (e.getWheelRotation() > 0)
+ double wheelRotation = e.getPreciseWheelRotation();
+ if (wheelRotation > 0)
{
if (e.isShiftDown())
{
av.getRanges().scrollRight(true);
}
- else if (!av.getWrapAlignment())
+ else
{
av.getRanges().scrollUp(false);
}
}
- else
+ else if (wheelRotation < 0)
{
if (e.isShiftDown())
{
av.getRanges().scrollRight(false);
}
- else if (!av.getWrapAlignment())
+ else
{
av.getRanges().scrollUp(true);
}
}
- // TODO Update tooltip for new position.
+
+ /*
+ * update status bar and tooltip for new position
+ * (need to synthesize a mouse movement to refresh tooltip)
+ */
+ mouseMoved(e);
+ ToolTipManager.sharedInstance().mouseMoved(e);
}
/**
* DOCUMENT ME!
*
- * @param evt
+ * @param pos
* DOCUMENT ME!
*/
- public void doMousePressedDefineMode(MouseEvent evt)
+ protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
{
- final int res = findColumn(evt);
- final int seq = findSeq(evt);
- oldSeq = seq;
- needOverviewUpdate = false;
-
- startWrapBlock = wrappedBlock;
-
- if (av.getWrapAlignment() && seq > av.getAlignment().getHeight())
+ if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
{
- JvOptionPane.showInternalMessageDialog(Desktop.desktop,
- MessageManager.getString(
- "label.cannot_edit_annotations_in_wrapped_view"),
- MessageManager.getString("label.wrapped_view_no_edit"),
- JvOptionPane.WARNING_MESSAGE);
return;
}
- if (seq < 0 || res < 0)
- {
- return;
- }
+ final int res = pos.column;
+ final int seq = pos.seqIndex;
+ oldSeq = seq;
+ updateOverviewAndStructs = false;
+
+ startWrapBlock = wrappedBlock;
SequenceI sequence = av.getAlignment().getSequenceAt(seq);
}
}
- if (evt.isPopupTrigger()) // Mac: mousePressed
- {
- showPopupMenu(evt);
- return;
- }
-
/*
* defer right-mouse click handling to mouseReleased on Windows
* (where isPopupTrigger() will answer true)
* NB isRightMouseButton is also true for Cmd-click on Mac
*/
- if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
+ if (Platform.isWinRightButton(evt))
+ {
+ return;
+ }
+
+ if (evt.isPopupTrigger()) // Mac: mousePressed
{
+ showPopupMenu(evt, pos);
return;
}
if (av.cursorMode)
{
- seqCanvas.cursorX = findColumn(evt);
- seqCanvas.cursorY = findSeq(evt);
+ seqCanvas.cursorX = res;
+ seqCanvas.cursorY = seq;
seqCanvas.repaint();
return;
}
if (stretchGroup == null)
{
- // Only if left mouse button do we want to change group sizes
+ createStretchGroup(res, sequence);
+ }
- // define a new group here
- SequenceGroup sg = new SequenceGroup();
- sg.setStartRes(res);
- sg.setEndRes(res);
- sg.addSequence(sequence, false);
- av.setSelectionGroup(sg);
- stretchGroup = sg;
+ if (stretchGroup != null)
+ {
+ stretchGroup.addPropertyChangeListener(seqCanvas);
+ }
- if (av.getConservationSelected())
- {
- SliderPanel.setConservationSlider(ap, av.getResidueShading(),
- ap.getViewName());
- }
+ seqCanvas.repaint();
+ }
- if (av.getAbovePIDThreshold())
- {
- SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
- ap.getViewName());
- }
- // TODO: stretchGroup will always be not null. Is this a merge error ?
- if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
- {
- // Edit end res position of selected group
- changeEndRes = true;
- }
- else if ((stretchGroup != null)
- && (stretchGroup.getStartRes() == res))
- {
- // Edit end res position of selected group
- changeStartRes = true;
- }
- stretchGroup.getWidth();
+ private void createStretchGroup(int res, SequenceI sequence)
+ {
+ // Only if left mouse button do we want to change group sizes
+ // define a new group here
+ SequenceGroup sg = new SequenceGroup();
+ sg.setStartRes(res);
+ sg.setEndRes(res);
+ sg.addSequence(sequence, false);
+ av.setSelectionGroup(sg);
+ stretchGroup = sg;
+
+ if (av.getConservationSelected())
+ {
+ SliderPanel.setConservationSlider(ap, av.getResidueShading(),
+ ap.getViewName());
}
- seqCanvas.repaint();
+ if (av.getAbovePIDThreshold())
+ {
+ SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
+ ap.getViewName());
+ }
+ // TODO: stretchGroup will always be not null. Is this a merge error ?
+ // or is there a threading issue here?
+ if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
+ {
+ // Edit end res position of selected group
+ changeEndRes = true;
+ }
+ else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
+ {
+ // Edit end res position of selected group
+ changeStartRes = true;
+ }
+ stretchGroup.getWidth();
+
}
/**
* Build and show a pop-up menu at the right-click mouse position
- *
+ *
* @param evt
- * @param res
- * @param sequences
+ * @param pos
*/
- void showPopupMenu(MouseEvent evt)
+ void showPopupMenu(MouseEvent evt, MousePos pos)
{
- final int column = findColumn(evt);
- final int seq = findSeq(evt);
+ final int column = pos.column;
+ final int seq = pos.seqIndex;
SequenceI sequence = av.getAlignment().getSequenceAt(seq);
- List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
- .findFeaturesAtColumn(sequence, column + 1);
- List<String> links = new ArrayList<>();
- for (SequenceFeature sf : allFeatures)
+ if (sequence != null)
{
- if (sf.links != null)
- {
- for (String link : sf.links)
- {
- links.add(link);
- }
- }
+ PopupMenu pop = new PopupMenu(ap, sequence, column);
+ pop.show(this, evt.getX(), evt.getY());
}
-
- PopupMenu pop = new PopupMenu(ap, null, links);
- pop.show(this, evt.getX(), evt.getY());
}
/**
* true if this event is happening after a mouse drag (rather than a
* mouse down)
*/
- public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
+ protected void doMouseReleasedDefineMode(MouseEvent evt,
+ boolean afterDrag)
{
if (stretchGroup == null)
{
return;
}
+
+ stretchGroup.removePropertyChangeListener(seqCanvas);
+
// always do this - annotation has own state
// but defer colourscheme update until hidden sequences are passed in
boolean vischange = stretchGroup.recalcConservation(true);
- needOverviewUpdate |= vischange && av.isSelectionDefinedGroup()
+ updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
&& afterDrag;
if (stretchGroup.cs != null)
{
- stretchGroup.cs.alignmentChanged(stretchGroup,
- av.getHiddenRepSequences());
+ if (afterDrag)
+ {
+ stretchGroup.cs.alignmentChanged(stretchGroup,
+ av.getHiddenRepSequences());
+ }
ResidueShaderI groupColourScheme = stretchGroup
.getGroupColourScheme();
}
}
PaintRefresher.Refresh(this, av.getSequenceSetId());
- ap.paintAlignment(needOverviewUpdate);
- needOverviewUpdate = false;
+ // TODO: structure colours only need updating if stretchGroup used to or now
+ // does contain sequences with structure views
+ ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
+ updateOverviewAndStructs = false;
changeEndRes = false;
changeStartRes = false;
stretchGroup = null;
}
/**
- * DOCUMENT ME!
+ * Resizes the borders of a selection group depending on the direction of
+ * mouse drag
*
* @param evt
- * DOCUMENT ME!
*/
- public void doMouseDraggedDefineMode(MouseEvent evt)
+ protected void dragStretchGroup(MouseEvent evt)
{
- int res = findColumn(evt);
- int y = findSeq(evt);
-
- if (wrappedBlock != startWrapBlock)
+ if (stretchGroup == null)
{
return;
}
- if (stretchGroup == null)
+ MousePos pos = findMousePosition(evt);
+ if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
{
return;
}
- if (res >= av.getAlignment().getWidth())
+ int res = pos.column;
+ int y = pos.seqIndex;
+
+ if (wrappedBlock != startWrapBlock)
{
- res = av.getAlignment().getWidth() - 1;
+ return;
}
+ res = Math.min(res, av.getAlignment().getWidth() - 1);
+
if (stretchGroup.getEndRes() == res)
{
// Edit end res position of selected group
if (res > (stretchGroup.getStartRes() - 1))
{
stretchGroup.setEndRes(res);
- needOverviewUpdate |= av.isSelectionDefinedGroup();
+ updateOverviewAndStructs |= av.isSelectionDefinedGroup();
}
}
else if (changeStartRes)
if (res < (stretchGroup.getEndRes() + 1))
{
stretchGroup.setStartRes(res);
- needOverviewUpdate |= av.isSelectionDefinedGroup();
+ updateOverviewAndStructs |= av.isSelectionDefinedGroup();
}
}
if (stretchGroup.getSequences(null).contains(nextSeq))
{
stretchGroup.deleteSequence(seq, false);
- needOverviewUpdate |= av.isSelectionDefinedGroup();
+ updateOverviewAndStructs |= av.isSelectionDefinedGroup();
}
else
{
}
stretchGroup.addSequence(nextSeq, false);
- needOverviewUpdate |= av.isSelectionDefinedGroup();
+ updateOverviewAndStructs |= av.isSelectionDefinedGroup();
}
}
if (scrollThread != null)
{
- scrollThread.setEvent(evt);
+ scrollThread.setMousePosition(evt.getPoint());
}
- seqCanvas.repaint();
+ /*
+ * 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)
+ /**
+ * Stops the scroll thread if it is running
+ */
+ void stopScrolling()
{
- if (evt == null)
+ if (scrollThread != null)
{
- if (scrollThread != null)
- {
- scrollThread.running = false;
- scrollThread = null;
- }
- mouseDragging = false;
+ scrollThread.stopScrolling();
+ scrollThread = null;
}
- else
+ mouseDragging = false;
+ }
+
+ /**
+ * Starts a thread to scroll the alignment, towards a given mouse position
+ * outside the panel bounds, unless the alignment is in wrapped mode
+ *
+ * @param mousePos
+ */
+ void startScrolling(Point mousePos)
+ {
+ /*
+ * set this.mouseDragging in case this was called from
+ * a drag in ScalePanel or AnnotationPanel
+ */
+ mouseDragging = true;
+ if (!av.getWrapAlignment() && scrollThread == null)
{
- if (scrollThread == null)
+ scrollThread = new ScrollThread();
+ scrollThread.setMousePosition(mousePos);
+ if (Platform.isJS())
+ {
+ /*
+ * Javascript - run every 20ms until scrolling stopped
+ * or reaches the limit of scrollable alignment
+ */
+ Timer t = new Timer(20, new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ if (scrollThread != null)
+ {
+ // if (!scrollOnce() {t.stop();}) gives compiler error :-(
+ scrollThread.scrollOnce();
+ }
+ }
+ });
+ t.addActionListener(new ActionListener()
+ {
+ @Override
+ public void actionPerformed(ActionEvent e)
+ {
+ if (scrollThread == null)
+ {
+ // SeqPanel.stopScrolling called
+ t.stop();
+ }
+ }
+ });
+ t.start();
+ }
+ else
{
- scrollThread = new ScrollThread();
+ /*
+ * Java - run in a new thread
+ */
+ scrollThread.start();
}
-
- mouseDragging = true;
- scrollThread.setEvent(evt);
}
-
}
- // this class allows scrolling off the bottom of the visible alignment
+ /**
+ * Performs scrolling of the visible alignment left, right, up or down, until
+ * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
+ * limit of the alignment is reached
+ */
class ScrollThread extends Thread
{
- MouseEvent evt;
+ private Point mousePos;
- boolean running = false;
+ private volatile boolean keepRunning = true;
+ /**
+ * Constructor
+ */
public ScrollThread()
{
- start();
+ setName("SeqPanel$ScrollThread");
}
- public void setEvent(MouseEvent e)
+ /**
+ * Sets the position of the mouse that determines the direction of the
+ * scroll to perform. If this is called as the mouse moves, scrolling should
+ * respond accordingly. For example, if the mouse is dragged right, scroll
+ * right should start; if the drag continues down, scroll down should also
+ * happen.
+ *
+ * @param p
+ */
+ public void setMousePosition(Point p)
{
- evt = e;
+ mousePos = p;
}
+ /**
+ * Sets a flag that will cause the thread to exit
+ */
public void stopScrolling()
{
- running = false;
+ keepRunning = false;
}
+ /**
+ * Scrolls the alignment left or right, and/or up or down, depending on the
+ * last notified mouse position, until the limit of the alignment is
+ * reached, or a flag is set to stop the scroll
+ */
@Override
public void run()
{
- running = true;
-
- while (running)
+ while (keepRunning)
{
- if (evt != null)
+ if (mousePos != null)
{
- if (mouseDragging && (evt.getY() < 0)
- && (av.getRanges().getStartSeq() > 0))
- {
- running = av.getRanges().scrollUp(true);
- }
-
- if (mouseDragging && (evt.getY() >= getHeight()) && (av
- .getAlignment().getHeight() > av.getRanges().getEndSeq()))
- {
- running = av.getRanges().scrollUp(false);
- }
-
- if (mouseDragging && (evt.getX() < 0))
- {
- running = av.getRanges().scrollRight(false);
- }
- else if (mouseDragging && (evt.getX() >= getWidth()))
- {
- running = av.getRanges().scrollRight(true);
- }
+ keepRunning = scrollOnce();
}
-
try
{
Thread.sleep(20);
{
}
}
+ SeqPanel.this.scrollThread = null;
+ }
+
+ /**
+ * Scrolls
+ * <ul>
+ * <li>one row up, if the mouse is above the panel</li>
+ * <li>one row down, if the mouse is below the panel</li>
+ * <li>one column left, if the mouse is left of the panel</li>
+ * <li>one column right, if the mouse is right of the panel</li>
+ * </ul>
+ * Answers true if a scroll was performed, false if not - meaning either
+ * that the mouse position is within the panel, or the edge of the alignment
+ * has been reached.
+ */
+ boolean scrollOnce()
+ {
+ /*
+ * quit after mouseUp ensures interrupt in JalviewJS
+ */
+ if (!mouseDragging)
+ {
+ return false;
+ }
+
+ boolean scrolled = false;
+ ViewportRanges ranges = SeqPanel.this.av.getRanges();
+
+ /*
+ * scroll up or down
+ */
+ if (mousePos.y < 0)
+ {
+ // mouse is above this panel - try scroll up
+ scrolled = ranges.scrollUp(true);
+ }
+ else if (mousePos.y >= getHeight())
+ {
+ // mouse is below this panel - try scroll down
+ scrolled = ranges.scrollUp(false);
+ }
+
+ /*
+ * scroll left or right
+ */
+ if (mousePos.x < 0)
+ {
+ scrolled |= ranges.scrollRight(false);
+ }
+ else if (mousePos.x >= getWidth())
+ {
+ scrolled |= ranges.scrollRight(true);
+ }
+ return scrolled;
}
}
{
if (av.getAlignment() == null)
{
- Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
+ Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
+ " ViewId=" + av.getViewId()
+ " 's alignment is NULL! returning immediately.");
return;
if (copycolsel && av.hasHiddenColumns()
&& (av.getAlignment().getHiddenColumns() == null))
{
- System.err.println("Bad things");
+ jalview.bin.Console.errPrintln("Bad things");
}
if (repaint) // always true!
{
* Map sequence selection
*/
SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
- av.setSelectionGroup(sg);
+ av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
av.isSelectionGroupChanged(true);
/*
HiddenColumns hs = new HiddenColumns();
MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
av.setColumnSelection(cs);
- av.getAlignment().setHiddenColumns(hs);
+ boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
// lastly, update any dependent dialogs
if (ap.getCalculationDialog() != null)
ap.getCalculationDialog().validateCalcTypes();
}
- PaintRefresher.Refresh(this, av.getSequenceSetId());
+ /*
+ * repaint alignment, and also Overview or Structure
+ * if hidden column selection has changed
+ */
+ ap.paintAlignment(hiddenChanged, hiddenChanged);
+ // propagate any selection changes
+ PaintRefresher.Refresh(ap, av.getSequenceSetId());
return true;
}
+
+ /**
+ *
+ * @return null or last search results handled by this panel
+ */
+ public SearchResultsI getLastSearchResults()
+ {
+ return lastSearchResults;
+ }
}