2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import java.awt.BorderLayout;
24 import java.awt.Color;
26 import java.awt.FontMetrics;
27 import java.awt.Point;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.awt.event.MouseEvent;
31 import java.awt.event.MouseListener;
32 import java.awt.event.MouseMotionListener;
33 import java.awt.event.MouseWheelEvent;
34 import java.awt.event.MouseWheelListener;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.List;
39 import javax.swing.JLabel;
40 import javax.swing.JPanel;
41 import javax.swing.JToolTip;
42 import javax.swing.SwingUtilities;
43 import javax.swing.Timer;
44 import javax.swing.ToolTipManager;
46 import jalview.api.AlignViewportI;
47 import jalview.bin.Cache;
48 import jalview.bin.Console;
49 import jalview.commands.EditCommand;
50 import jalview.commands.EditCommand.Action;
51 import jalview.commands.EditCommand.Edit;
52 import jalview.datamodel.AlignmentAnnotation;
53 import jalview.datamodel.AlignmentI;
54 import jalview.datamodel.ColumnSelection;
55 import jalview.datamodel.HiddenColumns;
56 import jalview.datamodel.MappedFeatures;
57 import jalview.datamodel.SearchResultMatchI;
58 import jalview.datamodel.SearchResults;
59 import jalview.datamodel.SearchResultsI;
60 import jalview.datamodel.Sequence;
61 import jalview.datamodel.SequenceFeature;
62 import jalview.datamodel.SequenceGroup;
63 import jalview.datamodel.SequenceI;
64 import jalview.io.SequenceAnnotationReport;
65 import jalview.renderer.ResidueShaderI;
66 import jalview.schemes.ResidueProperties;
67 import jalview.structure.SelectionListener;
68 import jalview.structure.SelectionSource;
69 import jalview.structure.SequenceListener;
70 import jalview.structure.StructureSelectionManager;
71 import jalview.structure.VamsasSource;
72 import jalview.util.Comparison;
73 import jalview.util.MappingUtils;
74 import jalview.util.MessageManager;
75 import jalview.util.Platform;
76 import jalview.viewmodel.AlignmentViewport;
77 import jalview.viewmodel.ViewportRanges;
78 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
84 * @version $Revision: 1.130 $
86 public class SeqPanel extends JPanel
87 implements MouseListener, MouseMotionListener, MouseWheelListener,
88 SequenceListener, SelectionListener
91 * a class that holds computed mouse position
92 * - column of the alignment (0...)
93 * - sequence offset (0...)
94 * - annotation row offset (0...)
95 * where annotation offset is -1 unless the alignment is shown
96 * in wrapped mode, annotations are shown, and the mouse is
97 * over an annnotation row
102 * alignment column position of cursor (0...)
107 * index in alignment of sequence under cursor,
108 * or nearest above if cursor is not over a sequence
113 * index in annotations array of annotation under the cursor
114 * (only possible in wrapped mode with annotations shown),
115 * or -1 if cursor is not over an annotation row
117 final int annotationIndex;
119 MousePos(int col, int seq, int ann)
123 annotationIndex = ann;
126 boolean isOverAnnotation()
128 return annotationIndex != -1;
132 public boolean equals(Object obj)
134 if (obj == null || !(obj instanceof MousePos))
138 MousePos o = (MousePos) obj;
139 boolean b = (column == o.column && seqIndex == o.seqIndex
140 && annotationIndex == o.annotationIndex);
141 // jalview.bin.Console.outPrintln(obj + (b ? "= " : "!= ") + this);
146 * A simple hashCode that ensures that instances that satisfy equals() have
150 public int hashCode()
152 return column + seqIndex + annotationIndex;
156 * toString method for debug output purposes only
159 public String toString()
161 return String.format("c%d:s%d:a%d", column, seqIndex,
167 * Rotation threshold for up/down trackpad/wheelmouse movement to register for
168 * an alignment in wrapped format. Up/down scrolling here results in a large
169 * jump so a larger threshold is appropriate, and reduces unintended up/down
170 * jumps when panning left/right. Should be at least 0.1 and less than 1.0
171 * since some platforms only send a value of 1.0.
173 private static double wrappedVerticalScrollRotationThreshold;
176 * Property name if user needs to change rotation threshold
178 private static String WRAPPEDVERTICALSCROLLROTATIONTHRESHOLDPC = "WRAPPEDVERTICALSCROLLROTATIONTHRESHOLDPC";
181 * Time threshold since last left/right trackpad/wheelmouse scroll for up/down
182 * trackpad/wheelmouse movement to register for an alignment in wrapped
183 * format. This reduces unintended up/down jumps when panning left/right. In
186 private static int wrappedVerticalScrollChangeTimeThreshold;
189 * Property name if user needs to change rotation threshold
191 private static String WRAPPEDVERTICALSCROLLCHANGETIMETHRESHOLD = "WRAPPEDVERTICALSCROLLCHANGETIMETHRESHOLD ";
195 wrappedVerticalScrollRotationThreshold = Cache.getDefault(
196 WRAPPEDVERTICALSCROLLROTATIONTHRESHOLDPC, 50) / 100.0;
197 wrappedVerticalScrollChangeTimeThreshold = Cache
198 .getDefault(WRAPPEDVERTICALSCROLLCHANGETIMETHRESHOLD, 200);
201 private static final int MAX_TOOLTIP_LENGTH = 300;
203 public SeqCanvas seqCanvas;
205 public AlignmentPanel ap;
208 * last position for mouseMoved event
210 private MousePos lastMousePosition;
212 protected int editLastRes;
214 protected int editStartSeq;
216 protected AlignViewport av;
218 ScrollThread scrollThread = null;
220 boolean mouseDragging = false;
222 boolean editingSeqs = false;
224 boolean groupEditing = false;
226 // ////////////////////////////////////////
227 // ///Everything below this is for defining the boundary of the rubberband
228 // ////////////////////////////////////////
231 boolean changeEndSeq = false;
233 boolean changeStartSeq = false;
235 boolean changeEndRes = false;
237 boolean changeStartRes = false;
239 SequenceGroup stretchGroup = null;
241 boolean remove = false;
243 Point lastMousePress;
245 boolean mouseWheelPressed = false;
247 StringBuffer keyboardNo1;
249 StringBuffer keyboardNo2;
251 private final SequenceAnnotationReport seqARep;
254 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
255 * - the tooltip is not set again if unchanged
256 * - this is the tooltip text _before_ formatting as html
258 private String lastTooltip;
261 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
262 * - used to decide where to place the tooltip in getTooltipLocation()
263 * - this is the tooltip text _after_ formatting as html
265 private String lastFormattedTooltip;
267 EditCommand editCommand;
269 StructureSelectionManager ssm;
271 SearchResultsI lastSearchResults;
274 * Creates a new SeqPanel object
279 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
281 seqARep = new SequenceAnnotationReport(true);
282 ToolTipManager.sharedInstance().registerComponent(this);
283 ToolTipManager.sharedInstance().setInitialDelay(0);
284 ToolTipManager.sharedInstance().setDismissDelay(10000);
287 setBackground(Color.white);
289 seqCanvas = new SeqCanvas(alignPanel);
290 setLayout(new BorderLayout());
291 add(seqCanvas, BorderLayout.CENTER);
293 this.ap = alignPanel;
295 if (!viewport.isDataset())
297 addMouseMotionListener(this);
298 addMouseListener(this);
299 addMouseWheelListener(this);
300 ssm = viewport.getStructureSelectionManager();
301 ssm.addStructureViewerListener(this);
302 ssm.addSelectionListener(this);
306 int startWrapBlock = -1;
308 int wrappedBlock = -1;
311 * Computes the column and sequence row (and possibly annotation row when in
312 * wrapped mode) for the given mouse position
314 * Mouse position is not set if in wrapped mode with the cursor either between
315 * sequences, or over the left or right vertical scale.
320 MousePos findMousePosition(MouseEvent evt)
322 int col = findColumn(evt);
327 int charHeight = av.getCharHeight();
328 int alignmentHeight = av.getAlignment().getHeight();
329 if (av.getWrapAlignment())
331 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
332 seqCanvas.getHeight());
335 * yPos modulo height of repeating width
337 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
340 * height of sequences plus space / scale above,
341 * plus gap between sequences and annotations
343 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
344 + alignmentHeight * charHeight
345 + SeqCanvas.SEQS_ANNOTATION_GAP;
346 if (yOffsetPx >= alignmentHeightPixels)
349 * mouse is over annotations; find annotation index, also set
350 * last sequence above (for backwards compatible behaviour)
352 AlignmentAnnotation[] anns = av.getAlignment()
353 .getAlignmentAnnotation();
354 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
355 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
356 seqIndex = alignmentHeight - 1;
361 * mouse is over sequence (or the space above sequences)
363 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
366 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
372 ViewportRanges ranges = av.getRanges();
373 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
374 alignmentHeight - 1);
375 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
378 return new MousePos(col, seqIndex, annIndex);
383 * @return absolute column in alignment nearest to the mouse pointer
385 int findAlignmentColumn(MouseEvent evt)
387 return findNearestColumn(evt, true);
391 * Returns the aligned sequence position (base 0) at the mouse position, or
392 * the closest visible one
394 * Returns -1 if in wrapped mode with the mouse over either left or right
400 int findColumn(MouseEvent evt)
402 return findNearestColumn(evt, false);
406 * @param nearestColumn
407 * when false returns negative values for out of bound positions - -1
408 * for scale left/right, <-1 if far to right
409 * @return nearest absolute column to mouse pointer
411 private int findNearestColumn(MouseEvent evt, boolean nearestColumn)
416 final int startRes = av.getRanges().getStartRes();
417 final int charWidth = av.getCharWidth();
419 if (av.getWrapAlignment())
421 int hgap = av.getCharHeight();
422 if (av.getScaleAboveWrapped())
424 hgap += av.getCharHeight();
427 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
428 + hgap + seqCanvas.getAnnotationHeight();
431 y = Math.max(0, y - hgap);
432 x -= seqCanvas.getLabelWidthWest();
435 // mouse is over left scale
446 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
451 if (x >= cwidth * charWidth)
455 // mouse is over right scale
460 x = cwidth * charWidth - 1;
464 wrappedBlock = y / cHeight;
465 wrappedBlock += startRes / cwidth;
466 // allow for wrapped view scrolled right (possible from Overview)
467 int startOffset = startRes % cwidth;
468 res = wrappedBlock * cwidth + startOffset
469 + Math.min(cwidth - 1, x / charWidth);
474 * make sure we calculate relative to visible alignment,
475 * rather than right-hand gutter
477 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
483 res = (x / charWidth) + startRes;
484 res = Math.min(res, av.getRanges().getEndRes());
488 if (av.hasHiddenColumns())
490 res = av.getAlignment().getHiddenColumns()
491 .visibleToAbsoluteColumn(res);
498 * When all of a sequence of edits are complete, put the resulting edit list
499 * on the history stack (undo list), and reset flags for editing in progress.
505 if (editCommand != null && editCommand.getSize() > 0)
507 ap.alignFrame.addHistoryItem(editCommand);
508 av.firePropertyChange("alignment", null,
509 av.getAlignment().getSequences());
514 * Tidy up come what may...
519 groupEditing = false;
528 seqCanvas.cursorY = getKeyboardNo1() - 1;
529 scrollToVisible(true);
532 void setCursorColumn()
534 seqCanvas.cursorX = getKeyboardNo1() - 1;
535 scrollToVisible(true);
538 void setCursorRowAndColumn()
540 if (keyboardNo2 == null)
542 keyboardNo2 = new StringBuffer();
546 seqCanvas.cursorX = getKeyboardNo1() - 1;
547 seqCanvas.cursorY = getKeyboardNo2() - 1;
548 scrollToVisible(true);
552 void setCursorPosition()
554 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
556 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
557 scrollToVisible(true);
560 void moveCursor(int dx, int dy)
562 moveCursor(dx, dy, false);
565 void moveCursor(int dx, int dy, boolean nextWord)
567 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
571 int maxWidth = av.getAlignment().getWidth();
572 int maxHeight = av.getAlignment().getHeight();
573 SequenceI seqAtRow = av.getAlignment()
574 .getSequenceAt(seqCanvas.cursorY);
575 // look for next gap or residue
576 boolean isGap = Comparison
577 .isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
578 int p = seqCanvas.cursorX, lastP, r = seqCanvas.cursorY, lastR;
594 seqAtRow = av.getAlignment().getSequenceAt(r);
596 p = nextVisible(hidden, maxWidth, p, dx);
597 } while ((dx != 0 ? p != lastP : r != lastR)
598 && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
599 seqCanvas.cursorX = p;
600 seqCanvas.cursorY = r;
604 int maxWidth = av.getAlignment().getWidth();
605 seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX,
607 seqCanvas.cursorY += dy;
609 scrollToVisible(false);
612 private int nextVisible(HiddenColumns hidden, int maxWidth, int original,
615 int newCursorX = original + dx;
616 if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
618 int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
619 int[] region = hidden.getRegionWithEdgeAtRes(visx);
621 if (region != null) // just in case
626 newCursorX = region[1] + 1;
631 newCursorX = region[0] - 1;
635 newCursorX = (newCursorX < 0) ? 0 : newCursorX;
636 if (newCursorX >= maxWidth || !hidden.isVisible(newCursorX))
638 newCursorX = original;
644 * Scroll to make the cursor visible in the viewport.
647 * just jump to the location rather than scrolling
649 void scrollToVisible(boolean jump)
651 if (seqCanvas.cursorX < 0)
653 seqCanvas.cursorX = 0;
655 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
657 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
660 if (seqCanvas.cursorY < 0)
662 seqCanvas.cursorY = 0;
664 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
666 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
671 boolean repaintNeeded = true;
674 // only need to repaint if the viewport did not move, as otherwise it will
676 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
681 if (av.getWrapAlignment())
683 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
684 int x = av.getAlignment().getHiddenColumns()
685 .absoluteToVisibleColumn(seqCanvas.cursorX);
686 av.getRanges().scrollToWrappedVisible(x);
690 av.getRanges().scrollToVisible(seqCanvas.cursorX,
695 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
697 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
698 seqCanvas.cursorX, seqCanvas.cursorY);
707 void setSelectionAreaAtCursor(boolean topLeft)
709 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
711 if (av.getSelectionGroup() != null)
713 SequenceGroup sg = av.getSelectionGroup();
714 // Find the top and bottom of this group
715 int min = av.getAlignment().getHeight(), max = 0;
716 for (int i = 0; i < sg.getSize(); i++)
718 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
733 sg.setStartRes(seqCanvas.cursorX);
734 if (sg.getEndRes() < seqCanvas.cursorX)
736 sg.setEndRes(seqCanvas.cursorX);
739 min = seqCanvas.cursorY;
743 sg.setEndRes(seqCanvas.cursorX);
744 if (sg.getStartRes() > seqCanvas.cursorX)
746 sg.setStartRes(seqCanvas.cursorX);
749 max = seqCanvas.cursorY + 1;
754 // Only the user can do this
755 av.setSelectionGroup(null);
759 // Now add any sequences between min and max
760 sg.getSequences(null).clear();
761 for (int i = min; i < max; i++)
763 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
768 if (av.getSelectionGroup() == null)
770 SequenceGroup sg = new SequenceGroup();
771 sg.setStartRes(seqCanvas.cursorX);
772 sg.setEndRes(seqCanvas.cursorX);
773 sg.addSequence(sequence, false);
774 av.setSelectionGroup(sg);
777 ap.paintAlignment(false, false);
781 void insertGapAtCursor(boolean group)
783 groupEditing = group;
784 editStartSeq = seqCanvas.cursorY;
785 editLastRes = seqCanvas.cursorX;
786 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
790 void deleteGapAtCursor(boolean group)
792 groupEditing = group;
793 editStartSeq = seqCanvas.cursorY;
794 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
795 editSequence(false, false, seqCanvas.cursorX);
799 void insertNucAtCursor(boolean group, String nuc)
801 // TODO not called - delete?
802 groupEditing = group;
803 editStartSeq = seqCanvas.cursorY;
804 editLastRes = seqCanvas.cursorX;
805 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
809 void numberPressed(char value)
811 if (keyboardNo1 == null)
813 keyboardNo1 = new StringBuffer();
816 if (keyboardNo2 != null)
818 keyboardNo2.append(value);
822 keyboardNo1.append(value);
830 if (keyboardNo1 != null)
832 int value = Integer.parseInt(keyboardNo1.toString());
836 } catch (Exception x)
847 if (keyboardNo2 != null)
849 int value = Integer.parseInt(keyboardNo2.toString());
853 } catch (Exception x)
867 public void mouseReleased(MouseEvent evt)
869 MousePos pos = findMousePosition(evt);
870 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
875 boolean didDrag = mouseDragging; // did we come here after a drag
876 mouseDragging = false;
877 mouseWheelPressed = false;
879 if (evt.isPopupTrigger()) // Windows: mouseReleased
881 showPopupMenu(evt, pos);
892 doMouseReleasedDefineMode(evt, didDrag);
903 public void mousePressed(MouseEvent evt)
905 lastMousePress = evt.getPoint();
906 MousePos pos = findMousePosition(evt);
907 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
912 if (SwingUtilities.isMiddleMouseButton(evt))
914 mouseWheelPressed = true;
918 boolean isControlDown = Platform.isControlDown(evt);
919 if (evt.isShiftDown() || isControlDown)
929 doMousePressedDefineMode(evt, pos);
933 int seq = pos.seqIndex;
934 int res = pos.column;
936 if ((seq < av.getAlignment().getHeight())
937 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
954 public void mouseOverSequence(SequenceI sequence, int index, int pos)
956 String tmp = sequence.hashCode() + " " + index + " " + pos;
958 if (lastMessage == null || !lastMessage.equals(tmp))
960 // jalview.bin.Console.errPrintln("mouseOver Sequence: "+tmp);
961 ssm.mouseOverSequence(sequence, index, pos, av);
967 * Highlight the mapped region described by the search results object (unless
968 * unchanged). This supports highlight of protein while mousing over linked
969 * cDNA and vice versa. The status bar is also updated to show the location of
970 * the start of the highlighted region.
973 public String highlightSequence(SearchResultsI results)
975 if (results == null || results.equals(lastSearchResults))
979 lastSearchResults = results;
981 boolean wasScrolled = false;
983 if (av.isFollowHighlight())
985 // don't allow highlight of protein/cDNA to also scroll a complementary
986 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
987 // over residue to change abruptly, causing highlighted residue in panel 2
988 // to change, causing a scroll in panel 1 etc)
989 ap.setToScrollComplementPanel(false);
990 wasScrolled = ap.scrollToPosition(results);
993 seqCanvas.revalidate();
995 ap.setToScrollComplementPanel(true);
998 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
999 if (seqCanvas.highlightSearchResults(results, fastPaint))
1001 setStatusMessage(results);
1003 return results.isEmpty() ? null : getHighlightInfo(results);
1007 * temporary hack: answers a message suitable to show on structure hover
1008 * label. This is normally null. It is a peptide variation description if
1010 * <li>results are a single residue in a protein alignment</li>
1011 * <li>there is a mapping to a coding sequence (codon)</li>
1012 * <li>there are one or more SNP variant features on the codon</li>
1014 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
1019 private String getHighlightInfo(SearchResultsI results)
1022 * ideally, just find mapped CDS (as we don't care about render style here);
1023 * for now, go via split frame complement's FeatureRenderer
1025 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
1026 if (complement == null)
1030 AlignFrame af = Desktop.getAlignFrameFor(complement);
1031 FeatureRendererModel fr2 = af.getFeatureRenderer();
1033 List<SearchResultMatchI> matches = results.getResults();
1034 int j = matches.size();
1035 List<String> infos = new ArrayList<>();
1036 for (int i = 0; i < j; i++)
1038 SearchResultMatchI match = matches.get(i);
1039 int pos = match.getStart();
1040 if (pos == match.getEnd())
1042 SequenceI seq = match.getSequence();
1043 SequenceI ds = seq.getDatasetSequence() == null ? seq
1044 : seq.getDatasetSequence();
1045 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(ds, pos);
1048 for (SequenceFeature sf : mf.features)
1050 String pv = mf.findProteinVariants(sf);
1051 if (pv.length() > 0 && !infos.contains(pv))
1060 if (infos.isEmpty())
1064 StringBuilder sb = new StringBuilder();
1065 for (String info : infos)
1067 if (sb.length() > 0)
1073 return sb.toString();
1077 public VamsasSource getVamsasSource()
1079 return this.ap == null ? null : this.ap.av;
1083 public void updateColours(SequenceI seq, int index)
1085 jalview.bin.Console.outPrintln("update the seqPanel colours");
1090 * Action on mouse movement is to update the status bar to show the current
1091 * sequence position, and (if features are shown) to show any features at the
1092 * position in a tooltip. Does nothing if the mouse move does not change
1098 public void mouseMoved(MouseEvent evt)
1102 // This is because MacOSX creates a mouseMoved
1103 // If control is down, other platforms will not.
1107 final MousePos mousePos = findMousePosition(evt);
1108 if (mousePos.equals(lastMousePosition))
1111 * just a pixel move without change of 'cell'
1113 moveTooltip = false;
1117 lastMousePosition = mousePos;
1119 if (mousePos.isOverAnnotation())
1121 mouseMovedOverAnnotation(mousePos);
1124 final int seq = mousePos.seqIndex;
1126 final int column = mousePos.column;
1127 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1129 lastMousePosition = null;
1130 setToolTipText(null);
1132 lastFormattedTooltip = null;
1133 ap.alignFrame.setStatus("");
1137 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1139 if (column >= sequence.getLength())
1145 * set status bar message, returning residue position in sequence
1147 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1148 final int pos = setStatusMessage(sequence, column, seq);
1149 if (ssm != null && !isGapped)
1151 mouseOverSequence(sequence, column, pos);
1154 StringBuilder tooltipText = new StringBuilder(64);
1156 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1159 for (int g = 0; g < groups.length; g++)
1161 if (groups[g].getStartRes() <= column
1162 && groups[g].getEndRes() >= column)
1164 if (!groups[g].getName().startsWith("JTreeGroup")
1165 && !groups[g].getName().startsWith("JGroup"))
1167 tooltipText.append(groups[g].getName());
1170 if (groups[g].getDescription() != null)
1172 tooltipText.append(": " + groups[g].getDescription());
1179 * add any features at the position to the tooltip; if over a gap, only
1180 * add features that straddle the gap (pos may be the residue before or
1183 int unshownFeatures = 0;
1184 if (av.isShowSequenceFeatures())
1186 List<SequenceFeature> features = ap.getFeatureRenderer()
1187 .findFeaturesAtColumn(sequence, column + 1);
1188 unshownFeatures = seqARep.appendFeatures(tooltipText, pos, features,
1189 this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
1192 * add features in CDS/protein complement at the corresponding
1193 * position if configured to do so
1195 if (av.isShowComplementFeatures())
1197 if (!Comparison.isGap(sequence.getCharAt(column)))
1199 AlignViewportI complement = ap.getAlignViewport()
1200 .getCodingComplement();
1201 AlignFrame af = Desktop.getAlignFrameFor(complement);
1202 FeatureRendererModel fr2 = af.getFeatureRenderer();
1203 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1207 unshownFeatures += seqARep.appendFeatures(tooltipText, pos, mf,
1208 fr2, MAX_TOOLTIP_LENGTH);
1213 if (tooltipText.length() == 0) // nothing added
1215 setToolTipText(null);
1220 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1222 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1223 tooltipText.append("...");
1225 if (unshownFeatures > 0)
1227 tooltipText.append("<br/>").append("... ").append("<i>")
1228 .append(MessageManager.formatMessage(
1229 "label.features_not_shown", unshownFeatures))
1232 String textString = tooltipText.toString();
1233 if (!textString.equals(lastTooltip))
1235 lastTooltip = textString;
1236 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true, textString);
1237 setToolTipText(lastFormattedTooltip);
1243 * When the view is in wrapped mode, and the mouse is over an annotation row,
1244 * shows the corresponding tooltip and status message (if any)
1249 protected void mouseMovedOverAnnotation(MousePos pos)
1251 final int column = pos.column;
1252 final int rowIndex = pos.annotationIndex;
1254 // TODO - get yOffset for annotation, too
1255 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1260 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1262 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1264 if (tooltip == null ? tooltip != lastTooltip
1265 : !tooltip.equals(lastTooltip))
1267 lastTooltip = tooltip;
1268 lastFormattedTooltip = tooltip == null ? null
1269 : JvSwingUtils.wrapTooltip(true, tooltip);
1270 setToolTipText(lastFormattedTooltip);
1273 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1274 anns[rowIndex], 0, av);
1275 ap.alignFrame.setStatus(msg);
1279 * if Shift key is held down while moving the mouse,
1280 * the tooltip location is not changed once shown
1282 private Point lastTooltipLocation = null;
1285 * this flag is false for pixel moves within a residue,
1286 * to reduce tooltip flicker
1288 private boolean moveTooltip = true;
1291 * a dummy tooltip used to estimate where to position tooltips
1293 private JToolTip tempTip = new JLabel().createToolTip();
1298 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1301 public Point getToolTipLocation(MouseEvent event)
1305 if (lastTooltip == null || !moveTooltip)
1310 if (lastTooltipLocation != null && event.isShiftDown())
1312 return lastTooltipLocation;
1315 int x = event.getX();
1316 int y = event.getY();
1319 tempTip.setTipText(lastFormattedTooltip);
1320 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1322 // was x += (w - x < 200) ? -(w / 2) : 5;
1323 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1324 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1326 return lastTooltipLocation = p;
1330 * set when the current UI interaction has resulted in a change that requires
1331 * shading in overviews and structures to be recalculated. this could be
1332 * changed to a something more expressive that indicates what actually has
1333 * changed, so selective redraws can be applied (ie. only structures, only
1336 private boolean updateOverviewAndStructs = false; // TODO: refactor to
1340 * set if av.getSelectionGroup() refers to a group that is defined on the
1341 * alignment view, rather than a transient selection
1343 // private boolean editingDefinedGroup = false; // TODO: refactor to
1344 // avcontroller or viewModel
1347 * Sets the status message in alignment panel, showing the sequence number
1348 * (index) and id, and residue and residue position if not at a gap, for the
1349 * given sequence and column position. Returns the residue position returned
1350 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1351 * if at a gapped position.
1354 * aligned sequence object
1358 * index of sequence in alignment
1359 * @return sequence position of residue at column, or adjacent residue if at a
1362 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1364 char sequenceChar = sequence.getCharAt(column);
1365 int pos = sequence.findPosition(column);
1366 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1372 * Builds the status message for the current cursor location and writes it to
1373 * the status bar, for example
1376 * Sequence 3 ID: FER1_SOLLC
1377 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1378 * Sequence 5 ID: FER1_PEA Residue: B (3)
1379 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1384 * sequence position in the alignment (1..)
1385 * @param sequenceChar
1386 * the character under the cursor
1388 * the sequence residue position (if not over a gap)
1390 protected void setStatusMessage(String seqName, int seqIndex,
1391 char sequenceChar, int residuePos)
1393 StringBuilder text = new StringBuilder(32);
1396 * Sequence number (if known), and sequence name.
1398 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1399 text.append("Sequence").append(seqno).append(" ID: ").append(seqName);
1401 String residue = null;
1404 * Try to translate the display character to residue name (null for gap).
1406 boolean isGapped = Comparison.isGap(sequenceChar);
1410 boolean nucleotide = av.getAlignment().isNucleotide();
1411 String displayChar = String.valueOf(sequenceChar);
1414 residue = ResidueProperties.nucleotideName.get(displayChar);
1418 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1419 : ("*".equals(displayChar) ? "STOP"
1420 : ResidueProperties.aa2Triplet.get(displayChar));
1422 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1423 .append(": ").append(residue == null ? displayChar : residue);
1425 text.append(" (").append(Integer.toString(residuePos)).append(")");
1427 ap.alignFrame.setStatus(text.toString());
1431 * Set the status bar message to highlight the first matched position in
1436 private void setStatusMessage(SearchResultsI results)
1438 AlignmentI al = this.av.getAlignment();
1439 int sequenceIndex = al.findIndex(results);
1440 if (sequenceIndex == -1)
1444 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1445 SequenceI ds = alignedSeq.getDatasetSequence();
1446 for (SearchResultMatchI m : results.getResults())
1448 SequenceI seq = m.getSequence();
1449 if (seq.getDatasetSequence() != null)
1451 seq = seq.getDatasetSequence();
1456 int start = m.getStart();
1457 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1458 seq.getCharAt(start - 1), start);
1468 public void mouseDragged(MouseEvent evt)
1470 MousePos pos = findMousePosition(evt);
1471 if (pos.isOverAnnotation() || pos.column == -1)
1476 if (mouseWheelPressed)
1478 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1479 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1481 int oldWidth = av.getCharWidth();
1483 // Which is bigger, left-right or up-down?
1484 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1485 .abs(evt.getX() - lastMousePress.getX()))
1488 * on drag up or down, decrement or increment font size
1490 int fontSize = av.font.getSize();
1491 boolean fontChanged = false;
1493 if (evt.getY() < lastMousePress.getY())
1498 else if (evt.getY() > lastMousePress.getY())
1511 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1513 av.setFont(newFont, true);
1514 av.setCharWidth(oldWidth);
1518 ap.av.getCodingComplement().setFont(newFont, true);
1519 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1520 .getSplitViewContainer();
1521 splitFrame.adjustLayout();
1522 splitFrame.repaint();
1529 * on drag left or right, decrement or increment character width
1532 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1534 newWidth = av.getCharWidth() - 1;
1535 av.setCharWidth(newWidth);
1537 else if (evt.getX() > lastMousePress.getX())
1539 newWidth = av.getCharWidth() + 1;
1540 av.setCharWidth(newWidth);
1544 ap.paintAlignment(false, false);
1548 * need to ensure newWidth is set on cdna, regardless of which
1549 * panel the mouse drag happened in; protein will compute its
1550 * character width as 1:1 or 3:1
1552 av.getCodingComplement().setCharWidth(newWidth);
1553 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1554 .getSplitViewContainer();
1555 splitFrame.adjustLayout();
1556 splitFrame.repaint();
1561 FontMetrics fm = getFontMetrics(av.getFont());
1562 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1564 lastMousePress = evt.getPoint();
1571 dragStretchGroup(evt);
1575 int res = pos.column;
1582 if ((editLastRes == -1) || (editLastRes == res))
1587 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1589 // dragLeft, delete gap
1590 editSequence(false, false, res);
1594 editSequence(true, false, res);
1597 mouseDragging = true;
1598 if (scrollThread != null)
1600 scrollThread.setMousePosition(evt.getPoint());
1605 * Edits the sequence to insert or delete one or more gaps, in response to a
1606 * mouse drag or cursor mode command. The number of inserts/deletes may be
1607 * specified with the cursor command, or else depends on the mouse event
1608 * (normally one column, but potentially more for a fast mouse drag).
1610 * Delete gaps is limited to the number of gaps left of the cursor position
1611 * (mouse drag), or at or right of the cursor position (cursor mode).
1613 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1614 * the current selection group.
1616 * In locked editing mode (with a selection group present), inserts/deletions
1617 * within the selection group are limited to its boundaries (and edits outside
1618 * the group stop at its border).
1621 * true to insert gaps, false to delete gaps
1623 * (unused parameter)
1625 * the column at which to perform the action; the number of columns
1626 * affected depends on <code>this.editLastRes</code> (cursor column
1629 synchronized void editSequence(boolean insertGap, boolean editSeq,
1633 int fixedRight = -1;
1634 boolean fixedColumns = false;
1635 SequenceGroup sg = av.getSelectionGroup();
1637 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1639 // No group, but the sequence may represent a group
1640 if (!groupEditing && av.hasHiddenRows())
1642 if (av.isHiddenRepSequence(seq))
1644 sg = av.getRepresentedSequences(seq);
1645 groupEditing = true;
1649 StringBuilder message = new StringBuilder(64); // for status bar
1652 * make a name for the edit action, for
1653 * status bar message and Undo/Redo menu
1655 String label = null;
1658 message.append("Edit group:");
1659 label = MessageManager.getString("action.edit_group");
1663 message.append("Edit sequence: " + seq.getName());
1664 label = seq.getName();
1665 if (label.length() > 10)
1667 label = label.substring(0, 10);
1669 label = MessageManager.formatMessage("label.edit_params",
1675 * initialise the edit command if there is not
1676 * already one being extended
1678 if (editCommand == null)
1680 editCommand = new EditCommand(label);
1685 message.append(" insert ");
1689 message.append(" delete ");
1692 message.append(Math.abs(startres - editLastRes) + " gaps.");
1693 ap.alignFrame.setStatus(message.toString());
1696 * is there a selection group containing the sequence being edited?
1697 * if so the boundary of the group is the limit of the edit
1698 * (but the edit may be inside or outside the selection group)
1700 boolean inSelectionGroup = sg != null
1701 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1702 if (groupEditing || inSelectionGroup)
1704 fixedColumns = true;
1706 // sg might be null as the user may only see 1 sequence,
1707 // but the sequence represents a group
1710 if (!av.isHiddenRepSequence(seq))
1715 sg = av.getRepresentedSequences(seq);
1718 fixedLeft = sg.getStartRes();
1719 fixedRight = sg.getEndRes();
1721 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1722 || (startres >= fixedLeft && editLastRes < fixedLeft)
1723 || (startres > fixedRight && editLastRes <= fixedRight)
1724 || (startres <= fixedRight && editLastRes > fixedRight))
1730 if (fixedLeft > startres)
1732 fixedRight = fixedLeft - 1;
1735 else if (fixedRight < startres)
1737 fixedLeft = fixedRight;
1742 if (av.hasHiddenColumns())
1744 fixedColumns = true;
1745 int y1 = av.getAlignment().getHiddenColumns()
1746 .getNextHiddenBoundary(true, startres);
1747 int y2 = av.getAlignment().getHiddenColumns()
1748 .getNextHiddenBoundary(false, startres);
1750 if ((insertGap && startres > y1 && editLastRes < y1)
1751 || (!insertGap && startres < y2 && editLastRes > y2))
1757 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1758 // Selection spans a hidden region
1759 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1767 fixedRight = y2 - 1;
1772 boolean success = doEditSequence(insertGap, editSeq, startres,
1773 fixedRight, fixedColumns, sg);
1776 * report what actually happened (might be less than
1777 * what was requested), by inspecting the edit commands added
1779 String msg = getEditStatusMessage(editCommand);
1780 ap.alignFrame.setStatus(msg == null ? " " : msg);
1786 editLastRes = startres;
1787 seqCanvas.repaint();
1791 * A helper method that performs the requested editing to insert or delete
1792 * gaps (if possible). Answers true if the edit was successful, false if could
1793 * only be performed in part or not at all. Failure may occur in 'locked edit'
1794 * mode, when an insertion requires a matching gapped position (or column) to
1795 * delete, and deletion requires an adjacent gapped position (or column) to
1799 * true if inserting gap(s), false if deleting
1801 * (unused parameter, currently always false)
1803 * the column at which to perform the edit
1805 * fixed right boundary column of a locked edit (within or to the
1806 * left of a selection group)
1807 * @param fixedColumns
1808 * true if this is a locked edit
1810 * the sequence group (if group edit is being performed)
1813 protected boolean doEditSequence(final boolean insertGap,
1814 final boolean editSeq, final int startres, int fixedRight,
1815 final boolean fixedColumns, final SequenceGroup sg)
1817 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1818 SequenceI[] seqs = new SequenceI[] { seq };
1822 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1823 int g, groupSize = vseqs.size();
1824 SequenceI[] groupSeqs = new SequenceI[groupSize];
1825 for (g = 0; g < groupSeqs.length; g++)
1827 groupSeqs[g] = vseqs.get(g);
1833 // If the user has selected the whole sequence, and is dragging to
1834 // the right, we can still extend the alignment and selectionGroup
1835 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1836 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1839 av.getAlignment().getWidth() + startres - editLastRes);
1840 fixedRight = sg.getEndRes();
1843 // Is it valid with fixed columns??
1844 // Find the next gap before the end
1845 // of the visible region boundary
1846 boolean blank = false;
1847 for (; fixedRight > editLastRes; fixedRight--)
1851 for (g = 0; g < groupSize; g++)
1853 for (int j = 0; j < startres - editLastRes; j++)
1855 if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1870 if (sg.getSize() == av.getAlignment().getHeight())
1872 if ((av.hasHiddenColumns()
1873 && startres < av.getAlignment().getHiddenColumns()
1874 .getNextHiddenBoundary(false, startres)))
1879 int alWidth = av.getAlignment().getWidth();
1880 if (av.hasHiddenRows())
1882 int hwidth = av.getAlignment().getHiddenSequences()
1884 if (hwidth > alWidth)
1889 // We can still insert gaps if the selectionGroup
1890 // contains all the sequences
1891 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1892 fixedRight = alWidth + startres - editLastRes;
1902 else if (!insertGap)
1904 // / Are we able to delete?
1905 // ie are all columns blank?
1907 for (g = 0; g < groupSize; g++)
1909 for (int j = startres; j < editLastRes; j++)
1911 if (groupSeqs[g].getLength() <= j)
1916 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1918 // Not a gap, block edit not valid
1927 // dragging to the right
1928 if (fixedColumns && fixedRight != -1)
1930 for (int j = editLastRes; j < startres; j++)
1932 insertGap(j, groupSeqs, fixedRight);
1937 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1938 startres - editLastRes, false);
1943 // dragging to the left
1944 if (fixedColumns && fixedRight != -1)
1946 for (int j = editLastRes; j > startres; j--)
1948 deleteChar(startres, groupSeqs, fixedRight);
1953 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1954 editLastRes - startres, false);
1961 * editing a single sequence
1965 // dragging to the right
1966 if (fixedColumns && fixedRight != -1)
1968 for (int j = editLastRes; j < startres; j++)
1970 if (!insertGap(j, seqs, fixedRight))
1973 * e.g. cursor mode command specified
1974 * more inserts than are possible
1982 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1983 startres - editLastRes, false);
1990 // dragging to the left
1991 if (fixedColumns && fixedRight != -1)
1993 for (int j = editLastRes; j > startres; j--)
1995 if (!Comparison.isGap(seq.getCharAt(startres)))
1999 deleteChar(startres, seqs, fixedRight);
2004 // could be a keyboard edit trying to delete none gaps
2006 for (int m = startres; m < editLastRes; m++)
2008 if (!Comparison.isGap(seq.getCharAt(m)))
2016 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
2021 {// insertGap==false AND editSeq==TRUE;
2022 if (fixedColumns && fixedRight != -1)
2024 for (int j = editLastRes; j < startres; j++)
2026 insertGap(j, seqs, fixedRight);
2031 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
2032 startres - editLastRes, false);
2042 * Constructs an informative status bar message while dragging to insert or
2043 * delete gaps. Answers null if inserts and deletes cancel out.
2045 * @param editCommand
2046 * a command containing the list of individual edits
2049 protected static String getEditStatusMessage(EditCommand editCommand)
2051 if (editCommand == null)
2057 * add any inserts, and subtract any deletes,
2058 * not counting those auto-inserted when doing a 'locked edit'
2059 * (so only counting edits 'under the cursor')
2062 for (Edit cmd : editCommand.getEdits())
2064 if (!cmd.isSystemGenerated())
2066 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
2074 * inserts and deletes cancel out
2079 String msgKey = count > 1 ? "label.insert_gaps"
2080 : (count == 1 ? "label.insert_gap"
2081 : (count == -1 ? "label.delete_gap"
2082 : "label.delete_gaps"));
2083 count = Math.abs(count);
2085 return MessageManager.formatMessage(msgKey, String.valueOf(count));
2089 * Inserts one gap at column j, deleting the right-most gapped column up to
2090 * (and including) fixedColumn. Returns true if the edit is successful, false
2091 * if no blank column is available to allow the insertion to be balanced by a
2096 * @param fixedColumn
2099 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
2101 int blankColumn = fixedColumn;
2102 for (int s = 0; s < seq.length; s++)
2104 // Find the next gap before the end of the visible region boundary
2105 // If lastCol > j, theres a boundary after the gap insertion
2107 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
2109 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
2111 // Theres a space, so break and insert the gap
2116 if (blankColumn <= j)
2118 blankColumn = fixedColumn;
2124 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2126 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2132 * Helper method to add and perform one edit action
2138 * @param systemGenerated
2139 * true if the edit is a 'balancing' delete (or insert) to match a
2140 * user's insert (or delete) in a locked editing region
2142 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2143 int count, boolean systemGenerated)
2146 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2147 av.getAlignment().getGapCharacter());
2148 edit.setSystemGenerated(systemGenerated);
2150 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2154 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2155 * each of the given sequences. The caller should ensure that all sequences
2156 * are gapped in column j.
2160 * @param fixedColumn
2162 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2164 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2166 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2170 * On reentering the panel, stops any scrolling that was started on dragging
2176 public void mouseEntered(MouseEvent e)
2186 * On leaving the panel, if the mouse is being dragged, starts a thread to
2187 * scroll it until the mouse is released (in unwrapped mode only)
2192 public void mouseExited(MouseEvent e)
2194 lastMousePosition = null;
2195 ap.alignFrame.setStatus(" ");
2196 if (av.getWrapAlignment())
2201 if (mouseDragging && scrollThread == null)
2203 startScrolling(e.getPoint());
2208 * Handler for double-click on a position with one or more sequence features.
2209 * Opens the Amend Features dialog to allow feature details to be amended, or
2210 * the feature deleted.
2213 public void mouseClicked(MouseEvent evt)
2215 SequenceGroup sg = null;
2216 MousePos pos = findMousePosition(evt);
2217 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2222 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2224 sg = av.getSelectionGroup();
2225 if (sg != null && sg.getSize() == 1
2226 && sg.getEndRes() - sg.getStartRes() < 2)
2228 av.setSelectionGroup(null);
2231 int column = pos.column;
2234 * find features at the position (if not gapped), or straddling
2235 * the position (if at a gap)
2237 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2238 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2239 .findFeaturesAtColumn(sequence, column + 1);
2241 if (!features.isEmpty())
2244 * highlight the first feature at the position on the alignment
2246 SearchResultsI highlight = new SearchResults();
2247 highlight.addResult(sequence, features.get(0).getBegin(),
2248 features.get(0).getEnd());
2249 seqCanvas.highlightSearchResults(highlight, true);
2252 * open the Amend Features dialog
2254 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2255 false).showDialog();
2261 * recorded time of last left/right mousewheel/trackpad scroll in wrapped mode
2263 private long lastLeftRightWrappedScrollTime = 0;
2266 * Responds to a mouse wheel movement by scrolling the alignment
2268 * <li>left or right, if the shift key is down, else up or down</li>
2269 * <li>right (or down) if the reported mouse movement is positive</li>
2270 * <li>left (or up) if the reported mouse movement is negative</li>
2272 * Note that this method may also be fired by scrolling with a gesture on a
2276 public void mouseWheelMoved(MouseWheelEvent e)
2279 double preciseWheelRotation = e.getPreciseWheelRotation();
2280 int wheelRotation = e.getWheelRotation();
2281 if (wheelRotation == 0 && Math.abs(preciseWheelRotation) > 0.1)
2283 // this is one of -1, 0 ,+1 for <0, ==0, >0
2284 wheelRotation = (int) Math.signum(preciseWheelRotation);
2288 * scroll more for large (fast) mouse movements
2290 int size = Math.abs(wheelRotation);
2292 if (wheelRotation > 0)
2294 if (e.isShiftDown())
2298 * stop trying to scroll right when limit is reached (saves
2299 * expensive calls to Alignment.getWidth())
2301 if (!ap.isScrolledFullyRight())
2303 av.getRanges().scrollRight(true, size);
2305 this.lastLeftRightWrappedScrollTime = System.currentTimeMillis();
2312 // apply a more definite threshold for up and down scrolling in wrap
2313 // format (either not a wrapped alignment, or BOTH time since last
2314 // left/right scroll is above threshold AND trackpad/mousewheel movement
2315 // is above threshold)
2316 if (!ap.getAlignViewport().getWrapAlignment() || (Math.abs(
2317 preciseWheelRotation) > wrappedVerticalScrollRotationThreshold
2318 && System.currentTimeMillis()
2319 - lastLeftRightWrappedScrollTime > wrappedVerticalScrollChangeTimeThreshold))
2323 if (!av.getRanges().scrollUp(false))
2331 else if (wheelRotation < 0)
2333 if (e.isShiftDown())
2336 * scroll left if not already at start
2338 if (av.getRanges().getStartRes() > 0)
2340 av.getRanges().scrollRight(false, size);
2342 this.lastLeftRightWrappedScrollTime = System.currentTimeMillis();
2349 // apply a more definite threshold for up and down scrolling in wrap
2350 // format (either not a wrapped alignment, or BOTH time since last
2351 // left/right scroll is above threshold AND trackpad/mousewheel movement
2352 // is above threshold)
2353 if (!ap.getAlignViewport().getWrapAlignment() || (Math.abs(
2354 preciseWheelRotation) > wrappedVerticalScrollRotationThreshold
2355 && System.currentTimeMillis()
2356 - lastLeftRightWrappedScrollTime > wrappedVerticalScrollChangeTimeThreshold))
2360 if (!av.getRanges().scrollUp(true))
2370 * update status bar and tooltip for new position
2371 * (need to synthesize a mouse movement to refresh tooltip)
2374 ToolTipManager.sharedInstance().mouseMoved(e);
2383 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2385 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2390 final int res = pos.column;
2391 final int seq = pos.seqIndex;
2393 updateOverviewAndStructs = false;
2395 startWrapBlock = wrappedBlock;
2397 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2399 if ((sequence == null) || (res > sequence.getLength()))
2404 stretchGroup = av.getSelectionGroup();
2406 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2408 stretchGroup = av.getAlignment().findGroup(sequence, res);
2409 if (stretchGroup != null)
2411 // only update the current selection if the popup menu has a group to
2413 av.setSelectionGroup(stretchGroup);
2418 * defer right-mouse click handling to mouseReleased on Windows
2419 * (where isPopupTrigger() will answer true)
2420 * NB isRightMouseButton is also true for Cmd-click on Mac
2422 if (Platform.isWinRightButton(evt))
2427 if (evt.isPopupTrigger()) // Mac: mousePressed
2429 showPopupMenu(evt, pos);
2435 seqCanvas.cursorX = res;
2436 seqCanvas.cursorY = seq;
2437 seqCanvas.repaint();
2441 if (stretchGroup == null)
2443 createStretchGroup(res, sequence);
2446 if (stretchGroup != null)
2448 stretchGroup.addPropertyChangeListener(seqCanvas);
2451 seqCanvas.repaint();
2454 private void createStretchGroup(int res, SequenceI sequence)
2456 // Only if left mouse button do we want to change group sizes
2457 // define a new group here
2458 SequenceGroup sg = new SequenceGroup();
2459 sg.setStartRes(res);
2461 sg.addSequence(sequence, false);
2462 av.setSelectionGroup(sg);
2465 if (av.getConservationSelected())
2467 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2471 if (av.getAbovePIDThreshold())
2473 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2476 // TODO: stretchGroup will always be not null. Is this a merge error ?
2477 // or is there a threading issue here?
2478 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2480 // Edit end res position of selected group
2481 changeEndRes = true;
2483 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2485 // Edit end res position of selected group
2486 changeStartRes = true;
2488 stretchGroup.getWidth();
2493 * Build and show a pop-up menu at the right-click mouse position
2498 void showPopupMenu(MouseEvent evt, MousePos pos)
2500 final int column = pos.column;
2501 final int seq = pos.seqIndex;
2502 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2503 if (sequence != null)
2505 PopupMenu pop = new PopupMenu(ap, sequence, column);
2506 pop.show(this, evt.getX(), evt.getY());
2511 * Update the display after mouse up on a selection or group
2514 * mouse released event details
2516 * true if this event is happening after a mouse drag (rather than a
2519 protected void doMouseReleasedDefineMode(MouseEvent evt,
2522 if (stretchGroup == null)
2527 stretchGroup.removePropertyChangeListener(seqCanvas);
2529 // always do this - annotation has own state
2530 // but defer colourscheme update until hidden sequences are passed in
2531 boolean vischange = stretchGroup.recalcConservation(true);
2532 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2534 if (stretchGroup.cs != null)
2538 stretchGroup.cs.alignmentChanged(stretchGroup,
2539 av.getHiddenRepSequences());
2542 ResidueShaderI groupColourScheme = stretchGroup
2543 .getGroupColourScheme();
2544 String name = stretchGroup.getName();
2545 if (stretchGroup.cs.conservationApplied())
2547 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2549 if (stretchGroup.cs.getThreshold() > 0)
2551 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2554 PaintRefresher.Refresh(this, av.getSequenceSetId());
2555 // TODO: structure colours only need updating if stretchGroup used to or now
2556 // does contain sequences with structure views
2557 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2558 updateOverviewAndStructs = false;
2559 changeEndRes = false;
2560 changeStartRes = false;
2561 stretchGroup = null;
2566 * Resizes the borders of a selection group depending on the direction of
2571 protected void dragStretchGroup(MouseEvent evt)
2573 if (stretchGroup == null)
2578 MousePos pos = findMousePosition(evt);
2579 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2584 int res = pos.column;
2585 int y = pos.seqIndex;
2587 if (wrappedBlock != startWrapBlock)
2592 res = Math.min(res, av.getAlignment().getWidth() - 1);
2594 if (stretchGroup.getEndRes() == res)
2596 // Edit end res position of selected group
2597 changeEndRes = true;
2599 else if (stretchGroup.getStartRes() == res)
2601 // Edit start res position of selected group
2602 changeStartRes = true;
2605 if (res < av.getRanges().getStartRes())
2607 res = av.getRanges().getStartRes();
2612 if (res > (stretchGroup.getStartRes() - 1))
2614 stretchGroup.setEndRes(res);
2615 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2618 else if (changeStartRes)
2620 if (res < (stretchGroup.getEndRes() + 1))
2622 stretchGroup.setStartRes(res);
2623 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2627 int dragDirection = 0;
2633 else if (y < oldSeq)
2638 while ((y != oldSeq) && (oldSeq > -1)
2639 && (y < av.getAlignment().getHeight()))
2641 // This routine ensures we don't skip any sequences, as the
2642 // selection is quite slow.
2643 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2645 oldSeq += dragDirection;
2652 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2654 if (stretchGroup.getSequences(null).contains(nextSeq))
2656 stretchGroup.deleteSequence(seq, false);
2657 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2663 stretchGroup.addSequence(seq, false);
2666 stretchGroup.addSequence(nextSeq, false);
2667 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2676 mouseDragging = true;
2678 if (scrollThread != null)
2680 scrollThread.setMousePosition(evt.getPoint());
2684 * construct a status message showing the range of the selection
2686 StringBuilder status = new StringBuilder(64);
2687 List<SequenceI> seqs = stretchGroup.getSequences();
2688 String name = seqs.get(0).getName();
2689 if (name.length() > 20)
2691 name = name.substring(0, 20);
2693 status.append(name).append(" - ");
2694 name = seqs.get(seqs.size() - 1).getName();
2695 if (name.length() > 20)
2697 name = name.substring(0, 20);
2699 status.append(name).append(" ");
2700 int startRes = stretchGroup.getStartRes();
2701 status.append(" cols ").append(String.valueOf(startRes + 1))
2703 int endRes = stretchGroup.getEndRes();
2704 status.append(String.valueOf(endRes + 1));
2705 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2706 .append(String.valueOf(endRes - startRes + 1)).append(")");
2707 ap.alignFrame.setStatus(status.toString());
2711 * Stops the scroll thread if it is running
2713 void stopScrolling()
2715 if (scrollThread != null)
2717 scrollThread.stopScrolling();
2718 scrollThread = null;
2720 mouseDragging = false;
2724 * Starts a thread to scroll the alignment, towards a given mouse position
2725 * outside the panel bounds, unless the alignment is in wrapped mode
2729 void startScrolling(Point mousePos)
2732 * set this.mouseDragging in case this was called from
2733 * a drag in ScalePanel or AnnotationPanel
2735 mouseDragging = true;
2736 if (!av.getWrapAlignment() && scrollThread == null)
2738 scrollThread = new ScrollThread();
2739 scrollThread.setMousePosition(mousePos);
2740 if (Platform.isJS())
2743 * Javascript - run every 20ms until scrolling stopped
2744 * or reaches the limit of scrollable alignment
2746 Timer t = new Timer(20, new ActionListener()
2749 public void actionPerformed(ActionEvent e)
2751 if (scrollThread != null)
2753 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2754 scrollThread.scrollOnce();
2758 t.addActionListener(new ActionListener()
2761 public void actionPerformed(ActionEvent e)
2763 if (scrollThread == null)
2765 // SeqPanel.stopScrolling called
2775 * Java - run in a new thread
2777 scrollThread.start();
2783 * Performs scrolling of the visible alignment left, right, up or down, until
2784 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2785 * limit of the alignment is reached
2787 class ScrollThread extends Thread
2789 private Point mousePos;
2791 private volatile boolean keepRunning = true;
2796 public ScrollThread()
2798 setName("SeqPanel$ScrollThread");
2802 * Sets the position of the mouse that determines the direction of the
2803 * scroll to perform. If this is called as the mouse moves, scrolling should
2804 * respond accordingly. For example, if the mouse is dragged right, scroll
2805 * right should start; if the drag continues down, scroll down should also
2810 public void setMousePosition(Point p)
2816 * Sets a flag that will cause the thread to exit
2818 public void stopScrolling()
2820 keepRunning = false;
2824 * Scrolls the alignment left or right, and/or up or down, depending on the
2825 * last notified mouse position, until the limit of the alignment is
2826 * reached, or a flag is set to stop the scroll
2833 if (mousePos != null)
2835 keepRunning = scrollOnce();
2840 } catch (Exception ex)
2844 SeqPanel.this.scrollThread = null;
2850 * <li>one row up, if the mouse is above the panel</li>
2851 * <li>one row down, if the mouse is below the panel</li>
2852 * <li>one column left, if the mouse is left of the panel</li>
2853 * <li>one column right, if the mouse is right of the panel</li>
2855 * Answers true if a scroll was performed, false if not - meaning either
2856 * that the mouse position is within the panel, or the edge of the alignment
2859 boolean scrollOnce()
2862 * quit after mouseUp ensures interrupt in JalviewJS
2869 boolean scrolled = false;
2870 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2877 // mouse is above this panel - try scroll up
2878 scrolled = ranges.scrollUp(true);
2880 else if (mousePos.y >= getHeight())
2882 // mouse is below this panel - try scroll down
2883 scrolled = ranges.scrollUp(false);
2887 * scroll left or right
2891 scrolled |= ranges.scrollRight(false);
2893 else if (mousePos.x >= getWidth())
2895 scrolled |= ranges.scrollRight(true);
2902 * modify current selection according to a received message.
2905 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2906 HiddenColumns hidden, SelectionSource source)
2908 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2909 // handles selection messages...
2910 // TODO: extend config options to allow user to control if selections may be
2911 // shared between viewports.
2912 boolean iSentTheSelection = (av == source
2913 || (source instanceof AlignViewport
2914 && ((AlignmentViewport) source).getSequenceSetId()
2915 .equals(av.getSequenceSetId())));
2917 if (iSentTheSelection)
2919 // respond to our own event by updating dependent dialogs
2920 if (ap.getCalculationDialog() != null)
2922 ap.getCalculationDialog().validateCalcTypes();
2928 // process further ?
2929 if (!av.followSelection)
2935 * Ignore the selection if there is one of our own pending.
2937 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2943 * Check for selection in a view of which this one is a dna/protein
2946 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2951 // do we want to thread this ? (contention with seqsel and colsel locks, I
2954 * only copy colsel if there is a real intersection between
2955 * sequence selection and this panel's alignment
2957 boolean repaint = false;
2958 boolean copycolsel = false;
2960 SequenceGroup sgroup = null;
2961 if (seqsel != null && seqsel.getSize() > 0)
2963 if (av.getAlignment() == null)
2965 Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2966 + " ViewId=" + av.getViewId()
2967 + " 's alignment is NULL! returning immediately.");
2970 sgroup = seqsel.intersect(av.getAlignment(),
2971 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2972 if ((sgroup != null && sgroup.getSize() > 0))
2977 if (sgroup != null && sgroup.getSize() > 0)
2979 av.setSelectionGroup(sgroup);
2983 av.setSelectionGroup(null);
2985 av.isSelectionGroupChanged(true);
2990 // the current selection is unset or from a previous message
2991 // so import the new colsel.
2992 if (colsel == null || colsel.isEmpty())
2994 if (av.getColumnSelection() != null)
2996 av.getColumnSelection().clear();
3002 // TODO: shift colSel according to the intersecting sequences
3003 if (av.getColumnSelection() == null)
3005 av.setColumnSelection(new ColumnSelection(colsel));
3009 av.getColumnSelection().setElementsFrom(colsel,
3010 av.getAlignment().getHiddenColumns());
3013 av.isColSelChanged(true);
3017 if (copycolsel && av.hasHiddenColumns()
3018 && (av.getAlignment().getHiddenColumns() == null))
3020 jalview.bin.Console.errPrintln("Bad things");
3022 if (repaint) // always true!
3024 // probably finessing with multiple redraws here
3025 PaintRefresher.Refresh(this, av.getSequenceSetId());
3026 // ap.paintAlignment(false);
3029 // lastly, update dependent dialogs
3030 if (ap.getCalculationDialog() != null)
3032 ap.getCalculationDialog().validateCalcTypes();
3038 * If this panel is a cdna/protein translation view of the selection source,
3039 * tries to map the source selection to a local one, and returns true. Else
3046 protected boolean selectionFromTranslation(SequenceGroup seqsel,
3047 ColumnSelection colsel, HiddenColumns hidden,
3048 SelectionSource source)
3050 if (!(source instanceof AlignViewportI))
3054 final AlignViewportI sourceAv = (AlignViewportI) source;
3055 if (sourceAv.getCodingComplement() != av
3056 && av.getCodingComplement() != sourceAv)
3062 * Map sequence selection
3064 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
3065 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
3066 av.isSelectionGroupChanged(true);
3069 * Map column selection
3071 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
3073 ColumnSelection cs = new ColumnSelection();
3074 HiddenColumns hs = new HiddenColumns();
3075 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
3076 av.setColumnSelection(cs);
3077 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
3079 // lastly, update any dependent dialogs
3080 if (ap.getCalculationDialog() != null)
3082 ap.getCalculationDialog().validateCalcTypes();
3086 * repaint alignment, and also Overview or Structure
3087 * if hidden column selection has changed
3089 ap.paintAlignment(hiddenChanged, hiddenChanged);
3090 // propagate any selection changes
3091 PaintRefresher.Refresh(ap, av.getSequenceSetId());
3098 * @return null or last search results handled by this panel
3100 public SearchResultsI getLastSearchResults()
3102 return lastSearchResults;