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 jalview.api.AlignViewportI;
24 import jalview.bin.Cache;
25 import jalview.commands.EditCommand;
26 import jalview.commands.EditCommand.Action;
27 import jalview.commands.EditCommand.Edit;
28 import jalview.datamodel.AlignmentAnnotation;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.ColumnSelection;
31 import jalview.datamodel.HiddenColumns;
32 import jalview.datamodel.SearchResultMatchI;
33 import jalview.datamodel.SearchResults;
34 import jalview.datamodel.SearchResultsI;
35 import jalview.datamodel.Sequence;
36 import jalview.datamodel.SequenceFeature;
37 import jalview.datamodel.SequenceGroup;
38 import jalview.datamodel.SequenceI;
39 import jalview.io.SequenceAnnotationReport;
40 import jalview.renderer.ResidueShaderI;
41 import jalview.schemes.ResidueProperties;
42 import jalview.structure.SelectionListener;
43 import jalview.structure.SelectionSource;
44 import jalview.structure.SequenceListener;
45 import jalview.structure.StructureSelectionManager;
46 import jalview.structure.VamsasSource;
47 import jalview.util.Comparison;
48 import jalview.util.MappingUtils;
49 import jalview.util.MessageManager;
50 import jalview.util.Platform;
51 import jalview.viewmodel.AlignmentViewport;
52 import jalview.viewmodel.ViewportRanges;
54 import java.awt.BorderLayout;
55 import java.awt.Color;
57 import java.awt.FontMetrics;
58 import java.awt.Point;
59 import java.awt.event.ActionEvent;
60 import java.awt.event.ActionListener;
61 import java.awt.event.MouseEvent;
62 import java.awt.event.MouseListener;
63 import java.awt.event.MouseMotionListener;
64 import java.awt.event.MouseWheelEvent;
65 import java.awt.event.MouseWheelListener;
66 import java.util.Collections;
67 import java.util.List;
69 import javax.swing.JLabel;
70 import javax.swing.JPanel;
71 import javax.swing.JToolTip;
72 import javax.swing.SwingUtilities;
73 import javax.swing.Timer;
74 import javax.swing.ToolTipManager;
77 * The main scrollable region containing the alignment and just to the right of
81 * @version $Revision: 1.130 $
83 public class SeqPanel extends JPanel
84 implements MouseListener, MouseMotionListener, MouseWheelListener,
85 SequenceListener, SelectionListener
88 * a class that holds computed mouse position
89 * - column of the alignment (0...)
90 * - sequence offset (0...)
91 * - annotation row offset (0...)
92 * where annotation offset is -1 unless the alignment is shown
93 * in wrapped mode, annotations are shown, and the mouse is
94 * over an annnotation row
99 * alignment column position of cursor (0...)
104 * index in alignment of sequence under cursor,
105 * or nearest above if cursor is not over a sequence
110 * index in annotations array of annotation under the cursor
111 * (only possible in wrapped mode with annotations shown),
112 * or -1 if cursor is not over an annotation row
114 final int annotationIndex;
116 MousePos(int col, int seq, int ann)
120 annotationIndex = ann;
123 boolean isOverAnnotation()
125 return annotationIndex != -1;
129 public boolean equals(Object obj)
131 if (obj == null || !(obj instanceof MousePos))
135 MousePos o = (MousePos) obj;
136 boolean b = (column == o.column && seqIndex == o.seqIndex
137 && annotationIndex == o.annotationIndex);
142 * A simple hashCode that ensures that instances that satisfy equals() have
146 public int hashCode()
148 return column + seqIndex + annotationIndex;
152 * toString method for debug output purposes only
155 public String toString()
157 return String.format("c%d:s%d:a%d", column, seqIndex,
162 private static final int MAX_TOOLTIP_LENGTH = 300;
164 public SeqCanvas seqCanvas;
166 public AlignmentPanel ap;
169 * last position for mouseMoved event
171 private MousePos lastMousePosition;
173 protected int editLastRes;
175 protected int editStartSeq;
177 protected AlignViewport av;
179 ScrollThread scrollThread = null;
181 boolean mouseDragging = false;
183 boolean editingSeqs = false;
185 boolean groupEditing = false;
187 // ////////////////////////////////////////
188 // ///Everything below this is for defining the boundary of the rubberband
189 // ////////////////////////////////////////
192 boolean changeEndSeq = false;
194 boolean changeStartSeq = false;
196 boolean changeEndRes = false;
198 boolean changeStartRes = false;
200 SequenceGroup stretchGroup = null;
202 boolean remove = false;
204 Point lastMousePress;
206 boolean mouseWheelPressed = false;
208 StringBuffer keyboardNo1;
210 StringBuffer keyboardNo2;
212 java.net.URL linkImageURL;
214 private final SequenceAnnotationReport seqARep;
216 StringBuilder tooltipText = new StringBuilder();
220 EditCommand editCommand;
222 StructureSelectionManager ssm;
224 SearchResultsI lastSearchResults;
227 * Create a new SeqPanel.
232 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
234 linkImageURL = getClass().getResource("/images/link.gif");
235 seqARep = new SequenceAnnotationReport(linkImageURL.toString());
236 ToolTipManager.sharedInstance().registerComponent(this);
237 ToolTipManager.sharedInstance().setInitialDelay(0);
238 ToolTipManager.sharedInstance().setDismissDelay(10000);
242 setBackground(Color.white);
244 seqCanvas = new SeqCanvas(alignPanel);
245 setLayout(new BorderLayout());
246 add(seqCanvas, BorderLayout.CENTER);
248 this.ap = alignPanel;
250 if (!viewport.isDataset())
252 addMouseMotionListener(this);
253 addMouseListener(this);
254 addMouseWheelListener(this);
255 ssm = viewport.getStructureSelectionManager();
256 ssm.addStructureViewerListener(this);
257 ssm.addSelectionListener(this);
261 int startWrapBlock = -1;
263 int wrappedBlock = -1;
266 * Computes the column and sequence row (and possibly annotation row when in
267 * wrapped mode) for the given mouse position
272 MousePos findMousePosition(MouseEvent evt)
274 int col = findColumn(evt);
279 int charHeight = av.getCharHeight();
280 int alignmentHeight = av.getAlignment().getHeight();
281 if (av.getWrapAlignment())
283 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
284 seqCanvas.getHeight());
287 * yPos modulo height of repeating width
289 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
292 * height of sequences plus space / scale above,
293 * plus gap between sequences and annotations
295 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
296 + alignmentHeight * charHeight
297 + SeqCanvas.SEQS_ANNOTATION_GAP;
298 if (yOffsetPx >= alignmentHeightPixels)
301 * mouse is over annotations; find annotation index, also set
302 * last sequence above (for backwards compatible behaviour)
304 AlignmentAnnotation[] anns = av.getAlignment()
305 .getAlignmentAnnotation();
306 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
307 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
308 seqIndex = alignmentHeight - 1;
313 * mouse is over sequence (or the space above sequences)
315 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
318 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
324 ViewportRanges ranges = av.getRanges();
325 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
326 alignmentHeight - 1);
327 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
330 return new MousePos(col, seqIndex, annIndex);
333 * Returns the aligned sequence position (base 0) at the mouse position, or
334 * the closest visible one
339 int findColumn(MouseEvent evt)
344 final int startRes = av.getRanges().getStartRes();
345 final int charWidth = av.getCharWidth();
347 if (av.getWrapAlignment())
349 int hgap = av.getCharHeight();
350 if (av.getScaleAboveWrapped())
352 hgap += av.getCharHeight();
355 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
356 + hgap + seqCanvas.getAnnotationHeight();
359 y = Math.max(0, y - hgap);
360 x -= seqCanvas.getLabelWidthWest();
363 // mouse is over left scale
367 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
372 if (x >= cwidth * charWidth)
374 // mouse is over right scale
378 wrappedBlock = y / cHeight;
379 wrappedBlock += startRes / cwidth;
380 // allow for wrapped view scrolled right (possible from Overview)
381 int startOffset = startRes % cwidth;
382 res = wrappedBlock * cwidth + startOffset
383 + Math.min(cwidth - 1, x / charWidth);
388 * make sure we calculate relative to visible alignment,
389 * rather than right-hand gutter
391 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
392 res = (x / charWidth) + startRes;
393 res = Math.min(res, av.getRanges().getEndRes());
396 if (av.hasHiddenColumns())
398 res = av.getAlignment().getHiddenColumns()
399 .visibleToAbsoluteColumn(res);
406 * When all of a sequence of edits are complete, put the resulting edit list
407 * on the history stack (undo list), and reset flags for editing in progress.
413 if (editCommand != null && editCommand.getSize() > 0)
415 ap.alignFrame.addHistoryItem(editCommand);
416 av.firePropertyChange("alignment", null,
417 av.getAlignment().getSequences());
422 * Tidy up come what may...
427 groupEditing = false;
436 seqCanvas.cursorY = getKeyboardNo1() - 1;
437 scrollToVisible(true);
440 void setCursorColumn()
442 seqCanvas.cursorX = getKeyboardNo1() - 1;
443 scrollToVisible(true);
446 void setCursorRowAndColumn()
448 if (keyboardNo2 == null)
450 keyboardNo2 = new StringBuffer();
454 seqCanvas.cursorX = getKeyboardNo1() - 1;
455 seqCanvas.cursorY = getKeyboardNo2() - 1;
456 scrollToVisible(true);
460 void setCursorPosition()
462 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
464 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
465 scrollToVisible(true);
468 void moveCursor(int dx, int dy)
470 seqCanvas.cursorX += dx;
471 seqCanvas.cursorY += dy;
473 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
475 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
477 int original = seqCanvas.cursorX - dx;
478 int maxWidth = av.getAlignment().getWidth();
480 if (!hidden.isVisible(seqCanvas.cursorX))
482 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
483 int[] region = hidden.getRegionWithEdgeAtRes(visx);
485 if (region != null) // just in case
490 seqCanvas.cursorX = region[1] + 1;
495 seqCanvas.cursorX = region[0] - 1;
498 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
501 if (seqCanvas.cursorX >= maxWidth
502 || !hidden.isVisible(seqCanvas.cursorX))
504 seqCanvas.cursorX = original;
508 scrollToVisible(false);
512 * Scroll to make the cursor visible in the viewport.
515 * just jump to the location rather than scrolling
517 void scrollToVisible(boolean jump)
519 if (seqCanvas.cursorX < 0)
521 seqCanvas.cursorX = 0;
523 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
525 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
528 if (seqCanvas.cursorY < 0)
530 seqCanvas.cursorY = 0;
532 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
534 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
539 boolean repaintNeeded = true;
542 // only need to repaint if the viewport did not move, as otherwise it will
544 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
549 if (av.getWrapAlignment())
551 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
552 int x = av.getAlignment().getHiddenColumns()
553 .absoluteToVisibleColumn(seqCanvas.cursorX);
554 av.getRanges().scrollToWrappedVisible(x);
558 av.getRanges().scrollToVisible(seqCanvas.cursorX,
563 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
565 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
566 seqCanvas.cursorX, seqCanvas.cursorY);
576 void setSelectionAreaAtCursor(boolean topLeft)
578 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
580 if (av.getSelectionGroup() != null)
582 SequenceGroup sg = av.getSelectionGroup();
583 // Find the top and bottom of this group
584 int min = av.getAlignment().getHeight(), max = 0;
585 for (int i = 0; i < sg.getSize(); i++)
587 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
602 sg.setStartRes(seqCanvas.cursorX);
603 if (sg.getEndRes() < seqCanvas.cursorX)
605 sg.setEndRes(seqCanvas.cursorX);
608 min = seqCanvas.cursorY;
612 sg.setEndRes(seqCanvas.cursorX);
613 if (sg.getStartRes() > seqCanvas.cursorX)
615 sg.setStartRes(seqCanvas.cursorX);
618 max = seqCanvas.cursorY + 1;
623 // Only the user can do this
624 av.setSelectionGroup(null);
628 // Now add any sequences between min and max
629 sg.getSequences(null).clear();
630 for (int i = min; i < max; i++)
632 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
637 if (av.getSelectionGroup() == null)
639 SequenceGroup sg = new SequenceGroup();
640 sg.setStartRes(seqCanvas.cursorX);
641 sg.setEndRes(seqCanvas.cursorX);
642 sg.addSequence(sequence, false);
643 av.setSelectionGroup(sg);
646 ap.paintAlignment(false, false);
650 void insertGapAtCursor(boolean group)
652 groupEditing = group;
653 editStartSeq = seqCanvas.cursorY;
654 editLastRes = seqCanvas.cursorX;
655 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
659 void deleteGapAtCursor(boolean group)
661 groupEditing = group;
662 editStartSeq = seqCanvas.cursorY;
663 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
664 editSequence(false, false, seqCanvas.cursorX);
668 void insertNucAtCursor(boolean group, String nuc)
670 // TODO not called - delete?
671 groupEditing = group;
672 editStartSeq = seqCanvas.cursorY;
673 editLastRes = seqCanvas.cursorX;
674 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
678 void numberPressed(char value)
680 if (keyboardNo1 == null)
682 keyboardNo1 = new StringBuffer();
685 if (keyboardNo2 != null)
687 keyboardNo2.append(value);
691 keyboardNo1.append(value);
699 if (keyboardNo1 != null)
701 int value = Integer.parseInt(keyboardNo1.toString());
705 } catch (Exception x)
716 if (keyboardNo2 != null)
718 int value = Integer.parseInt(keyboardNo2.toString());
722 } catch (Exception x)
736 public void mouseReleased(MouseEvent evt)
738 MousePos pos = findMousePosition(evt);
739 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
744 boolean didDrag = mouseDragging; // did we come here after a drag
745 mouseDragging = false;
746 mouseWheelPressed = false;
748 if (evt.isPopupTrigger()) // Windows: mouseReleased
750 showPopupMenu(evt, pos);
761 doMouseReleasedDefineMode(evt, didDrag);
772 public void mousePressed(MouseEvent evt)
774 lastMousePress = evt.getPoint();
775 MousePos pos = findMousePosition(evt);
776 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
781 if (SwingUtilities.isMiddleMouseButton(evt))
783 mouseWheelPressed = true;
787 boolean isControlDown = Platform.isControlDown(evt);
788 if (evt.isShiftDown() || isControlDown)
798 doMousePressedDefineMode(evt, pos);
802 int seq = pos.seqIndex;
803 int res = pos.column;
805 if ((seq < av.getAlignment().getHeight())
806 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
822 private String formattedTooltipText;
825 public void mouseOverSequence(SequenceI sequence, int index, int pos)
827 String tmp = sequence.hashCode() + " " + index + " " + pos;
829 if (lastMessage == null || !lastMessage.equals(tmp))
831 // System.err.println("mouseOver Sequence: "+tmp);
832 ssm.mouseOverSequence(sequence, index, pos, av);
838 * Highlight the mapped region described by the search results object (unless
839 * unchanged). This supports highlight of protein while mousing over linked
840 * cDNA and vice versa. The status bar is also updated to show the location of
841 * the start of the highlighted region.
844 public void highlightSequence(SearchResultsI results)
846 if (results == null || results.equals(lastSearchResults))
850 lastSearchResults = results;
852 boolean wasScrolled = false;
854 if (av.isFollowHighlight())
856 // don't allow highlight of protein/cDNA to also scroll a complementary
857 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
858 // over residue to change abruptly, causing highlighted residue in panel 2
859 // to change, causing a scroll in panel 1 etc)
860 ap.setToScrollComplementPanel(false);
861 wasScrolled = ap.scrollToPosition(results);
864 seqCanvas.revalidate();
866 ap.setToScrollComplementPanel(true);
869 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
870 if (seqCanvas.highlightSearchResults(results, fastPaint))
872 setStatusMessage(results);
877 public VamsasSource getVamsasSource()
879 return this.ap == null ? null : this.ap.av;
883 public void updateColours(SequenceI seq, int index)
885 System.out.println("update the seqPanel colours");
890 * Action on mouse movement is to update the status bar to show the current
891 * sequence position, and (if features are shown) to show any features at the
892 * position in a tooltip. Does nothing if the mouse move does not change
898 public void mouseMoved(MouseEvent evt)
902 // This is because MacOSX creates a mouseMoved
903 // If control is down, other platforms will not.
907 final MousePos mousePos = findMousePosition(evt);
908 if (mousePos.equals(lastMousePosition))
911 * just a pixel move without change of 'cell'
915 lastMousePosition = mousePos;
917 if (mousePos.isOverAnnotation())
919 mouseMovedOverAnnotation(mousePos);
922 final int seq = mousePos.seqIndex;
924 final int column = mousePos.column;
925 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
927 lastMousePosition = null;
928 setToolTipText(null);
930 ap.alignFrame.setStatus("");
934 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
936 if (column >= sequence.getLength())
942 * set status bar message, returning residue position in sequence
944 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
945 final int pos = setStatusMessage(sequence, column, seq);
946 if (ssm != null && !isGapped)
948 mouseOverSequence(sequence, column, pos);
951 tooltipText.setLength(6); // "<html>"
953 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
956 for (int g = 0; g < groups.length; g++)
958 if (groups[g].getStartRes() <= column
959 && groups[g].getEndRes() >= column)
961 if (!groups[g].getName().startsWith("JTreeGroup")
962 && !groups[g].getName().startsWith("JGroup"))
964 tooltipText.append(groups[g].getName());
967 if (groups[g].getDescription() != null)
969 tooltipText.append(": " + groups[g].getDescription());
976 * add any features at the position to the tooltip; if over a gap, only
977 * add features that straddle the gap (pos may be the residue before or
980 if (av.isShowSequenceFeatures())
982 List<SequenceFeature> features = ap.getFeatureRenderer()
983 .findFeaturesAtColumn(sequence, column + 1);
984 seqARep.appendFeatures(tooltipText, pos, features,
985 this.ap.getSeqPanel().seqCanvas.fr);
987 if (tooltipText.length() == 6) // <html>
989 setToolTipText(null);
994 if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
996 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
997 tooltipText.append("...");
999 String textString = tooltipText.toString();
1000 if (lastTooltip == null || !lastTooltip.equals(textString))
1002 formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1004 setToolTipText(formattedTooltipText);
1005 lastTooltip = textString;
1011 * When the view is in wrapped mode, and the mouse is over an annotation row,
1012 * shows the corresponding tooltip and status message (if any)
1017 protected void mouseMovedOverAnnotation(MousePos pos)
1019 final int column = pos.column;
1020 final int rowIndex = pos.annotationIndex;
1022 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1027 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1029 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1031 setToolTipText(tooltip);
1032 lastTooltip = tooltip;
1034 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1036 ap.alignFrame.setStatus(msg);
1039 private Point lastp = null;
1041 private JToolTip tempTip = new JLabel().createToolTip();
1046 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1049 public Point getToolTipLocation(MouseEvent event)
1053 if (tooltipText == null || tooltipText.length() <= 6)
1058 if (lastp != null && event.isShiftDown())
1064 int x = event.getX();
1065 int y = event.getY();
1068 tempTip.setTipText(formattedTooltipText);
1069 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1071 // was x += (w - x < 200) ? -(w / 2) : 5;
1072 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1073 p = new Point(x, y + 20); // BH 2018 was - 20?
1081 * set when the current UI interaction has resulted in a change that requires
1082 * shading in overviews and structures to be recalculated. this could be
1083 * changed to a something more expressive that indicates what actually has
1084 * changed, so selective redraws can be applied (ie. only structures, only
1087 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1090 * set if av.getSelectionGroup() refers to a group that is defined on the
1091 * alignment view, rather than a transient selection
1093 // private boolean editingDefinedGroup = false; // TODO: refactor to
1094 // avcontroller or viewModel
1097 * Sets the status message in alignment panel, showing the sequence number
1098 * (index) and id, and residue and residue position if not at a gap, for the
1099 * given sequence and column position. Returns the residue position returned
1100 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1101 * if at a gapped position.
1104 * aligned sequence object
1108 * index of sequence in alignment
1109 * @return sequence position of residue at column, or adjacent residue if at a
1112 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1114 char sequenceChar = sequence.getCharAt(column);
1115 int pos = sequence.findPosition(column);
1116 setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1122 * Builds the status message for the current cursor location and writes it to
1123 * the status bar, for example
1126 * Sequence 3 ID: FER1_SOLLC
1127 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1128 * Sequence 5 ID: FER1_PEA Residue: B (3)
1129 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1134 * sequence position in the alignment (1..)
1135 * @param sequenceChar
1136 * the character under the cursor
1138 * the sequence residue position (if not over a gap)
1140 protected void setStatusMessage(SequenceI sequence, int seqIndex,
1141 char sequenceChar, int residuePos)
1143 StringBuilder text = new StringBuilder(32);
1146 * Sequence number (if known), and sequence name.
1148 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1149 text.append("Sequence").append(seqno).append(" ID: ")
1150 .append(sequence.getName());
1152 String residue = null;
1155 * Try to translate the display character to residue name (null for gap).
1157 boolean isGapped = Comparison.isGap(sequenceChar);
1161 boolean nucleotide = av.getAlignment().isNucleotide();
1162 String displayChar = String.valueOf(sequenceChar);
1165 residue = ResidueProperties.nucleotideName.get(displayChar);
1169 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1170 : ("*".equals(displayChar) ? "STOP"
1171 : ResidueProperties.aa2Triplet.get(displayChar));
1173 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1174 .append(": ").append(residue == null ? displayChar : residue);
1176 text.append(" (").append(Integer.toString(residuePos)).append(")");
1178 ap.alignFrame.setStatus(text.toString());
1182 * Set the status bar message to highlight the first matched position in
1187 private void setStatusMessage(SearchResultsI results)
1189 AlignmentI al = this.av.getAlignment();
1190 int sequenceIndex = al.findIndex(results);
1191 if (sequenceIndex == -1)
1195 SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1196 for (SearchResultMatchI m : results.getResults())
1198 SequenceI seq = m.getSequence();
1199 if (seq.getDatasetSequence() != null)
1201 seq = seq.getDatasetSequence();
1206 int start = m.getStart();
1207 setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1218 public void mouseDragged(MouseEvent evt)
1220 MousePos pos = findMousePosition(evt);
1221 if (pos.isOverAnnotation() || pos.column == -1)
1226 if (mouseWheelPressed)
1228 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1229 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1231 int oldWidth = av.getCharWidth();
1233 // Which is bigger, left-right or up-down?
1234 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1235 .abs(evt.getX() - lastMousePress.getX()))
1238 * on drag up or down, decrement or increment font size
1240 int fontSize = av.font.getSize();
1241 boolean fontChanged = false;
1243 if (evt.getY() < lastMousePress.getY())
1248 else if (evt.getY() > lastMousePress.getY())
1261 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1263 av.setFont(newFont, true);
1264 av.setCharWidth(oldWidth);
1268 ap.av.getCodingComplement().setFont(newFont, true);
1269 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1270 .getSplitViewContainer();
1271 splitFrame.adjustLayout();
1272 splitFrame.repaint();
1279 * on drag left or right, decrement or increment character width
1282 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1284 newWidth = av.getCharWidth() - 1;
1285 av.setCharWidth(newWidth);
1287 else if (evt.getX() > lastMousePress.getX())
1289 newWidth = av.getCharWidth() + 1;
1290 av.setCharWidth(newWidth);
1294 ap.paintAlignment(false, false);
1298 * need to ensure newWidth is set on cdna, regardless of which
1299 * panel the mouse drag happened in; protein will compute its
1300 * character width as 1:1 or 3:1
1302 av.getCodingComplement().setCharWidth(newWidth);
1303 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1304 .getSplitViewContainer();
1305 splitFrame.adjustLayout();
1306 splitFrame.repaint();
1311 FontMetrics fm = getFontMetrics(av.getFont());
1312 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1314 lastMousePress = evt.getPoint();
1321 dragStretchGroup(evt);
1325 int res = pos.column;
1332 if ((editLastRes == -1) || (editLastRes == res))
1337 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1339 // dragLeft, delete gap
1340 editSequence(false, false, res);
1344 editSequence(true, false, res);
1347 mouseDragging = true;
1348 if (scrollThread != null)
1350 scrollThread.setMousePosition(evt.getPoint());
1355 * Edits the sequence to insert or delete one or more gaps, in response to a
1356 * mouse drag or cursor mode command. The number of inserts/deletes may be
1357 * specified with the cursor command, or else depends on the mouse event
1358 * (normally one column, but potentially more for a fast mouse drag).
1360 * Delete gaps is limited to the number of gaps left of the cursor position
1361 * (mouse drag), or at or right of the cursor position (cursor mode).
1363 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1364 * the current selection group.
1366 * In locked editing mode (with a selection group present), inserts/deletions
1367 * within the selection group are limited to its boundaries (and edits outside
1368 * the group stop at its border).
1371 * true to insert gaps, false to delete gaps
1373 * (unused parameter)
1375 * the column at which to perform the action; the number of columns
1376 * affected depends on <code>this.editLastRes</code> (cursor column
1379 synchronized void editSequence(boolean insertGap, boolean editSeq,
1383 int fixedRight = -1;
1384 boolean fixedColumns = false;
1385 SequenceGroup sg = av.getSelectionGroup();
1387 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1389 // No group, but the sequence may represent a group
1390 if (!groupEditing && av.hasHiddenRows())
1392 if (av.isHiddenRepSequence(seq))
1394 sg = av.getRepresentedSequences(seq);
1395 groupEditing = true;
1399 StringBuilder message = new StringBuilder(64); // for status bar
1402 * make a name for the edit action, for
1403 * status bar message and Undo/Redo menu
1405 String label = null;
1408 message.append("Edit group:");
1409 label = MessageManager.getString("action.edit_group");
1413 message.append("Edit sequence: " + seq.getName());
1414 label = seq.getName();
1415 if (label.length() > 10)
1417 label = label.substring(0, 10);
1419 label = MessageManager.formatMessage("label.edit_params",
1425 * initialise the edit command if there is not
1426 * already one being extended
1428 if (editCommand == null)
1430 editCommand = new EditCommand(label);
1435 message.append(" insert ");
1439 message.append(" delete ");
1442 message.append(Math.abs(startres - editLastRes) + " gaps.");
1443 ap.alignFrame.setStatus(message.toString());
1446 * is there a selection group containing the sequence being edited?
1447 * if so the boundary of the group is the limit of the edit
1448 * (but the edit may be inside or outside the selection group)
1450 boolean inSelectionGroup = sg != null
1451 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1452 if (groupEditing || inSelectionGroup)
1454 fixedColumns = true;
1456 // sg might be null as the user may only see 1 sequence,
1457 // but the sequence represents a group
1460 if (!av.isHiddenRepSequence(seq))
1465 sg = av.getRepresentedSequences(seq);
1468 fixedLeft = sg.getStartRes();
1469 fixedRight = sg.getEndRes();
1471 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1472 || (startres >= fixedLeft && editLastRes < fixedLeft)
1473 || (startres > fixedRight && editLastRes <= fixedRight)
1474 || (startres <= fixedRight && editLastRes > fixedRight))
1480 if (fixedLeft > startres)
1482 fixedRight = fixedLeft - 1;
1485 else if (fixedRight < startres)
1487 fixedLeft = fixedRight;
1492 if (av.hasHiddenColumns())
1494 fixedColumns = true;
1495 int y1 = av.getAlignment().getHiddenColumns()
1496 .getNextHiddenBoundary(true, startres);
1497 int y2 = av.getAlignment().getHiddenColumns()
1498 .getNextHiddenBoundary(false, startres);
1500 if ((insertGap && startres > y1 && editLastRes < y1)
1501 || (!insertGap && startres < y2 && editLastRes > y2))
1507 // Selection spans a hidden region
1508 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1516 fixedRight = y2 - 1;
1521 boolean success = doEditSequence(insertGap, editSeq, startres,
1522 fixedRight, fixedColumns, sg);
1525 * report what actually happened (might be less than
1526 * what was requested), by inspecting the edit commands added
1528 String msg = getEditStatusMessage(editCommand);
1529 ap.alignFrame.setStatus(msg == null ? " " : msg);
1535 editLastRes = startres;
1536 seqCanvas.repaint();
1540 * A helper method that performs the requested editing to insert or delete
1541 * gaps (if possible). Answers true if the edit was successful, false if could
1542 * only be performed in part or not at all. Failure may occur in 'locked edit'
1543 * mode, when an insertion requires a matching gapped position (or column) to
1544 * delete, and deletion requires an adjacent gapped position (or column) to
1548 * true if inserting gap(s), false if deleting
1550 * (unused parameter, currently always false)
1552 * the column at which to perform the edit
1554 * fixed right boundary column of a locked edit (within or to the
1555 * left of a selection group)
1556 * @param fixedColumns
1557 * true if this is a locked edit
1559 * the sequence group (if group edit is being performed)
1562 protected boolean doEditSequence(final boolean insertGap,
1563 final boolean editSeq, final int startres, int fixedRight,
1564 final boolean fixedColumns, final SequenceGroup sg)
1566 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1567 SequenceI[] seqs = new SequenceI[] { seq };
1571 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1572 int g, groupSize = vseqs.size();
1573 SequenceI[] groupSeqs = new SequenceI[groupSize];
1574 for (g = 0; g < groupSeqs.length; g++)
1576 groupSeqs[g] = vseqs.get(g);
1582 // If the user has selected the whole sequence, and is dragging to
1583 // the right, we can still extend the alignment and selectionGroup
1584 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1585 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1588 av.getAlignment().getWidth() + startres - editLastRes);
1589 fixedRight = sg.getEndRes();
1592 // Is it valid with fixed columns??
1593 // Find the next gap before the end
1594 // of the visible region boundary
1595 boolean blank = false;
1596 for (; fixedRight > editLastRes; fixedRight--)
1600 for (g = 0; g < groupSize; g++)
1602 for (int j = 0; j < startres - editLastRes; j++)
1605 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1620 if (sg.getSize() == av.getAlignment().getHeight())
1622 if ((av.hasHiddenColumns()
1623 && startres < av.getAlignment().getHiddenColumns()
1624 .getNextHiddenBoundary(false, startres)))
1629 int alWidth = av.getAlignment().getWidth();
1630 if (av.hasHiddenRows())
1632 int hwidth = av.getAlignment().getHiddenSequences()
1634 if (hwidth > alWidth)
1639 // We can still insert gaps if the selectionGroup
1640 // contains all the sequences
1641 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1642 fixedRight = alWidth + startres - editLastRes;
1652 else if (!insertGap)
1654 // / Are we able to delete?
1655 // ie are all columns blank?
1657 for (g = 0; g < groupSize; g++)
1659 for (int j = startres; j < editLastRes; j++)
1661 if (groupSeqs[g].getLength() <= j)
1666 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1668 // Not a gap, block edit not valid
1677 // dragging to the right
1678 if (fixedColumns && fixedRight != -1)
1680 for (int j = editLastRes; j < startres; j++)
1682 insertGap(j, groupSeqs, fixedRight);
1687 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1688 startres - editLastRes, false);
1693 // dragging to the left
1694 if (fixedColumns && fixedRight != -1)
1696 for (int j = editLastRes; j > startres; j--)
1698 deleteChar(startres, groupSeqs, fixedRight);
1703 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1704 editLastRes - startres, false);
1711 * editing a single sequence
1715 // dragging to the right
1716 if (fixedColumns && fixedRight != -1)
1718 for (int j = editLastRes; j < startres; j++)
1720 if (!insertGap(j, seqs, fixedRight))
1723 * e.g. cursor mode command specified
1724 * more inserts than are possible
1732 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1733 startres - editLastRes, false);
1740 // dragging to the left
1741 if (fixedColumns && fixedRight != -1)
1743 for (int j = editLastRes; j > startres; j--)
1745 if (!Comparison.isGap(seq.getCharAt(startres)))
1749 deleteChar(startres, seqs, fixedRight);
1754 // could be a keyboard edit trying to delete none gaps
1756 for (int m = startres; m < editLastRes; m++)
1758 if (!Comparison.isGap(seq.getCharAt(m)))
1766 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1771 {// insertGap==false AND editSeq==TRUE;
1772 if (fixedColumns && fixedRight != -1)
1774 for (int j = editLastRes; j < startres; j++)
1776 insertGap(j, seqs, fixedRight);
1781 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1782 startres - editLastRes, false);
1792 * Constructs an informative status bar message while dragging to insert or
1793 * delete gaps. Answers null if inserts and deletes cancel out.
1795 * @param editCommand
1796 * a command containing the list of individual edits
1799 protected static String getEditStatusMessage(EditCommand editCommand)
1801 if (editCommand == null)
1807 * add any inserts, and subtract any deletes,
1808 * not counting those auto-inserted when doing a 'locked edit'
1809 * (so only counting edits 'under the cursor')
1812 for (Edit cmd : editCommand.getEdits())
1814 if (!cmd.isSystemGenerated())
1816 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1824 * inserts and deletes cancel out
1829 String msgKey = count > 1 ? "label.insert_gaps"
1830 : (count == 1 ? "label.insert_gap"
1831 : (count == -1 ? "label.delete_gap"
1832 : "label.delete_gaps"));
1833 count = Math.abs(count);
1835 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1839 * Inserts one gap at column j, deleting the right-most gapped column up to
1840 * (and including) fixedColumn. Returns true if the edit is successful, false
1841 * if no blank column is available to allow the insertion to be balanced by a
1846 * @param fixedColumn
1849 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1851 int blankColumn = fixedColumn;
1852 for (int s = 0; s < seq.length; s++)
1854 // Find the next gap before the end of the visible region boundary
1855 // If lastCol > j, theres a boundary after the gap insertion
1857 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1859 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1861 // Theres a space, so break and insert the gap
1866 if (blankColumn <= j)
1868 blankColumn = fixedColumn;
1874 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1876 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1882 * Helper method to add and perform one edit action
1888 * @param systemGenerated
1889 * true if the edit is a 'balancing' delete (or insert) to match a
1890 * user's insert (or delete) in a locked editing region
1892 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1893 int count, boolean systemGenerated)
1896 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1897 av.getAlignment().getGapCharacter());
1898 edit.setSystemGenerated(systemGenerated);
1900 editCommand.appendEdit(edit, av.getAlignment(), true, null);
1904 * Deletes the character at column j, and inserts a gap at fixedColumn, in
1905 * each of the given sequences. The caller should ensure that all sequences
1906 * are gapped in column j.
1910 * @param fixedColumn
1912 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
1914 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
1916 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
1920 * On reentering the panel, stops any scrolling that was started on dragging
1926 public void mouseEntered(MouseEvent e)
1936 * On leaving the panel, if the mouse is being dragged, starts a thread to
1937 * scroll it until the mouse is released (in unwrapped mode only)
1942 public void mouseExited(MouseEvent e)
1944 lastMousePosition = null;
1945 ap.alignFrame.setStatus(" ");
1947 if (av.getWrapAlignment())
1953 * start scrolling if mouse dragging, whether the drag started
1954 * in the scale panel or this panel
1956 if (ap.getScalePanel().isMouseDragging())
1958 ap.getScalePanel().mouseExited(e);
1960 else if (mouseDragging && scrollThread == null)
1962 startScrolling(e.getPoint());
1967 * Handler for double-click on a position with one or more sequence features.
1968 * Opens the Amend Features dialog to allow feature details to be amended, or
1969 * the feature deleted.
1972 public void mouseClicked(MouseEvent evt)
1974 SequenceGroup sg = null;
1975 MousePos pos = findMousePosition(evt);
1976 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
1981 if (evt.getClickCount() > 1)
1983 sg = av.getSelectionGroup();
1984 if (sg != null && sg.getSize() == 1
1985 && sg.getEndRes() - sg.getStartRes() < 2)
1987 av.setSelectionGroup(null);
1990 int column = pos.column;
1993 * find features at the position (if not gapped), or straddling
1994 * the position (if at a gap)
1996 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
1997 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
1998 .findFeaturesAtColumn(sequence, column + 1);
2000 if (!features.isEmpty())
2003 * highlight the first feature at the position on the alignment
2005 SearchResultsI highlight = new SearchResults();
2006 highlight.addResult(sequence, features.get(0).getBegin(), features
2008 seqCanvas.highlightSearchResults(highlight, true);
2011 * open the Amend Features dialog
2013 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2014 false).showDialog();
2020 public void mouseWheelMoved(MouseWheelEvent e)
2023 double wheelRotation = e.getPreciseWheelRotation();
2024 if (wheelRotation > 0)
2026 if (e.isShiftDown())
2028 av.getRanges().scrollRight(true);
2033 av.getRanges().scrollUp(false);
2036 else if (wheelRotation < 0)
2038 if (e.isShiftDown())
2040 av.getRanges().scrollRight(false);
2044 av.getRanges().scrollUp(true);
2049 * update status bar and tooltip for new position
2050 * (need to synthesize a mouse movement to refresh tooltip)
2053 ToolTipManager.sharedInstance().mouseMoved(e);
2062 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2064 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2069 final int res = pos.column;
2070 final int seq = pos.seqIndex;
2072 updateOverviewAndStructs = false;
2074 startWrapBlock = wrappedBlock;
2076 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2078 if ((sequence == null) || (res > sequence.getLength()))
2083 stretchGroup = av.getSelectionGroup();
2085 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2087 stretchGroup = av.getAlignment().findGroup(sequence, res);
2088 if (stretchGroup != null)
2090 // only update the current selection if the popup menu has a group to
2092 av.setSelectionGroup(stretchGroup);
2097 * defer right-mouse click handling to mouseReleased on Windows
2098 * (where isPopupTrigger() will answer true)
2099 * NB isRightMouseButton is also true for Cmd-click on Mac
2101 if (Platform.isWinRightButton(evt))
2106 if (evt.isPopupTrigger()) // Mac: mousePressed
2108 showPopupMenu(evt, pos);
2114 seqCanvas.cursorX = res;
2115 seqCanvas.cursorY = seq;
2116 seqCanvas.repaint();
2120 if (stretchGroup == null)
2122 createStretchGroup(res, sequence);
2125 if (stretchGroup != null)
2127 stretchGroup.addPropertyChangeListener(seqCanvas);
2130 seqCanvas.repaint();
2133 private void createStretchGroup(int res, SequenceI sequence)
2135 // Only if left mouse button do we want to change group sizes
2136 // define a new group here
2137 SequenceGroup sg = new SequenceGroup();
2138 sg.setStartRes(res);
2140 sg.addSequence(sequence, false);
2141 av.setSelectionGroup(sg);
2144 if (av.getConservationSelected())
2146 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2150 if (av.getAbovePIDThreshold())
2152 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2155 // TODO: stretchGroup will always be not null. Is this a merge error ?
2156 // or is there a threading issue here?
2157 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2159 // Edit end res position of selected group
2160 changeEndRes = true;
2162 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2164 // Edit end res position of selected group
2165 changeStartRes = true;
2167 stretchGroup.getWidth();
2172 * Build and show a pop-up menu at the right-click mouse position
2177 void showPopupMenu(MouseEvent evt, MousePos pos)
2179 final int column = pos.column;
2180 final int seq = pos.seqIndex;
2181 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2182 List<SequenceFeature> features = ap.getFeatureRenderer()
2183 .findFeaturesAtColumn(sequence, column + 1);
2185 PopupMenu pop = new PopupMenu(ap, null, features);
2186 pop.show(this, evt.getX(), evt.getY());
2190 * Update the display after mouse up on a selection or group
2193 * mouse released event details
2195 * true if this event is happening after a mouse drag (rather than a
2198 protected void doMouseReleasedDefineMode(MouseEvent evt,
2201 if (stretchGroup == null)
2206 stretchGroup.removePropertyChangeListener(seqCanvas);
2208 // always do this - annotation has own state
2209 // but defer colourscheme update until hidden sequences are passed in
2210 boolean vischange = stretchGroup.recalcConservation(true);
2211 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2213 if (stretchGroup.cs != null)
2217 stretchGroup.cs.alignmentChanged(stretchGroup,
2218 av.getHiddenRepSequences());
2221 ResidueShaderI groupColourScheme = stretchGroup
2222 .getGroupColourScheme();
2223 String name = stretchGroup.getName();
2224 if (stretchGroup.cs.conservationApplied())
2226 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2228 if (stretchGroup.cs.getThreshold() > 0)
2230 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2233 PaintRefresher.Refresh(this, av.getSequenceSetId());
2234 // TODO: structure colours only need updating if stretchGroup used to or now
2235 // does contain sequences with structure views
2236 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2237 updateOverviewAndStructs = false;
2238 changeEndRes = false;
2239 changeStartRes = false;
2240 stretchGroup = null;
2245 * Resizes the borders of a selection group depending on the direction of
2250 protected void dragStretchGroup(MouseEvent evt)
2252 if (stretchGroup == null)
2257 MousePos pos = findMousePosition(evt);
2258 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2263 int res = pos.column;
2264 int y = pos.seqIndex;
2266 if (wrappedBlock != startWrapBlock)
2271 res = Math.min(res, av.getAlignment().getWidth()-1);
2273 if (stretchGroup.getEndRes() == res)
2275 // Edit end res position of selected group
2276 changeEndRes = true;
2278 else if (stretchGroup.getStartRes() == res)
2280 // Edit start res position of selected group
2281 changeStartRes = true;
2284 if (res < av.getRanges().getStartRes())
2286 res = av.getRanges().getStartRes();
2291 if (res > (stretchGroup.getStartRes() - 1))
2293 stretchGroup.setEndRes(res);
2294 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2297 else if (changeStartRes)
2299 if (res < (stretchGroup.getEndRes() + 1))
2301 stretchGroup.setStartRes(res);
2302 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2306 int dragDirection = 0;
2312 else if (y < oldSeq)
2317 while ((y != oldSeq) && (oldSeq > -1)
2318 && (y < av.getAlignment().getHeight()))
2320 // This routine ensures we don't skip any sequences, as the
2321 // selection is quite slow.
2322 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2324 oldSeq += dragDirection;
2331 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2333 if (stretchGroup.getSequences(null).contains(nextSeq))
2335 stretchGroup.deleteSequence(seq, false);
2336 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2342 stretchGroup.addSequence(seq, false);
2345 stretchGroup.addSequence(nextSeq, false);
2346 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2355 mouseDragging = true;
2357 if (scrollThread != null)
2359 scrollThread.setMousePosition(evt.getPoint());
2363 * construct a status message showing the range of the selection
2365 StringBuilder status = new StringBuilder(64);
2366 List<SequenceI> seqs = stretchGroup.getSequences();
2367 String name = seqs.get(0).getName();
2368 if (name.length() > 20)
2370 name = name.substring(0, 20);
2372 status.append(name).append(" - ");
2373 name = seqs.get(seqs.size() - 1).getName();
2374 if (name.length() > 20)
2376 name = name.substring(0, 20);
2378 status.append(name).append(" ");
2379 int startRes = stretchGroup.getStartRes();
2380 status.append(" cols ").append(String.valueOf(startRes + 1))
2382 int endRes = stretchGroup.getEndRes();
2383 status.append(String.valueOf(endRes + 1));
2384 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2385 .append(String.valueOf(endRes - startRes + 1)).append(")");
2386 ap.alignFrame.setStatus(status.toString());
2390 * Stops the scroll thread if it is running
2392 void stopScrolling()
2394 if (scrollThread != null)
2396 scrollThread.stopScrolling();
2397 scrollThread = null;
2399 mouseDragging = false;
2403 * Starts a thread to scroll the alignment, towards a given mouse position
2404 * outside the panel bounds, unless the alignment is in wrapped mode
2408 void startScrolling(Point mousePos)
2411 * set this.mouseDragging in case this was called from
2412 * a drag in ScalePanel or AnnotationPanel
2414 mouseDragging = true;
2415 if (!av.getWrapAlignment() && scrollThread == null)
2417 scrollThread = new ScrollThread();
2418 scrollThread.setMousePosition(mousePos);
2419 if (Platform.isJS())
2422 * Javascript - run every 20ms until scrolling stopped
2423 * or reaches the limit of scrollable alignment
2425 Timer t = new Timer(20, new ActionListener()
2428 public void actionPerformed(ActionEvent e)
2430 if (scrollThread != null)
2432 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2433 scrollThread.scrollOnce();
2435 if (scrollThread == null)
2437 // SeqPanel.stopScrolling called
2438 ((Timer) e.getSource()).stop();
2447 * Java - run in a new thread
2449 scrollThread.start();
2455 * Performs scrolling of the visible alignment left, right, up or down, until
2456 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2457 * limit of the alignment is reached
2459 class ScrollThread extends Thread
2461 private Point mousePos;
2463 private volatile boolean keepRunning = true;
2468 public ScrollThread()
2470 setName("SeqPanel$ScrollThread");
2474 * Sets the position of the mouse that determines the direction of the
2475 * scroll to perform. If this is called as the mouse moves, scrolling should
2476 * respond accordingly. For example, if the mouse is dragged right, scroll
2477 * right should start; if the drag continues down, scroll down should also
2482 public void setMousePosition(Point p)
2488 * Sets a flag that will cause the thread to exit
2490 public void stopScrolling()
2492 keepRunning = false;
2496 * Scrolls the alignment left or right, and/or up or down, depending on the
2497 * last notified mouse position, until the limit of the alignment is
2498 * reached, or a flag is set to stop the scroll
2505 if (mousePos != null)
2507 keepRunning = scrollOnce();
2512 } catch (Exception ex)
2516 SeqPanel.this.scrollThread = null;
2522 * <li>one row up, if the mouse is above the panel</li>
2523 * <li>one row down, if the mouse is below the panel</li>
2524 * <li>one column left, if the mouse is left of the panel</li>
2525 * <li>one column right, if the mouse is right of the panel</li>
2527 * Answers true if a scroll was performed, false if not - meaning either
2528 * that the mouse position is within the panel, or the edge of the alignment
2531 boolean scrollOnce()
2534 * quit after mouseUp ensures interrupt in JalviewJS
2541 boolean scrolled = false;
2542 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2549 // mouse is above this panel - try scroll up
2550 scrolled = ranges.scrollUp(true);
2552 else if (mousePos.y >= getHeight())
2554 // mouse is below this panel - try scroll down
2555 scrolled = ranges.scrollUp(false);
2559 * scroll left or right
2563 scrolled |= ranges.scrollRight(false);
2565 else if (mousePos.x >= getWidth())
2567 scrolled |= ranges.scrollRight(true);
2574 * modify current selection according to a received message.
2577 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2578 HiddenColumns hidden, SelectionSource source)
2580 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2581 // handles selection messages...
2582 // TODO: extend config options to allow user to control if selections may be
2583 // shared between viewports.
2584 boolean iSentTheSelection = (av == source
2585 || (source instanceof AlignViewport
2586 && ((AlignmentViewport) source).getSequenceSetId()
2587 .equals(av.getSequenceSetId())));
2589 if (iSentTheSelection)
2591 // respond to our own event by updating dependent dialogs
2592 if (ap.getCalculationDialog() != null)
2594 ap.getCalculationDialog().validateCalcTypes();
2600 // process further ?
2601 if (!av.followSelection)
2607 * Ignore the selection if there is one of our own pending.
2609 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2615 * Check for selection in a view of which this one is a dna/protein
2618 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2623 // do we want to thread this ? (contention with seqsel and colsel locks, I
2626 * only copy colsel if there is a real intersection between
2627 * sequence selection and this panel's alignment
2629 boolean repaint = false;
2630 boolean copycolsel = false;
2632 SequenceGroup sgroup = null;
2633 if (seqsel != null && seqsel.getSize() > 0)
2635 if (av.getAlignment() == null)
2637 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2638 + " ViewId=" + av.getViewId()
2639 + " 's alignment is NULL! returning immediately.");
2642 sgroup = seqsel.intersect(av.getAlignment(),
2643 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2644 if ((sgroup != null && sgroup.getSize() > 0))
2649 if (sgroup != null && sgroup.getSize() > 0)
2651 av.setSelectionGroup(sgroup);
2655 av.setSelectionGroup(null);
2657 av.isSelectionGroupChanged(true);
2662 // the current selection is unset or from a previous message
2663 // so import the new colsel.
2664 if (colsel == null || colsel.isEmpty())
2666 if (av.getColumnSelection() != null)
2668 av.getColumnSelection().clear();
2674 // TODO: shift colSel according to the intersecting sequences
2675 if (av.getColumnSelection() == null)
2677 av.setColumnSelection(new ColumnSelection(colsel));
2681 av.getColumnSelection().setElementsFrom(colsel,
2682 av.getAlignment().getHiddenColumns());
2685 av.isColSelChanged(true);
2689 if (copycolsel && av.hasHiddenColumns()
2690 && (av.getAlignment().getHiddenColumns() == null))
2692 System.err.println("Bad things");
2694 if (repaint) // always true!
2696 // probably finessing with multiple redraws here
2697 PaintRefresher.Refresh(this, av.getSequenceSetId());
2698 // ap.paintAlignment(false);
2701 // lastly, update dependent dialogs
2702 if (ap.getCalculationDialog() != null)
2704 ap.getCalculationDialog().validateCalcTypes();
2710 * If this panel is a cdna/protein translation view of the selection source,
2711 * tries to map the source selection to a local one, and returns true. Else
2718 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2719 ColumnSelection colsel, HiddenColumns hidden,
2720 SelectionSource source)
2722 if (!(source instanceof AlignViewportI))
2726 final AlignViewportI sourceAv = (AlignViewportI) source;
2727 if (sourceAv.getCodingComplement() != av
2728 && av.getCodingComplement() != sourceAv)
2734 * Map sequence selection
2736 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2737 av.setSelectionGroup(sg);
2738 av.isSelectionGroupChanged(true);
2741 * Map column selection
2743 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2745 ColumnSelection cs = new ColumnSelection();
2746 HiddenColumns hs = new HiddenColumns();
2747 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2748 av.setColumnSelection(cs);
2749 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2751 // lastly, update any dependent dialogs
2752 if (ap.getCalculationDialog() != null)
2754 ap.getCalculationDialog().validateCalcTypes();
2758 * repaint alignment, and also Overview or Structure
2759 * if hidden column selection has changed
2761 ap.paintAlignment(hiddenChanged, hiddenChanged);
2768 * @return null or last search results handled by this panel
2770 public SearchResultsI getLastSearchResults()
2772 return lastSearchResults;
2776 * scroll to the given row/column - or nearest visible location
2781 public void scrollTo(int row, int column)
2784 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2785 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2786 ap.scrollTo(column, column, row, true, true);
2790 * scroll to the given row - or nearest visible location
2794 public void scrollToRow(int row)
2797 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2798 ap.scrollTo(ap.av.getRanges().getStartRes(),
2799 ap.av.getRanges().getStartRes(), row, true, true);
2803 * scroll to the given column - or nearest visible location
2807 public void scrollToColumn(int column)
2810 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2811 ap.scrollTo(column, column, ap.av.getRanges().getStartSeq(), true,