2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import java.awt.BorderLayout;
24 import java.awt.Color;
26 import java.awt.FontMetrics;
27 import java.awt.Point;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.awt.event.MouseEvent;
31 import java.awt.event.MouseListener;
32 import java.awt.event.MouseMotionListener;
33 import java.awt.event.MouseWheelEvent;
34 import java.awt.event.MouseWheelListener;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.List;
39 import javax.swing.JLabel;
40 import javax.swing.JPanel;
41 import javax.swing.JToolTip;
42 import javax.swing.SwingUtilities;
43 import javax.swing.Timer;
44 import javax.swing.ToolTipManager;
46 import jalview.api.AlignViewportI;
47 import jalview.bin.Console;
48 import jalview.commands.EditCommand;
49 import jalview.commands.EditCommand.Action;
50 import jalview.commands.EditCommand.Edit;
51 import jalview.datamodel.AlignmentAnnotation;
52 import jalview.datamodel.AlignmentI;
53 import jalview.datamodel.ColumnSelection;
54 import jalview.datamodel.HiddenColumns;
55 import jalview.datamodel.MappedFeatures;
56 import jalview.datamodel.SearchResultMatchI;
57 import jalview.datamodel.SearchResults;
58 import jalview.datamodel.SearchResultsI;
59 import jalview.datamodel.Sequence;
60 import jalview.datamodel.SequenceFeature;
61 import jalview.datamodel.SequenceGroup;
62 import jalview.datamodel.SequenceI;
63 import jalview.io.SequenceAnnotationReport;
64 import jalview.renderer.ResidueShaderI;
65 import jalview.schemes.ResidueProperties;
66 import jalview.structure.SelectionListener;
67 import jalview.structure.SelectionSource;
68 import jalview.structure.SequenceListener;
69 import jalview.structure.StructureSelectionManager;
70 import jalview.structure.VamsasSource;
71 import jalview.util.Comparison;
72 import jalview.util.MappingUtils;
73 import jalview.util.MessageManager;
74 import jalview.util.Platform;
75 import jalview.viewmodel.AlignmentViewport;
76 import jalview.viewmodel.ViewportRanges;
77 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
83 * @version $Revision: 1.130 $
85 public class SeqPanel extends JPanel
86 implements MouseListener, MouseMotionListener, MouseWheelListener,
87 SequenceListener, SelectionListener
90 * a class that holds computed mouse position
91 * - column of the alignment (0...)
92 * - sequence offset (0...)
93 * - annotation row offset (0...)
94 * where annotation offset is -1 unless the alignment is shown
95 * in wrapped mode, annotations are shown, and the mouse is
96 * over an annnotation row
101 * alignment column position of cursor (0...)
106 * index in alignment of sequence under cursor,
107 * or nearest above if cursor is not over a sequence
112 * index in annotations array of annotation under the cursor
113 * (only possible in wrapped mode with annotations shown),
114 * or -1 if cursor is not over an annotation row
116 final int annotationIndex;
118 MousePos(int col, int seq, int ann)
122 annotationIndex = ann;
125 boolean isOverAnnotation()
127 return annotationIndex != -1;
131 public boolean equals(Object obj)
133 if (obj == null || !(obj instanceof MousePos))
137 MousePos o = (MousePos) obj;
138 boolean b = (column == o.column && seqIndex == o.seqIndex
139 && annotationIndex == o.annotationIndex);
140 // System.out.println(obj + (b ? "= " : "!= ") + this);
145 * A simple hashCode that ensures that instances that satisfy equals() have
149 public int hashCode()
151 return column + seqIndex + annotationIndex;
155 * toString method for debug output purposes only
158 public String toString()
160 return String.format("c%d:s%d:a%d", column, seqIndex,
165 private static final int MAX_TOOLTIP_LENGTH = 300;
167 public SeqCanvas seqCanvas;
169 public AlignmentPanel ap;
172 * last position for mouseMoved event
174 private MousePos lastMousePosition;
176 protected int editLastRes;
178 protected int editStartSeq;
180 protected AlignViewport av;
182 ScrollThread scrollThread = null;
184 boolean mouseDragging = false;
186 boolean editingSeqs = false;
188 boolean groupEditing = false;
190 // ////////////////////////////////////////
191 // ///Everything below this is for defining the boundary of the rubberband
192 // ////////////////////////////////////////
195 boolean changeEndSeq = false;
197 boolean changeStartSeq = false;
199 boolean changeEndRes = false;
201 boolean changeStartRes = false;
203 SequenceGroup stretchGroup = null;
205 boolean remove = false;
207 Point lastMousePress;
209 boolean mouseWheelPressed = false;
211 StringBuffer keyboardNo1;
213 StringBuffer keyboardNo2;
215 private final SequenceAnnotationReport seqARep;
218 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
219 * - the tooltip is not set again if unchanged
220 * - this is the tooltip text _before_ formatting as html
222 private String lastTooltip;
225 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
226 * - used to decide where to place the tooltip in getTooltipLocation()
227 * - this is the tooltip text _after_ formatting as html
229 private String lastFormattedTooltip;
231 EditCommand editCommand;
233 StructureSelectionManager ssm;
235 SearchResultsI lastSearchResults;
238 * Creates a new SeqPanel object
243 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
245 seqARep = new SequenceAnnotationReport(true);
246 ToolTipManager.sharedInstance().registerComponent(this);
247 ToolTipManager.sharedInstance().setInitialDelay(0);
248 ToolTipManager.sharedInstance().setDismissDelay(10000);
252 setBackground(Color.white);
254 seqCanvas = new SeqCanvas(alignPanel);
255 setLayout(new BorderLayout());
256 add(seqCanvas, BorderLayout.CENTER);
258 this.ap = alignPanel;
260 if (!viewport.isDataset())
262 addMouseMotionListener(this);
263 addMouseListener(this);
264 addMouseWheelListener(this);
265 ssm = viewport.getStructureSelectionManager();
266 ssm.addStructureViewerListener(this);
267 ssm.addSelectionListener(this);
271 int startWrapBlock = -1;
273 int wrappedBlock = -1;
276 * Computes the column and sequence row (and possibly annotation row when in
277 * wrapped mode) for the given mouse position
279 * Mouse position is not set if in wrapped mode with the cursor either between
280 * sequences, or over the left or right vertical scale.
285 MousePos findMousePosition(MouseEvent evt)
287 int col = findColumn(evt);
292 int charHeight = av.getCharHeight();
293 int alignmentHeight = av.getAlignment().getHeight();
294 if (av.getWrapAlignment())
296 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
297 seqCanvas.getHeight());
300 * yPos modulo height of repeating width
302 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
305 * height of sequences plus space / scale above,
306 * plus gap between sequences and annotations
308 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
309 + alignmentHeight * charHeight
310 + SeqCanvas.SEQS_ANNOTATION_GAP;
311 if (yOffsetPx >= alignmentHeightPixels)
314 * mouse is over annotations; find annotation index, also set
315 * last sequence above (for backwards compatible behaviour)
317 AlignmentAnnotation[] anns = av.getAlignment()
318 .getAlignmentAnnotation();
319 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
320 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
321 seqIndex = alignmentHeight - 1;
326 * mouse is over sequence (or the space above sequences)
328 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
331 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
337 ViewportRanges ranges = av.getRanges();
338 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
339 alignmentHeight - 1);
340 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
343 return new MousePos(col, seqIndex, annIndex);
346 * Returns the aligned sequence position (base 0) at the mouse position, or
347 * the closest visible one
349 * Returns -1 if in wrapped mode with the mouse over either left or right
355 int findColumn(MouseEvent evt)
360 final int startRes = av.getRanges().getStartRes();
361 final int charWidth = av.getCharWidth();
363 if (av.getWrapAlignment())
365 int hgap = av.getCharHeight();
366 if (av.getScaleAboveWrapped())
368 hgap += av.getCharHeight();
371 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
372 + hgap + seqCanvas.getAnnotationHeight();
375 y = Math.max(0, y - hgap);
376 x -= seqCanvas.getLabelWidthWest();
379 // mouse is over left scale
383 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
388 if (x >= cwidth * charWidth)
390 // mouse is over right scale
394 wrappedBlock = y / cHeight;
395 wrappedBlock += startRes / cwidth;
396 // allow for wrapped view scrolled right (possible from Overview)
397 int startOffset = startRes % cwidth;
398 res = wrappedBlock * cwidth + startOffset
399 + Math.min(cwidth - 1, x / charWidth);
404 * make sure we calculate relative to visible alignment,
405 * rather than right-hand gutter
407 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
408 res = (x / charWidth) + startRes;
409 res = Math.min(res, av.getRanges().getEndRes());
412 if (av.hasHiddenColumns())
414 res = av.getAlignment().getHiddenColumns()
415 .visibleToAbsoluteColumn(res);
422 * When all of a sequence of edits are complete, put the resulting edit list
423 * on the history stack (undo list), and reset flags for editing in progress.
429 if (editCommand != null && editCommand.getSize() > 0)
431 ap.alignFrame.addHistoryItem(editCommand);
432 av.firePropertyChange("alignment", null,
433 av.getAlignment().getSequences());
438 * Tidy up come what may...
443 groupEditing = false;
452 seqCanvas.cursorY = getKeyboardNo1() - 1;
453 scrollToVisible(true);
456 void setCursorColumn()
458 seqCanvas.cursorX = getKeyboardNo1() - 1;
459 scrollToVisible(true);
462 void setCursorRowAndColumn()
464 if (keyboardNo2 == null)
466 keyboardNo2 = new StringBuffer();
470 seqCanvas.cursorX = getKeyboardNo1() - 1;
471 seqCanvas.cursorY = getKeyboardNo2() - 1;
472 scrollToVisible(true);
476 void setCursorPosition()
478 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
480 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
481 scrollToVisible(true);
484 void moveCursor(int dx, int dy)
486 moveCursor(dx, dy,false);
488 void moveCursor(int dx, int dy, boolean nextWord)
490 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
494 int maxWidth = av.getAlignment().getWidth();
495 int maxHeight=av.getAlignment().getHeight();
496 SequenceI seqAtRow = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
497 // look for next gap or residue
498 boolean isGap = Comparison.isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
499 int p = seqCanvas.cursorX,lastP,r=seqCanvas.cursorY,lastR;
515 seqAtRow = av.getAlignment().getSequenceAt(r);
517 p = nextVisible(hidden, maxWidth, p, dx);
518 } while ((dx != 0 ? p != lastP : r != lastR)
519 && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
523 int maxWidth = av.getAlignment().getWidth();
524 seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX, dx);
525 seqCanvas.cursorY += dy;
527 scrollToVisible(false);
530 private int nextVisible(HiddenColumns hidden,int maxWidth, int original, int dx)
532 int newCursorX=original+dx;
533 if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
535 int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
536 int[] region = hidden.getRegionWithEdgeAtRes(visx);
538 if (region != null) // just in case
543 newCursorX = region[1] + 1;
548 newCursorX = region[0] - 1;
552 newCursorX = (newCursorX < 0) ? 0 : newCursorX;
553 if (newCursorX >= maxWidth
554 || !hidden.isVisible(newCursorX))
556 newCursorX = original;
561 * Scroll to make the cursor visible in the viewport.
564 * just jump to the location rather than scrolling
566 void scrollToVisible(boolean jump)
568 if (seqCanvas.cursorX < 0)
570 seqCanvas.cursorX = 0;
572 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
574 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
577 if (seqCanvas.cursorY < 0)
579 seqCanvas.cursorY = 0;
581 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
583 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
588 boolean repaintNeeded = true;
591 // only need to repaint if the viewport did not move, as otherwise it will
593 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
598 if (av.getWrapAlignment())
600 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
601 int x = av.getAlignment().getHiddenColumns()
602 .absoluteToVisibleColumn(seqCanvas.cursorX);
603 av.getRanges().scrollToWrappedVisible(x);
607 av.getRanges().scrollToVisible(seqCanvas.cursorX,
612 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
614 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
615 seqCanvas.cursorX, seqCanvas.cursorY);
625 void setSelectionAreaAtCursor(boolean topLeft)
627 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
629 if (av.getSelectionGroup() != null)
631 SequenceGroup sg = av.getSelectionGroup();
632 // Find the top and bottom of this group
633 int min = av.getAlignment().getHeight(), max = 0;
634 for (int i = 0; i < sg.getSize(); i++)
636 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
651 sg.setStartRes(seqCanvas.cursorX);
652 if (sg.getEndRes() < seqCanvas.cursorX)
654 sg.setEndRes(seqCanvas.cursorX);
657 min = seqCanvas.cursorY;
661 sg.setEndRes(seqCanvas.cursorX);
662 if (sg.getStartRes() > seqCanvas.cursorX)
664 sg.setStartRes(seqCanvas.cursorX);
667 max = seqCanvas.cursorY + 1;
672 // Only the user can do this
673 av.setSelectionGroup(null);
677 // Now add any sequences between min and max
678 sg.getSequences(null).clear();
679 for (int i = min; i < max; i++)
681 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
686 if (av.getSelectionGroup() == null)
688 SequenceGroup sg = new SequenceGroup();
689 sg.setStartRes(seqCanvas.cursorX);
690 sg.setEndRes(seqCanvas.cursorX);
691 sg.addSequence(sequence, false);
692 av.setSelectionGroup(sg);
695 ap.paintAlignment(false, false);
699 void insertGapAtCursor(boolean group)
701 groupEditing = group;
702 editStartSeq = seqCanvas.cursorY;
703 editLastRes = seqCanvas.cursorX;
704 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
708 void deleteGapAtCursor(boolean group)
710 groupEditing = group;
711 editStartSeq = seqCanvas.cursorY;
712 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
713 editSequence(false, false, seqCanvas.cursorX);
717 void insertNucAtCursor(boolean group, String nuc)
719 // TODO not called - delete?
720 groupEditing = group;
721 editStartSeq = seqCanvas.cursorY;
722 editLastRes = seqCanvas.cursorX;
723 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
727 void numberPressed(char value)
729 if (keyboardNo1 == null)
731 keyboardNo1 = new StringBuffer();
734 if (keyboardNo2 != null)
736 keyboardNo2.append(value);
740 keyboardNo1.append(value);
748 if (keyboardNo1 != null)
750 int value = Integer.parseInt(keyboardNo1.toString());
754 } catch (Exception x)
765 if (keyboardNo2 != null)
767 int value = Integer.parseInt(keyboardNo2.toString());
771 } catch (Exception x)
785 public void mouseReleased(MouseEvent evt)
787 MousePos pos = findMousePosition(evt);
788 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
793 boolean didDrag = mouseDragging; // did we come here after a drag
794 mouseDragging = false;
795 mouseWheelPressed = false;
797 if (evt.isPopupTrigger()) // Windows: mouseReleased
799 showPopupMenu(evt, pos);
810 doMouseReleasedDefineMode(evt, didDrag);
821 public void mousePressed(MouseEvent evt)
823 lastMousePress = evt.getPoint();
824 MousePos pos = findMousePosition(evt);
825 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
830 if (SwingUtilities.isMiddleMouseButton(evt))
832 mouseWheelPressed = true;
836 boolean isControlDown = Platform.isControlDown(evt);
837 if (evt.isShiftDown() || isControlDown)
847 doMousePressedDefineMode(evt, pos);
851 int seq = pos.seqIndex;
852 int res = pos.column;
854 if ((seq < av.getAlignment().getHeight())
855 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
872 public void mouseOverSequence(SequenceI sequence, int index, int pos)
874 String tmp = sequence.hashCode() + " " + index + " " + pos;
876 if (lastMessage == null || !lastMessage.equals(tmp))
878 // System.err.println("mouseOver Sequence: "+tmp);
879 ssm.mouseOverSequence(sequence, index, pos, av);
885 * Highlight the mapped region described by the search results object (unless
886 * unchanged). This supports highlight of protein while mousing over linked
887 * cDNA and vice versa. The status bar is also updated to show the location of
888 * the start of the highlighted region.
891 public String highlightSequence(SearchResultsI results)
893 if (results == null || results.equals(lastSearchResults))
897 lastSearchResults = results;
899 boolean wasScrolled = false;
901 if (av.isFollowHighlight())
903 // don't allow highlight of protein/cDNA to also scroll a complementary
904 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
905 // over residue to change abruptly, causing highlighted residue in panel 2
906 // to change, causing a scroll in panel 1 etc)
907 ap.setToScrollComplementPanel(false);
908 wasScrolled = ap.scrollToPosition(results);
911 seqCanvas.revalidate();
913 ap.setToScrollComplementPanel(true);
916 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
917 if (seqCanvas.highlightSearchResults(results, fastPaint))
919 setStatusMessage(results);
921 return results.isEmpty() ? null : getHighlightInfo(results);
925 * temporary hack: answers a message suitable to show on structure hover
926 * label. This is normally null. It is a peptide variation description if
928 * <li>results are a single residue in a protein alignment</li>
929 * <li>there is a mapping to a coding sequence (codon)</li>
930 * <li>there are one or more SNP variant features on the codon</li>
932 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
937 private String getHighlightInfo(SearchResultsI results)
940 * ideally, just find mapped CDS (as we don't care about render style here);
941 * for now, go via split frame complement's FeatureRenderer
943 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
944 if (complement == null)
948 AlignFrame af = Desktop.getAlignFrameFor(complement);
949 FeatureRendererModel fr2 = af.getFeatureRenderer();
951 List<SearchResultMatchI> matches = results.getResults();
952 int j = matches.size();
953 List<String> infos = new ArrayList<>();
954 for (int i = 0; i < j; i++)
956 SearchResultMatchI match = matches.get(i);
957 int pos = match.getStart();
958 if (pos == match.getEnd())
960 SequenceI seq = match.getSequence();
961 SequenceI ds = seq.getDatasetSequence() == null ? seq
962 : seq.getDatasetSequence();
963 MappedFeatures mf = fr2
964 .findComplementFeaturesAtResidue(ds, pos);
967 for (SequenceFeature sf : mf.features)
969 String pv = mf.findProteinVariants(sf);
970 if (pv.length() > 0 && !infos.contains(pv))
983 StringBuilder sb = new StringBuilder();
984 for (String info : infos)
992 return sb.toString();
996 public VamsasSource getVamsasSource()
998 return this.ap == null ? null : this.ap.av;
1002 public void updateColours(SequenceI seq, int index)
1004 System.out.println("update the seqPanel colours");
1009 * Action on mouse movement is to update the status bar to show the current
1010 * sequence position, and (if features are shown) to show any features at the
1011 * position in a tooltip. Does nothing if the mouse move does not change
1017 public void mouseMoved(MouseEvent evt)
1021 // This is because MacOSX creates a mouseMoved
1022 // If control is down, other platforms will not.
1026 final MousePos mousePos = findMousePosition(evt);
1027 if (mousePos.equals(lastMousePosition))
1030 * just a pixel move without change of 'cell'
1032 moveTooltip = false;
1036 lastMousePosition = mousePos;
1038 if (mousePos.isOverAnnotation())
1040 mouseMovedOverAnnotation(mousePos);
1043 final int seq = mousePos.seqIndex;
1045 final int column = mousePos.column;
1046 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1048 lastMousePosition = null;
1049 setToolTipText(null);
1051 lastFormattedTooltip = null;
1052 ap.alignFrame.setStatus("");
1056 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1058 if (column >= sequence.getLength())
1064 * set status bar message, returning residue position in sequence
1066 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1067 final int pos = setStatusMessage(sequence, column, seq);
1068 if (ssm != null && !isGapped)
1070 mouseOverSequence(sequence, column, pos);
1073 StringBuilder tooltipText = new StringBuilder(64);
1075 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1078 for (int g = 0; g < groups.length; g++)
1080 if (groups[g].getStartRes() <= column
1081 && groups[g].getEndRes() >= column)
1083 if (!groups[g].getName().startsWith("JTreeGroup")
1084 && !groups[g].getName().startsWith("JGroup"))
1086 tooltipText.append(groups[g].getName());
1089 if (groups[g].getDescription() != null)
1091 tooltipText.append(": " + groups[g].getDescription());
1098 * add any features at the position to the tooltip; if over a gap, only
1099 * add features that straddle the gap (pos may be the residue before or
1102 int unshownFeatures = 0;
1103 if (av.isShowSequenceFeatures())
1105 List<SequenceFeature> features = ap.getFeatureRenderer()
1106 .findFeaturesAtColumn(sequence, column + 1);
1107 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1108 features, this.ap.getSeqPanel().seqCanvas.fr,
1109 MAX_TOOLTIP_LENGTH);
1112 * add features in CDS/protein complement at the corresponding
1113 * position if configured to do so
1115 if (av.isShowComplementFeatures())
1117 if (!Comparison.isGap(sequence.getCharAt(column)))
1119 AlignViewportI complement = ap.getAlignViewport()
1120 .getCodingComplement();
1121 AlignFrame af = Desktop.getAlignFrameFor(complement);
1122 FeatureRendererModel fr2 = af.getFeatureRenderer();
1123 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1127 unshownFeatures += seqARep.appendFeatures(tooltipText,
1128 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1133 if (tooltipText.length() == 0) // nothing added
1135 setToolTipText(null);
1140 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1142 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1143 tooltipText.append("...");
1145 if (unshownFeatures > 0)
1147 tooltipText.append("<br/>").append("... ").append("<i>")
1148 .append(MessageManager.formatMessage(
1149 "label.features_not_shown", unshownFeatures))
1152 String textString = tooltipText.toString();
1153 if (!textString.equals(lastTooltip))
1155 lastTooltip = textString;
1156 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1158 setToolTipText(lastFormattedTooltip);
1164 * When the view is in wrapped mode, and the mouse is over an annotation row,
1165 * shows the corresponding tooltip and status message (if any)
1170 protected void mouseMovedOverAnnotation(MousePos pos)
1172 final int column = pos.column;
1173 final int rowIndex = pos.annotationIndex;
1175 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1180 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1182 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1184 if (!tooltip.equals(lastTooltip))
1186 lastTooltip = tooltip;
1187 lastFormattedTooltip = tooltip == null ? null
1188 : JvSwingUtils.wrapTooltip(true, tooltip);
1189 setToolTipText(lastFormattedTooltip);
1192 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1194 ap.alignFrame.setStatus(msg);
1198 * if Shift key is held down while moving the mouse,
1199 * the tooltip location is not changed once shown
1201 private Point lastTooltipLocation = null;
1204 * this flag is false for pixel moves within a residue,
1205 * to reduce tooltip flicker
1207 private boolean moveTooltip = true;
1210 * a dummy tooltip used to estimate where to position tooltips
1212 private JToolTip tempTip = new JLabel().createToolTip();
1217 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1220 public Point getToolTipLocation(MouseEvent event)
1224 if (lastTooltip == null || !moveTooltip)
1229 if (lastTooltipLocation != null && event.isShiftDown())
1231 return lastTooltipLocation;
1234 int x = event.getX();
1235 int y = event.getY();
1238 tempTip.setTipText(lastFormattedTooltip);
1239 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1241 // was x += (w - x < 200) ? -(w / 2) : 5;
1242 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1243 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1245 return lastTooltipLocation = p;
1249 * set when the current UI interaction has resulted in a change that requires
1250 * shading in overviews and structures to be recalculated. this could be
1251 * changed to a something more expressive that indicates what actually has
1252 * changed, so selective redraws can be applied (ie. only structures, only
1255 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1258 * set if av.getSelectionGroup() refers to a group that is defined on the
1259 * alignment view, rather than a transient selection
1261 // private boolean editingDefinedGroup = false; // TODO: refactor to
1262 // avcontroller or viewModel
1265 * Sets the status message in alignment panel, showing the sequence number
1266 * (index) and id, and residue and residue position if not at a gap, for the
1267 * given sequence and column position. Returns the residue position returned
1268 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1269 * if at a gapped position.
1272 * aligned sequence object
1276 * index of sequence in alignment
1277 * @return sequence position of residue at column, or adjacent residue if at a
1280 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1282 char sequenceChar = sequence.getCharAt(column);
1283 int pos = sequence.findPosition(column);
1284 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1290 * Builds the status message for the current cursor location and writes it to
1291 * the status bar, for example
1294 * Sequence 3 ID: FER1_SOLLC
1295 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1296 * Sequence 5 ID: FER1_PEA Residue: B (3)
1297 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1302 * sequence position in the alignment (1..)
1303 * @param sequenceChar
1304 * the character under the cursor
1306 * the sequence residue position (if not over a gap)
1308 protected void setStatusMessage(String seqName, int seqIndex,
1309 char sequenceChar, int residuePos)
1311 StringBuilder text = new StringBuilder(32);
1314 * Sequence number (if known), and sequence name.
1316 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1317 text.append("Sequence").append(seqno).append(" ID: ")
1320 String residue = null;
1323 * Try to translate the display character to residue name (null for gap).
1325 boolean isGapped = Comparison.isGap(sequenceChar);
1329 boolean nucleotide = av.getAlignment().isNucleotide();
1330 String displayChar = String.valueOf(sequenceChar);
1333 residue = ResidueProperties.nucleotideName.get(displayChar);
1337 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1338 : ("*".equals(displayChar) ? "STOP"
1339 : ResidueProperties.aa2Triplet.get(displayChar));
1341 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1342 .append(": ").append(residue == null ? displayChar : residue);
1344 text.append(" (").append(Integer.toString(residuePos)).append(")");
1346 ap.alignFrame.setStatus(text.toString());
1350 * Set the status bar message to highlight the first matched position in
1355 private void setStatusMessage(SearchResultsI results)
1357 AlignmentI al = this.av.getAlignment();
1358 int sequenceIndex = al.findIndex(results);
1359 if (sequenceIndex == -1)
1363 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1364 SequenceI ds = alignedSeq.getDatasetSequence();
1365 for (SearchResultMatchI m : results.getResults())
1367 SequenceI seq = m.getSequence();
1368 if (seq.getDatasetSequence() != null)
1370 seq = seq.getDatasetSequence();
1375 int start = m.getStart();
1376 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1377 seq.getCharAt(start - 1), start);
1387 public void mouseDragged(MouseEvent evt)
1389 MousePos pos = findMousePosition(evt);
1390 if (pos.isOverAnnotation() || pos.column == -1)
1395 if (mouseWheelPressed)
1397 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1398 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1400 int oldWidth = av.getCharWidth();
1402 // Which is bigger, left-right or up-down?
1403 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1404 .abs(evt.getX() - lastMousePress.getX()))
1407 * on drag up or down, decrement or increment font size
1409 int fontSize = av.font.getSize();
1410 boolean fontChanged = false;
1412 if (evt.getY() < lastMousePress.getY())
1417 else if (evt.getY() > lastMousePress.getY())
1430 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1432 av.setFont(newFont, true);
1433 av.setCharWidth(oldWidth);
1437 ap.av.getCodingComplement().setFont(newFont, true);
1438 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1439 .getSplitViewContainer();
1440 splitFrame.adjustLayout();
1441 splitFrame.repaint();
1448 * on drag left or right, decrement or increment character width
1451 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1453 newWidth = av.getCharWidth() - 1;
1454 av.setCharWidth(newWidth);
1456 else if (evt.getX() > lastMousePress.getX())
1458 newWidth = av.getCharWidth() + 1;
1459 av.setCharWidth(newWidth);
1463 ap.paintAlignment(false, false);
1467 * need to ensure newWidth is set on cdna, regardless of which
1468 * panel the mouse drag happened in; protein will compute its
1469 * character width as 1:1 or 3:1
1471 av.getCodingComplement().setCharWidth(newWidth);
1472 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1473 .getSplitViewContainer();
1474 splitFrame.adjustLayout();
1475 splitFrame.repaint();
1480 FontMetrics fm = getFontMetrics(av.getFont());
1481 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1483 lastMousePress = evt.getPoint();
1490 dragStretchGroup(evt);
1494 int res = pos.column;
1501 if ((editLastRes == -1) || (editLastRes == res))
1506 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1508 // dragLeft, delete gap
1509 editSequence(false, false, res);
1513 editSequence(true, false, res);
1516 mouseDragging = true;
1517 if (scrollThread != null)
1519 scrollThread.setMousePosition(evt.getPoint());
1524 * Edits the sequence to insert or delete one or more gaps, in response to a
1525 * mouse drag or cursor mode command. The number of inserts/deletes may be
1526 * specified with the cursor command, or else depends on the mouse event
1527 * (normally one column, but potentially more for a fast mouse drag).
1529 * Delete gaps is limited to the number of gaps left of the cursor position
1530 * (mouse drag), or at or right of the cursor position (cursor mode).
1532 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1533 * the current selection group.
1535 * In locked editing mode (with a selection group present), inserts/deletions
1536 * within the selection group are limited to its boundaries (and edits outside
1537 * the group stop at its border).
1540 * true to insert gaps, false to delete gaps
1542 * (unused parameter)
1544 * the column at which to perform the action; the number of columns
1545 * affected depends on <code>this.editLastRes</code> (cursor column
1548 synchronized void editSequence(boolean insertGap, boolean editSeq,
1552 int fixedRight = -1;
1553 boolean fixedColumns = false;
1554 SequenceGroup sg = av.getSelectionGroup();
1556 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1558 // No group, but the sequence may represent a group
1559 if (!groupEditing && av.hasHiddenRows())
1561 if (av.isHiddenRepSequence(seq))
1563 sg = av.getRepresentedSequences(seq);
1564 groupEditing = true;
1568 StringBuilder message = new StringBuilder(64); // for status bar
1571 * make a name for the edit action, for
1572 * status bar message and Undo/Redo menu
1574 String label = null;
1577 message.append("Edit group:");
1578 label = MessageManager.getString("action.edit_group");
1582 message.append("Edit sequence: " + seq.getName());
1583 label = seq.getName();
1584 if (label.length() > 10)
1586 label = label.substring(0, 10);
1588 label = MessageManager.formatMessage("label.edit_params",
1594 * initialise the edit command if there is not
1595 * already one being extended
1597 if (editCommand == null)
1599 editCommand = new EditCommand(label);
1604 message.append(" insert ");
1608 message.append(" delete ");
1611 message.append(Math.abs(startres - editLastRes) + " gaps.");
1612 ap.alignFrame.setStatus(message.toString());
1615 * is there a selection group containing the sequence being edited?
1616 * if so the boundary of the group is the limit of the edit
1617 * (but the edit may be inside or outside the selection group)
1619 boolean inSelectionGroup = sg != null
1620 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1621 if (groupEditing || inSelectionGroup)
1623 fixedColumns = true;
1625 // sg might be null as the user may only see 1 sequence,
1626 // but the sequence represents a group
1629 if (!av.isHiddenRepSequence(seq))
1634 sg = av.getRepresentedSequences(seq);
1637 fixedLeft = sg.getStartRes();
1638 fixedRight = sg.getEndRes();
1640 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1641 || (startres >= fixedLeft && editLastRes < fixedLeft)
1642 || (startres > fixedRight && editLastRes <= fixedRight)
1643 || (startres <= fixedRight && editLastRes > fixedRight))
1649 if (fixedLeft > startres)
1651 fixedRight = fixedLeft - 1;
1654 else if (fixedRight < startres)
1656 fixedLeft = fixedRight;
1661 if (av.hasHiddenColumns())
1663 fixedColumns = true;
1664 int y1 = av.getAlignment().getHiddenColumns()
1665 .getNextHiddenBoundary(true, startres);
1666 int y2 = av.getAlignment().getHiddenColumns()
1667 .getNextHiddenBoundary(false, startres);
1669 if ((insertGap && startres > y1 && editLastRes < y1)
1670 || (!insertGap && startres < y2 && editLastRes > y2))
1676 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1677 // Selection spans a hidden region
1678 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1686 fixedRight = y2 - 1;
1691 boolean success = doEditSequence(insertGap, editSeq, startres,
1692 fixedRight, fixedColumns, sg);
1695 * report what actually happened (might be less than
1696 * what was requested), by inspecting the edit commands added
1698 String msg = getEditStatusMessage(editCommand);
1699 ap.alignFrame.setStatus(msg == null ? " " : msg);
1705 editLastRes = startres;
1706 seqCanvas.repaint();
1710 * A helper method that performs the requested editing to insert or delete
1711 * gaps (if possible). Answers true if the edit was successful, false if could
1712 * only be performed in part or not at all. Failure may occur in 'locked edit'
1713 * mode, when an insertion requires a matching gapped position (or column) to
1714 * delete, and deletion requires an adjacent gapped position (or column) to
1718 * true if inserting gap(s), false if deleting
1720 * (unused parameter, currently always false)
1722 * the column at which to perform the edit
1724 * fixed right boundary column of a locked edit (within or to the
1725 * left of a selection group)
1726 * @param fixedColumns
1727 * true if this is a locked edit
1729 * the sequence group (if group edit is being performed)
1732 protected boolean doEditSequence(final boolean insertGap,
1733 final boolean editSeq, final int startres, int fixedRight,
1734 final boolean fixedColumns, final SequenceGroup sg)
1736 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1737 SequenceI[] seqs = new SequenceI[] { seq };
1741 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1742 int g, groupSize = vseqs.size();
1743 SequenceI[] groupSeqs = new SequenceI[groupSize];
1744 for (g = 0; g < groupSeqs.length; g++)
1746 groupSeqs[g] = vseqs.get(g);
1752 // If the user has selected the whole sequence, and is dragging to
1753 // the right, we can still extend the alignment and selectionGroup
1754 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1755 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1758 av.getAlignment().getWidth() + startres - editLastRes);
1759 fixedRight = sg.getEndRes();
1762 // Is it valid with fixed columns??
1763 // Find the next gap before the end
1764 // of the visible region boundary
1765 boolean blank = false;
1766 for (; fixedRight > editLastRes; fixedRight--)
1770 for (g = 0; g < groupSize; g++)
1772 for (int j = 0; j < startres - editLastRes; j++)
1775 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1790 if (sg.getSize() == av.getAlignment().getHeight())
1792 if ((av.hasHiddenColumns()
1793 && startres < av.getAlignment().getHiddenColumns()
1794 .getNextHiddenBoundary(false, startres)))
1799 int alWidth = av.getAlignment().getWidth();
1800 if (av.hasHiddenRows())
1802 int hwidth = av.getAlignment().getHiddenSequences()
1804 if (hwidth > alWidth)
1809 // We can still insert gaps if the selectionGroup
1810 // contains all the sequences
1811 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1812 fixedRight = alWidth + startres - editLastRes;
1822 else if (!insertGap)
1824 // / Are we able to delete?
1825 // ie are all columns blank?
1827 for (g = 0; g < groupSize; g++)
1829 for (int j = startres; j < editLastRes; j++)
1831 if (groupSeqs[g].getLength() <= j)
1836 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1838 // Not a gap, block edit not valid
1847 // dragging to the right
1848 if (fixedColumns && fixedRight != -1)
1850 for (int j = editLastRes; j < startres; j++)
1852 insertGap(j, groupSeqs, fixedRight);
1857 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1858 startres - editLastRes, false);
1863 // dragging to the left
1864 if (fixedColumns && fixedRight != -1)
1866 for (int j = editLastRes; j > startres; j--)
1868 deleteChar(startres, groupSeqs, fixedRight);
1873 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1874 editLastRes - startres, false);
1881 * editing a single sequence
1885 // dragging to the right
1886 if (fixedColumns && fixedRight != -1)
1888 for (int j = editLastRes; j < startres; j++)
1890 if (!insertGap(j, seqs, fixedRight))
1893 * e.g. cursor mode command specified
1894 * more inserts than are possible
1902 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1903 startres - editLastRes, false);
1910 // dragging to the left
1911 if (fixedColumns && fixedRight != -1)
1913 for (int j = editLastRes; j > startres; j--)
1915 if (!Comparison.isGap(seq.getCharAt(startres)))
1919 deleteChar(startres, seqs, fixedRight);
1924 // could be a keyboard edit trying to delete none gaps
1926 for (int m = startres; m < editLastRes; m++)
1928 if (!Comparison.isGap(seq.getCharAt(m)))
1936 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1941 {// insertGap==false AND editSeq==TRUE;
1942 if (fixedColumns && fixedRight != -1)
1944 for (int j = editLastRes; j < startres; j++)
1946 insertGap(j, seqs, fixedRight);
1951 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1952 startres - editLastRes, false);
1962 * Constructs an informative status bar message while dragging to insert or
1963 * delete gaps. Answers null if inserts and deletes cancel out.
1965 * @param editCommand
1966 * a command containing the list of individual edits
1969 protected static String getEditStatusMessage(EditCommand editCommand)
1971 if (editCommand == null)
1977 * add any inserts, and subtract any deletes,
1978 * not counting those auto-inserted when doing a 'locked edit'
1979 * (so only counting edits 'under the cursor')
1982 for (Edit cmd : editCommand.getEdits())
1984 if (!cmd.isSystemGenerated())
1986 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1994 * inserts and deletes cancel out
1999 String msgKey = count > 1 ? "label.insert_gaps"
2000 : (count == 1 ? "label.insert_gap"
2001 : (count == -1 ? "label.delete_gap"
2002 : "label.delete_gaps"));
2003 count = Math.abs(count);
2005 return MessageManager.formatMessage(msgKey, String.valueOf(count));
2009 * Inserts one gap at column j, deleting the right-most gapped column up to
2010 * (and including) fixedColumn. Returns true if the edit is successful, false
2011 * if no blank column is available to allow the insertion to be balanced by a
2016 * @param fixedColumn
2019 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
2021 int blankColumn = fixedColumn;
2022 for (int s = 0; s < seq.length; s++)
2024 // Find the next gap before the end of the visible region boundary
2025 // If lastCol > j, theres a boundary after the gap insertion
2027 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
2029 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
2031 // Theres a space, so break and insert the gap
2036 if (blankColumn <= j)
2038 blankColumn = fixedColumn;
2044 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2046 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2052 * Helper method to add and perform one edit action
2058 * @param systemGenerated
2059 * true if the edit is a 'balancing' delete (or insert) to match a
2060 * user's insert (or delete) in a locked editing region
2062 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2063 int count, boolean systemGenerated)
2066 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2067 av.getAlignment().getGapCharacter());
2068 edit.setSystemGenerated(systemGenerated);
2070 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2074 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2075 * each of the given sequences. The caller should ensure that all sequences
2076 * are gapped in column j.
2080 * @param fixedColumn
2082 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2084 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2086 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2090 * On reentering the panel, stops any scrolling that was started on dragging
2096 public void mouseEntered(MouseEvent e)
2106 * On leaving the panel, if the mouse is being dragged, starts a thread to
2107 * scroll it until the mouse is released (in unwrapped mode only)
2112 public void mouseExited(MouseEvent e)
2114 lastMousePosition = null;
2115 ap.alignFrame.setStatus(" ");
2116 if (av.getWrapAlignment())
2121 if (mouseDragging && scrollThread == null)
2123 startScrolling(e.getPoint());
2128 * Handler for double-click on a position with one or more sequence features.
2129 * Opens the Amend Features dialog to allow feature details to be amended, or
2130 * the feature deleted.
2133 public void mouseClicked(MouseEvent evt)
2135 SequenceGroup sg = null;
2136 MousePos pos = findMousePosition(evt);
2137 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2142 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2144 sg = av.getSelectionGroup();
2145 if (sg != null && sg.getSize() == 1
2146 && sg.getEndRes() - sg.getStartRes() < 2)
2148 av.setSelectionGroup(null);
2151 int column = pos.column;
2154 * find features at the position (if not gapped), or straddling
2155 * the position (if at a gap)
2157 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2158 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2159 .findFeaturesAtColumn(sequence, column + 1);
2161 if (!features.isEmpty())
2164 * highlight the first feature at the position on the alignment
2166 SearchResultsI highlight = new SearchResults();
2167 highlight.addResult(sequence, features.get(0).getBegin(), features
2169 seqCanvas.highlightSearchResults(highlight, true);
2172 * open the Amend Features dialog
2174 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2175 false).showDialog();
2181 public void mouseWheelMoved(MouseWheelEvent e)
2184 double wheelRotation = e.getPreciseWheelRotation();
2185 if (wheelRotation > 0)
2187 if (e.isShiftDown())
2189 av.getRanges().scrollRight(true);
2194 av.getRanges().scrollUp(false);
2197 else if (wheelRotation < 0)
2199 if (e.isShiftDown())
2201 av.getRanges().scrollRight(false);
2205 av.getRanges().scrollUp(true);
2210 * update status bar and tooltip for new position
2211 * (need to synthesize a mouse movement to refresh tooltip)
2214 ToolTipManager.sharedInstance().mouseMoved(e);
2223 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2225 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2230 final int res = pos.column;
2231 final int seq = pos.seqIndex;
2233 updateOverviewAndStructs = false;
2235 startWrapBlock = wrappedBlock;
2237 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2239 if ((sequence == null) || (res > sequence.getLength()))
2244 stretchGroup = av.getSelectionGroup();
2246 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2248 stretchGroup = av.getAlignment().findGroup(sequence, res);
2249 if (stretchGroup != null)
2251 // only update the current selection if the popup menu has a group to
2253 av.setSelectionGroup(stretchGroup);
2258 * defer right-mouse click handling to mouseReleased on Windows
2259 * (where isPopupTrigger() will answer true)
2260 * NB isRightMouseButton is also true for Cmd-click on Mac
2262 if (Platform.isWinRightButton(evt))
2267 if (evt.isPopupTrigger()) // Mac: mousePressed
2269 showPopupMenu(evt, pos);
2275 seqCanvas.cursorX = res;
2276 seqCanvas.cursorY = seq;
2277 seqCanvas.repaint();
2281 if (stretchGroup == null)
2283 createStretchGroup(res, sequence);
2286 if (stretchGroup != null)
2288 stretchGroup.addPropertyChangeListener(seqCanvas);
2291 seqCanvas.repaint();
2294 private void createStretchGroup(int res, SequenceI sequence)
2296 // Only if left mouse button do we want to change group sizes
2297 // define a new group here
2298 SequenceGroup sg = new SequenceGroup();
2299 sg.setStartRes(res);
2301 sg.addSequence(sequence, false);
2302 av.setSelectionGroup(sg);
2305 if (av.getConservationSelected())
2307 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2311 if (av.getAbovePIDThreshold())
2313 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2316 // TODO: stretchGroup will always be not null. Is this a merge error ?
2317 // or is there a threading issue here?
2318 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2320 // Edit end res position of selected group
2321 changeEndRes = true;
2323 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2325 // Edit end res position of selected group
2326 changeStartRes = true;
2328 stretchGroup.getWidth();
2333 * Build and show a pop-up menu at the right-click mouse position
2338 void showPopupMenu(MouseEvent evt, MousePos pos)
2340 final int column = pos.column;
2341 final int seq = pos.seqIndex;
2342 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2343 if (sequence != null)
2345 PopupMenu pop = new PopupMenu(ap, sequence, column);
2346 pop.show(this, evt.getX(), evt.getY());
2351 * Update the display after mouse up on a selection or group
2354 * mouse released event details
2356 * true if this event is happening after a mouse drag (rather than a
2359 protected void doMouseReleasedDefineMode(MouseEvent evt,
2362 if (stretchGroup == null)
2367 stretchGroup.removePropertyChangeListener(seqCanvas);
2369 // always do this - annotation has own state
2370 // but defer colourscheme update until hidden sequences are passed in
2371 boolean vischange = stretchGroup.recalcConservation(true);
2372 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2374 if (stretchGroup.cs != null)
2378 stretchGroup.cs.alignmentChanged(stretchGroup,
2379 av.getHiddenRepSequences());
2382 ResidueShaderI groupColourScheme = stretchGroup
2383 .getGroupColourScheme();
2384 String name = stretchGroup.getName();
2385 if (stretchGroup.cs.conservationApplied())
2387 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2389 if (stretchGroup.cs.getThreshold() > 0)
2391 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2394 PaintRefresher.Refresh(this, av.getSequenceSetId());
2395 // TODO: structure colours only need updating if stretchGroup used to or now
2396 // does contain sequences with structure views
2397 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2398 updateOverviewAndStructs = false;
2399 changeEndRes = false;
2400 changeStartRes = false;
2401 stretchGroup = null;
2406 * Resizes the borders of a selection group depending on the direction of
2411 protected void dragStretchGroup(MouseEvent evt)
2413 if (stretchGroup == null)
2418 MousePos pos = findMousePosition(evt);
2419 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2424 int res = pos.column;
2425 int y = pos.seqIndex;
2427 if (wrappedBlock != startWrapBlock)
2432 res = Math.min(res, av.getAlignment().getWidth()-1);
2434 if (stretchGroup.getEndRes() == res)
2436 // Edit end res position of selected group
2437 changeEndRes = true;
2439 else if (stretchGroup.getStartRes() == res)
2441 // Edit start res position of selected group
2442 changeStartRes = true;
2445 if (res < av.getRanges().getStartRes())
2447 res = av.getRanges().getStartRes();
2452 if (res > (stretchGroup.getStartRes() - 1))
2454 stretchGroup.setEndRes(res);
2455 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2458 else if (changeStartRes)
2460 if (res < (stretchGroup.getEndRes() + 1))
2462 stretchGroup.setStartRes(res);
2463 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2467 int dragDirection = 0;
2473 else if (y < oldSeq)
2478 while ((y != oldSeq) && (oldSeq > -1)
2479 && (y < av.getAlignment().getHeight()))
2481 // This routine ensures we don't skip any sequences, as the
2482 // selection is quite slow.
2483 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2485 oldSeq += dragDirection;
2492 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2494 if (stretchGroup.getSequences(null).contains(nextSeq))
2496 stretchGroup.deleteSequence(seq, false);
2497 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2503 stretchGroup.addSequence(seq, false);
2506 stretchGroup.addSequence(nextSeq, false);
2507 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2516 mouseDragging = true;
2518 if (scrollThread != null)
2520 scrollThread.setMousePosition(evt.getPoint());
2524 * construct a status message showing the range of the selection
2526 StringBuilder status = new StringBuilder(64);
2527 List<SequenceI> seqs = stretchGroup.getSequences();
2528 String name = seqs.get(0).getName();
2529 if (name.length() > 20)
2531 name = name.substring(0, 20);
2533 status.append(name).append(" - ");
2534 name = seqs.get(seqs.size() - 1).getName();
2535 if (name.length() > 20)
2537 name = name.substring(0, 20);
2539 status.append(name).append(" ");
2540 int startRes = stretchGroup.getStartRes();
2541 status.append(" cols ").append(String.valueOf(startRes + 1))
2543 int endRes = stretchGroup.getEndRes();
2544 status.append(String.valueOf(endRes + 1));
2545 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2546 .append(String.valueOf(endRes - startRes + 1)).append(")");
2547 ap.alignFrame.setStatus(status.toString());
2551 * Stops the scroll thread if it is running
2553 void stopScrolling()
2555 if (scrollThread != null)
2557 scrollThread.stopScrolling();
2558 scrollThread = null;
2560 mouseDragging = false;
2564 * Starts a thread to scroll the alignment, towards a given mouse position
2565 * outside the panel bounds, unless the alignment is in wrapped mode
2569 void startScrolling(Point mousePos)
2572 * set this.mouseDragging in case this was called from
2573 * a drag in ScalePanel or AnnotationPanel
2575 mouseDragging = true;
2576 if (!av.getWrapAlignment() && scrollThread == null)
2578 scrollThread = new ScrollThread();
2579 scrollThread.setMousePosition(mousePos);
2580 if (Platform.isJS())
2583 * Javascript - run every 20ms until scrolling stopped
2584 * or reaches the limit of scrollable alignment
2586 Timer t = new Timer(20, new ActionListener()
2589 public void actionPerformed(ActionEvent e)
2591 if (scrollThread != null)
2593 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2594 scrollThread.scrollOnce();
2598 t.addActionListener(new ActionListener()
2601 public void actionPerformed(ActionEvent e)
2603 if (scrollThread == null)
2605 // SeqPanel.stopScrolling called
2615 * Java - run in a new thread
2617 scrollThread.start();
2623 * Performs scrolling of the visible alignment left, right, up or down, until
2624 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2625 * limit of the alignment is reached
2627 class ScrollThread extends Thread
2629 private Point mousePos;
2631 private volatile boolean keepRunning = true;
2636 public ScrollThread()
2638 setName("SeqPanel$ScrollThread");
2642 * Sets the position of the mouse that determines the direction of the
2643 * scroll to perform. If this is called as the mouse moves, scrolling should
2644 * respond accordingly. For example, if the mouse is dragged right, scroll
2645 * right should start; if the drag continues down, scroll down should also
2650 public void setMousePosition(Point p)
2656 * Sets a flag that will cause the thread to exit
2658 public void stopScrolling()
2660 keepRunning = false;
2664 * Scrolls the alignment left or right, and/or up or down, depending on the
2665 * last notified mouse position, until the limit of the alignment is
2666 * reached, or a flag is set to stop the scroll
2673 if (mousePos != null)
2675 keepRunning = scrollOnce();
2680 } catch (Exception ex)
2684 SeqPanel.this.scrollThread = null;
2690 * <li>one row up, if the mouse is above the panel</li>
2691 * <li>one row down, if the mouse is below the panel</li>
2692 * <li>one column left, if the mouse is left of the panel</li>
2693 * <li>one column right, if the mouse is right of the panel</li>
2695 * Answers true if a scroll was performed, false if not - meaning either
2696 * that the mouse position is within the panel, or the edge of the alignment
2699 boolean scrollOnce()
2702 * quit after mouseUp ensures interrupt in JalviewJS
2709 boolean scrolled = false;
2710 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2717 // mouse is above this panel - try scroll up
2718 scrolled = ranges.scrollUp(true);
2720 else if (mousePos.y >= getHeight())
2722 // mouse is below this panel - try scroll down
2723 scrolled = ranges.scrollUp(false);
2727 * scroll left or right
2731 scrolled |= ranges.scrollRight(false);
2733 else if (mousePos.x >= getWidth())
2735 scrolled |= ranges.scrollRight(true);
2742 * modify current selection according to a received message.
2745 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2746 HiddenColumns hidden, SelectionSource source)
2748 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2749 // handles selection messages...
2750 // TODO: extend config options to allow user to control if selections may be
2751 // shared between viewports.
2752 boolean iSentTheSelection = (av == source
2753 || (source instanceof AlignViewport
2754 && ((AlignmentViewport) source).getSequenceSetId()
2755 .equals(av.getSequenceSetId())));
2757 if (iSentTheSelection)
2759 // respond to our own event by updating dependent dialogs
2760 if (ap.getCalculationDialog() != null)
2762 ap.getCalculationDialog().validateCalcTypes();
2768 // process further ?
2769 if (!av.followSelection)
2775 * Ignore the selection if there is one of our own pending.
2777 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2783 * Check for selection in a view of which this one is a dna/protein
2786 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2791 // do we want to thread this ? (contention with seqsel and colsel locks, I
2794 * only copy colsel if there is a real intersection between
2795 * sequence selection and this panel's alignment
2797 boolean repaint = false;
2798 boolean copycolsel = false;
2800 SequenceGroup sgroup = null;
2801 if (seqsel != null && seqsel.getSize() > 0)
2803 if (av.getAlignment() == null)
2805 Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2806 + " ViewId=" + av.getViewId()
2807 + " 's alignment is NULL! returning immediately.");
2810 sgroup = seqsel.intersect(av.getAlignment(),
2811 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2812 if ((sgroup != null && sgroup.getSize() > 0))
2817 if (sgroup != null && sgroup.getSize() > 0)
2819 av.setSelectionGroup(sgroup);
2823 av.setSelectionGroup(null);
2825 av.isSelectionGroupChanged(true);
2830 // the current selection is unset or from a previous message
2831 // so import the new colsel.
2832 if (colsel == null || colsel.isEmpty())
2834 if (av.getColumnSelection() != null)
2836 av.getColumnSelection().clear();
2842 // TODO: shift colSel according to the intersecting sequences
2843 if (av.getColumnSelection() == null)
2845 av.setColumnSelection(new ColumnSelection(colsel));
2849 av.getColumnSelection().setElementsFrom(colsel,
2850 av.getAlignment().getHiddenColumns());
2853 av.isColSelChanged(true);
2857 if (copycolsel && av.hasHiddenColumns()
2858 && (av.getAlignment().getHiddenColumns() == null))
2860 System.err.println("Bad things");
2862 if (repaint) // always true!
2864 // probably finessing with multiple redraws here
2865 PaintRefresher.Refresh(this, av.getSequenceSetId());
2866 // ap.paintAlignment(false);
2869 // lastly, update dependent dialogs
2870 if (ap.getCalculationDialog() != null)
2872 ap.getCalculationDialog().validateCalcTypes();
2878 * If this panel is a cdna/protein translation view of the selection source,
2879 * tries to map the source selection to a local one, and returns true. Else
2886 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2887 ColumnSelection colsel, HiddenColumns hidden,
2888 SelectionSource source)
2890 if (!(source instanceof AlignViewportI))
2894 final AlignViewportI sourceAv = (AlignViewportI) source;
2895 if (sourceAv.getCodingComplement() != av
2896 && av.getCodingComplement() != sourceAv)
2902 * Map sequence selection
2904 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2905 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2906 av.isSelectionGroupChanged(true);
2909 * Map column selection
2911 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2913 ColumnSelection cs = new ColumnSelection();
2914 HiddenColumns hs = new HiddenColumns();
2915 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2916 av.setColumnSelection(cs);
2917 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2919 // lastly, update any dependent dialogs
2920 if (ap.getCalculationDialog() != null)
2922 ap.getCalculationDialog().validateCalcTypes();
2926 * repaint alignment, and also Overview or Structure
2927 * if hidden column selection has changed
2929 ap.paintAlignment(hiddenChanged, hiddenChanged);
2936 * @return null or last search results handled by this panel
2938 public SearchResultsI getLastSearchResults()
2940 return lastSearchResults;