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);
251 setBackground(Color.white);
253 seqCanvas = new SeqCanvas(alignPanel);
254 setLayout(new BorderLayout());
255 add(seqCanvas, BorderLayout.CENTER);
257 this.ap = alignPanel;
259 if (!viewport.isDataset())
261 addMouseMotionListener(this);
262 addMouseListener(this);
263 addMouseWheelListener(this);
264 ssm = viewport.getStructureSelectionManager();
265 ssm.addStructureViewerListener(this);
266 ssm.addSelectionListener(this);
270 int startWrapBlock = -1;
272 int wrappedBlock = -1;
275 * Computes the column and sequence row (and possibly annotation row when in
276 * wrapped mode) for the given mouse position
278 * Mouse position is not set if in wrapped mode with the cursor either between
279 * sequences, or over the left or right vertical scale.
284 MousePos findMousePosition(MouseEvent evt)
286 int col = findColumn(evt);
291 int charHeight = av.getCharHeight();
292 int alignmentHeight = av.getAlignment().getHeight();
293 if (av.getWrapAlignment())
295 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
296 seqCanvas.getHeight());
299 * yPos modulo height of repeating width
301 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
304 * height of sequences plus space / scale above,
305 * plus gap between sequences and annotations
307 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
308 + alignmentHeight * charHeight
309 + SeqCanvas.SEQS_ANNOTATION_GAP;
310 if (yOffsetPx >= alignmentHeightPixels)
313 * mouse is over annotations; find annotation index, also set
314 * last sequence above (for backwards compatible behaviour)
316 AlignmentAnnotation[] anns = av.getAlignment()
317 .getAlignmentAnnotation();
318 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
319 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
320 seqIndex = alignmentHeight - 1;
325 * mouse is over sequence (or the space above sequences)
327 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
330 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
336 ViewportRanges ranges = av.getRanges();
337 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
338 alignmentHeight - 1);
339 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
342 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);
489 void moveCursor(int dx, int dy, boolean nextWord)
491 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
495 int maxWidth = av.getAlignment().getWidth();
496 int maxHeight = av.getAlignment().getHeight();
497 SequenceI seqAtRow = av.getAlignment()
498 .getSequenceAt(seqCanvas.cursorY);
499 // look for next gap or residue
500 boolean isGap = Comparison
501 .isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
502 int p = seqCanvas.cursorX, lastP, r = seqCanvas.cursorY, lastR;
518 seqAtRow = av.getAlignment().getSequenceAt(r);
520 p = nextVisible(hidden, maxWidth, p, dx);
521 } while ((dx != 0 ? p != lastP : r != lastR)
522 && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
523 seqCanvas.cursorX = p;
524 seqCanvas.cursorY = r;
528 int maxWidth = av.getAlignment().getWidth();
529 seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX,
531 seqCanvas.cursorY += dy;
533 scrollToVisible(false);
536 private int nextVisible(HiddenColumns hidden, int maxWidth, int original,
539 int newCursorX = original + dx;
540 if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
542 int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
543 int[] region = hidden.getRegionWithEdgeAtRes(visx);
545 if (region != null) // just in case
550 newCursorX = region[1] + 1;
555 newCursorX = region[0] - 1;
559 newCursorX = (newCursorX < 0) ? 0 : newCursorX;
560 if (newCursorX >= maxWidth || !hidden.isVisible(newCursorX))
562 newCursorX = original;
568 * Scroll to make the cursor visible in the viewport.
571 * just jump to the location rather than scrolling
573 void scrollToVisible(boolean jump)
575 if (seqCanvas.cursorX < 0)
577 seqCanvas.cursorX = 0;
579 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
581 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
584 if (seqCanvas.cursorY < 0)
586 seqCanvas.cursorY = 0;
588 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
590 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
595 boolean repaintNeeded = true;
598 // only need to repaint if the viewport did not move, as otherwise it will
600 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
605 if (av.getWrapAlignment())
607 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
608 int x = av.getAlignment().getHiddenColumns()
609 .absoluteToVisibleColumn(seqCanvas.cursorX);
610 av.getRanges().scrollToWrappedVisible(x);
614 av.getRanges().scrollToVisible(seqCanvas.cursorX,
619 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
621 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
622 seqCanvas.cursorX, seqCanvas.cursorY);
631 void setSelectionAreaAtCursor(boolean topLeft)
633 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
635 if (av.getSelectionGroup() != null)
637 SequenceGroup sg = av.getSelectionGroup();
638 // Find the top and bottom of this group
639 int min = av.getAlignment().getHeight(), max = 0;
640 for (int i = 0; i < sg.getSize(); i++)
642 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
657 sg.setStartRes(seqCanvas.cursorX);
658 if (sg.getEndRes() < seqCanvas.cursorX)
660 sg.setEndRes(seqCanvas.cursorX);
663 min = seqCanvas.cursorY;
667 sg.setEndRes(seqCanvas.cursorX);
668 if (sg.getStartRes() > seqCanvas.cursorX)
670 sg.setStartRes(seqCanvas.cursorX);
673 max = seqCanvas.cursorY + 1;
678 // Only the user can do this
679 av.setSelectionGroup(null);
683 // Now add any sequences between min and max
684 sg.getSequences(null).clear();
685 for (int i = min; i < max; i++)
687 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
692 if (av.getSelectionGroup() == null)
694 SequenceGroup sg = new SequenceGroup();
695 sg.setStartRes(seqCanvas.cursorX);
696 sg.setEndRes(seqCanvas.cursorX);
697 sg.addSequence(sequence, false);
698 av.setSelectionGroup(sg);
701 ap.paintAlignment(false, false);
705 void insertGapAtCursor(boolean group)
707 groupEditing = group;
708 editStartSeq = seqCanvas.cursorY;
709 editLastRes = seqCanvas.cursorX;
710 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
714 void deleteGapAtCursor(boolean group)
716 groupEditing = group;
717 editStartSeq = seqCanvas.cursorY;
718 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
719 editSequence(false, false, seqCanvas.cursorX);
723 void insertNucAtCursor(boolean group, String nuc)
725 // TODO not called - delete?
726 groupEditing = group;
727 editStartSeq = seqCanvas.cursorY;
728 editLastRes = seqCanvas.cursorX;
729 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
733 void numberPressed(char value)
735 if (keyboardNo1 == null)
737 keyboardNo1 = new StringBuffer();
740 if (keyboardNo2 != null)
742 keyboardNo2.append(value);
746 keyboardNo1.append(value);
754 if (keyboardNo1 != null)
756 int value = Integer.parseInt(keyboardNo1.toString());
760 } catch (Exception x)
771 if (keyboardNo2 != null)
773 int value = Integer.parseInt(keyboardNo2.toString());
777 } catch (Exception x)
791 public void mouseReleased(MouseEvent evt)
793 MousePos pos = findMousePosition(evt);
794 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
799 boolean didDrag = mouseDragging; // did we come here after a drag
800 mouseDragging = false;
801 mouseWheelPressed = false;
803 if (evt.isPopupTrigger()) // Windows: mouseReleased
805 showPopupMenu(evt, pos);
816 doMouseReleasedDefineMode(evt, didDrag);
827 public void mousePressed(MouseEvent evt)
829 lastMousePress = evt.getPoint();
830 MousePos pos = findMousePosition(evt);
831 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
836 if (SwingUtilities.isMiddleMouseButton(evt))
838 mouseWheelPressed = true;
842 boolean isControlDown = Platform.isControlDown(evt);
843 if (evt.isShiftDown() || isControlDown)
853 doMousePressedDefineMode(evt, pos);
857 int seq = pos.seqIndex;
858 int res = pos.column;
860 if ((seq < av.getAlignment().getHeight())
861 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
878 public void mouseOverSequence(SequenceI sequence, int index, int pos)
880 String tmp = sequence.hashCode() + " " + index + " " + pos;
882 if (lastMessage == null || !lastMessage.equals(tmp))
884 // System.err.println("mouseOver Sequence: "+tmp);
885 ssm.mouseOverSequence(sequence, index, pos, av);
891 * Highlight the mapped region described by the search results object (unless
892 * unchanged). This supports highlight of protein while mousing over linked
893 * cDNA and vice versa. The status bar is also updated to show the location of
894 * the start of the highlighted region.
897 public String highlightSequence(SearchResultsI results)
899 if (results == null || results.equals(lastSearchResults))
903 lastSearchResults = results;
905 boolean wasScrolled = false;
907 if (av.isFollowHighlight())
909 // don't allow highlight of protein/cDNA to also scroll a complementary
910 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
911 // over residue to change abruptly, causing highlighted residue in panel 2
912 // to change, causing a scroll in panel 1 etc)
913 ap.setToScrollComplementPanel(false);
914 wasScrolled = ap.scrollToPosition(results);
917 seqCanvas.revalidate();
919 ap.setToScrollComplementPanel(true);
922 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
923 if (seqCanvas.highlightSearchResults(results, fastPaint))
925 setStatusMessage(results);
927 return results.isEmpty() ? null : getHighlightInfo(results);
931 * temporary hack: answers a message suitable to show on structure hover
932 * label. This is normally null. It is a peptide variation description if
934 * <li>results are a single residue in a protein alignment</li>
935 * <li>there is a mapping to a coding sequence (codon)</li>
936 * <li>there are one or more SNP variant features on the codon</li>
938 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
943 private String getHighlightInfo(SearchResultsI results)
946 * ideally, just find mapped CDS (as we don't care about render style here);
947 * for now, go via split frame complement's FeatureRenderer
949 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
950 if (complement == null)
954 AlignFrame af = Desktop.getAlignFrameFor(complement);
955 FeatureRendererModel fr2 = af.getFeatureRenderer();
957 List<SearchResultMatchI> matches = results.getResults();
958 int j = matches.size();
959 List<String> infos = new ArrayList<>();
960 for (int i = 0; i < j; i++)
962 SearchResultMatchI match = matches.get(i);
963 int pos = match.getStart();
964 if (pos == match.getEnd())
966 SequenceI seq = match.getSequence();
967 SequenceI ds = seq.getDatasetSequence() == null ? seq
968 : seq.getDatasetSequence();
969 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(ds, pos);
972 for (SequenceFeature sf : mf.features)
974 String pv = mf.findProteinVariants(sf);
975 if (pv.length() > 0 && !infos.contains(pv))
988 StringBuilder sb = new StringBuilder();
989 for (String info : infos)
997 return sb.toString();
1001 public VamsasSource getVamsasSource()
1003 return this.ap == null ? null : this.ap.av;
1007 public void updateColours(SequenceI seq, int index)
1009 System.out.println("update the seqPanel colours");
1014 * Action on mouse movement is to update the status bar to show the current
1015 * sequence position, and (if features are shown) to show any features at the
1016 * position in a tooltip. Does nothing if the mouse move does not change
1022 public void mouseMoved(MouseEvent evt)
1026 // This is because MacOSX creates a mouseMoved
1027 // If control is down, other platforms will not.
1031 final MousePos mousePos = findMousePosition(evt);
1032 if (mousePos.equals(lastMousePosition))
1035 * just a pixel move without change of 'cell'
1037 moveTooltip = false;
1041 lastMousePosition = mousePos;
1043 if (mousePos.isOverAnnotation())
1045 mouseMovedOverAnnotation(mousePos);
1048 final int seq = mousePos.seqIndex;
1050 final int column = mousePos.column;
1051 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1053 lastMousePosition = null;
1054 setToolTipText(null);
1056 lastFormattedTooltip = null;
1057 ap.alignFrame.setStatus("");
1061 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1063 if (column >= sequence.getLength())
1069 * set status bar message, returning residue position in sequence
1071 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1072 final int pos = setStatusMessage(sequence, column, seq);
1073 if (ssm != null && !isGapped)
1075 mouseOverSequence(sequence, column, pos);
1078 StringBuilder tooltipText = new StringBuilder(64);
1080 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1083 for (int g = 0; g < groups.length; g++)
1085 if (groups[g].getStartRes() <= column
1086 && groups[g].getEndRes() >= column)
1088 if (!groups[g].getName().startsWith("JTreeGroup")
1089 && !groups[g].getName().startsWith("JGroup"))
1091 tooltipText.append(groups[g].getName());
1094 if (groups[g].getDescription() != null)
1096 tooltipText.append(": " + groups[g].getDescription());
1103 * add any features at the position to the tooltip; if over a gap, only
1104 * add features that straddle the gap (pos may be the residue before or
1107 int unshownFeatures = 0;
1108 if (av.isShowSequenceFeatures())
1110 List<SequenceFeature> features = ap.getFeatureRenderer()
1111 .findFeaturesAtColumn(sequence, column + 1);
1112 unshownFeatures = seqARep.appendFeatures(tooltipText, pos, features,
1113 this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
1116 * add features in CDS/protein complement at the corresponding
1117 * position if configured to do so
1119 if (av.isShowComplementFeatures())
1121 if (!Comparison.isGap(sequence.getCharAt(column)))
1123 AlignViewportI complement = ap.getAlignViewport()
1124 .getCodingComplement();
1125 AlignFrame af = Desktop.getAlignFrameFor(complement);
1126 FeatureRendererModel fr2 = af.getFeatureRenderer();
1127 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1131 unshownFeatures += seqARep.appendFeatures(tooltipText, pos, mf,
1132 fr2, MAX_TOOLTIP_LENGTH);
1137 if (tooltipText.length() == 0) // nothing added
1139 setToolTipText(null);
1144 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1146 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1147 tooltipText.append("...");
1149 if (unshownFeatures > 0)
1151 tooltipText.append("<br/>").append("... ").append("<i>")
1152 .append(MessageManager.formatMessage(
1153 "label.features_not_shown", unshownFeatures))
1156 String textString = tooltipText.toString();
1157 if (!textString.equals(lastTooltip))
1159 lastTooltip = textString;
1160 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true, textString);
1161 setToolTipText(lastFormattedTooltip);
1167 * When the view is in wrapped mode, and the mouse is over an annotation row,
1168 * shows the corresponding tooltip and status message (if any)
1173 protected void mouseMovedOverAnnotation(MousePos pos)
1175 final int column = pos.column;
1176 final int rowIndex = pos.annotationIndex;
1178 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1183 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1185 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1187 if (!tooltip.equals(lastTooltip))
1189 lastTooltip = tooltip;
1190 lastFormattedTooltip = tooltip == null ? null
1191 : JvSwingUtils.wrapTooltip(true, tooltip);
1192 setToolTipText(lastFormattedTooltip);
1195 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1197 ap.alignFrame.setStatus(msg);
1201 * if Shift key is held down while moving the mouse,
1202 * the tooltip location is not changed once shown
1204 private Point lastTooltipLocation = null;
1207 * this flag is false for pixel moves within a residue,
1208 * to reduce tooltip flicker
1210 private boolean moveTooltip = true;
1213 * a dummy tooltip used to estimate where to position tooltips
1215 private JToolTip tempTip = new JLabel().createToolTip();
1220 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1223 public Point getToolTipLocation(MouseEvent event)
1227 if (lastTooltip == null || !moveTooltip)
1232 if (lastTooltipLocation != null && event.isShiftDown())
1234 return lastTooltipLocation;
1237 int x = event.getX();
1238 int y = event.getY();
1241 tempTip.setTipText(lastFormattedTooltip);
1242 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1244 // was x += (w - x < 200) ? -(w / 2) : 5;
1245 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1246 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1248 return lastTooltipLocation = p;
1252 * set when the current UI interaction has resulted in a change that requires
1253 * shading in overviews and structures to be recalculated. this could be
1254 * changed to a something more expressive that indicates what actually has
1255 * changed, so selective redraws can be applied (ie. only structures, only
1258 private boolean updateOverviewAndStructs = false; // TODO: refactor to
1262 * set if av.getSelectionGroup() refers to a group that is defined on the
1263 * alignment view, rather than a transient selection
1265 // private boolean editingDefinedGroup = false; // TODO: refactor to
1266 // avcontroller or viewModel
1269 * Sets the status message in alignment panel, showing the sequence number
1270 * (index) and id, and residue and residue position if not at a gap, for the
1271 * given sequence and column position. Returns the residue position returned
1272 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1273 * if at a gapped position.
1276 * aligned sequence object
1280 * index of sequence in alignment
1281 * @return sequence position of residue at column, or adjacent residue if at a
1284 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1286 char sequenceChar = sequence.getCharAt(column);
1287 int pos = sequence.findPosition(column);
1288 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1294 * Builds the status message for the current cursor location and writes it to
1295 * the status bar, for example
1298 * Sequence 3 ID: FER1_SOLLC
1299 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1300 * Sequence 5 ID: FER1_PEA Residue: B (3)
1301 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1306 * sequence position in the alignment (1..)
1307 * @param sequenceChar
1308 * the character under the cursor
1310 * the sequence residue position (if not over a gap)
1312 protected void setStatusMessage(String seqName, int seqIndex,
1313 char sequenceChar, int residuePos)
1315 StringBuilder text = new StringBuilder(32);
1318 * Sequence number (if known), and sequence name.
1320 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1321 text.append("Sequence").append(seqno).append(" ID: ").append(seqName);
1323 String residue = null;
1326 * Try to translate the display character to residue name (null for gap).
1328 boolean isGapped = Comparison.isGap(sequenceChar);
1332 boolean nucleotide = av.getAlignment().isNucleotide();
1333 String displayChar = String.valueOf(sequenceChar);
1336 residue = ResidueProperties.nucleotideName.get(displayChar);
1340 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1341 : ("*".equals(displayChar) ? "STOP"
1342 : ResidueProperties.aa2Triplet.get(displayChar));
1344 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1345 .append(": ").append(residue == null ? displayChar : residue);
1347 text.append(" (").append(Integer.toString(residuePos)).append(")");
1349 ap.alignFrame.setStatus(text.toString());
1353 * Set the status bar message to highlight the first matched position in
1358 private void setStatusMessage(SearchResultsI results)
1360 AlignmentI al = this.av.getAlignment();
1361 int sequenceIndex = al.findIndex(results);
1362 if (sequenceIndex == -1)
1366 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1367 SequenceI ds = alignedSeq.getDatasetSequence();
1368 for (SearchResultMatchI m : results.getResults())
1370 SequenceI seq = m.getSequence();
1371 if (seq.getDatasetSequence() != null)
1373 seq = seq.getDatasetSequence();
1378 int start = m.getStart();
1379 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1380 seq.getCharAt(start - 1), start);
1390 public void mouseDragged(MouseEvent evt)
1392 MousePos pos = findMousePosition(evt);
1393 if (pos.isOverAnnotation() || pos.column == -1)
1398 if (mouseWheelPressed)
1400 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1401 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1403 int oldWidth = av.getCharWidth();
1405 // Which is bigger, left-right or up-down?
1406 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1407 .abs(evt.getX() - lastMousePress.getX()))
1410 * on drag up or down, decrement or increment font size
1412 int fontSize = av.font.getSize();
1413 boolean fontChanged = false;
1415 if (evt.getY() < lastMousePress.getY())
1420 else if (evt.getY() > lastMousePress.getY())
1433 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1435 av.setFont(newFont, true);
1436 av.setCharWidth(oldWidth);
1440 ap.av.getCodingComplement().setFont(newFont, true);
1441 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1442 .getSplitViewContainer();
1443 splitFrame.adjustLayout();
1444 splitFrame.repaint();
1451 * on drag left or right, decrement or increment character width
1454 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1456 newWidth = av.getCharWidth() - 1;
1457 av.setCharWidth(newWidth);
1459 else if (evt.getX() > lastMousePress.getX())
1461 newWidth = av.getCharWidth() + 1;
1462 av.setCharWidth(newWidth);
1466 ap.paintAlignment(false, false);
1470 * need to ensure newWidth is set on cdna, regardless of which
1471 * panel the mouse drag happened in; protein will compute its
1472 * character width as 1:1 or 3:1
1474 av.getCodingComplement().setCharWidth(newWidth);
1475 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1476 .getSplitViewContainer();
1477 splitFrame.adjustLayout();
1478 splitFrame.repaint();
1483 FontMetrics fm = getFontMetrics(av.getFont());
1484 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1486 lastMousePress = evt.getPoint();
1493 dragStretchGroup(evt);
1497 int res = pos.column;
1504 if ((editLastRes == -1) || (editLastRes == res))
1509 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1511 // dragLeft, delete gap
1512 editSequence(false, false, res);
1516 editSequence(true, false, res);
1519 mouseDragging = true;
1520 if (scrollThread != null)
1522 scrollThread.setMousePosition(evt.getPoint());
1527 * Edits the sequence to insert or delete one or more gaps, in response to a
1528 * mouse drag or cursor mode command. The number of inserts/deletes may be
1529 * specified with the cursor command, or else depends on the mouse event
1530 * (normally one column, but potentially more for a fast mouse drag).
1532 * Delete gaps is limited to the number of gaps left of the cursor position
1533 * (mouse drag), or at or right of the cursor position (cursor mode).
1535 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1536 * the current selection group.
1538 * In locked editing mode (with a selection group present), inserts/deletions
1539 * within the selection group are limited to its boundaries (and edits outside
1540 * the group stop at its border).
1543 * true to insert gaps, false to delete gaps
1545 * (unused parameter)
1547 * the column at which to perform the action; the number of columns
1548 * affected depends on <code>this.editLastRes</code> (cursor column
1551 synchronized void editSequence(boolean insertGap, boolean editSeq,
1555 int fixedRight = -1;
1556 boolean fixedColumns = false;
1557 SequenceGroup sg = av.getSelectionGroup();
1559 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1561 // No group, but the sequence may represent a group
1562 if (!groupEditing && av.hasHiddenRows())
1564 if (av.isHiddenRepSequence(seq))
1566 sg = av.getRepresentedSequences(seq);
1567 groupEditing = true;
1571 StringBuilder message = new StringBuilder(64); // for status bar
1574 * make a name for the edit action, for
1575 * status bar message and Undo/Redo menu
1577 String label = null;
1580 message.append("Edit group:");
1581 label = MessageManager.getString("action.edit_group");
1585 message.append("Edit sequence: " + seq.getName());
1586 label = seq.getName();
1587 if (label.length() > 10)
1589 label = label.substring(0, 10);
1591 label = MessageManager.formatMessage("label.edit_params",
1597 * initialise the edit command if there is not
1598 * already one being extended
1600 if (editCommand == null)
1602 editCommand = new EditCommand(label);
1607 message.append(" insert ");
1611 message.append(" delete ");
1614 message.append(Math.abs(startres - editLastRes) + " gaps.");
1615 ap.alignFrame.setStatus(message.toString());
1618 * is there a selection group containing the sequence being edited?
1619 * if so the boundary of the group is the limit of the edit
1620 * (but the edit may be inside or outside the selection group)
1622 boolean inSelectionGroup = sg != null
1623 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1624 if (groupEditing || inSelectionGroup)
1626 fixedColumns = true;
1628 // sg might be null as the user may only see 1 sequence,
1629 // but the sequence represents a group
1632 if (!av.isHiddenRepSequence(seq))
1637 sg = av.getRepresentedSequences(seq);
1640 fixedLeft = sg.getStartRes();
1641 fixedRight = sg.getEndRes();
1643 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1644 || (startres >= fixedLeft && editLastRes < fixedLeft)
1645 || (startres > fixedRight && editLastRes <= fixedRight)
1646 || (startres <= fixedRight && editLastRes > fixedRight))
1652 if (fixedLeft > startres)
1654 fixedRight = fixedLeft - 1;
1657 else if (fixedRight < startres)
1659 fixedLeft = fixedRight;
1664 if (av.hasHiddenColumns())
1666 fixedColumns = true;
1667 int y1 = av.getAlignment().getHiddenColumns()
1668 .getNextHiddenBoundary(true, startres);
1669 int y2 = av.getAlignment().getHiddenColumns()
1670 .getNextHiddenBoundary(false, startres);
1672 if ((insertGap && startres > y1 && editLastRes < y1)
1673 || (!insertGap && startres < y2 && editLastRes > y2))
1679 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1680 // Selection spans a hidden region
1681 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1689 fixedRight = y2 - 1;
1694 boolean success = doEditSequence(insertGap, editSeq, startres,
1695 fixedRight, fixedColumns, sg);
1698 * report what actually happened (might be less than
1699 * what was requested), by inspecting the edit commands added
1701 String msg = getEditStatusMessage(editCommand);
1702 ap.alignFrame.setStatus(msg == null ? " " : msg);
1708 editLastRes = startres;
1709 seqCanvas.repaint();
1713 * A helper method that performs the requested editing to insert or delete
1714 * gaps (if possible). Answers true if the edit was successful, false if could
1715 * only be performed in part or not at all. Failure may occur in 'locked edit'
1716 * mode, when an insertion requires a matching gapped position (or column) to
1717 * delete, and deletion requires an adjacent gapped position (or column) to
1721 * true if inserting gap(s), false if deleting
1723 * (unused parameter, currently always false)
1725 * the column at which to perform the edit
1727 * fixed right boundary column of a locked edit (within or to the
1728 * left of a selection group)
1729 * @param fixedColumns
1730 * true if this is a locked edit
1732 * the sequence group (if group edit is being performed)
1735 protected boolean doEditSequence(final boolean insertGap,
1736 final boolean editSeq, final int startres, int fixedRight,
1737 final boolean fixedColumns, final SequenceGroup sg)
1739 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1740 SequenceI[] seqs = new SequenceI[] { seq };
1744 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1745 int g, groupSize = vseqs.size();
1746 SequenceI[] groupSeqs = new SequenceI[groupSize];
1747 for (g = 0; g < groupSeqs.length; g++)
1749 groupSeqs[g] = vseqs.get(g);
1755 // If the user has selected the whole sequence, and is dragging to
1756 // the right, we can still extend the alignment and selectionGroup
1757 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1758 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1761 av.getAlignment().getWidth() + startres - editLastRes);
1762 fixedRight = sg.getEndRes();
1765 // Is it valid with fixed columns??
1766 // Find the next gap before the end
1767 // of the visible region boundary
1768 boolean blank = false;
1769 for (; fixedRight > editLastRes; fixedRight--)
1773 for (g = 0; g < groupSize; g++)
1775 for (int j = 0; j < startres - editLastRes; j++)
1777 if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1792 if (sg.getSize() == av.getAlignment().getHeight())
1794 if ((av.hasHiddenColumns()
1795 && startres < av.getAlignment().getHiddenColumns()
1796 .getNextHiddenBoundary(false, startres)))
1801 int alWidth = av.getAlignment().getWidth();
1802 if (av.hasHiddenRows())
1804 int hwidth = av.getAlignment().getHiddenSequences()
1806 if (hwidth > alWidth)
1811 // We can still insert gaps if the selectionGroup
1812 // contains all the sequences
1813 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1814 fixedRight = alWidth + startres - editLastRes;
1824 else if (!insertGap)
1826 // / Are we able to delete?
1827 // ie are all columns blank?
1829 for (g = 0; g < groupSize; g++)
1831 for (int j = startres; j < editLastRes; j++)
1833 if (groupSeqs[g].getLength() <= j)
1838 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1840 // Not a gap, block edit not valid
1849 // dragging to the right
1850 if (fixedColumns && fixedRight != -1)
1852 for (int j = editLastRes; j < startres; j++)
1854 insertGap(j, groupSeqs, fixedRight);
1859 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1860 startres - editLastRes, false);
1865 // dragging to the left
1866 if (fixedColumns && fixedRight != -1)
1868 for (int j = editLastRes; j > startres; j--)
1870 deleteChar(startres, groupSeqs, fixedRight);
1875 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1876 editLastRes - startres, false);
1883 * editing a single sequence
1887 // dragging to the right
1888 if (fixedColumns && fixedRight != -1)
1890 for (int j = editLastRes; j < startres; j++)
1892 if (!insertGap(j, seqs, fixedRight))
1895 * e.g. cursor mode command specified
1896 * more inserts than are possible
1904 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1905 startres - editLastRes, false);
1912 // dragging to the left
1913 if (fixedColumns && fixedRight != -1)
1915 for (int j = editLastRes; j > startres; j--)
1917 if (!Comparison.isGap(seq.getCharAt(startres)))
1921 deleteChar(startres, seqs, fixedRight);
1926 // could be a keyboard edit trying to delete none gaps
1928 for (int m = startres; m < editLastRes; m++)
1930 if (!Comparison.isGap(seq.getCharAt(m)))
1938 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1943 {// insertGap==false AND editSeq==TRUE;
1944 if (fixedColumns && fixedRight != -1)
1946 for (int j = editLastRes; j < startres; j++)
1948 insertGap(j, seqs, fixedRight);
1953 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1954 startres - editLastRes, false);
1964 * Constructs an informative status bar message while dragging to insert or
1965 * delete gaps. Answers null if inserts and deletes cancel out.
1967 * @param editCommand
1968 * a command containing the list of individual edits
1971 protected static String getEditStatusMessage(EditCommand editCommand)
1973 if (editCommand == null)
1979 * add any inserts, and subtract any deletes,
1980 * not counting those auto-inserted when doing a 'locked edit'
1981 * (so only counting edits 'under the cursor')
1984 for (Edit cmd : editCommand.getEdits())
1986 if (!cmd.isSystemGenerated())
1988 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1996 * inserts and deletes cancel out
2001 String msgKey = count > 1 ? "label.insert_gaps"
2002 : (count == 1 ? "label.insert_gap"
2003 : (count == -1 ? "label.delete_gap"
2004 : "label.delete_gaps"));
2005 count = Math.abs(count);
2007 return MessageManager.formatMessage(msgKey, String.valueOf(count));
2011 * Inserts one gap at column j, deleting the right-most gapped column up to
2012 * (and including) fixedColumn. Returns true if the edit is successful, false
2013 * if no blank column is available to allow the insertion to be balanced by a
2018 * @param fixedColumn
2021 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
2023 int blankColumn = fixedColumn;
2024 for (int s = 0; s < seq.length; s++)
2026 // Find the next gap before the end of the visible region boundary
2027 // If lastCol > j, theres a boundary after the gap insertion
2029 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
2031 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
2033 // Theres a space, so break and insert the gap
2038 if (blankColumn <= j)
2040 blankColumn = fixedColumn;
2046 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2048 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2054 * Helper method to add and perform one edit action
2060 * @param systemGenerated
2061 * true if the edit is a 'balancing' delete (or insert) to match a
2062 * user's insert (or delete) in a locked editing region
2064 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2065 int count, boolean systemGenerated)
2068 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2069 av.getAlignment().getGapCharacter());
2070 edit.setSystemGenerated(systemGenerated);
2072 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2076 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2077 * each of the given sequences. The caller should ensure that all sequences
2078 * are gapped in column j.
2082 * @param fixedColumn
2084 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2086 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2088 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2092 * On reentering the panel, stops any scrolling that was started on dragging
2098 public void mouseEntered(MouseEvent e)
2108 * On leaving the panel, if the mouse is being dragged, starts a thread to
2109 * scroll it until the mouse is released (in unwrapped mode only)
2114 public void mouseExited(MouseEvent e)
2116 lastMousePosition = null;
2117 ap.alignFrame.setStatus(" ");
2118 if (av.getWrapAlignment())
2123 if (mouseDragging && scrollThread == null)
2125 startScrolling(e.getPoint());
2130 * Handler for double-click on a position with one or more sequence features.
2131 * Opens the Amend Features dialog to allow feature details to be amended, or
2132 * the feature deleted.
2135 public void mouseClicked(MouseEvent evt)
2137 SequenceGroup sg = null;
2138 MousePos pos = findMousePosition(evt);
2139 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2144 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2146 sg = av.getSelectionGroup();
2147 if (sg != null && sg.getSize() == 1
2148 && sg.getEndRes() - sg.getStartRes() < 2)
2150 av.setSelectionGroup(null);
2153 int column = pos.column;
2156 * find features at the position (if not gapped), or straddling
2157 * the position (if at a gap)
2159 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2160 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2161 .findFeaturesAtColumn(sequence, column + 1);
2163 if (!features.isEmpty())
2166 * highlight the first feature at the position on the alignment
2168 SearchResultsI highlight = new SearchResults();
2169 highlight.addResult(sequence, features.get(0).getBegin(),
2170 features.get(0).getEnd());
2171 seqCanvas.highlightSearchResults(highlight, true);
2174 * open the Amend Features dialog
2176 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2177 false).showDialog();
2183 public void mouseWheelMoved(MouseWheelEvent e)
2186 double wheelRotation = e.getPreciseWheelRotation();
2187 if (wheelRotation > 0)
2189 if (e.isShiftDown())
2191 av.getRanges().scrollRight(true);
2196 av.getRanges().scrollUp(false);
2199 else if (wheelRotation < 0)
2201 if (e.isShiftDown())
2203 av.getRanges().scrollRight(false);
2207 av.getRanges().scrollUp(true);
2212 * update status bar and tooltip for new position
2213 * (need to synthesize a mouse movement to refresh tooltip)
2216 ToolTipManager.sharedInstance().mouseMoved(e);
2225 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2227 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2232 final int res = pos.column;
2233 final int seq = pos.seqIndex;
2235 updateOverviewAndStructs = false;
2237 startWrapBlock = wrappedBlock;
2239 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2241 if ((sequence == null) || (res > sequence.getLength()))
2246 stretchGroup = av.getSelectionGroup();
2248 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2250 stretchGroup = av.getAlignment().findGroup(sequence, res);
2251 if (stretchGroup != null)
2253 // only update the current selection if the popup menu has a group to
2255 av.setSelectionGroup(stretchGroup);
2260 * defer right-mouse click handling to mouseReleased on Windows
2261 * (where isPopupTrigger() will answer true)
2262 * NB isRightMouseButton is also true for Cmd-click on Mac
2264 if (Platform.isWinRightButton(evt))
2269 if (evt.isPopupTrigger()) // Mac: mousePressed
2271 showPopupMenu(evt, pos);
2277 seqCanvas.cursorX = res;
2278 seqCanvas.cursorY = seq;
2279 seqCanvas.repaint();
2283 if (stretchGroup == null)
2285 createStretchGroup(res, sequence);
2288 if (stretchGroup != null)
2290 stretchGroup.addPropertyChangeListener(seqCanvas);
2293 seqCanvas.repaint();
2296 private void createStretchGroup(int res, SequenceI sequence)
2298 // Only if left mouse button do we want to change group sizes
2299 // define a new group here
2300 SequenceGroup sg = new SequenceGroup();
2301 sg.setStartRes(res);
2303 sg.addSequence(sequence, false);
2304 av.setSelectionGroup(sg);
2307 if (av.getConservationSelected())
2309 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2313 if (av.getAbovePIDThreshold())
2315 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2318 // TODO: stretchGroup will always be not null. Is this a merge error ?
2319 // or is there a threading issue here?
2320 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2322 // Edit end res position of selected group
2323 changeEndRes = true;
2325 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2327 // Edit end res position of selected group
2328 changeStartRes = true;
2330 stretchGroup.getWidth();
2335 * Build and show a pop-up menu at the right-click mouse position
2340 void showPopupMenu(MouseEvent evt, MousePos pos)
2342 final int column = pos.column;
2343 final int seq = pos.seqIndex;
2344 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2345 if (sequence != null)
2347 PopupMenu pop = new PopupMenu(ap, sequence, column);
2348 pop.show(this, evt.getX(), evt.getY());
2353 * Update the display after mouse up on a selection or group
2356 * mouse released event details
2358 * true if this event is happening after a mouse drag (rather than a
2361 protected void doMouseReleasedDefineMode(MouseEvent evt,
2364 if (stretchGroup == null)
2369 stretchGroup.removePropertyChangeListener(seqCanvas);
2371 // always do this - annotation has own state
2372 // but defer colourscheme update until hidden sequences are passed in
2373 boolean vischange = stretchGroup.recalcConservation(true);
2374 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2376 if (stretchGroup.cs != null)
2380 stretchGroup.cs.alignmentChanged(stretchGroup,
2381 av.getHiddenRepSequences());
2384 ResidueShaderI groupColourScheme = stretchGroup
2385 .getGroupColourScheme();
2386 String name = stretchGroup.getName();
2387 if (stretchGroup.cs.conservationApplied())
2389 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2391 if (stretchGroup.cs.getThreshold() > 0)
2393 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2396 PaintRefresher.Refresh(this, av.getSequenceSetId());
2397 // TODO: structure colours only need updating if stretchGroup used to or now
2398 // does contain sequences with structure views
2399 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2400 updateOverviewAndStructs = false;
2401 changeEndRes = false;
2402 changeStartRes = false;
2403 stretchGroup = null;
2408 * Resizes the borders of a selection group depending on the direction of
2413 protected void dragStretchGroup(MouseEvent evt)
2415 if (stretchGroup == null)
2420 MousePos pos = findMousePosition(evt);
2421 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2426 int res = pos.column;
2427 int y = pos.seqIndex;
2429 if (wrappedBlock != startWrapBlock)
2434 res = Math.min(res, av.getAlignment().getWidth() - 1);
2436 if (stretchGroup.getEndRes() == res)
2438 // Edit end res position of selected group
2439 changeEndRes = true;
2441 else if (stretchGroup.getStartRes() == res)
2443 // Edit start res position of selected group
2444 changeStartRes = true;
2447 if (res < av.getRanges().getStartRes())
2449 res = av.getRanges().getStartRes();
2454 if (res > (stretchGroup.getStartRes() - 1))
2456 stretchGroup.setEndRes(res);
2457 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2460 else if (changeStartRes)
2462 if (res < (stretchGroup.getEndRes() + 1))
2464 stretchGroup.setStartRes(res);
2465 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2469 int dragDirection = 0;
2475 else if (y < oldSeq)
2480 while ((y != oldSeq) && (oldSeq > -1)
2481 && (y < av.getAlignment().getHeight()))
2483 // This routine ensures we don't skip any sequences, as the
2484 // selection is quite slow.
2485 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2487 oldSeq += dragDirection;
2494 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2496 if (stretchGroup.getSequences(null).contains(nextSeq))
2498 stretchGroup.deleteSequence(seq, false);
2499 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2505 stretchGroup.addSequence(seq, false);
2508 stretchGroup.addSequence(nextSeq, false);
2509 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2518 mouseDragging = true;
2520 if (scrollThread != null)
2522 scrollThread.setMousePosition(evt.getPoint());
2526 * construct a status message showing the range of the selection
2528 StringBuilder status = new StringBuilder(64);
2529 List<SequenceI> seqs = stretchGroup.getSequences();
2530 String name = seqs.get(0).getName();
2531 if (name.length() > 20)
2533 name = name.substring(0, 20);
2535 status.append(name).append(" - ");
2536 name = seqs.get(seqs.size() - 1).getName();
2537 if (name.length() > 20)
2539 name = name.substring(0, 20);
2541 status.append(name).append(" ");
2542 int startRes = stretchGroup.getStartRes();
2543 status.append(" cols ").append(String.valueOf(startRes + 1))
2545 int endRes = stretchGroup.getEndRes();
2546 status.append(String.valueOf(endRes + 1));
2547 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2548 .append(String.valueOf(endRes - startRes + 1)).append(")");
2549 ap.alignFrame.setStatus(status.toString());
2553 * Stops the scroll thread if it is running
2555 void stopScrolling()
2557 if (scrollThread != null)
2559 scrollThread.stopScrolling();
2560 scrollThread = null;
2562 mouseDragging = false;
2566 * Starts a thread to scroll the alignment, towards a given mouse position
2567 * outside the panel bounds, unless the alignment is in wrapped mode
2571 void startScrolling(Point mousePos)
2574 * set this.mouseDragging in case this was called from
2575 * a drag in ScalePanel or AnnotationPanel
2577 mouseDragging = true;
2578 if (!av.getWrapAlignment() && scrollThread == null)
2580 scrollThread = new ScrollThread();
2581 scrollThread.setMousePosition(mousePos);
2582 if (Platform.isJS())
2585 * Javascript - run every 20ms until scrolling stopped
2586 * or reaches the limit of scrollable alignment
2588 Timer t = new Timer(20, new ActionListener()
2591 public void actionPerformed(ActionEvent e)
2593 if (scrollThread != null)
2595 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2596 scrollThread.scrollOnce();
2600 t.addActionListener(new ActionListener()
2603 public void actionPerformed(ActionEvent e)
2605 if (scrollThread == null)
2607 // SeqPanel.stopScrolling called
2617 * Java - run in a new thread
2619 scrollThread.start();
2625 * Performs scrolling of the visible alignment left, right, up or down, until
2626 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2627 * limit of the alignment is reached
2629 class ScrollThread extends Thread
2631 private Point mousePos;
2633 private volatile boolean keepRunning = true;
2638 public ScrollThread()
2640 setName("SeqPanel$ScrollThread");
2644 * Sets the position of the mouse that determines the direction of the
2645 * scroll to perform. If this is called as the mouse moves, scrolling should
2646 * respond accordingly. For example, if the mouse is dragged right, scroll
2647 * right should start; if the drag continues down, scroll down should also
2652 public void setMousePosition(Point p)
2658 * Sets a flag that will cause the thread to exit
2660 public void stopScrolling()
2662 keepRunning = false;
2666 * Scrolls the alignment left or right, and/or up or down, depending on the
2667 * last notified mouse position, until the limit of the alignment is
2668 * reached, or a flag is set to stop the scroll
2675 if (mousePos != null)
2677 keepRunning = scrollOnce();
2682 } catch (Exception ex)
2686 SeqPanel.this.scrollThread = null;
2692 * <li>one row up, if the mouse is above the panel</li>
2693 * <li>one row down, if the mouse is below the panel</li>
2694 * <li>one column left, if the mouse is left of the panel</li>
2695 * <li>one column right, if the mouse is right of the panel</li>
2697 * Answers true if a scroll was performed, false if not - meaning either
2698 * that the mouse position is within the panel, or the edge of the alignment
2701 boolean scrollOnce()
2704 * quit after mouseUp ensures interrupt in JalviewJS
2711 boolean scrolled = false;
2712 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2719 // mouse is above this panel - try scroll up
2720 scrolled = ranges.scrollUp(true);
2722 else if (mousePos.y >= getHeight())
2724 // mouse is below this panel - try scroll down
2725 scrolled = ranges.scrollUp(false);
2729 * scroll left or right
2733 scrolled |= ranges.scrollRight(false);
2735 else if (mousePos.x >= getWidth())
2737 scrolled |= ranges.scrollRight(true);
2744 * modify current selection according to a received message.
2747 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2748 HiddenColumns hidden, SelectionSource source)
2750 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2751 // handles selection messages...
2752 // TODO: extend config options to allow user to control if selections may be
2753 // shared between viewports.
2754 boolean iSentTheSelection = (av == source
2755 || (source instanceof AlignViewport
2756 && ((AlignmentViewport) source).getSequenceSetId()
2757 .equals(av.getSequenceSetId())));
2759 if (iSentTheSelection)
2761 // respond to our own event by updating dependent dialogs
2762 if (ap.getCalculationDialog() != null)
2764 ap.getCalculationDialog().validateCalcTypes();
2770 // process further ?
2771 if (!av.followSelection)
2777 * Ignore the selection if there is one of our own pending.
2779 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2785 * Check for selection in a view of which this one is a dna/protein
2788 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2793 // do we want to thread this ? (contention with seqsel and colsel locks, I
2796 * only copy colsel if there is a real intersection between
2797 * sequence selection and this panel's alignment
2799 boolean repaint = false;
2800 boolean copycolsel = false;
2802 SequenceGroup sgroup = null;
2803 if (seqsel != null && seqsel.getSize() > 0)
2805 if (av.getAlignment() == null)
2807 Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2808 + " ViewId=" + av.getViewId()
2809 + " 's alignment is NULL! returning immediately.");
2812 sgroup = seqsel.intersect(av.getAlignment(),
2813 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2814 if ((sgroup != null && sgroup.getSize() > 0))
2819 if (sgroup != null && sgroup.getSize() > 0)
2821 av.setSelectionGroup(sgroup);
2825 av.setSelectionGroup(null);
2827 av.isSelectionGroupChanged(true);
2832 // the current selection is unset or from a previous message
2833 // so import the new colsel.
2834 if (colsel == null || colsel.isEmpty())
2836 if (av.getColumnSelection() != null)
2838 av.getColumnSelection().clear();
2844 // TODO: shift colSel according to the intersecting sequences
2845 if (av.getColumnSelection() == null)
2847 av.setColumnSelection(new ColumnSelection(colsel));
2851 av.getColumnSelection().setElementsFrom(colsel,
2852 av.getAlignment().getHiddenColumns());
2855 av.isColSelChanged(true);
2859 if (copycolsel && av.hasHiddenColumns()
2860 && (av.getAlignment().getHiddenColumns() == null))
2862 System.err.println("Bad things");
2864 if (repaint) // always true!
2866 // probably finessing with multiple redraws here
2867 PaintRefresher.Refresh(this, av.getSequenceSetId());
2868 // ap.paintAlignment(false);
2871 // lastly, update dependent dialogs
2872 if (ap.getCalculationDialog() != null)
2874 ap.getCalculationDialog().validateCalcTypes();
2880 * If this panel is a cdna/protein translation view of the selection source,
2881 * tries to map the source selection to a local one, and returns true. Else
2888 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2889 ColumnSelection colsel, HiddenColumns hidden,
2890 SelectionSource source)
2892 if (!(source instanceof AlignViewportI))
2896 final AlignViewportI sourceAv = (AlignViewportI) source;
2897 if (sourceAv.getCodingComplement() != av
2898 && av.getCodingComplement() != sourceAv)
2904 * Map sequence selection
2906 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2907 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2908 av.isSelectionGroupChanged(true);
2911 * Map column selection
2913 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2915 ColumnSelection cs = new ColumnSelection();
2916 HiddenColumns hs = new HiddenColumns();
2917 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2918 av.setColumnSelection(cs);
2919 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2921 // lastly, update any dependent dialogs
2922 if (ap.getCalculationDialog() != null)
2924 ap.getCalculationDialog().validateCalcTypes();
2928 * repaint alignment, and also Overview or Structure
2929 * if hidden column selection has changed
2931 ap.paintAlignment(hiddenChanged, hiddenChanged);
2938 * @return null or last search results handled by this panel
2940 public SearchResultsI getLastSearchResults()
2942 return lastSearchResults;