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 List<SearchResultMatchI> matches = results.getResults();
897 int j = matches.size();
898 List<String> infos = new ArrayList<>();
899 for (int i = 0; i < j; i++)
901 SearchResultMatchI match = matches.get(i);
902 int pos = match.getStart();
903 if (pos == match.getEnd())
905 SequenceI seq = match.getSequence();
906 SequenceI ds = seq.getDatasetSequence() == null ? seq
907 : seq.getDatasetSequence();
908 MappedFeatures mf = fr2
909 .findComplementFeaturesAtResidue(ds, pos);
912 for (SequenceFeature sf : mf.features)
914 String pv = mf.findProteinVariants(sf);
915 if (pv.length() > 0 && !infos.contains(pv))
928 StringBuilder sb = new StringBuilder();
929 for (String info : infos)
937 return sb.toString();
941 public VamsasSource getVamsasSource()
943 return this.ap == null ? null : this.ap.av;
947 public void updateColours(SequenceI seq, int index)
949 System.out.println("update the seqPanel colours");
954 * Action on mouse movement is to update the status bar to show the current
955 * sequence position, and (if features are shown) to show any features at the
956 * position in a tooltip. Does nothing if the mouse move does not change
962 public void mouseMoved(MouseEvent evt)
966 // This is because MacOSX creates a mouseMoved
967 // If control is down, other platforms will not.
971 final MousePos mousePos = findMousePosition(evt);
972 if (mousePos.equals(lastMousePosition))
975 * just a pixel move without change of 'cell'
979 lastMousePosition = mousePos;
981 if (mousePos.isOverAnnotation())
983 mouseMovedOverAnnotation(mousePos);
986 final int seq = mousePos.seqIndex;
988 final int column = mousePos.column;
989 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
991 lastMousePosition = null;
992 setToolTipText(null);
994 ap.alignFrame.setStatus("");
998 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1000 if (column >= sequence.getLength())
1006 * set status bar message, returning residue position in sequence
1008 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1009 final int pos = setStatusMessage(sequence, column, seq);
1010 if (ssm != null && !isGapped)
1012 mouseOverSequence(sequence, column, pos);
1015 tooltipText.setLength(6); // Cuts the buffer back to <html>
1017 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1020 for (int g = 0; g < groups.length; g++)
1022 if (groups[g].getStartRes() <= column
1023 && groups[g].getEndRes() >= column)
1025 if (!groups[g].getName().startsWith("JTreeGroup")
1026 && !groups[g].getName().startsWith("JGroup"))
1028 tooltipText.append(groups[g].getName());
1031 if (groups[g].getDescription() != null)
1033 tooltipText.append(": " + groups[g].getDescription());
1040 * add any features at the position to the tooltip; if over a gap, only
1041 * add features that straddle the gap (pos may be the residue before or
1044 int unshownFeatures = 0;
1045 if (av.isShowSequenceFeatures())
1047 List<SequenceFeature> features = ap.getFeatureRenderer()
1048 .findFeaturesAtColumn(sequence, column + 1);
1049 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1050 features, this.ap.getSeqPanel().seqCanvas.fr,
1051 MAX_TOOLTIP_LENGTH);
1054 * add features in CDS/protein complement at the corresponding
1055 * position if configured to do so
1057 if (av.isShowComplementFeatures())
1059 if (!Comparison.isGap(sequence.getCharAt(column)))
1061 AlignViewportI complement = ap.getAlignViewport()
1062 .getCodingComplement();
1063 AlignFrame af = Desktop.getAlignFrameFor(complement);
1064 FeatureRendererModel fr2 = af.getFeatureRenderer();
1065 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1069 unshownFeatures = seqARep.appendFeatures(tooltipText,
1070 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1075 if (tooltipText.length() == 6) // "<html>"
1077 setToolTipText(null);
1082 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1084 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1085 tooltipText.append("...");
1087 if (unshownFeatures > 0)
1089 tooltipText.append("<br/>").append("... ").append("<i>")
1090 .append(MessageManager.formatMessage(
1091 "label.features_not_shown", unshownFeatures))
1094 String textString = tooltipText.toString();
1095 if (lastTooltip == null || !lastTooltip.equals(textString))
1097 String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1099 setToolTipText(formattedTooltipText);
1100 lastTooltip = textString;
1106 * When the view is in wrapped mode, and the mouse is over an annotation row,
1107 * shows the corresponding tooltip and status message (if any)
1112 protected void mouseMovedOverAnnotation(MousePos pos)
1114 final int column = pos.column;
1115 final int rowIndex = pos.annotationIndex;
1117 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1122 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1124 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1126 setToolTipText(tooltip);
1127 lastTooltip = tooltip;
1129 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1131 ap.alignFrame.setStatus(msg);
1134 private Point lastp = null;
1139 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1142 public Point getToolTipLocation(MouseEvent event)
1144 if (tooltipText == null || tooltipText.length() <= 6)
1150 int x = event.getX();
1152 // switch sides when tooltip is too close to edge
1153 int wdth = (w - x < 200) ? -(w / 2) : 5;
1155 if (!event.isShiftDown() || p == null)
1157 p = new Point(event.getX() + wdth, event.getY() - 20);
1161 * TODO: try to set position so region is not obscured by tooltip
1169 * set when the current UI interaction has resulted in a change that requires
1170 * shading in overviews and structures to be recalculated. this could be
1171 * changed to a something more expressive that indicates what actually has
1172 * changed, so selective redraws can be applied (ie. only structures, only
1175 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1178 * set if av.getSelectionGroup() refers to a group that is defined on the
1179 * alignment view, rather than a transient selection
1181 // private boolean editingDefinedGroup = false; // TODO: refactor to
1182 // avcontroller or viewModel
1185 * Sets the status message in alignment panel, showing the sequence number
1186 * (index) and id, and residue and residue position if not at a gap, for the
1187 * given sequence and column position. Returns the residue position returned
1188 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1189 * if at a gapped position.
1192 * aligned sequence object
1196 * index of sequence in alignment
1197 * @return sequence position of residue at column, or adjacent residue if at a
1200 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1202 char sequenceChar = sequence.getCharAt(column);
1203 int pos = sequence.findPosition(column);
1204 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1210 * Builds the status message for the current cursor location and writes it to
1211 * the status bar, for example
1214 * Sequence 3 ID: FER1_SOLLC
1215 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1216 * Sequence 5 ID: FER1_PEA Residue: B (3)
1217 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1222 * sequence position in the alignment (1..)
1223 * @param sequenceChar
1224 * the character under the cursor
1226 * the sequence residue position (if not over a gap)
1228 protected void setStatusMessage(String seqName, int seqIndex,
1229 char sequenceChar, int residuePos)
1231 StringBuilder text = new StringBuilder(32);
1234 * Sequence number (if known), and sequence name.
1236 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1237 text.append("Sequence").append(seqno).append(" ID: ")
1240 String residue = null;
1243 * Try to translate the display character to residue name (null for gap).
1245 boolean isGapped = Comparison.isGap(sequenceChar);
1249 boolean nucleotide = av.getAlignment().isNucleotide();
1250 String displayChar = String.valueOf(sequenceChar);
1253 residue = ResidueProperties.nucleotideName.get(displayChar);
1257 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1258 : ("*".equals(displayChar) ? "STOP"
1259 : ResidueProperties.aa2Triplet.get(displayChar));
1261 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1262 .append(": ").append(residue == null ? displayChar : residue);
1264 text.append(" (").append(Integer.toString(residuePos)).append(")");
1266 ap.alignFrame.setStatus(text.toString());
1270 * Set the status bar message to highlight the first matched position in
1275 private void setStatusMessage(SearchResultsI results)
1277 AlignmentI al = this.av.getAlignment();
1278 int sequenceIndex = al.findIndex(results);
1279 if (sequenceIndex == -1)
1283 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1284 SequenceI ds = alignedSeq.getDatasetSequence();
1285 for (SearchResultMatchI m : results.getResults())
1287 SequenceI seq = m.getSequence();
1288 if (seq.getDatasetSequence() != null)
1290 seq = seq.getDatasetSequence();
1295 int start = m.getStart();
1296 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1297 seq.getCharAt(start - 1), start);
1307 public void mouseDragged(MouseEvent evt)
1309 MousePos pos = findMousePosition(evt);
1310 if (pos.isOverAnnotation() || pos.column == -1)
1315 if (mouseWheelPressed)
1317 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1318 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1320 int oldWidth = av.getCharWidth();
1322 // Which is bigger, left-right or up-down?
1323 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1324 .abs(evt.getX() - lastMousePress.getX()))
1327 * on drag up or down, decrement or increment font size
1329 int fontSize = av.font.getSize();
1330 boolean fontChanged = false;
1332 if (evt.getY() < lastMousePress.getY())
1337 else if (evt.getY() > lastMousePress.getY())
1350 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1352 av.setFont(newFont, true);
1353 av.setCharWidth(oldWidth);
1357 ap.av.getCodingComplement().setFont(newFont, true);
1358 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1359 .getSplitViewContainer();
1360 splitFrame.adjustLayout();
1361 splitFrame.repaint();
1368 * on drag left or right, decrement or increment character width
1371 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1373 newWidth = av.getCharWidth() - 1;
1374 av.setCharWidth(newWidth);
1376 else if (evt.getX() > lastMousePress.getX())
1378 newWidth = av.getCharWidth() + 1;
1379 av.setCharWidth(newWidth);
1383 ap.paintAlignment(false, false);
1387 * need to ensure newWidth is set on cdna, regardless of which
1388 * panel the mouse drag happened in; protein will compute its
1389 * character width as 1:1 or 3:1
1391 av.getCodingComplement().setCharWidth(newWidth);
1392 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1393 .getSplitViewContainer();
1394 splitFrame.adjustLayout();
1395 splitFrame.repaint();
1400 FontMetrics fm = getFontMetrics(av.getFont());
1401 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1403 lastMousePress = evt.getPoint();
1410 dragStretchGroup(evt);
1414 int res = pos.column;
1421 if ((editLastRes == -1) || (editLastRes == res))
1426 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1428 // dragLeft, delete gap
1429 editSequence(false, false, res);
1433 editSequence(true, false, res);
1436 mouseDragging = true;
1437 if (scrollThread != null)
1439 scrollThread.setMousePosition(evt.getPoint());
1444 * Edits the sequence to insert or delete one or more gaps, in response to a
1445 * mouse drag or cursor mode command. The number of inserts/deletes may be
1446 * specified with the cursor command, or else depends on the mouse event
1447 * (normally one column, but potentially more for a fast mouse drag).
1449 * Delete gaps is limited to the number of gaps left of the cursor position
1450 * (mouse drag), or at or right of the cursor position (cursor mode).
1452 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1453 * the current selection group.
1455 * In locked editing mode (with a selection group present), inserts/deletions
1456 * within the selection group are limited to its boundaries (and edits outside
1457 * the group stop at its border).
1460 * true to insert gaps, false to delete gaps
1462 * (unused parameter)
1464 * the column at which to perform the action; the number of columns
1465 * affected depends on <code>this.editLastRes</code> (cursor column
1468 synchronized void editSequence(boolean insertGap, boolean editSeq,
1472 int fixedRight = -1;
1473 boolean fixedColumns = false;
1474 SequenceGroup sg = av.getSelectionGroup();
1476 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1478 // No group, but the sequence may represent a group
1479 if (!groupEditing && av.hasHiddenRows())
1481 if (av.isHiddenRepSequence(seq))
1483 sg = av.getRepresentedSequences(seq);
1484 groupEditing = true;
1488 StringBuilder message = new StringBuilder(64); // for status bar
1491 * make a name for the edit action, for
1492 * status bar message and Undo/Redo menu
1494 String label = null;
1497 message.append("Edit group:");
1498 label = MessageManager.getString("action.edit_group");
1502 message.append("Edit sequence: " + seq.getName());
1503 label = seq.getName();
1504 if (label.length() > 10)
1506 label = label.substring(0, 10);
1508 label = MessageManager.formatMessage("label.edit_params",
1514 * initialise the edit command if there is not
1515 * already one being extended
1517 if (editCommand == null)
1519 editCommand = new EditCommand(label);
1524 message.append(" insert ");
1528 message.append(" delete ");
1531 message.append(Math.abs(startres - editLastRes) + " gaps.");
1532 ap.alignFrame.setStatus(message.toString());
1535 * is there a selection group containing the sequence being edited?
1536 * if so the boundary of the group is the limit of the edit
1537 * (but the edit may be inside or outside the selection group)
1539 boolean inSelectionGroup = sg != null
1540 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1541 if (groupEditing || inSelectionGroup)
1543 fixedColumns = true;
1545 // sg might be null as the user may only see 1 sequence,
1546 // but the sequence represents a group
1549 if (!av.isHiddenRepSequence(seq))
1554 sg = av.getRepresentedSequences(seq);
1557 fixedLeft = sg.getStartRes();
1558 fixedRight = sg.getEndRes();
1560 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1561 || (startres >= fixedLeft && editLastRes < fixedLeft)
1562 || (startres > fixedRight && editLastRes <= fixedRight)
1563 || (startres <= fixedRight && editLastRes > fixedRight))
1569 if (fixedLeft > startres)
1571 fixedRight = fixedLeft - 1;
1574 else if (fixedRight < startres)
1576 fixedLeft = fixedRight;
1581 if (av.hasHiddenColumns())
1583 fixedColumns = true;
1584 int y1 = av.getAlignment().getHiddenColumns()
1585 .getNextHiddenBoundary(true, startres);
1586 int y2 = av.getAlignment().getHiddenColumns()
1587 .getNextHiddenBoundary(false, startres);
1589 if ((insertGap && startres > y1 && editLastRes < y1)
1590 || (!insertGap && startres < y2 && editLastRes > y2))
1596 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1597 // Selection spans a hidden region
1598 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1606 fixedRight = y2 - 1;
1611 boolean success = doEditSequence(insertGap, editSeq, startres,
1612 fixedRight, fixedColumns, sg);
1615 * report what actually happened (might be less than
1616 * what was requested), by inspecting the edit commands added
1618 String msg = getEditStatusMessage(editCommand);
1619 ap.alignFrame.setStatus(msg == null ? " " : msg);
1625 editLastRes = startres;
1626 seqCanvas.repaint();
1630 * A helper method that performs the requested editing to insert or delete
1631 * gaps (if possible). Answers true if the edit was successful, false if could
1632 * only be performed in part or not at all. Failure may occur in 'locked edit'
1633 * mode, when an insertion requires a matching gapped position (or column) to
1634 * delete, and deletion requires an adjacent gapped position (or column) to
1638 * true if inserting gap(s), false if deleting
1640 * (unused parameter, currently always false)
1642 * the column at which to perform the edit
1644 * fixed right boundary column of a locked edit (within or to the
1645 * left of a selection group)
1646 * @param fixedColumns
1647 * true if this is a locked edit
1649 * the sequence group (if group edit is being performed)
1652 protected boolean doEditSequence(final boolean insertGap,
1653 final boolean editSeq, final int startres, int fixedRight,
1654 final boolean fixedColumns, final SequenceGroup sg)
1656 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1657 SequenceI[] seqs = new SequenceI[] { seq };
1661 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1662 int g, groupSize = vseqs.size();
1663 SequenceI[] groupSeqs = new SequenceI[groupSize];
1664 for (g = 0; g < groupSeqs.length; g++)
1666 groupSeqs[g] = vseqs.get(g);
1672 // If the user has selected the whole sequence, and is dragging to
1673 // the right, we can still extend the alignment and selectionGroup
1674 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1675 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1678 av.getAlignment().getWidth() + startres - editLastRes);
1679 fixedRight = sg.getEndRes();
1682 // Is it valid with fixed columns??
1683 // Find the next gap before the end
1684 // of the visible region boundary
1685 boolean blank = false;
1686 for (; fixedRight > editLastRes; fixedRight--)
1690 for (g = 0; g < groupSize; g++)
1692 for (int j = 0; j < startres - editLastRes; j++)
1695 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1710 if (sg.getSize() == av.getAlignment().getHeight())
1712 if ((av.hasHiddenColumns()
1713 && startres < av.getAlignment().getHiddenColumns()
1714 .getNextHiddenBoundary(false, startres)))
1719 int alWidth = av.getAlignment().getWidth();
1720 if (av.hasHiddenRows())
1722 int hwidth = av.getAlignment().getHiddenSequences()
1724 if (hwidth > alWidth)
1729 // We can still insert gaps if the selectionGroup
1730 // contains all the sequences
1731 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1732 fixedRight = alWidth + startres - editLastRes;
1742 else if (!insertGap)
1744 // / Are we able to delete?
1745 // ie are all columns blank?
1747 for (g = 0; g < groupSize; g++)
1749 for (int j = startres; j < editLastRes; j++)
1751 if (groupSeqs[g].getLength() <= j)
1756 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1758 // Not a gap, block edit not valid
1767 // dragging to the right
1768 if (fixedColumns && fixedRight != -1)
1770 for (int j = editLastRes; j < startres; j++)
1772 insertGap(j, groupSeqs, fixedRight);
1777 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1778 startres - editLastRes, false);
1783 // dragging to the left
1784 if (fixedColumns && fixedRight != -1)
1786 for (int j = editLastRes; j > startres; j--)
1788 deleteChar(startres, groupSeqs, fixedRight);
1793 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1794 editLastRes - startres, false);
1801 * editing a single sequence
1805 // dragging to the right
1806 if (fixedColumns && fixedRight != -1)
1808 for (int j = editLastRes; j < startres; j++)
1810 if (!insertGap(j, seqs, fixedRight))
1813 * e.g. cursor mode command specified
1814 * more inserts than are possible
1822 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1823 startres - editLastRes, false);
1830 // dragging to the left
1831 if (fixedColumns && fixedRight != -1)
1833 for (int j = editLastRes; j > startres; j--)
1835 if (!Comparison.isGap(seq.getCharAt(startres)))
1839 deleteChar(startres, seqs, fixedRight);
1844 // could be a keyboard edit trying to delete none gaps
1846 for (int m = startres; m < editLastRes; m++)
1848 if (!Comparison.isGap(seq.getCharAt(m)))
1856 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1861 {// insertGap==false AND editSeq==TRUE;
1862 if (fixedColumns && fixedRight != -1)
1864 for (int j = editLastRes; j < startres; j++)
1866 insertGap(j, seqs, fixedRight);
1871 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1872 startres - editLastRes, false);
1882 * Constructs an informative status bar message while dragging to insert or
1883 * delete gaps. Answers null if inserts and deletes cancel out.
1885 * @param editCommand
1886 * a command containing the list of individual edits
1889 protected static String getEditStatusMessage(EditCommand editCommand)
1891 if (editCommand == null)
1897 * add any inserts, and subtract any deletes,
1898 * not counting those auto-inserted when doing a 'locked edit'
1899 * (so only counting edits 'under the cursor')
1902 for (Edit cmd : editCommand.getEdits())
1904 if (!cmd.isSystemGenerated())
1906 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1914 * inserts and deletes cancel out
1919 String msgKey = count > 1 ? "label.insert_gaps"
1920 : (count == 1 ? "label.insert_gap"
1921 : (count == -1 ? "label.delete_gap"
1922 : "label.delete_gaps"));
1923 count = Math.abs(count);
1925 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1929 * Inserts one gap at column j, deleting the right-most gapped column up to
1930 * (and including) fixedColumn. Returns true if the edit is successful, false
1931 * if no blank column is available to allow the insertion to be balanced by a
1936 * @param fixedColumn
1939 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1941 int blankColumn = fixedColumn;
1942 for (int s = 0; s < seq.length; s++)
1944 // Find the next gap before the end of the visible region boundary
1945 // If lastCol > j, theres a boundary after the gap insertion
1947 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1949 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1951 // Theres a space, so break and insert the gap
1956 if (blankColumn <= j)
1958 blankColumn = fixedColumn;
1964 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1966 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1972 * Helper method to add and perform one edit action
1978 * @param systemGenerated
1979 * true if the edit is a 'balancing' delete (or insert) to match a
1980 * user's insert (or delete) in a locked editing region
1982 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1983 int count, boolean systemGenerated)
1986 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1987 av.getAlignment().getGapCharacter());
1988 edit.setSystemGenerated(systemGenerated);
1990 editCommand.appendEdit(edit, av.getAlignment(), true, null);
1994 * Deletes the character at column j, and inserts a gap at fixedColumn, in
1995 * each of the given sequences. The caller should ensure that all sequences
1996 * are gapped in column j.
2000 * @param fixedColumn
2002 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2004 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2006 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2010 * On reentering the panel, stops any scrolling that was started on dragging
2016 public void mouseEntered(MouseEvent e)
2026 * On leaving the panel, if the mouse is being dragged, starts a thread to
2027 * scroll it until the mouse is released (in unwrapped mode only)
2032 public void mouseExited(MouseEvent e)
2034 lastMousePosition = null;
2035 ap.alignFrame.setStatus(" ");
2036 if (av.getWrapAlignment())
2041 if (mouseDragging && scrollThread == null)
2043 scrollThread = new ScrollThread();
2048 * Handler for double-click on a position with one or more sequence features.
2049 * Opens the Amend Features dialog to allow feature details to be amended, or
2050 * the feature deleted.
2053 public void mouseClicked(MouseEvent evt)
2055 SequenceGroup sg = null;
2056 MousePos pos = findMousePosition(evt);
2057 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2062 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2064 sg = av.getSelectionGroup();
2065 if (sg != null && sg.getSize() == 1
2066 && sg.getEndRes() - sg.getStartRes() < 2)
2068 av.setSelectionGroup(null);
2071 int column = pos.column;
2074 * find features at the position (if not gapped), or straddling
2075 * the position (if at a gap)
2077 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2078 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2079 .findFeaturesAtColumn(sequence, column + 1);
2081 if (!features.isEmpty())
2084 * highlight the first feature at the position on the alignment
2086 SearchResultsI highlight = new SearchResults();
2087 highlight.addResult(sequence, features.get(0).getBegin(), features
2089 seqCanvas.highlightSearchResults(highlight, false);
2092 * open the Amend Features dialog; clear highlighting afterwards,
2093 * whether changes were made or not
2095 List<SequenceI> seqs = Collections.singletonList(sequence);
2096 seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2098 av.setSearchResults(null); // clear highlighting
2099 seqCanvas.repaint(); // draw new/amended features
2105 public void mouseWheelMoved(MouseWheelEvent e)
2108 double wheelRotation = e.getPreciseWheelRotation();
2109 if (wheelRotation > 0)
2111 if (e.isShiftDown())
2113 av.getRanges().scrollRight(true);
2118 av.getRanges().scrollUp(false);
2121 else if (wheelRotation < 0)
2123 if (e.isShiftDown())
2125 av.getRanges().scrollRight(false);
2129 av.getRanges().scrollUp(true);
2134 * update status bar and tooltip for new position
2135 * (need to synthesize a mouse movement to refresh tooltip)
2138 ToolTipManager.sharedInstance().mouseMoved(e);
2147 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2149 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2154 final int res = pos.column;
2155 final int seq = pos.seqIndex;
2157 updateOverviewAndStructs = false;
2159 startWrapBlock = wrappedBlock;
2161 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2163 if ((sequence == null) || (res > sequence.getLength()))
2168 stretchGroup = av.getSelectionGroup();
2170 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2172 stretchGroup = av.getAlignment().findGroup(sequence, res);
2173 if (stretchGroup != null)
2175 // only update the current selection if the popup menu has a group to
2177 av.setSelectionGroup(stretchGroup);
2181 if (evt.isPopupTrigger()) // Mac: mousePressed
2183 showPopupMenu(evt, pos);
2188 * defer right-mouse click handling to mouseReleased on Windows
2189 * (where isPopupTrigger() will answer true)
2190 * NB isRightMouseButton is also true for Cmd-click on Mac
2192 if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2199 seqCanvas.cursorX = res;
2200 seqCanvas.cursorY = seq;
2201 seqCanvas.repaint();
2205 if (stretchGroup == null)
2207 createStretchGroup(res, sequence);
2210 if (stretchGroup != null)
2212 stretchGroup.addPropertyChangeListener(seqCanvas);
2215 seqCanvas.repaint();
2218 private void createStretchGroup(int res, SequenceI sequence)
2220 // Only if left mouse button do we want to change group sizes
2221 // define a new group here
2222 SequenceGroup sg = new SequenceGroup();
2223 sg.setStartRes(res);
2225 sg.addSequence(sequence, false);
2226 av.setSelectionGroup(sg);
2229 if (av.getConservationSelected())
2231 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2235 if (av.getAbovePIDThreshold())
2237 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2240 // TODO: stretchGroup will always be not null. Is this a merge error ?
2241 // or is there a threading issue here?
2242 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2244 // Edit end res position of selected group
2245 changeEndRes = true;
2247 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2249 // Edit end res position of selected group
2250 changeStartRes = true;
2252 stretchGroup.getWidth();
2257 * Build and show a pop-up menu at the right-click mouse position
2262 void showPopupMenu(MouseEvent evt, MousePos pos)
2264 final int column = pos.column;
2265 final int seq = pos.seqIndex;
2266 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2267 if (sequence != null)
2269 PopupMenu pop = new PopupMenu(ap, sequence, column);
2270 pop.show(this, evt.getX(), evt.getY());
2275 * Update the display after mouse up on a selection or group
2278 * mouse released event details
2280 * true if this event is happening after a mouse drag (rather than a
2283 protected void doMouseReleasedDefineMode(MouseEvent evt,
2286 if (stretchGroup == null)
2291 stretchGroup.removePropertyChangeListener(seqCanvas);
2293 // always do this - annotation has own state
2294 // but defer colourscheme update until hidden sequences are passed in
2295 boolean vischange = stretchGroup.recalcConservation(true);
2296 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2298 if (stretchGroup.cs != null)
2302 stretchGroup.cs.alignmentChanged(stretchGroup,
2303 av.getHiddenRepSequences());
2306 ResidueShaderI groupColourScheme = stretchGroup
2307 .getGroupColourScheme();
2308 String name = stretchGroup.getName();
2309 if (stretchGroup.cs.conservationApplied())
2311 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2313 if (stretchGroup.cs.getThreshold() > 0)
2315 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2318 PaintRefresher.Refresh(this, av.getSequenceSetId());
2319 // TODO: structure colours only need updating if stretchGroup used to or now
2320 // does contain sequences with structure views
2321 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2322 updateOverviewAndStructs = false;
2323 changeEndRes = false;
2324 changeStartRes = false;
2325 stretchGroup = null;
2330 * Resizes the borders of a selection group depending on the direction of
2335 protected void dragStretchGroup(MouseEvent evt)
2337 if (stretchGroup == null)
2342 MousePos pos = findMousePosition(evt);
2343 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2348 int res = pos.column;
2349 int y = pos.seqIndex;
2351 if (wrappedBlock != startWrapBlock)
2356 res = Math.min(res, av.getAlignment().getWidth()-1);
2358 if (stretchGroup.getEndRes() == res)
2360 // Edit end res position of selected group
2361 changeEndRes = true;
2363 else if (stretchGroup.getStartRes() == res)
2365 // Edit start res position of selected group
2366 changeStartRes = true;
2369 if (res < av.getRanges().getStartRes())
2371 res = av.getRanges().getStartRes();
2376 if (res > (stretchGroup.getStartRes() - 1))
2378 stretchGroup.setEndRes(res);
2379 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2382 else if (changeStartRes)
2384 if (res < (stretchGroup.getEndRes() + 1))
2386 stretchGroup.setStartRes(res);
2387 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2391 int dragDirection = 0;
2397 else if (y < oldSeq)
2402 while ((y != oldSeq) && (oldSeq > -1)
2403 && (y < av.getAlignment().getHeight()))
2405 // This routine ensures we don't skip any sequences, as the
2406 // selection is quite slow.
2407 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2409 oldSeq += dragDirection;
2416 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2418 if (stretchGroup.getSequences(null).contains(nextSeq))
2420 stretchGroup.deleteSequence(seq, false);
2421 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2427 stretchGroup.addSequence(seq, false);
2430 stretchGroup.addSequence(nextSeq, false);
2431 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2440 mouseDragging = true;
2442 if (scrollThread != null)
2444 scrollThread.setMousePosition(evt.getPoint());
2448 * construct a status message showing the range of the selection
2450 StringBuilder status = new StringBuilder(64);
2451 List<SequenceI> seqs = stretchGroup.getSequences();
2452 String name = seqs.get(0).getName();
2453 if (name.length() > 20)
2455 name = name.substring(0, 20);
2457 status.append(name).append(" - ");
2458 name = seqs.get(seqs.size() - 1).getName();
2459 if (name.length() > 20)
2461 name = name.substring(0, 20);
2463 status.append(name).append(" ");
2464 int startRes = stretchGroup.getStartRes();
2465 status.append(" cols ").append(String.valueOf(startRes + 1))
2467 int endRes = stretchGroup.getEndRes();
2468 status.append(String.valueOf(endRes + 1));
2469 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2470 .append(String.valueOf(endRes - startRes + 1)).append(")");
2471 ap.alignFrame.setStatus(status.toString());
2475 * Stops the scroll thread if it is running
2477 void stopScrolling()
2479 if (scrollThread != null)
2481 scrollThread.stopScrolling();
2482 scrollThread = null;
2484 mouseDragging = false;
2488 * Starts a thread to scroll the alignment, towards a given mouse position
2489 * outside the panel bounds
2493 void startScrolling(Point mousePos)
2495 if (scrollThread == null)
2497 scrollThread = new ScrollThread();
2500 mouseDragging = true;
2501 scrollThread.setMousePosition(mousePos);
2505 * Performs scrolling of the visible alignment left, right, up or down
2507 class ScrollThread extends Thread
2509 private Point mousePos;
2511 private volatile boolean threadRunning = true;
2516 public ScrollThread()
2518 setName("SeqPanel$ScrollThread");
2523 * Sets the position of the mouse that determines the direction of the
2528 public void setMousePosition(Point p)
2534 * Sets a flag that will cause the thread to exit
2536 public void stopScrolling()
2538 threadRunning = false;
2542 * Scrolls the alignment left or right, and/or up or down, depending on the
2543 * last notified mouse position, until the limit of the alignment is
2544 * reached, or a flag is set to stop the scroll
2549 while (threadRunning && mouseDragging)
2551 if (mousePos != null)
2553 boolean scrolled = false;
2554 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2561 // mouse is above this panel - try scroll up
2562 scrolled = ranges.scrollUp(true);
2564 else if (mousePos.y >= getHeight())
2566 // mouse is below this panel - try scroll down
2567 scrolled = ranges.scrollUp(false);
2571 * scroll left or right
2575 scrolled |= ranges.scrollRight(false);
2577 else if (mousePos.x >= getWidth())
2579 scrolled |= ranges.scrollRight(true);
2584 * we have reached the limit of the visible alignment - quit
2586 threadRunning = false;
2587 SeqPanel.this.ap.repaint();
2594 } catch (Exception ex)
2602 * modify current selection according to a received message.
2605 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2606 HiddenColumns hidden, SelectionSource source)
2608 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2609 // handles selection messages...
2610 // TODO: extend config options to allow user to control if selections may be
2611 // shared between viewports.
2612 boolean iSentTheSelection = (av == source
2613 || (source instanceof AlignViewport
2614 && ((AlignmentViewport) source).getSequenceSetId()
2615 .equals(av.getSequenceSetId())));
2617 if (iSentTheSelection)
2619 // respond to our own event by updating dependent dialogs
2620 if (ap.getCalculationDialog() != null)
2622 ap.getCalculationDialog().validateCalcTypes();
2628 // process further ?
2629 if (!av.followSelection)
2635 * Ignore the selection if there is one of our own pending.
2637 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2643 * Check for selection in a view of which this one is a dna/protein
2646 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2651 // do we want to thread this ? (contention with seqsel and colsel locks, I
2654 * only copy colsel if there is a real intersection between
2655 * sequence selection and this panel's alignment
2657 boolean repaint = false;
2658 boolean copycolsel = false;
2660 SequenceGroup sgroup = null;
2661 if (seqsel != null && seqsel.getSize() > 0)
2663 if (av.getAlignment() == null)
2665 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2666 + " ViewId=" + av.getViewId()
2667 + " 's alignment is NULL! returning immediately.");
2670 sgroup = seqsel.intersect(av.getAlignment(),
2671 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2672 if ((sgroup != null && sgroup.getSize() > 0))
2677 if (sgroup != null && sgroup.getSize() > 0)
2679 av.setSelectionGroup(sgroup);
2683 av.setSelectionGroup(null);
2685 av.isSelectionGroupChanged(true);
2690 // the current selection is unset or from a previous message
2691 // so import the new colsel.
2692 if (colsel == null || colsel.isEmpty())
2694 if (av.getColumnSelection() != null)
2696 av.getColumnSelection().clear();
2702 // TODO: shift colSel according to the intersecting sequences
2703 if (av.getColumnSelection() == null)
2705 av.setColumnSelection(new ColumnSelection(colsel));
2709 av.getColumnSelection().setElementsFrom(colsel,
2710 av.getAlignment().getHiddenColumns());
2713 av.isColSelChanged(true);
2717 if (copycolsel && av.hasHiddenColumns()
2718 && (av.getAlignment().getHiddenColumns() == null))
2720 System.err.println("Bad things");
2722 if (repaint) // always true!
2724 // probably finessing with multiple redraws here
2725 PaintRefresher.Refresh(this, av.getSequenceSetId());
2726 // ap.paintAlignment(false);
2729 // lastly, update dependent dialogs
2730 if (ap.getCalculationDialog() != null)
2732 ap.getCalculationDialog().validateCalcTypes();
2738 * If this panel is a cdna/protein translation view of the selection source,
2739 * tries to map the source selection to a local one, and returns true. Else
2746 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2747 ColumnSelection colsel, HiddenColumns hidden,
2748 SelectionSource source)
2750 if (!(source instanceof AlignViewportI))
2754 final AlignViewportI sourceAv = (AlignViewportI) source;
2755 if (sourceAv.getCodingComplement() != av
2756 && av.getCodingComplement() != sourceAv)
2762 * Map sequence selection
2764 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2765 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2766 av.isSelectionGroupChanged(true);
2769 * Map column selection
2771 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2773 ColumnSelection cs = new ColumnSelection();
2774 HiddenColumns hs = new HiddenColumns();
2775 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2776 av.setColumnSelection(cs);
2777 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2779 // lastly, update any dependent dialogs
2780 if (ap.getCalculationDialog() != null)
2782 ap.getCalculationDialog().validateCalcTypes();
2786 * repaint alignment, and also Overview or Structure
2787 * if hidden column selection has changed
2789 ap.paintAlignment(hiddenChanged, hiddenChanged);
2796 * @return null or last search results handled by this panel
2798 public SearchResultsI getLastSearchResults()
2800 return lastSearchResults;