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)
246 seqARep = new SequenceAnnotationReport(true);
247 ToolTipManager.sharedInstance().registerComponent(this);
248 ToolTipManager.sharedInstance().setInitialDelay(0);
249 ToolTipManager.sharedInstance().setDismissDelay(10000);
253 setBackground(Color.white);
255 seqCanvas = new SeqCanvas(alignPanel);
256 setLayout(new BorderLayout());
257 add(seqCanvas, BorderLayout.CENTER);
259 this.ap = alignPanel;
261 if (!viewport.isDataset())
263 addMouseMotionListener(this);
264 addMouseListener(this);
265 addMouseWheelListener(this);
266 ssm = viewport.getStructureSelectionManager();
267 ssm.addStructureViewerListener(this);
268 ssm.addSelectionListener(this);
272 int startWrapBlock = -1;
274 int wrappedBlock = -1;
277 * Computes the column and sequence row (and possibly annotation row when in
278 * wrapped mode) for the given mouse position
283 MousePos findMousePosition(MouseEvent evt)
285 int col = findColumn(evt);
290 int charHeight = av.getCharHeight();
291 int alignmentHeight = av.getAlignment().getHeight();
292 if (av.getWrapAlignment())
294 seqCanvas.calculateWrappedGeometry();
297 * yPos modulo height of repeating width
299 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
302 * height of sequences plus space / scale above,
303 * plus gap between sequences and annotations
305 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
306 + alignmentHeight * charHeight
307 + SeqCanvas.SEQS_ANNOTATION_GAP;
308 if (yOffsetPx >= alignmentHeightPixels)
311 * mouse is over annotations; find annotation index, also set
312 * last sequence above (for backwards compatible behaviour)
314 AlignmentAnnotation[] anns = av.getAlignment()
315 .getAlignmentAnnotation();
316 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
317 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
318 seqIndex = alignmentHeight - 1;
323 * mouse is over sequence (or the space above sequences)
325 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
328 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
334 ViewportRanges ranges = av.getRanges();
335 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
336 alignmentHeight - 1);
337 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
340 return new MousePos(col, seqIndex, annIndex);
343 * Returns the aligned sequence position (base 0) at the mouse position, or
344 * the closest visible one
349 int findColumn(MouseEvent evt)
354 final int startRes = av.getRanges().getStartRes();
355 final int charWidth = av.getCharWidth();
357 if (av.getWrapAlignment())
359 int hgap = av.getCharHeight();
360 if (av.getScaleAboveWrapped())
362 hgap += av.getCharHeight();
365 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
366 + hgap + seqCanvas.getAnnotationHeight();
369 y = Math.max(0, y - hgap);
370 x -= seqCanvas.getLabelWidthWest();
373 // mouse is over left scale
377 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
382 if (x >= cwidth * charWidth)
384 // mouse is over right scale
388 wrappedBlock = y / cHeight;
389 wrappedBlock += startRes / cwidth;
390 // allow for wrapped view scrolled right (possible from Overview)
391 int startOffset = startRes % cwidth;
392 res = wrappedBlock * cwidth + startOffset
393 + Math.min(cwidth - 1, x / charWidth);
398 * make sure we calculate relative to visible alignment,
399 * rather than right-hand gutter
401 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
402 res = (x / charWidth) + startRes;
403 res = Math.min(res, av.getRanges().getEndRes());
406 if (av.hasHiddenColumns())
408 res = av.getAlignment().getHiddenColumns()
409 .visibleToAbsoluteColumn(res);
416 * When all of a sequence of edits are complete, put the resulting edit list
417 * on the history stack (undo list), and reset flags for editing in progress.
423 if (editCommand != null && editCommand.getSize() > 0)
425 ap.alignFrame.addHistoryItem(editCommand);
426 ap.av.notifyAlignment();
431 * Tidy up come what may...
436 groupEditing = false;
445 seqCanvas.cursorY = getKeyboardNo1() - 1;
446 scrollToVisible(true);
449 void setCursorColumn()
451 seqCanvas.cursorX = getKeyboardNo1() - 1;
452 scrollToVisible(true);
455 void setCursorRowAndColumn()
457 if (keyboardNo2 == null)
459 keyboardNo2 = new StringBuffer();
463 seqCanvas.cursorX = getKeyboardNo1() - 1;
464 seqCanvas.cursorY = getKeyboardNo2() - 1;
465 scrollToVisible(true);
469 void setCursorPosition()
471 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
473 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
474 scrollToVisible(true);
477 void moveCursor(int dx, int dy)
479 seqCanvas.cursorX += dx;
480 seqCanvas.cursorY += dy;
482 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
484 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
486 int original = seqCanvas.cursorX - dx;
487 int maxWidth = av.getAlignment().getWidth();
489 if (!hidden.isVisible(seqCanvas.cursorX))
491 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
492 int[] region = hidden.getRegionWithEdgeAtRes(visx);
494 if (region != null) // just in case
499 seqCanvas.cursorX = region[1] + 1;
504 seqCanvas.cursorX = region[0] - 1;
507 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
510 if (seqCanvas.cursorX >= maxWidth
511 || !hidden.isVisible(seqCanvas.cursorX))
513 seqCanvas.cursorX = original;
517 scrollToVisible(false);
521 * Scroll to make the cursor visible in the viewport.
524 * just jump to the location rather than scrolling
526 void scrollToVisible(boolean jump)
528 if (seqCanvas.cursorX < 0)
530 seqCanvas.cursorX = 0;
532 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
534 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
537 if (seqCanvas.cursorY < 0)
539 seqCanvas.cursorY = 0;
541 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
543 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
548 boolean repaintNeeded = true;
551 // only need to repaint if the viewport did not move, as otherwise it will
553 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
558 if (av.getWrapAlignment())
560 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
561 int x = av.getAlignment().getHiddenColumns()
562 .absoluteToVisibleColumn(seqCanvas.cursorX);
563 av.getRanges().scrollToWrappedVisible(x);
567 av.getRanges().scrollToVisible(seqCanvas.cursorX,
572 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
574 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
575 seqCanvas.cursorX, seqCanvas.cursorY);
585 void setSelectionAreaAtCursor(boolean topLeft)
587 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
589 if (av.getSelectionGroup() != null)
591 SequenceGroup sg = av.getSelectionGroup();
592 // Find the top and bottom of this group
593 int min = av.getAlignment().getHeight(), max = 0;
594 for (int i = 0; i < sg.getSize(); i++)
596 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
611 sg.setStartRes(seqCanvas.cursorX);
612 if (sg.getEndRes() < seqCanvas.cursorX)
614 sg.setEndRes(seqCanvas.cursorX);
617 min = seqCanvas.cursorY;
621 sg.setEndRes(seqCanvas.cursorX);
622 if (sg.getStartRes() > seqCanvas.cursorX)
624 sg.setStartRes(seqCanvas.cursorX);
627 max = seqCanvas.cursorY + 1;
632 // Only the user can do this
633 av.setSelectionGroup(null);
637 // Now add any sequences between min and max
638 sg.getSequences(null).clear();
639 for (int i = min; i < max; i++)
641 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
646 if (av.getSelectionGroup() == null)
648 SequenceGroup sg = new SequenceGroup();
649 sg.setStartRes(seqCanvas.cursorX);
650 sg.setEndRes(seqCanvas.cursorX);
651 sg.addSequence(sequence, false);
652 av.setSelectionGroup(sg);
655 ap.paintAlignment(false, false);
659 void insertGapAtCursor(boolean group)
661 groupEditing = group;
662 editStartSeq = seqCanvas.cursorY;
663 editLastRes = seqCanvas.cursorX;
664 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
668 void deleteGapAtCursor(boolean group)
670 groupEditing = group;
671 editStartSeq = seqCanvas.cursorY;
672 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
673 editSequence(false, false, seqCanvas.cursorX);
677 void insertNucAtCursor(boolean group, String nuc)
679 // TODO not called - delete?
680 groupEditing = group;
681 editStartSeq = seqCanvas.cursorY;
682 editLastRes = seqCanvas.cursorX;
683 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
687 void numberPressed(char value)
689 if (keyboardNo1 == null)
691 keyboardNo1 = new StringBuffer();
694 if (keyboardNo2 != null)
696 keyboardNo2.append(value);
700 keyboardNo1.append(value);
708 if (keyboardNo1 != null)
710 int value = Integer.parseInt(keyboardNo1.toString());
714 } catch (Exception x)
725 if (keyboardNo2 != null)
727 int value = Integer.parseInt(keyboardNo2.toString());
731 } catch (Exception x)
745 public void mouseReleased(MouseEvent evt)
747 MousePos pos = findMousePosition(evt);
748 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
753 boolean didDrag = mouseDragging; // did we come here after a drag
754 mouseDragging = false;
755 mouseWheelPressed = false;
757 if (evt.isPopupTrigger()) // Windows: mouseReleased
759 showPopupMenu(evt, pos);
770 doMouseReleasedDefineMode(evt, didDrag);
781 public void mousePressed(MouseEvent evt)
783 lastMousePress = evt.getPoint();
784 MousePos pos = findMousePosition(evt);
785 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
790 if (SwingUtilities.isMiddleMouseButton(evt))
792 mouseWheelPressed = true;
796 boolean isControlDown = Platform.isControlDown(evt);
797 if (evt.isShiftDown() || isControlDown)
807 doMousePressedDefineMode(evt, pos);
811 int seq = pos.seqIndex;
812 int res = pos.column;
814 if ((seq < av.getAlignment().getHeight())
815 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
832 public void mouseOverSequence(SequenceI sequence, int index, int pos)
834 String tmp = sequence.hashCode() + " " + index + " " + pos;
836 if (lastMessage == null || !lastMessage.equals(tmp))
838 // System.err.println("mouseOver Sequence: "+tmp);
839 ssm.mouseOverSequence(sequence, index, pos, av);
845 * Highlight the mapped region described by the search results object (unless
846 * unchanged). This supports highlight of protein while mousing over linked
847 * cDNA and vice versa. The status bar is also updated to show the location of
848 * the start of the highlighted region.
851 public String highlightSequence(SearchResultsI results)
853 if (results == null || results.equals(lastSearchResults))
857 lastSearchResults = results;
859 boolean wasScrolled = false;
861 if (av.isFollowHighlight())
863 // don't allow highlight of protein/cDNA to also scroll a complementary
864 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
865 // over residue to change abruptly, causing highlighted residue in panel 2
866 // to change, causing a scroll in panel 1 etc)
867 ap.setToScrollComplementPanel(false);
868 wasScrolled = ap.scrollToPosition(results);
871 seqCanvas.revalidate();
873 ap.setToScrollComplementPanel(true);
876 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
877 if (seqCanvas.highlightSearchResults(results, fastPaint))
879 setStatusMessage(results);
881 return results.isEmpty() ? null : getHighlightInfo(results);
885 * temporary hack: answers a message suitable to show on structure hover
886 * label. This is normally null. It is a peptide variation description if
888 * <li>results are a single residue in a protein alignment</li>
889 * <li>there is a mapping to a coding sequence (codon)</li>
890 * <li>there are one or more SNP variant features on the codon</li>
892 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
897 private String getHighlightInfo(SearchResultsI results)
900 * ideally, just find mapped CDS (as we don't care about render style here);
901 * for now, go via split frame complement's FeatureRenderer
903 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
904 if (complement == null)
908 AlignFrame af = Desktop.getAlignFrameFor(complement);
909 FeatureRendererModel fr2 = af.getFeatureRenderer();
911 int j = results.getSize();
912 List<String> infos = new ArrayList<>();
913 for (int i = 0; i < j; i++)
915 SearchResultMatchI match = results.getResults().get(i);
916 int pos = match.getStart();
917 if (pos == match.getEnd())
919 SequenceI seq = match.getSequence();
920 SequenceI ds = seq.getDatasetSequence() == null ? seq
921 : seq.getDatasetSequence();
922 MappedFeatures mf = fr2
923 .findComplementFeaturesAtResidue(ds, pos);
926 for (SequenceFeature sf : mf.features)
928 String pv = mf.findProteinVariants(sf);
929 if (pv.length() > 0 && !infos.contains(pv))
942 StringBuilder sb = new StringBuilder();
943 for (String info : infos)
951 return sb.toString();
955 public VamsasSource getVamsasSource()
957 return this.ap == null ? null : this.ap.av;
961 public void updateColours(SequenceI seq, int index)
963 System.out.println("update the seqPanel colours");
968 * Action on mouse movement is to update the status bar to show the current
969 * sequence position, and (if features are shown) to show any features at the
970 * position in a tooltip. Does nothing if the mouse move does not change
976 public void mouseMoved(MouseEvent evt)
980 // This is because MacOSX creates a mouseMoved
981 // If control is down, other platforms will not.
985 final MousePos mousePos = findMousePosition(evt);
986 if (mousePos.equals(lastMousePosition))
989 * just a pixel move without change of 'cell'
995 lastMousePosition = mousePos;
997 if (mousePos.isOverAnnotation())
999 mouseMovedOverAnnotation(mousePos);
1002 final int seq = mousePos.seqIndex;
1004 final int column = mousePos.column;
1005 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1007 lastMousePosition = null;
1008 setToolTipText(null);
1010 lastFormattedTooltip = null;
1011 ap.alignFrame.setStatus("");
1015 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1017 if (column >= sequence.getLength())
1023 * set status bar message, returning residue position in sequence
1025 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1026 final int pos = setStatusMessage(sequence, column, seq);
1027 if (ssm != null && !isGapped)
1029 mouseOverSequence(sequence, column, pos);
1032 StringBuilder tooltipText = new StringBuilder(64);
1034 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1037 for (int g = 0; g < groups.length; g++)
1039 if (groups[g].getStartRes() <= column
1040 && groups[g].getEndRes() >= column)
1042 if (!groups[g].getName().startsWith("JTreeGroup")
1043 && !groups[g].getName().startsWith("JGroup"))
1045 tooltipText.append(groups[g].getName());
1048 if (groups[g].getDescription() != null)
1050 tooltipText.append(": " + groups[g].getDescription());
1057 * add any features at the position to the tooltip; if over a gap, only
1058 * add features that straddle the gap (pos may be the residue before or
1061 int unshownFeatures = 0;
1062 if (av.isShowSequenceFeatures())
1064 List<SequenceFeature> features = ap.getFeatureRenderer()
1065 .findFeaturesAtColumn(sequence, column + 1);
1066 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1067 features, this.ap.getSeqPanel().seqCanvas.fr,
1068 MAX_TOOLTIP_LENGTH);
1071 * add features in CDS/protein complement at the corresponding
1072 * position if configured to do so
1074 if (av.isShowComplementFeatures())
1076 if (!Comparison.isGap(sequence.getCharAt(column)))
1078 AlignViewportI complement = ap.getAlignViewport()
1079 .getCodingComplement();
1080 AlignFrame af = Desktop.getAlignFrameFor(complement);
1081 FeatureRendererModel fr2 = af.getFeatureRenderer();
1082 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1086 unshownFeatures = seqARep.appendFeatures(tooltipText,
1087 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1092 if (tooltipText.length() == 0) // nothing added
1094 setToolTipText(null);
1099 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1101 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1102 tooltipText.append("...");
1104 if (unshownFeatures > 0)
1106 tooltipText.append("<br/>").append("... ").append("<i>")
1107 .append(MessageManager.formatMessage(
1108 "label.features_not_shown", unshownFeatures))
1111 String textString = tooltipText.toString();
1112 if (!textString.equals(lastTooltip))
1114 lastTooltip = textString;
1115 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1117 setToolTipText(lastFormattedTooltip);
1123 * When the view is in wrapped mode, and the mouse is over an annotation row,
1124 * shows the corresponding tooltip and status message (if any)
1129 protected void mouseMovedOverAnnotation(MousePos pos)
1131 final int column = pos.column;
1132 final int rowIndex = pos.annotationIndex;
1134 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1139 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1141 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1143 if (!tooltip.equals(lastTooltip))
1145 lastTooltip = tooltip;
1146 lastFormattedTooltip = tooltip == null ? null
1147 : JvSwingUtils.wrapTooltip(true, tooltip);
1148 setToolTipText(lastFormattedTooltip);
1151 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1153 ap.alignFrame.setStatus(msg);
1157 * if Shift key is held down while moving the mouse,
1158 * the tooltip location is not changed once shown
1160 private Point lastTooltipLocation = null;
1163 * this flag is false for pixel moves within a residue,
1164 * to reduce tooltip flicker
1166 private boolean moveTooltip = true;
1169 * a dummy tooltip used to estimate where to position tooltips
1171 private JToolTip tempTip = new JLabel().createToolTip();
1176 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1179 public Point getToolTipLocation(MouseEvent event)
1183 if (lastTooltip == null || !moveTooltip)
1188 if (lastTooltipLocation != null && event.isShiftDown())
1190 return lastTooltipLocation;
1193 int x = event.getX();
1194 int y = event.getY();
1197 tempTip.setTipText(lastFormattedTooltip);
1198 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1200 // was x += (w - x < 200) ? -(w / 2) : 5;
1201 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1202 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1204 return lastTooltipLocation = p;
1208 * set when the current UI interaction has resulted in a change that requires
1209 * shading in overviews and structures to be recalculated. this could be
1210 * changed to a something more expressive that indicates what actually has
1211 * changed, so selective redraws can be applied (ie. only structures, only
1214 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1217 * set if av.getSelectionGroup() refers to a group that is defined on the
1218 * alignment view, rather than a transient selection
1220 // private boolean editingDefinedGroup = false; // TODO: refactor to
1221 // avcontroller or viewModel
1224 * Sets the status message in alignment panel, showing the sequence number
1225 * (index) and id, and residue and residue position if not at a gap, for the
1226 * given sequence and column position. Returns the residue position returned
1227 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1228 * if at a gapped position.
1231 * aligned sequence object
1235 * index of sequence in alignment
1236 * @return sequence position of residue at column, or adjacent residue if at a
1239 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1241 char sequenceChar = sequence.getCharAt(column);
1242 int pos = sequence.findPosition(column);
1243 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1249 * Builds the status message for the current cursor location and writes it to
1250 * the status bar, for example
1253 * Sequence 3 ID: FER1_SOLLC
1254 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1255 * Sequence 5 ID: FER1_PEA Residue: B (3)
1256 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1261 * sequence position in the alignment (1..)
1262 * @param sequenceChar
1263 * the character under the cursor
1265 * the sequence residue position (if not over a gap)
1267 protected void setStatusMessage(String seqName, int seqIndex,
1268 char sequenceChar, int residuePos)
1270 StringBuilder text = new StringBuilder(32);
1273 * Sequence number (if known), and sequence name.
1275 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1276 text.append("Sequence").append(seqno).append(" ID: ")
1279 String residue = null;
1282 * Try to translate the display character to residue name (null for gap).
1284 boolean isGapped = Comparison.isGap(sequenceChar);
1288 boolean nucleotide = av.getAlignment().isNucleotide();
1289 String displayChar = String.valueOf(sequenceChar);
1292 residue = ResidueProperties.nucleotideName.get(displayChar);
1296 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1297 : ("*".equals(displayChar) ? "STOP"
1298 : ResidueProperties.aa2Triplet.get(displayChar));
1300 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1301 .append(": ").append(residue == null ? displayChar : residue);
1303 text.append(" (").append(Integer.toString(residuePos)).append(")");
1305 ap.alignFrame.setStatus(text.toString());
1309 * Set the status bar message to highlight the first matched position in
1314 private void setStatusMessage(SearchResultsI results)
1316 AlignmentI al = this.av.getAlignment();
1317 int sequenceIndex = al.findIndex(results);
1318 if (sequenceIndex == -1)
1322 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1323 SequenceI ds = alignedSeq.getDatasetSequence();
1324 for (SearchResultMatchI m : results.getResults())
1326 SequenceI seq = m.getSequence();
1327 if (seq.getDatasetSequence() != null)
1329 seq = seq.getDatasetSequence();
1334 int start = m.getStart();
1335 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1336 seq.getCharAt(start - 1), start);
1346 public void mouseDragged(MouseEvent evt)
1348 MousePos pos = findMousePosition(evt);
1349 if (pos.isOverAnnotation() || pos.column == -1)
1354 if (mouseWheelPressed)
1356 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1357 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1359 int oldWidth = av.getCharWidth();
1361 // Which is bigger, left-right or up-down?
1362 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1363 .abs(evt.getX() - lastMousePress.getX()))
1366 * on drag up or down, decrement or increment font size
1368 int fontSize = av.font.getSize();
1369 boolean fontChanged = false;
1371 if (evt.getY() < lastMousePress.getY())
1376 else if (evt.getY() > lastMousePress.getY())
1389 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1391 av.setFont(newFont, true);
1392 av.setCharWidth(oldWidth);
1396 ap.av.getCodingComplement().setFont(newFont, true);
1397 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1398 .getSplitViewContainer();
1399 splitFrame.adjustLayout();
1400 splitFrame.repaint();
1407 * on drag left or right, decrement or increment character width
1410 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1412 newWidth = av.getCharWidth() - 1;
1413 av.setCharWidth(newWidth);
1415 else if (evt.getX() > lastMousePress.getX())
1417 newWidth = av.getCharWidth() + 1;
1418 av.setCharWidth(newWidth);
1422 ap.paintAlignment(false, false);
1426 * need to ensure newWidth is set on cdna, regardless of which
1427 * panel the mouse drag happened in; protein will compute its
1428 * character width as 1:1 or 3:1
1430 av.getCodingComplement().setCharWidth(newWidth);
1431 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1432 .getSplitViewContainer();
1433 splitFrame.adjustLayout();
1434 splitFrame.repaint();
1439 FontMetrics fm = getFontMetrics(av.getFont());
1440 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1442 lastMousePress = evt.getPoint();
1449 dragStretchGroup(evt);
1453 int res = pos.column;
1460 if ((editLastRes == -1) || (editLastRes == res))
1465 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1467 // dragLeft, delete gap
1468 editSequence(false, false, res);
1472 editSequence(true, false, res);
1475 mouseDragging = true;
1476 if (scrollThread != null)
1478 scrollThread.setMousePosition(evt.getPoint());
1483 * Edits the sequence to insert or delete one or more gaps, in response to a
1484 * mouse drag or cursor mode command. The number of inserts/deletes may be
1485 * specified with the cursor command, or else depends on the mouse event
1486 * (normally one column, but potentially more for a fast mouse drag).
1488 * Delete gaps is limited to the number of gaps left of the cursor position
1489 * (mouse drag), or at or right of the cursor position (cursor mode).
1491 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1492 * the current selection group.
1494 * In locked editing mode (with a selection group present), inserts/deletions
1495 * within the selection group are limited to its boundaries (and edits outside
1496 * the group stop at its border).
1499 * true to insert gaps, false to delete gaps
1501 * (unused parameter)
1503 * the column at which to perform the action; the number of columns
1504 * affected depends on <code>this.editLastRes</code> (cursor column
1507 synchronized void editSequence(boolean insertGap, boolean editSeq,
1511 int fixedRight = -1;
1512 boolean fixedColumns = false;
1513 SequenceGroup sg = av.getSelectionGroup();
1515 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1517 // No group, but the sequence may represent a group
1518 if (!groupEditing && av.hasHiddenRows())
1520 if (av.isHiddenRepSequence(seq))
1522 sg = av.getRepresentedSequences(seq);
1523 groupEditing = true;
1527 StringBuilder message = new StringBuilder(64); // for status bar
1530 * make a name for the edit action, for
1531 * status bar message and Undo/Redo menu
1533 String label = null;
1536 message.append("Edit group:");
1537 label = MessageManager.getString("action.edit_group");
1541 message.append("Edit sequence: " + seq.getName());
1542 label = seq.getName();
1543 if (label.length() > 10)
1545 label = label.substring(0, 10);
1547 label = MessageManager.formatMessage("label.edit_params",
1553 * initialise the edit command if there is not
1554 * already one being extended
1556 if (editCommand == null)
1558 editCommand = new EditCommand(label);
1563 message.append(" insert ");
1567 message.append(" delete ");
1570 message.append(Math.abs(startres - editLastRes) + " gaps.");
1571 ap.alignFrame.setStatus(message.toString());
1574 * is there a selection group containing the sequence being edited?
1575 * if so the boundary of the group is the limit of the edit
1576 * (but the edit may be inside or outside the selection group)
1578 boolean inSelectionGroup = sg != null
1579 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1580 if (groupEditing || inSelectionGroup)
1582 fixedColumns = true;
1584 // sg might be null as the user may only see 1 sequence,
1585 // but the sequence represents a group
1588 if (!av.isHiddenRepSequence(seq))
1593 sg = av.getRepresentedSequences(seq);
1596 fixedLeft = sg.getStartRes();
1597 fixedRight = sg.getEndRes();
1599 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1600 || (startres >= fixedLeft && editLastRes < fixedLeft)
1601 || (startres > fixedRight && editLastRes <= fixedRight)
1602 || (startres <= fixedRight && editLastRes > fixedRight))
1608 if (fixedLeft > startres)
1610 fixedRight = fixedLeft - 1;
1613 else if (fixedRight < startres)
1615 fixedLeft = fixedRight;
1620 if (av.hasHiddenColumns())
1622 fixedColumns = true;
1623 int y1 = av.getAlignment().getHiddenColumns()
1624 .getNextHiddenBoundary(true, startres);
1625 int y2 = av.getAlignment().getHiddenColumns()
1626 .getNextHiddenBoundary(false, startres);
1628 if ((insertGap && startres > y1 && editLastRes < y1)
1629 || (!insertGap && startres < y2 && editLastRes > y2))
1635 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1636 // Selection spans a hidden region
1637 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1645 fixedRight = y2 - 1;
1650 boolean success = doEditSequence(insertGap, editSeq, startres,
1651 fixedRight, fixedColumns, sg);
1654 * report what actually happened (might be less than
1655 * what was requested), by inspecting the edit commands added
1657 String msg = getEditStatusMessage(editCommand);
1658 ap.alignFrame.setStatus(msg == null ? " " : msg);
1664 editLastRes = startres;
1665 seqCanvas.repaint();
1669 * A helper method that performs the requested editing to insert or delete
1670 * gaps (if possible). Answers true if the edit was successful, false if could
1671 * only be performed in part or not at all. Failure may occur in 'locked edit'
1672 * mode, when an insertion requires a matching gapped position (or column) to
1673 * delete, and deletion requires an adjacent gapped position (or column) to
1677 * true if inserting gap(s), false if deleting
1679 * (unused parameter, currently always false)
1681 * the column at which to perform the edit
1683 * fixed right boundary column of a locked edit (within or to the
1684 * left of a selection group)
1685 * @param fixedColumns
1686 * true if this is a locked edit
1688 * the sequence group (if group edit is being performed)
1691 protected boolean doEditSequence(final boolean insertGap,
1692 final boolean editSeq, final int startres, int fixedRight,
1693 final boolean fixedColumns, final SequenceGroup sg)
1695 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1696 SequenceI[] seqs = new SequenceI[] { seq };
1700 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1701 int g, groupSize = vseqs.size();
1702 SequenceI[] groupSeqs = new SequenceI[groupSize];
1703 for (g = 0; g < groupSeqs.length; g++)
1705 groupSeqs[g] = vseqs.get(g);
1711 // If the user has selected the whole sequence, and is dragging to
1712 // the right, we can still extend the alignment and selectionGroup
1713 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1714 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1717 av.getAlignment().getWidth() + startres - editLastRes);
1718 fixedRight = sg.getEndRes();
1721 // Is it valid with fixed columns??
1722 // Find the next gap before the end
1723 // of the visible region boundary
1724 boolean blank = false;
1725 for (; fixedRight > editLastRes; fixedRight--)
1729 for (g = 0; g < groupSize; g++)
1731 for (int j = 0; j < startres - editLastRes; j++)
1734 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1749 if (sg.getSize() == av.getAlignment().getHeight())
1751 if ((av.hasHiddenColumns()
1752 && startres < av.getAlignment().getHiddenColumns()
1753 .getNextHiddenBoundary(false, startres)))
1758 int alWidth = av.getAlignment().getWidth();
1759 if (av.hasHiddenRows())
1761 int hwidth = av.getAlignment().getHiddenSequences()
1763 if (hwidth > alWidth)
1768 // We can still insert gaps if the selectionGroup
1769 // contains all the sequences
1770 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1771 fixedRight = alWidth + startres - editLastRes;
1781 else if (!insertGap)
1783 // / Are we able to delete?
1784 // ie are all columns blank?
1786 for (g = 0; g < groupSize; g++)
1788 for (int j = startres; j < editLastRes; j++)
1790 if (groupSeqs[g].getLength() <= j)
1795 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1797 // Not a gap, block edit not valid
1806 // dragging to the right
1807 if (fixedColumns && fixedRight != -1)
1809 for (int j = editLastRes; j < startres; j++)
1811 insertGap(j, groupSeqs, fixedRight);
1816 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1817 startres - editLastRes, false);
1822 // dragging to the left
1823 if (fixedColumns && fixedRight != -1)
1825 for (int j = editLastRes; j > startres; j--)
1827 deleteChar(startres, groupSeqs, fixedRight);
1832 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1833 editLastRes - startres, false);
1840 * editing a single sequence
1844 // dragging to the right
1845 if (fixedColumns && fixedRight != -1)
1847 for (int j = editLastRes; j < startres; j++)
1849 if (!insertGap(j, seqs, fixedRight))
1852 * e.g. cursor mode command specified
1853 * more inserts than are possible
1861 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1862 startres - editLastRes, false);
1869 // dragging to the left
1870 if (fixedColumns && fixedRight != -1)
1872 for (int j = editLastRes; j > startres; j--)
1874 if (!Comparison.isGap(seq.getCharAt(startres)))
1878 deleteChar(startres, seqs, fixedRight);
1883 // could be a keyboard edit trying to delete none gaps
1885 for (int m = startres; m < editLastRes; m++)
1887 if (!Comparison.isGap(seq.getCharAt(m)))
1895 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1900 {// insertGap==false AND editSeq==TRUE;
1901 if (fixedColumns && fixedRight != -1)
1903 for (int j = editLastRes; j < startres; j++)
1905 insertGap(j, seqs, fixedRight);
1910 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1911 startres - editLastRes, false);
1921 * Constructs an informative status bar message while dragging to insert or
1922 * delete gaps. Answers null if inserts and deletes cancel out.
1924 * @param editCommand
1925 * a command containing the list of individual edits
1928 protected static String getEditStatusMessage(EditCommand editCommand)
1930 if (editCommand == null)
1936 * add any inserts, and subtract any deletes,
1937 * not counting those auto-inserted when doing a 'locked edit'
1938 * (so only counting edits 'under the cursor')
1941 for (Edit cmd : editCommand.getEdits())
1943 if (!cmd.isSystemGenerated())
1945 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1953 * inserts and deletes cancel out
1958 String msgKey = count > 1 ? "label.insert_gaps"
1959 : (count == 1 ? "label.insert_gap"
1960 : (count == -1 ? "label.delete_gap"
1961 : "label.delete_gaps"));
1962 count = Math.abs(count);
1964 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1968 * Inserts one gap at column j, deleting the right-most gapped column up to
1969 * (and including) fixedColumn. Returns true if the edit is successful, false
1970 * if no blank column is available to allow the insertion to be balanced by a
1975 * @param fixedColumn
1978 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1980 int blankColumn = fixedColumn;
1981 for (int s = 0; s < seq.length; s++)
1983 // Find the next gap before the end of the visible region boundary
1984 // If lastCol > j, theres a boundary after the gap insertion
1986 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1988 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1990 // Theres a space, so break and insert the gap
1995 if (blankColumn <= j)
1997 blankColumn = fixedColumn;
2003 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2005 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2011 * Helper method to add and perform one edit action
2017 * @param systemGenerated
2018 * true if the edit is a 'balancing' delete (or insert) to match a
2019 * user's insert (or delete) in a locked editing region
2021 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2022 int count, boolean systemGenerated)
2025 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2026 av.getAlignment().getGapCharacter());
2027 edit.setSystemGenerated(systemGenerated);
2029 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2033 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2034 * each of the given sequences. The caller should ensure that all sequences
2035 * are gapped in column j.
2039 * @param fixedColumn
2041 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2043 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2045 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2049 * On reentering the panel, stops any scrolling that was started on dragging
2055 public void mouseEntered(MouseEvent e)
2065 * On leaving the panel, if the mouse is being dragged, starts a thread to
2066 * scroll it until the mouse is released (in unwrapped mode only)
2071 public void mouseExited(MouseEvent e)
2073 lastMousePosition = null;
2074 ap.alignFrame.setStatus(" ");
2075 if (av.getWrapAlignment())
2080 if (mouseDragging && scrollThread == null)
2082 startScrolling(e.getPoint());
2087 * Handler for double-click on a position with one or more sequence features.
2088 * Opens the Amend Features dialog to allow feature details to be amended, or
2089 * the feature deleted.
2092 public void mouseClicked(MouseEvent evt)
2094 SequenceGroup sg = null;
2095 MousePos pos = findMousePosition(evt);
2096 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2101 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2103 sg = av.getSelectionGroup();
2104 if (sg != null && sg.getSize() == 1
2105 && sg.getEndRes() - sg.getStartRes() < 2)
2107 av.setSelectionGroup(null);
2110 int column = pos.column;
2113 * find features at the position (if not gapped), or straddling
2114 * the position (if at a gap)
2116 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2117 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2118 .findFeaturesAtColumn(sequence, column + 1);
2120 if (!features.isEmpty())
2123 * highlight the first feature at the position on the alignment
2125 SearchResultsI highlight = new SearchResults();
2126 highlight.addResult(sequence, features.get(0).getBegin(), features
2128 seqCanvas.highlightSearchResults(highlight, true);
2131 * open the Amend Features dialog
2133 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2134 false).showDialog();
2140 public void mouseWheelMoved(MouseWheelEvent e)
2143 double wheelRotation = e.getPreciseWheelRotation();
2144 if (wheelRotation > 0)
2146 if (e.isShiftDown())
2148 av.getRanges().scrollRight(true);
2153 av.getRanges().scrollUp(false);
2156 else if (wheelRotation < 0)
2158 if (e.isShiftDown())
2160 av.getRanges().scrollRight(false);
2164 av.getRanges().scrollUp(true);
2169 * update status bar and tooltip for new position
2170 * (need to synthesize a mouse movement to refresh tooltip)
2173 ToolTipManager.sharedInstance().mouseMoved(e);
2182 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2184 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2189 final int res = pos.column;
2190 final int seq = pos.seqIndex;
2192 updateOverviewAndStructs = false;
2194 startWrapBlock = wrappedBlock;
2196 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2198 if ((sequence == null) || (res > sequence.getLength()))
2203 stretchGroup = av.getSelectionGroup();
2205 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2207 stretchGroup = av.getAlignment().findGroup(sequence, res);
2208 if (stretchGroup != null)
2210 // only update the current selection if the popup menu has a group to
2212 av.setSelectionGroup(stretchGroup);
2217 * defer right-mouse click handling to mouseReleased on Windows
2218 * (where isPopupTrigger() will answer true)
2219 * NB isRightMouseButton is also true for Cmd-click on Mac
2221 if (Platform.isWinRightButton(evt))
2226 if (evt.isPopupTrigger()) // Mac: mousePressed
2228 showPopupMenu(evt, pos);
2234 seqCanvas.cursorX = res;
2235 seqCanvas.cursorY = seq;
2236 seqCanvas.repaint();
2240 if (stretchGroup == null)
2242 createStretchGroup(res, sequence);
2245 if (stretchGroup != null)
2247 stretchGroup.addPropertyChangeListener(seqCanvas);
2250 seqCanvas.repaint();
2253 private void createStretchGroup(int res, SequenceI sequence)
2255 // Only if left mouse button do we want to change group sizes
2256 // define a new group here
2257 SequenceGroup sg = new SequenceGroup();
2258 sg.setStartRes(res);
2260 sg.addSequence(sequence, false);
2261 av.setSelectionGroup(sg);
2264 if (av.getConservationSelected())
2266 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2270 if (av.getAbovePIDThreshold())
2272 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2275 // TODO: stretchGroup will always be not null. Is this a merge error ?
2276 // or is there a threading issue here?
2277 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2279 // Edit end res position of selected group
2280 changeEndRes = true;
2282 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2284 // Edit end res position of selected group
2285 changeStartRes = true;
2287 stretchGroup.getWidth();
2292 * Build and show a pop-up menu at the right-click mouse position
2297 void showPopupMenu(MouseEvent evt, MousePos pos)
2299 final int column = pos.column;
2300 final int seq = pos.seqIndex;
2301 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2302 if (sequence != null)
2304 PopupMenu pop = new PopupMenu(ap, sequence, column);
2305 pop.show(this, evt.getX(), evt.getY());
2310 * Update the display after mouse up on a selection or group
2313 * mouse released event details
2315 * true if this event is happening after a mouse drag (rather than a
2318 protected void doMouseReleasedDefineMode(MouseEvent evt,
2321 if (stretchGroup == null)
2326 stretchGroup.removePropertyChangeListener(seqCanvas);
2328 // always do this - annotation has own state
2329 // but defer colourscheme update until hidden sequences are passed in
2330 boolean vischange = stretchGroup.recalcConservation(true);
2331 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2333 if (stretchGroup.cs != null)
2337 stretchGroup.cs.alignmentChanged(stretchGroup,
2338 av.getHiddenRepSequences());
2341 ResidueShaderI groupColourScheme = stretchGroup
2342 .getGroupColourScheme();
2343 String name = stretchGroup.getName();
2344 if (stretchGroup.cs.conservationApplied())
2346 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2348 if (stretchGroup.cs.getThreshold() > 0)
2350 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2353 PaintRefresher.Refresh(this, av.getSequenceSetId());
2354 // TODO: structure colours only need updating if stretchGroup used to or now
2355 // does contain sequences with structure views
2356 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2357 updateOverviewAndStructs = false;
2358 changeEndRes = false;
2359 changeStartRes = false;
2360 stretchGroup = null;
2365 * Resizes the borders of a selection group depending on the direction of
2370 protected void dragStretchGroup(MouseEvent evt)
2372 if (stretchGroup == null)
2377 MousePos pos = findMousePosition(evt);
2378 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2383 int res = pos.column;
2384 int y = pos.seqIndex;
2386 if (wrappedBlock != startWrapBlock)
2391 res = Math.min(res, av.getAlignment().getWidth()-1);
2393 if (stretchGroup.getEndRes() == res)
2395 // Edit end res position of selected group
2396 changeEndRes = true;
2398 else if (stretchGroup.getStartRes() == res)
2400 // Edit start res position of selected group
2401 changeStartRes = true;
2404 if (res < av.getRanges().getStartRes())
2406 res = av.getRanges().getStartRes();
2411 if (res > (stretchGroup.getStartRes() - 1))
2413 stretchGroup.setEndRes(res);
2414 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2417 else if (changeStartRes)
2419 if (res < (stretchGroup.getEndRes() + 1))
2421 stretchGroup.setStartRes(res);
2422 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2426 int dragDirection = 0;
2432 else if (y < oldSeq)
2437 while ((y != oldSeq) && (oldSeq > -1)
2438 && (y < av.getAlignment().getHeight()))
2440 // This routine ensures we don't skip any sequences, as the
2441 // selection is quite slow.
2442 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2444 oldSeq += dragDirection;
2451 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2453 if (stretchGroup.getSequences(null).contains(nextSeq))
2455 stretchGroup.deleteSequence(seq, false);
2456 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2462 stretchGroup.addSequence(seq, false);
2465 stretchGroup.addSequence(nextSeq, false);
2466 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2475 mouseDragging = true;
2477 if (scrollThread != null)
2479 scrollThread.setMousePosition(evt.getPoint());
2483 * construct a status message showing the range of the selection
2485 StringBuilder status = new StringBuilder(64);
2486 List<SequenceI> seqs = stretchGroup.getSequences();
2487 String name = seqs.get(0).getName();
2488 if (name.length() > 20)
2490 name = name.substring(0, 20);
2492 status.append(name).append(" - ");
2493 name = seqs.get(seqs.size() - 1).getName();
2494 if (name.length() > 20)
2496 name = name.substring(0, 20);
2498 status.append(name).append(" ");
2499 int startRes = stretchGroup.getStartRes();
2500 status.append(" cols ").append(String.valueOf(startRes + 1))
2502 int endRes = stretchGroup.getEndRes();
2503 status.append(String.valueOf(endRes + 1));
2504 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2505 .append(String.valueOf(endRes - startRes + 1)).append(")");
2506 ap.alignFrame.setStatus(status.toString());
2510 * Stops the scroll thread if it is running
2512 void stopScrolling()
2514 if (scrollThread != null)
2516 scrollThread.stopScrolling();
2517 scrollThread = null;
2519 mouseDragging = false;
2523 * Starts a thread to scroll the alignment, towards a given mouse position
2524 * outside the panel bounds, unless the alignment is in wrapped mode
2528 void startScrolling(Point mousePos)
2531 * set this.mouseDragging in case this was called from
2532 * a drag in ScalePanel or AnnotationPanel
2534 mouseDragging = true;
2535 if (!av.getWrapAlignment() && scrollThread == null)
2537 scrollThread = new ScrollThread();
2538 scrollThread.setMousePosition(mousePos);
2539 if (Platform.isJS())
2542 * Javascript - run every 20ms until scrolling stopped
2543 * or reaches the limit of scrollable alignment
2545 Timer t = new Timer(20, new ActionListener()
2548 public void actionPerformed(ActionEvent e)
2550 if (scrollThread != null)
2552 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2553 scrollThread.scrollOnce();
2557 t.addActionListener(new ActionListener()
2560 public void actionPerformed(ActionEvent e)
2562 if (scrollThread == null)
2564 // SeqPanel.stopScrolling called
2574 * Java - run in a new thread
2576 scrollThread.start();
2582 * Performs scrolling of the visible alignment left, right, up or down, until
2583 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2584 * limit of the alignment is reached
2586 class ScrollThread extends Thread
2588 private Point mousePos;
2590 private volatile boolean keepRunning = true;
2595 public ScrollThread()
2597 setName("SeqPanel$ScrollThread");
2601 * Sets the position of the mouse that determines the direction of the
2602 * scroll to perform. If this is called as the mouse moves, scrolling should
2603 * respond accordingly. For example, if the mouse is dragged right, scroll
2604 * right should start; if the drag continues down, scroll down should also
2609 public void setMousePosition(Point p)
2615 * Sets a flag that will cause the thread to exit
2617 public void stopScrolling()
2619 keepRunning = false;
2623 * Scrolls the alignment left or right, and/or up or down, depending on the
2624 * last notified mouse position, until the limit of the alignment is
2625 * reached, or a flag is set to stop the scroll
2632 if (mousePos != null)
2634 keepRunning = scrollOnce();
2639 } catch (Exception ex)
2643 SeqPanel.this.scrollThread = null;
2649 * <li>one row up, if the mouse is above the panel</li>
2650 * <li>one row down, if the mouse is below the panel</li>
2651 * <li>one column left, if the mouse is left of the panel</li>
2652 * <li>one column right, if the mouse is right of the panel</li>
2654 * Answers true if a scroll was performed, false if not - meaning either
2655 * that the mouse position is within the panel, or the edge of the alignment
2658 boolean scrollOnce()
2661 * quit after mouseUp ensures interrupt in JalviewJS
2668 boolean scrolled = false;
2669 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2676 // mouse is above this panel - try scroll up
2677 scrolled = ranges.scrollUp(true);
2679 else if (mousePos.y >= getHeight())
2681 // mouse is below this panel - try scroll down
2682 scrolled = ranges.scrollUp(false);
2686 * scroll left or right
2690 scrolled |= ranges.scrollRight(false);
2692 else if (mousePos.x >= getWidth())
2694 scrolled |= ranges.scrollRight(true);
2701 * modify current selection according to a received message.
2704 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2705 HiddenColumns hidden, SelectionSource source)
2707 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2708 // handles selection messages...
2709 // TODO: extend config options to allow user to control if selections may be
2710 // shared between viewports.
2711 boolean iSentTheSelection = (av == source
2712 || (source instanceof AlignViewport
2713 && ((AlignmentViewport) source).getSequenceSetId()
2714 .equals(av.getSequenceSetId())));
2716 if (iSentTheSelection)
2718 // respond to our own event by updating dependent dialogs
2719 if (ap.getCalculationDialog() != null)
2721 ap.getCalculationDialog().validateCalcTypes();
2727 // process further ?
2728 if (!av.followSelection)
2734 * Ignore the selection if there is one of our own pending.
2736 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2742 * Check for selection in a view of which this one is a dna/protein
2745 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2750 // do we want to thread this ? (contention with seqsel and colsel locks, I
2753 * only copy colsel if there is a real intersection between
2754 * sequence selection and this panel's alignment
2756 boolean repaint = false;
2757 boolean copycolsel = false;
2759 SequenceGroup sgroup = null;
2760 if (seqsel != null && seqsel.getSize() > 0)
2762 if (av.getAlignment() == null)
2764 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2765 + " ViewId=" + av.getViewId()
2766 + " 's alignment is NULL! returning immediately.");
2769 sgroup = seqsel.intersect(av.getAlignment(),
2770 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2771 if ((sgroup != null && sgroup.getSize() > 0))
2776 if (sgroup != null && sgroup.getSize() > 0)
2778 av.setSelectionGroup(sgroup);
2782 av.setSelectionGroup(null);
2784 av.isSelectionGroupChanged(true);
2789 // the current selection is unset or from a previous message
2790 // so import the new colsel.
2791 if (colsel == null || colsel.isEmpty())
2793 if (av.getColumnSelection() != null)
2795 av.getColumnSelection().clear();
2801 // TODO: shift colSel according to the intersecting sequences
2802 if (av.getColumnSelection() == null)
2804 av.setColumnSelection(new ColumnSelection(colsel));
2808 av.getColumnSelection().setElementsFrom(colsel,
2809 av.getAlignment().getHiddenColumns());
2812 av.isColSelChanged(true);
2816 if (copycolsel && av.hasHiddenColumns()
2817 && (av.getAlignment().getHiddenColumns() == null))
2819 System.err.println("Bad things");
2821 if (repaint) // always true!
2823 // probably finessing with multiple redraws here
2824 PaintRefresher.Refresh(this, av.getSequenceSetId());
2825 // ap.paintAlignment(false);
2828 // lastly, update dependent dialogs
2829 if (ap.getCalculationDialog() != null)
2831 ap.getCalculationDialog().validateCalcTypes();
2837 * If this panel is a cdna/protein translation view of the selection source,
2838 * tries to map the source selection to a local one, and returns true. Else
2845 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2846 ColumnSelection colsel, HiddenColumns hidden,
2847 SelectionSource source)
2849 if (!(source instanceof AlignViewportI))
2853 final AlignViewportI sourceAv = (AlignViewportI) source;
2854 if (sourceAv.getCodingComplement() != av
2855 && av.getCodingComplement() != sourceAv)
2861 * Map sequence selection
2863 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2864 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2865 av.isSelectionGroupChanged(true);
2868 * Map column selection
2870 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2872 ColumnSelection cs = new ColumnSelection();
2873 HiddenColumns hs = new HiddenColumns();
2874 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2875 av.setColumnSelection(cs);
2876 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2878 // lastly, update any dependent dialogs
2879 if (ap.getCalculationDialog() != null)
2881 ap.getCalculationDialog().validateCalcTypes();
2885 * repaint alignment, and also Overview or Structure
2886 * if hidden column selection has changed
2888 ap.paintAlignment(hiddenChanged, hiddenChanged);
2895 * @return null or last search results handled by this panel
2897 public SearchResultsI getLastSearchResults()
2899 return lastSearchResults;
2903 * scroll to the given row/column - or nearest visible location
2908 public void scrollTo(int row, int column)
2911 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2912 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2913 ap.scrollTo(column, column, row, true, true);
2917 * scroll to the given row - or nearest visible location
2921 public void scrollToRow(int row)
2924 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2925 ap.scrollTo(ap.av.getRanges().getStartRes(),
2926 ap.av.getRanges().getStartRes(), row, true, true);
2930 * scroll to the given column - or nearest visible location
2934 public void scrollToColumn(int column)
2937 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2938 ap.scrollTo(column, column, ap.av.getRanges().getStartSeq(), true,