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 seqCanvas.cursorX += dx;
464 seqCanvas.cursorY += dy;
466 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
468 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
470 int original = seqCanvas.cursorX - dx;
471 int maxWidth = av.getAlignment().getWidth();
473 if (!hidden.isVisible(seqCanvas.cursorX))
475 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
476 int[] region = hidden.getRegionWithEdgeAtRes(visx);
478 if (region != null) // just in case
483 seqCanvas.cursorX = region[1] + 1;
488 seqCanvas.cursorX = region[0] - 1;
491 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
494 if (seqCanvas.cursorX >= maxWidth
495 || !hidden.isVisible(seqCanvas.cursorX))
497 seqCanvas.cursorX = original;
501 scrollToVisible(false);
505 * Scroll to make the cursor visible in the viewport.
508 * just jump to the location rather than scrolling
510 void scrollToVisible(boolean jump)
512 if (seqCanvas.cursorX < 0)
514 seqCanvas.cursorX = 0;
516 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
518 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
521 if (seqCanvas.cursorY < 0)
523 seqCanvas.cursorY = 0;
525 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
527 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
532 boolean repaintNeeded = true;
535 // only need to repaint if the viewport did not move, as otherwise it will
537 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
542 if (av.getWrapAlignment())
544 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
545 int x = av.getAlignment().getHiddenColumns()
546 .absoluteToVisibleColumn(seqCanvas.cursorX);
547 av.getRanges().scrollToWrappedVisible(x);
551 av.getRanges().scrollToVisible(seqCanvas.cursorX,
556 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
558 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
559 seqCanvas.cursorX, seqCanvas.cursorY);
569 void setSelectionAreaAtCursor(boolean topLeft)
571 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
573 if (av.getSelectionGroup() != null)
575 SequenceGroup sg = av.getSelectionGroup();
576 // Find the top and bottom of this group
577 int min = av.getAlignment().getHeight(), max = 0;
578 for (int i = 0; i < sg.getSize(); i++)
580 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
595 sg.setStartRes(seqCanvas.cursorX);
596 if (sg.getEndRes() < seqCanvas.cursorX)
598 sg.setEndRes(seqCanvas.cursorX);
601 min = seqCanvas.cursorY;
605 sg.setEndRes(seqCanvas.cursorX);
606 if (sg.getStartRes() > seqCanvas.cursorX)
608 sg.setStartRes(seqCanvas.cursorX);
611 max = seqCanvas.cursorY + 1;
616 // Only the user can do this
617 av.setSelectionGroup(null);
621 // Now add any sequences between min and max
622 sg.getSequences(null).clear();
623 for (int i = min; i < max; i++)
625 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
630 if (av.getSelectionGroup() == null)
632 SequenceGroup sg = new SequenceGroup();
633 sg.setStartRes(seqCanvas.cursorX);
634 sg.setEndRes(seqCanvas.cursorX);
635 sg.addSequence(sequence, false);
636 av.setSelectionGroup(sg);
639 ap.paintAlignment(false, false);
643 void insertGapAtCursor(boolean group)
645 groupEditing = group;
646 editStartSeq = seqCanvas.cursorY;
647 editLastRes = seqCanvas.cursorX;
648 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
652 void deleteGapAtCursor(boolean group)
654 groupEditing = group;
655 editStartSeq = seqCanvas.cursorY;
656 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
657 editSequence(false, false, seqCanvas.cursorX);
661 void insertNucAtCursor(boolean group, String nuc)
663 // TODO not called - delete?
664 groupEditing = group;
665 editStartSeq = seqCanvas.cursorY;
666 editLastRes = seqCanvas.cursorX;
667 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
671 void numberPressed(char value)
673 if (keyboardNo1 == null)
675 keyboardNo1 = new StringBuffer();
678 if (keyboardNo2 != null)
680 keyboardNo2.append(value);
684 keyboardNo1.append(value);
692 if (keyboardNo1 != null)
694 int value = Integer.parseInt(keyboardNo1.toString());
698 } catch (Exception x)
709 if (keyboardNo2 != null)
711 int value = Integer.parseInt(keyboardNo2.toString());
715 } catch (Exception x)
729 public void mouseReleased(MouseEvent evt)
731 MousePos pos = findMousePosition(evt);
732 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
737 boolean didDrag = mouseDragging; // did we come here after a drag
738 mouseDragging = false;
739 mouseWheelPressed = false;
741 if (evt.isPopupTrigger()) // Windows: mouseReleased
743 showPopupMenu(evt, pos);
754 doMouseReleasedDefineMode(evt, didDrag);
765 public void mousePressed(MouseEvent evt)
767 lastMousePress = evt.getPoint();
768 MousePos pos = findMousePosition(evt);
769 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
774 if (SwingUtilities.isMiddleMouseButton(evt))
776 mouseWheelPressed = true;
780 boolean isControlDown = Platform.isControlDown(evt);
781 if (evt.isShiftDown() || isControlDown)
791 doMousePressedDefineMode(evt, pos);
795 int seq = pos.seqIndex;
796 int res = pos.column;
798 if ((seq < av.getAlignment().getHeight())
799 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
816 public void mouseOverSequence(SequenceI sequence, int index, int pos)
818 String tmp = sequence.hashCode() + " " + index + " " + pos;
820 if (lastMessage == null || !lastMessage.equals(tmp))
822 // System.err.println("mouseOver Sequence: "+tmp);
823 ssm.mouseOverSequence(sequence, index, pos, av);
829 * Highlight the mapped region described by the search results object (unless
830 * unchanged). This supports highlight of protein while mousing over linked
831 * cDNA and vice versa. The status bar is also updated to show the location of
832 * the start of the highlighted region.
835 public String highlightSequence(SearchResultsI results)
837 if (results == null || results.equals(lastSearchResults))
841 lastSearchResults = results;
843 boolean wasScrolled = false;
845 if (av.isFollowHighlight())
847 // don't allow highlight of protein/cDNA to also scroll a complementary
848 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
849 // over residue to change abruptly, causing highlighted residue in panel 2
850 // to change, causing a scroll in panel 1 etc)
851 ap.setToScrollComplementPanel(false);
852 wasScrolled = ap.scrollToPosition(results);
855 seqCanvas.revalidate();
857 ap.setToScrollComplementPanel(true);
860 boolean noFastPaint = wasScrolled && av.getWrapAlignment();
861 if (seqCanvas.highlightSearchResults(results, noFastPaint))
863 setStatusMessage(results);
865 // JAL-3303 feature suppressed for now pending review
866 return null; // results.isEmpty() ? null : getHighlightInfo(results);
870 * temporary hack: answers a message suitable to show on structure hover
871 * label. This is normally null. It is a peptide variation description if
873 * <li>results are a single residue in a protein alignment</li>
874 * <li>there is a mapping to a coding sequence (codon)</li>
875 * <li>there are one or more SNP variant features on the codon</li>
877 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
882 private String getHighlightInfo(SearchResultsI results)
885 * ideally, just find mapped CDS (as we don't care about render style here);
886 * for now, go via split frame complement's FeatureRenderer
888 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
889 if (complement == null)
893 AlignFrame af = Desktop.getAlignFrameFor(complement);
894 FeatureRendererModel fr2 = af.getFeatureRenderer();
896 int j = results.getSize();
897 List<String> infos = new ArrayList<>();
898 for (int i = 0; i < j; i++)
900 SearchResultMatchI match = results.getResults().get(i);
901 int pos = match.getStart();
902 if (pos == match.getEnd())
904 SequenceI seq = match.getSequence();
905 SequenceI ds = seq.getDatasetSequence() == null ? seq
906 : seq.getDatasetSequence();
907 MappedFeatures mf = fr2
908 .findComplementFeaturesAtResidue(ds, pos);
911 for (SequenceFeature sf : mf.features)
913 String pv = mf.findProteinVariants(sf);
914 if (pv.length() > 0 && !infos.contains(pv))
927 StringBuilder sb = new StringBuilder();
928 for (String info : infos)
936 return sb.toString();
940 public VamsasSource getVamsasSource()
942 return this.ap == null ? null : this.ap.av;
946 public void updateColours(SequenceI seq, int index)
948 System.out.println("update the seqPanel colours");
953 * Action on mouse movement is to update the status bar to show the current
954 * sequence position, and (if features are shown) to show any features at the
955 * position in a tooltip. Does nothing if the mouse move does not change
961 public void mouseMoved(MouseEvent evt)
965 // This is because MacOSX creates a mouseMoved
966 // If control is down, other platforms will not.
970 final MousePos mousePos = findMousePosition(evt);
971 if (mousePos.equals(lastMousePosition))
974 * just a pixel move without change of 'cell'
978 lastMousePosition = mousePos;
980 if (mousePos.isOverAnnotation())
982 mouseMovedOverAnnotation(mousePos);
985 final int seq = mousePos.seqIndex;
987 final int column = mousePos.column;
988 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
990 lastMousePosition = null;
991 setToolTipText(null);
993 ap.alignFrame.setStatus("");
997 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
999 if (column >= sequence.getLength())
1005 * set status bar message, returning residue position in sequence
1007 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1008 final int pos = setStatusMessage(sequence, column, seq);
1009 if (ssm != null && !isGapped)
1011 mouseOverSequence(sequence, column, pos);
1014 tooltipText.setLength(6); // Cuts the buffer back to <html>
1016 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1019 for (int g = 0; g < groups.length; g++)
1021 if (groups[g].getStartRes() <= column
1022 && groups[g].getEndRes() >= column)
1024 if (!groups[g].getName().startsWith("JTreeGroup")
1025 && !groups[g].getName().startsWith("JGroup"))
1027 tooltipText.append(groups[g].getName());
1030 if (groups[g].getDescription() != null)
1032 tooltipText.append(": " + groups[g].getDescription());
1039 * add any features at the position to the tooltip; if over a gap, only
1040 * add features that straddle the gap (pos may be the residue before or
1043 int unshownFeatures = 0;
1044 if (av.isShowSequenceFeatures())
1046 List<SequenceFeature> features = ap.getFeatureRenderer()
1047 .findFeaturesAtColumn(sequence, column + 1);
1048 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1049 features, this.ap.getSeqPanel().seqCanvas.fr,
1050 MAX_TOOLTIP_LENGTH);
1053 * add features in CDS/protein complement at the corresponding
1054 * position if configured to do so
1056 if (av.isShowComplementFeatures())
1058 if (!Comparison.isGap(sequence.getCharAt(column)))
1060 AlignViewportI complement = ap.getAlignViewport()
1061 .getCodingComplement();
1062 AlignFrame af = Desktop.getAlignFrameFor(complement);
1063 FeatureRendererModel fr2 = af.getFeatureRenderer();
1064 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1068 unshownFeatures = seqARep.appendFeatures(tooltipText,
1069 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1074 if (tooltipText.length() == 6) // "<html>"
1076 setToolTipText(null);
1081 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1083 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1084 tooltipText.append("...");
1086 if (unshownFeatures > 0)
1088 tooltipText.append("<br/>").append("... ").append("<i>")
1089 .append(MessageManager.formatMessage(
1090 "label.features_not_shown", unshownFeatures))
1093 String textString = tooltipText.toString();
1094 if (lastTooltip == null || !lastTooltip.equals(textString))
1096 String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1098 setToolTipText(formattedTooltipText);
1099 lastTooltip = textString;
1105 * When the view is in wrapped mode, and the mouse is over an annotation row,
1106 * shows the corresponding tooltip and status message (if any)
1111 protected void mouseMovedOverAnnotation(MousePos pos)
1113 final int column = pos.column;
1114 final int rowIndex = pos.annotationIndex;
1116 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1121 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1123 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1125 setToolTipText(tooltip);
1126 lastTooltip = tooltip;
1128 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1130 ap.alignFrame.setStatus(msg);
1133 private Point lastp = null;
1138 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1141 public Point getToolTipLocation(MouseEvent event)
1143 if (tooltipText == null || tooltipText.length() <= 6)
1149 int x = event.getX();
1151 // switch sides when tooltip is too close to edge
1152 int wdth = (w - x < 200) ? -(w / 2) : 5;
1154 if (!event.isShiftDown() || p == null)
1156 p = new Point(event.getX() + wdth, event.getY() - 20);
1160 * TODO: try to set position so region is not obscured by tooltip
1168 * set when the current UI interaction has resulted in a change that requires
1169 * shading in overviews and structures to be recalculated. this could be
1170 * changed to a something more expressive that indicates what actually has
1171 * changed, so selective redraws can be applied (ie. only structures, only
1174 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1177 * set if av.getSelectionGroup() refers to a group that is defined on the
1178 * alignment view, rather than a transient selection
1180 // private boolean editingDefinedGroup = false; // TODO: refactor to
1181 // avcontroller or viewModel
1184 * Sets the status message in alignment panel, showing the sequence number
1185 * (index) and id, and residue and residue position if not at a gap, for the
1186 * given sequence and column position. Returns the residue position returned
1187 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1188 * if at a gapped position.
1191 * aligned sequence object
1195 * index of sequence in alignment
1196 * @return sequence position of residue at column, or adjacent residue if at a
1199 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1201 char sequenceChar = sequence.getCharAt(column);
1202 int pos = sequence.findPosition(column);
1203 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1209 * Builds the status message for the current cursor location and writes it to
1210 * the status bar, for example
1213 * Sequence 3 ID: FER1_SOLLC
1214 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1215 * Sequence 5 ID: FER1_PEA Residue: B (3)
1216 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1221 * sequence position in the alignment (1..)
1222 * @param sequenceChar
1223 * the character under the cursor
1225 * the sequence residue position (if not over a gap)
1227 protected void setStatusMessage(String seqName, int seqIndex,
1228 char sequenceChar, int residuePos)
1230 StringBuilder text = new StringBuilder(32);
1233 * Sequence number (if known), and sequence name.
1235 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1236 text.append("Sequence").append(seqno).append(" ID: ")
1239 String residue = null;
1242 * Try to translate the display character to residue name (null for gap).
1244 boolean isGapped = Comparison.isGap(sequenceChar);
1248 boolean nucleotide = av.getAlignment().isNucleotide();
1249 String displayChar = String.valueOf(sequenceChar);
1252 residue = ResidueProperties.nucleotideName.get(displayChar);
1256 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1257 : ("*".equals(displayChar) ? "STOP"
1258 : ResidueProperties.aa2Triplet.get(displayChar));
1260 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1261 .append(": ").append(residue == null ? displayChar : residue);
1263 text.append(" (").append(Integer.toString(residuePos)).append(")");
1265 ap.alignFrame.setStatus(text.toString());
1269 * Set the status bar message to highlight the first matched position in
1274 private void setStatusMessage(SearchResultsI results)
1276 AlignmentI al = this.av.getAlignment();
1277 int sequenceIndex = al.findIndex(results);
1278 if (sequenceIndex == -1)
1282 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1283 SequenceI ds = alignedSeq.getDatasetSequence();
1284 for (SearchResultMatchI m : results.getResults())
1286 SequenceI seq = m.getSequence();
1287 if (seq.getDatasetSequence() != null)
1289 seq = seq.getDatasetSequence();
1294 int start = m.getStart();
1295 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1296 seq.getCharAt(start - 1), start);
1306 public void mouseDragged(MouseEvent evt)
1308 MousePos pos = findMousePosition(evt);
1309 if (pos.isOverAnnotation() || pos.column == -1)
1314 if (mouseWheelPressed)
1316 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1317 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1319 int oldWidth = av.getCharWidth();
1321 // Which is bigger, left-right or up-down?
1322 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1323 .abs(evt.getX() - lastMousePress.getX()))
1326 * on drag up or down, decrement or increment font size
1328 int fontSize = av.font.getSize();
1329 boolean fontChanged = false;
1331 if (evt.getY() < lastMousePress.getY())
1336 else if (evt.getY() > lastMousePress.getY())
1349 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1351 av.setFont(newFont, true);
1352 av.setCharWidth(oldWidth);
1356 ap.av.getCodingComplement().setFont(newFont, true);
1357 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1358 .getSplitViewContainer();
1359 splitFrame.adjustLayout();
1360 splitFrame.repaint();
1367 * on drag left or right, decrement or increment character width
1370 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1372 newWidth = av.getCharWidth() - 1;
1373 av.setCharWidth(newWidth);
1375 else if (evt.getX() > lastMousePress.getX())
1377 newWidth = av.getCharWidth() + 1;
1378 av.setCharWidth(newWidth);
1382 ap.paintAlignment(false, false);
1386 * need to ensure newWidth is set on cdna, regardless of which
1387 * panel the mouse drag happened in; protein will compute its
1388 * character width as 1:1 or 3:1
1390 av.getCodingComplement().setCharWidth(newWidth);
1391 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1392 .getSplitViewContainer();
1393 splitFrame.adjustLayout();
1394 splitFrame.repaint();
1399 FontMetrics fm = getFontMetrics(av.getFont());
1400 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1402 lastMousePress = evt.getPoint();
1409 dragStretchGroup(evt);
1413 int res = pos.column;
1420 if ((editLastRes == -1) || (editLastRes == res))
1425 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1427 // dragLeft, delete gap
1428 editSequence(false, false, res);
1432 editSequence(true, false, res);
1435 mouseDragging = true;
1436 if (scrollThread != null)
1438 scrollThread.setMousePosition(evt.getPoint());
1443 * Edits the sequence to insert or delete one or more gaps, in response to a
1444 * mouse drag or cursor mode command. The number of inserts/deletes may be
1445 * specified with the cursor command, or else depends on the mouse event
1446 * (normally one column, but potentially more for a fast mouse drag).
1448 * Delete gaps is limited to the number of gaps left of the cursor position
1449 * (mouse drag), or at or right of the cursor position (cursor mode).
1451 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1452 * the current selection group.
1454 * In locked editing mode (with a selection group present), inserts/deletions
1455 * within the selection group are limited to its boundaries (and edits outside
1456 * the group stop at its border).
1459 * true to insert gaps, false to delete gaps
1461 * (unused parameter)
1463 * the column at which to perform the action; the number of columns
1464 * affected depends on <code>this.editLastRes</code> (cursor column
1467 synchronized void editSequence(boolean insertGap, boolean editSeq,
1471 int fixedRight = -1;
1472 boolean fixedColumns = false;
1473 SequenceGroup sg = av.getSelectionGroup();
1475 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1477 // No group, but the sequence may represent a group
1478 if (!groupEditing && av.hasHiddenRows())
1480 if (av.isHiddenRepSequence(seq))
1482 sg = av.getRepresentedSequences(seq);
1483 groupEditing = true;
1487 StringBuilder message = new StringBuilder(64); // for status bar
1490 * make a name for the edit action, for
1491 * status bar message and Undo/Redo menu
1493 String label = null;
1496 message.append("Edit group:");
1497 label = MessageManager.getString("action.edit_group");
1501 message.append("Edit sequence: " + seq.getName());
1502 label = seq.getName();
1503 if (label.length() > 10)
1505 label = label.substring(0, 10);
1507 label = MessageManager.formatMessage("label.edit_params",
1513 * initialise the edit command if there is not
1514 * already one being extended
1516 if (editCommand == null)
1518 editCommand = new EditCommand(label);
1523 message.append(" insert ");
1527 message.append(" delete ");
1530 message.append(Math.abs(startres - editLastRes) + " gaps.");
1531 ap.alignFrame.setStatus(message.toString());
1534 * is there a selection group containing the sequence being edited?
1535 * if so the boundary of the group is the limit of the edit
1536 * (but the edit may be inside or outside the selection group)
1538 boolean inSelectionGroup = sg != null
1539 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1540 if (groupEditing || inSelectionGroup)
1542 fixedColumns = true;
1544 // sg might be null as the user may only see 1 sequence,
1545 // but the sequence represents a group
1548 if (!av.isHiddenRepSequence(seq))
1553 sg = av.getRepresentedSequences(seq);
1556 fixedLeft = sg.getStartRes();
1557 fixedRight = sg.getEndRes();
1559 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1560 || (startres >= fixedLeft && editLastRes < fixedLeft)
1561 || (startres > fixedRight && editLastRes <= fixedRight)
1562 || (startres <= fixedRight && editLastRes > fixedRight))
1568 if (fixedLeft > startres)
1570 fixedRight = fixedLeft - 1;
1573 else if (fixedRight < startres)
1575 fixedLeft = fixedRight;
1580 if (av.hasHiddenColumns())
1582 fixedColumns = true;
1583 int y1 = av.getAlignment().getHiddenColumns()
1584 .getNextHiddenBoundary(true, startres);
1585 int y2 = av.getAlignment().getHiddenColumns()
1586 .getNextHiddenBoundary(false, startres);
1588 if ((insertGap && startres > y1 && editLastRes < y1)
1589 || (!insertGap && startres < y2 && editLastRes > y2))
1595 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1596 // Selection spans a hidden region
1597 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1605 fixedRight = y2 - 1;
1610 boolean success = doEditSequence(insertGap, editSeq, startres,
1611 fixedRight, fixedColumns, sg);
1614 * report what actually happened (might be less than
1615 * what was requested), by inspecting the edit commands added
1617 String msg = getEditStatusMessage(editCommand);
1618 ap.alignFrame.setStatus(msg == null ? " " : msg);
1624 editLastRes = startres;
1625 seqCanvas.repaint();
1629 * A helper method that performs the requested editing to insert or delete
1630 * gaps (if possible). Answers true if the edit was successful, false if could
1631 * only be performed in part or not at all. Failure may occur in 'locked edit'
1632 * mode, when an insertion requires a matching gapped position (or column) to
1633 * delete, and deletion requires an adjacent gapped position (or column) to
1637 * true if inserting gap(s), false if deleting
1639 * (unused parameter, currently always false)
1641 * the column at which to perform the edit
1643 * fixed right boundary column of a locked edit (within or to the
1644 * left of a selection group)
1645 * @param fixedColumns
1646 * true if this is a locked edit
1648 * the sequence group (if group edit is being performed)
1651 protected boolean doEditSequence(final boolean insertGap,
1652 final boolean editSeq, final int startres, int fixedRight,
1653 final boolean fixedColumns, final SequenceGroup sg)
1655 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1656 SequenceI[] seqs = new SequenceI[] { seq };
1660 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1661 int g, groupSize = vseqs.size();
1662 SequenceI[] groupSeqs = new SequenceI[groupSize];
1663 for (g = 0; g < groupSeqs.length; g++)
1665 groupSeqs[g] = vseqs.get(g);
1671 // If the user has selected the whole sequence, and is dragging to
1672 // the right, we can still extend the alignment and selectionGroup
1673 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1674 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1677 av.getAlignment().getWidth() + startres - editLastRes);
1678 fixedRight = sg.getEndRes();
1681 // Is it valid with fixed columns??
1682 // Find the next gap before the end
1683 // of the visible region boundary
1684 boolean blank = false;
1685 for (; fixedRight > editLastRes; fixedRight--)
1689 for (g = 0; g < groupSize; g++)
1691 for (int j = 0; j < startres - editLastRes; j++)
1694 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1709 if (sg.getSize() == av.getAlignment().getHeight())
1711 if ((av.hasHiddenColumns()
1712 && startres < av.getAlignment().getHiddenColumns()
1713 .getNextHiddenBoundary(false, startres)))
1718 int alWidth = av.getAlignment().getWidth();
1719 if (av.hasHiddenRows())
1721 int hwidth = av.getAlignment().getHiddenSequences()
1723 if (hwidth > alWidth)
1728 // We can still insert gaps if the selectionGroup
1729 // contains all the sequences
1730 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1731 fixedRight = alWidth + startres - editLastRes;
1741 else if (!insertGap)
1743 // / Are we able to delete?
1744 // ie are all columns blank?
1746 for (g = 0; g < groupSize; g++)
1748 for (int j = startres; j < editLastRes; j++)
1750 if (groupSeqs[g].getLength() <= j)
1755 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1757 // Not a gap, block edit not valid
1766 // dragging to the right
1767 if (fixedColumns && fixedRight != -1)
1769 for (int j = editLastRes; j < startres; j++)
1771 insertGap(j, groupSeqs, fixedRight);
1776 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1777 startres - editLastRes, false);
1782 // dragging to the left
1783 if (fixedColumns && fixedRight != -1)
1785 for (int j = editLastRes; j > startres; j--)
1787 deleteChar(startres, groupSeqs, fixedRight);
1792 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1793 editLastRes - startres, false);
1800 * editing a single sequence
1804 // dragging to the right
1805 if (fixedColumns && fixedRight != -1)
1807 for (int j = editLastRes; j < startres; j++)
1809 if (!insertGap(j, seqs, fixedRight))
1812 * e.g. cursor mode command specified
1813 * more inserts than are possible
1821 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1822 startres - editLastRes, false);
1829 // dragging to the left
1830 if (fixedColumns && fixedRight != -1)
1832 for (int j = editLastRes; j > startres; j--)
1834 if (!Comparison.isGap(seq.getCharAt(startres)))
1838 deleteChar(startres, seqs, fixedRight);
1843 // could be a keyboard edit trying to delete none gaps
1845 for (int m = startres; m < editLastRes; m++)
1847 if (!Comparison.isGap(seq.getCharAt(m)))
1855 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1860 {// insertGap==false AND editSeq==TRUE;
1861 if (fixedColumns && fixedRight != -1)
1863 for (int j = editLastRes; j < startres; j++)
1865 insertGap(j, seqs, fixedRight);
1870 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1871 startres - editLastRes, false);
1881 * Constructs an informative status bar message while dragging to insert or
1882 * delete gaps. Answers null if inserts and deletes cancel out.
1884 * @param editCommand
1885 * a command containing the list of individual edits
1888 protected static String getEditStatusMessage(EditCommand editCommand)
1890 if (editCommand == null)
1896 * add any inserts, and subtract any deletes,
1897 * not counting those auto-inserted when doing a 'locked edit'
1898 * (so only counting edits 'under the cursor')
1901 for (Edit cmd : editCommand.getEdits())
1903 if (!cmd.isSystemGenerated())
1905 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1913 * inserts and deletes cancel out
1918 String msgKey = count > 1 ? "label.insert_gaps"
1919 : (count == 1 ? "label.insert_gap"
1920 : (count == -1 ? "label.delete_gap"
1921 : "label.delete_gaps"));
1922 count = Math.abs(count);
1924 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1928 * Inserts one gap at column j, deleting the right-most gapped column up to
1929 * (and including) fixedColumn. Returns true if the edit is successful, false
1930 * if no blank column is available to allow the insertion to be balanced by a
1935 * @param fixedColumn
1938 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1940 int blankColumn = fixedColumn;
1941 for (int s = 0; s < seq.length; s++)
1943 // Find the next gap before the end of the visible region boundary
1944 // If lastCol > j, theres a boundary after the gap insertion
1946 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1948 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1950 // Theres a space, so break and insert the gap
1955 if (blankColumn <= j)
1957 blankColumn = fixedColumn;
1963 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1965 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1971 * Helper method to add and perform one edit action
1977 * @param systemGenerated
1978 * true if the edit is a 'balancing' delete (or insert) to match a
1979 * user's insert (or delete) in a locked editing region
1981 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1982 int count, boolean systemGenerated)
1985 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1986 av.getAlignment().getGapCharacter());
1987 edit.setSystemGenerated(systemGenerated);
1989 editCommand.appendEdit(edit, av.getAlignment(), true, null);
1993 * Deletes the character at column j, and inserts a gap at fixedColumn, in
1994 * each of the given sequences. The caller should ensure that all sequences
1995 * are gapped in column j.
1999 * @param fixedColumn
2001 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2003 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2005 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2009 * On reentering the panel, stops any scrolling that was started on dragging
2015 public void mouseEntered(MouseEvent e)
2025 * On leaving the panel, if the mouse is being dragged, starts a thread to
2026 * scroll it until the mouse is released (in unwrapped mode only)
2031 public void mouseExited(MouseEvent e)
2033 lastMousePosition = null;
2034 ap.alignFrame.setStatus(" ");
2035 if (av.getWrapAlignment())
2040 if (mouseDragging && scrollThread == null)
2042 scrollThread = new ScrollThread();
2047 * Handler for double-click on a position with one or more sequence features.
2048 * Opens the Amend Features dialog to allow feature details to be amended, or
2049 * the feature deleted.
2052 public void mouseClicked(MouseEvent evt)
2054 SequenceGroup sg = null;
2055 MousePos pos = findMousePosition(evt);
2056 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2061 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2063 sg = av.getSelectionGroup();
2064 if (sg != null && sg.getSize() == 1
2065 && sg.getEndRes() - sg.getStartRes() < 2)
2067 av.setSelectionGroup(null);
2070 int column = pos.column;
2073 * find features at the position (if not gapped), or straddling
2074 * the position (if at a gap)
2076 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2077 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2078 .findFeaturesAtColumn(sequence, column + 1);
2080 if (!features.isEmpty())
2083 * highlight the first feature at the position on the alignment
2085 SearchResultsI highlight = new SearchResults();
2086 highlight.addResult(sequence, features.get(0).getBegin(), features
2088 seqCanvas.highlightSearchResults(highlight, false);
2091 * open the Amend Features dialog; clear highlighting afterwards,
2092 * whether changes were made or not
2094 List<SequenceI> seqs = Collections.singletonList(sequence);
2095 seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2097 av.setSearchResults(null); // clear highlighting
2098 seqCanvas.repaint(); // draw new/amended features
2104 public void mouseWheelMoved(MouseWheelEvent e)
2107 double wheelRotation = e.getPreciseWheelRotation();
2108 if (wheelRotation > 0)
2110 if (e.isShiftDown())
2112 av.getRanges().scrollRight(true);
2117 av.getRanges().scrollUp(false);
2120 else if (wheelRotation < 0)
2122 if (e.isShiftDown())
2124 av.getRanges().scrollRight(false);
2128 av.getRanges().scrollUp(true);
2133 * update status bar and tooltip for new position
2134 * (need to synthesize a mouse movement to refresh tooltip)
2137 ToolTipManager.sharedInstance().mouseMoved(e);
2146 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2148 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2153 final int res = pos.column;
2154 final int seq = pos.seqIndex;
2156 updateOverviewAndStructs = false;
2158 startWrapBlock = wrappedBlock;
2160 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2162 if ((sequence == null) || (res > sequence.getLength()))
2167 stretchGroup = av.getSelectionGroup();
2169 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2171 stretchGroup = av.getAlignment().findGroup(sequence, res);
2172 if (stretchGroup != null)
2174 // only update the current selection if the popup menu has a group to
2176 av.setSelectionGroup(stretchGroup);
2180 if (evt.isPopupTrigger()) // Mac: mousePressed
2182 showPopupMenu(evt, pos);
2187 * defer right-mouse click handling to mouseReleased on Windows
2188 * (where isPopupTrigger() will answer true)
2189 * NB isRightMouseButton is also true for Cmd-click on Mac
2191 if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2198 seqCanvas.cursorX = res;
2199 seqCanvas.cursorY = seq;
2200 seqCanvas.repaint();
2204 if (stretchGroup == null)
2206 createStretchGroup(res, sequence);
2209 if (stretchGroup != null)
2211 stretchGroup.addPropertyChangeListener(seqCanvas);
2214 seqCanvas.repaint();
2217 private void createStretchGroup(int res, SequenceI sequence)
2219 // Only if left mouse button do we want to change group sizes
2220 // define a new group here
2221 SequenceGroup sg = new SequenceGroup();
2222 sg.setStartRes(res);
2224 sg.addSequence(sequence, false);
2225 av.setSelectionGroup(sg);
2228 if (av.getConservationSelected())
2230 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2234 if (av.getAbovePIDThreshold())
2236 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2239 // TODO: stretchGroup will always be not null. Is this a merge error ?
2240 // or is there a threading issue here?
2241 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2243 // Edit end res position of selected group
2244 changeEndRes = true;
2246 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2248 // Edit end res position of selected group
2249 changeStartRes = true;
2251 stretchGroup.getWidth();
2256 * Build and show a pop-up menu at the right-click mouse position
2261 void showPopupMenu(MouseEvent evt, MousePos pos)
2263 final int column = pos.column;
2264 final int seq = pos.seqIndex;
2265 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2266 if (sequence != null)
2268 PopupMenu pop = new PopupMenu(ap, sequence, column);
2269 pop.show(this, evt.getX(), evt.getY());
2274 * Update the display after mouse up on a selection or group
2277 * mouse released event details
2279 * true if this event is happening after a mouse drag (rather than a
2282 protected void doMouseReleasedDefineMode(MouseEvent evt,
2285 if (stretchGroup == null)
2290 stretchGroup.removePropertyChangeListener(seqCanvas);
2292 // always do this - annotation has own state
2293 // but defer colourscheme update until hidden sequences are passed in
2294 boolean vischange = stretchGroup.recalcConservation(true);
2295 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2297 if (stretchGroup.cs != null)
2301 stretchGroup.cs.alignmentChanged(stretchGroup,
2302 av.getHiddenRepSequences());
2305 ResidueShaderI groupColourScheme = stretchGroup
2306 .getGroupColourScheme();
2307 String name = stretchGroup.getName();
2308 if (stretchGroup.cs.conservationApplied())
2310 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2312 if (stretchGroup.cs.getThreshold() > 0)
2314 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2317 PaintRefresher.Refresh(this, av.getSequenceSetId());
2318 // TODO: structure colours only need updating if stretchGroup used to or now
2319 // does contain sequences with structure views
2320 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2321 updateOverviewAndStructs = false;
2322 changeEndRes = false;
2323 changeStartRes = false;
2324 stretchGroup = null;
2329 * Resizes the borders of a selection group depending on the direction of
2334 protected void dragStretchGroup(MouseEvent evt)
2336 if (stretchGroup == null)
2341 MousePos pos = findMousePosition(evt);
2342 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2347 int res = pos.column;
2348 int y = pos.seqIndex;
2350 if (wrappedBlock != startWrapBlock)
2355 res = Math.min(res, av.getAlignment().getWidth()-1);
2357 if (stretchGroup.getEndRes() == res)
2359 // Edit end res position of selected group
2360 changeEndRes = true;
2362 else if (stretchGroup.getStartRes() == res)
2364 // Edit start res position of selected group
2365 changeStartRes = true;
2368 if (res < av.getRanges().getStartRes())
2370 res = av.getRanges().getStartRes();
2375 if (res > (stretchGroup.getStartRes() - 1))
2377 stretchGroup.setEndRes(res);
2378 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2381 else if (changeStartRes)
2383 if (res < (stretchGroup.getEndRes() + 1))
2385 stretchGroup.setStartRes(res);
2386 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2390 int dragDirection = 0;
2396 else if (y < oldSeq)
2401 while ((y != oldSeq) && (oldSeq > -1)
2402 && (y < av.getAlignment().getHeight()))
2404 // This routine ensures we don't skip any sequences, as the
2405 // selection is quite slow.
2406 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2408 oldSeq += dragDirection;
2415 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2417 if (stretchGroup.getSequences(null).contains(nextSeq))
2419 stretchGroup.deleteSequence(seq, false);
2420 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2426 stretchGroup.addSequence(seq, false);
2429 stretchGroup.addSequence(nextSeq, false);
2430 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2439 mouseDragging = true;
2441 if (scrollThread != null)
2443 scrollThread.setMousePosition(evt.getPoint());
2447 * construct a status message showing the range of the selection
2449 StringBuilder status = new StringBuilder(64);
2450 List<SequenceI> seqs = stretchGroup.getSequences();
2451 String name = seqs.get(0).getName();
2452 if (name.length() > 20)
2454 name = name.substring(0, 20);
2456 status.append(name).append(" - ");
2457 name = seqs.get(seqs.size() - 1).getName();
2458 if (name.length() > 20)
2460 name = name.substring(0, 20);
2462 status.append(name).append(" ");
2463 int startRes = stretchGroup.getStartRes();
2464 status.append(" cols ").append(String.valueOf(startRes + 1))
2466 int endRes = stretchGroup.getEndRes();
2467 status.append(String.valueOf(endRes + 1));
2468 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2469 .append(String.valueOf(endRes - startRes + 1)).append(")");
2470 ap.alignFrame.setStatus(status.toString());
2474 * Stops the scroll thread if it is running
2476 void stopScrolling()
2478 if (scrollThread != null)
2480 scrollThread.stopScrolling();
2481 scrollThread = null;
2483 mouseDragging = false;
2487 * Starts a thread to scroll the alignment, towards a given mouse position
2488 * outside the panel bounds
2492 void startScrolling(Point mousePos)
2494 if (scrollThread == null)
2496 scrollThread = new ScrollThread();
2499 mouseDragging = true;
2500 scrollThread.setMousePosition(mousePos);
2504 * Performs scrolling of the visible alignment left, right, up or down
2506 class ScrollThread extends Thread
2508 private Point mousePos;
2510 private volatile boolean threadRunning = true;
2515 public ScrollThread()
2517 setName("SeqPanel$ScrollThread");
2522 * Sets the position of the mouse that determines the direction of the
2527 public void setMousePosition(Point p)
2533 * Sets a flag that will cause the thread to exit
2535 public void stopScrolling()
2537 threadRunning = false;
2541 * Scrolls the alignment left or right, and/or up or down, depending on the
2542 * last notified mouse position, until the limit of the alignment is
2543 * reached, or a flag is set to stop the scroll
2548 while (threadRunning && mouseDragging)
2550 if (mousePos != null)
2552 boolean scrolled = false;
2553 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2560 // mouse is above this panel - try scroll up
2561 scrolled = ranges.scrollUp(true);
2563 else if (mousePos.y >= getHeight())
2565 // mouse is below this panel - try scroll down
2566 scrolled = ranges.scrollUp(false);
2570 * scroll left or right
2574 scrolled |= ranges.scrollRight(false);
2576 else if (mousePos.x >= getWidth())
2578 scrolled |= ranges.scrollRight(true);
2583 * we have reached the limit of the visible alignment - quit
2585 threadRunning = false;
2586 SeqPanel.this.ap.repaint();
2593 } catch (Exception ex)
2601 * modify current selection according to a received message.
2604 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2605 HiddenColumns hidden, SelectionSource source)
2607 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2608 // handles selection messages...
2609 // TODO: extend config options to allow user to control if selections may be
2610 // shared between viewports.
2611 boolean iSentTheSelection = (av == source
2612 || (source instanceof AlignViewport
2613 && ((AlignmentViewport) source).getSequenceSetId()
2614 .equals(av.getSequenceSetId())));
2616 if (iSentTheSelection)
2618 // respond to our own event by updating dependent dialogs
2619 if (ap.getCalculationDialog() != null)
2621 ap.getCalculationDialog().validateCalcTypes();
2627 // process further ?
2628 if (!av.followSelection)
2634 * Ignore the selection if there is one of our own pending.
2636 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2642 * Check for selection in a view of which this one is a dna/protein
2645 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2650 // do we want to thread this ? (contention with seqsel and colsel locks, I
2653 * only copy colsel if there is a real intersection between
2654 * sequence selection and this panel's alignment
2656 boolean repaint = false;
2657 boolean copycolsel = false;
2659 SequenceGroup sgroup = null;
2660 if (seqsel != null && seqsel.getSize() > 0)
2662 if (av.getAlignment() == null)
2664 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2665 + " ViewId=" + av.getViewId()
2666 + " 's alignment is NULL! returning immediately.");
2669 sgroup = seqsel.intersect(av.getAlignment(),
2670 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2671 if ((sgroup != null && sgroup.getSize() > 0))
2676 if (sgroup != null && sgroup.getSize() > 0)
2678 av.setSelectionGroup(sgroup);
2682 av.setSelectionGroup(null);
2684 av.isSelectionGroupChanged(true);
2689 // the current selection is unset or from a previous message
2690 // so import the new colsel.
2691 if (colsel == null || colsel.isEmpty())
2693 if (av.getColumnSelection() != null)
2695 av.getColumnSelection().clear();
2701 // TODO: shift colSel according to the intersecting sequences
2702 if (av.getColumnSelection() == null)
2704 av.setColumnSelection(new ColumnSelection(colsel));
2708 av.getColumnSelection().setElementsFrom(colsel,
2709 av.getAlignment().getHiddenColumns());
2712 av.isColSelChanged(true);
2716 if (copycolsel && av.hasHiddenColumns()
2717 && (av.getAlignment().getHiddenColumns() == null))
2719 System.err.println("Bad things");
2721 if (repaint) // always true!
2723 // probably finessing with multiple redraws here
2724 PaintRefresher.Refresh(this, av.getSequenceSetId());
2725 // ap.paintAlignment(false);
2728 // lastly, update dependent dialogs
2729 if (ap.getCalculationDialog() != null)
2731 ap.getCalculationDialog().validateCalcTypes();
2737 * If this panel is a cdna/protein translation view of the selection source,
2738 * tries to map the source selection to a local one, and returns true. Else
2745 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2746 ColumnSelection colsel, HiddenColumns hidden,
2747 SelectionSource source)
2749 if (!(source instanceof AlignViewportI))
2753 final AlignViewportI sourceAv = (AlignViewportI) source;
2754 if (sourceAv.getCodingComplement() != av
2755 && av.getCodingComplement() != sourceAv)
2761 * Map sequence selection
2763 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2764 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2765 av.isSelectionGroupChanged(true);
2768 * Map column selection
2770 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2772 ColumnSelection cs = new ColumnSelection();
2773 HiddenColumns hs = new HiddenColumns();
2774 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2775 av.setColumnSelection(cs);
2776 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2778 // lastly, update any dependent dialogs
2779 if (ap.getCalculationDialog() != null)
2781 ap.getCalculationDialog().validateCalcTypes();
2785 * repaint alignment, and also Overview or Structure
2786 * if hidden column selection has changed
2788 ap.paintAlignment(hiddenChanged, hiddenChanged);
2795 * @return null or last search results handled by this panel
2797 public SearchResultsI getLastSearchResults()
2799 return lastSearchResults;