2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import java.awt.BorderLayout;
24 import java.awt.Color;
26 import java.awt.FontMetrics;
27 import java.awt.Point;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.awt.event.MouseEvent;
31 import java.awt.event.MouseListener;
32 import java.awt.event.MouseMotionListener;
33 import java.awt.event.MouseWheelEvent;
34 import java.awt.event.MouseWheelListener;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.List;
39 import javax.swing.JLabel;
40 import javax.swing.JPanel;
41 import javax.swing.JToolTip;
42 import javax.swing.SwingUtilities;
43 import javax.swing.Timer;
44 import javax.swing.ToolTipManager;
46 import jalview.api.AlignViewportI;
47 import jalview.bin.Console;
48 import jalview.commands.EditCommand;
49 import jalview.commands.EditCommand.Action;
50 import jalview.commands.EditCommand.Edit;
51 import jalview.datamodel.AlignmentAnnotation;
52 import jalview.datamodel.AlignmentI;
53 import jalview.datamodel.ColumnSelection;
54 import jalview.datamodel.HiddenColumns;
55 import jalview.datamodel.MappedFeatures;
56 import jalview.datamodel.SearchResultMatchI;
57 import jalview.datamodel.SearchResults;
58 import jalview.datamodel.SearchResultsI;
59 import jalview.datamodel.Sequence;
60 import jalview.datamodel.SequenceFeature;
61 import jalview.datamodel.SequenceGroup;
62 import jalview.datamodel.SequenceI;
63 import jalview.io.SequenceAnnotationReport;
64 import jalview.renderer.ResidueShaderI;
65 import jalview.schemes.ResidueProperties;
66 import jalview.structure.SelectionListener;
67 import jalview.structure.SelectionSource;
68 import jalview.structure.SequenceListener;
69 import jalview.structure.StructureSelectionManager;
70 import jalview.structure.VamsasSource;
71 import jalview.util.Comparison;
72 import jalview.util.MappingUtils;
73 import jalview.util.MessageManager;
74 import jalview.util.Platform;
75 import jalview.viewmodel.AlignmentViewport;
76 import jalview.viewmodel.ViewportRanges;
77 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
83 * @version $Revision: 1.130 $
85 public class SeqPanel extends JPanel
86 implements MouseListener, MouseMotionListener, MouseWheelListener,
87 SequenceListener, SelectionListener
91 * a class that holds computed mouse position
92 * - column of the alignment (0...)
93 * - sequence offset (0...)
94 * - annotation row offset (0...)
95 * where annotation offset is -1 unless the alignment is shown
96 * in wrapped mode, annotations are shown, and the mouse is
97 * over an annnotation row
102 * alignment column position of cursor (0...)
107 * index in alignment of sequence under cursor,
108 * or nearest above if cursor is not over a sequence
113 * index in annotations array of annotation under the cursor
114 * (only possible in wrapped mode with annotations shown),
115 * or -1 if cursor is not over an annotation row
117 final int annotationIndex;
119 MousePos(int col, int seq, int ann)
123 annotationIndex = ann;
126 boolean isOverAnnotation()
128 return annotationIndex != -1;
132 public boolean equals(Object obj)
134 if (obj == null || !(obj instanceof MousePos))
138 MousePos o = (MousePos) obj;
139 boolean b = (column == o.column && seqIndex == o.seqIndex
140 && annotationIndex == o.annotationIndex);
141 // System.out.println(obj + (b ? "= " : "!= ") + this);
146 * A simple hashCode that ensures that instances that satisfy equals() have
150 public int hashCode()
152 return column + seqIndex + annotationIndex;
156 * toString method for debug output purposes only
159 public String toString()
161 return String.format("c%d:s%d:a%d", column, seqIndex,
166 private static final int MAX_TOOLTIP_LENGTH = 300;
168 public SeqCanvas seqCanvas;
170 public AlignmentPanel ap;
173 * last position for mouseMoved event
175 private MousePos lastMousePosition;
177 protected int editLastRes;
179 protected int editStartSeq;
181 protected AlignViewport av;
183 ScrollThread scrollThread = null;
185 boolean mouseDragging = false;
187 boolean editingSeqs = false;
189 boolean groupEditing = false;
191 // ////////////////////////////////////////
192 // ///Everything below this is for defining the boundary of the rubberband
193 // ////////////////////////////////////////
196 boolean changeEndSeq = false;
198 boolean changeStartSeq = false;
200 boolean changeEndRes = false;
202 boolean changeStartRes = false;
204 SequenceGroup stretchGroup = null;
206 boolean remove = false;
208 Point lastMousePress;
210 boolean mouseWheelPressed = false;
212 StringBuffer keyboardNo1;
214 StringBuffer keyboardNo2;
216 private final SequenceAnnotationReport seqARep;
219 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
220 * - the tooltip is not set again if unchanged
221 * - this is the tooltip text _before_ formatting as html
223 private String lastTooltip;
226 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
227 * - used to decide where to place the tooltip in getTooltipLocation()
228 * - this is the tooltip text _after_ formatting as html
230 private String lastFormattedTooltip;
232 EditCommand editCommand;
234 StructureSelectionManager ssm;
236 SearchResultsI lastSearchResults;
239 * Creates a new SeqPanel object
244 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
247 seqARep = new SequenceAnnotationReport(true);
248 ToolTipManager.sharedInstance().registerComponent(this);
249 ToolTipManager.sharedInstance().setInitialDelay(0);
250 ToolTipManager.sharedInstance().setDismissDelay(10000);
254 setBackground(Color.white);
256 seqCanvas = new SeqCanvas(alignPanel);
257 setLayout(new BorderLayout());
258 add(seqCanvas, BorderLayout.CENTER);
260 this.ap = alignPanel;
262 if (!viewport.isDataset())
264 addMouseMotionListener(this);
265 addMouseListener(this);
266 addMouseWheelListener(this);
267 ssm = viewport.getStructureSelectionManager();
268 ssm.addStructureViewerListener(this);
269 ssm.addSelectionListener(this);
273 int startWrapBlock = -1;
275 int wrappedBlock = -1;
278 * Computes the column and sequence row (and possibly annotation row when in
279 * wrapped mode) for the given mouse position
281 * Mouse position is not set if in wrapped mode with the cursor either between
282 * sequences, or over the left or right vertical scale.
287 MousePos findMousePosition(MouseEvent evt)
289 int col = findColumn(evt);
294 int charHeight = av.getCharHeight();
295 int alignmentHeight = av.getAlignment().getHeight();
296 if (av.getWrapAlignment())
298 seqCanvas.calculateWrappedGeometry();
301 * yPos modulo height of repeating width
303 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
306 * height of sequences plus space / scale above,
307 * plus gap between sequences and annotations
309 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
310 + alignmentHeight * charHeight
311 + SeqCanvas.SEQS_ANNOTATION_GAP;
312 if (yOffsetPx >= alignmentHeightPixels)
315 * mouse is over annotations; find annotation index, also set
316 * last sequence above (for backwards compatible behaviour)
318 AlignmentAnnotation[] anns = av.getAlignment()
319 .getAlignmentAnnotation();
320 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
321 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
322 seqIndex = alignmentHeight - 1;
327 * mouse is over sequence (or the space above sequences)
329 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
332 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
338 ViewportRanges ranges = av.getRanges();
339 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
340 alignmentHeight - 1);
341 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
344 return new MousePos(col, seqIndex, annIndex);
347 * Returns the aligned sequence position (base 0) at the mouse position, or
348 * the closest visible one
350 * Returns -1 if in wrapped mode with the mouse over either left or right
356 int findColumn(MouseEvent evt)
361 final int startRes = av.getRanges().getStartRes();
362 final int charWidth = av.getCharWidth();
364 if (av.getWrapAlignment())
366 int hgap = av.getCharHeight();
367 if (av.getScaleAboveWrapped())
369 hgap += av.getCharHeight();
372 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
373 + hgap + seqCanvas.getAnnotationHeight();
376 y = Math.max(0, y - hgap);
377 x -= seqCanvas.getLabelWidthWest();
380 // mouse is over left scale
384 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
389 if (x >= cwidth * charWidth)
391 // mouse is over right scale
395 wrappedBlock = y / cHeight;
396 wrappedBlock += startRes / cwidth;
397 // allow for wrapped view scrolled right (possible from Overview)
398 int startOffset = startRes % cwidth;
399 res = wrappedBlock * cwidth + startOffset
400 + Math.min(cwidth - 1, x / charWidth);
405 * make sure we calculate relative to visible alignment,
406 * rather than right-hand gutter
408 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
409 res = (x / charWidth) + startRes;
410 res = Math.min(res, av.getRanges().getEndRes());
413 if (av.hasHiddenColumns())
415 res = av.getAlignment().getHiddenColumns()
416 .visibleToAbsoluteColumn(res);
423 * When all of a sequence of edits are complete, put the resulting edit list
424 * on the history stack (undo list), and reset flags for editing in progress.
430 if (editCommand != null && editCommand.getSize() > 0)
432 ap.alignFrame.addHistoryItem(editCommand);
433 ap.av.notifyAlignment();
438 * Tidy up come what may...
443 groupEditing = false;
452 seqCanvas.cursorY = getKeyboardNo1() - 1;
453 scrollToVisible(true);
456 void setCursorColumn()
458 seqCanvas.cursorX = getKeyboardNo1() - 1;
459 scrollToVisible(true);
462 void setCursorRowAndColumn()
464 if (keyboardNo2 == null)
466 keyboardNo2 = new StringBuffer();
470 seqCanvas.cursorX = getKeyboardNo1() - 1;
471 seqCanvas.cursorY = getKeyboardNo2() - 1;
472 scrollToVisible(true);
476 void setCursorPosition()
478 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
480 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
481 scrollToVisible(true);
484 void moveCursor(int dx, int dy)
486 moveCursor(dx, dy,false);
488 void moveCursor(int dx, int dy, boolean nextWord)
490 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
494 int maxWidth = av.getAlignment().getWidth();
495 int maxHeight=av.getAlignment().getHeight();
496 SequenceI seqAtRow = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
497 // look for next gap or residue
498 boolean isGap = Comparison.isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
499 int p = seqCanvas.cursorX,lastP,r=seqCanvas.cursorY,lastR;
515 seqAtRow = av.getAlignment().getSequenceAt(r);
517 p = nextVisible(hidden, maxWidth, p, dx);
518 } while ((dx != 0 ? p != lastP : r != lastR)
519 && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
523 int maxWidth = av.getAlignment().getWidth();
524 seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX, dx);
525 seqCanvas.cursorY += dy;
527 scrollToVisible(false);
530 private int nextVisible(HiddenColumns hidden,int maxWidth, int original, int dx)
532 int newCursorX=original+dx;
533 if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
535 int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
536 int[] region = hidden.getRegionWithEdgeAtRes(visx);
538 if (region != null) // just in case
543 newCursorX = region[1] + 1;
548 newCursorX = region[0] - 1;
552 newCursorX = (newCursorX < 0) ? 0 : newCursorX;
553 if (newCursorX >= maxWidth
554 || !hidden.isVisible(newCursorX))
556 newCursorX = original;
561 * Scroll to make the cursor visible in the viewport.
564 * just jump to the location rather than scrolling
566 void scrollToVisible(boolean jump)
568 if (seqCanvas.cursorX < 0)
570 seqCanvas.cursorX = 0;
572 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
574 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
577 if (seqCanvas.cursorY < 0)
579 seqCanvas.cursorY = 0;
581 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
583 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
588 boolean repaintNeeded = true;
591 // only need to repaint if the viewport did not move, as otherwise it will
593 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
598 if (av.getWrapAlignment())
600 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
601 int x = av.getAlignment().getHiddenColumns()
602 .absoluteToVisibleColumn(seqCanvas.cursorX);
603 av.getRanges().scrollToWrappedVisible(x);
607 av.getRanges().scrollToVisible(seqCanvas.cursorX,
612 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
614 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
615 seqCanvas.cursorX, seqCanvas.cursorY);
625 void setSelectionAreaAtCursor(boolean topLeft)
627 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
629 if (av.getSelectionGroup() != null)
631 SequenceGroup sg = av.getSelectionGroup();
632 // Find the top and bottom of this group
633 int min = av.getAlignment().getHeight(), max = 0;
634 for (int i = 0; i < sg.getSize(); i++)
636 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
651 sg.setStartRes(seqCanvas.cursorX);
652 if (sg.getEndRes() < seqCanvas.cursorX)
654 sg.setEndRes(seqCanvas.cursorX);
657 min = seqCanvas.cursorY;
661 sg.setEndRes(seqCanvas.cursorX);
662 if (sg.getStartRes() > seqCanvas.cursorX)
664 sg.setStartRes(seqCanvas.cursorX);
667 max = seqCanvas.cursorY + 1;
672 // Only the user can do this
673 av.setSelectionGroup(null);
677 // Now add any sequences between min and max
678 sg.getSequences(null).clear();
679 for (int i = min; i < max; i++)
681 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
686 if (av.getSelectionGroup() == null)
688 SequenceGroup sg = new SequenceGroup();
689 sg.setStartRes(seqCanvas.cursorX);
690 sg.setEndRes(seqCanvas.cursorX);
691 sg.addSequence(sequence, false);
692 av.setSelectionGroup(sg);
695 ap.paintAlignment(false, false);
699 void insertGapAtCursor(boolean group)
701 groupEditing = group;
702 editStartSeq = seqCanvas.cursorY;
703 editLastRes = seqCanvas.cursorX;
704 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
708 void deleteGapAtCursor(boolean group)
710 groupEditing = group;
711 editStartSeq = seqCanvas.cursorY;
712 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
713 editSequence(false, false, seqCanvas.cursorX);
717 void insertNucAtCursor(boolean group, String nuc)
719 // TODO not called - delete?
720 groupEditing = group;
721 editStartSeq = seqCanvas.cursorY;
722 editLastRes = seqCanvas.cursorX;
723 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
727 void numberPressed(char value)
729 if (keyboardNo1 == null)
731 keyboardNo1 = new StringBuffer();
734 if (keyboardNo2 != null)
736 keyboardNo2.append(value);
740 keyboardNo1.append(value);
748 if (keyboardNo1 != null)
750 int value = Integer.parseInt(keyboardNo1.toString());
754 } catch (Exception x)
765 if (keyboardNo2 != null)
767 int value = Integer.parseInt(keyboardNo2.toString());
771 } catch (Exception x)
785 public void mouseReleased(MouseEvent evt)
787 MousePos pos = findMousePosition(evt);
788 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
793 boolean didDrag = mouseDragging; // did we come here after a drag
794 mouseDragging = false;
795 mouseWheelPressed = false;
797 if (evt.isPopupTrigger()) // Windows: mouseReleased
799 showPopupMenu(evt, pos);
810 doMouseReleasedDefineMode(evt, didDrag);
821 public void mousePressed(MouseEvent evt)
823 lastMousePress = evt.getPoint();
824 MousePos pos = findMousePosition(evt);
825 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
830 if (SwingUtilities.isMiddleMouseButton(evt))
832 mouseWheelPressed = true;
836 boolean isControlDown = Platform.isControlDown(evt);
837 if (evt.isShiftDown() || isControlDown)
847 doMousePressedDefineMode(evt, pos);
851 int seq = pos.seqIndex;
852 int res = pos.column;
854 if ((seq < av.getAlignment().getHeight())
855 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
872 public void mouseOverSequence(SequenceI sequence, int index, int pos)
874 String tmp = sequence.hashCode() + " " + index + " " + pos;
876 if (lastMessage == null || !lastMessage.equals(tmp))
878 // System.err.println("mouseOver Sequence: "+tmp);
879 ssm.mouseOverSequence(sequence, index, pos, av);
885 * Highlight the mapped region described by the search results object (unless
886 * unchanged). This supports highlight of protein while mousing over linked
887 * cDNA and vice versa. The status bar is also updated to show the location of
888 * the start of the highlighted region.
891 public String highlightSequence(SearchResultsI results)
893 if (results == null || results.equals(lastSearchResults))
897 lastSearchResults = results;
899 boolean wasScrolled = false;
901 if (av.isFollowHighlight())
903 // don't allow highlight of protein/cDNA to also scroll a complementary
904 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
905 // over residue to change abruptly, causing highlighted residue in panel 2
906 // to change, causing a scroll in panel 1 etc)
907 ap.setToScrollComplementPanel(false);
908 wasScrolled = ap.scrollToPosition(results);
911 seqCanvas.revalidate();
913 ap.setToScrollComplementPanel(true);
916 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
917 if (seqCanvas.highlightSearchResults(results, fastPaint))
919 setStatusMessage(results);
921 return results.isEmpty() ? null : getHighlightInfo(results);
925 * temporary hack: answers a message suitable to show on structure hover
926 * label. This is normally null. It is a peptide variation description if
928 * <li>results are a single residue in a protein alignment</li>
929 * <li>there is a mapping to a coding sequence (codon)</li>
930 * <li>there are one or more SNP variant features on the codon</li>
932 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
937 private String getHighlightInfo(SearchResultsI results)
940 * ideally, just find mapped CDS (as we don't care about render style here);
941 * for now, go via split frame complement's FeatureRenderer
943 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
944 if (complement == null)
948 AlignFrame af = Desktop.getAlignFrameFor(complement);
949 FeatureRendererModel fr2 = af.getFeatureRenderer();
951 List<SearchResultMatchI> matches = results.getResults();
952 int j = matches.size();
953 List<String> infos = new ArrayList<>();
954 for (int i = 0; i < j; i++)
956 SearchResultMatchI match = matches.get(i);
957 int pos = match.getStart();
958 if (pos == match.getEnd())
960 SequenceI seq = match.getSequence();
961 SequenceI ds = seq.getDatasetSequence() == null ? seq
962 : seq.getDatasetSequence();
963 MappedFeatures mf = fr2
964 .findComplementFeaturesAtResidue(ds, pos);
967 for (SequenceFeature sf : mf.features)
969 String pv = mf.findProteinVariants(sf);
970 if (pv.length() > 0 && !infos.contains(pv))
983 StringBuilder sb = new StringBuilder();
984 for (String info : infos)
992 return sb.toString();
996 public VamsasSource getVamsasSource()
998 return this.ap == null ? null : this.ap.av;
1002 public void updateColours(SequenceI seq, int index)
1004 System.out.println("update the seqPanel colours");
1009 * Action on mouse movement is to update the status bar to show the current
1010 * sequence position, and (if features are shown) to show any features at the
1011 * position in a tooltip. Does nothing if the mouse move does not change
1017 public void mouseMoved(MouseEvent evt)
1021 // This is because MacOSX creates a mouseMoved
1022 // If control is down, other platforms will not.
1026 final MousePos mousePos = findMousePosition(evt);
1027 if (mousePos.equals(lastMousePosition))
1030 * just a pixel move without change of 'cell'
1032 moveTooltip = false;
1036 lastMousePosition = mousePos;
1038 if (mousePos.isOverAnnotation())
1040 mouseMovedOverAnnotation(mousePos);
1043 final int seq = mousePos.seqIndex;
1045 final int column = mousePos.column;
1046 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1048 lastMousePosition = null;
1049 setToolTipText(null);
1051 lastFormattedTooltip = null;
1052 ap.alignFrame.setStatus("");
1056 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1058 if (column >= sequence.getLength())
1064 * set status bar message, returning residue position in sequence
1066 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1067 final int pos = setStatusMessage(sequence, column, seq);
1068 if (ssm != null && !isGapped)
1070 mouseOverSequence(sequence, column, pos);
1073 StringBuilder tooltipText = new StringBuilder(64);
1075 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1078 for (int g = 0; g < groups.length; g++)
1080 if (groups[g].getStartRes() <= column
1081 && groups[g].getEndRes() >= column)
1083 if (!groups[g].getName().startsWith("JTreeGroup")
1084 && !groups[g].getName().startsWith("JGroup"))
1086 tooltipText.append(groups[g].getName());
1089 if (groups[g].getDescription() != null)
1091 tooltipText.append(": " + groups[g].getDescription());
1098 * add any features at the position to the tooltip; if over a gap, only
1099 * add features that straddle the gap (pos may be the residue before or
1102 int unshownFeatures = 0;
1103 if (av.isShowSequenceFeatures())
1105 List<SequenceFeature> features = ap.getFeatureRenderer()
1106 .findFeaturesAtColumn(sequence, column + 1);
1107 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1108 features, this.ap.getSeqPanel().seqCanvas.fr,
1109 MAX_TOOLTIP_LENGTH);
1112 * add features in CDS/protein complement at the corresponding
1113 * position if configured to do so
1115 if (av.isShowComplementFeatures())
1117 if (!Comparison.isGap(sequence.getCharAt(column)))
1119 AlignViewportI complement = ap.getAlignViewport()
1120 .getCodingComplement();
1121 AlignFrame af = Desktop.getAlignFrameFor(complement);
1122 FeatureRendererModel fr2 = af.getFeatureRenderer();
1123 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1127 unshownFeatures += seqARep.appendFeatures(tooltipText,
1128 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1133 if (tooltipText.length() == 0) // nothing added
1135 setToolTipText(null);
1140 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1142 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1143 tooltipText.append("...");
1145 if (unshownFeatures > 0)
1147 tooltipText.append("<br/>").append("... ").append("<i>")
1148 .append(MessageManager.formatMessage(
1149 "label.features_not_shown", unshownFeatures))
1152 String textString = tooltipText.toString();
1153 if (!textString.equals(lastTooltip))
1155 lastTooltip = textString;
1156 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1158 setToolTipText(lastFormattedTooltip);
1164 * When the view is in wrapped mode, and the mouse is over an annotation row,
1165 * shows the corresponding tooltip and status message (if any)
1170 protected void mouseMovedOverAnnotation(MousePos pos)
1172 final int column = pos.column;
1173 final int rowIndex = pos.annotationIndex;
1175 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1180 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1182 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1184 boolean tooltipChanged = tooltip == null ? lastTooltip != null : !tooltip.equals(lastTooltip);
1187 lastTooltip = tooltip;
1188 lastFormattedTooltip = tooltip == null ? null
1189 : JvSwingUtils.wrapTooltip(true, tooltip);
1190 setToolTipText(lastFormattedTooltip);
1193 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1195 ap.alignFrame.setStatus(msg);
1199 * if Shift key is held down while moving the mouse,
1200 * the tooltip location is not changed once shown
1202 private Point lastTooltipLocation = null;
1205 * this flag is false for pixel moves within a residue,
1206 * to reduce tooltip flicker
1208 private boolean moveTooltip = true;
1211 * a dummy tooltip used to estimate where to position tooltips
1213 private JToolTip tempTip = new JLabel().createToolTip();
1218 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1221 public Point getToolTipLocation(MouseEvent event)
1225 if (lastTooltip == null || !moveTooltip)
1230 if (lastTooltipLocation != null && event.isShiftDown())
1232 return lastTooltipLocation;
1235 int x = event.getX();
1236 int y = event.getY();
1239 tempTip.setTipText(lastFormattedTooltip);
1240 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1242 // was x += (w - x < 200) ? -(w / 2) : 5;
1243 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1244 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1246 return lastTooltipLocation = p;
1250 * set when the current UI interaction has resulted in a change that requires
1251 * shading in overviews and structures to be recalculated. this could be
1252 * changed to a something more expressive that indicates what actually has
1253 * changed, so selective redraws can be applied (ie. only structures, only
1256 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1259 * set if av.getSelectionGroup() refers to a group that is defined on the
1260 * alignment view, rather than a transient selection
1262 // private boolean editingDefinedGroup = false; // TODO: refactor to
1263 // avcontroller or viewModel
1266 * Sets the status message in alignment panel, showing the sequence number
1267 * (index) and id, and residue and residue position if not at a gap, for the
1268 * given sequence and column position. Returns the residue position returned
1269 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1270 * if at a gapped position.
1273 * aligned sequence object
1277 * index of sequence in alignment
1278 * @return sequence position of residue at column, or adjacent residue if at a
1281 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1283 char sequenceChar = sequence.getCharAt(column);
1284 int pos = sequence.findPosition(column);
1285 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1291 * Builds the status message for the current cursor location and writes it to
1292 * the status bar, for example
1295 * Sequence 3 ID: FER1_SOLLC
1296 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1297 * Sequence 5 ID: FER1_PEA Residue: B (3)
1298 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1303 * sequence position in the alignment (1..)
1304 * @param sequenceChar
1305 * the character under the cursor
1307 * the sequence residue position (if not over a gap)
1309 protected void setStatusMessage(String seqName, int seqIndex,
1310 char sequenceChar, int residuePos)
1312 StringBuilder text = new StringBuilder(32);
1315 * Sequence number (if known), and sequence name.
1317 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1318 text.append("Sequence").append(seqno).append(" ID: ")
1321 String residue = null;
1324 * Try to translate the display character to residue name (null for gap).
1326 boolean isGapped = Comparison.isGap(sequenceChar);
1330 boolean nucleotide = av.getAlignment().isNucleotide();
1331 String displayChar = String.valueOf(sequenceChar);
1334 residue = ResidueProperties.nucleotideName.get(displayChar);
1338 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1339 : ("*".equals(displayChar) ? "STOP"
1340 : ResidueProperties.aa2Triplet.get(displayChar));
1342 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1343 .append(": ").append(residue == null ? displayChar : residue);
1345 text.append(" (").append(Integer.toString(residuePos)).append(")");
1347 ap.alignFrame.setStatus(text.toString());
1351 * Set the status bar message to highlight the first matched position in
1356 private void setStatusMessage(SearchResultsI results)
1358 AlignmentI al = this.av.getAlignment();
1359 int sequenceIndex = al.findIndex(results);
1360 if (sequenceIndex == -1)
1364 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1365 SequenceI ds = alignedSeq.getDatasetSequence();
1366 for (SearchResultMatchI m : results.getResults())
1368 SequenceI seq = m.getSequence();
1369 if (seq.getDatasetSequence() != null)
1371 seq = seq.getDatasetSequence();
1376 int start = m.getStart();
1377 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1378 seq.getCharAt(start - 1), start);
1388 public void mouseDragged(MouseEvent evt)
1390 MousePos pos = findMousePosition(evt);
1391 if (pos.isOverAnnotation() || pos.column == -1)
1396 if (mouseWheelPressed)
1398 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1399 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1401 int oldWidth = av.getCharWidth();
1403 // Which is bigger, left-right or up-down?
1404 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1405 .abs(evt.getX() - lastMousePress.getX()))
1408 * on drag up or down, decrement or increment font size
1410 int fontSize = av.font.getSize();
1411 boolean fontChanged = false;
1413 if (evt.getY() < lastMousePress.getY())
1418 else if (evt.getY() > lastMousePress.getY())
1431 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1433 av.setFont(newFont, true);
1434 av.setCharWidth(oldWidth);
1438 ap.av.getCodingComplement().setFont(newFont, true);
1439 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1440 .getSplitViewContainer();
1441 splitFrame.adjustLayout();
1442 splitFrame.repaint();
1449 * on drag left or right, decrement or increment character width
1452 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1454 newWidth = av.getCharWidth() - 1;
1455 av.setCharWidth(newWidth);
1457 else if (evt.getX() > lastMousePress.getX())
1459 newWidth = av.getCharWidth() + 1;
1460 av.setCharWidth(newWidth);
1464 ap.paintAlignment(false, false);
1468 * need to ensure newWidth is set on cdna, regardless of which
1469 * panel the mouse drag happened in; protein will compute its
1470 * character width as 1:1 or 3:1
1472 av.getCodingComplement().setCharWidth(newWidth);
1473 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1474 .getSplitViewContainer();
1475 splitFrame.adjustLayout();
1476 splitFrame.repaint();
1481 FontMetrics fm = getFontMetrics(av.getFont());
1482 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1484 lastMousePress = evt.getPoint();
1491 dragStretchGroup(evt);
1495 int res = pos.column;
1502 if ((editLastRes == -1) || (editLastRes == res))
1507 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1509 // dragLeft, delete gap
1510 editSequence(false, false, res);
1514 editSequence(true, false, res);
1517 mouseDragging = true;
1518 if (scrollThread != null)
1520 scrollThread.setMousePosition(evt.getPoint());
1525 * Edits the sequence to insert or delete one or more gaps, in response to a
1526 * mouse drag or cursor mode command. The number of inserts/deletes may be
1527 * specified with the cursor command, or else depends on the mouse event
1528 * (normally one column, but potentially more for a fast mouse drag).
1530 * Delete gaps is limited to the number of gaps left of the cursor position
1531 * (mouse drag), or at or right of the cursor position (cursor mode).
1533 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1534 * the current selection group.
1536 * In locked editing mode (with a selection group present), inserts/deletions
1537 * within the selection group are limited to its boundaries (and edits outside
1538 * the group stop at its border).
1541 * true to insert gaps, false to delete gaps
1543 * (unused parameter)
1545 * the column at which to perform the action; the number of columns
1546 * affected depends on <code>this.editLastRes</code> (cursor column
1549 synchronized void editSequence(boolean insertGap, boolean editSeq,
1553 int fixedRight = -1;
1554 boolean fixedColumns = false;
1555 SequenceGroup sg = av.getSelectionGroup();
1557 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1559 // No group, but the sequence may represent a group
1560 if (!groupEditing && av.hasHiddenRows())
1562 if (av.isHiddenRepSequence(seq))
1564 sg = av.getRepresentedSequences(seq);
1565 groupEditing = true;
1569 StringBuilder message = new StringBuilder(64); // for status bar
1572 * make a name for the edit action, for
1573 * status bar message and Undo/Redo menu
1575 String label = null;
1578 message.append("Edit group:");
1579 label = MessageManager.getString("action.edit_group");
1583 message.append("Edit sequence: " + seq.getName());
1584 label = seq.getName();
1585 if (label.length() > 10)
1587 label = label.substring(0, 10);
1589 label = MessageManager.formatMessage("label.edit_params",
1595 * initialise the edit command if there is not
1596 * already one being extended
1598 if (editCommand == null)
1600 editCommand = new EditCommand(label);
1605 message.append(" insert ");
1609 message.append(" delete ");
1612 message.append(Math.abs(startres - editLastRes) + " gaps.");
1613 ap.alignFrame.setStatus(message.toString());
1616 * is there a selection group containing the sequence being edited?
1617 * if so the boundary of the group is the limit of the edit
1618 * (but the edit may be inside or outside the selection group)
1620 boolean inSelectionGroup = sg != null
1621 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1622 if (groupEditing || inSelectionGroup)
1624 fixedColumns = true;
1626 // sg might be null as the user may only see 1 sequence,
1627 // but the sequence represents a group
1630 if (!av.isHiddenRepSequence(seq))
1635 sg = av.getRepresentedSequences(seq);
1638 fixedLeft = sg.getStartRes();
1639 fixedRight = sg.getEndRes();
1641 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1642 || (startres >= fixedLeft && editLastRes < fixedLeft)
1643 || (startres > fixedRight && editLastRes <= fixedRight)
1644 || (startres <= fixedRight && editLastRes > fixedRight))
1650 if (fixedLeft > startres)
1652 fixedRight = fixedLeft - 1;
1655 else if (fixedRight < startres)
1657 fixedLeft = fixedRight;
1662 if (av.hasHiddenColumns())
1664 fixedColumns = true;
1665 int y1 = av.getAlignment().getHiddenColumns()
1666 .getNextHiddenBoundary(true, startres);
1667 int y2 = av.getAlignment().getHiddenColumns()
1668 .getNextHiddenBoundary(false, startres);
1670 if ((insertGap && startres > y1 && editLastRes < y1)
1671 || (!insertGap && startres < y2 && editLastRes > y2))
1677 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1678 // Selection spans a hidden region
1679 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1687 fixedRight = y2 - 1;
1692 boolean success = doEditSequence(insertGap, editSeq, startres,
1693 fixedRight, fixedColumns, sg);
1696 * report what actually happened (might be less than
1697 * what was requested), by inspecting the edit commands added
1699 String msg = getEditStatusMessage(editCommand);
1700 ap.alignFrame.setStatus(msg == null ? " " : msg);
1706 editLastRes = startres;
1707 seqCanvas.repaint();
1711 * A helper method that performs the requested editing to insert or delete
1712 * gaps (if possible). Answers true if the edit was successful, false if could
1713 * only be performed in part or not at all. Failure may occur in 'locked edit'
1714 * mode, when an insertion requires a matching gapped position (or column) to
1715 * delete, and deletion requires an adjacent gapped position (or column) to
1719 * true if inserting gap(s), false if deleting
1721 * (unused parameter, currently always false)
1723 * the column at which to perform the edit
1725 * fixed right boundary column of a locked edit (within or to the
1726 * left of a selection group)
1727 * @param fixedColumns
1728 * true if this is a locked edit
1730 * the sequence group (if group edit is being performed)
1733 protected boolean doEditSequence(final boolean insertGap,
1734 final boolean editSeq, final int startres, int fixedRight,
1735 final boolean fixedColumns, final SequenceGroup sg)
1737 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1738 SequenceI[] seqs = new SequenceI[] { seq };
1742 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1743 int g, groupSize = vseqs.size();
1744 SequenceI[] groupSeqs = new SequenceI[groupSize];
1745 for (g = 0; g < groupSeqs.length; g++)
1747 groupSeqs[g] = vseqs.get(g);
1753 // If the user has selected the whole sequence, and is dragging to
1754 // the right, we can still extend the alignment and selectionGroup
1755 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1756 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1759 av.getAlignment().getWidth() + startres - editLastRes);
1760 fixedRight = sg.getEndRes();
1763 // Is it valid with fixed columns??
1764 // Find the next gap before the end
1765 // of the visible region boundary
1766 boolean blank = false;
1767 for (; fixedRight > editLastRes; fixedRight--)
1771 for (g = 0; g < groupSize; g++)
1773 for (int j = 0; j < startres - editLastRes; j++)
1776 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1791 if (sg.getSize() == av.getAlignment().getHeight())
1793 if ((av.hasHiddenColumns()
1794 && startres < av.getAlignment().getHiddenColumns()
1795 .getNextHiddenBoundary(false, startres)))
1800 int alWidth = av.getAlignment().getWidth();
1801 if (av.hasHiddenRows())
1803 int hwidth = av.getAlignment().getHiddenSequences()
1805 if (hwidth > alWidth)
1810 // We can still insert gaps if the selectionGroup
1811 // contains all the sequences
1812 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1813 fixedRight = alWidth + startres - editLastRes;
1823 else if (!insertGap)
1825 // / Are we able to delete?
1826 // ie are all columns blank?
1828 for (g = 0; g < groupSize; g++)
1830 for (int j = startres; j < editLastRes; j++)
1832 if (groupSeqs[g].getLength() <= j)
1837 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1839 // Not a gap, block edit not valid
1848 // dragging to the right
1849 if (fixedColumns && fixedRight != -1)
1851 for (int j = editLastRes; j < startres; j++)
1853 insertGap(j, groupSeqs, fixedRight);
1858 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1859 startres - editLastRes, false);
1864 // dragging to the left
1865 if (fixedColumns && fixedRight != -1)
1867 for (int j = editLastRes; j > startres; j--)
1869 deleteChar(startres, groupSeqs, fixedRight);
1874 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1875 editLastRes - startres, false);
1882 * editing a single sequence
1886 // dragging to the right
1887 if (fixedColumns && fixedRight != -1)
1889 for (int j = editLastRes; j < startres; j++)
1891 if (!insertGap(j, seqs, fixedRight))
1894 * e.g. cursor mode command specified
1895 * more inserts than are possible
1903 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1904 startres - editLastRes, false);
1911 // dragging to the left
1912 if (fixedColumns && fixedRight != -1)
1914 for (int j = editLastRes; j > startres; j--)
1916 if (!Comparison.isGap(seq.getCharAt(startres)))
1920 deleteChar(startres, seqs, fixedRight);
1925 // could be a keyboard edit trying to delete none gaps
1927 for (int m = startres; m < editLastRes; m++)
1929 if (!Comparison.isGap(seq.getCharAt(m)))
1937 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1942 {// insertGap==false AND editSeq==TRUE;
1943 if (fixedColumns && fixedRight != -1)
1945 for (int j = editLastRes; j < startres; j++)
1947 insertGap(j, seqs, fixedRight);
1952 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1953 startres - editLastRes, false);
1963 * Constructs an informative status bar message while dragging to insert or
1964 * delete gaps. Answers null if inserts and deletes cancel out.
1966 * @param editCommand
1967 * a command containing the list of individual edits
1970 protected static String getEditStatusMessage(EditCommand editCommand)
1972 if (editCommand == null)
1978 * add any inserts, and subtract any deletes,
1979 * not counting those auto-inserted when doing a 'locked edit'
1980 * (so only counting edits 'under the cursor')
1983 for (Edit cmd : editCommand.getEdits())
1985 if (!cmd.isSystemGenerated())
1987 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1995 * inserts and deletes cancel out
2000 String msgKey = count > 1 ? "label.insert_gaps"
2001 : (count == 1 ? "label.insert_gap"
2002 : (count == -1 ? "label.delete_gap"
2003 : "label.delete_gaps"));
2004 count = Math.abs(count);
2006 return MessageManager.formatMessage(msgKey, String.valueOf(count));
2010 * Inserts one gap at column j, deleting the right-most gapped column up to
2011 * (and including) fixedColumn. Returns true if the edit is successful, false
2012 * if no blank column is available to allow the insertion to be balanced by a
2017 * @param fixedColumn
2020 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
2022 int blankColumn = fixedColumn;
2023 for (int s = 0; s < seq.length; s++)
2025 // Find the next gap before the end of the visible region boundary
2026 // If lastCol > j, theres a boundary after the gap insertion
2028 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
2030 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
2032 // Theres a space, so break and insert the gap
2037 if (blankColumn <= j)
2039 blankColumn = fixedColumn;
2045 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2047 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2053 * Helper method to add and perform one edit action
2059 * @param systemGenerated
2060 * true if the edit is a 'balancing' delete (or insert) to match a
2061 * user's insert (or delete) in a locked editing region
2063 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2064 int count, boolean systemGenerated)
2067 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2068 av.getAlignment().getGapCharacter());
2069 edit.setSystemGenerated(systemGenerated);
2071 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2075 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2076 * each of the given sequences. The caller should ensure that all sequences
2077 * are gapped in column j.
2081 * @param fixedColumn
2083 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2085 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2087 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2091 * On reentering the panel, stops any scrolling that was started on dragging
2097 public void mouseEntered(MouseEvent e)
2107 * On leaving the panel, if the mouse is being dragged, starts a thread to
2108 * scroll it until the mouse is released (in unwrapped mode only)
2113 public void mouseExited(MouseEvent e)
2115 lastMousePosition = null;
2116 ap.alignFrame.setStatus(" ");
2117 if (av.getWrapAlignment())
2122 if (mouseDragging && scrollThread == null)
2124 startScrolling(e.getPoint());
2129 * Handler for double-click on a position with one or more sequence features.
2130 * Opens the Amend Features dialog to allow feature details to be amended, or
2131 * the feature deleted.
2134 public void mouseClicked(MouseEvent evt)
2136 SequenceGroup sg = null;
2137 MousePos pos = findMousePosition(evt);
2138 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2143 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2145 sg = av.getSelectionGroup();
2146 if (sg != null && sg.getSize() == 1
2147 && sg.getEndRes() - sg.getStartRes() < 2)
2149 av.setSelectionGroup(null);
2152 int column = pos.column;
2155 * find features at the position (if not gapped), or straddling
2156 * the position (if at a gap)
2158 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2159 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2160 .findFeaturesAtColumn(sequence, column + 1);
2162 if (!features.isEmpty())
2165 * highlight the first feature at the position on the alignment
2167 SearchResultsI highlight = new SearchResults();
2168 highlight.addResult(sequence, features.get(0).getBegin(), features
2170 seqCanvas.highlightSearchResults(highlight, true);
2173 * open the Amend Features dialog
2175 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2176 false).showDialog();
2182 public void mouseWheelMoved(MouseWheelEvent e)
2185 double wheelRotation = e.getPreciseWheelRotation();
2186 if (wheelRotation > 0)
2188 if (e.isShiftDown())
2190 av.getRanges().scrollRight(true);
2195 av.getRanges().scrollUp(false);
2198 else if (wheelRotation < 0)
2200 if (e.isShiftDown())
2202 av.getRanges().scrollRight(false);
2206 av.getRanges().scrollUp(true);
2211 * update status bar and tooltip for new position
2212 * (need to synthesize a mouse movement to refresh tooltip)
2215 ToolTipManager.sharedInstance().mouseMoved(e);
2224 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2226 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2231 final int res = pos.column;
2232 final int seq = pos.seqIndex;
2234 updateOverviewAndStructs = false;
2236 startWrapBlock = wrappedBlock;
2238 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2240 if ((sequence == null) || (res > sequence.getLength()))
2245 stretchGroup = av.getSelectionGroup();
2247 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2249 stretchGroup = av.getAlignment().findGroup(sequence, res);
2250 if (stretchGroup != null)
2252 // only update the current selection if the popup menu has a group to
2254 av.setSelectionGroup(stretchGroup);
2259 * defer right-mouse click handling to mouseReleased on Windows
2260 * (where isPopupTrigger() will answer true)
2261 * NB isRightMouseButton is also true for Cmd-click on Mac
2263 if (Platform.isWinRightButton(evt))
2268 if (evt.isPopupTrigger()) // Mac: mousePressed
2270 showPopupMenu(evt, pos);
2276 seqCanvas.cursorX = res;
2277 seqCanvas.cursorY = seq;
2278 seqCanvas.repaint();
2282 if (stretchGroup == null)
2284 createStretchGroup(res, sequence);
2287 if (stretchGroup != null)
2289 stretchGroup.addPropertyChangeListener(seqCanvas);
2292 seqCanvas.repaint();
2295 private void createStretchGroup(int res, SequenceI sequence)
2297 // Only if left mouse button do we want to change group sizes
2298 // define a new group here
2299 SequenceGroup sg = new SequenceGroup();
2300 sg.setStartRes(res);
2302 sg.addSequence(sequence, false);
2303 av.setSelectionGroup(sg);
2306 if (av.getConservationSelected())
2308 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2312 if (av.getAbovePIDThreshold())
2314 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2317 // TODO: stretchGroup will always be not null. Is this a merge error ?
2318 // or is there a threading issue here?
2319 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2321 // Edit end res position of selected group
2322 changeEndRes = true;
2324 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2326 // Edit end res position of selected group
2327 changeStartRes = true;
2329 stretchGroup.getWidth();
2334 * Build and show a pop-up menu at the right-click mouse position
2339 void showPopupMenu(MouseEvent evt, MousePos pos)
2341 final int column = pos.column;
2342 final int seq = pos.seqIndex;
2343 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2344 if (sequence != null)
2346 PopupMenu pop = new PopupMenu(ap, sequence, column);
2347 pop.show(this, evt.getX(), evt.getY());
2352 * Update the display after mouse up on a selection or group
2355 * mouse released event details
2357 * true if this event is happening after a mouse drag (rather than a
2360 protected void doMouseReleasedDefineMode(MouseEvent evt,
2363 if (stretchGroup == null)
2368 stretchGroup.removePropertyChangeListener(seqCanvas);
2370 // always do this - annotation has own state
2371 // but defer colourscheme update until hidden sequences are passed in
2372 boolean vischange = stretchGroup.recalcConservation(true);
2373 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2375 if (stretchGroup.cs != null)
2379 stretchGroup.cs.alignmentChanged(stretchGroup,
2380 av.getHiddenRepSequences());
2383 ResidueShaderI groupColourScheme = stretchGroup
2384 .getGroupColourScheme();
2385 String name = stretchGroup.getName();
2386 if (stretchGroup.cs.conservationApplied())
2388 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2390 if (stretchGroup.cs.getThreshold() > 0)
2392 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2395 PaintRefresher.Refresh(this, av.getSequenceSetId());
2396 // TODO: structure colours only need updating if stretchGroup used to or now
2397 // does contain sequences with structure views
2398 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2399 updateOverviewAndStructs = false;
2400 changeEndRes = false;
2401 changeStartRes = false;
2402 stretchGroup = null;
2407 * Resizes the borders of a selection group depending on the direction of
2412 protected void dragStretchGroup(MouseEvent evt)
2414 if (stretchGroup == null)
2419 MousePos pos = findMousePosition(evt);
2420 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2425 int res = pos.column;
2426 int y = pos.seqIndex;
2428 if (wrappedBlock != startWrapBlock)
2433 res = Math.min(res, av.getAlignment().getWidth()-1);
2435 if (stretchGroup.getEndRes() == res)
2437 // Edit end res position of selected group
2438 changeEndRes = true;
2440 else if (stretchGroup.getStartRes() == res)
2442 // Edit start res position of selected group
2443 changeStartRes = true;
2446 if (res < av.getRanges().getStartRes())
2448 res = av.getRanges().getStartRes();
2453 if (res > (stretchGroup.getStartRes() - 1))
2455 stretchGroup.setEndRes(res);
2456 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2459 else if (changeStartRes)
2461 if (res < (stretchGroup.getEndRes() + 1))
2463 stretchGroup.setStartRes(res);
2464 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2468 int dragDirection = 0;
2474 else if (y < oldSeq)
2479 while ((y != oldSeq) && (oldSeq > -1)
2480 && (y < av.getAlignment().getHeight()))
2482 // This routine ensures we don't skip any sequences, as the
2483 // selection is quite slow.
2484 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2486 oldSeq += dragDirection;
2493 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2495 if (stretchGroup.getSequences(null).contains(nextSeq))
2497 stretchGroup.deleteSequence(seq, false);
2498 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2504 stretchGroup.addSequence(seq, false);
2507 stretchGroup.addSequence(nextSeq, false);
2508 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2517 mouseDragging = true;
2519 if (scrollThread != null)
2521 scrollThread.setMousePosition(evt.getPoint());
2525 * construct a status message showing the range of the selection
2527 StringBuilder status = new StringBuilder(64);
2528 List<SequenceI> seqs = stretchGroup.getSequences();
2529 String name = seqs.get(0).getName();
2530 if (name.length() > 20)
2532 name = name.substring(0, 20);
2534 status.append(name).append(" - ");
2535 name = seqs.get(seqs.size() - 1).getName();
2536 if (name.length() > 20)
2538 name = name.substring(0, 20);
2540 status.append(name).append(" ");
2541 int startRes = stretchGroup.getStartRes();
2542 status.append(" cols ").append(String.valueOf(startRes + 1))
2544 int endRes = stretchGroup.getEndRes();
2545 status.append(String.valueOf(endRes + 1));
2546 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2547 .append(String.valueOf(endRes - startRes + 1)).append(")");
2548 ap.alignFrame.setStatus(status.toString());
2552 * Stops the scroll thread if it is running
2554 void stopScrolling()
2556 if (scrollThread != null)
2558 scrollThread.stopScrolling();
2559 scrollThread = null;
2561 mouseDragging = false;
2565 * Starts a thread to scroll the alignment, towards a given mouse position
2566 * outside the panel bounds, unless the alignment is in wrapped mode
2570 void startScrolling(Point mousePos)
2573 * set this.mouseDragging in case this was called from
2574 * a drag in ScalePanel or AnnotationPanel
2576 mouseDragging = true;
2577 if (!av.getWrapAlignment() && scrollThread == null)
2579 scrollThread = new ScrollThread();
2580 scrollThread.setMousePosition(mousePos);
2581 if (Platform.isJS())
2584 * Javascript - run every 20ms until scrolling stopped
2585 * or reaches the limit of scrollable alignment
2587 Timer t = new Timer(20, new ActionListener()
2590 public void actionPerformed(ActionEvent e)
2592 if (scrollThread != null)
2594 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2595 scrollThread.scrollOnce();
2599 t.addActionListener(new ActionListener()
2602 public void actionPerformed(ActionEvent e)
2604 if (scrollThread == null)
2606 // SeqPanel.stopScrolling called
2616 * Java - run in a new thread
2618 scrollThread.start();
2624 * Performs scrolling of the visible alignment left, right, up or down, until
2625 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2626 * limit of the alignment is reached
2628 class ScrollThread extends Thread
2630 private Point mousePos;
2632 private volatile boolean keepRunning = true;
2637 public ScrollThread()
2639 setName("SeqPanel$ScrollThread");
2643 * Sets the position of the mouse that determines the direction of the
2644 * scroll to perform. If this is called as the mouse moves, scrolling should
2645 * respond accordingly. For example, if the mouse is dragged right, scroll
2646 * right should start; if the drag continues down, scroll down should also
2651 public void setMousePosition(Point p)
2657 * Sets a flag that will cause the thread to exit
2659 public void stopScrolling()
2661 keepRunning = false;
2665 * Scrolls the alignment left or right, and/or up or down, depending on the
2666 * last notified mouse position, until the limit of the alignment is
2667 * reached, or a flag is set to stop the scroll
2674 if (mousePos != null)
2676 keepRunning = scrollOnce();
2681 } catch (Exception ex)
2685 SeqPanel.this.scrollThread = null;
2691 * <li>one row up, if the mouse is above the panel</li>
2692 * <li>one row down, if the mouse is below the panel</li>
2693 * <li>one column left, if the mouse is left of the panel</li>
2694 * <li>one column right, if the mouse is right of the panel</li>
2696 * Answers true if a scroll was performed, false if not - meaning either
2697 * that the mouse position is within the panel, or the edge of the alignment
2700 boolean scrollOnce()
2703 * quit after mouseUp ensures interrupt in JalviewJS
2710 boolean scrolled = false;
2711 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2718 // mouse is above this panel - try scroll up
2719 scrolled = ranges.scrollUp(true);
2721 else if (mousePos.y >= getHeight())
2723 // mouse is below this panel - try scroll down
2724 scrolled = ranges.scrollUp(false);
2728 * scroll left or right
2732 scrolled |= ranges.scrollRight(false);
2734 else if (mousePos.x >= getWidth())
2736 scrolled |= ranges.scrollRight(true);
2743 * modify current selection according to a received message.
2746 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2747 HiddenColumns hidden, SelectionSource source)
2749 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2750 // handles selection messages...
2751 // TODO: extend config options to allow user to control if selections may be
2752 // shared between viewports.
2753 boolean iSentTheSelection = (av == source
2754 || (source instanceof AlignViewport
2755 && ((AlignmentViewport) source).getSequenceSetId()
2756 .equals(av.getSequenceSetId())));
2758 if (iSentTheSelection)
2760 // respond to our own event by updating dependent dialogs
2761 if (ap.getCalculationDialog() != null)
2763 ap.getCalculationDialog().validateCalcTypes();
2769 // process further ?
2770 if (!av.followSelection)
2776 * Ignore the selection if there is one of our own pending.
2778 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2784 * Check for selection in a view of which this one is a dna/protein
2787 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2792 // do we want to thread this ? (contention with seqsel and colsel locks, I
2795 * only copy colsel if there is a real intersection between
2796 * sequence selection and this panel's alignment
2798 boolean repaint = false;
2799 boolean copycolsel = false;
2801 SequenceGroup sgroup = null;
2802 if (seqsel != null && seqsel.getSize() > 0)
2804 if (av.getAlignment() == null)
2806 Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2807 + " ViewId=" + av.getViewId()
2808 + " 's alignment is NULL! returning immediately.");
2811 sgroup = seqsel.intersect(av.getAlignment(),
2812 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2813 if ((sgroup != null && sgroup.getSize() > 0))
2818 if (sgroup != null && sgroup.getSize() > 0)
2820 av.setSelectionGroup(sgroup);
2824 av.setSelectionGroup(null);
2826 av.isSelectionGroupChanged(true);
2831 // the current selection is unset or from a previous message
2832 // so import the new colsel.
2833 if (colsel == null || colsel.isEmpty())
2835 if (av.getColumnSelection() != null)
2837 av.getColumnSelection().clear();
2843 // TODO: shift colSel according to the intersecting sequences
2844 if (av.getColumnSelection() == null)
2846 av.setColumnSelection(new ColumnSelection(colsel));
2850 av.getColumnSelection().setElementsFrom(colsel,
2851 av.getAlignment().getHiddenColumns());
2854 av.isColSelChanged(true);
2858 if (copycolsel && av.hasHiddenColumns()
2859 && (av.getAlignment().getHiddenColumns() == null))
2861 System.err.println("Bad things");
2863 if (repaint) // always true!
2865 // probably finessing with multiple redraws here
2866 PaintRefresher.Refresh(this, av.getSequenceSetId());
2867 // ap.paintAlignment(false);
2870 // lastly, update dependent dialogs
2871 if (ap.getCalculationDialog() != null)
2873 ap.getCalculationDialog().validateCalcTypes();
2879 * If this panel is a cdna/protein translation view of the selection source,
2880 * tries to map the source selection to a local one, and returns true. Else
2887 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2888 ColumnSelection colsel, HiddenColumns hidden,
2889 SelectionSource source)
2891 if (!(source instanceof AlignViewportI))
2895 final AlignViewportI sourceAv = (AlignViewportI) source;
2896 if (sourceAv.getCodingComplement() != av
2897 && av.getCodingComplement() != sourceAv)
2903 * Map sequence selection
2905 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2906 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2907 av.isSelectionGroupChanged(true);
2910 * Map column selection
2912 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2914 ColumnSelection cs = new ColumnSelection();
2915 HiddenColumns hs = new HiddenColumns();
2916 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2917 av.setColumnSelection(cs);
2918 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2920 // lastly, update any dependent dialogs
2921 if (ap.getCalculationDialog() != null)
2923 ap.getCalculationDialog().validateCalcTypes();
2927 * repaint alignment, and also Overview or Structure
2928 * if hidden column selection has changed
2930 ap.paintAlignment(hiddenChanged, hiddenChanged);
2937 * @return null or last search results handled by this panel
2939 public SearchResultsI getLastSearchResults()
2941 return lastSearchResults;
2945 * scroll to the given row/column - or nearest visible location
2950 public void scrollTo(int row, int column)
2953 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2954 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2955 ap.scrollTo(column, column, row, true, true);
2959 * scroll to the given row - or nearest visible location
2963 public void scrollToRow(int row)
2966 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2967 ap.scrollTo(ap.av.getRanges().getStartRes(),
2968 ap.av.getRanges().getStartRes(), row, true, true);
2972 * scroll to the given column - or nearest visible location
2976 public void scrollToColumn(int column)
2979 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2980 ap.scrollTo(column, column, ap.av.getRanges().getStartSeq(), true,