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.commands.EditCommand;
49 import jalview.commands.EditCommand.Action;
50 import jalview.commands.EditCommand.Edit;
51 import jalview.datamodel.AlignmentAnnotation;
52 import jalview.datamodel.AlignmentI;
53 import jalview.datamodel.ColumnSelection;
54 import jalview.datamodel.HiddenColumns;
55 import jalview.datamodel.MappedFeatures;
56 import jalview.datamodel.SearchResultMatchI;
57 import jalview.datamodel.SearchResults;
58 import jalview.datamodel.SearchResultsI;
59 import jalview.datamodel.Sequence;
60 import jalview.datamodel.SequenceFeature;
61 import jalview.datamodel.SequenceGroup;
62 import jalview.datamodel.SequenceI;
63 import jalview.io.SequenceAnnotationReport;
64 import jalview.renderer.ResidueShaderI;
65 import jalview.schemes.ResidueProperties;
66 import jalview.structure.SelectionListener;
67 import jalview.structure.SelectionSource;
68 import jalview.structure.SequenceListener;
69 import jalview.structure.StructureSelectionManager;
70 import jalview.structure.VamsasSource;
71 import jalview.util.Comparison;
72 import jalview.util.MappingUtils;
73 import jalview.util.MessageManager;
74 import jalview.util.Platform;
75 import jalview.viewmodel.AlignmentViewport;
76 import jalview.viewmodel.ViewportRanges;
77 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
83 * @version $Revision: 1.130 $
85 public class SeqPanel extends JPanel
86 implements MouseListener, MouseMotionListener, MouseWheelListener,
87 SequenceListener, SelectionListener
90 * a class that holds computed mouse position
91 * - column of the alignment (0...)
92 * - sequence offset (0...)
93 * - annotation row offset (0...)
94 * where annotation offset is -1 unless the alignment is shown
95 * in wrapped mode, annotations are shown, and the mouse is
96 * over an annnotation row
101 * alignment column position of cursor (0...)
106 * index in alignment of sequence under cursor,
107 * or nearest above if cursor is not over a sequence
112 * index in annotations array of annotation under the cursor
113 * (only possible in wrapped mode with annotations shown),
114 * or -1 if cursor is not over an annotation row
116 final int annotationIndex;
118 MousePos(int col, int seq, int ann)
122 annotationIndex = ann;
125 boolean isOverAnnotation()
127 return annotationIndex != -1;
131 public boolean equals(Object obj)
133 if (obj == null || !(obj instanceof MousePos))
137 MousePos o = (MousePos) obj;
138 boolean b = (column == o.column && seqIndex == o.seqIndex
139 && annotationIndex == o.annotationIndex);
140 // System.out.println(obj + (b ? "= " : "!= ") + this);
145 * A simple hashCode that ensures that instances that satisfy equals() have
149 public int hashCode()
151 return column + seqIndex + annotationIndex;
155 * toString method for debug output purposes only
158 public String toString()
160 return String.format("c%d:s%d:a%d", column, seqIndex,
165 private static final int MAX_TOOLTIP_LENGTH = 300;
167 public SeqCanvas seqCanvas;
169 public AlignmentPanel ap;
172 * last position for mouseMoved event
174 private MousePos lastMousePosition;
176 protected int editLastRes;
178 protected int editStartSeq;
180 protected AlignViewport av;
182 ScrollThread scrollThread = null;
184 boolean mouseDragging = false;
186 boolean editingSeqs = false;
188 boolean groupEditing = false;
190 // ////////////////////////////////////////
191 // ///Everything below this is for defining the boundary of the rubberband
192 // ////////////////////////////////////////
195 boolean changeEndSeq = false;
197 boolean changeStartSeq = false;
199 boolean changeEndRes = false;
201 boolean changeStartRes = false;
203 SequenceGroup stretchGroup = null;
205 boolean remove = false;
207 Point lastMousePress;
209 boolean mouseWheelPressed = false;
211 StringBuffer keyboardNo1;
213 StringBuffer keyboardNo2;
215 private final SequenceAnnotationReport seqARep;
218 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
219 * - the tooltip is not set again if unchanged
220 * - this is the tooltip text _before_ formatting as html
222 private String lastTooltip;
225 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
226 * - used to decide where to place the tooltip in getTooltipLocation()
227 * - this is the tooltip text _after_ formatting as html
229 private String lastFormattedTooltip;
231 EditCommand editCommand;
233 StructureSelectionManager ssm;
235 SearchResultsI lastSearchResults;
238 * Creates a new SeqPanel object
243 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
245 seqARep = new SequenceAnnotationReport(true);
246 ToolTipManager.sharedInstance().registerComponent(this);
247 ToolTipManager.sharedInstance().setInitialDelay(0);
248 ToolTipManager.sharedInstance().setDismissDelay(10000);
252 setBackground(Color.white);
254 seqCanvas = new SeqCanvas(alignPanel);
255 setLayout(new BorderLayout());
256 add(seqCanvas, BorderLayout.CENTER);
258 this.ap = alignPanel;
260 if (!viewport.isDataset())
262 addMouseMotionListener(this);
263 addMouseListener(this);
264 addMouseWheelListener(this);
265 ssm = viewport.getStructureSelectionManager();
266 ssm.addStructureViewerListener(this);
267 ssm.addSelectionListener(this);
271 int startWrapBlock = -1;
273 int wrappedBlock = -1;
276 * Computes the column and sequence row (and possibly annotation row when in
277 * wrapped mode) for the given mouse position
279 * Mouse position is not set if in wrapped mode with the cursor either between
280 * sequences, or over the left or right vertical scale.
285 MousePos findMousePosition(MouseEvent evt)
287 int col = findColumn(evt);
292 int charHeight = av.getCharHeight();
293 int alignmentHeight = av.getAlignment().getHeight();
294 if (av.getWrapAlignment())
296 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
297 seqCanvas.getHeight());
300 * yPos modulo height of repeating width
302 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
305 * height of sequences plus space / scale above,
306 * plus gap between sequences and annotations
308 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
309 + alignmentHeight * charHeight
310 + SeqCanvas.SEQS_ANNOTATION_GAP;
311 if (yOffsetPx >= alignmentHeightPixels)
314 * mouse is over annotations; find annotation index, also set
315 * last sequence above (for backwards compatible behaviour)
317 AlignmentAnnotation[] anns = av.getAlignment()
318 .getAlignmentAnnotation();
319 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
320 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
321 seqIndex = alignmentHeight - 1;
326 * mouse is over sequence (or the space above sequences)
328 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
331 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
337 ViewportRanges ranges = av.getRanges();
338 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
339 alignmentHeight - 1);
340 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
343 return new MousePos(col, seqIndex, annIndex);
346 * Returns the aligned sequence position (base 0) at the mouse position, or
347 * the closest visible one
349 * Returns -1 if in wrapped mode with the mouse over either left or right
355 int findColumn(MouseEvent evt)
360 final int startRes = av.getRanges().getStartRes();
361 final int charWidth = av.getCharWidth();
363 if (av.getWrapAlignment())
365 int hgap = av.getCharHeight();
366 if (av.getScaleAboveWrapped())
368 hgap += av.getCharHeight();
371 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
372 + hgap + seqCanvas.getAnnotationHeight();
375 y = Math.max(0, y - hgap);
376 x -= seqCanvas.getLabelWidthWest();
379 // mouse is over left scale
383 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
388 if (x >= cwidth * charWidth)
390 // mouse is over right scale
394 wrappedBlock = y / cHeight;
395 wrappedBlock += startRes / cwidth;
396 // allow for wrapped view scrolled right (possible from Overview)
397 int startOffset = startRes % cwidth;
398 res = wrappedBlock * cwidth + startOffset
399 + Math.min(cwidth - 1, x / charWidth);
404 * make sure we calculate relative to visible alignment,
405 * rather than right-hand gutter
407 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
408 res = (x / charWidth) + startRes;
409 res = Math.min(res, av.getRanges().getEndRes());
412 if (av.hasHiddenColumns())
414 res = av.getAlignment().getHiddenColumns()
415 .visibleToAbsoluteColumn(res);
422 * When all of a sequence of edits are complete, put the resulting edit list
423 * on the history stack (undo list), and reset flags for editing in progress.
429 if (editCommand != null && editCommand.getSize() > 0)
431 ap.alignFrame.addHistoryItem(editCommand);
432 av.firePropertyChange("alignment", null,
433 av.getAlignment().getSequences());
438 * Tidy up come what may...
443 groupEditing = false;
452 seqCanvas.cursorY = getKeyboardNo1() - 1;
453 scrollToVisible(true);
456 void setCursorColumn()
458 seqCanvas.cursorX = getKeyboardNo1() - 1;
459 scrollToVisible(true);
462 void setCursorRowAndColumn()
464 if (keyboardNo2 == null)
466 keyboardNo2 = new StringBuffer();
470 seqCanvas.cursorX = getKeyboardNo1() - 1;
471 seqCanvas.cursorY = getKeyboardNo2() - 1;
472 scrollToVisible(true);
476 void setCursorPosition()
478 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
480 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
481 scrollToVisible(true);
484 void moveCursor(int dx, int dy)
486 seqCanvas.cursorX += dx;
487 seqCanvas.cursorY += dy;
489 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
491 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
493 int original = seqCanvas.cursorX - dx;
494 int maxWidth = av.getAlignment().getWidth();
496 if (!hidden.isVisible(seqCanvas.cursorX))
498 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
499 int[] region = hidden.getRegionWithEdgeAtRes(visx);
501 if (region != null) // just in case
506 seqCanvas.cursorX = region[1] + 1;
511 seqCanvas.cursorX = region[0] - 1;
514 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
517 if (seqCanvas.cursorX >= maxWidth
518 || !hidden.isVisible(seqCanvas.cursorX))
520 seqCanvas.cursorX = original;
524 scrollToVisible(false);
528 * Scroll to make the cursor visible in the viewport.
531 * just jump to the location rather than scrolling
533 void scrollToVisible(boolean jump)
535 if (seqCanvas.cursorX < 0)
537 seqCanvas.cursorX = 0;
539 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
541 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
544 if (seqCanvas.cursorY < 0)
546 seqCanvas.cursorY = 0;
548 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
550 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
555 boolean repaintNeeded = true;
558 // only need to repaint if the viewport did not move, as otherwise it will
560 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
565 if (av.getWrapAlignment())
567 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
568 int x = av.getAlignment().getHiddenColumns()
569 .absoluteToVisibleColumn(seqCanvas.cursorX);
570 av.getRanges().scrollToWrappedVisible(x);
574 av.getRanges().scrollToVisible(seqCanvas.cursorX,
579 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
581 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
582 seqCanvas.cursorX, seqCanvas.cursorY);
592 void setSelectionAreaAtCursor(boolean topLeft)
594 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
596 if (av.getSelectionGroup() != null)
598 SequenceGroup sg = av.getSelectionGroup();
599 // Find the top and bottom of this group
600 int min = av.getAlignment().getHeight(), max = 0;
601 for (int i = 0; i < sg.getSize(); i++)
603 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
618 sg.setStartRes(seqCanvas.cursorX);
619 if (sg.getEndRes() < seqCanvas.cursorX)
621 sg.setEndRes(seqCanvas.cursorX);
624 min = seqCanvas.cursorY;
628 sg.setEndRes(seqCanvas.cursorX);
629 if (sg.getStartRes() > seqCanvas.cursorX)
631 sg.setStartRes(seqCanvas.cursorX);
634 max = seqCanvas.cursorY + 1;
639 // Only the user can do this
640 av.setSelectionGroup(null);
644 // Now add any sequences between min and max
645 sg.getSequences(null).clear();
646 for (int i = min; i < max; i++)
648 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
653 if (av.getSelectionGroup() == null)
655 SequenceGroup sg = new SequenceGroup();
656 sg.setStartRes(seqCanvas.cursorX);
657 sg.setEndRes(seqCanvas.cursorX);
658 sg.addSequence(sequence, false);
659 av.setSelectionGroup(sg);
662 ap.paintAlignment(false, false);
666 void insertGapAtCursor(boolean group)
668 groupEditing = group;
669 editStartSeq = seqCanvas.cursorY;
670 editLastRes = seqCanvas.cursorX;
671 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
675 void deleteGapAtCursor(boolean group)
677 groupEditing = group;
678 editStartSeq = seqCanvas.cursorY;
679 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
680 editSequence(false, false, seqCanvas.cursorX);
684 void insertNucAtCursor(boolean group, String nuc)
686 // TODO not called - delete?
687 groupEditing = group;
688 editStartSeq = seqCanvas.cursorY;
689 editLastRes = seqCanvas.cursorX;
690 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
694 void numberPressed(char value)
696 if (keyboardNo1 == null)
698 keyboardNo1 = new StringBuffer();
701 if (keyboardNo2 != null)
703 keyboardNo2.append(value);
707 keyboardNo1.append(value);
715 if (keyboardNo1 != null)
717 int value = Integer.parseInt(keyboardNo1.toString());
721 } catch (Exception x)
732 if (keyboardNo2 != null)
734 int value = Integer.parseInt(keyboardNo2.toString());
738 } catch (Exception x)
752 public void mouseReleased(MouseEvent evt)
754 MousePos pos = findMousePosition(evt);
755 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
760 boolean didDrag = mouseDragging; // did we come here after a drag
761 mouseDragging = false;
762 mouseWheelPressed = false;
764 if (evt.isPopupTrigger()) // Windows: mouseReleased
766 showPopupMenu(evt, pos);
777 doMouseReleasedDefineMode(evt, didDrag);
788 public void mousePressed(MouseEvent evt)
790 lastMousePress = evt.getPoint();
791 MousePos pos = findMousePosition(evt);
792 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
797 if (SwingUtilities.isMiddleMouseButton(evt))
799 mouseWheelPressed = true;
803 boolean isControlDown = Platform.isControlDown(evt);
804 if (evt.isShiftDown() || isControlDown)
814 doMousePressedDefineMode(evt, pos);
818 int seq = pos.seqIndex;
819 int res = pos.column;
821 if ((seq < av.getAlignment().getHeight())
822 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
839 public void mouseOverSequence(SequenceI sequence, int index, int pos)
841 String tmp = sequence.hashCode() + " " + index + " " + pos;
843 if (lastMessage == null || !lastMessage.equals(tmp))
845 // System.err.println("mouseOver Sequence: "+tmp);
846 ssm.mouseOverSequence(sequence, index, pos, av);
852 * Highlight the mapped region described by the search results object (unless
853 * unchanged). This supports highlight of protein while mousing over linked
854 * cDNA and vice versa. The status bar is also updated to show the location of
855 * the start of the highlighted region.
858 public String highlightSequence(SearchResultsI results)
860 if (results == null || results.equals(lastSearchResults))
864 lastSearchResults = results;
866 boolean wasScrolled = false;
868 if (av.isFollowHighlight())
870 // don't allow highlight of protein/cDNA to also scroll a complementary
871 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
872 // over residue to change abruptly, causing highlighted residue in panel 2
873 // to change, causing a scroll in panel 1 etc)
874 ap.setToScrollComplementPanel(false);
875 wasScrolled = ap.scrollToPosition(results);
878 seqCanvas.revalidate();
880 ap.setToScrollComplementPanel(true);
883 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
884 if (seqCanvas.highlightSearchResults(results, fastPaint))
886 setStatusMessage(results);
888 return results.isEmpty() ? null : getHighlightInfo(results);
892 * temporary hack: answers a message suitable to show on structure hover
893 * label. This is normally null. It is a peptide variation description if
895 * <li>results are a single residue in a protein alignment</li>
896 * <li>there is a mapping to a coding sequence (codon)</li>
897 * <li>there are one or more SNP variant features on the codon</li>
899 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
904 private String getHighlightInfo(SearchResultsI results)
907 * ideally, just find mapped CDS (as we don't care about render style here);
908 * for now, go via split frame complement's FeatureRenderer
910 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
911 if (complement == null)
915 AlignFrame af = Desktop.getAlignFrameFor(complement);
916 FeatureRendererModel fr2 = af.getFeatureRenderer();
918 List<SearchResultMatchI> matches = results.getResults();
919 int j = matches.size();
920 List<String> infos = new ArrayList<>();
921 for (int i = 0; i < j; i++)
923 SearchResultMatchI match = matches.get(i);
924 int pos = match.getStart();
925 if (pos == match.getEnd())
927 SequenceI seq = match.getSequence();
928 SequenceI ds = seq.getDatasetSequence() == null ? seq
929 : seq.getDatasetSequence();
930 MappedFeatures mf = fr2
931 .findComplementFeaturesAtResidue(ds, pos);
934 for (SequenceFeature sf : mf.features)
936 String pv = mf.findProteinVariants(sf);
937 if (pv.length() > 0 && !infos.contains(pv))
950 StringBuilder sb = new StringBuilder();
951 for (String info : infos)
959 return sb.toString();
963 public VamsasSource getVamsasSource()
965 return this.ap == null ? null : this.ap.av;
969 public void updateColours(SequenceI seq, int index)
971 System.out.println("update the seqPanel colours");
976 * Action on mouse movement is to update the status bar to show the current
977 * sequence position, and (if features are shown) to show any features at the
978 * position in a tooltip. Does nothing if the mouse move does not change
984 public void mouseMoved(MouseEvent evt)
988 // This is because MacOSX creates a mouseMoved
989 // If control is down, other platforms will not.
993 final MousePos mousePos = findMousePosition(evt);
994 if (mousePos.equals(lastMousePosition))
997 * just a pixel move without change of 'cell'
1003 lastMousePosition = mousePos;
1005 if (mousePos.isOverAnnotation())
1007 mouseMovedOverAnnotation(mousePos);
1010 final int seq = mousePos.seqIndex;
1012 final int column = mousePos.column;
1013 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1015 lastMousePosition = null;
1016 setToolTipText(null);
1018 lastFormattedTooltip = null;
1019 ap.alignFrame.setStatus("");
1023 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1025 if (column >= sequence.getLength())
1031 * set status bar message, returning residue position in sequence
1033 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1034 final int pos = setStatusMessage(sequence, column, seq);
1035 if (ssm != null && !isGapped)
1037 mouseOverSequence(sequence, column, pos);
1040 StringBuilder tooltipText = new StringBuilder(64);
1042 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1045 for (int g = 0; g < groups.length; g++)
1047 if (groups[g].getStartRes() <= column
1048 && groups[g].getEndRes() >= column)
1050 if (!groups[g].getName().startsWith("JTreeGroup")
1051 && !groups[g].getName().startsWith("JGroup"))
1053 tooltipText.append(groups[g].getName());
1056 if (groups[g].getDescription() != null)
1058 tooltipText.append(": " + groups[g].getDescription());
1065 * add any features at the position to the tooltip; if over a gap, only
1066 * add features that straddle the gap (pos may be the residue before or
1069 int unshownFeatures = 0;
1070 if (av.isShowSequenceFeatures())
1072 List<SequenceFeature> features = ap.getFeatureRenderer()
1073 .findFeaturesAtColumn(sequence, column + 1);
1074 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1075 features, this.ap.getSeqPanel().seqCanvas.fr,
1076 MAX_TOOLTIP_LENGTH);
1079 * add features in CDS/protein complement at the corresponding
1080 * position if configured to do so
1082 if (av.isShowComplementFeatures())
1084 if (!Comparison.isGap(sequence.getCharAt(column)))
1086 AlignViewportI complement = ap.getAlignViewport()
1087 .getCodingComplement();
1088 AlignFrame af = Desktop.getAlignFrameFor(complement);
1089 FeatureRendererModel fr2 = af.getFeatureRenderer();
1090 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1094 unshownFeatures += seqARep.appendFeatures(tooltipText,
1095 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1100 if (tooltipText.length() == 0) // nothing added
1102 setToolTipText(null);
1107 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1109 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1110 tooltipText.append("...");
1112 if (unshownFeatures > 0)
1114 tooltipText.append("<br/>").append("... ").append("<i>")
1115 .append(MessageManager.formatMessage(
1116 "label.features_not_shown", unshownFeatures))
1119 String textString = tooltipText.toString();
1120 if (!textString.equals(lastTooltip))
1122 lastTooltip = textString;
1123 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1125 setToolTipText(lastFormattedTooltip);
1131 * When the view is in wrapped mode, and the mouse is over an annotation row,
1132 * shows the corresponding tooltip and status message (if any)
1137 protected void mouseMovedOverAnnotation(MousePos pos)
1139 final int column = pos.column;
1140 final int rowIndex = pos.annotationIndex;
1142 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1147 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1149 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1151 if (!tooltip.equals(lastTooltip))
1153 lastTooltip = tooltip;
1154 lastFormattedTooltip = tooltip == null ? null
1155 : JvSwingUtils.wrapTooltip(true, tooltip);
1156 setToolTipText(lastFormattedTooltip);
1159 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1161 ap.alignFrame.setStatus(msg);
1165 * if Shift key is held down while moving the mouse,
1166 * the tooltip location is not changed once shown
1168 private Point lastTooltipLocation = null;
1171 * this flag is false for pixel moves within a residue,
1172 * to reduce tooltip flicker
1174 private boolean moveTooltip = true;
1177 * a dummy tooltip used to estimate where to position tooltips
1179 private JToolTip tempTip = new JLabel().createToolTip();
1184 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1187 public Point getToolTipLocation(MouseEvent event)
1191 if (lastTooltip == null || !moveTooltip)
1196 if (lastTooltipLocation != null && event.isShiftDown())
1198 return lastTooltipLocation;
1201 int x = event.getX();
1202 int y = event.getY();
1205 tempTip.setTipText(lastFormattedTooltip);
1206 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1208 // was x += (w - x < 200) ? -(w / 2) : 5;
1209 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1210 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1212 return lastTooltipLocation = p;
1216 * set when the current UI interaction has resulted in a change that requires
1217 * shading in overviews and structures to be recalculated. this could be
1218 * changed to a something more expressive that indicates what actually has
1219 * changed, so selective redraws can be applied (ie. only structures, only
1222 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1225 * set if av.getSelectionGroup() refers to a group that is defined on the
1226 * alignment view, rather than a transient selection
1228 // private boolean editingDefinedGroup = false; // TODO: refactor to
1229 // avcontroller or viewModel
1232 * Sets the status message in alignment panel, showing the sequence number
1233 * (index) and id, and residue and residue position if not at a gap, for the
1234 * given sequence and column position. Returns the residue position returned
1235 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1236 * if at a gapped position.
1239 * aligned sequence object
1243 * index of sequence in alignment
1244 * @return sequence position of residue at column, or adjacent residue if at a
1247 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1249 char sequenceChar = sequence.getCharAt(column);
1250 int pos = sequence.findPosition(column);
1251 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1257 * Builds the status message for the current cursor location and writes it to
1258 * the status bar, for example
1261 * Sequence 3 ID: FER1_SOLLC
1262 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1263 * Sequence 5 ID: FER1_PEA Residue: B (3)
1264 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1269 * sequence position in the alignment (1..)
1270 * @param sequenceChar
1271 * the character under the cursor
1273 * the sequence residue position (if not over a gap)
1275 protected void setStatusMessage(String seqName, int seqIndex,
1276 char sequenceChar, int residuePos)
1278 StringBuilder text = new StringBuilder(32);
1281 * Sequence number (if known), and sequence name.
1283 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1284 text.append("Sequence").append(seqno).append(" ID: ")
1287 String residue = null;
1290 * Try to translate the display character to residue name (null for gap).
1292 boolean isGapped = Comparison.isGap(sequenceChar);
1296 boolean nucleotide = av.getAlignment().isNucleotide();
1297 String displayChar = String.valueOf(sequenceChar);
1300 residue = ResidueProperties.nucleotideName.get(displayChar);
1304 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1305 : ("*".equals(displayChar) ? "STOP"
1306 : ResidueProperties.aa2Triplet.get(displayChar));
1308 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1309 .append(": ").append(residue == null ? displayChar : residue);
1311 text.append(" (").append(Integer.toString(residuePos)).append(")");
1313 ap.alignFrame.setStatus(text.toString());
1317 * Set the status bar message to highlight the first matched position in
1322 private void setStatusMessage(SearchResultsI results)
1324 AlignmentI al = this.av.getAlignment();
1325 int sequenceIndex = al.findIndex(results);
1326 if (sequenceIndex == -1)
1330 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1331 SequenceI ds = alignedSeq.getDatasetSequence();
1332 for (SearchResultMatchI m : results.getResults())
1334 SequenceI seq = m.getSequence();
1335 if (seq.getDatasetSequence() != null)
1337 seq = seq.getDatasetSequence();
1342 int start = m.getStart();
1343 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1344 seq.getCharAt(start - 1), start);
1354 public void mouseDragged(MouseEvent evt)
1356 MousePos pos = findMousePosition(evt);
1357 if (pos.isOverAnnotation() || pos.column == -1)
1362 if (mouseWheelPressed)
1364 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1365 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1367 int oldWidth = av.getCharWidth();
1369 // Which is bigger, left-right or up-down?
1370 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1371 .abs(evt.getX() - lastMousePress.getX()))
1374 * on drag up or down, decrement or increment font size
1376 int fontSize = av.font.getSize();
1377 boolean fontChanged = false;
1379 if (evt.getY() < lastMousePress.getY())
1384 else if (evt.getY() > lastMousePress.getY())
1397 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1399 av.setFont(newFont, true);
1400 av.setCharWidth(oldWidth);
1404 ap.av.getCodingComplement().setFont(newFont, true);
1405 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1406 .getSplitViewContainer();
1407 splitFrame.adjustLayout();
1408 splitFrame.repaint();
1415 * on drag left or right, decrement or increment character width
1418 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1420 newWidth = av.getCharWidth() - 1;
1421 av.setCharWidth(newWidth);
1423 else if (evt.getX() > lastMousePress.getX())
1425 newWidth = av.getCharWidth() + 1;
1426 av.setCharWidth(newWidth);
1430 ap.paintAlignment(false, false);
1434 * need to ensure newWidth is set on cdna, regardless of which
1435 * panel the mouse drag happened in; protein will compute its
1436 * character width as 1:1 or 3:1
1438 av.getCodingComplement().setCharWidth(newWidth);
1439 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1440 .getSplitViewContainer();
1441 splitFrame.adjustLayout();
1442 splitFrame.repaint();
1447 FontMetrics fm = getFontMetrics(av.getFont());
1448 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1450 lastMousePress = evt.getPoint();
1457 dragStretchGroup(evt);
1461 int res = pos.column;
1468 if ((editLastRes == -1) || (editLastRes == res))
1473 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1475 // dragLeft, delete gap
1476 editSequence(false, false, res);
1480 editSequence(true, false, res);
1483 mouseDragging = true;
1484 if (scrollThread != null)
1486 scrollThread.setMousePosition(evt.getPoint());
1491 * Edits the sequence to insert or delete one or more gaps, in response to a
1492 * mouse drag or cursor mode command. The number of inserts/deletes may be
1493 * specified with the cursor command, or else depends on the mouse event
1494 * (normally one column, but potentially more for a fast mouse drag).
1496 * Delete gaps is limited to the number of gaps left of the cursor position
1497 * (mouse drag), or at or right of the cursor position (cursor mode).
1499 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1500 * the current selection group.
1502 * In locked editing mode (with a selection group present), inserts/deletions
1503 * within the selection group are limited to its boundaries (and edits outside
1504 * the group stop at its border).
1507 * true to insert gaps, false to delete gaps
1509 * (unused parameter)
1511 * the column at which to perform the action; the number of columns
1512 * affected depends on <code>this.editLastRes</code> (cursor column
1515 synchronized void editSequence(boolean insertGap, boolean editSeq,
1519 int fixedRight = -1;
1520 boolean fixedColumns = false;
1521 SequenceGroup sg = av.getSelectionGroup();
1523 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1525 // No group, but the sequence may represent a group
1526 if (!groupEditing && av.hasHiddenRows())
1528 if (av.isHiddenRepSequence(seq))
1530 sg = av.getRepresentedSequences(seq);
1531 groupEditing = true;
1535 StringBuilder message = new StringBuilder(64); // for status bar
1538 * make a name for the edit action, for
1539 * status bar message and Undo/Redo menu
1541 String label = null;
1544 message.append("Edit group:");
1545 label = MessageManager.getString("action.edit_group");
1549 message.append("Edit sequence: " + seq.getName());
1550 label = seq.getName();
1551 if (label.length() > 10)
1553 label = label.substring(0, 10);
1555 label = MessageManager.formatMessage("label.edit_params",
1561 * initialise the edit command if there is not
1562 * already one being extended
1564 if (editCommand == null)
1566 editCommand = new EditCommand(label);
1571 message.append(" insert ");
1575 message.append(" delete ");
1578 message.append(Math.abs(startres - editLastRes) + " gaps.");
1579 ap.alignFrame.setStatus(message.toString());
1582 * is there a selection group containing the sequence being edited?
1583 * if so the boundary of the group is the limit of the edit
1584 * (but the edit may be inside or outside the selection group)
1586 boolean inSelectionGroup = sg != null
1587 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1588 if (groupEditing || inSelectionGroup)
1590 fixedColumns = true;
1592 // sg might be null as the user may only see 1 sequence,
1593 // but the sequence represents a group
1596 if (!av.isHiddenRepSequence(seq))
1601 sg = av.getRepresentedSequences(seq);
1604 fixedLeft = sg.getStartRes();
1605 fixedRight = sg.getEndRes();
1607 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1608 || (startres >= fixedLeft && editLastRes < fixedLeft)
1609 || (startres > fixedRight && editLastRes <= fixedRight)
1610 || (startres <= fixedRight && editLastRes > fixedRight))
1616 if (fixedLeft > startres)
1618 fixedRight = fixedLeft - 1;
1621 else if (fixedRight < startres)
1623 fixedLeft = fixedRight;
1628 if (av.hasHiddenColumns())
1630 fixedColumns = true;
1631 int y1 = av.getAlignment().getHiddenColumns()
1632 .getNextHiddenBoundary(true, startres);
1633 int y2 = av.getAlignment().getHiddenColumns()
1634 .getNextHiddenBoundary(false, startres);
1636 if ((insertGap && startres > y1 && editLastRes < y1)
1637 || (!insertGap && startres < y2 && editLastRes > y2))
1643 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1644 // Selection spans a hidden region
1645 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1653 fixedRight = y2 - 1;
1658 boolean success = doEditSequence(insertGap, editSeq, startres,
1659 fixedRight, fixedColumns, sg);
1662 * report what actually happened (might be less than
1663 * what was requested), by inspecting the edit commands added
1665 String msg = getEditStatusMessage(editCommand);
1666 ap.alignFrame.setStatus(msg == null ? " " : msg);
1672 editLastRes = startres;
1673 seqCanvas.repaint();
1677 * A helper method that performs the requested editing to insert or delete
1678 * gaps (if possible). Answers true if the edit was successful, false if could
1679 * only be performed in part or not at all. Failure may occur in 'locked edit'
1680 * mode, when an insertion requires a matching gapped position (or column) to
1681 * delete, and deletion requires an adjacent gapped position (or column) to
1685 * true if inserting gap(s), false if deleting
1687 * (unused parameter, currently always false)
1689 * the column at which to perform the edit
1691 * fixed right boundary column of a locked edit (within or to the
1692 * left of a selection group)
1693 * @param fixedColumns
1694 * true if this is a locked edit
1696 * the sequence group (if group edit is being performed)
1699 protected boolean doEditSequence(final boolean insertGap,
1700 final boolean editSeq, final int startres, int fixedRight,
1701 final boolean fixedColumns, final SequenceGroup sg)
1703 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1704 SequenceI[] seqs = new SequenceI[] { seq };
1708 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1709 int g, groupSize = vseqs.size();
1710 SequenceI[] groupSeqs = new SequenceI[groupSize];
1711 for (g = 0; g < groupSeqs.length; g++)
1713 groupSeqs[g] = vseqs.get(g);
1719 // If the user has selected the whole sequence, and is dragging to
1720 // the right, we can still extend the alignment and selectionGroup
1721 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1722 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1725 av.getAlignment().getWidth() + startres - editLastRes);
1726 fixedRight = sg.getEndRes();
1729 // Is it valid with fixed columns??
1730 // Find the next gap before the end
1731 // of the visible region boundary
1732 boolean blank = false;
1733 for (; fixedRight > editLastRes; fixedRight--)
1737 for (g = 0; g < groupSize; g++)
1739 for (int j = 0; j < startres - editLastRes; j++)
1742 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1757 if (sg.getSize() == av.getAlignment().getHeight())
1759 if ((av.hasHiddenColumns()
1760 && startres < av.getAlignment().getHiddenColumns()
1761 .getNextHiddenBoundary(false, startres)))
1766 int alWidth = av.getAlignment().getWidth();
1767 if (av.hasHiddenRows())
1769 int hwidth = av.getAlignment().getHiddenSequences()
1771 if (hwidth > alWidth)
1776 // We can still insert gaps if the selectionGroup
1777 // contains all the sequences
1778 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1779 fixedRight = alWidth + startres - editLastRes;
1789 else if (!insertGap)
1791 // / Are we able to delete?
1792 // ie are all columns blank?
1794 for (g = 0; g < groupSize; g++)
1796 for (int j = startres; j < editLastRes; j++)
1798 if (groupSeqs[g].getLength() <= j)
1803 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1805 // Not a gap, block edit not valid
1814 // dragging to the right
1815 if (fixedColumns && fixedRight != -1)
1817 for (int j = editLastRes; j < startres; j++)
1819 insertGap(j, groupSeqs, fixedRight);
1824 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1825 startres - editLastRes, false);
1830 // dragging to the left
1831 if (fixedColumns && fixedRight != -1)
1833 for (int j = editLastRes; j > startres; j--)
1835 deleteChar(startres, groupSeqs, fixedRight);
1840 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1841 editLastRes - startres, false);
1848 * editing a single sequence
1852 // dragging to the right
1853 if (fixedColumns && fixedRight != -1)
1855 for (int j = editLastRes; j < startres; j++)
1857 if (!insertGap(j, seqs, fixedRight))
1860 * e.g. cursor mode command specified
1861 * more inserts than are possible
1869 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1870 startres - editLastRes, false);
1877 // dragging to the left
1878 if (fixedColumns && fixedRight != -1)
1880 for (int j = editLastRes; j > startres; j--)
1882 if (!Comparison.isGap(seq.getCharAt(startres)))
1886 deleteChar(startres, seqs, fixedRight);
1891 // could be a keyboard edit trying to delete none gaps
1893 for (int m = startres; m < editLastRes; m++)
1895 if (!Comparison.isGap(seq.getCharAt(m)))
1903 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1908 {// insertGap==false AND editSeq==TRUE;
1909 if (fixedColumns && fixedRight != -1)
1911 for (int j = editLastRes; j < startres; j++)
1913 insertGap(j, seqs, fixedRight);
1918 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1919 startres - editLastRes, false);
1929 * Constructs an informative status bar message while dragging to insert or
1930 * delete gaps. Answers null if inserts and deletes cancel out.
1932 * @param editCommand
1933 * a command containing the list of individual edits
1936 protected static String getEditStatusMessage(EditCommand editCommand)
1938 if (editCommand == null)
1944 * add any inserts, and subtract any deletes,
1945 * not counting those auto-inserted when doing a 'locked edit'
1946 * (so only counting edits 'under the cursor')
1949 for (Edit cmd : editCommand.getEdits())
1951 if (!cmd.isSystemGenerated())
1953 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1961 * inserts and deletes cancel out
1966 String msgKey = count > 1 ? "label.insert_gaps"
1967 : (count == 1 ? "label.insert_gap"
1968 : (count == -1 ? "label.delete_gap"
1969 : "label.delete_gaps"));
1970 count = Math.abs(count);
1972 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1976 * Inserts one gap at column j, deleting the right-most gapped column up to
1977 * (and including) fixedColumn. Returns true if the edit is successful, false
1978 * if no blank column is available to allow the insertion to be balanced by a
1983 * @param fixedColumn
1986 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1988 int blankColumn = fixedColumn;
1989 for (int s = 0; s < seq.length; s++)
1991 // Find the next gap before the end of the visible region boundary
1992 // If lastCol > j, theres a boundary after the gap insertion
1994 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1996 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1998 // Theres a space, so break and insert the gap
2003 if (blankColumn <= j)
2005 blankColumn = fixedColumn;
2011 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2013 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2019 * Helper method to add and perform one edit action
2025 * @param systemGenerated
2026 * true if the edit is a 'balancing' delete (or insert) to match a
2027 * user's insert (or delete) in a locked editing region
2029 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2030 int count, boolean systemGenerated)
2033 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2034 av.getAlignment().getGapCharacter());
2035 edit.setSystemGenerated(systemGenerated);
2037 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2041 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2042 * each of the given sequences. The caller should ensure that all sequences
2043 * are gapped in column j.
2047 * @param fixedColumn
2049 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2051 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2053 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2057 * On reentering the panel, stops any scrolling that was started on dragging
2063 public void mouseEntered(MouseEvent e)
2073 * On leaving the panel, if the mouse is being dragged, starts a thread to
2074 * scroll it until the mouse is released (in unwrapped mode only)
2079 public void mouseExited(MouseEvent e)
2081 lastMousePosition = null;
2082 ap.alignFrame.setStatus(" ");
2083 if (av.getWrapAlignment())
2088 if (mouseDragging && scrollThread == null)
2090 startScrolling(e.getPoint());
2095 * Handler for double-click on a position with one or more sequence features.
2096 * Opens the Amend Features dialog to allow feature details to be amended, or
2097 * the feature deleted.
2100 public void mouseClicked(MouseEvent evt)
2102 SequenceGroup sg = null;
2103 MousePos pos = findMousePosition(evt);
2104 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2109 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2111 sg = av.getSelectionGroup();
2112 if (sg != null && sg.getSize() == 1
2113 && sg.getEndRes() - sg.getStartRes() < 2)
2115 av.setSelectionGroup(null);
2118 int column = pos.column;
2121 * find features at the position (if not gapped), or straddling
2122 * the position (if at a gap)
2124 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2125 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2126 .findFeaturesAtColumn(sequence, column + 1);
2128 if (!features.isEmpty())
2131 * highlight the first feature at the position on the alignment
2133 SearchResultsI highlight = new SearchResults();
2134 highlight.addResult(sequence, features.get(0).getBegin(), features
2136 seqCanvas.highlightSearchResults(highlight, true);
2139 * open the Amend Features dialog
2141 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2142 false).showDialog();
2148 public void mouseWheelMoved(MouseWheelEvent e)
2151 double wheelRotation = e.getPreciseWheelRotation();
2152 if (wheelRotation > 0)
2154 if (e.isShiftDown())
2156 av.getRanges().scrollRight(true);
2161 av.getRanges().scrollUp(false);
2164 else if (wheelRotation < 0)
2166 if (e.isShiftDown())
2168 av.getRanges().scrollRight(false);
2172 av.getRanges().scrollUp(true);
2177 * update status bar and tooltip for new position
2178 * (need to synthesize a mouse movement to refresh tooltip)
2181 ToolTipManager.sharedInstance().mouseMoved(e);
2190 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2192 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2197 final int res = pos.column;
2198 final int seq = pos.seqIndex;
2200 updateOverviewAndStructs = false;
2202 startWrapBlock = wrappedBlock;
2204 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2206 if ((sequence == null) || (res > sequence.getLength()))
2211 stretchGroup = av.getSelectionGroup();
2213 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2215 stretchGroup = av.getAlignment().findGroup(sequence, res);
2216 if (stretchGroup != null)
2218 // only update the current selection if the popup menu has a group to
2220 av.setSelectionGroup(stretchGroup);
2225 * defer right-mouse click handling to mouseReleased on Windows
2226 * (where isPopupTrigger() will answer true)
2227 * NB isRightMouseButton is also true for Cmd-click on Mac
2229 if (Platform.isWinRightButton(evt))
2234 if (evt.isPopupTrigger()) // Mac: mousePressed
2236 showPopupMenu(evt, pos);
2242 seqCanvas.cursorX = res;
2243 seqCanvas.cursorY = seq;
2244 seqCanvas.repaint();
2248 if (stretchGroup == null)
2250 createStretchGroup(res, sequence);
2253 if (stretchGroup != null)
2255 stretchGroup.addPropertyChangeListener(seqCanvas);
2258 seqCanvas.repaint();
2261 private void createStretchGroup(int res, SequenceI sequence)
2263 // Only if left mouse button do we want to change group sizes
2264 // define a new group here
2265 SequenceGroup sg = new SequenceGroup();
2266 sg.setStartRes(res);
2268 sg.addSequence(sequence, false);
2269 av.setSelectionGroup(sg);
2272 if (av.getConservationSelected())
2274 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2278 if (av.getAbovePIDThreshold())
2280 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2283 // TODO: stretchGroup will always be not null. Is this a merge error ?
2284 // or is there a threading issue here?
2285 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2287 // Edit end res position of selected group
2288 changeEndRes = true;
2290 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2292 // Edit end res position of selected group
2293 changeStartRes = true;
2295 stretchGroup.getWidth();
2300 * Build and show a pop-up menu at the right-click mouse position
2305 void showPopupMenu(MouseEvent evt, MousePos pos)
2307 final int column = pos.column;
2308 final int seq = pos.seqIndex;
2309 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2310 if (sequence != null)
2312 PopupMenu pop = new PopupMenu(ap, sequence, column);
2313 pop.show(this, evt.getX(), evt.getY());
2318 * Update the display after mouse up on a selection or group
2321 * mouse released event details
2323 * true if this event is happening after a mouse drag (rather than a
2326 protected void doMouseReleasedDefineMode(MouseEvent evt,
2329 if (stretchGroup == null)
2334 stretchGroup.removePropertyChangeListener(seqCanvas);
2336 // always do this - annotation has own state
2337 // but defer colourscheme update until hidden sequences are passed in
2338 boolean vischange = stretchGroup.recalcConservation(true);
2339 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2341 if (stretchGroup.cs != null)
2345 stretchGroup.cs.alignmentChanged(stretchGroup,
2346 av.getHiddenRepSequences());
2349 ResidueShaderI groupColourScheme = stretchGroup
2350 .getGroupColourScheme();
2351 String name = stretchGroup.getName();
2352 if (stretchGroup.cs.conservationApplied())
2354 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2356 if (stretchGroup.cs.getThreshold() > 0)
2358 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2361 PaintRefresher.Refresh(this, av.getSequenceSetId());
2362 // TODO: structure colours only need updating if stretchGroup used to or now
2363 // does contain sequences with structure views
2364 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2365 updateOverviewAndStructs = false;
2366 changeEndRes = false;
2367 changeStartRes = false;
2368 stretchGroup = null;
2373 * Resizes the borders of a selection group depending on the direction of
2378 protected void dragStretchGroup(MouseEvent evt)
2380 if (stretchGroup == null)
2385 MousePos pos = findMousePosition(evt);
2386 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2391 int res = pos.column;
2392 int y = pos.seqIndex;
2394 if (wrappedBlock != startWrapBlock)
2399 res = Math.min(res, av.getAlignment().getWidth()-1);
2401 if (stretchGroup.getEndRes() == res)
2403 // Edit end res position of selected group
2404 changeEndRes = true;
2406 else if (stretchGroup.getStartRes() == res)
2408 // Edit start res position of selected group
2409 changeStartRes = true;
2412 if (res < av.getRanges().getStartRes())
2414 res = av.getRanges().getStartRes();
2419 if (res > (stretchGroup.getStartRes() - 1))
2421 stretchGroup.setEndRes(res);
2422 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2425 else if (changeStartRes)
2427 if (res < (stretchGroup.getEndRes() + 1))
2429 stretchGroup.setStartRes(res);
2430 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2434 int dragDirection = 0;
2440 else if (y < oldSeq)
2445 while ((y != oldSeq) && (oldSeq > -1)
2446 && (y < av.getAlignment().getHeight()))
2448 // This routine ensures we don't skip any sequences, as the
2449 // selection is quite slow.
2450 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2452 oldSeq += dragDirection;
2459 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2461 if (stretchGroup.getSequences(null).contains(nextSeq))
2463 stretchGroup.deleteSequence(seq, false);
2464 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2470 stretchGroup.addSequence(seq, false);
2473 stretchGroup.addSequence(nextSeq, false);
2474 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2483 mouseDragging = true;
2485 if (scrollThread != null)
2487 scrollThread.setMousePosition(evt.getPoint());
2491 * construct a status message showing the range of the selection
2493 StringBuilder status = new StringBuilder(64);
2494 List<SequenceI> seqs = stretchGroup.getSequences();
2495 String name = seqs.get(0).getName();
2496 if (name.length() > 20)
2498 name = name.substring(0, 20);
2500 status.append(name).append(" - ");
2501 name = seqs.get(seqs.size() - 1).getName();
2502 if (name.length() > 20)
2504 name = name.substring(0, 20);
2506 status.append(name).append(" ");
2507 int startRes = stretchGroup.getStartRes();
2508 status.append(" cols ").append(String.valueOf(startRes + 1))
2510 int endRes = stretchGroup.getEndRes();
2511 status.append(String.valueOf(endRes + 1));
2512 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2513 .append(String.valueOf(endRes - startRes + 1)).append(")");
2514 ap.alignFrame.setStatus(status.toString());
2518 * Stops the scroll thread if it is running
2520 void stopScrolling()
2522 if (scrollThread != null)
2524 scrollThread.stopScrolling();
2525 scrollThread = null;
2527 mouseDragging = false;
2531 * Starts a thread to scroll the alignment, towards a given mouse position
2532 * outside the panel bounds, unless the alignment is in wrapped mode
2536 void startScrolling(Point mousePos)
2539 * set this.mouseDragging in case this was called from
2540 * a drag in ScalePanel or AnnotationPanel
2542 mouseDragging = true;
2543 if (!av.getWrapAlignment() && scrollThread == null)
2545 scrollThread = new ScrollThread();
2546 scrollThread.setMousePosition(mousePos);
2547 if (Platform.isJS())
2550 * Javascript - run every 20ms until scrolling stopped
2551 * or reaches the limit of scrollable alignment
2553 Timer t = new Timer(20, new ActionListener()
2556 public void actionPerformed(ActionEvent e)
2558 if (scrollThread != null)
2560 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2561 scrollThread.scrollOnce();
2565 t.addActionListener(new ActionListener()
2568 public void actionPerformed(ActionEvent e)
2570 if (scrollThread == null)
2572 // SeqPanel.stopScrolling called
2582 * Java - run in a new thread
2584 scrollThread.start();
2590 * Performs scrolling of the visible alignment left, right, up or down, until
2591 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2592 * limit of the alignment is reached
2594 class ScrollThread extends Thread
2596 private Point mousePos;
2598 private volatile boolean keepRunning = true;
2603 public ScrollThread()
2605 setName("SeqPanel$ScrollThread");
2609 * Sets the position of the mouse that determines the direction of the
2610 * scroll to perform. If this is called as the mouse moves, scrolling should
2611 * respond accordingly. For example, if the mouse is dragged right, scroll
2612 * right should start; if the drag continues down, scroll down should also
2617 public void setMousePosition(Point p)
2623 * Sets a flag that will cause the thread to exit
2625 public void stopScrolling()
2627 keepRunning = false;
2631 * Scrolls the alignment left or right, and/or up or down, depending on the
2632 * last notified mouse position, until the limit of the alignment is
2633 * reached, or a flag is set to stop the scroll
2640 if (mousePos != null)
2642 keepRunning = scrollOnce();
2647 } catch (Exception ex)
2651 SeqPanel.this.scrollThread = null;
2657 * <li>one row up, if the mouse is above the panel</li>
2658 * <li>one row down, if the mouse is below the panel</li>
2659 * <li>one column left, if the mouse is left of the panel</li>
2660 * <li>one column right, if the mouse is right of the panel</li>
2662 * Answers true if a scroll was performed, false if not - meaning either
2663 * that the mouse position is within the panel, or the edge of the alignment
2666 boolean scrollOnce()
2669 * quit after mouseUp ensures interrupt in JalviewJS
2676 boolean scrolled = false;
2677 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2684 // mouse is above this panel - try scroll up
2685 scrolled = ranges.scrollUp(true);
2687 else if (mousePos.y >= getHeight())
2689 // mouse is below this panel - try scroll down
2690 scrolled = ranges.scrollUp(false);
2694 * scroll left or right
2698 scrolled |= ranges.scrollRight(false);
2700 else if (mousePos.x >= getWidth())
2702 scrolled |= ranges.scrollRight(true);
2709 * modify current selection according to a received message.
2712 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2713 HiddenColumns hidden, SelectionSource source)
2715 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2716 // handles selection messages...
2717 // TODO: extend config options to allow user to control if selections may be
2718 // shared between viewports.
2719 boolean iSentTheSelection = (av == source
2720 || (source instanceof AlignViewport
2721 && ((AlignmentViewport) source).getSequenceSetId()
2722 .equals(av.getSequenceSetId())));
2724 if (iSentTheSelection)
2726 // respond to our own event by updating dependent dialogs
2727 if (ap.getCalculationDialog() != null)
2729 ap.getCalculationDialog().validateCalcTypes();
2735 // process further ?
2736 if (!av.followSelection)
2742 * Ignore the selection if there is one of our own pending.
2744 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2750 * Check for selection in a view of which this one is a dna/protein
2753 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2758 // do we want to thread this ? (contention with seqsel and colsel locks, I
2761 * only copy colsel if there is a real intersection between
2762 * sequence selection and this panel's alignment
2764 boolean repaint = false;
2765 boolean copycolsel = false;
2767 SequenceGroup sgroup = null;
2768 if (seqsel != null && seqsel.getSize() > 0)
2770 if (av.getAlignment() == null)
2772 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2773 + " ViewId=" + av.getViewId()
2774 + " 's alignment is NULL! returning immediately.");
2777 sgroup = seqsel.intersect(av.getAlignment(),
2778 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2779 if ((sgroup != null && sgroup.getSize() > 0))
2784 if (sgroup != null && sgroup.getSize() > 0)
2786 av.setSelectionGroup(sgroup);
2790 av.setSelectionGroup(null);
2792 av.isSelectionGroupChanged(true);
2797 // the current selection is unset or from a previous message
2798 // so import the new colsel.
2799 if (colsel == null || colsel.isEmpty())
2801 if (av.getColumnSelection() != null)
2803 av.getColumnSelection().clear();
2809 // TODO: shift colSel according to the intersecting sequences
2810 if (av.getColumnSelection() == null)
2812 av.setColumnSelection(new ColumnSelection(colsel));
2816 av.getColumnSelection().setElementsFrom(colsel,
2817 av.getAlignment().getHiddenColumns());
2820 av.isColSelChanged(true);
2824 if (copycolsel && av.hasHiddenColumns()
2825 && (av.getAlignment().getHiddenColumns() == null))
2827 System.err.println("Bad things");
2829 if (repaint) // always true!
2831 // probably finessing with multiple redraws here
2832 PaintRefresher.Refresh(this, av.getSequenceSetId());
2833 // ap.paintAlignment(false);
2836 // lastly, update dependent dialogs
2837 if (ap.getCalculationDialog() != null)
2839 ap.getCalculationDialog().validateCalcTypes();
2845 * If this panel is a cdna/protein translation view of the selection source,
2846 * tries to map the source selection to a local one, and returns true. Else
2853 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2854 ColumnSelection colsel, HiddenColumns hidden,
2855 SelectionSource source)
2857 if (!(source instanceof AlignViewportI))
2861 final AlignViewportI sourceAv = (AlignViewportI) source;
2862 if (sourceAv.getCodingComplement() != av
2863 && av.getCodingComplement() != sourceAv)
2869 * Map sequence selection
2871 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2872 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2873 av.isSelectionGroupChanged(true);
2876 * Map column selection
2878 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2880 ColumnSelection cs = new ColumnSelection();
2881 HiddenColumns hs = new HiddenColumns();
2882 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2883 av.setColumnSelection(cs);
2884 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2886 // lastly, update any dependent dialogs
2887 if (ap.getCalculationDialog() != null)
2889 ap.getCalculationDialog().validateCalcTypes();
2893 * repaint alignment, and also Overview or Structure
2894 * if hidden column selection has changed
2896 ap.paintAlignment(hiddenChanged, hiddenChanged);
2903 * @return null or last search results handled by this panel
2905 public SearchResultsI getLastSearchResults()
2907 return lastSearchResults;