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.Cache;
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
284 MousePos findMousePosition(MouseEvent evt)
286 int col = findColumn(evt);
291 int charHeight = av.getCharHeight();
292 int alignmentHeight = av.getAlignment().getHeight();
293 if (av.getWrapAlignment())
295 seqCanvas.calculateWrappedGeometry();
298 * yPos modulo height of repeating width
300 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
303 * height of sequences plus space / scale above,
304 * plus gap between sequences and annotations
306 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
307 + alignmentHeight * charHeight
308 + SeqCanvas.SEQS_ANNOTATION_GAP;
309 if (yOffsetPx >= alignmentHeightPixels)
312 * mouse is over annotations; find annotation index, also set
313 * last sequence above (for backwards compatible behaviour)
315 AlignmentAnnotation[] anns = av.getAlignment()
316 .getAlignmentAnnotation();
317 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
318 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
319 seqIndex = alignmentHeight - 1;
324 * mouse is over sequence (or the space above sequences)
326 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
329 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
335 ViewportRanges ranges = av.getRanges();
336 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
337 alignmentHeight - 1);
338 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
341 return new MousePos(col, seqIndex, annIndex);
344 * Returns the aligned sequence position (base 0) at the mouse position, or
345 * the closest visible one
350 int findColumn(MouseEvent evt)
355 final int startRes = av.getRanges().getStartRes();
356 final int charWidth = av.getCharWidth();
358 if (av.getWrapAlignment())
360 int hgap = av.getCharHeight();
361 if (av.getScaleAboveWrapped())
363 hgap += av.getCharHeight();
366 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
367 + hgap + seqCanvas.getAnnotationHeight();
370 y = Math.max(0, y - hgap);
371 x -= seqCanvas.getLabelWidthWest();
374 // mouse is over left scale
378 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
383 if (x >= cwidth * charWidth)
385 // mouse is over right scale
389 wrappedBlock = y / cHeight;
390 wrappedBlock += startRes / cwidth;
391 // allow for wrapped view scrolled right (possible from Overview)
392 int startOffset = startRes % cwidth;
393 res = wrappedBlock * cwidth + startOffset
394 + Math.min(cwidth - 1, x / charWidth);
399 * make sure we calculate relative to visible alignment,
400 * rather than right-hand gutter
402 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
403 res = (x / charWidth) + startRes;
404 res = Math.min(res, av.getRanges().getEndRes());
407 if (av.hasHiddenColumns())
409 res = av.getAlignment().getHiddenColumns()
410 .visibleToAbsoluteColumn(res);
417 * When all of a sequence of edits are complete, put the resulting edit list
418 * on the history stack (undo list), and reset flags for editing in progress.
424 if (editCommand != null && editCommand.getSize() > 0)
426 ap.alignFrame.addHistoryItem(editCommand);
427 ap.av.notifyAlignment();
432 * Tidy up come what may...
437 groupEditing = false;
446 seqCanvas.cursorY = getKeyboardNo1() - 1;
447 scrollToVisible(true);
450 void setCursorColumn()
452 seqCanvas.cursorX = getKeyboardNo1() - 1;
453 scrollToVisible(true);
456 void setCursorRowAndColumn()
458 if (keyboardNo2 == null)
460 keyboardNo2 = new StringBuffer();
464 seqCanvas.cursorX = getKeyboardNo1() - 1;
465 seqCanvas.cursorY = getKeyboardNo2() - 1;
466 scrollToVisible(true);
470 void setCursorPosition()
472 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
474 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
475 scrollToVisible(true);
478 void moveCursor(int dx, int dy)
480 seqCanvas.cursorX += dx;
481 seqCanvas.cursorY += dy;
483 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
485 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
487 int original = seqCanvas.cursorX - dx;
488 int maxWidth = av.getAlignment().getWidth();
490 if (!hidden.isVisible(seqCanvas.cursorX))
492 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
493 int[] region = hidden.getRegionWithEdgeAtRes(visx);
495 if (region != null) // just in case
500 seqCanvas.cursorX = region[1] + 1;
505 seqCanvas.cursorX = region[0] - 1;
508 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
511 if (seqCanvas.cursorX >= maxWidth
512 || !hidden.isVisible(seqCanvas.cursorX))
514 seqCanvas.cursorX = original;
518 scrollToVisible(false);
522 * Scroll to make the cursor visible in the viewport.
525 * just jump to the location rather than scrolling
527 void scrollToVisible(boolean jump)
529 if (seqCanvas.cursorX < 0)
531 seqCanvas.cursorX = 0;
533 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
535 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
538 if (seqCanvas.cursorY < 0)
540 seqCanvas.cursorY = 0;
542 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
544 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
549 boolean repaintNeeded = true;
552 // only need to repaint if the viewport did not move, as otherwise it will
554 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
559 if (av.getWrapAlignment())
561 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
562 int x = av.getAlignment().getHiddenColumns()
563 .absoluteToVisibleColumn(seqCanvas.cursorX);
564 av.getRanges().scrollToWrappedVisible(x);
568 av.getRanges().scrollToVisible(seqCanvas.cursorX,
573 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
575 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
576 seqCanvas.cursorX, seqCanvas.cursorY);
586 void setSelectionAreaAtCursor(boolean topLeft)
588 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
590 if (av.getSelectionGroup() != null)
592 SequenceGroup sg = av.getSelectionGroup();
593 // Find the top and bottom of this group
594 int min = av.getAlignment().getHeight(), max = 0;
595 for (int i = 0; i < sg.getSize(); i++)
597 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
612 sg.setStartRes(seqCanvas.cursorX);
613 if (sg.getEndRes() < seqCanvas.cursorX)
615 sg.setEndRes(seqCanvas.cursorX);
618 min = seqCanvas.cursorY;
622 sg.setEndRes(seqCanvas.cursorX);
623 if (sg.getStartRes() > seqCanvas.cursorX)
625 sg.setStartRes(seqCanvas.cursorX);
628 max = seqCanvas.cursorY + 1;
633 // Only the user can do this
634 av.setSelectionGroup(null);
638 // Now add any sequences between min and max
639 sg.getSequences(null).clear();
640 for (int i = min; i < max; i++)
642 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
647 if (av.getSelectionGroup() == null)
649 SequenceGroup sg = new SequenceGroup();
650 sg.setStartRes(seqCanvas.cursorX);
651 sg.setEndRes(seqCanvas.cursorX);
652 sg.addSequence(sequence, false);
653 av.setSelectionGroup(sg);
656 ap.paintAlignment(false, false);
660 void insertGapAtCursor(boolean group)
662 groupEditing = group;
663 editStartSeq = seqCanvas.cursorY;
664 editLastRes = seqCanvas.cursorX;
665 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
669 void deleteGapAtCursor(boolean group)
671 groupEditing = group;
672 editStartSeq = seqCanvas.cursorY;
673 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
674 editSequence(false, false, seqCanvas.cursorX);
678 void insertNucAtCursor(boolean group, String nuc)
680 // TODO not called - delete?
681 groupEditing = group;
682 editStartSeq = seqCanvas.cursorY;
683 editLastRes = seqCanvas.cursorX;
684 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
688 void numberPressed(char value)
690 if (keyboardNo1 == null)
692 keyboardNo1 = new StringBuffer();
695 if (keyboardNo2 != null)
697 keyboardNo2.append(value);
701 keyboardNo1.append(value);
709 if (keyboardNo1 != null)
711 int value = Integer.parseInt(keyboardNo1.toString());
715 } catch (Exception x)
726 if (keyboardNo2 != null)
728 int value = Integer.parseInt(keyboardNo2.toString());
732 } catch (Exception x)
746 public void mouseReleased(MouseEvent evt)
748 MousePos pos = findMousePosition(evt);
749 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
754 boolean didDrag = mouseDragging; // did we come here after a drag
755 mouseDragging = false;
756 mouseWheelPressed = false;
758 if (evt.isPopupTrigger()) // Windows: mouseReleased
760 showPopupMenu(evt, pos);
771 doMouseReleasedDefineMode(evt, didDrag);
782 public void mousePressed(MouseEvent evt)
784 lastMousePress = evt.getPoint();
785 MousePos pos = findMousePosition(evt);
786 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
791 if (SwingUtilities.isMiddleMouseButton(evt))
793 mouseWheelPressed = true;
797 boolean isControlDown = Platform.isControlDown(evt);
798 if (evt.isShiftDown() || isControlDown)
808 doMousePressedDefineMode(evt, pos);
812 int seq = pos.seqIndex;
813 int res = pos.column;
815 if ((seq < av.getAlignment().getHeight())
816 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
833 public void mouseOverSequence(SequenceI sequence, int index, int pos)
835 String tmp = sequence.hashCode() + " " + index + " " + pos;
837 if (lastMessage == null || !lastMessage.equals(tmp))
839 // System.err.println("mouseOver Sequence: "+tmp);
840 ssm.mouseOverSequence(sequence, index, pos, av);
846 * Highlight the mapped region described by the search results object (unless
847 * unchanged). This supports highlight of protein while mousing over linked
848 * cDNA and vice versa. The status bar is also updated to show the location of
849 * the start of the highlighted region.
852 public String highlightSequence(SearchResultsI results)
854 if (results == null || results.equals(lastSearchResults))
858 lastSearchResults = results;
860 boolean wasScrolled = false;
862 if (av.isFollowHighlight())
864 // don't allow highlight of protein/cDNA to also scroll a complementary
865 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
866 // over residue to change abruptly, causing highlighted residue in panel 2
867 // to change, causing a scroll in panel 1 etc)
868 ap.setToScrollComplementPanel(false);
869 wasScrolled = ap.scrollToPosition(results);
872 seqCanvas.revalidate();
874 ap.setToScrollComplementPanel(true);
877 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
878 if (seqCanvas.highlightSearchResults(results, fastPaint))
880 setStatusMessage(results);
882 return results.isEmpty() ? null : getHighlightInfo(results);
886 * temporary hack: answers a message suitable to show on structure hover
887 * label. This is normally null. It is a peptide variation description if
889 * <li>results are a single residue in a protein alignment</li>
890 * <li>there is a mapping to a coding sequence (codon)</li>
891 * <li>there are one or more SNP variant features on the codon</li>
893 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
898 private String getHighlightInfo(SearchResultsI results)
901 * ideally, just find mapped CDS (as we don't care about render style here);
902 * for now, go via split frame complement's FeatureRenderer
904 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
905 if (complement == null)
909 AlignFrame af = Desktop.getAlignFrameFor(complement);
910 FeatureRendererModel fr2 = af.getFeatureRenderer();
912 int j = results.getSize();
913 List<String> infos = new ArrayList<>();
914 for (int i = 0; i < j; i++)
916 SearchResultMatchI match = results.getResults().get(i);
917 int pos = match.getStart();
918 if (pos == match.getEnd())
920 SequenceI seq = match.getSequence();
921 SequenceI ds = seq.getDatasetSequence() == null ? seq
922 : seq.getDatasetSequence();
923 MappedFeatures mf = fr2
924 .findComplementFeaturesAtResidue(ds, pos);
927 for (SequenceFeature sf : mf.features)
929 String pv = mf.findProteinVariants(sf);
930 if (pv.length() > 0 && !infos.contains(pv))
943 StringBuilder sb = new StringBuilder();
944 for (String info : infos)
952 return sb.toString();
956 public VamsasSource getVamsasSource()
958 return this.ap == null ? null : this.ap.av;
962 public void updateColours(SequenceI seq, int index)
964 System.out.println("update the seqPanel colours");
969 * Action on mouse movement is to update the status bar to show the current
970 * sequence position, and (if features are shown) to show any features at the
971 * position in a tooltip. Does nothing if the mouse move does not change
977 public void mouseMoved(MouseEvent evt)
981 // This is because MacOSX creates a mouseMoved
982 // If control is down, other platforms will not.
986 final MousePos mousePos = findMousePosition(evt);
987 if (mousePos.equals(lastMousePosition))
990 * just a pixel move without change of 'cell'
996 lastMousePosition = mousePos;
998 if (mousePos.isOverAnnotation())
1000 mouseMovedOverAnnotation(mousePos);
1003 final int seq = mousePos.seqIndex;
1005 final int column = mousePos.column;
1006 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1008 lastMousePosition = null;
1009 setToolTipText(null);
1011 lastFormattedTooltip = null;
1012 ap.alignFrame.setStatus("");
1016 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1018 if (column >= sequence.getLength())
1024 * set status bar message, returning residue position in sequence
1026 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1027 final int pos = setStatusMessage(sequence, column, seq);
1028 if (ssm != null && !isGapped)
1030 mouseOverSequence(sequence, column, pos);
1033 StringBuilder tooltipText = new StringBuilder(64);
1035 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1038 for (int g = 0; g < groups.length; g++)
1040 if (groups[g].getStartRes() <= column
1041 && groups[g].getEndRes() >= column)
1043 if (!groups[g].getName().startsWith("JTreeGroup")
1044 && !groups[g].getName().startsWith("JGroup"))
1046 tooltipText.append(groups[g].getName());
1049 if (groups[g].getDescription() != null)
1051 tooltipText.append(": " + groups[g].getDescription());
1058 * add any features at the position to the tooltip; if over a gap, only
1059 * add features that straddle the gap (pos may be the residue before or
1062 int unshownFeatures = 0;
1063 if (av.isShowSequenceFeatures())
1065 List<SequenceFeature> features = ap.getFeatureRenderer()
1066 .findFeaturesAtColumn(sequence, column + 1);
1067 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1068 features, this.ap.getSeqPanel().seqCanvas.fr,
1069 MAX_TOOLTIP_LENGTH);
1072 * add features in CDS/protein complement at the corresponding
1073 * position if configured to do so
1075 if (av.isShowComplementFeatures())
1077 if (!Comparison.isGap(sequence.getCharAt(column)))
1079 AlignViewportI complement = ap.getAlignViewport()
1080 .getCodingComplement();
1081 AlignFrame af = Desktop.getAlignFrameFor(complement);
1082 FeatureRendererModel fr2 = af.getFeatureRenderer();
1083 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1087 unshownFeatures = seqARep.appendFeatures(tooltipText,
1088 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1093 if (tooltipText.length() == 0) // nothing added
1095 setToolTipText(null);
1100 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1102 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1103 tooltipText.append("...");
1105 if (unshownFeatures > 0)
1107 tooltipText.append("<br/>").append("... ").append("<i>")
1108 .append(MessageManager.formatMessage(
1109 "label.features_not_shown", unshownFeatures))
1112 String textString = tooltipText.toString();
1113 if (!textString.equals(lastTooltip))
1115 lastTooltip = textString;
1116 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1118 setToolTipText(lastFormattedTooltip);
1124 * When the view is in wrapped mode, and the mouse is over an annotation row,
1125 * shows the corresponding tooltip and status message (if any)
1130 protected void mouseMovedOverAnnotation(MousePos pos)
1132 final int column = pos.column;
1133 final int rowIndex = pos.annotationIndex;
1135 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1140 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1142 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1144 boolean tooltipChanged = tooltip == null ? lastTooltip != null : !tooltip.equals(lastTooltip);
1147 lastTooltip = tooltip;
1148 lastFormattedTooltip = tooltip == null ? null
1149 : JvSwingUtils.wrapTooltip(true, tooltip);
1150 setToolTipText(lastFormattedTooltip);
1153 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1155 ap.alignFrame.setStatus(msg);
1159 * if Shift key is held down while moving the mouse,
1160 * the tooltip location is not changed once shown
1162 private Point lastTooltipLocation = null;
1165 * this flag is false for pixel moves within a residue,
1166 * to reduce tooltip flicker
1168 private boolean moveTooltip = true;
1171 * a dummy tooltip used to estimate where to position tooltips
1173 private JToolTip tempTip = new JLabel().createToolTip();
1178 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1181 public Point getToolTipLocation(MouseEvent event)
1185 if (lastTooltip == null || !moveTooltip)
1190 if (lastTooltipLocation != null && event.isShiftDown())
1192 return lastTooltipLocation;
1195 int x = event.getX();
1196 int y = event.getY();
1199 tempTip.setTipText(lastFormattedTooltip);
1200 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1202 // was x += (w - x < 200) ? -(w / 2) : 5;
1203 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1204 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1206 return lastTooltipLocation = p;
1210 * set when the current UI interaction has resulted in a change that requires
1211 * shading in overviews and structures to be recalculated. this could be
1212 * changed to a something more expressive that indicates what actually has
1213 * changed, so selective redraws can be applied (ie. only structures, only
1216 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1219 * set if av.getSelectionGroup() refers to a group that is defined on the
1220 * alignment view, rather than a transient selection
1222 // private boolean editingDefinedGroup = false; // TODO: refactor to
1223 // avcontroller or viewModel
1226 * Sets the status message in alignment panel, showing the sequence number
1227 * (index) and id, and residue and residue position if not at a gap, for the
1228 * given sequence and column position. Returns the residue position returned
1229 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1230 * if at a gapped position.
1233 * aligned sequence object
1237 * index of sequence in alignment
1238 * @return sequence position of residue at column, or adjacent residue if at a
1241 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1243 char sequenceChar = sequence.getCharAt(column);
1244 int pos = sequence.findPosition(column);
1245 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1251 * Builds the status message for the current cursor location and writes it to
1252 * the status bar, for example
1255 * Sequence 3 ID: FER1_SOLLC
1256 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1257 * Sequence 5 ID: FER1_PEA Residue: B (3)
1258 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1263 * sequence position in the alignment (1..)
1264 * @param sequenceChar
1265 * the character under the cursor
1267 * the sequence residue position (if not over a gap)
1269 protected void setStatusMessage(String seqName, int seqIndex,
1270 char sequenceChar, int residuePos)
1272 StringBuilder text = new StringBuilder(32);
1275 * Sequence number (if known), and sequence name.
1277 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1278 text.append("Sequence").append(seqno).append(" ID: ")
1281 String residue = null;
1284 * Try to translate the display character to residue name (null for gap).
1286 boolean isGapped = Comparison.isGap(sequenceChar);
1290 boolean nucleotide = av.getAlignment().isNucleotide();
1291 String displayChar = String.valueOf(sequenceChar);
1294 residue = ResidueProperties.nucleotideName.get(displayChar);
1298 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1299 : ("*".equals(displayChar) ? "STOP"
1300 : ResidueProperties.aa2Triplet.get(displayChar));
1302 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1303 .append(": ").append(residue == null ? displayChar : residue);
1305 text.append(" (").append(Integer.toString(residuePos)).append(")");
1307 ap.alignFrame.setStatus(text.toString());
1311 * Set the status bar message to highlight the first matched position in
1316 private void setStatusMessage(SearchResultsI results)
1318 AlignmentI al = this.av.getAlignment();
1319 int sequenceIndex = al.findIndex(results);
1320 if (sequenceIndex == -1)
1324 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1325 SequenceI ds = alignedSeq.getDatasetSequence();
1326 for (SearchResultMatchI m : results.getResults())
1328 SequenceI seq = m.getSequence();
1329 if (seq.getDatasetSequence() != null)
1331 seq = seq.getDatasetSequence();
1336 int start = m.getStart();
1337 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1338 seq.getCharAt(start - 1), start);
1348 public void mouseDragged(MouseEvent evt)
1350 MousePos pos = findMousePosition(evt);
1351 if (pos.isOverAnnotation() || pos.column == -1)
1356 if (mouseWheelPressed)
1358 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1359 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1361 int oldWidth = av.getCharWidth();
1363 // Which is bigger, left-right or up-down?
1364 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1365 .abs(evt.getX() - lastMousePress.getX()))
1368 * on drag up or down, decrement or increment font size
1370 int fontSize = av.font.getSize();
1371 boolean fontChanged = false;
1373 if (evt.getY() < lastMousePress.getY())
1378 else if (evt.getY() > lastMousePress.getY())
1391 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1393 av.setFont(newFont, true);
1394 av.setCharWidth(oldWidth);
1398 ap.av.getCodingComplement().setFont(newFont, true);
1399 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1400 .getSplitViewContainer();
1401 splitFrame.adjustLayout();
1402 splitFrame.repaint();
1409 * on drag left or right, decrement or increment character width
1412 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1414 newWidth = av.getCharWidth() - 1;
1415 av.setCharWidth(newWidth);
1417 else if (evt.getX() > lastMousePress.getX())
1419 newWidth = av.getCharWidth() + 1;
1420 av.setCharWidth(newWidth);
1424 ap.paintAlignment(false, false);
1428 * need to ensure newWidth is set on cdna, regardless of which
1429 * panel the mouse drag happened in; protein will compute its
1430 * character width as 1:1 or 3:1
1432 av.getCodingComplement().setCharWidth(newWidth);
1433 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1434 .getSplitViewContainer();
1435 splitFrame.adjustLayout();
1436 splitFrame.repaint();
1441 FontMetrics fm = getFontMetrics(av.getFont());
1442 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1444 lastMousePress = evt.getPoint();
1451 dragStretchGroup(evt);
1455 int res = pos.column;
1462 if ((editLastRes == -1) || (editLastRes == res))
1467 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1469 // dragLeft, delete gap
1470 editSequence(false, false, res);
1474 editSequence(true, false, res);
1477 mouseDragging = true;
1478 if (scrollThread != null)
1480 scrollThread.setMousePosition(evt.getPoint());
1485 * Edits the sequence to insert or delete one or more gaps, in response to a
1486 * mouse drag or cursor mode command. The number of inserts/deletes may be
1487 * specified with the cursor command, or else depends on the mouse event
1488 * (normally one column, but potentially more for a fast mouse drag).
1490 * Delete gaps is limited to the number of gaps left of the cursor position
1491 * (mouse drag), or at or right of the cursor position (cursor mode).
1493 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1494 * the current selection group.
1496 * In locked editing mode (with a selection group present), inserts/deletions
1497 * within the selection group are limited to its boundaries (and edits outside
1498 * the group stop at its border).
1501 * true to insert gaps, false to delete gaps
1503 * (unused parameter)
1505 * the column at which to perform the action; the number of columns
1506 * affected depends on <code>this.editLastRes</code> (cursor column
1509 synchronized void editSequence(boolean insertGap, boolean editSeq,
1513 int fixedRight = -1;
1514 boolean fixedColumns = false;
1515 SequenceGroup sg = av.getSelectionGroup();
1517 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1519 // No group, but the sequence may represent a group
1520 if (!groupEditing && av.hasHiddenRows())
1522 if (av.isHiddenRepSequence(seq))
1524 sg = av.getRepresentedSequences(seq);
1525 groupEditing = true;
1529 StringBuilder message = new StringBuilder(64); // for status bar
1532 * make a name for the edit action, for
1533 * status bar message and Undo/Redo menu
1535 String label = null;
1538 message.append("Edit group:");
1539 label = MessageManager.getString("action.edit_group");
1543 message.append("Edit sequence: " + seq.getName());
1544 label = seq.getName();
1545 if (label.length() > 10)
1547 label = label.substring(0, 10);
1549 label = MessageManager.formatMessage("label.edit_params",
1555 * initialise the edit command if there is not
1556 * already one being extended
1558 if (editCommand == null)
1560 editCommand = new EditCommand(label);
1565 message.append(" insert ");
1569 message.append(" delete ");
1572 message.append(Math.abs(startres - editLastRes) + " gaps.");
1573 ap.alignFrame.setStatus(message.toString());
1576 * is there a selection group containing the sequence being edited?
1577 * if so the boundary of the group is the limit of the edit
1578 * (but the edit may be inside or outside the selection group)
1580 boolean inSelectionGroup = sg != null
1581 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1582 if (groupEditing || inSelectionGroup)
1584 fixedColumns = true;
1586 // sg might be null as the user may only see 1 sequence,
1587 // but the sequence represents a group
1590 if (!av.isHiddenRepSequence(seq))
1595 sg = av.getRepresentedSequences(seq);
1598 fixedLeft = sg.getStartRes();
1599 fixedRight = sg.getEndRes();
1601 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1602 || (startres >= fixedLeft && editLastRes < fixedLeft)
1603 || (startres > fixedRight && editLastRes <= fixedRight)
1604 || (startres <= fixedRight && editLastRes > fixedRight))
1610 if (fixedLeft > startres)
1612 fixedRight = fixedLeft - 1;
1615 else if (fixedRight < startres)
1617 fixedLeft = fixedRight;
1622 if (av.hasHiddenColumns())
1624 fixedColumns = true;
1625 int y1 = av.getAlignment().getHiddenColumns()
1626 .getNextHiddenBoundary(true, startres);
1627 int y2 = av.getAlignment().getHiddenColumns()
1628 .getNextHiddenBoundary(false, startres);
1630 if ((insertGap && startres > y1 && editLastRes < y1)
1631 || (!insertGap && startres < y2 && editLastRes > y2))
1637 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1638 // Selection spans a hidden region
1639 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1647 fixedRight = y2 - 1;
1652 boolean success = doEditSequence(insertGap, editSeq, startres,
1653 fixedRight, fixedColumns, sg);
1656 * report what actually happened (might be less than
1657 * what was requested), by inspecting the edit commands added
1659 String msg = getEditStatusMessage(editCommand);
1660 ap.alignFrame.setStatus(msg == null ? " " : msg);
1666 editLastRes = startres;
1667 seqCanvas.repaint();
1671 * A helper method that performs the requested editing to insert or delete
1672 * gaps (if possible). Answers true if the edit was successful, false if could
1673 * only be performed in part or not at all. Failure may occur in 'locked edit'
1674 * mode, when an insertion requires a matching gapped position (or column) to
1675 * delete, and deletion requires an adjacent gapped position (or column) to
1679 * true if inserting gap(s), false if deleting
1681 * (unused parameter, currently always false)
1683 * the column at which to perform the edit
1685 * fixed right boundary column of a locked edit (within or to the
1686 * left of a selection group)
1687 * @param fixedColumns
1688 * true if this is a locked edit
1690 * the sequence group (if group edit is being performed)
1693 protected boolean doEditSequence(final boolean insertGap,
1694 final boolean editSeq, final int startres, int fixedRight,
1695 final boolean fixedColumns, final SequenceGroup sg)
1697 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1698 SequenceI[] seqs = new SequenceI[] { seq };
1702 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1703 int g, groupSize = vseqs.size();
1704 SequenceI[] groupSeqs = new SequenceI[groupSize];
1705 for (g = 0; g < groupSeqs.length; g++)
1707 groupSeqs[g] = vseqs.get(g);
1713 // If the user has selected the whole sequence, and is dragging to
1714 // the right, we can still extend the alignment and selectionGroup
1715 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1716 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1719 av.getAlignment().getWidth() + startres - editLastRes);
1720 fixedRight = sg.getEndRes();
1723 // Is it valid with fixed columns??
1724 // Find the next gap before the end
1725 // of the visible region boundary
1726 boolean blank = false;
1727 for (; fixedRight > editLastRes; fixedRight--)
1731 for (g = 0; g < groupSize; g++)
1733 for (int j = 0; j < startres - editLastRes; j++)
1736 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1751 if (sg.getSize() == av.getAlignment().getHeight())
1753 if ((av.hasHiddenColumns()
1754 && startres < av.getAlignment().getHiddenColumns()
1755 .getNextHiddenBoundary(false, startres)))
1760 int alWidth = av.getAlignment().getWidth();
1761 if (av.hasHiddenRows())
1763 int hwidth = av.getAlignment().getHiddenSequences()
1765 if (hwidth > alWidth)
1770 // We can still insert gaps if the selectionGroup
1771 // contains all the sequences
1772 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1773 fixedRight = alWidth + startres - editLastRes;
1783 else if (!insertGap)
1785 // / Are we able to delete?
1786 // ie are all columns blank?
1788 for (g = 0; g < groupSize; g++)
1790 for (int j = startres; j < editLastRes; j++)
1792 if (groupSeqs[g].getLength() <= j)
1797 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1799 // Not a gap, block edit not valid
1808 // dragging to the right
1809 if (fixedColumns && fixedRight != -1)
1811 for (int j = editLastRes; j < startres; j++)
1813 insertGap(j, groupSeqs, fixedRight);
1818 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1819 startres - editLastRes, false);
1824 // dragging to the left
1825 if (fixedColumns && fixedRight != -1)
1827 for (int j = editLastRes; j > startres; j--)
1829 deleteChar(startres, groupSeqs, fixedRight);
1834 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1835 editLastRes - startres, false);
1842 * editing a single sequence
1846 // dragging to the right
1847 if (fixedColumns && fixedRight != -1)
1849 for (int j = editLastRes; j < startres; j++)
1851 if (!insertGap(j, seqs, fixedRight))
1854 * e.g. cursor mode command specified
1855 * more inserts than are possible
1863 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1864 startres - editLastRes, false);
1871 // dragging to the left
1872 if (fixedColumns && fixedRight != -1)
1874 for (int j = editLastRes; j > startres; j--)
1876 if (!Comparison.isGap(seq.getCharAt(startres)))
1880 deleteChar(startres, seqs, fixedRight);
1885 // could be a keyboard edit trying to delete none gaps
1887 for (int m = startres; m < editLastRes; m++)
1889 if (!Comparison.isGap(seq.getCharAt(m)))
1897 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1902 {// insertGap==false AND editSeq==TRUE;
1903 if (fixedColumns && fixedRight != -1)
1905 for (int j = editLastRes; j < startres; j++)
1907 insertGap(j, seqs, fixedRight);
1912 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1913 startres - editLastRes, false);
1923 * Constructs an informative status bar message while dragging to insert or
1924 * delete gaps. Answers null if inserts and deletes cancel out.
1926 * @param editCommand
1927 * a command containing the list of individual edits
1930 protected static String getEditStatusMessage(EditCommand editCommand)
1932 if (editCommand == null)
1938 * add any inserts, and subtract any deletes,
1939 * not counting those auto-inserted when doing a 'locked edit'
1940 * (so only counting edits 'under the cursor')
1943 for (Edit cmd : editCommand.getEdits())
1945 if (!cmd.isSystemGenerated())
1947 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1955 * inserts and deletes cancel out
1960 String msgKey = count > 1 ? "label.insert_gaps"
1961 : (count == 1 ? "label.insert_gap"
1962 : (count == -1 ? "label.delete_gap"
1963 : "label.delete_gaps"));
1964 count = Math.abs(count);
1966 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1970 * Inserts one gap at column j, deleting the right-most gapped column up to
1971 * (and including) fixedColumn. Returns true if the edit is successful, false
1972 * if no blank column is available to allow the insertion to be balanced by a
1977 * @param fixedColumn
1980 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1982 int blankColumn = fixedColumn;
1983 for (int s = 0; s < seq.length; s++)
1985 // Find the next gap before the end of the visible region boundary
1986 // If lastCol > j, theres a boundary after the gap insertion
1988 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1990 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1992 // Theres a space, so break and insert the gap
1997 if (blankColumn <= j)
1999 blankColumn = fixedColumn;
2005 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2007 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2013 * Helper method to add and perform one edit action
2019 * @param systemGenerated
2020 * true if the edit is a 'balancing' delete (or insert) to match a
2021 * user's insert (or delete) in a locked editing region
2023 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2024 int count, boolean systemGenerated)
2027 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2028 av.getAlignment().getGapCharacter());
2029 edit.setSystemGenerated(systemGenerated);
2031 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2035 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2036 * each of the given sequences. The caller should ensure that all sequences
2037 * are gapped in column j.
2041 * @param fixedColumn
2043 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2045 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2047 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2051 * On reentering the panel, stops any scrolling that was started on dragging
2057 public void mouseEntered(MouseEvent e)
2067 * On leaving the panel, if the mouse is being dragged, starts a thread to
2068 * scroll it until the mouse is released (in unwrapped mode only)
2073 public void mouseExited(MouseEvent e)
2075 lastMousePosition = null;
2076 ap.alignFrame.setStatus(" ");
2077 if (av.getWrapAlignment())
2082 if (mouseDragging && scrollThread == null)
2084 startScrolling(e.getPoint());
2089 * Handler for double-click on a position with one or more sequence features.
2090 * Opens the Amend Features dialog to allow feature details to be amended, or
2091 * the feature deleted.
2094 public void mouseClicked(MouseEvent evt)
2096 SequenceGroup sg = null;
2097 MousePos pos = findMousePosition(evt);
2098 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2103 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2105 sg = av.getSelectionGroup();
2106 if (sg != null && sg.getSize() == 1
2107 && sg.getEndRes() - sg.getStartRes() < 2)
2109 av.setSelectionGroup(null);
2112 int column = pos.column;
2115 * find features at the position (if not gapped), or straddling
2116 * the position (if at a gap)
2118 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2119 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2120 .findFeaturesAtColumn(sequence, column + 1);
2122 if (!features.isEmpty())
2125 * highlight the first feature at the position on the alignment
2127 SearchResultsI highlight = new SearchResults();
2128 highlight.addResult(sequence, features.get(0).getBegin(), features
2130 seqCanvas.highlightSearchResults(highlight, true);
2133 * open the Amend Features dialog
2135 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2136 false).showDialog();
2142 public void mouseWheelMoved(MouseWheelEvent e)
2145 double wheelRotation = e.getPreciseWheelRotation();
2146 if (wheelRotation > 0)
2148 if (e.isShiftDown())
2150 av.getRanges().scrollRight(true);
2155 av.getRanges().scrollUp(false);
2158 else if (wheelRotation < 0)
2160 if (e.isShiftDown())
2162 av.getRanges().scrollRight(false);
2166 av.getRanges().scrollUp(true);
2171 * update status bar and tooltip for new position
2172 * (need to synthesize a mouse movement to refresh tooltip)
2175 ToolTipManager.sharedInstance().mouseMoved(e);
2184 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2186 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2191 final int res = pos.column;
2192 final int seq = pos.seqIndex;
2194 updateOverviewAndStructs = false;
2196 startWrapBlock = wrappedBlock;
2198 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2200 if ((sequence == null) || (res > sequence.getLength()))
2205 stretchGroup = av.getSelectionGroup();
2207 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2209 stretchGroup = av.getAlignment().findGroup(sequence, res);
2210 if (stretchGroup != null)
2212 // only update the current selection if the popup menu has a group to
2214 av.setSelectionGroup(stretchGroup);
2219 * defer right-mouse click handling to mouseReleased on Windows
2220 * (where isPopupTrigger() will answer true)
2221 * NB isRightMouseButton is also true for Cmd-click on Mac
2223 if (Platform.isWinRightButton(evt))
2228 if (evt.isPopupTrigger()) // Mac: mousePressed
2230 showPopupMenu(evt, pos);
2236 seqCanvas.cursorX = res;
2237 seqCanvas.cursorY = seq;
2238 seqCanvas.repaint();
2242 if (stretchGroup == null)
2244 createStretchGroup(res, sequence);
2247 if (stretchGroup != null)
2249 stretchGroup.addPropertyChangeListener(seqCanvas);
2252 seqCanvas.repaint();
2255 private void createStretchGroup(int res, SequenceI sequence)
2257 // Only if left mouse button do we want to change group sizes
2258 // define a new group here
2259 SequenceGroup sg = new SequenceGroup();
2260 sg.setStartRes(res);
2262 sg.addSequence(sequence, false);
2263 av.setSelectionGroup(sg);
2266 if (av.getConservationSelected())
2268 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2272 if (av.getAbovePIDThreshold())
2274 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2277 // TODO: stretchGroup will always be not null. Is this a merge error ?
2278 // or is there a threading issue here?
2279 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2281 // Edit end res position of selected group
2282 changeEndRes = true;
2284 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2286 // Edit end res position of selected group
2287 changeStartRes = true;
2289 stretchGroup.getWidth();
2294 * Build and show a pop-up menu at the right-click mouse position
2299 void showPopupMenu(MouseEvent evt, MousePos pos)
2301 final int column = pos.column;
2302 final int seq = pos.seqIndex;
2303 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2304 if (sequence != null)
2306 PopupMenu pop = new PopupMenu(ap, sequence, column);
2307 pop.show(this, evt.getX(), evt.getY());
2312 * Update the display after mouse up on a selection or group
2315 * mouse released event details
2317 * true if this event is happening after a mouse drag (rather than a
2320 protected void doMouseReleasedDefineMode(MouseEvent evt,
2323 if (stretchGroup == null)
2328 stretchGroup.removePropertyChangeListener(seqCanvas);
2330 // always do this - annotation has own state
2331 // but defer colourscheme update until hidden sequences are passed in
2332 boolean vischange = stretchGroup.recalcConservation(true);
2333 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2335 if (stretchGroup.cs != null)
2339 stretchGroup.cs.alignmentChanged(stretchGroup,
2340 av.getHiddenRepSequences());
2343 ResidueShaderI groupColourScheme = stretchGroup
2344 .getGroupColourScheme();
2345 String name = stretchGroup.getName();
2346 if (stretchGroup.cs.conservationApplied())
2348 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2350 if (stretchGroup.cs.getThreshold() > 0)
2352 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2355 PaintRefresher.Refresh(this, av.getSequenceSetId());
2356 // TODO: structure colours only need updating if stretchGroup used to or now
2357 // does contain sequences with structure views
2358 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2359 updateOverviewAndStructs = false;
2360 changeEndRes = false;
2361 changeStartRes = false;
2362 stretchGroup = null;
2367 * Resizes the borders of a selection group depending on the direction of
2372 protected void dragStretchGroup(MouseEvent evt)
2374 if (stretchGroup == null)
2379 MousePos pos = findMousePosition(evt);
2380 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2385 int res = pos.column;
2386 int y = pos.seqIndex;
2388 if (wrappedBlock != startWrapBlock)
2393 res = Math.min(res, av.getAlignment().getWidth()-1);
2395 if (stretchGroup.getEndRes() == res)
2397 // Edit end res position of selected group
2398 changeEndRes = true;
2400 else if (stretchGroup.getStartRes() == res)
2402 // Edit start res position of selected group
2403 changeStartRes = true;
2406 if (res < av.getRanges().getStartRes())
2408 res = av.getRanges().getStartRes();
2413 if (res > (stretchGroup.getStartRes() - 1))
2415 stretchGroup.setEndRes(res);
2416 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2419 else if (changeStartRes)
2421 if (res < (stretchGroup.getEndRes() + 1))
2423 stretchGroup.setStartRes(res);
2424 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2428 int dragDirection = 0;
2434 else if (y < oldSeq)
2439 while ((y != oldSeq) && (oldSeq > -1)
2440 && (y < av.getAlignment().getHeight()))
2442 // This routine ensures we don't skip any sequences, as the
2443 // selection is quite slow.
2444 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2446 oldSeq += dragDirection;
2453 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2455 if (stretchGroup.getSequences(null).contains(nextSeq))
2457 stretchGroup.deleteSequence(seq, false);
2458 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2464 stretchGroup.addSequence(seq, false);
2467 stretchGroup.addSequence(nextSeq, false);
2468 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2477 mouseDragging = true;
2479 if (scrollThread != null)
2481 scrollThread.setMousePosition(evt.getPoint());
2485 * construct a status message showing the range of the selection
2487 StringBuilder status = new StringBuilder(64);
2488 List<SequenceI> seqs = stretchGroup.getSequences();
2489 String name = seqs.get(0).getName();
2490 if (name.length() > 20)
2492 name = name.substring(0, 20);
2494 status.append(name).append(" - ");
2495 name = seqs.get(seqs.size() - 1).getName();
2496 if (name.length() > 20)
2498 name = name.substring(0, 20);
2500 status.append(name).append(" ");
2501 int startRes = stretchGroup.getStartRes();
2502 status.append(" cols ").append(String.valueOf(startRes + 1))
2504 int endRes = stretchGroup.getEndRes();
2505 status.append(String.valueOf(endRes + 1));
2506 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2507 .append(String.valueOf(endRes - startRes + 1)).append(")");
2508 ap.alignFrame.setStatus(status.toString());
2512 * Stops the scroll thread if it is running
2514 void stopScrolling()
2516 if (scrollThread != null)
2518 scrollThread.stopScrolling();
2519 scrollThread = null;
2521 mouseDragging = false;
2525 * Starts a thread to scroll the alignment, towards a given mouse position
2526 * outside the panel bounds, unless the alignment is in wrapped mode
2530 void startScrolling(Point mousePos)
2533 * set this.mouseDragging in case this was called from
2534 * a drag in ScalePanel or AnnotationPanel
2536 mouseDragging = true;
2537 if (!av.getWrapAlignment() && scrollThread == null)
2539 scrollThread = new ScrollThread();
2540 scrollThread.setMousePosition(mousePos);
2541 if (Platform.isJS())
2544 * Javascript - run every 20ms until scrolling stopped
2545 * or reaches the limit of scrollable alignment
2547 Timer t = new Timer(20, new ActionListener()
2550 public void actionPerformed(ActionEvent e)
2552 if (scrollThread != null)
2554 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2555 scrollThread.scrollOnce();
2559 t.addActionListener(new ActionListener()
2562 public void actionPerformed(ActionEvent e)
2564 if (scrollThread == null)
2566 // SeqPanel.stopScrolling called
2576 * Java - run in a new thread
2578 scrollThread.start();
2584 * Performs scrolling of the visible alignment left, right, up or down, until
2585 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2586 * limit of the alignment is reached
2588 class ScrollThread extends Thread
2590 private Point mousePos;
2592 private volatile boolean keepRunning = true;
2597 public ScrollThread()
2599 setName("SeqPanel$ScrollThread");
2603 * Sets the position of the mouse that determines the direction of the
2604 * scroll to perform. If this is called as the mouse moves, scrolling should
2605 * respond accordingly. For example, if the mouse is dragged right, scroll
2606 * right should start; if the drag continues down, scroll down should also
2611 public void setMousePosition(Point p)
2617 * Sets a flag that will cause the thread to exit
2619 public void stopScrolling()
2621 keepRunning = false;
2625 * Scrolls the alignment left or right, and/or up or down, depending on the
2626 * last notified mouse position, until the limit of the alignment is
2627 * reached, or a flag is set to stop the scroll
2634 if (mousePos != null)
2636 keepRunning = scrollOnce();
2641 } catch (Exception ex)
2645 SeqPanel.this.scrollThread = null;
2651 * <li>one row up, if the mouse is above the panel</li>
2652 * <li>one row down, if the mouse is below the panel</li>
2653 * <li>one column left, if the mouse is left of the panel</li>
2654 * <li>one column right, if the mouse is right of the panel</li>
2656 * Answers true if a scroll was performed, false if not - meaning either
2657 * that the mouse position is within the panel, or the edge of the alignment
2660 boolean scrollOnce()
2663 * quit after mouseUp ensures interrupt in JalviewJS
2670 boolean scrolled = false;
2671 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2678 // mouse is above this panel - try scroll up
2679 scrolled = ranges.scrollUp(true);
2681 else if (mousePos.y >= getHeight())
2683 // mouse is below this panel - try scroll down
2684 scrolled = ranges.scrollUp(false);
2688 * scroll left or right
2692 scrolled |= ranges.scrollRight(false);
2694 else if (mousePos.x >= getWidth())
2696 scrolled |= ranges.scrollRight(true);
2703 * modify current selection according to a received message.
2706 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2707 HiddenColumns hidden, SelectionSource source)
2709 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2710 // handles selection messages...
2711 // TODO: extend config options to allow user to control if selections may be
2712 // shared between viewports.
2713 boolean iSentTheSelection = (av == source
2714 || (source instanceof AlignViewport
2715 && ((AlignmentViewport) source).getSequenceSetId()
2716 .equals(av.getSequenceSetId())));
2718 if (iSentTheSelection)
2720 // respond to our own event by updating dependent dialogs
2721 if (ap.getCalculationDialog() != null)
2723 ap.getCalculationDialog().validateCalcTypes();
2729 // process further ?
2730 if (!av.followSelection)
2736 * Ignore the selection if there is one of our own pending.
2738 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2744 * Check for selection in a view of which this one is a dna/protein
2747 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2752 // do we want to thread this ? (contention with seqsel and colsel locks, I
2755 * only copy colsel if there is a real intersection between
2756 * sequence selection and this panel's alignment
2758 boolean repaint = false;
2759 boolean copycolsel = false;
2761 SequenceGroup sgroup = null;
2762 if (seqsel != null && seqsel.getSize() > 0)
2764 if (av.getAlignment() == null)
2766 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2767 + " ViewId=" + av.getViewId()
2768 + " 's alignment is NULL! returning immediately.");
2771 sgroup = seqsel.intersect(av.getAlignment(),
2772 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2773 if ((sgroup != null && sgroup.getSize() > 0))
2778 if (sgroup != null && sgroup.getSize() > 0)
2780 av.setSelectionGroup(sgroup);
2784 av.setSelectionGroup(null);
2786 av.isSelectionGroupChanged(true);
2791 // the current selection is unset or from a previous message
2792 // so import the new colsel.
2793 if (colsel == null || colsel.isEmpty())
2795 if (av.getColumnSelection() != null)
2797 av.getColumnSelection().clear();
2803 // TODO: shift colSel according to the intersecting sequences
2804 if (av.getColumnSelection() == null)
2806 av.setColumnSelection(new ColumnSelection(colsel));
2810 av.getColumnSelection().setElementsFrom(colsel,
2811 av.getAlignment().getHiddenColumns());
2814 av.isColSelChanged(true);
2818 if (copycolsel && av.hasHiddenColumns()
2819 && (av.getAlignment().getHiddenColumns() == null))
2821 System.err.println("Bad things");
2823 if (repaint) // always true!
2825 // probably finessing with multiple redraws here
2826 PaintRefresher.Refresh(this, av.getSequenceSetId());
2827 // ap.paintAlignment(false);
2830 // lastly, update dependent dialogs
2831 if (ap.getCalculationDialog() != null)
2833 ap.getCalculationDialog().validateCalcTypes();
2839 * If this panel is a cdna/protein translation view of the selection source,
2840 * tries to map the source selection to a local one, and returns true. Else
2847 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2848 ColumnSelection colsel, HiddenColumns hidden,
2849 SelectionSource source)
2851 if (!(source instanceof AlignViewportI))
2855 final AlignViewportI sourceAv = (AlignViewportI) source;
2856 if (sourceAv.getCodingComplement() != av
2857 && av.getCodingComplement() != sourceAv)
2863 * Map sequence selection
2865 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2866 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2867 av.isSelectionGroupChanged(true);
2870 * Map column selection
2872 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2874 ColumnSelection cs = new ColumnSelection();
2875 HiddenColumns hs = new HiddenColumns();
2876 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2877 av.setColumnSelection(cs);
2878 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2880 // lastly, update any dependent dialogs
2881 if (ap.getCalculationDialog() != null)
2883 ap.getCalculationDialog().validateCalcTypes();
2887 * repaint alignment, and also Overview or Structure
2888 * if hidden column selection has changed
2890 ap.paintAlignment(hiddenChanged, hiddenChanged);
2897 * @return null or last search results handled by this panel
2899 public SearchResultsI getLastSearchResults()
2901 return lastSearchResults;
2905 * scroll to the given row/column - or nearest visible location
2910 public void scrollTo(int row, int column)
2913 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2914 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2915 ap.scrollTo(column, column, row, true, true);
2919 * scroll to the given row - or nearest visible location
2923 public void scrollToRow(int row)
2926 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2927 ap.scrollTo(ap.av.getRanges().getStartRes(),
2928 ap.av.getRanges().getStartRes(), row, true, true);
2932 * scroll to the given column - or nearest visible location
2936 public void scrollToColumn(int column)
2939 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2940 ap.scrollTo(column, column, ap.av.getRanges().getStartSeq(), true,