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 // TODO - get yOffset for annotation, too
1179 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1184 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1186 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1188 if (tooltip == null ? tooltip != lastTooltip
1189 : !tooltip.equals(lastTooltip))
1191 lastTooltip = tooltip;
1192 lastFormattedTooltip = tooltip == null ? null
1193 : JvSwingUtils.wrapTooltip(true, tooltip);
1194 setToolTipText(lastFormattedTooltip);
1197 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1198 anns[rowIndex], 0, av);
1199 ap.alignFrame.setStatus(msg);
1203 * if Shift key is held down while moving the mouse,
1204 * the tooltip location is not changed once shown
1206 private Point lastTooltipLocation = null;
1209 * this flag is false for pixel moves within a residue,
1210 * to reduce tooltip flicker
1212 private boolean moveTooltip = true;
1215 * a dummy tooltip used to estimate where to position tooltips
1217 private JToolTip tempTip = new JLabel().createToolTip();
1222 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1225 public Point getToolTipLocation(MouseEvent event)
1229 if (lastTooltip == null || !moveTooltip)
1234 if (lastTooltipLocation != null && event.isShiftDown())
1236 return lastTooltipLocation;
1239 int x = event.getX();
1240 int y = event.getY();
1243 tempTip.setTipText(lastFormattedTooltip);
1244 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1246 // was x += (w - x < 200) ? -(w / 2) : 5;
1247 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1248 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1250 return lastTooltipLocation = p;
1254 * set when the current UI interaction has resulted in a change that requires
1255 * shading in overviews and structures to be recalculated. this could be
1256 * changed to a something more expressive that indicates what actually has
1257 * changed, so selective redraws can be applied (ie. only structures, only
1260 private boolean updateOverviewAndStructs = false; // TODO: refactor to
1264 * set if av.getSelectionGroup() refers to a group that is defined on the
1265 * alignment view, rather than a transient selection
1267 // private boolean editingDefinedGroup = false; // TODO: refactor to
1268 // avcontroller or viewModel
1271 * Sets the status message in alignment panel, showing the sequence number
1272 * (index) and id, and residue and residue position if not at a gap, for the
1273 * given sequence and column position. Returns the residue position returned
1274 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1275 * if at a gapped position.
1278 * aligned sequence object
1282 * index of sequence in alignment
1283 * @return sequence position of residue at column, or adjacent residue if at a
1286 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1288 char sequenceChar = sequence.getCharAt(column);
1289 int pos = sequence.findPosition(column);
1290 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1296 * Builds the status message for the current cursor location and writes it to
1297 * the status bar, for example
1300 * Sequence 3 ID: FER1_SOLLC
1301 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1302 * Sequence 5 ID: FER1_PEA Residue: B (3)
1303 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1308 * sequence position in the alignment (1..)
1309 * @param sequenceChar
1310 * the character under the cursor
1312 * the sequence residue position (if not over a gap)
1314 protected void setStatusMessage(String seqName, int seqIndex,
1315 char sequenceChar, int residuePos)
1317 StringBuilder text = new StringBuilder(32);
1320 * Sequence number (if known), and sequence name.
1322 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1323 text.append("Sequence").append(seqno).append(" ID: ").append(seqName);
1325 String residue = null;
1328 * Try to translate the display character to residue name (null for gap).
1330 boolean isGapped = Comparison.isGap(sequenceChar);
1334 boolean nucleotide = av.getAlignment().isNucleotide();
1335 String displayChar = String.valueOf(sequenceChar);
1338 residue = ResidueProperties.nucleotideName.get(displayChar);
1342 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1343 : ("*".equals(displayChar) ? "STOP"
1344 : ResidueProperties.aa2Triplet.get(displayChar));
1346 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1347 .append(": ").append(residue == null ? displayChar : residue);
1349 text.append(" (").append(Integer.toString(residuePos)).append(")");
1351 ap.alignFrame.setStatus(text.toString());
1355 * Set the status bar message to highlight the first matched position in
1360 private void setStatusMessage(SearchResultsI results)
1362 AlignmentI al = this.av.getAlignment();
1363 int sequenceIndex = al.findIndex(results);
1364 if (sequenceIndex == -1)
1368 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1369 SequenceI ds = alignedSeq.getDatasetSequence();
1370 for (SearchResultMatchI m : results.getResults())
1372 SequenceI seq = m.getSequence();
1373 if (seq.getDatasetSequence() != null)
1375 seq = seq.getDatasetSequence();
1380 int start = m.getStart();
1381 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1382 seq.getCharAt(start - 1), start);
1392 public void mouseDragged(MouseEvent evt)
1394 MousePos pos = findMousePosition(evt);
1395 if (pos.isOverAnnotation() || pos.column == -1)
1400 if (mouseWheelPressed)
1402 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1403 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1405 int oldWidth = av.getCharWidth();
1407 // Which is bigger, left-right or up-down?
1408 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1409 .abs(evt.getX() - lastMousePress.getX()))
1412 * on drag up or down, decrement or increment font size
1414 int fontSize = av.font.getSize();
1415 boolean fontChanged = false;
1417 if (evt.getY() < lastMousePress.getY())
1422 else if (evt.getY() > lastMousePress.getY())
1435 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1437 av.setFont(newFont, true);
1438 av.setCharWidth(oldWidth);
1442 ap.av.getCodingComplement().setFont(newFont, true);
1443 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1444 .getSplitViewContainer();
1445 splitFrame.adjustLayout();
1446 splitFrame.repaint();
1453 * on drag left or right, decrement or increment character width
1456 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1458 newWidth = av.getCharWidth() - 1;
1459 av.setCharWidth(newWidth);
1461 else if (evt.getX() > lastMousePress.getX())
1463 newWidth = av.getCharWidth() + 1;
1464 av.setCharWidth(newWidth);
1468 ap.paintAlignment(false, false);
1472 * need to ensure newWidth is set on cdna, regardless of which
1473 * panel the mouse drag happened in; protein will compute its
1474 * character width as 1:1 or 3:1
1476 av.getCodingComplement().setCharWidth(newWidth);
1477 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1478 .getSplitViewContainer();
1479 splitFrame.adjustLayout();
1480 splitFrame.repaint();
1485 FontMetrics fm = getFontMetrics(av.getFont());
1486 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1488 lastMousePress = evt.getPoint();
1495 dragStretchGroup(evt);
1499 int res = pos.column;
1506 if ((editLastRes == -1) || (editLastRes == res))
1511 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1513 // dragLeft, delete gap
1514 editSequence(false, false, res);
1518 editSequence(true, false, res);
1521 mouseDragging = true;
1522 if (scrollThread != null)
1524 scrollThread.setMousePosition(evt.getPoint());
1529 * Edits the sequence to insert or delete one or more gaps, in response to a
1530 * mouse drag or cursor mode command. The number of inserts/deletes may be
1531 * specified with the cursor command, or else depends on the mouse event
1532 * (normally one column, but potentially more for a fast mouse drag).
1534 * Delete gaps is limited to the number of gaps left of the cursor position
1535 * (mouse drag), or at or right of the cursor position (cursor mode).
1537 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1538 * the current selection group.
1540 * In locked editing mode (with a selection group present), inserts/deletions
1541 * within the selection group are limited to its boundaries (and edits outside
1542 * the group stop at its border).
1545 * true to insert gaps, false to delete gaps
1547 * (unused parameter)
1549 * the column at which to perform the action; the number of columns
1550 * affected depends on <code>this.editLastRes</code> (cursor column
1553 synchronized void editSequence(boolean insertGap, boolean editSeq,
1557 int fixedRight = -1;
1558 boolean fixedColumns = false;
1559 SequenceGroup sg = av.getSelectionGroup();
1561 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1563 // No group, but the sequence may represent a group
1564 if (!groupEditing && av.hasHiddenRows())
1566 if (av.isHiddenRepSequence(seq))
1568 sg = av.getRepresentedSequences(seq);
1569 groupEditing = true;
1573 StringBuilder message = new StringBuilder(64); // for status bar
1576 * make a name for the edit action, for
1577 * status bar message and Undo/Redo menu
1579 String label = null;
1582 message.append("Edit group:");
1583 label = MessageManager.getString("action.edit_group");
1587 message.append("Edit sequence: " + seq.getName());
1588 label = seq.getName();
1589 if (label.length() > 10)
1591 label = label.substring(0, 10);
1593 label = MessageManager.formatMessage("label.edit_params",
1599 * initialise the edit command if there is not
1600 * already one being extended
1602 if (editCommand == null)
1604 editCommand = new EditCommand(label);
1609 message.append(" insert ");
1613 message.append(" delete ");
1616 message.append(Math.abs(startres - editLastRes) + " gaps.");
1617 ap.alignFrame.setStatus(message.toString());
1620 * is there a selection group containing the sequence being edited?
1621 * if so the boundary of the group is the limit of the edit
1622 * (but the edit may be inside or outside the selection group)
1624 boolean inSelectionGroup = sg != null
1625 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1626 if (groupEditing || inSelectionGroup)
1628 fixedColumns = true;
1630 // sg might be null as the user may only see 1 sequence,
1631 // but the sequence represents a group
1634 if (!av.isHiddenRepSequence(seq))
1639 sg = av.getRepresentedSequences(seq);
1642 fixedLeft = sg.getStartRes();
1643 fixedRight = sg.getEndRes();
1645 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1646 || (startres >= fixedLeft && editLastRes < fixedLeft)
1647 || (startres > fixedRight && editLastRes <= fixedRight)
1648 || (startres <= fixedRight && editLastRes > fixedRight))
1654 if (fixedLeft > startres)
1656 fixedRight = fixedLeft - 1;
1659 else if (fixedRight < startres)
1661 fixedLeft = fixedRight;
1666 if (av.hasHiddenColumns())
1668 fixedColumns = true;
1669 int y1 = av.getAlignment().getHiddenColumns()
1670 .getNextHiddenBoundary(true, startres);
1671 int y2 = av.getAlignment().getHiddenColumns()
1672 .getNextHiddenBoundary(false, startres);
1674 if ((insertGap && startres > y1 && editLastRes < y1)
1675 || (!insertGap && startres < y2 && editLastRes > y2))
1681 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1682 // Selection spans a hidden region
1683 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1691 fixedRight = y2 - 1;
1696 boolean success = doEditSequence(insertGap, editSeq, startres,
1697 fixedRight, fixedColumns, sg);
1700 * report what actually happened (might be less than
1701 * what was requested), by inspecting the edit commands added
1703 String msg = getEditStatusMessage(editCommand);
1704 ap.alignFrame.setStatus(msg == null ? " " : msg);
1710 editLastRes = startres;
1711 seqCanvas.repaint();
1715 * A helper method that performs the requested editing to insert or delete
1716 * gaps (if possible). Answers true if the edit was successful, false if could
1717 * only be performed in part or not at all. Failure may occur in 'locked edit'
1718 * mode, when an insertion requires a matching gapped position (or column) to
1719 * delete, and deletion requires an adjacent gapped position (or column) to
1723 * true if inserting gap(s), false if deleting
1725 * (unused parameter, currently always false)
1727 * the column at which to perform the edit
1729 * fixed right boundary column of a locked edit (within or to the
1730 * left of a selection group)
1731 * @param fixedColumns
1732 * true if this is a locked edit
1734 * the sequence group (if group edit is being performed)
1737 protected boolean doEditSequence(final boolean insertGap,
1738 final boolean editSeq, final int startres, int fixedRight,
1739 final boolean fixedColumns, final SequenceGroup sg)
1741 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1742 SequenceI[] seqs = new SequenceI[] { seq };
1746 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1747 int g, groupSize = vseqs.size();
1748 SequenceI[] groupSeqs = new SequenceI[groupSize];
1749 for (g = 0; g < groupSeqs.length; g++)
1751 groupSeqs[g] = vseqs.get(g);
1757 // If the user has selected the whole sequence, and is dragging to
1758 // the right, we can still extend the alignment and selectionGroup
1759 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1760 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1763 av.getAlignment().getWidth() + startres - editLastRes);
1764 fixedRight = sg.getEndRes();
1767 // Is it valid with fixed columns??
1768 // Find the next gap before the end
1769 // of the visible region boundary
1770 boolean blank = false;
1771 for (; fixedRight > editLastRes; fixedRight--)
1775 for (g = 0; g < groupSize; g++)
1777 for (int j = 0; j < startres - editLastRes; j++)
1779 if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1794 if (sg.getSize() == av.getAlignment().getHeight())
1796 if ((av.hasHiddenColumns()
1797 && startres < av.getAlignment().getHiddenColumns()
1798 .getNextHiddenBoundary(false, startres)))
1803 int alWidth = av.getAlignment().getWidth();
1804 if (av.hasHiddenRows())
1806 int hwidth = av.getAlignment().getHiddenSequences()
1808 if (hwidth > alWidth)
1813 // We can still insert gaps if the selectionGroup
1814 // contains all the sequences
1815 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1816 fixedRight = alWidth + startres - editLastRes;
1826 else if (!insertGap)
1828 // / Are we able to delete?
1829 // ie are all columns blank?
1831 for (g = 0; g < groupSize; g++)
1833 for (int j = startres; j < editLastRes; j++)
1835 if (groupSeqs[g].getLength() <= j)
1840 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1842 // Not a gap, block edit not valid
1851 // dragging to the right
1852 if (fixedColumns && fixedRight != -1)
1854 for (int j = editLastRes; j < startres; j++)
1856 insertGap(j, groupSeqs, fixedRight);
1861 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1862 startres - editLastRes, false);
1867 // dragging to the left
1868 if (fixedColumns && fixedRight != -1)
1870 for (int j = editLastRes; j > startres; j--)
1872 deleteChar(startres, groupSeqs, fixedRight);
1877 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1878 editLastRes - startres, false);
1885 * editing a single sequence
1889 // dragging to the right
1890 if (fixedColumns && fixedRight != -1)
1892 for (int j = editLastRes; j < startres; j++)
1894 if (!insertGap(j, seqs, fixedRight))
1897 * e.g. cursor mode command specified
1898 * more inserts than are possible
1906 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1907 startres - editLastRes, false);
1914 // dragging to the left
1915 if (fixedColumns && fixedRight != -1)
1917 for (int j = editLastRes; j > startres; j--)
1919 if (!Comparison.isGap(seq.getCharAt(startres)))
1923 deleteChar(startres, seqs, fixedRight);
1928 // could be a keyboard edit trying to delete none gaps
1930 for (int m = startres; m < editLastRes; m++)
1932 if (!Comparison.isGap(seq.getCharAt(m)))
1940 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1945 {// insertGap==false AND editSeq==TRUE;
1946 if (fixedColumns && fixedRight != -1)
1948 for (int j = editLastRes; j < startres; j++)
1950 insertGap(j, seqs, fixedRight);
1955 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1956 startres - editLastRes, false);
1966 * Constructs an informative status bar message while dragging to insert or
1967 * delete gaps. Answers null if inserts and deletes cancel out.
1969 * @param editCommand
1970 * a command containing the list of individual edits
1973 protected static String getEditStatusMessage(EditCommand editCommand)
1975 if (editCommand == null)
1981 * add any inserts, and subtract any deletes,
1982 * not counting those auto-inserted when doing a 'locked edit'
1983 * (so only counting edits 'under the cursor')
1986 for (Edit cmd : editCommand.getEdits())
1988 if (!cmd.isSystemGenerated())
1990 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1998 * inserts and deletes cancel out
2003 String msgKey = count > 1 ? "label.insert_gaps"
2004 : (count == 1 ? "label.insert_gap"
2005 : (count == -1 ? "label.delete_gap"
2006 : "label.delete_gaps"));
2007 count = Math.abs(count);
2009 return MessageManager.formatMessage(msgKey, String.valueOf(count));
2013 * Inserts one gap at column j, deleting the right-most gapped column up to
2014 * (and including) fixedColumn. Returns true if the edit is successful, false
2015 * if no blank column is available to allow the insertion to be balanced by a
2020 * @param fixedColumn
2023 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
2025 int blankColumn = fixedColumn;
2026 for (int s = 0; s < seq.length; s++)
2028 // Find the next gap before the end of the visible region boundary
2029 // If lastCol > j, theres a boundary after the gap insertion
2031 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
2033 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
2035 // Theres a space, so break and insert the gap
2040 if (blankColumn <= j)
2042 blankColumn = fixedColumn;
2048 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2050 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2056 * Helper method to add and perform one edit action
2062 * @param systemGenerated
2063 * true if the edit is a 'balancing' delete (or insert) to match a
2064 * user's insert (or delete) in a locked editing region
2066 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2067 int count, boolean systemGenerated)
2070 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2071 av.getAlignment().getGapCharacter());
2072 edit.setSystemGenerated(systemGenerated);
2074 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2078 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2079 * each of the given sequences. The caller should ensure that all sequences
2080 * are gapped in column j.
2084 * @param fixedColumn
2086 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2088 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2090 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2094 * On reentering the panel, stops any scrolling that was started on dragging
2100 public void mouseEntered(MouseEvent e)
2110 * On leaving the panel, if the mouse is being dragged, starts a thread to
2111 * scroll it until the mouse is released (in unwrapped mode only)
2116 public void mouseExited(MouseEvent e)
2118 lastMousePosition = null;
2119 ap.alignFrame.setStatus(" ");
2120 if (av.getWrapAlignment())
2125 if (mouseDragging && scrollThread == null)
2127 startScrolling(e.getPoint());
2132 * Handler for double-click on a position with one or more sequence features.
2133 * Opens the Amend Features dialog to allow feature details to be amended, or
2134 * the feature deleted.
2137 public void mouseClicked(MouseEvent evt)
2139 SequenceGroup sg = null;
2140 MousePos pos = findMousePosition(evt);
2141 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2146 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2148 sg = av.getSelectionGroup();
2149 if (sg != null && sg.getSize() == 1
2150 && sg.getEndRes() - sg.getStartRes() < 2)
2152 av.setSelectionGroup(null);
2155 int column = pos.column;
2158 * find features at the position (if not gapped), or straddling
2159 * the position (if at a gap)
2161 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2162 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2163 .findFeaturesAtColumn(sequence, column + 1);
2165 if (!features.isEmpty())
2168 * highlight the first feature at the position on the alignment
2170 SearchResultsI highlight = new SearchResults();
2171 highlight.addResult(sequence, features.get(0).getBegin(),
2172 features.get(0).getEnd());
2173 seqCanvas.highlightSearchResults(highlight, true);
2176 * open the Amend Features dialog
2178 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2179 false).showDialog();
2185 public void mouseWheelMoved(MouseWheelEvent e)
2188 double wheelRotation = e.getPreciseWheelRotation();
2189 if (wheelRotation > 0)
2191 if (e.isShiftDown())
2193 av.getRanges().scrollRight(true);
2198 av.getRanges().scrollUp(false);
2201 else if (wheelRotation < 0)
2203 if (e.isShiftDown())
2205 av.getRanges().scrollRight(false);
2209 av.getRanges().scrollUp(true);
2214 * update status bar and tooltip for new position
2215 * (need to synthesize a mouse movement to refresh tooltip)
2218 ToolTipManager.sharedInstance().mouseMoved(e);
2227 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2229 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2234 final int res = pos.column;
2235 final int seq = pos.seqIndex;
2237 updateOverviewAndStructs = false;
2239 startWrapBlock = wrappedBlock;
2241 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2243 if ((sequence == null) || (res > sequence.getLength()))
2248 stretchGroup = av.getSelectionGroup();
2250 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2252 stretchGroup = av.getAlignment().findGroup(sequence, res);
2253 if (stretchGroup != null)
2255 // only update the current selection if the popup menu has a group to
2257 av.setSelectionGroup(stretchGroup);
2262 * defer right-mouse click handling to mouseReleased on Windows
2263 * (where isPopupTrigger() will answer true)
2264 * NB isRightMouseButton is also true for Cmd-click on Mac
2266 if (Platform.isWinRightButton(evt))
2271 if (evt.isPopupTrigger()) // Mac: mousePressed
2273 showPopupMenu(evt, pos);
2279 seqCanvas.cursorX = res;
2280 seqCanvas.cursorY = seq;
2281 seqCanvas.repaint();
2285 if (stretchGroup == null)
2287 createStretchGroup(res, sequence);
2290 if (stretchGroup != null)
2292 stretchGroup.addPropertyChangeListener(seqCanvas);
2295 seqCanvas.repaint();
2298 private void createStretchGroup(int res, SequenceI sequence)
2300 // Only if left mouse button do we want to change group sizes
2301 // define a new group here
2302 SequenceGroup sg = new SequenceGroup();
2303 sg.setStartRes(res);
2305 sg.addSequence(sequence, false);
2306 av.setSelectionGroup(sg);
2309 if (av.getConservationSelected())
2311 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2315 if (av.getAbovePIDThreshold())
2317 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2320 // TODO: stretchGroup will always be not null. Is this a merge error ?
2321 // or is there a threading issue here?
2322 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2324 // Edit end res position of selected group
2325 changeEndRes = true;
2327 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2329 // Edit end res position of selected group
2330 changeStartRes = true;
2332 stretchGroup.getWidth();
2337 * Build and show a pop-up menu at the right-click mouse position
2342 void showPopupMenu(MouseEvent evt, MousePos pos)
2344 final int column = pos.column;
2345 final int seq = pos.seqIndex;
2346 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2347 if (sequence != null)
2349 PopupMenu pop = new PopupMenu(ap, sequence, column);
2350 pop.show(this, evt.getX(), evt.getY());
2355 * Update the display after mouse up on a selection or group
2358 * mouse released event details
2360 * true if this event is happening after a mouse drag (rather than a
2363 protected void doMouseReleasedDefineMode(MouseEvent evt,
2366 if (stretchGroup == null)
2371 stretchGroup.removePropertyChangeListener(seqCanvas);
2373 // always do this - annotation has own state
2374 // but defer colourscheme update until hidden sequences are passed in
2375 boolean vischange = stretchGroup.recalcConservation(true);
2376 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2378 if (stretchGroup.cs != null)
2382 stretchGroup.cs.alignmentChanged(stretchGroup,
2383 av.getHiddenRepSequences());
2386 ResidueShaderI groupColourScheme = stretchGroup
2387 .getGroupColourScheme();
2388 String name = stretchGroup.getName();
2389 if (stretchGroup.cs.conservationApplied())
2391 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2393 if (stretchGroup.cs.getThreshold() > 0)
2395 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2398 PaintRefresher.Refresh(this, av.getSequenceSetId());
2399 // TODO: structure colours only need updating if stretchGroup used to or now
2400 // does contain sequences with structure views
2401 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2402 updateOverviewAndStructs = false;
2403 changeEndRes = false;
2404 changeStartRes = false;
2405 stretchGroup = null;
2410 * Resizes the borders of a selection group depending on the direction of
2415 protected void dragStretchGroup(MouseEvent evt)
2417 if (stretchGroup == null)
2422 MousePos pos = findMousePosition(evt);
2423 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2428 int res = pos.column;
2429 int y = pos.seqIndex;
2431 if (wrappedBlock != startWrapBlock)
2436 res = Math.min(res, av.getAlignment().getWidth() - 1);
2438 if (stretchGroup.getEndRes() == res)
2440 // Edit end res position of selected group
2441 changeEndRes = true;
2443 else if (stretchGroup.getStartRes() == res)
2445 // Edit start res position of selected group
2446 changeStartRes = true;
2449 if (res < av.getRanges().getStartRes())
2451 res = av.getRanges().getStartRes();
2456 if (res > (stretchGroup.getStartRes() - 1))
2458 stretchGroup.setEndRes(res);
2459 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2462 else if (changeStartRes)
2464 if (res < (stretchGroup.getEndRes() + 1))
2466 stretchGroup.setStartRes(res);
2467 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2471 int dragDirection = 0;
2477 else if (y < oldSeq)
2482 while ((y != oldSeq) && (oldSeq > -1)
2483 && (y < av.getAlignment().getHeight()))
2485 // This routine ensures we don't skip any sequences, as the
2486 // selection is quite slow.
2487 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2489 oldSeq += dragDirection;
2496 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2498 if (stretchGroup.getSequences(null).contains(nextSeq))
2500 stretchGroup.deleteSequence(seq, false);
2501 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2507 stretchGroup.addSequence(seq, false);
2510 stretchGroup.addSequence(nextSeq, false);
2511 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2520 mouseDragging = true;
2522 if (scrollThread != null)
2524 scrollThread.setMousePosition(evt.getPoint());
2528 * construct a status message showing the range of the selection
2530 StringBuilder status = new StringBuilder(64);
2531 List<SequenceI> seqs = stretchGroup.getSequences();
2532 String name = seqs.get(0).getName();
2533 if (name.length() > 20)
2535 name = name.substring(0, 20);
2537 status.append(name).append(" - ");
2538 name = seqs.get(seqs.size() - 1).getName();
2539 if (name.length() > 20)
2541 name = name.substring(0, 20);
2543 status.append(name).append(" ");
2544 int startRes = stretchGroup.getStartRes();
2545 status.append(" cols ").append(String.valueOf(startRes + 1))
2547 int endRes = stretchGroup.getEndRes();
2548 status.append(String.valueOf(endRes + 1));
2549 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2550 .append(String.valueOf(endRes - startRes + 1)).append(")");
2551 ap.alignFrame.setStatus(status.toString());
2555 * Stops the scroll thread if it is running
2557 void stopScrolling()
2559 if (scrollThread != null)
2561 scrollThread.stopScrolling();
2562 scrollThread = null;
2564 mouseDragging = false;
2568 * Starts a thread to scroll the alignment, towards a given mouse position
2569 * outside the panel bounds, unless the alignment is in wrapped mode
2573 void startScrolling(Point mousePos)
2576 * set this.mouseDragging in case this was called from
2577 * a drag in ScalePanel or AnnotationPanel
2579 mouseDragging = true;
2580 if (!av.getWrapAlignment() && scrollThread == null)
2582 scrollThread = new ScrollThread();
2583 scrollThread.setMousePosition(mousePos);
2584 if (Platform.isJS())
2587 * Javascript - run every 20ms until scrolling stopped
2588 * or reaches the limit of scrollable alignment
2590 Timer t = new Timer(20, new ActionListener()
2593 public void actionPerformed(ActionEvent e)
2595 if (scrollThread != null)
2597 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2598 scrollThread.scrollOnce();
2602 t.addActionListener(new ActionListener()
2605 public void actionPerformed(ActionEvent e)
2607 if (scrollThread == null)
2609 // SeqPanel.stopScrolling called
2619 * Java - run in a new thread
2621 scrollThread.start();
2627 * Performs scrolling of the visible alignment left, right, up or down, until
2628 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2629 * limit of the alignment is reached
2631 class ScrollThread extends Thread
2633 private Point mousePos;
2635 private volatile boolean keepRunning = true;
2640 public ScrollThread()
2642 setName("SeqPanel$ScrollThread");
2646 * Sets the position of the mouse that determines the direction of the
2647 * scroll to perform. If this is called as the mouse moves, scrolling should
2648 * respond accordingly. For example, if the mouse is dragged right, scroll
2649 * right should start; if the drag continues down, scroll down should also
2654 public void setMousePosition(Point p)
2660 * Sets a flag that will cause the thread to exit
2662 public void stopScrolling()
2664 keepRunning = false;
2668 * Scrolls the alignment left or right, and/or up or down, depending on the
2669 * last notified mouse position, until the limit of the alignment is
2670 * reached, or a flag is set to stop the scroll
2677 if (mousePos != null)
2679 keepRunning = scrollOnce();
2684 } catch (Exception ex)
2688 SeqPanel.this.scrollThread = null;
2694 * <li>one row up, if the mouse is above the panel</li>
2695 * <li>one row down, if the mouse is below the panel</li>
2696 * <li>one column left, if the mouse is left of the panel</li>
2697 * <li>one column right, if the mouse is right of the panel</li>
2699 * Answers true if a scroll was performed, false if not - meaning either
2700 * that the mouse position is within the panel, or the edge of the alignment
2703 boolean scrollOnce()
2706 * quit after mouseUp ensures interrupt in JalviewJS
2713 boolean scrolled = false;
2714 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2721 // mouse is above this panel - try scroll up
2722 scrolled = ranges.scrollUp(true);
2724 else if (mousePos.y >= getHeight())
2726 // mouse is below this panel - try scroll down
2727 scrolled = ranges.scrollUp(false);
2731 * scroll left or right
2735 scrolled |= ranges.scrollRight(false);
2737 else if (mousePos.x >= getWidth())
2739 scrolled |= ranges.scrollRight(true);
2746 * modify current selection according to a received message.
2749 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2750 HiddenColumns hidden, SelectionSource source)
2752 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2753 // handles selection messages...
2754 // TODO: extend config options to allow user to control if selections may be
2755 // shared between viewports.
2756 boolean iSentTheSelection = (av == source
2757 || (source instanceof AlignViewport
2758 && ((AlignmentViewport) source).getSequenceSetId()
2759 .equals(av.getSequenceSetId())));
2761 if (iSentTheSelection)
2763 // respond to our own event by updating dependent dialogs
2764 if (ap.getCalculationDialog() != null)
2766 ap.getCalculationDialog().validateCalcTypes();
2772 // process further ?
2773 if (!av.followSelection)
2779 * Ignore the selection if there is one of our own pending.
2781 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2787 * Check for selection in a view of which this one is a dna/protein
2790 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2795 // do we want to thread this ? (contention with seqsel and colsel locks, I
2798 * only copy colsel if there is a real intersection between
2799 * sequence selection and this panel's alignment
2801 boolean repaint = false;
2802 boolean copycolsel = false;
2804 SequenceGroup sgroup = null;
2805 if (seqsel != null && seqsel.getSize() > 0)
2807 if (av.getAlignment() == null)
2809 Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2810 + " ViewId=" + av.getViewId()
2811 + " 's alignment is NULL! returning immediately.");
2814 sgroup = seqsel.intersect(av.getAlignment(),
2815 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2816 if ((sgroup != null && sgroup.getSize() > 0))
2821 if (sgroup != null && sgroup.getSize() > 0)
2823 av.setSelectionGroup(sgroup);
2827 av.setSelectionGroup(null);
2829 av.isSelectionGroupChanged(true);
2834 // the current selection is unset or from a previous message
2835 // so import the new colsel.
2836 if (colsel == null || colsel.isEmpty())
2838 if (av.getColumnSelection() != null)
2840 av.getColumnSelection().clear();
2846 // TODO: shift colSel according to the intersecting sequences
2847 if (av.getColumnSelection() == null)
2849 av.setColumnSelection(new ColumnSelection(colsel));
2853 av.getColumnSelection().setElementsFrom(colsel,
2854 av.getAlignment().getHiddenColumns());
2857 av.isColSelChanged(true);
2861 if (copycolsel && av.hasHiddenColumns()
2862 && (av.getAlignment().getHiddenColumns() == null))
2864 System.err.println("Bad things");
2866 if (repaint) // always true!
2868 // probably finessing with multiple redraws here
2869 PaintRefresher.Refresh(this, av.getSequenceSetId());
2870 // ap.paintAlignment(false);
2873 // lastly, update dependent dialogs
2874 if (ap.getCalculationDialog() != null)
2876 ap.getCalculationDialog().validateCalcTypes();
2882 * If this panel is a cdna/protein translation view of the selection source,
2883 * tries to map the source selection to a local one, and returns true. Else
2890 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2891 ColumnSelection colsel, HiddenColumns hidden,
2892 SelectionSource source)
2894 if (!(source instanceof AlignViewportI))
2898 final AlignViewportI sourceAv = (AlignViewportI) source;
2899 if (sourceAv.getCodingComplement() != av
2900 && av.getCodingComplement() != sourceAv)
2906 * Map sequence selection
2908 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2909 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2910 av.isSelectionGroupChanged(true);
2913 * Map column selection
2915 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2917 ColumnSelection cs = new ColumnSelection();
2918 HiddenColumns hs = new HiddenColumns();
2919 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2920 av.setColumnSelection(cs);
2921 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2923 // lastly, update any dependent dialogs
2924 if (ap.getCalculationDialog() != null)
2926 ap.getCalculationDialog().validateCalcTypes();
2930 * repaint alignment, and also Overview or Structure
2931 * if hidden column selection has changed
2933 ap.paintAlignment(hiddenChanged, hiddenChanged);
2934 // propagate any selection changes
2935 PaintRefresher.Refresh(ap, av.getSequenceSetId());
2942 * @return null or last search results handled by this panel
2944 public SearchResultsI getLastSearchResults()
2946 return lastSearchResults;