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);
137 // System.out.println(obj + (b ? "= " : "!= ") + this);
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 * Creates a new SeqPanel object
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 seqIndex = Math.min((y / charHeight) + av.getRanges().getStartSeq(),
325 alignmentHeight - 1);
328 return new MousePos(col, seqIndex, annIndex);
331 * Returns the aligned sequence position (base 0) at the mouse position, or
332 * the closest visible one
337 int findColumn(MouseEvent evt)
342 final int startRes = av.getRanges().getStartRes();
343 final int charWidth = av.getCharWidth();
345 if (av.getWrapAlignment())
347 int hgap = av.getCharHeight();
348 if (av.getScaleAboveWrapped())
350 hgap += av.getCharHeight();
353 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
354 + hgap + seqCanvas.getAnnotationHeight();
357 y = Math.max(0, y - hgap);
358 x -= seqCanvas.getLabelWidthWest();
361 // mouse is over left scale
365 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
370 if (x >= cwidth * charWidth)
372 // mouse is over right scale
376 wrappedBlock = y / cHeight;
377 wrappedBlock += startRes / cwidth;
378 // allow for wrapped view scrolled right (possible from Overview)
379 int startOffset = startRes % cwidth;
380 res = wrappedBlock * cwidth + startOffset
381 + Math.min(cwidth - 1, x / charWidth);
386 * make sure we calculate relative to visible alignment,
387 * rather than right-hand gutter
389 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
390 res = (x / charWidth) + startRes;
391 res = Math.min(res, av.getRanges().getEndRes());
394 if (av.hasHiddenColumns())
396 res = av.getAlignment().getHiddenColumns()
397 .visibleToAbsoluteColumn(res);
404 * When all of a sequence of edits are complete, put the resulting edit list
405 * on the history stack (undo list), and reset flags for editing in progress.
411 if (editCommand != null && editCommand.getSize() > 0)
413 ap.alignFrame.addHistoryItem(editCommand);
414 av.firePropertyChange("alignment", null,
415 av.getAlignment().getSequences());
420 * Tidy up come what may...
425 groupEditing = false;
434 seqCanvas.cursorY = getKeyboardNo1() - 1;
435 scrollToVisible(true);
438 void setCursorColumn()
440 seqCanvas.cursorX = getKeyboardNo1() - 1;
441 scrollToVisible(true);
444 void setCursorRowAndColumn()
446 if (keyboardNo2 == null)
448 keyboardNo2 = new StringBuffer();
452 seqCanvas.cursorX = getKeyboardNo1() - 1;
453 seqCanvas.cursorY = getKeyboardNo2() - 1;
454 scrollToVisible(true);
458 void setCursorPosition()
460 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
462 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
463 scrollToVisible(true);
466 void moveCursor(int dx, int dy)
468 seqCanvas.cursorX += dx;
469 seqCanvas.cursorY += dy;
471 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
473 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
475 int original = seqCanvas.cursorX - dx;
476 int maxWidth = av.getAlignment().getWidth();
478 if (!hidden.isVisible(seqCanvas.cursorX))
480 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
481 int[] region = hidden.getRegionWithEdgeAtRes(visx);
483 if (region != null) // just in case
488 seqCanvas.cursorX = region[1] + 1;
493 seqCanvas.cursorX = region[0] - 1;
496 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
499 if (seqCanvas.cursorX >= maxWidth
500 || !hidden.isVisible(seqCanvas.cursorX))
502 seqCanvas.cursorX = original;
506 scrollToVisible(false);
510 * Scroll to make the cursor visible in the viewport.
513 * just jump to the location rather than scrolling
515 void scrollToVisible(boolean jump)
517 if (seqCanvas.cursorX < 0)
519 seqCanvas.cursorX = 0;
521 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
523 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
526 if (seqCanvas.cursorY < 0)
528 seqCanvas.cursorY = 0;
530 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
532 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
537 boolean repaintNeeded = true;
540 // only need to repaint if the viewport did not move, as otherwise it will
542 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
547 if (av.getWrapAlignment())
549 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
550 int x = av.getAlignment().getHiddenColumns()
551 .absoluteToVisibleColumn(seqCanvas.cursorX);
552 av.getRanges().scrollToWrappedVisible(x);
556 av.getRanges().scrollToVisible(seqCanvas.cursorX,
561 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
563 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
564 seqCanvas.cursorX, seqCanvas.cursorY);
574 void setSelectionAreaAtCursor(boolean topLeft)
576 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
578 if (av.getSelectionGroup() != null)
580 SequenceGroup sg = av.getSelectionGroup();
581 // Find the top and bottom of this group
582 int min = av.getAlignment().getHeight(), max = 0;
583 for (int i = 0; i < sg.getSize(); i++)
585 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
600 sg.setStartRes(seqCanvas.cursorX);
601 if (sg.getEndRes() < seqCanvas.cursorX)
603 sg.setEndRes(seqCanvas.cursorX);
606 min = seqCanvas.cursorY;
610 sg.setEndRes(seqCanvas.cursorX);
611 if (sg.getStartRes() > seqCanvas.cursorX)
613 sg.setStartRes(seqCanvas.cursorX);
616 max = seqCanvas.cursorY + 1;
621 // Only the user can do this
622 av.setSelectionGroup(null);
626 // Now add any sequences between min and max
627 sg.getSequences(null).clear();
628 for (int i = min; i < max; i++)
630 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
635 if (av.getSelectionGroup() == null)
637 SequenceGroup sg = new SequenceGroup();
638 sg.setStartRes(seqCanvas.cursorX);
639 sg.setEndRes(seqCanvas.cursorX);
640 sg.addSequence(sequence, false);
641 av.setSelectionGroup(sg);
644 ap.paintAlignment(false, false);
648 void insertGapAtCursor(boolean group)
650 groupEditing = group;
651 editStartSeq = seqCanvas.cursorY;
652 editLastRes = seqCanvas.cursorX;
653 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
657 void deleteGapAtCursor(boolean group)
659 groupEditing = group;
660 editStartSeq = seqCanvas.cursorY;
661 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
662 editSequence(false, false, seqCanvas.cursorX);
666 void insertNucAtCursor(boolean group, String nuc)
668 // TODO not called - delete?
669 groupEditing = group;
670 editStartSeq = seqCanvas.cursorY;
671 editLastRes = seqCanvas.cursorX;
672 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
676 void numberPressed(char value)
678 if (keyboardNo1 == null)
680 keyboardNo1 = new StringBuffer();
683 if (keyboardNo2 != null)
685 keyboardNo2.append(value);
689 keyboardNo1.append(value);
697 if (keyboardNo1 != null)
699 int value = Integer.parseInt(keyboardNo1.toString());
703 } catch (Exception x)
714 if (keyboardNo2 != null)
716 int value = Integer.parseInt(keyboardNo2.toString());
720 } catch (Exception x)
734 public void mouseReleased(MouseEvent evt)
736 MousePos pos = findMousePosition(evt);
737 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
742 boolean didDrag = mouseDragging; // did we come here after a drag
743 mouseDragging = false;
744 mouseWheelPressed = false;
746 if (evt.isPopupTrigger()) // Windows: mouseReleased
748 showPopupMenu(evt, pos);
759 doMouseReleasedDefineMode(evt, didDrag);
770 public void mousePressed(MouseEvent evt)
772 lastMousePress = evt.getPoint();
773 MousePos pos = findMousePosition(evt);
774 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
779 if (SwingUtilities.isMiddleMouseButton(evt))
781 mouseWheelPressed = true;
785 boolean isControlDown = Platform.isControlDown(evt);
786 if (evt.isShiftDown() || isControlDown)
796 doMousePressedDefineMode(evt, pos);
800 int seq = pos.seqIndex;
801 int res = pos.column;
803 if ((seq < av.getAlignment().getHeight())
804 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
820 private String formattedTooltipText;
823 public void mouseOverSequence(SequenceI sequence, int index, int pos)
825 String tmp = sequence.hashCode() + " " + index + " " + pos;
827 if (lastMessage == null || !lastMessage.equals(tmp))
829 // System.err.println("mouseOver Sequence: "+tmp);
830 ssm.mouseOverSequence(sequence, index, pos, av);
836 * Highlight the mapped region described by the search results object (unless
837 * unchanged). This supports highlight of protein while mousing over linked
838 * cDNA and vice versa. The status bar is also updated to show the location of
839 * the start of the highlighted region.
842 public void highlightSequence(SearchResultsI results)
844 if (results == null || results.equals(lastSearchResults))
848 lastSearchResults = results;
850 boolean wasScrolled = false;
852 if (av.isFollowHighlight())
854 // don't allow highlight of protein/cDNA to also scroll a complementary
855 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
856 // over residue to change abruptly, causing highlighted residue in panel 2
857 // to change, causing a scroll in panel 1 etc)
858 ap.setToScrollComplementPanel(false);
859 wasScrolled = ap.scrollToPosition(results);
862 seqCanvas.revalidate();
864 ap.setToScrollComplementPanel(true);
867 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
868 if (seqCanvas.highlightSearchResults(results, fastPaint))
870 setStatusMessage(results);
875 public VamsasSource getVamsasSource()
877 return this.ap == null ? null : this.ap.av;
881 public void updateColours(SequenceI seq, int index)
883 System.out.println("update the seqPanel colours");
888 * Action on mouse movement is to update the status bar to show the current
889 * sequence position, and (if features are shown) to show any features at the
890 * position in a tooltip. Does nothing if the mouse move does not change
896 public void mouseMoved(MouseEvent evt)
900 // This is because MacOSX creates a mouseMoved
901 // If control is down, other platforms will not.
905 final MousePos mousePos = findMousePosition(evt);
906 if (mousePos.equals(lastMousePosition))
909 * just a pixel move without change of 'cell'
913 lastMousePosition = mousePos;
915 if (mousePos.isOverAnnotation())
917 mouseMovedOverAnnotation(mousePos);
920 final int seq = mousePos.seqIndex;
922 final int column = mousePos.column;
923 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
925 lastMousePosition = null;
926 setToolTipText(null);
928 ap.alignFrame.setStatus("");
932 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
934 if (column >= sequence.getLength())
940 * set status bar message, returning residue position in sequence
942 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
943 final int pos = setStatusMessage(sequence, column, seq);
944 if (ssm != null && !isGapped)
946 mouseOverSequence(sequence, column, pos);
949 tooltipText.setLength(6); // "<html>"
951 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
954 for (int g = 0; g < groups.length; g++)
956 if (groups[g].getStartRes() <= column
957 && groups[g].getEndRes() >= column)
959 if (!groups[g].getName().startsWith("JTreeGroup")
960 && !groups[g].getName().startsWith("JGroup"))
962 tooltipText.append(groups[g].getName());
965 if (groups[g].getDescription() != null)
967 tooltipText.append(": " + groups[g].getDescription());
974 * add any features at the position to the tooltip; if over a gap, only
975 * add features that straddle the gap (pos may be the residue before or
978 if (av.isShowSequenceFeatures())
980 List<SequenceFeature> features = ap.getFeatureRenderer()
981 .findFeaturesAtColumn(sequence, column + 1);
982 seqARep.appendFeatures(tooltipText, pos, features,
983 this.ap.getSeqPanel().seqCanvas.fr);
985 if (tooltipText.length() == 6) // <html>
987 setToolTipText(null);
992 if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
994 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
995 tooltipText.append("...");
997 String textString = tooltipText.toString();
998 if (lastTooltip == null || !lastTooltip.equals(textString))
1000 formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1002 setToolTipText(formattedTooltipText);
1003 lastTooltip = textString;
1009 * When the view is in wrapped mode, and the mouse is over an annotation row,
1010 * shows the corresponding tooltip and status message (if any)
1015 protected void mouseMovedOverAnnotation(MousePos pos)
1017 final int column = pos.column;
1018 final int rowIndex = pos.annotationIndex;
1020 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1025 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1027 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1029 setToolTipText(tooltip);
1030 lastTooltip = tooltip;
1032 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1034 ap.alignFrame.setStatus(msg);
1037 private Point lastp = null;
1039 private JToolTip tempTip = new JLabel().createToolTip();
1044 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1047 public Point getToolTipLocation(MouseEvent event)
1051 if (tooltipText == null || tooltipText.length() <= 6)
1056 if (lastp != null && event.isShiftDown())
1062 int x = event.getX();
1063 int y = event.getY();
1066 tempTip.setTipText(formattedTooltipText);
1067 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1069 // was x += (w - x < 200) ? -(w / 2) : 5;
1070 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1071 p = new Point(x, y + 20); // BH 2018 was - 20?
1079 * set when the current UI interaction has resulted in a change that requires
1080 * shading in overviews and structures to be recalculated. this could be
1081 * changed to a something more expressive that indicates what actually has
1082 * changed, so selective redraws can be applied (ie. only structures, only
1085 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1088 * set if av.getSelectionGroup() refers to a group that is defined on the
1089 * alignment view, rather than a transient selection
1091 // private boolean editingDefinedGroup = false; // TODO: refactor to
1092 // avcontroller or viewModel
1095 * Sets the status message in alignment panel, showing the sequence number
1096 * (index) and id, and residue and residue position if not at a gap, for the
1097 * given sequence and column position. Returns the residue position returned
1098 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1099 * if at a gapped position.
1102 * aligned sequence object
1106 * index of sequence in alignment
1107 * @return sequence position of residue at column, or adjacent residue if at a
1110 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1112 char sequenceChar = sequence.getCharAt(column);
1113 int pos = sequence.findPosition(column);
1114 setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1120 * Builds the status message for the current cursor location and writes it to
1121 * the status bar, for example
1124 * Sequence 3 ID: FER1_SOLLC
1125 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1126 * Sequence 5 ID: FER1_PEA Residue: B (3)
1127 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1132 * sequence position in the alignment (1..)
1133 * @param sequenceChar
1134 * the character under the cursor
1136 * the sequence residue position (if not over a gap)
1138 protected void setStatusMessage(SequenceI sequence, int seqIndex,
1139 char sequenceChar, int residuePos)
1141 StringBuilder text = new StringBuilder(32);
1144 * Sequence number (if known), and sequence name.
1146 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1147 text.append("Sequence").append(seqno).append(" ID: ")
1148 .append(sequence.getName());
1150 String residue = null;
1153 * Try to translate the display character to residue name (null for gap).
1155 boolean isGapped = Comparison.isGap(sequenceChar);
1159 boolean nucleotide = av.getAlignment().isNucleotide();
1160 String displayChar = String.valueOf(sequenceChar);
1163 residue = ResidueProperties.nucleotideName.get(displayChar);
1167 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1168 : ("*".equals(displayChar) ? "STOP"
1169 : ResidueProperties.aa2Triplet.get(displayChar));
1171 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1172 .append(": ").append(residue == null ? displayChar : residue);
1174 text.append(" (").append(Integer.toString(residuePos)).append(")");
1176 ap.alignFrame.setStatus(text.toString());
1180 * Set the status bar message to highlight the first matched position in
1185 private void setStatusMessage(SearchResultsI results)
1187 AlignmentI al = this.av.getAlignment();
1188 int sequenceIndex = al.findIndex(results);
1189 if (sequenceIndex == -1)
1193 SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1194 for (SearchResultMatchI m : results.getResults())
1196 SequenceI seq = m.getSequence();
1197 if (seq.getDatasetSequence() != null)
1199 seq = seq.getDatasetSequence();
1204 int start = m.getStart();
1205 setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1216 public void mouseDragged(MouseEvent evt)
1218 MousePos pos = findMousePosition(evt);
1219 if (pos.isOverAnnotation() || pos.column == -1)
1224 if (mouseWheelPressed)
1226 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1227 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1229 int oldWidth = av.getCharWidth();
1231 // Which is bigger, left-right or up-down?
1232 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1233 .abs(evt.getX() - lastMousePress.getX()))
1236 * on drag up or down, decrement or increment font size
1238 int fontSize = av.font.getSize();
1239 boolean fontChanged = false;
1241 if (evt.getY() < lastMousePress.getY())
1246 else if (evt.getY() > lastMousePress.getY())
1259 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1261 av.setFont(newFont, true);
1262 av.setCharWidth(oldWidth);
1266 ap.av.getCodingComplement().setFont(newFont, true);
1267 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1268 .getSplitViewContainer();
1269 splitFrame.adjustLayout();
1270 splitFrame.repaint();
1277 * on drag left or right, decrement or increment character width
1280 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1282 newWidth = av.getCharWidth() - 1;
1283 av.setCharWidth(newWidth);
1285 else if (evt.getX() > lastMousePress.getX())
1287 newWidth = av.getCharWidth() + 1;
1288 av.setCharWidth(newWidth);
1292 ap.paintAlignment(false, false);
1296 * need to ensure newWidth is set on cdna, regardless of which
1297 * panel the mouse drag happened in; protein will compute its
1298 * character width as 1:1 or 3:1
1300 av.getCodingComplement().setCharWidth(newWidth);
1301 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1302 .getSplitViewContainer();
1303 splitFrame.adjustLayout();
1304 splitFrame.repaint();
1309 FontMetrics fm = getFontMetrics(av.getFont());
1310 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1312 lastMousePress = evt.getPoint();
1319 dragStretchGroup(evt);
1323 int res = pos.column;
1330 if ((editLastRes == -1) || (editLastRes == res))
1335 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1337 // dragLeft, delete gap
1338 editSequence(false, false, res);
1342 editSequence(true, false, res);
1345 mouseDragging = true;
1346 if (scrollThread != null)
1348 scrollThread.setMousePosition(evt.getPoint());
1353 * Edits the sequence to insert or delete one or more gaps, in response to a
1354 * mouse drag or cursor mode command. The number of inserts/deletes may be
1355 * specified with the cursor command, or else depends on the mouse event
1356 * (normally one column, but potentially more for a fast mouse drag).
1358 * Delete gaps is limited to the number of gaps left of the cursor position
1359 * (mouse drag), or at or right of the cursor position (cursor mode).
1361 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1362 * the current selection group.
1364 * In locked editing mode (with a selection group present), inserts/deletions
1365 * within the selection group are limited to its boundaries (and edits outside
1366 * the group stop at its border).
1369 * true to insert gaps, false to delete gaps
1371 * (unused parameter)
1373 * the column at which to perform the action; the number of columns
1374 * affected depends on <code>this.editLastRes</code> (cursor column
1377 synchronized void editSequence(boolean insertGap, boolean editSeq,
1381 int fixedRight = -1;
1382 boolean fixedColumns = false;
1383 SequenceGroup sg = av.getSelectionGroup();
1385 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1387 // No group, but the sequence may represent a group
1388 if (!groupEditing && av.hasHiddenRows())
1390 if (av.isHiddenRepSequence(seq))
1392 sg = av.getRepresentedSequences(seq);
1393 groupEditing = true;
1397 StringBuilder message = new StringBuilder(64); // for status bar
1400 * make a name for the edit action, for
1401 * status bar message and Undo/Redo menu
1403 String label = null;
1406 message.append("Edit group:");
1407 label = MessageManager.getString("action.edit_group");
1411 message.append("Edit sequence: " + seq.getName());
1412 label = seq.getName();
1413 if (label.length() > 10)
1415 label = label.substring(0, 10);
1417 label = MessageManager.formatMessage("label.edit_params",
1423 * initialise the edit command if there is not
1424 * already one being extended
1426 if (editCommand == null)
1428 editCommand = new EditCommand(label);
1433 message.append(" insert ");
1437 message.append(" delete ");
1440 message.append(Math.abs(startres - editLastRes) + " gaps.");
1441 ap.alignFrame.setStatus(message.toString());
1444 * is there a selection group containing the sequence being edited?
1445 * if so the boundary of the group is the limit of the edit
1446 * (but the edit may be inside or outside the selection group)
1448 boolean inSelectionGroup = sg != null
1449 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1450 if (groupEditing || inSelectionGroup)
1452 fixedColumns = true;
1454 // sg might be null as the user may only see 1 sequence,
1455 // but the sequence represents a group
1458 if (!av.isHiddenRepSequence(seq))
1463 sg = av.getRepresentedSequences(seq);
1466 fixedLeft = sg.getStartRes();
1467 fixedRight = sg.getEndRes();
1469 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1470 || (startres >= fixedLeft && editLastRes < fixedLeft)
1471 || (startres > fixedRight && editLastRes <= fixedRight)
1472 || (startres <= fixedRight && editLastRes > fixedRight))
1478 if (fixedLeft > startres)
1480 fixedRight = fixedLeft - 1;
1483 else if (fixedRight < startres)
1485 fixedLeft = fixedRight;
1490 if (av.hasHiddenColumns())
1492 fixedColumns = true;
1493 int y1 = av.getAlignment().getHiddenColumns()
1494 .getNextHiddenBoundary(true, startres);
1495 int y2 = av.getAlignment().getHiddenColumns()
1496 .getNextHiddenBoundary(false, startres);
1498 if ((insertGap && startres > y1 && editLastRes < y1)
1499 || (!insertGap && startres < y2 && editLastRes > y2))
1505 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
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 ap.alignFrame.setStatus(" ");
1944 if (av.getWrapAlignment())
1949 if (mouseDragging && scrollThread == null)
1951 startScrolling(e.getPoint());
1956 * Handler for double-click on a position with one or more sequence features.
1957 * Opens the Amend Features dialog to allow feature details to be amended, or
1958 * the feature deleted.
1961 public void mouseClicked(MouseEvent evt)
1963 SequenceGroup sg = null;
1964 MousePos pos = findMousePosition(evt);
1965 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
1970 if (evt.getClickCount() > 1)
1972 sg = av.getSelectionGroup();
1973 if (sg != null && sg.getSize() == 1
1974 && sg.getEndRes() - sg.getStartRes() < 2)
1976 av.setSelectionGroup(null);
1979 int column = pos.column;
1982 * find features at the position (if not gapped), or straddling
1983 * the position (if at a gap)
1985 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
1986 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
1987 .findFeaturesAtColumn(sequence, column + 1);
1989 if (!features.isEmpty())
1992 * highlight the first feature at the position on the alignment
1994 SearchResultsI highlight = new SearchResults();
1995 highlight.addResult(sequence, features.get(0).getBegin(), features
1997 seqCanvas.highlightSearchResults(highlight, true);
2000 * open the Amend Features dialog
2002 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2003 false).showDialog();
2009 public void mouseWheelMoved(MouseWheelEvent e)
2012 double wheelRotation = e.getPreciseWheelRotation();
2013 if (wheelRotation > 0)
2015 if (e.isShiftDown())
2017 av.getRanges().scrollRight(true);
2022 av.getRanges().scrollUp(false);
2025 else if (wheelRotation < 0)
2027 if (e.isShiftDown())
2029 av.getRanges().scrollRight(false);
2033 av.getRanges().scrollUp(true);
2038 * update status bar and tooltip for new position
2039 * (need to synthesize a mouse movement to refresh tooltip)
2042 ToolTipManager.sharedInstance().mouseMoved(e);
2051 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2053 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2058 final int res = pos.column;
2059 final int seq = pos.seqIndex;
2061 updateOverviewAndStructs = false;
2063 startWrapBlock = wrappedBlock;
2065 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2067 if ((sequence == null) || (res > sequence.getLength()))
2072 stretchGroup = av.getSelectionGroup();
2074 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2076 stretchGroup = av.getAlignment().findGroup(sequence, res);
2077 if (stretchGroup != null)
2079 // only update the current selection if the popup menu has a group to
2081 av.setSelectionGroup(stretchGroup);
2086 * defer right-mouse click handling to mouseReleased on Windows
2087 * (where isPopupTrigger() will answer true)
2088 * NB isRightMouseButton is also true for Cmd-click on Mac
2090 if (Platform.isWinRightButton(evt))
2095 if (evt.isPopupTrigger()) // Mac: mousePressed
2097 showPopupMenu(evt, pos);
2103 seqCanvas.cursorX = res;
2104 seqCanvas.cursorY = seq;
2105 seqCanvas.repaint();
2109 if (stretchGroup == null)
2111 createStretchGroup(res, sequence);
2114 if (stretchGroup != null)
2116 stretchGroup.addPropertyChangeListener(seqCanvas);
2119 seqCanvas.repaint();
2122 private void createStretchGroup(int res, SequenceI sequence)
2124 // Only if left mouse button do we want to change group sizes
2125 // define a new group here
2126 SequenceGroup sg = new SequenceGroup();
2127 sg.setStartRes(res);
2129 sg.addSequence(sequence, false);
2130 av.setSelectionGroup(sg);
2133 if (av.getConservationSelected())
2135 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2139 if (av.getAbovePIDThreshold())
2141 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2144 // TODO: stretchGroup will always be not null. Is this a merge error ?
2145 // or is there a threading issue here?
2146 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2148 // Edit end res position of selected group
2149 changeEndRes = true;
2151 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2153 // Edit end res position of selected group
2154 changeStartRes = true;
2156 stretchGroup.getWidth();
2161 * Build and show a pop-up menu at the right-click mouse position
2166 void showPopupMenu(MouseEvent evt, MousePos pos)
2168 final int column = pos.column;
2169 final int seq = pos.seqIndex;
2170 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2171 List<SequenceFeature> features = ap.getFeatureRenderer()
2172 .findFeaturesAtColumn(sequence, column + 1);
2174 PopupMenu pop = new PopupMenu(ap, null, features);
2175 pop.show(this, evt.getX(), evt.getY());
2179 * Update the display after mouse up on a selection or group
2182 * mouse released event details
2184 * true if this event is happening after a mouse drag (rather than a
2187 protected void doMouseReleasedDefineMode(MouseEvent evt,
2190 if (stretchGroup == null)
2195 stretchGroup.removePropertyChangeListener(seqCanvas);
2197 // always do this - annotation has own state
2198 // but defer colourscheme update until hidden sequences are passed in
2199 boolean vischange = stretchGroup.recalcConservation(true);
2200 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2202 if (stretchGroup.cs != null)
2206 stretchGroup.cs.alignmentChanged(stretchGroup,
2207 av.getHiddenRepSequences());
2210 ResidueShaderI groupColourScheme = stretchGroup
2211 .getGroupColourScheme();
2212 String name = stretchGroup.getName();
2213 if (stretchGroup.cs.conservationApplied())
2215 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2217 if (stretchGroup.cs.getThreshold() > 0)
2219 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2222 PaintRefresher.Refresh(this, av.getSequenceSetId());
2223 // TODO: structure colours only need updating if stretchGroup used to or now
2224 // does contain sequences with structure views
2225 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2226 updateOverviewAndStructs = false;
2227 changeEndRes = false;
2228 changeStartRes = false;
2229 stretchGroup = null;
2234 * Resizes the borders of a selection group depending on the direction of
2239 protected void dragStretchGroup(MouseEvent evt)
2241 if (stretchGroup == null)
2246 MousePos pos = findMousePosition(evt);
2247 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2252 int res = pos.column;
2253 int y = pos.seqIndex;
2255 if (wrappedBlock != startWrapBlock)
2260 res = Math.min(res, av.getAlignment().getWidth()-1);
2262 if (stretchGroup.getEndRes() == res)
2264 // Edit end res position of selected group
2265 changeEndRes = true;
2267 else if (stretchGroup.getStartRes() == res)
2269 // Edit start res position of selected group
2270 changeStartRes = true;
2273 if (res < av.getRanges().getStartRes())
2275 res = av.getRanges().getStartRes();
2280 if (res > (stretchGroup.getStartRes() - 1))
2282 stretchGroup.setEndRes(res);
2283 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2286 else if (changeStartRes)
2288 if (res < (stretchGroup.getEndRes() + 1))
2290 stretchGroup.setStartRes(res);
2291 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2295 int dragDirection = 0;
2301 else if (y < oldSeq)
2306 while ((y != oldSeq) && (oldSeq > -1)
2307 && (y < av.getAlignment().getHeight()))
2309 // This routine ensures we don't skip any sequences, as the
2310 // selection is quite slow.
2311 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2313 oldSeq += dragDirection;
2320 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2322 if (stretchGroup.getSequences(null).contains(nextSeq))
2324 stretchGroup.deleteSequence(seq, false);
2325 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2331 stretchGroup.addSequence(seq, false);
2334 stretchGroup.addSequence(nextSeq, false);
2335 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2344 mouseDragging = true;
2346 if (scrollThread != null)
2348 scrollThread.setMousePosition(evt.getPoint());
2352 * construct a status message showing the range of the selection
2354 StringBuilder status = new StringBuilder(64);
2355 List<SequenceI> seqs = stretchGroup.getSequences();
2356 String name = seqs.get(0).getName();
2357 if (name.length() > 20)
2359 name = name.substring(0, 20);
2361 status.append(name).append(" - ");
2362 name = seqs.get(seqs.size() - 1).getName();
2363 if (name.length() > 20)
2365 name = name.substring(0, 20);
2367 status.append(name).append(" ");
2368 int startRes = stretchGroup.getStartRes();
2369 status.append(" cols ").append(String.valueOf(startRes + 1))
2371 int endRes = stretchGroup.getEndRes();
2372 status.append(String.valueOf(endRes + 1));
2373 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2374 .append(String.valueOf(endRes - startRes + 1)).append(")");
2375 ap.alignFrame.setStatus(status.toString());
2379 * Stops the scroll thread if it is running
2381 void stopScrolling()
2383 if (scrollThread != null)
2385 scrollThread.stopScrolling();
2386 scrollThread = null;
2388 mouseDragging = false;
2392 * Starts a thread to scroll the alignment, towards a given mouse position
2393 * outside the panel bounds, unless the alignment is in wrapped mode
2397 void startScrolling(Point mousePos)
2400 * set this.mouseDragging in case this was called from
2401 * a drag in ScalePanel or AnnotationPanel
2403 mouseDragging = true;
2404 if (!av.getWrapAlignment() && scrollThread == null)
2406 scrollThread = new ScrollThread();
2407 scrollThread.setMousePosition(mousePos);
2408 if (!Platform.isJS())
2411 * Java - run in a new thread
2413 scrollThread.start();
2418 * Javascript - run every 20ms until scrolling stopped
2419 * or reaches the limit of scrollable alignment
2421 // java.util.Timer version:
2422 // Timer t = new Timer("ScrollThreadTimer", true);
2423 // TimerTask task = new TimerTask()
2426 // public void run()
2428 // if (!scrollThread.scrollOnce())
2434 // t.schedule(task, 20, 20);
2435 Timer t = new Timer(20, new ActionListener()
2438 public void actionPerformed(ActionEvent e)
2440 if (scrollThread != null)
2442 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2443 scrollThread.scrollOnce();
2447 t.addActionListener(new ActionListener()
2450 public void actionPerformed(ActionEvent e)
2452 if (scrollThread == null)
2454 // finished and nulled itself
2465 * Performs scrolling of the visible alignment left, right, up or down, until
2466 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2467 * limit of the alignment is reached
2469 class ScrollThread extends Thread
2471 private Point mousePos;
2473 private volatile boolean keepRunning = true;
2478 public ScrollThread()
2480 setName("SeqPanel$ScrollThread");
2484 * Sets the position of the mouse that determines the direction of the
2485 * scroll to perform. If this is called as the mouse moves, scrolling should
2486 * respond accordingly. For example, if the mouse is dragged right, scroll
2487 * right should start; if the drag continues down, scroll down should also
2492 public void setMousePosition(Point p)
2498 * Sets a flag that will cause the thread to exit
2500 public void stopScrolling()
2502 keepRunning = false;
2506 * Scrolls the alignment left or right, and/or up or down, depending on the
2507 * last notified mouse position, until the limit of the alignment is
2508 * reached, or a flag is set to stop the scroll
2515 if (mousePos != null)
2517 keepRunning = scrollOnce();
2522 } catch (Exception ex)
2526 SeqPanel.this.scrollThread = null;
2532 * <li>one row up, if the mouse is above the panel</li>
2533 * <li>one row down, if the mouse is below the panel</li>
2534 * <li>one column left, if the mouse is left of the panel</li>
2535 * <li>one column right, if the mouse is right of the panel</li>
2537 * Answers true if a scroll was performed, false if not - meaning either
2538 * that the mouse position is within the panel, or the edge of the alignment
2541 boolean scrollOnce()
2544 * quit after mouseUp ensures interrupt in JalviewJS
2551 boolean scrolled = false;
2552 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2559 // mouse is above this panel - try scroll up
2560 scrolled = ranges.scrollUp(true);
2562 else if (mousePos.y >= getHeight())
2564 // mouse is below this panel - try scroll down
2565 scrolled = ranges.scrollUp(false);
2569 * scroll left or right
2573 scrolled |= ranges.scrollRight(false);
2575 else if (mousePos.x >= getWidth())
2577 scrolled |= ranges.scrollRight(true);
2584 * modify current selection according to a received message.
2587 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2588 HiddenColumns hidden, SelectionSource source)
2590 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2591 // handles selection messages...
2592 // TODO: extend config options to allow user to control if selections may be
2593 // shared between viewports.
2594 boolean iSentTheSelection = (av == source
2595 || (source instanceof AlignViewport
2596 && ((AlignmentViewport) source).getSequenceSetId()
2597 .equals(av.getSequenceSetId())));
2599 if (iSentTheSelection)
2601 // respond to our own event by updating dependent dialogs
2602 if (ap.getCalculationDialog() != null)
2604 ap.getCalculationDialog().validateCalcTypes();
2610 // process further ?
2611 if (!av.followSelection)
2617 * Ignore the selection if there is one of our own pending.
2619 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2625 * Check for selection in a view of which this one is a dna/protein
2628 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2633 // do we want to thread this ? (contention with seqsel and colsel locks, I
2636 * only copy colsel if there is a real intersection between
2637 * sequence selection and this panel's alignment
2639 boolean repaint = false;
2640 boolean copycolsel = false;
2642 SequenceGroup sgroup = null;
2643 if (seqsel != null && seqsel.getSize() > 0)
2645 if (av.getAlignment() == null)
2647 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2648 + " ViewId=" + av.getViewId()
2649 + " 's alignment is NULL! returning immediately.");
2652 sgroup = seqsel.intersect(av.getAlignment(),
2653 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2654 if ((sgroup != null && sgroup.getSize() > 0))
2659 if (sgroup != null && sgroup.getSize() > 0)
2661 av.setSelectionGroup(sgroup);
2665 av.setSelectionGroup(null);
2667 av.isSelectionGroupChanged(true);
2672 // the current selection is unset or from a previous message
2673 // so import the new colsel.
2674 if (colsel == null || colsel.isEmpty())
2676 if (av.getColumnSelection() != null)
2678 av.getColumnSelection().clear();
2684 // TODO: shift colSel according to the intersecting sequences
2685 if (av.getColumnSelection() == null)
2687 av.setColumnSelection(new ColumnSelection(colsel));
2691 av.getColumnSelection().setElementsFrom(colsel,
2692 av.getAlignment().getHiddenColumns());
2695 av.isColSelChanged(true);
2699 if (copycolsel && av.hasHiddenColumns()
2700 && (av.getAlignment().getHiddenColumns() == null))
2702 System.err.println("Bad things");
2704 if (repaint) // always true!
2706 // probably finessing with multiple redraws here
2707 PaintRefresher.Refresh(this, av.getSequenceSetId());
2708 // ap.paintAlignment(false);
2711 // lastly, update dependent dialogs
2712 if (ap.getCalculationDialog() != null)
2714 ap.getCalculationDialog().validateCalcTypes();
2720 * If this panel is a cdna/protein translation view of the selection source,
2721 * tries to map the source selection to a local one, and returns true. Else
2728 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2729 ColumnSelection colsel, HiddenColumns hidden,
2730 SelectionSource source)
2732 if (!(source instanceof AlignViewportI))
2736 final AlignViewportI sourceAv = (AlignViewportI) source;
2737 if (sourceAv.getCodingComplement() != av
2738 && av.getCodingComplement() != sourceAv)
2744 * Map sequence selection
2746 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2747 av.setSelectionGroup(sg);
2748 av.isSelectionGroupChanged(true);
2751 * Map column selection
2753 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2755 ColumnSelection cs = new ColumnSelection();
2756 HiddenColumns hs = new HiddenColumns();
2757 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2758 av.setColumnSelection(cs);
2759 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2761 // lastly, update any dependent dialogs
2762 if (ap.getCalculationDialog() != null)
2764 ap.getCalculationDialog().validateCalcTypes();
2768 * repaint alignment, and also Overview or Structure
2769 * if hidden column selection has changed
2771 ap.paintAlignment(hiddenChanged, hiddenChanged);
2778 * @return null or last search results handled by this panel
2780 public SearchResultsI getLastSearchResults()
2782 return lastSearchResults;