2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import java.awt.BorderLayout;
24 import java.awt.Color;
26 import java.awt.FontMetrics;
27 import java.awt.Point;
28 import java.awt.event.MouseEvent;
29 import java.awt.event.MouseListener;
30 import java.awt.event.MouseMotionListener;
31 import java.awt.event.MouseWheelEvent;
32 import java.awt.event.MouseWheelListener;
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.List;
37 import javax.swing.JPanel;
38 import javax.swing.SwingUtilities;
39 import javax.swing.ToolTipManager;
41 import jalview.api.AlignViewportI;
42 import jalview.bin.Cache;
43 import jalview.commands.EditCommand;
44 import jalview.commands.EditCommand.Action;
45 import jalview.commands.EditCommand.Edit;
46 import jalview.datamodel.AlignmentAnnotation;
47 import jalview.datamodel.AlignmentI;
48 import jalview.datamodel.ColumnSelection;
49 import jalview.datamodel.HiddenColumns;
50 import jalview.datamodel.MappedFeatures;
51 import jalview.datamodel.SearchResultMatchI;
52 import jalview.datamodel.SearchResults;
53 import jalview.datamodel.SearchResultsI;
54 import jalview.datamodel.Sequence;
55 import jalview.datamodel.SequenceFeature;
56 import jalview.datamodel.SequenceGroup;
57 import jalview.datamodel.SequenceI;
58 import jalview.io.SequenceAnnotationReport;
59 import jalview.renderer.ResidueShaderI;
60 import jalview.schemes.ResidueProperties;
61 import jalview.structure.SelectionListener;
62 import jalview.structure.SelectionSource;
63 import jalview.structure.SequenceListener;
64 import jalview.structure.StructureSelectionManager;
65 import jalview.structure.VamsasSource;
66 import jalview.util.Comparison;
67 import jalview.util.MappingUtils;
68 import jalview.util.MessageManager;
69 import jalview.util.Platform;
70 import jalview.viewmodel.AlignmentViewport;
71 import jalview.viewmodel.ViewportRanges;
72 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
78 * @version $Revision: 1.130 $
80 public class SeqPanel extends JPanel
81 implements MouseListener, MouseMotionListener, MouseWheelListener,
82 SequenceListener, SelectionListener
85 * a class that holds computed mouse position
86 * - column of the alignment (0...)
87 * - sequence offset (0...)
88 * - annotation row offset (0...)
89 * where annotation offset is -1 unless the alignment is shown
90 * in wrapped mode, annotations are shown, and the mouse is
91 * over an annnotation row
96 * alignment column position of cursor (0...)
101 * index in alignment of sequence under cursor,
102 * or nearest above if cursor is not over a sequence
107 * index in annotations array of annotation under the cursor
108 * (only possible in wrapped mode with annotations shown),
109 * or -1 if cursor is not over an annotation row
111 final int annotationIndex;
113 MousePos(int col, int seq, int ann)
117 annotationIndex = ann;
120 boolean isOverAnnotation()
122 return annotationIndex != -1;
126 public boolean equals(Object obj)
128 if (obj == null || !(obj instanceof MousePos))
132 MousePos o = (MousePos) obj;
133 boolean b = (column == o.column && seqIndex == o.seqIndex
134 && annotationIndex == o.annotationIndex);
135 // System.out.println(obj + (b ? "= " : "!= ") + this);
140 * A simple hashCode that ensures that instances that satisfy equals() have
144 public int hashCode()
146 return column + seqIndex + annotationIndex;
150 * toString method for debug output purposes only
153 public String toString()
155 return String.format("c%d:s%d:a%d", column, seqIndex,
160 private static final int MAX_TOOLTIP_LENGTH = 300;
162 public SeqCanvas seqCanvas;
164 public AlignmentPanel ap;
167 * last position for mouseMoved event
169 private MousePos lastMousePosition;
171 protected int editLastRes;
173 protected int editStartSeq;
175 protected AlignViewport av;
177 ScrollThread scrollThread = null;
179 boolean mouseDragging = false;
181 boolean editingSeqs = false;
183 boolean groupEditing = false;
185 // ////////////////////////////////////////
186 // ///Everything below this is for defining the boundary of the rubberband
187 // ////////////////////////////////////////
190 boolean changeEndSeq = false;
192 boolean changeStartSeq = false;
194 boolean changeEndRes = false;
196 boolean changeStartRes = false;
198 SequenceGroup stretchGroup = null;
200 boolean remove = false;
202 Point lastMousePress;
204 boolean mouseWheelPressed = false;
206 StringBuffer keyboardNo1;
208 StringBuffer keyboardNo2;
210 private final SequenceAnnotationReport seqARep;
212 StringBuilder tooltipText = new StringBuilder();
216 EditCommand editCommand;
218 StructureSelectionManager ssm;
220 SearchResultsI lastSearchResults;
223 * Creates a new SeqPanel object
228 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
230 seqARep = new SequenceAnnotationReport(true);
231 ToolTipManager.sharedInstance().registerComponent(this);
232 ToolTipManager.sharedInstance().setInitialDelay(0);
233 ToolTipManager.sharedInstance().setDismissDelay(10000);
235 setBackground(Color.white);
237 seqCanvas = new SeqCanvas(alignPanel);
238 setLayout(new BorderLayout());
239 add(seqCanvas, BorderLayout.CENTER);
241 this.ap = alignPanel;
243 if (!viewport.isDataset())
245 addMouseMotionListener(this);
246 addMouseListener(this);
247 addMouseWheelListener(this);
248 ssm = viewport.getStructureSelectionManager();
249 ssm.addStructureViewerListener(this);
250 ssm.addSelectionListener(this);
254 int startWrapBlock = -1;
256 int wrappedBlock = -1;
259 * Computes the column and sequence row (and possibly annotation row when in
260 * wrapped mode) for the given mouse position
265 MousePos findMousePosition(MouseEvent evt)
267 int col = findColumn(evt);
272 int charHeight = av.getCharHeight();
273 int alignmentHeight = av.getAlignment().getHeight();
274 if (av.getWrapAlignment())
276 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
277 seqCanvas.getHeight());
280 * yPos modulo height of repeating width
282 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
285 * height of sequences plus space / scale above,
286 * plus gap between sequences and annotations
288 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
289 + alignmentHeight * charHeight
290 + SeqCanvas.SEQS_ANNOTATION_GAP;
291 if (yOffsetPx >= alignmentHeightPixels)
294 * mouse is over annotations; find annotation index, also set
295 * last sequence above (for backwards compatible behaviour)
297 AlignmentAnnotation[] anns = av.getAlignment()
298 .getAlignmentAnnotation();
299 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
300 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
301 seqIndex = alignmentHeight - 1;
306 * mouse is over sequence (or the space above sequences)
308 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
311 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
317 ViewportRanges ranges = av.getRanges();
318 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
319 alignmentHeight - 1);
320 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
323 return new MousePos(col, seqIndex, annIndex);
326 * Returns the aligned sequence position (base 0) at the mouse position, or
327 * the closest visible one
332 int findColumn(MouseEvent evt)
337 final int startRes = av.getRanges().getStartRes();
338 final int charWidth = av.getCharWidth();
340 if (av.getWrapAlignment())
342 int hgap = av.getCharHeight();
343 if (av.getScaleAboveWrapped())
345 hgap += av.getCharHeight();
348 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
349 + hgap + seqCanvas.getAnnotationHeight();
352 y = Math.max(0, y - hgap);
353 x -= seqCanvas.getLabelWidthWest();
356 // mouse is over left scale
360 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
365 if (x >= cwidth * charWidth)
367 // mouse is over right scale
371 wrappedBlock = y / cHeight;
372 wrappedBlock += startRes / cwidth;
373 // allow for wrapped view scrolled right (possible from Overview)
374 int startOffset = startRes % cwidth;
375 res = wrappedBlock * cwidth + startOffset
376 + Math.min(cwidth - 1, x / charWidth);
381 * make sure we calculate relative to visible alignment,
382 * rather than right-hand gutter
384 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
385 res = (x / charWidth) + startRes;
386 res = Math.min(res, av.getRanges().getEndRes());
389 if (av.hasHiddenColumns())
391 res = av.getAlignment().getHiddenColumns()
392 .visibleToAbsoluteColumn(res);
399 * When all of a sequence of edits are complete, put the resulting edit list
400 * on the history stack (undo list), and reset flags for editing in progress.
406 if (editCommand != null && editCommand.getSize() > 0)
408 ap.alignFrame.addHistoryItem(editCommand);
409 av.firePropertyChange("alignment", null,
410 av.getAlignment().getSequences());
415 * Tidy up come what may...
420 groupEditing = false;
429 seqCanvas.cursorY = getKeyboardNo1() - 1;
430 scrollToVisible(true);
433 void setCursorColumn()
435 seqCanvas.cursorX = getKeyboardNo1() - 1;
436 scrollToVisible(true);
439 void setCursorRowAndColumn()
441 if (keyboardNo2 == null)
443 keyboardNo2 = new StringBuffer();
447 seqCanvas.cursorX = getKeyboardNo1() - 1;
448 seqCanvas.cursorY = getKeyboardNo2() - 1;
449 scrollToVisible(true);
453 void setCursorPosition()
455 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
457 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
458 scrollToVisible(true);
461 void moveCursor(int dx, int dy)
463 moveCursor(dx, dy,false);
465 void moveCursor(int dx, int dy, boolean nextWord)
467 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
471 int maxWidth = av.getAlignment().getWidth();
472 int maxHeight=av.getAlignment().getHeight();
473 SequenceI seqAtRow = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
474 // look for next gap or residue
475 boolean isGap = Comparison.isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
476 int p = seqCanvas.cursorX,lastP,r=seqCanvas.cursorY,lastR;
492 seqAtRow = av.getAlignment().getSequenceAt(r);
494 p = nextVisible(hidden, maxWidth, p, dx);
495 } while ((dx != 0 ? p != lastP : r != lastR)
496 && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
500 int maxWidth = av.getAlignment().getWidth();
501 seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX, dx);
502 seqCanvas.cursorY += dy;
504 scrollToVisible(false);
507 private int nextVisible(HiddenColumns hidden,int maxWidth, int original, int dx)
509 int newCursorX=original+dx;
510 if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
512 int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
513 int[] region = hidden.getRegionWithEdgeAtRes(visx);
515 if (region != null) // just in case
520 newCursorX = region[1] + 1;
525 newCursorX = region[0] - 1;
529 newCursorX = (newCursorX < 0) ? 0 : newCursorX;
530 if (newCursorX >= maxWidth
531 || !hidden.isVisible(newCursorX))
533 newCursorX = original;
538 * Scroll to make the cursor visible in the viewport.
541 * just jump to the location rather than scrolling
543 void scrollToVisible(boolean jump)
545 if (seqCanvas.cursorX < 0)
547 seqCanvas.cursorX = 0;
549 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
551 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
554 if (seqCanvas.cursorY < 0)
556 seqCanvas.cursorY = 0;
558 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
560 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
565 boolean repaintNeeded = true;
568 // only need to repaint if the viewport did not move, as otherwise it will
570 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
575 if (av.getWrapAlignment())
577 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
578 int x = av.getAlignment().getHiddenColumns()
579 .absoluteToVisibleColumn(seqCanvas.cursorX);
580 av.getRanges().scrollToWrappedVisible(x);
584 av.getRanges().scrollToVisible(seqCanvas.cursorX,
589 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
591 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
592 seqCanvas.cursorX, seqCanvas.cursorY);
602 void setSelectionAreaAtCursor(boolean topLeft)
604 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
606 if (av.getSelectionGroup() != null)
608 SequenceGroup sg = av.getSelectionGroup();
609 // Find the top and bottom of this group
610 int min = av.getAlignment().getHeight(), max = 0;
611 for (int i = 0; i < sg.getSize(); i++)
613 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
628 sg.setStartRes(seqCanvas.cursorX);
629 if (sg.getEndRes() < seqCanvas.cursorX)
631 sg.setEndRes(seqCanvas.cursorX);
634 min = seqCanvas.cursorY;
638 sg.setEndRes(seqCanvas.cursorX);
639 if (sg.getStartRes() > seqCanvas.cursorX)
641 sg.setStartRes(seqCanvas.cursorX);
644 max = seqCanvas.cursorY + 1;
649 // Only the user can do this
650 av.setSelectionGroup(null);
654 // Now add any sequences between min and max
655 sg.getSequences(null).clear();
656 for (int i = min; i < max; i++)
658 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
663 if (av.getSelectionGroup() == null)
665 SequenceGroup sg = new SequenceGroup();
666 sg.setStartRes(seqCanvas.cursorX);
667 sg.setEndRes(seqCanvas.cursorX);
668 sg.addSequence(sequence, false);
669 av.setSelectionGroup(sg);
672 ap.paintAlignment(false, false);
676 void insertGapAtCursor(boolean group)
678 groupEditing = group;
679 editStartSeq = seqCanvas.cursorY;
680 editLastRes = seqCanvas.cursorX;
681 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
685 void deleteGapAtCursor(boolean group)
687 groupEditing = group;
688 editStartSeq = seqCanvas.cursorY;
689 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
690 editSequence(false, false, seqCanvas.cursorX);
694 void insertNucAtCursor(boolean group, String nuc)
696 // TODO not called - delete?
697 groupEditing = group;
698 editStartSeq = seqCanvas.cursorY;
699 editLastRes = seqCanvas.cursorX;
700 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
704 void numberPressed(char value)
706 if (keyboardNo1 == null)
708 keyboardNo1 = new StringBuffer();
711 if (keyboardNo2 != null)
713 keyboardNo2.append(value);
717 keyboardNo1.append(value);
725 if (keyboardNo1 != null)
727 int value = Integer.parseInt(keyboardNo1.toString());
731 } catch (Exception x)
742 if (keyboardNo2 != null)
744 int value = Integer.parseInt(keyboardNo2.toString());
748 } catch (Exception x)
762 public void mouseReleased(MouseEvent evt)
764 MousePos pos = findMousePosition(evt);
765 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
770 boolean didDrag = mouseDragging; // did we come here after a drag
771 mouseDragging = false;
772 mouseWheelPressed = false;
774 if (evt.isPopupTrigger()) // Windows: mouseReleased
776 showPopupMenu(evt, pos);
787 doMouseReleasedDefineMode(evt, didDrag);
798 public void mousePressed(MouseEvent evt)
800 lastMousePress = evt.getPoint();
801 MousePos pos = findMousePosition(evt);
802 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
807 if (SwingUtilities.isMiddleMouseButton(evt))
809 mouseWheelPressed = true;
813 boolean isControlDown = Platform.isControlDown(evt);
814 if (evt.isShiftDown() || isControlDown)
824 doMousePressedDefineMode(evt, pos);
828 int seq = pos.seqIndex;
829 int res = pos.column;
831 if ((seq < av.getAlignment().getHeight())
832 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
849 public void mouseOverSequence(SequenceI sequence, int index, int pos)
851 String tmp = sequence.hashCode() + " " + index + " " + pos;
853 if (lastMessage == null || !lastMessage.equals(tmp))
855 // System.err.println("mouseOver Sequence: "+tmp);
856 ssm.mouseOverSequence(sequence, index, pos, av);
862 * Highlight the mapped region described by the search results object (unless
863 * unchanged). This supports highlight of protein while mousing over linked
864 * cDNA and vice versa. The status bar is also updated to show the location of
865 * the start of the highlighted region.
868 public String highlightSequence(SearchResultsI results)
870 if (results == null || results.equals(lastSearchResults))
874 lastSearchResults = results;
876 boolean wasScrolled = false;
878 if (av.isFollowHighlight())
880 // don't allow highlight of protein/cDNA to also scroll a complementary
881 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
882 // over residue to change abruptly, causing highlighted residue in panel 2
883 // to change, causing a scroll in panel 1 etc)
884 ap.setToScrollComplementPanel(false);
885 wasScrolled = ap.scrollToPosition(results);
888 seqCanvas.revalidate();
890 ap.setToScrollComplementPanel(true);
893 boolean noFastPaint = wasScrolled && av.getWrapAlignment();
894 if (seqCanvas.highlightSearchResults(results, noFastPaint))
896 setStatusMessage(results);
898 // JAL-3303 feature suppressed for now pending review
899 return null; // results.isEmpty() ? null : getHighlightInfo(results);
903 * temporary hack: answers a message suitable to show on structure hover
904 * label. This is normally null. It is a peptide variation description if
906 * <li>results are a single residue in a protein alignment</li>
907 * <li>there is a mapping to a coding sequence (codon)</li>
908 * <li>there are one or more SNP variant features on the codon</li>
910 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
915 private String getHighlightInfo(SearchResultsI results)
918 * ideally, just find mapped CDS (as we don't care about render style here);
919 * for now, go via split frame complement's FeatureRenderer
921 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
922 if (complement == null)
926 AlignFrame af = Desktop.getAlignFrameFor(complement);
927 FeatureRendererModel fr2 = af.getFeatureRenderer();
929 int j = results.getSize();
930 List<String> infos = new ArrayList<>();
931 for (int i = 0; i < j; i++)
933 SearchResultMatchI match = results.getResults().get(i);
934 int pos = match.getStart();
935 if (pos == match.getEnd())
937 SequenceI seq = match.getSequence();
938 SequenceI ds = seq.getDatasetSequence() == null ? seq
939 : seq.getDatasetSequence();
940 MappedFeatures mf = fr2
941 .findComplementFeaturesAtResidue(ds, pos);
944 for (SequenceFeature sf : mf.features)
946 String pv = mf.findProteinVariants(sf);
947 if (pv.length() > 0 && !infos.contains(pv))
960 StringBuilder sb = new StringBuilder();
961 for (String info : infos)
969 return sb.toString();
973 public VamsasSource getVamsasSource()
975 return this.ap == null ? null : this.ap.av;
979 public void updateColours(SequenceI seq, int index)
981 System.out.println("update the seqPanel colours");
986 * Action on mouse movement is to update the status bar to show the current
987 * sequence position, and (if features are shown) to show any features at the
988 * position in a tooltip. Does nothing if the mouse move does not change
994 public void mouseMoved(MouseEvent evt)
998 // This is because MacOSX creates a mouseMoved
999 // If control is down, other platforms will not.
1003 final MousePos mousePos = findMousePosition(evt);
1004 if (mousePos.equals(lastMousePosition))
1007 * just a pixel move without change of 'cell'
1011 lastMousePosition = mousePos;
1013 if (mousePos.isOverAnnotation())
1015 mouseMovedOverAnnotation(mousePos);
1018 final int seq = mousePos.seqIndex;
1020 final int column = mousePos.column;
1021 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1023 lastMousePosition = null;
1024 setToolTipText(null);
1026 ap.alignFrame.setStatus("");
1030 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1032 if (column >= sequence.getLength())
1038 * set status bar message, returning residue position in sequence
1040 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1041 final int pos = setStatusMessage(sequence, column, seq);
1042 if (ssm != null && !isGapped)
1044 mouseOverSequence(sequence, column, pos);
1047 tooltipText.setLength(6); // Cuts the buffer back to <html>
1049 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1052 for (int g = 0; g < groups.length; g++)
1054 if (groups[g].getStartRes() <= column
1055 && groups[g].getEndRes() >= column)
1057 if (!groups[g].getName().startsWith("JTreeGroup")
1058 && !groups[g].getName().startsWith("JGroup"))
1060 tooltipText.append(groups[g].getName());
1063 if (groups[g].getDescription() != null)
1065 tooltipText.append(": " + groups[g].getDescription());
1072 * add any features at the position to the tooltip; if over a gap, only
1073 * add features that straddle the gap (pos may be the residue before or
1076 int unshownFeatures = 0;
1077 if (av.isShowSequenceFeatures())
1079 List<SequenceFeature> features = ap.getFeatureRenderer()
1080 .findFeaturesAtColumn(sequence, column + 1);
1081 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1082 features, this.ap.getSeqPanel().seqCanvas.fr,
1083 MAX_TOOLTIP_LENGTH);
1086 * add features in CDS/protein complement at the corresponding
1087 * position if configured to do so
1089 if (av.isShowComplementFeatures())
1091 if (!Comparison.isGap(sequence.getCharAt(column)))
1093 AlignViewportI complement = ap.getAlignViewport()
1094 .getCodingComplement();
1095 AlignFrame af = Desktop.getAlignFrameFor(complement);
1096 FeatureRendererModel fr2 = af.getFeatureRenderer();
1097 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1101 unshownFeatures += seqARep.appendFeatures(tooltipText,
1102 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1107 if (tooltipText.length() == 6) // "<html>"
1109 setToolTipText(null);
1114 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1116 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1117 tooltipText.append("...");
1119 if (unshownFeatures > 0)
1121 tooltipText.append("<br/>").append("... ").append("<i>")
1122 .append(MessageManager.formatMessage(
1123 "label.features_not_shown", unshownFeatures))
1126 String textString = tooltipText.toString();
1127 if (lastTooltip == null || !lastTooltip.equals(textString))
1129 String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1131 setToolTipText(formattedTooltipText);
1132 lastTooltip = textString;
1138 * When the view is in wrapped mode, and the mouse is over an annotation row,
1139 * shows the corresponding tooltip and status message (if any)
1144 protected void mouseMovedOverAnnotation(MousePos pos)
1146 final int column = pos.column;
1147 final int rowIndex = pos.annotationIndex;
1149 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1154 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1156 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1158 setToolTipText(tooltip);
1159 lastTooltip = tooltip;
1161 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1163 ap.alignFrame.setStatus(msg);
1166 private Point lastp = null;
1171 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1174 public Point getToolTipLocation(MouseEvent event)
1176 if (tooltipText == null || tooltipText.length() <= 6)
1182 int x = event.getX();
1184 // switch sides when tooltip is too close to edge
1185 int wdth = (w - x < 200) ? -(w / 2) : 5;
1187 if (!event.isShiftDown() || p == null)
1189 p = new Point(event.getX() + wdth, event.getY() - 20);
1193 * TODO: try to set position so region is not obscured by tooltip
1201 * set when the current UI interaction has resulted in a change that requires
1202 * shading in overviews and structures to be recalculated. this could be
1203 * changed to a something more expressive that indicates what actually has
1204 * changed, so selective redraws can be applied (ie. only structures, only
1207 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1210 * set if av.getSelectionGroup() refers to a group that is defined on the
1211 * alignment view, rather than a transient selection
1213 // private boolean editingDefinedGroup = false; // TODO: refactor to
1214 // avcontroller or viewModel
1217 * Sets the status message in alignment panel, showing the sequence number
1218 * (index) and id, and residue and residue position if not at a gap, for the
1219 * given sequence and column position. Returns the residue position returned
1220 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1221 * if at a gapped position.
1224 * aligned sequence object
1228 * index of sequence in alignment
1229 * @return sequence position of residue at column, or adjacent residue if at a
1232 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1234 char sequenceChar = sequence.getCharAt(column);
1235 int pos = sequence.findPosition(column);
1236 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1242 * Builds the status message for the current cursor location and writes it to
1243 * the status bar, for example
1246 * Sequence 3 ID: FER1_SOLLC
1247 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1248 * Sequence 5 ID: FER1_PEA Residue: B (3)
1249 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1254 * sequence position in the alignment (1..)
1255 * @param sequenceChar
1256 * the character under the cursor
1258 * the sequence residue position (if not over a gap)
1260 protected void setStatusMessage(String seqName, int seqIndex,
1261 char sequenceChar, int residuePos)
1263 StringBuilder text = new StringBuilder(32);
1266 * Sequence number (if known), and sequence name.
1268 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1269 text.append("Sequence").append(seqno).append(" ID: ")
1272 String residue = null;
1275 * Try to translate the display character to residue name (null for gap).
1277 boolean isGapped = Comparison.isGap(sequenceChar);
1281 boolean nucleotide = av.getAlignment().isNucleotide();
1282 String displayChar = String.valueOf(sequenceChar);
1285 residue = ResidueProperties.nucleotideName.get(displayChar);
1289 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1290 : ("*".equals(displayChar) ? "STOP"
1291 : ResidueProperties.aa2Triplet.get(displayChar));
1293 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1294 .append(": ").append(residue == null ? displayChar : residue);
1296 text.append(" (").append(Integer.toString(residuePos)).append(")");
1298 ap.alignFrame.setStatus(text.toString());
1302 * Set the status bar message to highlight the first matched position in
1307 private void setStatusMessage(SearchResultsI results)
1309 AlignmentI al = this.av.getAlignment();
1310 int sequenceIndex = al.findIndex(results);
1311 if (sequenceIndex == -1)
1315 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1316 SequenceI ds = alignedSeq.getDatasetSequence();
1317 for (SearchResultMatchI m : results.getResults())
1319 SequenceI seq = m.getSequence();
1320 if (seq.getDatasetSequence() != null)
1322 seq = seq.getDatasetSequence();
1327 int start = m.getStart();
1328 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1329 seq.getCharAt(start - 1), start);
1339 public void mouseDragged(MouseEvent evt)
1341 MousePos pos = findMousePosition(evt);
1342 if (pos.isOverAnnotation() || pos.column == -1)
1347 if (mouseWheelPressed)
1349 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1350 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1352 int oldWidth = av.getCharWidth();
1354 // Which is bigger, left-right or up-down?
1355 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1356 .abs(evt.getX() - lastMousePress.getX()))
1359 * on drag up or down, decrement or increment font size
1361 int fontSize = av.font.getSize();
1362 boolean fontChanged = false;
1364 if (evt.getY() < lastMousePress.getY())
1369 else if (evt.getY() > lastMousePress.getY())
1382 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1384 av.setFont(newFont, true);
1385 av.setCharWidth(oldWidth);
1389 ap.av.getCodingComplement().setFont(newFont, true);
1390 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1391 .getSplitViewContainer();
1392 splitFrame.adjustLayout();
1393 splitFrame.repaint();
1400 * on drag left or right, decrement or increment character width
1403 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1405 newWidth = av.getCharWidth() - 1;
1406 av.setCharWidth(newWidth);
1408 else if (evt.getX() > lastMousePress.getX())
1410 newWidth = av.getCharWidth() + 1;
1411 av.setCharWidth(newWidth);
1415 ap.paintAlignment(false, false);
1419 * need to ensure newWidth is set on cdna, regardless of which
1420 * panel the mouse drag happened in; protein will compute its
1421 * character width as 1:1 or 3:1
1423 av.getCodingComplement().setCharWidth(newWidth);
1424 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1425 .getSplitViewContainer();
1426 splitFrame.adjustLayout();
1427 splitFrame.repaint();
1432 FontMetrics fm = getFontMetrics(av.getFont());
1433 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1435 lastMousePress = evt.getPoint();
1442 dragStretchGroup(evt);
1446 int res = pos.column;
1453 if ((editLastRes == -1) || (editLastRes == res))
1458 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1460 // dragLeft, delete gap
1461 editSequence(false, false, res);
1465 editSequence(true, false, res);
1468 mouseDragging = true;
1469 if (scrollThread != null)
1471 scrollThread.setMousePosition(evt.getPoint());
1476 * Edits the sequence to insert or delete one or more gaps, in response to a
1477 * mouse drag or cursor mode command. The number of inserts/deletes may be
1478 * specified with the cursor command, or else depends on the mouse event
1479 * (normally one column, but potentially more for a fast mouse drag).
1481 * Delete gaps is limited to the number of gaps left of the cursor position
1482 * (mouse drag), or at or right of the cursor position (cursor mode).
1484 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1485 * the current selection group.
1487 * In locked editing mode (with a selection group present), inserts/deletions
1488 * within the selection group are limited to its boundaries (and edits outside
1489 * the group stop at its border).
1492 * true to insert gaps, false to delete gaps
1494 * (unused parameter)
1496 * the column at which to perform the action; the number of columns
1497 * affected depends on <code>this.editLastRes</code> (cursor column
1500 synchronized void editSequence(boolean insertGap, boolean editSeq,
1504 int fixedRight = -1;
1505 boolean fixedColumns = false;
1506 SequenceGroup sg = av.getSelectionGroup();
1508 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1510 // No group, but the sequence may represent a group
1511 if (!groupEditing && av.hasHiddenRows())
1513 if (av.isHiddenRepSequence(seq))
1515 sg = av.getRepresentedSequences(seq);
1516 groupEditing = true;
1520 StringBuilder message = new StringBuilder(64); // for status bar
1523 * make a name for the edit action, for
1524 * status bar message and Undo/Redo menu
1526 String label = null;
1529 message.append("Edit group:");
1530 label = MessageManager.getString("action.edit_group");
1534 message.append("Edit sequence: " + seq.getName());
1535 label = seq.getName();
1536 if (label.length() > 10)
1538 label = label.substring(0, 10);
1540 label = MessageManager.formatMessage("label.edit_params",
1546 * initialise the edit command if there is not
1547 * already one being extended
1549 if (editCommand == null)
1551 editCommand = new EditCommand(label);
1556 message.append(" insert ");
1560 message.append(" delete ");
1563 message.append(Math.abs(startres - editLastRes) + " gaps.");
1564 ap.alignFrame.setStatus(message.toString());
1567 * is there a selection group containing the sequence being edited?
1568 * if so the boundary of the group is the limit of the edit
1569 * (but the edit may be inside or outside the selection group)
1571 boolean inSelectionGroup = sg != null
1572 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1573 if (groupEditing || inSelectionGroup)
1575 fixedColumns = true;
1577 // sg might be null as the user may only see 1 sequence,
1578 // but the sequence represents a group
1581 if (!av.isHiddenRepSequence(seq))
1586 sg = av.getRepresentedSequences(seq);
1589 fixedLeft = sg.getStartRes();
1590 fixedRight = sg.getEndRes();
1592 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1593 || (startres >= fixedLeft && editLastRes < fixedLeft)
1594 || (startres > fixedRight && editLastRes <= fixedRight)
1595 || (startres <= fixedRight && editLastRes > fixedRight))
1601 if (fixedLeft > startres)
1603 fixedRight = fixedLeft - 1;
1606 else if (fixedRight < startres)
1608 fixedLeft = fixedRight;
1613 if (av.hasHiddenColumns())
1615 fixedColumns = true;
1616 int y1 = av.getAlignment().getHiddenColumns()
1617 .getNextHiddenBoundary(true, startres);
1618 int y2 = av.getAlignment().getHiddenColumns()
1619 .getNextHiddenBoundary(false, startres);
1621 if ((insertGap && startres > y1 && editLastRes < y1)
1622 || (!insertGap && startres < y2 && editLastRes > y2))
1628 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1629 // Selection spans a hidden region
1630 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1638 fixedRight = y2 - 1;
1643 boolean success = doEditSequence(insertGap, editSeq, startres,
1644 fixedRight, fixedColumns, sg);
1647 * report what actually happened (might be less than
1648 * what was requested), by inspecting the edit commands added
1650 String msg = getEditStatusMessage(editCommand);
1651 ap.alignFrame.setStatus(msg == null ? " " : msg);
1657 editLastRes = startres;
1658 seqCanvas.repaint();
1662 * A helper method that performs the requested editing to insert or delete
1663 * gaps (if possible). Answers true if the edit was successful, false if could
1664 * only be performed in part or not at all. Failure may occur in 'locked edit'
1665 * mode, when an insertion requires a matching gapped position (or column) to
1666 * delete, and deletion requires an adjacent gapped position (or column) to
1670 * true if inserting gap(s), false if deleting
1672 * (unused parameter, currently always false)
1674 * the column at which to perform the edit
1676 * fixed right boundary column of a locked edit (within or to the
1677 * left of a selection group)
1678 * @param fixedColumns
1679 * true if this is a locked edit
1681 * the sequence group (if group edit is being performed)
1684 protected boolean doEditSequence(final boolean insertGap,
1685 final boolean editSeq, final int startres, int fixedRight,
1686 final boolean fixedColumns, final SequenceGroup sg)
1688 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1689 SequenceI[] seqs = new SequenceI[] { seq };
1693 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1694 int g, groupSize = vseqs.size();
1695 SequenceI[] groupSeqs = new SequenceI[groupSize];
1696 for (g = 0; g < groupSeqs.length; g++)
1698 groupSeqs[g] = vseqs.get(g);
1704 // If the user has selected the whole sequence, and is dragging to
1705 // the right, we can still extend the alignment and selectionGroup
1706 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1707 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1710 av.getAlignment().getWidth() + startres - editLastRes);
1711 fixedRight = sg.getEndRes();
1714 // Is it valid with fixed columns??
1715 // Find the next gap before the end
1716 // of the visible region boundary
1717 boolean blank = false;
1718 for (; fixedRight > editLastRes; fixedRight--)
1722 for (g = 0; g < groupSize; g++)
1724 for (int j = 0; j < startres - editLastRes; j++)
1727 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1742 if (sg.getSize() == av.getAlignment().getHeight())
1744 if ((av.hasHiddenColumns()
1745 && startres < av.getAlignment().getHiddenColumns()
1746 .getNextHiddenBoundary(false, startres)))
1751 int alWidth = av.getAlignment().getWidth();
1752 if (av.hasHiddenRows())
1754 int hwidth = av.getAlignment().getHiddenSequences()
1756 if (hwidth > alWidth)
1761 // We can still insert gaps if the selectionGroup
1762 // contains all the sequences
1763 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1764 fixedRight = alWidth + startres - editLastRes;
1774 else if (!insertGap)
1776 // / Are we able to delete?
1777 // ie are all columns blank?
1779 for (g = 0; g < groupSize; g++)
1781 for (int j = startres; j < editLastRes; j++)
1783 if (groupSeqs[g].getLength() <= j)
1788 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1790 // Not a gap, block edit not valid
1799 // dragging to the right
1800 if (fixedColumns && fixedRight != -1)
1802 for (int j = editLastRes; j < startres; j++)
1804 insertGap(j, groupSeqs, fixedRight);
1809 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1810 startres - editLastRes, false);
1815 // dragging to the left
1816 if (fixedColumns && fixedRight != -1)
1818 for (int j = editLastRes; j > startres; j--)
1820 deleteChar(startres, groupSeqs, fixedRight);
1825 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1826 editLastRes - startres, false);
1833 * editing a single sequence
1837 // dragging to the right
1838 if (fixedColumns && fixedRight != -1)
1840 for (int j = editLastRes; j < startres; j++)
1842 if (!insertGap(j, seqs, fixedRight))
1845 * e.g. cursor mode command specified
1846 * more inserts than are possible
1854 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1855 startres - editLastRes, false);
1862 // dragging to the left
1863 if (fixedColumns && fixedRight != -1)
1865 for (int j = editLastRes; j > startres; j--)
1867 if (!Comparison.isGap(seq.getCharAt(startres)))
1871 deleteChar(startres, seqs, fixedRight);
1876 // could be a keyboard edit trying to delete none gaps
1878 for (int m = startres; m < editLastRes; m++)
1880 if (!Comparison.isGap(seq.getCharAt(m)))
1888 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1893 {// insertGap==false AND editSeq==TRUE;
1894 if (fixedColumns && fixedRight != -1)
1896 for (int j = editLastRes; j < startres; j++)
1898 insertGap(j, seqs, fixedRight);
1903 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1904 startres - editLastRes, false);
1914 * Constructs an informative status bar message while dragging to insert or
1915 * delete gaps. Answers null if inserts and deletes cancel out.
1917 * @param editCommand
1918 * a command containing the list of individual edits
1921 protected static String getEditStatusMessage(EditCommand editCommand)
1923 if (editCommand == null)
1929 * add any inserts, and subtract any deletes,
1930 * not counting those auto-inserted when doing a 'locked edit'
1931 * (so only counting edits 'under the cursor')
1934 for (Edit cmd : editCommand.getEdits())
1936 if (!cmd.isSystemGenerated())
1938 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1946 * inserts and deletes cancel out
1951 String msgKey = count > 1 ? "label.insert_gaps"
1952 : (count == 1 ? "label.insert_gap"
1953 : (count == -1 ? "label.delete_gap"
1954 : "label.delete_gaps"));
1955 count = Math.abs(count);
1957 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1961 * Inserts one gap at column j, deleting the right-most gapped column up to
1962 * (and including) fixedColumn. Returns true if the edit is successful, false
1963 * if no blank column is available to allow the insertion to be balanced by a
1968 * @param fixedColumn
1971 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1973 int blankColumn = fixedColumn;
1974 for (int s = 0; s < seq.length; s++)
1976 // Find the next gap before the end of the visible region boundary
1977 // If lastCol > j, theres a boundary after the gap insertion
1979 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1981 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1983 // Theres a space, so break and insert the gap
1988 if (blankColumn <= j)
1990 blankColumn = fixedColumn;
1996 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1998 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2004 * Helper method to add and perform one edit action
2010 * @param systemGenerated
2011 * true if the edit is a 'balancing' delete (or insert) to match a
2012 * user's insert (or delete) in a locked editing region
2014 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2015 int count, boolean systemGenerated)
2018 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2019 av.getAlignment().getGapCharacter());
2020 edit.setSystemGenerated(systemGenerated);
2022 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2026 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2027 * each of the given sequences. The caller should ensure that all sequences
2028 * are gapped in column j.
2032 * @param fixedColumn
2034 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2036 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2038 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2042 * On reentering the panel, stops any scrolling that was started on dragging
2048 public void mouseEntered(MouseEvent e)
2058 * On leaving the panel, if the mouse is being dragged, starts a thread to
2059 * scroll it until the mouse is released (in unwrapped mode only)
2064 public void mouseExited(MouseEvent e)
2066 lastMousePosition = null;
2067 ap.alignFrame.setStatus(" ");
2068 if (av.getWrapAlignment())
2073 if (mouseDragging && scrollThread == null)
2075 scrollThread = new ScrollThread();
2080 * Handler for double-click on a position with one or more sequence features.
2081 * Opens the Amend Features dialog to allow feature details to be amended, or
2082 * the feature deleted.
2085 public void mouseClicked(MouseEvent evt)
2087 SequenceGroup sg = null;
2088 MousePos pos = findMousePosition(evt);
2089 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2094 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2096 sg = av.getSelectionGroup();
2097 if (sg != null && sg.getSize() == 1
2098 && sg.getEndRes() - sg.getStartRes() < 2)
2100 av.setSelectionGroup(null);
2103 int column = pos.column;
2106 * find features at the position (if not gapped), or straddling
2107 * the position (if at a gap)
2109 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2110 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2111 .findFeaturesAtColumn(sequence, column + 1);
2113 if (!features.isEmpty())
2116 * highlight the first feature at the position on the alignment
2118 SearchResultsI highlight = new SearchResults();
2119 highlight.addResult(sequence, features.get(0).getBegin(), features
2121 seqCanvas.highlightSearchResults(highlight, false);
2124 * open the Amend Features dialog; clear highlighting afterwards,
2125 * whether changes were made or not
2127 List<SequenceI> seqs = Collections.singletonList(sequence);
2128 seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2130 av.setSearchResults(null); // clear highlighting
2131 seqCanvas.repaint(); // draw new/amended features
2137 public void mouseWheelMoved(MouseWheelEvent e)
2140 double wheelRotation = e.getPreciseWheelRotation();
2141 if (wheelRotation > 0)
2143 if (e.isShiftDown())
2145 av.getRanges().scrollRight(true);
2150 av.getRanges().scrollUp(false);
2153 else if (wheelRotation < 0)
2155 if (e.isShiftDown())
2157 av.getRanges().scrollRight(false);
2161 av.getRanges().scrollUp(true);
2166 * update status bar and tooltip for new position
2167 * (need to synthesize a mouse movement to refresh tooltip)
2170 ToolTipManager.sharedInstance().mouseMoved(e);
2179 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2181 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2186 final int res = pos.column;
2187 final int seq = pos.seqIndex;
2189 updateOverviewAndStructs = false;
2191 startWrapBlock = wrappedBlock;
2193 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2195 if ((sequence == null) || (res > sequence.getLength()))
2200 stretchGroup = av.getSelectionGroup();
2202 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2204 stretchGroup = av.getAlignment().findGroup(sequence, res);
2205 if (stretchGroup != null)
2207 // only update the current selection if the popup menu has a group to
2209 av.setSelectionGroup(stretchGroup);
2213 if (evt.isPopupTrigger()) // Mac: mousePressed
2215 showPopupMenu(evt, pos);
2220 * defer right-mouse click handling to mouseReleased on Windows
2221 * (where isPopupTrigger() will answer true)
2222 * NB isRightMouseButton is also true for Cmd-click on Mac
2224 if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2231 seqCanvas.cursorX = res;
2232 seqCanvas.cursorY = seq;
2233 seqCanvas.repaint();
2237 if (stretchGroup == null)
2239 createStretchGroup(res, sequence);
2242 if (stretchGroup != null)
2244 stretchGroup.addPropertyChangeListener(seqCanvas);
2247 seqCanvas.repaint();
2250 private void createStretchGroup(int res, SequenceI sequence)
2252 // Only if left mouse button do we want to change group sizes
2253 // define a new group here
2254 SequenceGroup sg = new SequenceGroup();
2255 sg.setStartRes(res);
2257 sg.addSequence(sequence, false);
2258 av.setSelectionGroup(sg);
2261 if (av.getConservationSelected())
2263 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2267 if (av.getAbovePIDThreshold())
2269 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2272 // TODO: stretchGroup will always be not null. Is this a merge error ?
2273 // or is there a threading issue here?
2274 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2276 // Edit end res position of selected group
2277 changeEndRes = true;
2279 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2281 // Edit end res position of selected group
2282 changeStartRes = true;
2284 stretchGroup.getWidth();
2289 * Build and show a pop-up menu at the right-click mouse position
2294 void showPopupMenu(MouseEvent evt, MousePos pos)
2296 final int column = pos.column;
2297 final int seq = pos.seqIndex;
2298 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2299 if (sequence != null)
2301 PopupMenu pop = new PopupMenu(ap, sequence, column);
2302 pop.show(this, evt.getX(), evt.getY());
2307 * Update the display after mouse up on a selection or group
2310 * mouse released event details
2312 * true if this event is happening after a mouse drag (rather than a
2315 protected void doMouseReleasedDefineMode(MouseEvent evt,
2318 if (stretchGroup == null)
2323 stretchGroup.removePropertyChangeListener(seqCanvas);
2325 // always do this - annotation has own state
2326 // but defer colourscheme update until hidden sequences are passed in
2327 boolean vischange = stretchGroup.recalcConservation(true);
2328 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2330 if (stretchGroup.cs != null)
2334 stretchGroup.cs.alignmentChanged(stretchGroup,
2335 av.getHiddenRepSequences());
2338 ResidueShaderI groupColourScheme = stretchGroup
2339 .getGroupColourScheme();
2340 String name = stretchGroup.getName();
2341 if (stretchGroup.cs.conservationApplied())
2343 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2345 if (stretchGroup.cs.getThreshold() > 0)
2347 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2350 PaintRefresher.Refresh(this, av.getSequenceSetId());
2351 // TODO: structure colours only need updating if stretchGroup used to or now
2352 // does contain sequences with structure views
2353 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2354 updateOverviewAndStructs = false;
2355 changeEndRes = false;
2356 changeStartRes = false;
2357 stretchGroup = null;
2362 * Resizes the borders of a selection group depending on the direction of
2367 protected void dragStretchGroup(MouseEvent evt)
2369 if (stretchGroup == null)
2374 MousePos pos = findMousePosition(evt);
2375 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2380 int res = pos.column;
2381 int y = pos.seqIndex;
2383 if (wrappedBlock != startWrapBlock)
2388 res = Math.min(res, av.getAlignment().getWidth()-1);
2390 if (stretchGroup.getEndRes() == res)
2392 // Edit end res position of selected group
2393 changeEndRes = true;
2395 else if (stretchGroup.getStartRes() == res)
2397 // Edit start res position of selected group
2398 changeStartRes = true;
2401 if (res < av.getRanges().getStartRes())
2403 res = av.getRanges().getStartRes();
2408 if (res > (stretchGroup.getStartRes() - 1))
2410 stretchGroup.setEndRes(res);
2411 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2414 else if (changeStartRes)
2416 if (res < (stretchGroup.getEndRes() + 1))
2418 stretchGroup.setStartRes(res);
2419 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2423 int dragDirection = 0;
2429 else if (y < oldSeq)
2434 while ((y != oldSeq) && (oldSeq > -1)
2435 && (y < av.getAlignment().getHeight()))
2437 // This routine ensures we don't skip any sequences, as the
2438 // selection is quite slow.
2439 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2441 oldSeq += dragDirection;
2448 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2450 if (stretchGroup.getSequences(null).contains(nextSeq))
2452 stretchGroup.deleteSequence(seq, false);
2453 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2459 stretchGroup.addSequence(seq, false);
2462 stretchGroup.addSequence(nextSeq, false);
2463 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2472 mouseDragging = true;
2474 if (scrollThread != null)
2476 scrollThread.setMousePosition(evt.getPoint());
2480 * construct a status message showing the range of the selection
2482 StringBuilder status = new StringBuilder(64);
2483 List<SequenceI> seqs = stretchGroup.getSequences();
2484 String name = seqs.get(0).getName();
2485 if (name.length() > 20)
2487 name = name.substring(0, 20);
2489 status.append(name).append(" - ");
2490 name = seqs.get(seqs.size() - 1).getName();
2491 if (name.length() > 20)
2493 name = name.substring(0, 20);
2495 status.append(name).append(" ");
2496 int startRes = stretchGroup.getStartRes();
2497 status.append(" cols ").append(String.valueOf(startRes + 1))
2499 int endRes = stretchGroup.getEndRes();
2500 status.append(String.valueOf(endRes + 1));
2501 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2502 .append(String.valueOf(endRes - startRes + 1)).append(")");
2503 ap.alignFrame.setStatus(status.toString());
2507 * Stops the scroll thread if it is running
2509 void stopScrolling()
2511 if (scrollThread != null)
2513 scrollThread.stopScrolling();
2514 scrollThread = null;
2516 mouseDragging = false;
2520 * Starts a thread to scroll the alignment, towards a given mouse position
2521 * outside the panel bounds
2525 void startScrolling(Point mousePos)
2527 if (scrollThread == null)
2529 scrollThread = new ScrollThread();
2532 mouseDragging = true;
2533 scrollThread.setMousePosition(mousePos);
2537 * Performs scrolling of the visible alignment left, right, up or down
2539 class ScrollThread extends Thread
2541 private Point mousePos;
2543 private volatile boolean threadRunning = true;
2548 public ScrollThread()
2550 setName("SeqPanel$ScrollThread");
2555 * Sets the position of the mouse that determines the direction of the
2560 public void setMousePosition(Point p)
2566 * Sets a flag that will cause the thread to exit
2568 public void stopScrolling()
2570 threadRunning = false;
2574 * Scrolls the alignment left or right, and/or up or down, depending on the
2575 * last notified mouse position, until the limit of the alignment is
2576 * reached, or a flag is set to stop the scroll
2581 while (threadRunning && mouseDragging)
2583 if (mousePos != null)
2585 boolean scrolled = false;
2586 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2593 // mouse is above this panel - try scroll up
2594 scrolled = ranges.scrollUp(true);
2596 else if (mousePos.y >= getHeight())
2598 // mouse is below this panel - try scroll down
2599 scrolled = ranges.scrollUp(false);
2603 * scroll left or right
2607 scrolled |= ranges.scrollRight(false);
2609 else if (mousePos.x >= getWidth())
2611 scrolled |= ranges.scrollRight(true);
2616 * we have reached the limit of the visible alignment - quit
2618 threadRunning = false;
2619 SeqPanel.this.ap.repaint();
2626 } catch (Exception ex)
2634 * modify current selection according to a received message.
2637 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2638 HiddenColumns hidden, SelectionSource source)
2640 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2641 // handles selection messages...
2642 // TODO: extend config options to allow user to control if selections may be
2643 // shared between viewports.
2644 boolean iSentTheSelection = (av == source
2645 || (source instanceof AlignViewport
2646 && ((AlignmentViewport) source).getSequenceSetId()
2647 .equals(av.getSequenceSetId())));
2649 if (iSentTheSelection)
2651 // respond to our own event by updating dependent dialogs
2652 if (ap.getCalculationDialog() != null)
2654 ap.getCalculationDialog().validateCalcTypes();
2660 // process further ?
2661 if (!av.followSelection)
2667 * Ignore the selection if there is one of our own pending.
2669 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2675 * Check for selection in a view of which this one is a dna/protein
2678 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2683 // do we want to thread this ? (contention with seqsel and colsel locks, I
2686 * only copy colsel if there is a real intersection between
2687 * sequence selection and this panel's alignment
2689 boolean repaint = false;
2690 boolean copycolsel = false;
2692 SequenceGroup sgroup = null;
2693 if (seqsel != null && seqsel.getSize() > 0)
2695 if (av.getAlignment() == null)
2697 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2698 + " ViewId=" + av.getViewId()
2699 + " 's alignment is NULL! returning immediately.");
2702 sgroup = seqsel.intersect(av.getAlignment(),
2703 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2704 if ((sgroup != null && sgroup.getSize() > 0))
2709 if (sgroup != null && sgroup.getSize() > 0)
2711 av.setSelectionGroup(sgroup);
2715 av.setSelectionGroup(null);
2717 av.isSelectionGroupChanged(true);
2722 // the current selection is unset or from a previous message
2723 // so import the new colsel.
2724 if (colsel == null || colsel.isEmpty())
2726 if (av.getColumnSelection() != null)
2728 av.getColumnSelection().clear();
2734 // TODO: shift colSel according to the intersecting sequences
2735 if (av.getColumnSelection() == null)
2737 av.setColumnSelection(new ColumnSelection(colsel));
2741 av.getColumnSelection().setElementsFrom(colsel,
2742 av.getAlignment().getHiddenColumns());
2745 av.isColSelChanged(true);
2749 if (copycolsel && av.hasHiddenColumns()
2750 && (av.getAlignment().getHiddenColumns() == null))
2752 System.err.println("Bad things");
2754 if (repaint) // always true!
2756 // probably finessing with multiple redraws here
2757 PaintRefresher.Refresh(this, av.getSequenceSetId());
2758 // ap.paintAlignment(false);
2761 // lastly, update dependent dialogs
2762 if (ap.getCalculationDialog() != null)
2764 ap.getCalculationDialog().validateCalcTypes();
2770 * If this panel is a cdna/protein translation view of the selection source,
2771 * tries to map the source selection to a local one, and returns true. Else
2778 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2779 ColumnSelection colsel, HiddenColumns hidden,
2780 SelectionSource source)
2782 if (!(source instanceof AlignViewportI))
2786 final AlignViewportI sourceAv = (AlignViewportI) source;
2787 if (sourceAv.getCodingComplement() != av
2788 && av.getCodingComplement() != sourceAv)
2794 * Map sequence selection
2796 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2797 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2798 av.isSelectionGroupChanged(true);
2801 * Map column selection
2803 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2805 ColumnSelection cs = new ColumnSelection();
2806 HiddenColumns hs = new HiddenColumns();
2807 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2808 av.setColumnSelection(cs);
2809 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2811 // lastly, update any dependent dialogs
2812 if (ap.getCalculationDialog() != null)
2814 ap.getCalculationDialog().validateCalcTypes();
2818 * repaint alignment, and also Overview or Structure
2819 * if hidden column selection has changed
2821 ap.paintAlignment(hiddenChanged, hiddenChanged);
2828 * @return null or last search results handled by this panel
2830 public SearchResultsI getLastSearchResults()
2832 return lastSearchResults;