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;
80 * @version $Revision: 1.130 $
82 public class SeqPanel extends JPanel
83 implements MouseListener, MouseMotionListener, MouseWheelListener,
84 SequenceListener, SelectionListener
87 * a class that holds computed mouse position
88 * - column of the alignment (0...)
89 * - sequence offset (0...)
90 * - annotation row offset (0...)
91 * where annotation offset is -1 unless the alignment is shown
92 * in wrapped mode, annotations are shown, and the mouse is
93 * over an annnotation row
98 * alignment column position of cursor (0...)
103 * index in alignment of sequence under cursor,
104 * or nearest above if cursor is not over a sequence
109 * index in annotations array of annotation under the cursor
110 * (only possible in wrapped mode with annotations shown),
111 * or -1 if cursor is not over an annotation row
113 final int annotationIndex;
115 MousePos(int col, int seq, int ann)
119 annotationIndex = ann;
122 boolean isOverAnnotation()
124 return annotationIndex != -1;
128 public boolean equals(Object obj)
130 if (obj == null || !(obj instanceof MousePos))
134 MousePos o = (MousePos) obj;
135 boolean b = (column == o.column && seqIndex == o.seqIndex
136 && annotationIndex == o.annotationIndex);
141 * A simple hashCode that ensures that instances that satisfy equals() have
145 public int hashCode()
147 return column + seqIndex + annotationIndex;
151 * toString method for debug output purposes only
154 public String toString()
156 return String.format("c%d:s%d:a%d", column, seqIndex,
161 private static final int MAX_TOOLTIP_LENGTH = 300;
163 public SeqCanvas seqCanvas;
165 public AlignmentPanel ap;
168 * last position for mouseMoved event
170 private MousePos lastMousePosition;
172 protected int editLastRes;
174 protected int editStartSeq;
176 protected AlignViewport av;
178 ScrollThread scrollThread = null;
180 boolean mouseDragging = false;
182 boolean editingSeqs = false;
184 boolean groupEditing = false;
186 // ////////////////////////////////////////
187 // ///Everything below this is for defining the boundary of the rubberband
188 // ////////////////////////////////////////
191 boolean changeEndSeq = false;
193 boolean changeStartSeq = false;
195 boolean changeEndRes = false;
197 boolean changeStartRes = false;
199 SequenceGroup stretchGroup = null;
201 boolean remove = false;
203 Point lastMousePress;
205 boolean mouseWheelPressed = false;
207 StringBuffer keyboardNo1;
209 StringBuffer keyboardNo2;
211 java.net.URL linkImageURL;
213 private final SequenceAnnotationReport seqARep;
215 StringBuilder tooltipText = new StringBuilder();
219 EditCommand editCommand;
221 StructureSelectionManager ssm;
223 SearchResultsI lastSearchResults;
226 * Creates a new SeqPanel object
231 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
233 linkImageURL = getClass().getResource("/images/link.gif");
234 seqARep = new SequenceAnnotationReport(linkImageURL.toString());
235 ToolTipManager.sharedInstance().registerComponent(this);
236 ToolTipManager.sharedInstance().setInitialDelay(0);
237 ToolTipManager.sharedInstance().setDismissDelay(10000);
241 setBackground(Color.white);
243 seqCanvas = new SeqCanvas(alignPanel);
244 setLayout(new BorderLayout());
245 add(seqCanvas, BorderLayout.CENTER);
247 this.ap = alignPanel;
249 if (!viewport.isDataset())
251 addMouseMotionListener(this);
252 addMouseListener(this);
253 addMouseWheelListener(this);
254 ssm = viewport.getStructureSelectionManager();
255 ssm.addStructureViewerListener(this);
256 ssm.addSelectionListener(this);
260 int startWrapBlock = -1;
262 int wrappedBlock = -1;
265 * Computes the column and sequence row (and possibly annotation row when in
266 * wrapped mode) for the given mouse position
271 MousePos findMousePosition(MouseEvent evt)
273 int col = findColumn(evt);
278 int charHeight = av.getCharHeight();
279 int alignmentHeight = av.getAlignment().getHeight();
280 if (av.getWrapAlignment())
282 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
283 seqCanvas.getHeight());
286 * yPos modulo height of repeating width
288 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
291 * height of sequences plus space / scale above,
292 * plus gap between sequences and annotations
294 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
295 + alignmentHeight * charHeight
296 + SeqCanvas.SEQS_ANNOTATION_GAP;
297 if (yOffsetPx >= alignmentHeightPixels)
300 * mouse is over annotations; find annotation index, also set
301 * last sequence above (for backwards compatible behaviour)
303 AlignmentAnnotation[] anns = av.getAlignment()
304 .getAlignmentAnnotation();
305 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
306 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
307 seqIndex = alignmentHeight - 1;
312 * mouse is over sequence (or the space above sequences)
314 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
317 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
323 ViewportRanges ranges = av.getRanges();
324 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
325 alignmentHeight - 1);
326 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
329 return new MousePos(col, seqIndex, annIndex);
332 * Returns the aligned sequence position (base 0) at the mouse position, or
333 * the closest visible one
338 int findColumn(MouseEvent evt)
343 final int startRes = av.getRanges().getStartRes();
344 final int charWidth = av.getCharWidth();
346 if (av.getWrapAlignment())
348 int hgap = av.getCharHeight();
349 if (av.getScaleAboveWrapped())
351 hgap += av.getCharHeight();
354 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
355 + hgap + seqCanvas.getAnnotationHeight();
358 y = Math.max(0, y - hgap);
359 x -= seqCanvas.getLabelWidthWest();
362 // mouse is over left scale
366 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
371 if (x >= cwidth * charWidth)
373 // mouse is over right scale
377 wrappedBlock = y / cHeight;
378 wrappedBlock += startRes / cwidth;
379 // allow for wrapped view scrolled right (possible from Overview)
380 int startOffset = startRes % cwidth;
381 res = wrappedBlock * cwidth + startOffset
382 + Math.min(cwidth - 1, x / charWidth);
387 * make sure we calculate relative to visible alignment,
388 * rather than right-hand gutter
390 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
391 res = (x / charWidth) + startRes;
392 res = Math.min(res, av.getRanges().getEndRes());
395 if (av.hasHiddenColumns())
397 res = av.getAlignment().getHiddenColumns()
398 .visibleToAbsoluteColumn(res);
405 * When all of a sequence of edits are complete, put the resulting edit list
406 * on the history stack (undo list), and reset flags for editing in progress.
412 if (editCommand != null && editCommand.getSize() > 0)
414 ap.alignFrame.addHistoryItem(editCommand);
415 av.firePropertyChange("alignment", null,
416 av.getAlignment().getSequences());
421 * Tidy up come what may...
426 groupEditing = false;
435 seqCanvas.cursorY = getKeyboardNo1() - 1;
436 scrollToVisible(true);
439 void setCursorColumn()
441 seqCanvas.cursorX = getKeyboardNo1() - 1;
442 scrollToVisible(true);
445 void setCursorRowAndColumn()
447 if (keyboardNo2 == null)
449 keyboardNo2 = new StringBuffer();
453 seqCanvas.cursorX = getKeyboardNo1() - 1;
454 seqCanvas.cursorY = getKeyboardNo2() - 1;
455 scrollToVisible(true);
459 void setCursorPosition()
461 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
463 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
464 scrollToVisible(true);
467 void moveCursor(int dx, int dy)
469 seqCanvas.cursorX += dx;
470 seqCanvas.cursorY += dy;
472 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
474 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
476 int original = seqCanvas.cursorX - dx;
477 int maxWidth = av.getAlignment().getWidth();
479 if (!hidden.isVisible(seqCanvas.cursorX))
481 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
482 int[] region = hidden.getRegionWithEdgeAtRes(visx);
484 if (region != null) // just in case
489 seqCanvas.cursorX = region[1] + 1;
494 seqCanvas.cursorX = region[0] - 1;
497 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
500 if (seqCanvas.cursorX >= maxWidth
501 || !hidden.isVisible(seqCanvas.cursorX))
503 seqCanvas.cursorX = original;
507 scrollToVisible(false);
511 * Scroll to make the cursor visible in the viewport.
514 * just jump to the location rather than scrolling
516 void scrollToVisible(boolean jump)
518 if (seqCanvas.cursorX < 0)
520 seqCanvas.cursorX = 0;
522 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
524 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
527 if (seqCanvas.cursorY < 0)
529 seqCanvas.cursorY = 0;
531 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
533 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
538 boolean repaintNeeded = true;
541 // only need to repaint if the viewport did not move, as otherwise it will
543 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
548 if (av.getWrapAlignment())
550 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
551 int x = av.getAlignment().getHiddenColumns()
552 .absoluteToVisibleColumn(seqCanvas.cursorX);
553 av.getRanges().scrollToWrappedVisible(x);
557 av.getRanges().scrollToVisible(seqCanvas.cursorX,
562 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
564 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
565 seqCanvas.cursorX, seqCanvas.cursorY);
575 void setSelectionAreaAtCursor(boolean topLeft)
577 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
579 if (av.getSelectionGroup() != null)
581 SequenceGroup sg = av.getSelectionGroup();
582 // Find the top and bottom of this group
583 int min = av.getAlignment().getHeight(), max = 0;
584 for (int i = 0; i < sg.getSize(); i++)
586 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
601 sg.setStartRes(seqCanvas.cursorX);
602 if (sg.getEndRes() < seqCanvas.cursorX)
604 sg.setEndRes(seqCanvas.cursorX);
607 min = seqCanvas.cursorY;
611 sg.setEndRes(seqCanvas.cursorX);
612 if (sg.getStartRes() > seqCanvas.cursorX)
614 sg.setStartRes(seqCanvas.cursorX);
617 max = seqCanvas.cursorY + 1;
622 // Only the user can do this
623 av.setSelectionGroup(null);
627 // Now add any sequences between min and max
628 sg.getSequences(null).clear();
629 for (int i = min; i < max; i++)
631 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
636 if (av.getSelectionGroup() == null)
638 SequenceGroup sg = new SequenceGroup();
639 sg.setStartRes(seqCanvas.cursorX);
640 sg.setEndRes(seqCanvas.cursorX);
641 sg.addSequence(sequence, false);
642 av.setSelectionGroup(sg);
645 ap.paintAlignment(false, false);
649 void insertGapAtCursor(boolean group)
651 groupEditing = group;
652 editStartSeq = seqCanvas.cursorY;
653 editLastRes = seqCanvas.cursorX;
654 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
658 void deleteGapAtCursor(boolean group)
660 groupEditing = group;
661 editStartSeq = seqCanvas.cursorY;
662 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
663 editSequence(false, false, seqCanvas.cursorX);
667 void insertNucAtCursor(boolean group, String nuc)
669 // TODO not called - delete?
670 groupEditing = group;
671 editStartSeq = seqCanvas.cursorY;
672 editLastRes = seqCanvas.cursorX;
673 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
677 void numberPressed(char value)
679 if (keyboardNo1 == null)
681 keyboardNo1 = new StringBuffer();
684 if (keyboardNo2 != null)
686 keyboardNo2.append(value);
690 keyboardNo1.append(value);
698 if (keyboardNo1 != null)
700 int value = Integer.parseInt(keyboardNo1.toString());
704 } catch (Exception x)
715 if (keyboardNo2 != null)
717 int value = Integer.parseInt(keyboardNo2.toString());
721 } catch (Exception x)
735 public void mouseReleased(MouseEvent evt)
737 MousePos pos = findMousePosition(evt);
738 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
743 boolean didDrag = mouseDragging; // did we come here after a drag
744 mouseDragging = false;
745 mouseWheelPressed = false;
747 if (evt.isPopupTrigger()) // Windows: mouseReleased
749 showPopupMenu(evt, pos);
760 doMouseReleasedDefineMode(evt, didDrag);
771 public void mousePressed(MouseEvent evt)
773 lastMousePress = evt.getPoint();
774 MousePos pos = findMousePosition(evt);
775 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
780 if (SwingUtilities.isMiddleMouseButton(evt))
782 mouseWheelPressed = true;
786 boolean isControlDown = Platform.isControlDown(evt);
787 if (evt.isShiftDown() || isControlDown)
797 doMousePressedDefineMode(evt, pos);
801 int seq = pos.seqIndex;
802 int res = pos.column;
804 if ((seq < av.getAlignment().getHeight())
805 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
821 private String formattedTooltipText;
824 public void mouseOverSequence(SequenceI sequence, int index, int pos)
826 String tmp = sequence.hashCode() + " " + index + " " + pos;
828 if (lastMessage == null || !lastMessage.equals(tmp))
830 // System.err.println("mouseOver Sequence: "+tmp);
831 ssm.mouseOverSequence(sequence, index, pos, av);
837 * Highlight the mapped region described by the search results object (unless
838 * unchanged). This supports highlight of protein while mousing over linked
839 * cDNA and vice versa. The status bar is also updated to show the location of
840 * the start of the highlighted region.
843 public void highlightSequence(SearchResultsI results)
845 if (results == null || results.equals(lastSearchResults))
849 lastSearchResults = results;
851 boolean wasScrolled = false;
853 if (av.isFollowHighlight())
855 // don't allow highlight of protein/cDNA to also scroll a complementary
856 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
857 // over residue to change abruptly, causing highlighted residue in panel 2
858 // to change, causing a scroll in panel 1 etc)
859 ap.setToScrollComplementPanel(false);
860 wasScrolled = ap.scrollToPosition(results);
863 seqCanvas.revalidate();
865 ap.setToScrollComplementPanel(true);
868 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
869 if (seqCanvas.highlightSearchResults(results, fastPaint))
871 setStatusMessage(results);
876 public VamsasSource getVamsasSource()
878 return this.ap == null ? null : this.ap.av;
882 public void updateColours(SequenceI seq, int index)
884 System.out.println("update the seqPanel colours");
889 * Action on mouse movement is to update the status bar to show the current
890 * sequence position, and (if features are shown) to show any features at the
891 * position in a tooltip. Does nothing if the mouse move does not change
897 public void mouseMoved(MouseEvent evt)
901 // This is because MacOSX creates a mouseMoved
902 // If control is down, other platforms will not.
906 final MousePos mousePos = findMousePosition(evt);
907 if (mousePos.equals(lastMousePosition))
910 * just a pixel move without change of 'cell'
914 lastMousePosition = mousePos;
916 if (mousePos.isOverAnnotation())
918 mouseMovedOverAnnotation(mousePos);
921 final int seq = mousePos.seqIndex;
923 final int column = mousePos.column;
924 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
926 lastMousePosition = null;
927 setToolTipText(null);
929 ap.alignFrame.setStatus("");
933 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
935 if (column >= sequence.getLength())
941 * set status bar message, returning residue position in sequence
943 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
944 final int pos = setStatusMessage(sequence, column, seq);
945 if (ssm != null && !isGapped)
947 mouseOverSequence(sequence, column, pos);
950 tooltipText.setLength(6); // "<html>"
952 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
955 for (int g = 0; g < groups.length; g++)
957 if (groups[g].getStartRes() <= column
958 && groups[g].getEndRes() >= column)
960 if (!groups[g].getName().startsWith("JTreeGroup")
961 && !groups[g].getName().startsWith("JGroup"))
963 tooltipText.append(groups[g].getName());
966 if (groups[g].getDescription() != null)
968 tooltipText.append(": " + groups[g].getDescription());
975 * add any features at the position to the tooltip; if over a gap, only
976 * add features that straddle the gap (pos may be the residue before or
979 if (av.isShowSequenceFeatures())
981 List<SequenceFeature> features = ap.getFeatureRenderer()
982 .findFeaturesAtColumn(sequence, column + 1);
983 seqARep.appendFeatures(tooltipText, pos, features,
984 this.ap.getSeqPanel().seqCanvas.fr);
986 if (tooltipText.length() == 6) // <html>
988 setToolTipText(null);
993 if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
995 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
996 tooltipText.append("...");
998 String textString = tooltipText.toString();
999 if (lastTooltip == null || !lastTooltip.equals(textString))
1001 formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1003 setToolTipText(formattedTooltipText);
1004 lastTooltip = textString;
1010 * When the view is in wrapped mode, and the mouse is over an annotation row,
1011 * shows the corresponding tooltip and status message (if any)
1016 protected void mouseMovedOverAnnotation(MousePos pos)
1018 final int column = pos.column;
1019 final int rowIndex = pos.annotationIndex;
1021 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1026 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1028 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1030 setToolTipText(tooltip);
1031 lastTooltip = tooltip;
1033 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1035 ap.alignFrame.setStatus(msg);
1038 private Point lastp = null;
1040 private JToolTip tempTip = new JLabel().createToolTip();
1045 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1048 public Point getToolTipLocation(MouseEvent event)
1052 if (tooltipText == null || tooltipText.length() <= 6)
1057 if (lastp != null && event.isShiftDown())
1063 int x = event.getX();
1064 int y = event.getY();
1067 tempTip.setTipText(formattedTooltipText);
1068 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1070 // was x += (w - x < 200) ? -(w / 2) : 5;
1071 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1072 p = new Point(x, y + 20); // BH 2018 was - 20?
1080 * set when the current UI interaction has resulted in a change that requires
1081 * shading in overviews and structures to be recalculated. this could be
1082 * changed to a something more expressive that indicates what actually has
1083 * changed, so selective redraws can be applied (ie. only structures, only
1086 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1089 * set if av.getSelectionGroup() refers to a group that is defined on the
1090 * alignment view, rather than a transient selection
1092 // private boolean editingDefinedGroup = false; // TODO: refactor to
1093 // avcontroller or viewModel
1096 * Sets the status message in alignment panel, showing the sequence number
1097 * (index) and id, and residue and residue position if not at a gap, for the
1098 * given sequence and column position. Returns the residue position returned
1099 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1100 * if at a gapped position.
1103 * aligned sequence object
1107 * index of sequence in alignment
1108 * @return sequence position of residue at column, or adjacent residue if at a
1111 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1113 char sequenceChar = sequence.getCharAt(column);
1114 int pos = sequence.findPosition(column);
1115 setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1121 * Builds the status message for the current cursor location and writes it to
1122 * the status bar, for example
1125 * Sequence 3 ID: FER1_SOLLC
1126 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1127 * Sequence 5 ID: FER1_PEA Residue: B (3)
1128 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1133 * sequence position in the alignment (1..)
1134 * @param sequenceChar
1135 * the character under the cursor
1137 * the sequence residue position (if not over a gap)
1139 protected void setStatusMessage(SequenceI sequence, int seqIndex,
1140 char sequenceChar, int residuePos)
1142 StringBuilder text = new StringBuilder(32);
1145 * Sequence number (if known), and sequence name.
1147 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1148 text.append("Sequence").append(seqno).append(" ID: ")
1149 .append(sequence.getName());
1151 String residue = null;
1154 * Try to translate the display character to residue name (null for gap).
1156 boolean isGapped = Comparison.isGap(sequenceChar);
1160 boolean nucleotide = av.getAlignment().isNucleotide();
1161 String displayChar = String.valueOf(sequenceChar);
1164 residue = ResidueProperties.nucleotideName.get(displayChar);
1168 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1169 : ("*".equals(displayChar) ? "STOP"
1170 : ResidueProperties.aa2Triplet.get(displayChar));
1172 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1173 .append(": ").append(residue == null ? displayChar : residue);
1175 text.append(" (").append(Integer.toString(residuePos)).append(")");
1177 ap.alignFrame.setStatus(text.toString());
1181 * Set the status bar message to highlight the first matched position in
1186 private void setStatusMessage(SearchResultsI results)
1188 AlignmentI al = this.av.getAlignment();
1189 int sequenceIndex = al.findIndex(results);
1190 if (sequenceIndex == -1)
1194 SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1195 for (SearchResultMatchI m : results.getResults())
1197 SequenceI seq = m.getSequence();
1198 if (seq.getDatasetSequence() != null)
1200 seq = seq.getDatasetSequence();
1205 int start = m.getStart();
1206 setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1217 public void mouseDragged(MouseEvent evt)
1219 MousePos pos = findMousePosition(evt);
1220 if (pos.isOverAnnotation() || pos.column == -1)
1225 if (mouseWheelPressed)
1227 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1228 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1230 int oldWidth = av.getCharWidth();
1232 // Which is bigger, left-right or up-down?
1233 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1234 .abs(evt.getX() - lastMousePress.getX()))
1237 * on drag up or down, decrement or increment font size
1239 int fontSize = av.font.getSize();
1240 boolean fontChanged = false;
1242 if (evt.getY() < lastMousePress.getY())
1247 else if (evt.getY() > lastMousePress.getY())
1260 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1262 av.setFont(newFont, true);
1263 av.setCharWidth(oldWidth);
1267 ap.av.getCodingComplement().setFont(newFont, true);
1268 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1269 .getSplitViewContainer();
1270 splitFrame.adjustLayout();
1271 splitFrame.repaint();
1278 * on drag left or right, decrement or increment character width
1281 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1283 newWidth = av.getCharWidth() - 1;
1284 av.setCharWidth(newWidth);
1286 else if (evt.getX() > lastMousePress.getX())
1288 newWidth = av.getCharWidth() + 1;
1289 av.setCharWidth(newWidth);
1293 ap.paintAlignment(false, false);
1297 * need to ensure newWidth is set on cdna, regardless of which
1298 * panel the mouse drag happened in; protein will compute its
1299 * character width as 1:1 or 3:1
1301 av.getCodingComplement().setCharWidth(newWidth);
1302 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1303 .getSplitViewContainer();
1304 splitFrame.adjustLayout();
1305 splitFrame.repaint();
1310 FontMetrics fm = getFontMetrics(av.getFont());
1311 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1313 lastMousePress = evt.getPoint();
1320 dragStretchGroup(evt);
1324 int res = pos.column;
1331 if ((editLastRes == -1) || (editLastRes == res))
1336 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1338 // dragLeft, delete gap
1339 editSequence(false, false, res);
1343 editSequence(true, false, res);
1346 mouseDragging = true;
1347 if (scrollThread != null)
1349 scrollThread.setMousePosition(evt.getPoint());
1354 * Edits the sequence to insert or delete one or more gaps, in response to a
1355 * mouse drag or cursor mode command. The number of inserts/deletes may be
1356 * specified with the cursor command, or else depends on the mouse event
1357 * (normally one column, but potentially more for a fast mouse drag).
1359 * Delete gaps is limited to the number of gaps left of the cursor position
1360 * (mouse drag), or at or right of the cursor position (cursor mode).
1362 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1363 * the current selection group.
1365 * In locked editing mode (with a selection group present), inserts/deletions
1366 * within the selection group are limited to its boundaries (and edits outside
1367 * the group stop at its border).
1370 * true to insert gaps, false to delete gaps
1372 * (unused parameter)
1374 * the column at which to perform the action; the number of columns
1375 * affected depends on <code>this.editLastRes</code> (cursor column
1378 synchronized void editSequence(boolean insertGap, boolean editSeq,
1382 int fixedRight = -1;
1383 boolean fixedColumns = false;
1384 SequenceGroup sg = av.getSelectionGroup();
1386 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1388 // No group, but the sequence may represent a group
1389 if (!groupEditing && av.hasHiddenRows())
1391 if (av.isHiddenRepSequence(seq))
1393 sg = av.getRepresentedSequences(seq);
1394 groupEditing = true;
1398 StringBuilder message = new StringBuilder(64); // for status bar
1401 * make a name for the edit action, for
1402 * status bar message and Undo/Redo menu
1404 String label = null;
1407 message.append("Edit group:");
1408 label = MessageManager.getString("action.edit_group");
1412 message.append("Edit sequence: " + seq.getName());
1413 label = seq.getName();
1414 if (label.length() > 10)
1416 label = label.substring(0, 10);
1418 label = MessageManager.formatMessage("label.edit_params",
1424 * initialise the edit command if there is not
1425 * already one being extended
1427 if (editCommand == null)
1429 editCommand = new EditCommand(label);
1434 message.append(" insert ");
1438 message.append(" delete ");
1441 message.append(Math.abs(startres - editLastRes) + " gaps.");
1442 ap.alignFrame.setStatus(message.toString());
1445 * is there a selection group containing the sequence being edited?
1446 * if so the boundary of the group is the limit of the edit
1447 * (but the edit may be inside or outside the selection group)
1449 boolean inSelectionGroup = sg != null
1450 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1451 if (groupEditing || inSelectionGroup)
1453 fixedColumns = true;
1455 // sg might be null as the user may only see 1 sequence,
1456 // but the sequence represents a group
1459 if (!av.isHiddenRepSequence(seq))
1464 sg = av.getRepresentedSequences(seq);
1467 fixedLeft = sg.getStartRes();
1468 fixedRight = sg.getEndRes();
1470 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1471 || (startres >= fixedLeft && editLastRes < fixedLeft)
1472 || (startres > fixedRight && editLastRes <= fixedRight)
1473 || (startres <= fixedRight && editLastRes > fixedRight))
1479 if (fixedLeft > startres)
1481 fixedRight = fixedLeft - 1;
1484 else if (fixedRight < startres)
1486 fixedLeft = fixedRight;
1491 if (av.hasHiddenColumns())
1493 fixedColumns = true;
1494 int y1 = av.getAlignment().getHiddenColumns()
1495 .getNextHiddenBoundary(true, startres);
1496 int y2 = av.getAlignment().getHiddenColumns()
1497 .getNextHiddenBoundary(false, startres);
1499 if ((insertGap && startres > y1 && editLastRes < y1)
1500 || (!insertGap && startres < y2 && editLastRes > y2))
1506 // Selection spans a hidden region
1507 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1515 fixedRight = y2 - 1;
1520 boolean success = doEditSequence(insertGap, editSeq, startres,
1521 fixedRight, fixedColumns, sg);
1524 * report what actually happened (might be less than
1525 * what was requested), by inspecting the edit commands added
1527 String msg = getEditStatusMessage(editCommand);
1528 ap.alignFrame.setStatus(msg == null ? " " : msg);
1534 editLastRes = startres;
1535 seqCanvas.repaint();
1539 * A helper method that performs the requested editing to insert or delete
1540 * gaps (if possible). Answers true if the edit was successful, false if could
1541 * only be performed in part or not at all. Failure may occur in 'locked edit'
1542 * mode, when an insertion requires a matching gapped position (or column) to
1543 * delete, and deletion requires an adjacent gapped position (or column) to
1547 * true if inserting gap(s), false if deleting
1549 * (unused parameter, currently always false)
1551 * the column at which to perform the edit
1553 * fixed right boundary column of a locked edit (within or to the
1554 * left of a selection group)
1555 * @param fixedColumns
1556 * true if this is a locked edit
1558 * the sequence group (if group edit is being performed)
1561 protected boolean doEditSequence(final boolean insertGap,
1562 final boolean editSeq, final int startres, int fixedRight,
1563 final boolean fixedColumns, final SequenceGroup sg)
1565 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1566 SequenceI[] seqs = new SequenceI[] { seq };
1570 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1571 int g, groupSize = vseqs.size();
1572 SequenceI[] groupSeqs = new SequenceI[groupSize];
1573 for (g = 0; g < groupSeqs.length; g++)
1575 groupSeqs[g] = vseqs.get(g);
1581 // If the user has selected the whole sequence, and is dragging to
1582 // the right, we can still extend the alignment and selectionGroup
1583 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1584 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1587 av.getAlignment().getWidth() + startres - editLastRes);
1588 fixedRight = sg.getEndRes();
1591 // Is it valid with fixed columns??
1592 // Find the next gap before the end
1593 // of the visible region boundary
1594 boolean blank = false;
1595 for (; fixedRight > editLastRes; fixedRight--)
1599 for (g = 0; g < groupSize; g++)
1601 for (int j = 0; j < startres - editLastRes; j++)
1604 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1619 if (sg.getSize() == av.getAlignment().getHeight())
1621 if ((av.hasHiddenColumns()
1622 && startres < av.getAlignment().getHiddenColumns()
1623 .getNextHiddenBoundary(false, startres)))
1628 int alWidth = av.getAlignment().getWidth();
1629 if (av.hasHiddenRows())
1631 int hwidth = av.getAlignment().getHiddenSequences()
1633 if (hwidth > alWidth)
1638 // We can still insert gaps if the selectionGroup
1639 // contains all the sequences
1640 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1641 fixedRight = alWidth + startres - editLastRes;
1651 else if (!insertGap)
1653 // / Are we able to delete?
1654 // ie are all columns blank?
1656 for (g = 0; g < groupSize; g++)
1658 for (int j = startres; j < editLastRes; j++)
1660 if (groupSeqs[g].getLength() <= j)
1665 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1667 // Not a gap, block edit not valid
1676 // dragging to the right
1677 if (fixedColumns && fixedRight != -1)
1679 for (int j = editLastRes; j < startres; j++)
1681 insertGap(j, groupSeqs, fixedRight);
1686 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1687 startres - editLastRes, false);
1692 // dragging to the left
1693 if (fixedColumns && fixedRight != -1)
1695 for (int j = editLastRes; j > startres; j--)
1697 deleteChar(startres, groupSeqs, fixedRight);
1702 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1703 editLastRes - startres, false);
1710 * editing a single sequence
1714 // dragging to the right
1715 if (fixedColumns && fixedRight != -1)
1717 for (int j = editLastRes; j < startres; j++)
1719 if (!insertGap(j, seqs, fixedRight))
1722 * e.g. cursor mode command specified
1723 * more inserts than are possible
1731 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1732 startres - editLastRes, false);
1739 // dragging to the left
1740 if (fixedColumns && fixedRight != -1)
1742 for (int j = editLastRes; j > startres; j--)
1744 if (!Comparison.isGap(seq.getCharAt(startres)))
1748 deleteChar(startres, seqs, fixedRight);
1753 // could be a keyboard edit trying to delete none gaps
1755 for (int m = startres; m < editLastRes; m++)
1757 if (!Comparison.isGap(seq.getCharAt(m)))
1765 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1770 {// insertGap==false AND editSeq==TRUE;
1771 if (fixedColumns && fixedRight != -1)
1773 for (int j = editLastRes; j < startres; j++)
1775 insertGap(j, seqs, fixedRight);
1780 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1781 startres - editLastRes, false);
1791 * Constructs an informative status bar message while dragging to insert or
1792 * delete gaps. Answers null if inserts and deletes cancel out.
1794 * @param editCommand
1795 * a command containing the list of individual edits
1798 protected static String getEditStatusMessage(EditCommand editCommand)
1800 if (editCommand == null)
1806 * add any inserts, and subtract any deletes,
1807 * not counting those auto-inserted when doing a 'locked edit'
1808 * (so only counting edits 'under the cursor')
1811 for (Edit cmd : editCommand.getEdits())
1813 if (!cmd.isSystemGenerated())
1815 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1823 * inserts and deletes cancel out
1828 String msgKey = count > 1 ? "label.insert_gaps"
1829 : (count == 1 ? "label.insert_gap"
1830 : (count == -1 ? "label.delete_gap"
1831 : "label.delete_gaps"));
1832 count = Math.abs(count);
1834 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1838 * Inserts one gap at column j, deleting the right-most gapped column up to
1839 * (and including) fixedColumn. Returns true if the edit is successful, false
1840 * if no blank column is available to allow the insertion to be balanced by a
1845 * @param fixedColumn
1848 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1850 int blankColumn = fixedColumn;
1851 for (int s = 0; s < seq.length; s++)
1853 // Find the next gap before the end of the visible region boundary
1854 // If lastCol > j, theres a boundary after the gap insertion
1856 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1858 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1860 // Theres a space, so break and insert the gap
1865 if (blankColumn <= j)
1867 blankColumn = fixedColumn;
1873 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1875 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1881 * Helper method to add and perform one edit action
1887 * @param systemGenerated
1888 * true if the edit is a 'balancing' delete (or insert) to match a
1889 * user's insert (or delete) in a locked editing region
1891 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1892 int count, boolean systemGenerated)
1895 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1896 av.getAlignment().getGapCharacter());
1897 edit.setSystemGenerated(systemGenerated);
1899 editCommand.appendEdit(edit, av.getAlignment(), true, null);
1903 * Deletes the character at column j, and inserts a gap at fixedColumn, in
1904 * each of the given sequences. The caller should ensure that all sequences
1905 * are gapped in column j.
1909 * @param fixedColumn
1911 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
1913 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
1915 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
1919 * On reentering the panel, stops any scrolling that was started on dragging
1925 public void mouseEntered(MouseEvent e)
1935 * On leaving the panel, if the mouse is being dragged, starts a thread to
1936 * scroll it until the mouse is released (in unwrapped mode only)
1941 public void mouseExited(MouseEvent e)
1943 lastMousePosition = null;
1944 ap.alignFrame.setStatus(" ");
1946 if (av.getWrapAlignment())
1952 * start scrolling if mouse dragging, whether the drag started
1953 * in the scale panel or this panel
1955 if (ap.getScalePanel().isMouseDragging())
1957 ap.getScalePanel().mouseExited(e);
1959 else if (mouseDragging && scrollThread == null)
1961 startScrolling(e.getPoint());
1966 * Handler for double-click on a position with one or more sequence features.
1967 * Opens the Amend Features dialog to allow feature details to be amended, or
1968 * the feature deleted.
1971 public void mouseClicked(MouseEvent evt)
1973 SequenceGroup sg = null;
1974 MousePos pos = findMousePosition(evt);
1975 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
1980 if (evt.getClickCount() > 1)
1982 sg = av.getSelectionGroup();
1983 if (sg != null && sg.getSize() == 1
1984 && sg.getEndRes() - sg.getStartRes() < 2)
1986 av.setSelectionGroup(null);
1989 int column = pos.column;
1992 * find features at the position (if not gapped), or straddling
1993 * the position (if at a gap)
1995 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
1996 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
1997 .findFeaturesAtColumn(sequence, column + 1);
1999 if (!features.isEmpty())
2002 * highlight the first feature at the position on the alignment
2004 SearchResultsI highlight = new SearchResults();
2005 highlight.addResult(sequence, features.get(0).getBegin(), features
2007 seqCanvas.highlightSearchResults(highlight, true);
2010 * open the Amend Features dialog
2012 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2013 false).showDialog();
2019 public void mouseWheelMoved(MouseWheelEvent e)
2022 double wheelRotation = e.getPreciseWheelRotation();
2023 if (wheelRotation > 0)
2025 if (e.isShiftDown())
2027 av.getRanges().scrollRight(true);
2032 av.getRanges().scrollUp(false);
2035 else if (wheelRotation < 0)
2037 if (e.isShiftDown())
2039 av.getRanges().scrollRight(false);
2043 av.getRanges().scrollUp(true);
2048 * update status bar and tooltip for new position
2049 * (need to synthesize a mouse movement to refresh tooltip)
2052 ToolTipManager.sharedInstance().mouseMoved(e);
2061 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2063 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2068 final int res = pos.column;
2069 final int seq = pos.seqIndex;
2071 updateOverviewAndStructs = false;
2073 startWrapBlock = wrappedBlock;
2075 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2077 if ((sequence == null) || (res > sequence.getLength()))
2082 stretchGroup = av.getSelectionGroup();
2084 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2086 stretchGroup = av.getAlignment().findGroup(sequence, res);
2087 if (stretchGroup != null)
2089 // only update the current selection if the popup menu has a group to
2091 av.setSelectionGroup(stretchGroup);
2096 * defer right-mouse click handling to mouseReleased on Windows
2097 * (where isPopupTrigger() will answer true)
2098 * NB isRightMouseButton is also true for Cmd-click on Mac
2100 if (Platform.isWinRightButton(evt))
2105 if (evt.isPopupTrigger()) // Mac: mousePressed
2107 showPopupMenu(evt, pos);
2113 seqCanvas.cursorX = res;
2114 seqCanvas.cursorY = seq;
2115 seqCanvas.repaint();
2119 if (stretchGroup == null)
2121 createStretchGroup(res, sequence);
2124 if (stretchGroup != null)
2126 stretchGroup.addPropertyChangeListener(seqCanvas);
2129 seqCanvas.repaint();
2132 private void createStretchGroup(int res, SequenceI sequence)
2134 // Only if left mouse button do we want to change group sizes
2135 // define a new group here
2136 SequenceGroup sg = new SequenceGroup();
2137 sg.setStartRes(res);
2139 sg.addSequence(sequence, false);
2140 av.setSelectionGroup(sg);
2143 if (av.getConservationSelected())
2145 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2149 if (av.getAbovePIDThreshold())
2151 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2154 // TODO: stretchGroup will always be not null. Is this a merge error ?
2155 // or is there a threading issue here?
2156 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2158 // Edit end res position of selected group
2159 changeEndRes = true;
2161 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2163 // Edit end res position of selected group
2164 changeStartRes = true;
2166 stretchGroup.getWidth();
2171 * Build and show a pop-up menu at the right-click mouse position
2176 void showPopupMenu(MouseEvent evt, MousePos pos)
2178 final int column = pos.column;
2179 final int seq = pos.seqIndex;
2180 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2181 List<SequenceFeature> features = ap.getFeatureRenderer()
2182 .findFeaturesAtColumn(sequence, column + 1);
2184 PopupMenu pop = new PopupMenu(ap, null, features);
2185 pop.show(this, evt.getX(), evt.getY());
2189 * Update the display after mouse up on a selection or group
2192 * mouse released event details
2194 * true if this event is happening after a mouse drag (rather than a
2197 protected void doMouseReleasedDefineMode(MouseEvent evt,
2200 if (stretchGroup == null)
2205 stretchGroup.removePropertyChangeListener(seqCanvas);
2207 // always do this - annotation has own state
2208 // but defer colourscheme update until hidden sequences are passed in
2209 boolean vischange = stretchGroup.recalcConservation(true);
2210 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2212 if (stretchGroup.cs != null)
2216 stretchGroup.cs.alignmentChanged(stretchGroup,
2217 av.getHiddenRepSequences());
2220 ResidueShaderI groupColourScheme = stretchGroup
2221 .getGroupColourScheme();
2222 String name = stretchGroup.getName();
2223 if (stretchGroup.cs.conservationApplied())
2225 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2227 if (stretchGroup.cs.getThreshold() > 0)
2229 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2232 PaintRefresher.Refresh(this, av.getSequenceSetId());
2233 // TODO: structure colours only need updating if stretchGroup used to or now
2234 // does contain sequences with structure views
2235 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2236 updateOverviewAndStructs = false;
2237 changeEndRes = false;
2238 changeStartRes = false;
2239 stretchGroup = null;
2244 * Resizes the borders of a selection group depending on the direction of
2249 protected void dragStretchGroup(MouseEvent evt)
2251 if (stretchGroup == null)
2256 MousePos pos = findMousePosition(evt);
2257 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2262 int res = pos.column;
2263 int y = pos.seqIndex;
2265 if (wrappedBlock != startWrapBlock)
2270 res = Math.min(res, av.getAlignment().getWidth()-1);
2272 if (stretchGroup.getEndRes() == res)
2274 // Edit end res position of selected group
2275 changeEndRes = true;
2277 else if (stretchGroup.getStartRes() == res)
2279 // Edit start res position of selected group
2280 changeStartRes = true;
2283 if (res < av.getRanges().getStartRes())
2285 res = av.getRanges().getStartRes();
2290 if (res > (stretchGroup.getStartRes() - 1))
2292 stretchGroup.setEndRes(res);
2293 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2296 else if (changeStartRes)
2298 if (res < (stretchGroup.getEndRes() + 1))
2300 stretchGroup.setStartRes(res);
2301 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2305 int dragDirection = 0;
2311 else if (y < oldSeq)
2316 while ((y != oldSeq) && (oldSeq > -1)
2317 && (y < av.getAlignment().getHeight()))
2319 // This routine ensures we don't skip any sequences, as the
2320 // selection is quite slow.
2321 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2323 oldSeq += dragDirection;
2330 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2332 if (stretchGroup.getSequences(null).contains(nextSeq))
2334 stretchGroup.deleteSequence(seq, false);
2335 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2341 stretchGroup.addSequence(seq, false);
2344 stretchGroup.addSequence(nextSeq, false);
2345 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2354 mouseDragging = true;
2356 if (scrollThread != null)
2358 scrollThread.setMousePosition(evt.getPoint());
2362 * construct a status message showing the range of the selection
2364 StringBuilder status = new StringBuilder(64);
2365 List<SequenceI> seqs = stretchGroup.getSequences();
2366 String name = seqs.get(0).getName();
2367 if (name.length() > 20)
2369 name = name.substring(0, 20);
2371 status.append(name).append(" - ");
2372 name = seqs.get(seqs.size() - 1).getName();
2373 if (name.length() > 20)
2375 name = name.substring(0, 20);
2377 status.append(name).append(" ");
2378 int startRes = stretchGroup.getStartRes();
2379 status.append(" cols ").append(String.valueOf(startRes + 1))
2381 int endRes = stretchGroup.getEndRes();
2382 status.append(String.valueOf(endRes + 1));
2383 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2384 .append(String.valueOf(endRes - startRes + 1)).append(")");
2385 ap.alignFrame.setStatus(status.toString());
2389 * Stops the scroll thread if it is running
2391 void stopScrolling()
2393 if (scrollThread != null)
2395 scrollThread.stopScrolling();
2396 scrollThread = null;
2398 mouseDragging = false;
2402 * Starts a thread to scroll the alignment, towards a given mouse position
2403 * outside the panel bounds, unless the alignment is in wrapped mode
2407 void startScrolling(Point mousePos)
2410 * set this.mouseDragging in case this was called from
2411 * a drag in ScalePanel or AnnotationPanel
2413 mouseDragging = true;
2414 if (!av.getWrapAlignment() && scrollThread == null)
2416 scrollThread = new ScrollThread();
2417 scrollThread.setMousePosition(mousePos);
2418 if (Platform.isJS())
2421 * Javascript - run every 20ms until scrolling stopped
2422 * or reaches the limit of scrollable alignment
2424 Timer t = new Timer(20, new ActionListener()
2427 public void actionPerformed(ActionEvent e)
2429 if (scrollThread != null)
2431 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2432 scrollThread.scrollOnce();
2434 if (scrollThread == null)
2436 // SeqPanel.stopScrolling called
2437 ((Timer) e.getSource()).stop();
2446 * Java - run in a new thread
2448 scrollThread.start();
2454 * Performs scrolling of the visible alignment left, right, up or down, until
2455 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2456 * limit of the alignment is reached
2458 class ScrollThread extends Thread
2460 private Point mousePos;
2462 private volatile boolean keepRunning = true;
2467 public ScrollThread()
2469 setName("SeqPanel$ScrollThread");
2473 * Sets the position of the mouse that determines the direction of the
2474 * scroll to perform. If this is called as the mouse moves, scrolling should
2475 * respond accordingly. For example, if the mouse is dragged right, scroll
2476 * right should start; if the drag continues down, scroll down should also
2481 public void setMousePosition(Point p)
2487 * Sets a flag that will cause the thread to exit
2489 public void stopScrolling()
2491 keepRunning = false;
2495 * Scrolls the alignment left or right, and/or up or down, depending on the
2496 * last notified mouse position, until the limit of the alignment is
2497 * reached, or a flag is set to stop the scroll
2504 if (mousePos != null)
2506 keepRunning = scrollOnce();
2511 } catch (Exception ex)
2515 SeqPanel.this.scrollThread = null;
2521 * <li>one row up, if the mouse is above the panel</li>
2522 * <li>one row down, if the mouse is below the panel</li>
2523 * <li>one column left, if the mouse is left of the panel</li>
2524 * <li>one column right, if the mouse is right of the panel</li>
2526 * Answers true if a scroll was performed, false if not - meaning either
2527 * that the mouse position is within the panel, or the edge of the alignment
2530 boolean scrollOnce()
2533 * quit after mouseUp ensures interrupt in JalviewJS
2540 boolean scrolled = false;
2541 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2548 // mouse is above this panel - try scroll up
2549 scrolled = ranges.scrollUp(true);
2551 else if (mousePos.y >= getHeight())
2553 // mouse is below this panel - try scroll down
2554 scrolled = ranges.scrollUp(false);
2558 * scroll left or right
2562 scrolled |= ranges.scrollRight(false);
2564 else if (mousePos.x >= getWidth())
2566 scrolled |= ranges.scrollRight(true);
2573 * modify current selection according to a received message.
2576 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2577 HiddenColumns hidden, SelectionSource source)
2579 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2580 // handles selection messages...
2581 // TODO: extend config options to allow user to control if selections may be
2582 // shared between viewports.
2583 boolean iSentTheSelection = (av == source
2584 || (source instanceof AlignViewport
2585 && ((AlignmentViewport) source).getSequenceSetId()
2586 .equals(av.getSequenceSetId())));
2588 if (iSentTheSelection)
2590 // respond to our own event by updating dependent dialogs
2591 if (ap.getCalculationDialog() != null)
2593 ap.getCalculationDialog().validateCalcTypes();
2599 // process further ?
2600 if (!av.followSelection)
2606 * Ignore the selection if there is one of our own pending.
2608 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2614 * Check for selection in a view of which this one is a dna/protein
2617 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2622 // do we want to thread this ? (contention with seqsel and colsel locks, I
2625 * only copy colsel if there is a real intersection between
2626 * sequence selection and this panel's alignment
2628 boolean repaint = false;
2629 boolean copycolsel = false;
2631 SequenceGroup sgroup = null;
2632 if (seqsel != null && seqsel.getSize() > 0)
2634 if (av.getAlignment() == null)
2636 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2637 + " ViewId=" + av.getViewId()
2638 + " 's alignment is NULL! returning immediately.");
2641 sgroup = seqsel.intersect(av.getAlignment(),
2642 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2643 if ((sgroup != null && sgroup.getSize() > 0))
2648 if (sgroup != null && sgroup.getSize() > 0)
2650 av.setSelectionGroup(sgroup);
2654 av.setSelectionGroup(null);
2656 av.isSelectionGroupChanged(true);
2661 // the current selection is unset or from a previous message
2662 // so import the new colsel.
2663 if (colsel == null || colsel.isEmpty())
2665 if (av.getColumnSelection() != null)
2667 av.getColumnSelection().clear();
2673 // TODO: shift colSel according to the intersecting sequences
2674 if (av.getColumnSelection() == null)
2676 av.setColumnSelection(new ColumnSelection(colsel));
2680 av.getColumnSelection().setElementsFrom(colsel,
2681 av.getAlignment().getHiddenColumns());
2684 av.isColSelChanged(true);
2688 if (copycolsel && av.hasHiddenColumns()
2689 && (av.getAlignment().getHiddenColumns() == null))
2691 System.err.println("Bad things");
2693 if (repaint) // always true!
2695 // probably finessing with multiple redraws here
2696 PaintRefresher.Refresh(this, av.getSequenceSetId());
2697 // ap.paintAlignment(false);
2700 // lastly, update dependent dialogs
2701 if (ap.getCalculationDialog() != null)
2703 ap.getCalculationDialog().validateCalcTypes();
2709 * If this panel is a cdna/protein translation view of the selection source,
2710 * tries to map the source selection to a local one, and returns true. Else
2717 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2718 ColumnSelection colsel, HiddenColumns hidden,
2719 SelectionSource source)
2721 if (!(source instanceof AlignViewportI))
2725 final AlignViewportI sourceAv = (AlignViewportI) source;
2726 if (sourceAv.getCodingComplement() != av
2727 && av.getCodingComplement() != sourceAv)
2733 * Map sequence selection
2735 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2736 av.setSelectionGroup(sg);
2737 av.isSelectionGroupChanged(true);
2740 * Map column selection
2742 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2744 ColumnSelection cs = new ColumnSelection();
2745 HiddenColumns hs = new HiddenColumns();
2746 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2747 av.setColumnSelection(cs);
2748 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2750 // lastly, update any dependent dialogs
2751 if (ap.getCalculationDialog() != null)
2753 ap.getCalculationDialog().validateCalcTypes();
2757 * repaint alignment, and also Overview or Structure
2758 * if hidden column selection has changed
2760 ap.paintAlignment(hiddenChanged, hiddenChanged);
2767 * @return null or last search results handled by this panel
2769 public SearchResultsI getLastSearchResults()
2771 return lastSearchResults;