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 // jalview.bin.Console.outPrintln(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);
347 * @return absolute column in alignment nearest to the mouse pointer
349 int findAlignmentColumn(MouseEvent evt)
351 return findNearestColumn(evt, true);
355 * Returns the aligned sequence position (base 0) at the mouse position, or
356 * the closest visible one
358 * Returns -1 if in wrapped mode with the mouse over either left or right
364 int findColumn(MouseEvent evt)
366 return findNearestColumn(evt, false);
370 * @param nearestColumn
371 * when false returns negative values for out of bound positions - -1
372 * for scale left/right, <-1 if far to right
373 * @return nearest absolute column to mouse pointer
375 private int findNearestColumn(MouseEvent evt, boolean nearestColumn)
380 final int startRes = av.getRanges().getStartRes();
381 final int charWidth = av.getCharWidth();
383 if (av.getWrapAlignment())
385 int hgap = av.getCharHeight();
386 if (av.getScaleAboveWrapped())
388 hgap += av.getCharHeight();
391 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
392 + hgap + seqCanvas.getAnnotationHeight();
395 y = Math.max(0, y - hgap);
396 x -= seqCanvas.getLabelWidthWest();
399 // mouse is over left scale
410 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
415 if (x >= cwidth * charWidth)
419 // mouse is over right scale
424 x = cwidth * charWidth - 1;
428 wrappedBlock = y / cHeight;
429 wrappedBlock += startRes / cwidth;
430 // allow for wrapped view scrolled right (possible from Overview)
431 int startOffset = startRes % cwidth;
432 res = wrappedBlock * cwidth + startOffset
433 + Math.min(cwidth - 1, x / charWidth);
438 * make sure we calculate relative to visible alignment,
439 * rather than right-hand gutter
441 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
447 res = (x / charWidth) + startRes;
448 res = Math.min(res, av.getRanges().getEndRes());
452 if (av.hasHiddenColumns())
454 res = av.getAlignment().getHiddenColumns()
455 .visibleToAbsoluteColumn(res);
462 * When all of a sequence of edits are complete, put the resulting edit list
463 * on the history stack (undo list), and reset flags for editing in progress.
469 if (editCommand != null && editCommand.getSize() > 0)
471 ap.alignFrame.addHistoryItem(editCommand);
472 av.firePropertyChange("alignment", null,
473 av.getAlignment().getSequences());
478 * Tidy up come what may...
483 groupEditing = false;
492 seqCanvas.cursorY = getKeyboardNo1() - 1;
493 scrollToVisible(true);
496 void setCursorColumn()
498 seqCanvas.cursorX = getKeyboardNo1() - 1;
499 scrollToVisible(true);
502 void setCursorRowAndColumn()
504 if (keyboardNo2 == null)
506 keyboardNo2 = new StringBuffer();
510 seqCanvas.cursorX = getKeyboardNo1() - 1;
511 seqCanvas.cursorY = getKeyboardNo2() - 1;
512 scrollToVisible(true);
516 void setCursorPosition()
518 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
520 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
521 scrollToVisible(true);
524 void moveCursor(int dx, int dy)
526 moveCursor(dx, dy, false);
529 void moveCursor(int dx, int dy, boolean nextWord)
531 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
535 int maxWidth = av.getAlignment().getWidth();
536 int maxHeight = av.getAlignment().getHeight();
537 SequenceI seqAtRow = av.getAlignment()
538 .getSequenceAt(seqCanvas.cursorY);
539 // look for next gap or residue
540 boolean isGap = Comparison
541 .isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
542 int p = seqCanvas.cursorX, lastP, r = seqCanvas.cursorY, lastR;
558 seqAtRow = av.getAlignment().getSequenceAt(r);
560 p = nextVisible(hidden, maxWidth, p, dx);
561 } while ((dx != 0 ? p != lastP : r != lastR)
562 && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
563 seqCanvas.cursorX = p;
564 seqCanvas.cursorY = r;
568 int maxWidth = av.getAlignment().getWidth();
569 seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX,
571 seqCanvas.cursorY += dy;
573 scrollToVisible(false);
576 private int nextVisible(HiddenColumns hidden, int maxWidth, int original,
579 int newCursorX = original + dx;
580 if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
582 int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
583 int[] region = hidden.getRegionWithEdgeAtRes(visx);
585 if (region != null) // just in case
590 newCursorX = region[1] + 1;
595 newCursorX = region[0] - 1;
599 newCursorX = (newCursorX < 0) ? 0 : newCursorX;
600 if (newCursorX >= maxWidth || !hidden.isVisible(newCursorX))
602 newCursorX = original;
608 * Scroll to make the cursor visible in the viewport.
611 * just jump to the location rather than scrolling
613 void scrollToVisible(boolean jump)
615 if (seqCanvas.cursorX < 0)
617 seqCanvas.cursorX = 0;
619 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
621 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
624 if (seqCanvas.cursorY < 0)
626 seqCanvas.cursorY = 0;
628 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
630 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
635 boolean repaintNeeded = true;
638 // only need to repaint if the viewport did not move, as otherwise it will
640 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
645 if (av.getWrapAlignment())
647 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
648 int x = av.getAlignment().getHiddenColumns()
649 .absoluteToVisibleColumn(seqCanvas.cursorX);
650 av.getRanges().scrollToWrappedVisible(x);
654 av.getRanges().scrollToVisible(seqCanvas.cursorX,
659 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
661 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
662 seqCanvas.cursorX, seqCanvas.cursorY);
671 void setSelectionAreaAtCursor(boolean topLeft)
673 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
675 if (av.getSelectionGroup() != null)
677 SequenceGroup sg = av.getSelectionGroup();
678 // Find the top and bottom of this group
679 int min = av.getAlignment().getHeight(), max = 0;
680 for (int i = 0; i < sg.getSize(); i++)
682 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
697 sg.setStartRes(seqCanvas.cursorX);
698 if (sg.getEndRes() < seqCanvas.cursorX)
700 sg.setEndRes(seqCanvas.cursorX);
703 min = seqCanvas.cursorY;
707 sg.setEndRes(seqCanvas.cursorX);
708 if (sg.getStartRes() > seqCanvas.cursorX)
710 sg.setStartRes(seqCanvas.cursorX);
713 max = seqCanvas.cursorY + 1;
718 // Only the user can do this
719 av.setSelectionGroup(null);
723 // Now add any sequences between min and max
724 sg.getSequences(null).clear();
725 for (int i = min; i < max; i++)
727 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
732 if (av.getSelectionGroup() == null)
734 SequenceGroup sg = new SequenceGroup();
735 sg.setStartRes(seqCanvas.cursorX);
736 sg.setEndRes(seqCanvas.cursorX);
737 sg.addSequence(sequence, false);
738 av.setSelectionGroup(sg);
741 ap.paintAlignment(false, false);
745 void insertGapAtCursor(boolean group)
747 groupEditing = group;
748 editStartSeq = seqCanvas.cursorY;
749 editLastRes = seqCanvas.cursorX;
750 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
754 void deleteGapAtCursor(boolean group)
756 groupEditing = group;
757 editStartSeq = seqCanvas.cursorY;
758 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
759 editSequence(false, false, seqCanvas.cursorX);
763 void insertNucAtCursor(boolean group, String nuc)
765 // TODO not called - delete?
766 groupEditing = group;
767 editStartSeq = seqCanvas.cursorY;
768 editLastRes = seqCanvas.cursorX;
769 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
773 void numberPressed(char value)
775 if (keyboardNo1 == null)
777 keyboardNo1 = new StringBuffer();
780 if (keyboardNo2 != null)
782 keyboardNo2.append(value);
786 keyboardNo1.append(value);
794 if (keyboardNo1 != null)
796 int value = Integer.parseInt(keyboardNo1.toString());
800 } catch (Exception x)
811 if (keyboardNo2 != null)
813 int value = Integer.parseInt(keyboardNo2.toString());
817 } catch (Exception x)
831 public void mouseReleased(MouseEvent evt)
833 MousePos pos = findMousePosition(evt);
834 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
839 boolean didDrag = mouseDragging; // did we come here after a drag
840 mouseDragging = false;
841 mouseWheelPressed = false;
843 if (evt.isPopupTrigger()) // Windows: mouseReleased
845 showPopupMenu(evt, pos);
856 doMouseReleasedDefineMode(evt, didDrag);
867 public void mousePressed(MouseEvent evt)
869 lastMousePress = evt.getPoint();
870 MousePos pos = findMousePosition(evt);
871 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
876 if (SwingUtilities.isMiddleMouseButton(evt))
878 mouseWheelPressed = true;
882 boolean isControlDown = Platform.isControlDown(evt);
883 if (evt.isShiftDown() || isControlDown)
893 doMousePressedDefineMode(evt, pos);
897 int seq = pos.seqIndex;
898 int res = pos.column;
900 if ((seq < av.getAlignment().getHeight())
901 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
918 public void mouseOverSequence(SequenceI sequence, int index, int pos)
920 String tmp = sequence.hashCode() + " " + index + " " + pos;
922 if (lastMessage == null || !lastMessage.equals(tmp))
924 // jalview.bin.Console.errPrintln("mouseOver Sequence: "+tmp);
925 ssm.mouseOverSequence(sequence, index, pos, av);
931 * Highlight the mapped region described by the search results object (unless
932 * unchanged). This supports highlight of protein while mousing over linked
933 * cDNA and vice versa. The status bar is also updated to show the location of
934 * the start of the highlighted region.
937 public String highlightSequence(SearchResultsI results)
939 if (results == null || results.equals(lastSearchResults))
943 lastSearchResults = results;
945 boolean wasScrolled = false;
947 if (av.isFollowHighlight())
949 // don't allow highlight of protein/cDNA to also scroll a complementary
950 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
951 // over residue to change abruptly, causing highlighted residue in panel 2
952 // to change, causing a scroll in panel 1 etc)
953 ap.setToScrollComplementPanel(false);
954 wasScrolled = ap.scrollToPosition(results);
957 seqCanvas.revalidate();
959 ap.setToScrollComplementPanel(true);
962 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
963 if (seqCanvas.highlightSearchResults(results, fastPaint))
965 setStatusMessage(results);
967 return results.isEmpty() ? null : getHighlightInfo(results);
971 * temporary hack: answers a message suitable to show on structure hover
972 * label. This is normally null. It is a peptide variation description if
974 * <li>results are a single residue in a protein alignment</li>
975 * <li>there is a mapping to a coding sequence (codon)</li>
976 * <li>there are one or more SNP variant features on the codon</li>
978 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
983 private String getHighlightInfo(SearchResultsI results)
986 * ideally, just find mapped CDS (as we don't care about render style here);
987 * for now, go via split frame complement's FeatureRenderer
989 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
990 if (complement == null)
994 AlignFrame af = Desktop.getAlignFrameFor(complement);
995 FeatureRendererModel fr2 = af.getFeatureRenderer();
997 List<SearchResultMatchI> matches = results.getResults();
998 int j = matches.size();
999 List<String> infos = new ArrayList<>();
1000 for (int i = 0; i < j; i++)
1002 SearchResultMatchI match = matches.get(i);
1003 int pos = match.getStart();
1004 if (pos == match.getEnd())
1006 SequenceI seq = match.getSequence();
1007 SequenceI ds = seq.getDatasetSequence() == null ? seq
1008 : seq.getDatasetSequence();
1009 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(ds, pos);
1012 for (SequenceFeature sf : mf.features)
1014 String pv = mf.findProteinVariants(sf);
1015 if (pv.length() > 0 && !infos.contains(pv))
1024 if (infos.isEmpty())
1028 StringBuilder sb = new StringBuilder();
1029 for (String info : infos)
1031 if (sb.length() > 0)
1037 return sb.toString();
1041 public VamsasSource getVamsasSource()
1043 return this.ap == null ? null : this.ap.av;
1047 public void updateColours(SequenceI seq, int index)
1049 jalview.bin.Console.outPrintln("update the seqPanel colours");
1054 * Action on mouse movement is to update the status bar to show the current
1055 * sequence position, and (if features are shown) to show any features at the
1056 * position in a tooltip. Does nothing if the mouse move does not change
1062 public void mouseMoved(MouseEvent evt)
1066 // This is because MacOSX creates a mouseMoved
1067 // If control is down, other platforms will not.
1071 final MousePos mousePos = findMousePosition(evt);
1072 if (mousePos.equals(lastMousePosition))
1075 * just a pixel move without change of 'cell'
1077 moveTooltip = false;
1081 lastMousePosition = mousePos;
1083 if (mousePos.isOverAnnotation())
1085 mouseMovedOverAnnotation(mousePos);
1088 final int seq = mousePos.seqIndex;
1090 final int column = mousePos.column;
1091 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1093 lastMousePosition = null;
1094 setToolTipText(null);
1096 lastFormattedTooltip = null;
1097 ap.alignFrame.setStatus("");
1101 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1103 if (column >= sequence.getLength())
1109 * set status bar message, returning residue position in sequence
1111 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1112 final int pos = setStatusMessage(sequence, column, seq);
1113 if (ssm != null && !isGapped)
1115 mouseOverSequence(sequence, column, pos);
1118 StringBuilder tooltipText = new StringBuilder(64);
1120 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1123 for (int g = 0; g < groups.length; g++)
1125 if (groups[g].getStartRes() <= column
1126 && groups[g].getEndRes() >= column)
1128 if (!groups[g].getName().startsWith("JTreeGroup")
1129 && !groups[g].getName().startsWith("JGroup"))
1131 tooltipText.append(groups[g].getName());
1134 if (groups[g].getDescription() != null)
1136 tooltipText.append(": " + groups[g].getDescription());
1143 * add any features at the position to the tooltip; if over a gap, only
1144 * add features that straddle the gap (pos may be the residue before or
1147 int unshownFeatures = 0;
1148 if (av.isShowSequenceFeatures())
1150 List<SequenceFeature> features = ap.getFeatureRenderer()
1151 .findFeaturesAtColumn(sequence, column + 1);
1152 unshownFeatures = seqARep.appendFeatures(tooltipText, pos, features,
1153 this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
1156 * add features in CDS/protein complement at the corresponding
1157 * position if configured to do so
1159 if (av.isShowComplementFeatures())
1161 if (!Comparison.isGap(sequence.getCharAt(column)))
1163 AlignViewportI complement = ap.getAlignViewport()
1164 .getCodingComplement();
1165 AlignFrame af = Desktop.getAlignFrameFor(complement);
1166 FeatureRendererModel fr2 = af.getFeatureRenderer();
1167 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1171 unshownFeatures += seqARep.appendFeatures(tooltipText, pos, mf,
1172 fr2, MAX_TOOLTIP_LENGTH);
1177 if (tooltipText.length() == 0) // nothing added
1179 setToolTipText(null);
1184 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1186 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1187 tooltipText.append("...");
1189 if (unshownFeatures > 0)
1191 tooltipText.append("<br/>").append("... ").append("<i>")
1192 .append(MessageManager.formatMessage(
1193 "label.features_not_shown", unshownFeatures))
1196 String textString = tooltipText.toString();
1197 if (!textString.equals(lastTooltip))
1199 lastTooltip = textString;
1200 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true, textString);
1201 setToolTipText(lastFormattedTooltip);
1207 * When the view is in wrapped mode, and the mouse is over an annotation row,
1208 * shows the corresponding tooltip and status message (if any)
1213 protected void mouseMovedOverAnnotation(MousePos pos)
1215 final int column = pos.column;
1216 final int rowIndex = pos.annotationIndex;
1218 // TODO - get yOffset for annotation, too
1219 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1224 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1226 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1228 if (tooltip == null ? tooltip != lastTooltip
1229 : !tooltip.equals(lastTooltip))
1231 lastTooltip = tooltip;
1232 lastFormattedTooltip = tooltip == null ? null
1233 : JvSwingUtils.wrapTooltip(true, tooltip);
1234 setToolTipText(lastFormattedTooltip);
1237 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1238 anns[rowIndex], 0, av);
1239 ap.alignFrame.setStatus(msg);
1243 * if Shift key is held down while moving the mouse,
1244 * the tooltip location is not changed once shown
1246 private Point lastTooltipLocation = null;
1249 * this flag is false for pixel moves within a residue,
1250 * to reduce tooltip flicker
1252 private boolean moveTooltip = true;
1255 * a dummy tooltip used to estimate where to position tooltips
1257 private JToolTip tempTip = new JLabel().createToolTip();
1262 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1265 public Point getToolTipLocation(MouseEvent event)
1269 if (lastTooltip == null || !moveTooltip)
1274 if (lastTooltipLocation != null && event.isShiftDown())
1276 return lastTooltipLocation;
1279 int x = event.getX();
1280 int y = event.getY();
1283 tempTip.setTipText(lastFormattedTooltip);
1284 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1286 // was x += (w - x < 200) ? -(w / 2) : 5;
1287 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1288 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1290 return lastTooltipLocation = p;
1294 * set when the current UI interaction has resulted in a change that requires
1295 * shading in overviews and structures to be recalculated. this could be
1296 * changed to a something more expressive that indicates what actually has
1297 * changed, so selective redraws can be applied (ie. only structures, only
1300 private boolean updateOverviewAndStructs = false; // TODO: refactor to
1304 * set if av.getSelectionGroup() refers to a group that is defined on the
1305 * alignment view, rather than a transient selection
1307 // private boolean editingDefinedGroup = false; // TODO: refactor to
1308 // avcontroller or viewModel
1311 * Sets the status message in alignment panel, showing the sequence number
1312 * (index) and id, and residue and residue position if not at a gap, for the
1313 * given sequence and column position. Returns the residue position returned
1314 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1315 * if at a gapped position.
1318 * aligned sequence object
1322 * index of sequence in alignment
1323 * @return sequence position of residue at column, or adjacent residue if at a
1326 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1328 char sequenceChar = sequence.getCharAt(column);
1329 int pos = sequence.findPosition(column);
1330 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1336 * Builds the status message for the current cursor location and writes it to
1337 * the status bar, for example
1340 * Sequence 3 ID: FER1_SOLLC
1341 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1342 * Sequence 5 ID: FER1_PEA Residue: B (3)
1343 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1348 * sequence position in the alignment (1..)
1349 * @param sequenceChar
1350 * the character under the cursor
1352 * the sequence residue position (if not over a gap)
1354 protected void setStatusMessage(String seqName, int seqIndex,
1355 char sequenceChar, int residuePos)
1357 StringBuilder text = new StringBuilder(32);
1360 * Sequence number (if known), and sequence name.
1362 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1363 text.append("Sequence").append(seqno).append(" ID: ").append(seqName);
1365 String residue = null;
1368 * Try to translate the display character to residue name (null for gap).
1370 boolean isGapped = Comparison.isGap(sequenceChar);
1374 boolean nucleotide = av.getAlignment().isNucleotide();
1375 String displayChar = String.valueOf(sequenceChar);
1378 residue = ResidueProperties.nucleotideName.get(displayChar);
1382 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1383 : ("*".equals(displayChar) ? "STOP"
1384 : ResidueProperties.aa2Triplet.get(displayChar));
1386 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1387 .append(": ").append(residue == null ? displayChar : residue);
1389 text.append(" (").append(Integer.toString(residuePos)).append(")");
1391 ap.alignFrame.setStatus(text.toString());
1395 * Set the status bar message to highlight the first matched position in
1400 private void setStatusMessage(SearchResultsI results)
1402 AlignmentI al = this.av.getAlignment();
1403 int sequenceIndex = al.findIndex(results);
1404 if (sequenceIndex == -1)
1408 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1409 SequenceI ds = alignedSeq.getDatasetSequence();
1410 for (SearchResultMatchI m : results.getResults())
1412 SequenceI seq = m.getSequence();
1413 if (seq.getDatasetSequence() != null)
1415 seq = seq.getDatasetSequence();
1420 int start = m.getStart();
1421 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1422 seq.getCharAt(start - 1), start);
1432 public void mouseDragged(MouseEvent evt)
1434 MousePos pos = findMousePosition(evt);
1435 if (pos.isOverAnnotation() || pos.column == -1)
1440 if (mouseWheelPressed)
1442 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1443 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1445 int oldWidth = av.getCharWidth();
1447 // Which is bigger, left-right or up-down?
1448 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1449 .abs(evt.getX() - lastMousePress.getX()))
1452 * on drag up or down, decrement or increment font size
1454 int fontSize = av.font.getSize();
1455 boolean fontChanged = false;
1457 if (evt.getY() < lastMousePress.getY())
1462 else if (evt.getY() > lastMousePress.getY())
1475 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1477 av.setFont(newFont, true);
1478 av.setCharWidth(oldWidth);
1482 ap.av.getCodingComplement().setFont(newFont, true);
1483 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1484 .getSplitViewContainer();
1485 splitFrame.adjustLayout();
1486 splitFrame.repaint();
1493 * on drag left or right, decrement or increment character width
1496 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1498 newWidth = av.getCharWidth() - 1;
1499 av.setCharWidth(newWidth);
1501 else if (evt.getX() > lastMousePress.getX())
1503 newWidth = av.getCharWidth() + 1;
1504 av.setCharWidth(newWidth);
1508 ap.paintAlignment(false, false);
1512 * need to ensure newWidth is set on cdna, regardless of which
1513 * panel the mouse drag happened in; protein will compute its
1514 * character width as 1:1 or 3:1
1516 av.getCodingComplement().setCharWidth(newWidth);
1517 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1518 .getSplitViewContainer();
1519 splitFrame.adjustLayout();
1520 splitFrame.repaint();
1525 FontMetrics fm = getFontMetrics(av.getFont());
1526 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1528 lastMousePress = evt.getPoint();
1535 dragStretchGroup(evt);
1539 int res = pos.column;
1546 if ((editLastRes == -1) || (editLastRes == res))
1551 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1553 // dragLeft, delete gap
1554 editSequence(false, false, res);
1558 editSequence(true, false, res);
1561 mouseDragging = true;
1562 if (scrollThread != null)
1564 scrollThread.setMousePosition(evt.getPoint());
1569 * Edits the sequence to insert or delete one or more gaps, in response to a
1570 * mouse drag or cursor mode command. The number of inserts/deletes may be
1571 * specified with the cursor command, or else depends on the mouse event
1572 * (normally one column, but potentially more for a fast mouse drag).
1574 * Delete gaps is limited to the number of gaps left of the cursor position
1575 * (mouse drag), or at or right of the cursor position (cursor mode).
1577 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1578 * the current selection group.
1580 * In locked editing mode (with a selection group present), inserts/deletions
1581 * within the selection group are limited to its boundaries (and edits outside
1582 * the group stop at its border).
1585 * true to insert gaps, false to delete gaps
1587 * (unused parameter)
1589 * the column at which to perform the action; the number of columns
1590 * affected depends on <code>this.editLastRes</code> (cursor column
1593 synchronized void editSequence(boolean insertGap, boolean editSeq,
1597 int fixedRight = -1;
1598 boolean fixedColumns = false;
1599 SequenceGroup sg = av.getSelectionGroup();
1601 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1603 // No group, but the sequence may represent a group
1604 if (!groupEditing && av.hasHiddenRows())
1606 if (av.isHiddenRepSequence(seq))
1608 sg = av.getRepresentedSequences(seq);
1609 groupEditing = true;
1613 StringBuilder message = new StringBuilder(64); // for status bar
1616 * make a name for the edit action, for
1617 * status bar message and Undo/Redo menu
1619 String label = null;
1622 message.append("Edit group:");
1623 label = MessageManager.getString("action.edit_group");
1627 message.append("Edit sequence: " + seq.getName());
1628 label = seq.getName();
1629 if (label.length() > 10)
1631 label = label.substring(0, 10);
1633 label = MessageManager.formatMessage("label.edit_params",
1639 * initialise the edit command if there is not
1640 * already one being extended
1642 if (editCommand == null)
1644 editCommand = new EditCommand(label);
1649 message.append(" insert ");
1653 message.append(" delete ");
1656 message.append(Math.abs(startres - editLastRes) + " gaps.");
1657 ap.alignFrame.setStatus(message.toString());
1660 * is there a selection group containing the sequence being edited?
1661 * if so the boundary of the group is the limit of the edit
1662 * (but the edit may be inside or outside the selection group)
1664 boolean inSelectionGroup = sg != null
1665 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1666 if (groupEditing || inSelectionGroup)
1668 fixedColumns = true;
1670 // sg might be null as the user may only see 1 sequence,
1671 // but the sequence represents a group
1674 if (!av.isHiddenRepSequence(seq))
1679 sg = av.getRepresentedSequences(seq);
1682 fixedLeft = sg.getStartRes();
1683 fixedRight = sg.getEndRes();
1685 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1686 || (startres >= fixedLeft && editLastRes < fixedLeft)
1687 || (startres > fixedRight && editLastRes <= fixedRight)
1688 || (startres <= fixedRight && editLastRes > fixedRight))
1694 if (fixedLeft > startres)
1696 fixedRight = fixedLeft - 1;
1699 else if (fixedRight < startres)
1701 fixedLeft = fixedRight;
1706 if (av.hasHiddenColumns())
1708 fixedColumns = true;
1709 int y1 = av.getAlignment().getHiddenColumns()
1710 .getNextHiddenBoundary(true, startres);
1711 int y2 = av.getAlignment().getHiddenColumns()
1712 .getNextHiddenBoundary(false, startres);
1714 if ((insertGap && startres > y1 && editLastRes < y1)
1715 || (!insertGap && startres < y2 && editLastRes > y2))
1721 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1722 // Selection spans a hidden region
1723 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1731 fixedRight = y2 - 1;
1736 boolean success = doEditSequence(insertGap, editSeq, startres,
1737 fixedRight, fixedColumns, sg);
1740 * report what actually happened (might be less than
1741 * what was requested), by inspecting the edit commands added
1743 String msg = getEditStatusMessage(editCommand);
1744 ap.alignFrame.setStatus(msg == null ? " " : msg);
1750 editLastRes = startres;
1751 seqCanvas.repaint();
1755 * A helper method that performs the requested editing to insert or delete
1756 * gaps (if possible). Answers true if the edit was successful, false if could
1757 * only be performed in part or not at all. Failure may occur in 'locked edit'
1758 * mode, when an insertion requires a matching gapped position (or column) to
1759 * delete, and deletion requires an adjacent gapped position (or column) to
1763 * true if inserting gap(s), false if deleting
1765 * (unused parameter, currently always false)
1767 * the column at which to perform the edit
1769 * fixed right boundary column of a locked edit (within or to the
1770 * left of a selection group)
1771 * @param fixedColumns
1772 * true if this is a locked edit
1774 * the sequence group (if group edit is being performed)
1777 protected boolean doEditSequence(final boolean insertGap,
1778 final boolean editSeq, final int startres, int fixedRight,
1779 final boolean fixedColumns, final SequenceGroup sg)
1781 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1782 SequenceI[] seqs = new SequenceI[] { seq };
1786 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1787 int g, groupSize = vseqs.size();
1788 SequenceI[] groupSeqs = new SequenceI[groupSize];
1789 for (g = 0; g < groupSeqs.length; g++)
1791 groupSeqs[g] = vseqs.get(g);
1797 // If the user has selected the whole sequence, and is dragging to
1798 // the right, we can still extend the alignment and selectionGroup
1799 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1800 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1803 av.getAlignment().getWidth() + startres - editLastRes);
1804 fixedRight = sg.getEndRes();
1807 // Is it valid with fixed columns??
1808 // Find the next gap before the end
1809 // of the visible region boundary
1810 boolean blank = false;
1811 for (; fixedRight > editLastRes; fixedRight--)
1815 for (g = 0; g < groupSize; g++)
1817 for (int j = 0; j < startres - editLastRes; j++)
1819 if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1834 if (sg.getSize() == av.getAlignment().getHeight())
1836 if ((av.hasHiddenColumns()
1837 && startres < av.getAlignment().getHiddenColumns()
1838 .getNextHiddenBoundary(false, startres)))
1843 int alWidth = av.getAlignment().getWidth();
1844 if (av.hasHiddenRows())
1846 int hwidth = av.getAlignment().getHiddenSequences()
1848 if (hwidth > alWidth)
1853 // We can still insert gaps if the selectionGroup
1854 // contains all the sequences
1855 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1856 fixedRight = alWidth + startres - editLastRes;
1866 else if (!insertGap)
1868 // / Are we able to delete?
1869 // ie are all columns blank?
1871 for (g = 0; g < groupSize; g++)
1873 for (int j = startres; j < editLastRes; j++)
1875 if (groupSeqs[g].getLength() <= j)
1880 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1882 // Not a gap, block edit not valid
1891 // dragging to the right
1892 if (fixedColumns && fixedRight != -1)
1894 for (int j = editLastRes; j < startres; j++)
1896 insertGap(j, groupSeqs, fixedRight);
1901 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1902 startres - editLastRes, false);
1907 // dragging to the left
1908 if (fixedColumns && fixedRight != -1)
1910 for (int j = editLastRes; j > startres; j--)
1912 deleteChar(startres, groupSeqs, fixedRight);
1917 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1918 editLastRes - startres, false);
1925 * editing a single sequence
1929 // dragging to the right
1930 if (fixedColumns && fixedRight != -1)
1932 for (int j = editLastRes; j < startres; j++)
1934 if (!insertGap(j, seqs, fixedRight))
1937 * e.g. cursor mode command specified
1938 * more inserts than are possible
1946 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1947 startres - editLastRes, false);
1954 // dragging to the left
1955 if (fixedColumns && fixedRight != -1)
1957 for (int j = editLastRes; j > startres; j--)
1959 if (!Comparison.isGap(seq.getCharAt(startres)))
1963 deleteChar(startres, seqs, fixedRight);
1968 // could be a keyboard edit trying to delete none gaps
1970 for (int m = startres; m < editLastRes; m++)
1972 if (!Comparison.isGap(seq.getCharAt(m)))
1980 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1985 {// insertGap==false AND editSeq==TRUE;
1986 if (fixedColumns && fixedRight != -1)
1988 for (int j = editLastRes; j < startres; j++)
1990 insertGap(j, seqs, fixedRight);
1995 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1996 startres - editLastRes, false);
2006 * Constructs an informative status bar message while dragging to insert or
2007 * delete gaps. Answers null if inserts and deletes cancel out.
2009 * @param editCommand
2010 * a command containing the list of individual edits
2013 protected static String getEditStatusMessage(EditCommand editCommand)
2015 if (editCommand == null)
2021 * add any inserts, and subtract any deletes,
2022 * not counting those auto-inserted when doing a 'locked edit'
2023 * (so only counting edits 'under the cursor')
2026 for (Edit cmd : editCommand.getEdits())
2028 if (!cmd.isSystemGenerated())
2030 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
2038 * inserts and deletes cancel out
2043 String msgKey = count > 1 ? "label.insert_gaps"
2044 : (count == 1 ? "label.insert_gap"
2045 : (count == -1 ? "label.delete_gap"
2046 : "label.delete_gaps"));
2047 count = Math.abs(count);
2049 return MessageManager.formatMessage(msgKey, String.valueOf(count));
2053 * Inserts one gap at column j, deleting the right-most gapped column up to
2054 * (and including) fixedColumn. Returns true if the edit is successful, false
2055 * if no blank column is available to allow the insertion to be balanced by a
2060 * @param fixedColumn
2063 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
2065 int blankColumn = fixedColumn;
2066 for (int s = 0; s < seq.length; s++)
2068 // Find the next gap before the end of the visible region boundary
2069 // If lastCol > j, theres a boundary after the gap insertion
2071 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
2073 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
2075 // Theres a space, so break and insert the gap
2080 if (blankColumn <= j)
2082 blankColumn = fixedColumn;
2088 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2090 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2096 * Helper method to add and perform one edit action
2102 * @param systemGenerated
2103 * true if the edit is a 'balancing' delete (or insert) to match a
2104 * user's insert (or delete) in a locked editing region
2106 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2107 int count, boolean systemGenerated)
2110 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2111 av.getAlignment().getGapCharacter());
2112 edit.setSystemGenerated(systemGenerated);
2114 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2118 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2119 * each of the given sequences. The caller should ensure that all sequences
2120 * are gapped in column j.
2124 * @param fixedColumn
2126 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2128 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2130 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2134 * On reentering the panel, stops any scrolling that was started on dragging
2140 public void mouseEntered(MouseEvent e)
2150 * On leaving the panel, if the mouse is being dragged, starts a thread to
2151 * scroll it until the mouse is released (in unwrapped mode only)
2156 public void mouseExited(MouseEvent e)
2158 lastMousePosition = null;
2159 ap.alignFrame.setStatus(" ");
2160 if (av.getWrapAlignment())
2165 if (mouseDragging && scrollThread == null)
2167 startScrolling(e.getPoint());
2172 * Handler for double-click on a position with one or more sequence features.
2173 * Opens the Amend Features dialog to allow feature details to be amended, or
2174 * the feature deleted.
2177 public void mouseClicked(MouseEvent evt)
2179 SequenceGroup sg = null;
2180 MousePos pos = findMousePosition(evt);
2181 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2186 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2188 sg = av.getSelectionGroup();
2189 if (sg != null && sg.getSize() == 1
2190 && sg.getEndRes() - sg.getStartRes() < 2)
2192 av.setSelectionGroup(null);
2195 int column = pos.column;
2198 * find features at the position (if not gapped), or straddling
2199 * the position (if at a gap)
2201 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2202 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2203 .findFeaturesAtColumn(sequence, column + 1);
2205 if (!features.isEmpty())
2208 * highlight the first feature at the position on the alignment
2210 SearchResultsI highlight = new SearchResults();
2211 highlight.addResult(sequence, features.get(0).getBegin(),
2212 features.get(0).getEnd());
2213 seqCanvas.highlightSearchResults(highlight, true);
2216 * open the Amend Features dialog
2218 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2219 false).showDialog();
2225 public void mouseWheelMoved(MouseWheelEvent e)
2228 double wheelRotation = e.getPreciseWheelRotation();
2229 if (wheelRotation > 0)
2231 if (e.isShiftDown())
2233 av.getRanges().scrollRight(true);
2238 av.getRanges().scrollUp(false);
2241 else if (wheelRotation < 0)
2243 if (e.isShiftDown())
2245 av.getRanges().scrollRight(false);
2249 av.getRanges().scrollUp(true);
2254 * update status bar and tooltip for new position
2255 * (need to synthesize a mouse movement to refresh tooltip)
2258 ToolTipManager.sharedInstance().mouseMoved(e);
2267 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2269 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2274 final int res = pos.column;
2275 final int seq = pos.seqIndex;
2277 updateOverviewAndStructs = false;
2279 startWrapBlock = wrappedBlock;
2281 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2283 if ((sequence == null) || (res > sequence.getLength()))
2288 stretchGroup = av.getSelectionGroup();
2290 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2292 stretchGroup = av.getAlignment().findGroup(sequence, res);
2293 if (stretchGroup != null)
2295 // only update the current selection if the popup menu has a group to
2297 av.setSelectionGroup(stretchGroup);
2302 * defer right-mouse click handling to mouseReleased on Windows
2303 * (where isPopupTrigger() will answer true)
2304 * NB isRightMouseButton is also true for Cmd-click on Mac
2306 if (Platform.isWinRightButton(evt))
2311 if (evt.isPopupTrigger()) // Mac: mousePressed
2313 showPopupMenu(evt, pos);
2319 seqCanvas.cursorX = res;
2320 seqCanvas.cursorY = seq;
2321 seqCanvas.repaint();
2325 if (stretchGroup == null)
2327 createStretchGroup(res, sequence);
2330 if (stretchGroup != null)
2332 stretchGroup.addPropertyChangeListener(seqCanvas);
2335 seqCanvas.repaint();
2338 private void createStretchGroup(int res, SequenceI sequence)
2340 // Only if left mouse button do we want to change group sizes
2341 // define a new group here
2342 SequenceGroup sg = new SequenceGroup();
2343 sg.setStartRes(res);
2345 sg.addSequence(sequence, false);
2346 av.setSelectionGroup(sg);
2349 if (av.getConservationSelected())
2351 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2355 if (av.getAbovePIDThreshold())
2357 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2360 // TODO: stretchGroup will always be not null. Is this a merge error ?
2361 // or is there a threading issue here?
2362 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2364 // Edit end res position of selected group
2365 changeEndRes = true;
2367 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2369 // Edit end res position of selected group
2370 changeStartRes = true;
2372 stretchGroup.getWidth();
2377 * Build and show a pop-up menu at the right-click mouse position
2382 void showPopupMenu(MouseEvent evt, MousePos pos)
2384 final int column = pos.column;
2385 final int seq = pos.seqIndex;
2386 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2387 if (sequence != null)
2389 PopupMenu pop = new PopupMenu(ap, sequence, column);
2390 pop.show(this, evt.getX(), evt.getY());
2395 * Update the display after mouse up on a selection or group
2398 * mouse released event details
2400 * true if this event is happening after a mouse drag (rather than a
2403 protected void doMouseReleasedDefineMode(MouseEvent evt,
2406 if (stretchGroup == null)
2411 stretchGroup.removePropertyChangeListener(seqCanvas);
2413 // always do this - annotation has own state
2414 // but defer colourscheme update until hidden sequences are passed in
2415 boolean vischange = stretchGroup.recalcConservation(true);
2416 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2418 if (stretchGroup.cs != null)
2422 stretchGroup.cs.alignmentChanged(stretchGroup,
2423 av.getHiddenRepSequences());
2426 ResidueShaderI groupColourScheme = stretchGroup
2427 .getGroupColourScheme();
2428 String name = stretchGroup.getName();
2429 if (stretchGroup.cs.conservationApplied())
2431 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2433 if (stretchGroup.cs.getThreshold() > 0)
2435 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2438 PaintRefresher.Refresh(this, av.getSequenceSetId());
2439 // TODO: structure colours only need updating if stretchGroup used to or now
2440 // does contain sequences with structure views
2441 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2442 updateOverviewAndStructs = false;
2443 changeEndRes = false;
2444 changeStartRes = false;
2445 stretchGroup = null;
2450 * Resizes the borders of a selection group depending on the direction of
2455 protected void dragStretchGroup(MouseEvent evt)
2457 if (stretchGroup == null)
2462 MousePos pos = findMousePosition(evt);
2463 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2468 int res = pos.column;
2469 int y = pos.seqIndex;
2471 if (wrappedBlock != startWrapBlock)
2476 res = Math.min(res, av.getAlignment().getWidth() - 1);
2478 if (stretchGroup.getEndRes() == res)
2480 // Edit end res position of selected group
2481 changeEndRes = true;
2483 else if (stretchGroup.getStartRes() == res)
2485 // Edit start res position of selected group
2486 changeStartRes = true;
2489 if (res < av.getRanges().getStartRes())
2491 res = av.getRanges().getStartRes();
2496 if (res > (stretchGroup.getStartRes() - 1))
2498 stretchGroup.setEndRes(res);
2499 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2502 else if (changeStartRes)
2504 if (res < (stretchGroup.getEndRes() + 1))
2506 stretchGroup.setStartRes(res);
2507 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2511 int dragDirection = 0;
2517 else if (y < oldSeq)
2522 while ((y != oldSeq) && (oldSeq > -1)
2523 && (y < av.getAlignment().getHeight()))
2525 // This routine ensures we don't skip any sequences, as the
2526 // selection is quite slow.
2527 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2529 oldSeq += dragDirection;
2536 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2538 if (stretchGroup.getSequences(null).contains(nextSeq))
2540 stretchGroup.deleteSequence(seq, false);
2541 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2547 stretchGroup.addSequence(seq, false);
2550 stretchGroup.addSequence(nextSeq, false);
2551 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2560 mouseDragging = true;
2562 if (scrollThread != null)
2564 scrollThread.setMousePosition(evt.getPoint());
2568 * construct a status message showing the range of the selection
2570 StringBuilder status = new StringBuilder(64);
2571 List<SequenceI> seqs = stretchGroup.getSequences();
2572 String name = seqs.get(0).getName();
2573 if (name.length() > 20)
2575 name = name.substring(0, 20);
2577 status.append(name).append(" - ");
2578 name = seqs.get(seqs.size() - 1).getName();
2579 if (name.length() > 20)
2581 name = name.substring(0, 20);
2583 status.append(name).append(" ");
2584 int startRes = stretchGroup.getStartRes();
2585 status.append(" cols ").append(String.valueOf(startRes + 1))
2587 int endRes = stretchGroup.getEndRes();
2588 status.append(String.valueOf(endRes + 1));
2589 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2590 .append(String.valueOf(endRes - startRes + 1)).append(")");
2591 ap.alignFrame.setStatus(status.toString());
2595 * Stops the scroll thread if it is running
2597 void stopScrolling()
2599 if (scrollThread != null)
2601 scrollThread.stopScrolling();
2602 scrollThread = null;
2604 mouseDragging = false;
2608 * Starts a thread to scroll the alignment, towards a given mouse position
2609 * outside the panel bounds, unless the alignment is in wrapped mode
2613 void startScrolling(Point mousePos)
2616 * set this.mouseDragging in case this was called from
2617 * a drag in ScalePanel or AnnotationPanel
2619 mouseDragging = true;
2620 if (!av.getWrapAlignment() && scrollThread == null)
2622 scrollThread = new ScrollThread();
2623 scrollThread.setMousePosition(mousePos);
2624 if (Platform.isJS())
2627 * Javascript - run every 20ms until scrolling stopped
2628 * or reaches the limit of scrollable alignment
2630 Timer t = new Timer(20, new ActionListener()
2633 public void actionPerformed(ActionEvent e)
2635 if (scrollThread != null)
2637 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2638 scrollThread.scrollOnce();
2642 t.addActionListener(new ActionListener()
2645 public void actionPerformed(ActionEvent e)
2647 if (scrollThread == null)
2649 // SeqPanel.stopScrolling called
2659 * Java - run in a new thread
2661 scrollThread.start();
2667 * Performs scrolling of the visible alignment left, right, up or down, until
2668 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2669 * limit of the alignment is reached
2671 class ScrollThread extends Thread
2673 private Point mousePos;
2675 private volatile boolean keepRunning = true;
2680 public ScrollThread()
2682 setName("SeqPanel$ScrollThread");
2686 * Sets the position of the mouse that determines the direction of the
2687 * scroll to perform. If this is called as the mouse moves, scrolling should
2688 * respond accordingly. For example, if the mouse is dragged right, scroll
2689 * right should start; if the drag continues down, scroll down should also
2694 public void setMousePosition(Point p)
2700 * Sets a flag that will cause the thread to exit
2702 public void stopScrolling()
2704 keepRunning = false;
2708 * Scrolls the alignment left or right, and/or up or down, depending on the
2709 * last notified mouse position, until the limit of the alignment is
2710 * reached, or a flag is set to stop the scroll
2717 if (mousePos != null)
2719 keepRunning = scrollOnce();
2724 } catch (Exception ex)
2728 SeqPanel.this.scrollThread = null;
2734 * <li>one row up, if the mouse is above the panel</li>
2735 * <li>one row down, if the mouse is below the panel</li>
2736 * <li>one column left, if the mouse is left of the panel</li>
2737 * <li>one column right, if the mouse is right of the panel</li>
2739 * Answers true if a scroll was performed, false if not - meaning either
2740 * that the mouse position is within the panel, or the edge of the alignment
2743 boolean scrollOnce()
2746 * quit after mouseUp ensures interrupt in JalviewJS
2753 boolean scrolled = false;
2754 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2761 // mouse is above this panel - try scroll up
2762 scrolled = ranges.scrollUp(true);
2764 else if (mousePos.y >= getHeight())
2766 // mouse is below this panel - try scroll down
2767 scrolled = ranges.scrollUp(false);
2771 * scroll left or right
2775 scrolled |= ranges.scrollRight(false);
2777 else if (mousePos.x >= getWidth())
2779 scrolled |= ranges.scrollRight(true);
2786 * modify current selection according to a received message.
2789 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2790 HiddenColumns hidden, SelectionSource source)
2792 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2793 // handles selection messages...
2794 // TODO: extend config options to allow user to control if selections may be
2795 // shared between viewports.
2796 boolean iSentTheSelection = (av == source
2797 || (source instanceof AlignViewport
2798 && ((AlignmentViewport) source).getSequenceSetId()
2799 .equals(av.getSequenceSetId())));
2801 if (iSentTheSelection)
2803 // respond to our own event by updating dependent dialogs
2804 if (ap.getCalculationDialog() != null)
2806 ap.getCalculationDialog().validateCalcTypes();
2812 // process further ?
2813 if (!av.followSelection)
2819 * Ignore the selection if there is one of our own pending.
2821 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2827 * Check for selection in a view of which this one is a dna/protein
2830 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2835 // do we want to thread this ? (contention with seqsel and colsel locks, I
2838 * only copy colsel if there is a real intersection between
2839 * sequence selection and this panel's alignment
2841 boolean repaint = false;
2842 boolean copycolsel = false;
2844 SequenceGroup sgroup = null;
2845 if (seqsel != null && seqsel.getSize() > 0)
2847 if (av.getAlignment() == null)
2849 Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2850 + " ViewId=" + av.getViewId()
2851 + " 's alignment is NULL! returning immediately.");
2854 sgroup = seqsel.intersect(av.getAlignment(),
2855 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2856 if ((sgroup != null && sgroup.getSize() > 0))
2861 if (sgroup != null && sgroup.getSize() > 0)
2863 av.setSelectionGroup(sgroup);
2867 av.setSelectionGroup(null);
2869 av.isSelectionGroupChanged(true);
2874 // the current selection is unset or from a previous message
2875 // so import the new colsel.
2876 if (colsel == null || colsel.isEmpty())
2878 if (av.getColumnSelection() != null)
2880 av.getColumnSelection().clear();
2886 // TODO: shift colSel according to the intersecting sequences
2887 if (av.getColumnSelection() == null)
2889 av.setColumnSelection(new ColumnSelection(colsel));
2893 av.getColumnSelection().setElementsFrom(colsel,
2894 av.getAlignment().getHiddenColumns());
2897 av.isColSelChanged(true);
2901 if (copycolsel && av.hasHiddenColumns()
2902 && (av.getAlignment().getHiddenColumns() == null))
2904 jalview.bin.Console.errPrintln("Bad things");
2906 if (repaint) // always true!
2908 // probably finessing with multiple redraws here
2909 PaintRefresher.Refresh(this, av.getSequenceSetId());
2910 // ap.paintAlignment(false);
2913 // lastly, update dependent dialogs
2914 if (ap.getCalculationDialog() != null)
2916 ap.getCalculationDialog().validateCalcTypes();
2922 * If this panel is a cdna/protein translation view of the selection source,
2923 * tries to map the source selection to a local one, and returns true. Else
2930 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2931 ColumnSelection colsel, HiddenColumns hidden,
2932 SelectionSource source)
2934 if (!(source instanceof AlignViewportI))
2938 final AlignViewportI sourceAv = (AlignViewportI) source;
2939 if (sourceAv.getCodingComplement() != av
2940 && av.getCodingComplement() != sourceAv)
2946 * Map sequence selection
2948 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2949 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2950 av.isSelectionGroupChanged(true);
2953 * Map column selection
2955 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2957 ColumnSelection cs = new ColumnSelection();
2958 HiddenColumns hs = new HiddenColumns();
2959 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2960 av.setColumnSelection(cs);
2961 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2963 // lastly, update any dependent dialogs
2964 if (ap.getCalculationDialog() != null)
2966 ap.getCalculationDialog().validateCalcTypes();
2970 * repaint alignment, and also Overview or Structure
2971 * if hidden column selection has changed
2973 ap.paintAlignment(hiddenChanged, hiddenChanged);
2974 // propagate any selection changes
2975 PaintRefresher.Refresh(ap, av.getSequenceSetId());
2982 * @return null or last search results handled by this panel
2984 public SearchResultsI getLastSearchResults()
2986 return lastSearchResults;