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 return results.isEmpty() ? null : getHighlightInfo(results);
869 * temporary hack: answers a message suitable to show on structure hover
870 * label. This is normally null. It is a peptide variation description if
872 * <li>results are a single residue in a protein alignment</li>
873 * <li>there is a mapping to a coding sequence (codon)</li>
874 * <li>there are one or more SNP variant features on the codon</li>
876 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
881 private String getHighlightInfo(SearchResultsI results)
884 * ideally, just find mapped CDS (as we don't care about render style here);
885 * for now, go via split frame complement's FeatureRenderer
887 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
888 if (complement == null)
892 AlignFrame af = Desktop.getAlignFrameFor(complement);
893 FeatureRendererModel fr2 = af.getFeatureRenderer();
895 int j = results.getSize();
896 List<String> infos = new ArrayList<>();
897 for (int i = 0; i < j; i++)
899 SearchResultMatchI match = results.getResults().get(i);
900 int pos = match.getStart();
901 if (pos == match.getEnd())
903 SequenceI seq = match.getSequence();
904 SequenceI ds = seq.getDatasetSequence() == null ? seq
905 : seq.getDatasetSequence();
906 MappedFeatures mf = fr2
907 .findComplementFeaturesAtResidue(ds, pos);
910 for (SequenceFeature sf : mf.features)
912 String pv = mf.findProteinVariants(sf);
913 if (pv.length() > 0 && !infos.contains(pv))
926 StringBuilder sb = new StringBuilder();
927 for (String info : infos)
935 return sb.toString();
939 public VamsasSource getVamsasSource()
941 return this.ap == null ? null : this.ap.av;
945 public void updateColours(SequenceI seq, int index)
947 System.out.println("update the seqPanel colours");
952 * Action on mouse movement is to update the status bar to show the current
953 * sequence position, and (if features are shown) to show any features at the
954 * position in a tooltip. Does nothing if the mouse move does not change
960 public void mouseMoved(MouseEvent evt)
964 // This is because MacOSX creates a mouseMoved
965 // If control is down, other platforms will not.
969 final MousePos mousePos = findMousePosition(evt);
970 if (mousePos.equals(lastMousePosition))
973 * just a pixel move without change of 'cell'
977 lastMousePosition = mousePos;
979 if (mousePos.isOverAnnotation())
981 mouseMovedOverAnnotation(mousePos);
984 final int seq = mousePos.seqIndex;
986 final int column = mousePos.column;
987 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
989 lastMousePosition = null;
990 setToolTipText(null);
992 ap.alignFrame.setStatus("");
996 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
998 if (column >= sequence.getLength())
1004 * set status bar message, returning residue position in sequence
1006 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1007 final int pos = setStatusMessage(sequence, column, seq);
1008 if (ssm != null && !isGapped)
1010 mouseOverSequence(sequence, column, pos);
1013 tooltipText.setLength(6); // Cuts the buffer back to <html>
1015 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1018 for (int g = 0; g < groups.length; g++)
1020 if (groups[g].getStartRes() <= column
1021 && groups[g].getEndRes() >= column)
1023 if (!groups[g].getName().startsWith("JTreeGroup")
1024 && !groups[g].getName().startsWith("JGroup"))
1026 tooltipText.append(groups[g].getName());
1029 if (groups[g].getDescription() != null)
1031 tooltipText.append(": " + groups[g].getDescription());
1038 * add any features at the position to the tooltip; if over a gap, only
1039 * add features that straddle the gap (pos may be the residue before or
1042 int unshownFeatures = 0;
1043 if (av.isShowSequenceFeatures())
1045 List<SequenceFeature> features = ap.getFeatureRenderer()
1046 .findFeaturesAtColumn(sequence, column + 1);
1047 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1048 features, this.ap.getSeqPanel().seqCanvas.fr,
1049 MAX_TOOLTIP_LENGTH);
1052 * add features in CDS/protein complement at the corresponding
1053 * position if configured to do so
1055 if (av.isShowComplementFeatures())
1057 if (!Comparison.isGap(sequence.getCharAt(column)))
1059 AlignViewportI complement = ap.getAlignViewport()
1060 .getCodingComplement();
1061 AlignFrame af = Desktop.getAlignFrameFor(complement);
1062 FeatureRendererModel fr2 = af.getFeatureRenderer();
1063 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1067 unshownFeatures = seqARep.appendFeatures(tooltipText,
1068 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1073 if (tooltipText.length() == 6) // "<html>"
1075 setToolTipText(null);
1080 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1082 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1083 tooltipText.append("...");
1085 if (unshownFeatures > 0)
1087 tooltipText.append("<br/>").append("... ").append("<i>")
1088 .append(MessageManager.formatMessage(
1089 "label.features_not_shown", unshownFeatures))
1092 String textString = tooltipText.toString();
1093 if (lastTooltip == null || !lastTooltip.equals(textString))
1095 String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1097 setToolTipText(formattedTooltipText);
1098 lastTooltip = textString;
1104 * When the view is in wrapped mode, and the mouse is over an annotation row,
1105 * shows the corresponding tooltip and status message (if any)
1110 protected void mouseMovedOverAnnotation(MousePos pos)
1112 final int column = pos.column;
1113 final int rowIndex = pos.annotationIndex;
1115 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1120 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1122 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1124 setToolTipText(tooltip);
1125 lastTooltip = tooltip;
1127 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1129 ap.alignFrame.setStatus(msg);
1132 private Point lastp = null;
1137 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1140 public Point getToolTipLocation(MouseEvent event)
1142 if (tooltipText == null || tooltipText.length() <= 6)
1148 int x = event.getX();
1150 // switch sides when tooltip is too close to edge
1151 int wdth = (w - x < 200) ? -(w / 2) : 5;
1153 if (!event.isShiftDown() || p == null)
1155 p = new Point(event.getX() + wdth, event.getY() - 20);
1159 * TODO: try to set position so region is not obscured by tooltip
1167 * set when the current UI interaction has resulted in a change that requires
1168 * shading in overviews and structures to be recalculated. this could be
1169 * changed to a something more expressive that indicates what actually has
1170 * changed, so selective redraws can be applied (ie. only structures, only
1173 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1176 * set if av.getSelectionGroup() refers to a group that is defined on the
1177 * alignment view, rather than a transient selection
1179 // private boolean editingDefinedGroup = false; // TODO: refactor to
1180 // avcontroller or viewModel
1183 * Sets the status message in alignment panel, showing the sequence number
1184 * (index) and id, and residue and residue position if not at a gap, for the
1185 * given sequence and column position. Returns the residue position returned
1186 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1187 * if at a gapped position.
1190 * aligned sequence object
1194 * index of sequence in alignment
1195 * @return sequence position of residue at column, or adjacent residue if at a
1198 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1200 char sequenceChar = sequence.getCharAt(column);
1201 int pos = sequence.findPosition(column);
1202 setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1208 * Builds the status message for the current cursor location and writes it to
1209 * the status bar, for example
1212 * Sequence 3 ID: FER1_SOLLC
1213 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1214 * Sequence 5 ID: FER1_PEA Residue: B (3)
1215 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1220 * sequence position in the alignment (1..)
1221 * @param sequenceChar
1222 * the character under the cursor
1224 * the sequence residue position (if not over a gap)
1226 protected void setStatusMessage(SequenceI sequence, int seqIndex,
1227 char sequenceChar, int residuePos)
1229 StringBuilder text = new StringBuilder(32);
1232 * Sequence number (if known), and sequence name.
1234 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1235 text.append("Sequence").append(seqno).append(" ID: ")
1236 .append(sequence.getName());
1238 String residue = null;
1241 * Try to translate the display character to residue name (null for gap).
1243 boolean isGapped = Comparison.isGap(sequenceChar);
1247 boolean nucleotide = av.getAlignment().isNucleotide();
1248 String displayChar = String.valueOf(sequenceChar);
1251 residue = ResidueProperties.nucleotideName.get(displayChar);
1255 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1256 : ("*".equals(displayChar) ? "STOP"
1257 : ResidueProperties.aa2Triplet.get(displayChar));
1259 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1260 .append(": ").append(residue == null ? displayChar : residue);
1262 text.append(" (").append(Integer.toString(residuePos)).append(")");
1264 ap.alignFrame.setStatus(text.toString());
1268 * Set the status bar message to highlight the first matched position in
1273 private void setStatusMessage(SearchResultsI results)
1275 AlignmentI al = this.av.getAlignment();
1276 int sequenceIndex = al.findIndex(results);
1277 if (sequenceIndex == -1)
1281 SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1282 for (SearchResultMatchI m : results.getResults())
1284 SequenceI seq = m.getSequence();
1285 if (seq.getDatasetSequence() != null)
1287 seq = seq.getDatasetSequence();
1292 int start = m.getStart();
1293 setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1304 public void mouseDragged(MouseEvent evt)
1306 MousePos pos = findMousePosition(evt);
1307 if (pos.isOverAnnotation() || pos.column == -1)
1312 if (mouseWheelPressed)
1314 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1315 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1317 int oldWidth = av.getCharWidth();
1319 // Which is bigger, left-right or up-down?
1320 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1321 .abs(evt.getX() - lastMousePress.getX()))
1324 * on drag up or down, decrement or increment font size
1326 int fontSize = av.font.getSize();
1327 boolean fontChanged = false;
1329 if (evt.getY() < lastMousePress.getY())
1334 else if (evt.getY() > lastMousePress.getY())
1347 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1349 av.setFont(newFont, true);
1350 av.setCharWidth(oldWidth);
1354 ap.av.getCodingComplement().setFont(newFont, true);
1355 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1356 .getSplitViewContainer();
1357 splitFrame.adjustLayout();
1358 splitFrame.repaint();
1365 * on drag left or right, decrement or increment character width
1368 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1370 newWidth = av.getCharWidth() - 1;
1371 av.setCharWidth(newWidth);
1373 else if (evt.getX() > lastMousePress.getX())
1375 newWidth = av.getCharWidth() + 1;
1376 av.setCharWidth(newWidth);
1380 ap.paintAlignment(false, false);
1384 * need to ensure newWidth is set on cdna, regardless of which
1385 * panel the mouse drag happened in; protein will compute its
1386 * character width as 1:1 or 3:1
1388 av.getCodingComplement().setCharWidth(newWidth);
1389 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1390 .getSplitViewContainer();
1391 splitFrame.adjustLayout();
1392 splitFrame.repaint();
1397 FontMetrics fm = getFontMetrics(av.getFont());
1398 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1400 lastMousePress = evt.getPoint();
1407 dragStretchGroup(evt);
1411 int res = pos.column;
1418 if ((editLastRes == -1) || (editLastRes == res))
1423 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1425 // dragLeft, delete gap
1426 editSequence(false, false, res);
1430 editSequence(true, false, res);
1433 mouseDragging = true;
1434 if (scrollThread != null)
1436 scrollThread.setMousePosition(evt.getPoint());
1441 * Edits the sequence to insert or delete one or more gaps, in response to a
1442 * mouse drag or cursor mode command. The number of inserts/deletes may be
1443 * specified with the cursor command, or else depends on the mouse event
1444 * (normally one column, but potentially more for a fast mouse drag).
1446 * Delete gaps is limited to the number of gaps left of the cursor position
1447 * (mouse drag), or at or right of the cursor position (cursor mode).
1449 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1450 * the current selection group.
1452 * In locked editing mode (with a selection group present), inserts/deletions
1453 * within the selection group are limited to its boundaries (and edits outside
1454 * the group stop at its border).
1457 * true to insert gaps, false to delete gaps
1459 * (unused parameter)
1461 * the column at which to perform the action; the number of columns
1462 * affected depends on <code>this.editLastRes</code> (cursor column
1465 synchronized void editSequence(boolean insertGap, boolean editSeq,
1469 int fixedRight = -1;
1470 boolean fixedColumns = false;
1471 SequenceGroup sg = av.getSelectionGroup();
1473 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1475 // No group, but the sequence may represent a group
1476 if (!groupEditing && av.hasHiddenRows())
1478 if (av.isHiddenRepSequence(seq))
1480 sg = av.getRepresentedSequences(seq);
1481 groupEditing = true;
1485 StringBuilder message = new StringBuilder(64); // for status bar
1488 * make a name for the edit action, for
1489 * status bar message and Undo/Redo menu
1491 String label = null;
1494 message.append("Edit group:");
1495 label = MessageManager.getString("action.edit_group");
1499 message.append("Edit sequence: " + seq.getName());
1500 label = seq.getName();
1501 if (label.length() > 10)
1503 label = label.substring(0, 10);
1505 label = MessageManager.formatMessage("label.edit_params",
1511 * initialise the edit command if there is not
1512 * already one being extended
1514 if (editCommand == null)
1516 editCommand = new EditCommand(label);
1521 message.append(" insert ");
1525 message.append(" delete ");
1528 message.append(Math.abs(startres - editLastRes) + " gaps.");
1529 ap.alignFrame.setStatus(message.toString());
1532 * is there a selection group containing the sequence being edited?
1533 * if so the boundary of the group is the limit of the edit
1534 * (but the edit may be inside or outside the selection group)
1536 boolean inSelectionGroup = sg != null
1537 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1538 if (groupEditing || inSelectionGroup)
1540 fixedColumns = true;
1542 // sg might be null as the user may only see 1 sequence,
1543 // but the sequence represents a group
1546 if (!av.isHiddenRepSequence(seq))
1551 sg = av.getRepresentedSequences(seq);
1554 fixedLeft = sg.getStartRes();
1555 fixedRight = sg.getEndRes();
1557 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1558 || (startres >= fixedLeft && editLastRes < fixedLeft)
1559 || (startres > fixedRight && editLastRes <= fixedRight)
1560 || (startres <= fixedRight && editLastRes > fixedRight))
1566 if (fixedLeft > startres)
1568 fixedRight = fixedLeft - 1;
1571 else if (fixedRight < startres)
1573 fixedLeft = fixedRight;
1578 if (av.hasHiddenColumns())
1580 fixedColumns = true;
1581 int y1 = av.getAlignment().getHiddenColumns()
1582 .getNextHiddenBoundary(true, startres);
1583 int y2 = av.getAlignment().getHiddenColumns()
1584 .getNextHiddenBoundary(false, startres);
1586 if ((insertGap && startres > y1 && editLastRes < y1)
1587 || (!insertGap && startres < y2 && editLastRes > y2))
1593 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1594 // Selection spans a hidden region
1595 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1603 fixedRight = y2 - 1;
1608 boolean success = doEditSequence(insertGap, editSeq, startres,
1609 fixedRight, fixedColumns, sg);
1612 * report what actually happened (might be less than
1613 * what was requested), by inspecting the edit commands added
1615 String msg = getEditStatusMessage(editCommand);
1616 ap.alignFrame.setStatus(msg == null ? " " : msg);
1622 editLastRes = startres;
1623 seqCanvas.repaint();
1627 * A helper method that performs the requested editing to insert or delete
1628 * gaps (if possible). Answers true if the edit was successful, false if could
1629 * only be performed in part or not at all. Failure may occur in 'locked edit'
1630 * mode, when an insertion requires a matching gapped position (or column) to
1631 * delete, and deletion requires an adjacent gapped position (or column) to
1635 * true if inserting gap(s), false if deleting
1637 * (unused parameter, currently always false)
1639 * the column at which to perform the edit
1641 * fixed right boundary column of a locked edit (within or to the
1642 * left of a selection group)
1643 * @param fixedColumns
1644 * true if this is a locked edit
1646 * the sequence group (if group edit is being performed)
1649 protected boolean doEditSequence(final boolean insertGap,
1650 final boolean editSeq, final int startres, int fixedRight,
1651 final boolean fixedColumns, final SequenceGroup sg)
1653 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1654 SequenceI[] seqs = new SequenceI[] { seq };
1658 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1659 int g, groupSize = vseqs.size();
1660 SequenceI[] groupSeqs = new SequenceI[groupSize];
1661 for (g = 0; g < groupSeqs.length; g++)
1663 groupSeqs[g] = vseqs.get(g);
1669 // If the user has selected the whole sequence, and is dragging to
1670 // the right, we can still extend the alignment and selectionGroup
1671 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1672 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1675 av.getAlignment().getWidth() + startres - editLastRes);
1676 fixedRight = sg.getEndRes();
1679 // Is it valid with fixed columns??
1680 // Find the next gap before the end
1681 // of the visible region boundary
1682 boolean blank = false;
1683 for (; fixedRight > editLastRes; fixedRight--)
1687 for (g = 0; g < groupSize; g++)
1689 for (int j = 0; j < startres - editLastRes; j++)
1692 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1707 if (sg.getSize() == av.getAlignment().getHeight())
1709 if ((av.hasHiddenColumns()
1710 && startres < av.getAlignment().getHiddenColumns()
1711 .getNextHiddenBoundary(false, startres)))
1716 int alWidth = av.getAlignment().getWidth();
1717 if (av.hasHiddenRows())
1719 int hwidth = av.getAlignment().getHiddenSequences()
1721 if (hwidth > alWidth)
1726 // We can still insert gaps if the selectionGroup
1727 // contains all the sequences
1728 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1729 fixedRight = alWidth + startres - editLastRes;
1739 else if (!insertGap)
1741 // / Are we able to delete?
1742 // ie are all columns blank?
1744 for (g = 0; g < groupSize; g++)
1746 for (int j = startres; j < editLastRes; j++)
1748 if (groupSeqs[g].getLength() <= j)
1753 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1755 // Not a gap, block edit not valid
1764 // dragging to the right
1765 if (fixedColumns && fixedRight != -1)
1767 for (int j = editLastRes; j < startres; j++)
1769 insertGap(j, groupSeqs, fixedRight);
1774 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1775 startres - editLastRes, false);
1780 // dragging to the left
1781 if (fixedColumns && fixedRight != -1)
1783 for (int j = editLastRes; j > startres; j--)
1785 deleteChar(startres, groupSeqs, fixedRight);
1790 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1791 editLastRes - startres, false);
1798 * editing a single sequence
1802 // dragging to the right
1803 if (fixedColumns && fixedRight != -1)
1805 for (int j = editLastRes; j < startres; j++)
1807 if (!insertGap(j, seqs, fixedRight))
1810 * e.g. cursor mode command specified
1811 * more inserts than are possible
1819 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1820 startres - editLastRes, false);
1827 // dragging to the left
1828 if (fixedColumns && fixedRight != -1)
1830 for (int j = editLastRes; j > startres; j--)
1832 if (!Comparison.isGap(seq.getCharAt(startres)))
1836 deleteChar(startres, seqs, fixedRight);
1841 // could be a keyboard edit trying to delete none gaps
1843 for (int m = startres; m < editLastRes; m++)
1845 if (!Comparison.isGap(seq.getCharAt(m)))
1853 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1858 {// insertGap==false AND editSeq==TRUE;
1859 if (fixedColumns && fixedRight != -1)
1861 for (int j = editLastRes; j < startres; j++)
1863 insertGap(j, seqs, fixedRight);
1868 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1869 startres - editLastRes, false);
1879 * Constructs an informative status bar message while dragging to insert or
1880 * delete gaps. Answers null if inserts and deletes cancel out.
1882 * @param editCommand
1883 * a command containing the list of individual edits
1886 protected static String getEditStatusMessage(EditCommand editCommand)
1888 if (editCommand == null)
1894 * add any inserts, and subtract any deletes,
1895 * not counting those auto-inserted when doing a 'locked edit'
1896 * (so only counting edits 'under the cursor')
1899 for (Edit cmd : editCommand.getEdits())
1901 if (!cmd.isSystemGenerated())
1903 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1911 * inserts and deletes cancel out
1916 String msgKey = count > 1 ? "label.insert_gaps"
1917 : (count == 1 ? "label.insert_gap"
1918 : (count == -1 ? "label.delete_gap"
1919 : "label.delete_gaps"));
1920 count = Math.abs(count);
1922 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1926 * Inserts one gap at column j, deleting the right-most gapped column up to
1927 * (and including) fixedColumn. Returns true if the edit is successful, false
1928 * if no blank column is available to allow the insertion to be balanced by a
1933 * @param fixedColumn
1936 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1938 int blankColumn = fixedColumn;
1939 for (int s = 0; s < seq.length; s++)
1941 // Find the next gap before the end of the visible region boundary
1942 // If lastCol > j, theres a boundary after the gap insertion
1944 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1946 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1948 // Theres a space, so break and insert the gap
1953 if (blankColumn <= j)
1955 blankColumn = fixedColumn;
1961 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1963 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1969 * Helper method to add and perform one edit action
1975 * @param systemGenerated
1976 * true if the edit is a 'balancing' delete (or insert) to match a
1977 * user's insert (or delete) in a locked editing region
1979 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1980 int count, boolean systemGenerated)
1983 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1984 av.getAlignment().getGapCharacter());
1985 edit.setSystemGenerated(systemGenerated);
1987 editCommand.appendEdit(edit, av.getAlignment(), true, null);
1991 * Deletes the character at column j, and inserts a gap at fixedColumn, in
1992 * each of the given sequences. The caller should ensure that all sequences
1993 * are gapped in column j.
1997 * @param fixedColumn
1999 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2001 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2003 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2007 * On reentering the panel, stops any scrolling that was started on dragging
2013 public void mouseEntered(MouseEvent e)
2023 * On leaving the panel, if the mouse is being dragged, starts a thread to
2024 * scroll it until the mouse is released (in unwrapped mode only)
2029 public void mouseExited(MouseEvent e)
2031 lastMousePosition = null;
2032 ap.alignFrame.setStatus(" ");
2033 if (av.getWrapAlignment())
2038 if (mouseDragging && scrollThread == null)
2040 scrollThread = new ScrollThread();
2045 * Handler for double-click on a position with one or more sequence features.
2046 * Opens the Amend Features dialog to allow feature details to be amended, or
2047 * the feature deleted.
2050 public void mouseClicked(MouseEvent evt)
2052 SequenceGroup sg = null;
2053 MousePos pos = findMousePosition(evt);
2054 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2059 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2061 sg = av.getSelectionGroup();
2062 if (sg != null && sg.getSize() == 1
2063 && sg.getEndRes() - sg.getStartRes() < 2)
2065 av.setSelectionGroup(null);
2068 int column = pos.column;
2071 * find features at the position (if not gapped), or straddling
2072 * the position (if at a gap)
2074 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2075 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2076 .findFeaturesAtColumn(sequence, column + 1);
2078 if (!features.isEmpty())
2081 * highlight the first feature at the position on the alignment
2083 SearchResultsI highlight = new SearchResults();
2084 highlight.addResult(sequence, features.get(0).getBegin(), features
2086 seqCanvas.highlightSearchResults(highlight, false);
2089 * open the Amend Features dialog; clear highlighting afterwards,
2090 * whether changes were made or not
2092 List<SequenceI> seqs = Collections.singletonList(sequence);
2093 seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2095 av.setSearchResults(null); // clear highlighting
2096 seqCanvas.repaint(); // draw new/amended features
2102 public void mouseWheelMoved(MouseWheelEvent e)
2105 double wheelRotation = e.getPreciseWheelRotation();
2106 if (wheelRotation > 0)
2108 if (e.isShiftDown())
2110 av.getRanges().scrollRight(true);
2115 av.getRanges().scrollUp(false);
2118 else if (wheelRotation < 0)
2120 if (e.isShiftDown())
2122 av.getRanges().scrollRight(false);
2126 av.getRanges().scrollUp(true);
2131 * update status bar and tooltip for new position
2132 * (need to synthesize a mouse movement to refresh tooltip)
2135 ToolTipManager.sharedInstance().mouseMoved(e);
2144 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2146 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2151 final int res = pos.column;
2152 final int seq = pos.seqIndex;
2154 updateOverviewAndStructs = false;
2156 startWrapBlock = wrappedBlock;
2158 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2160 if ((sequence == null) || (res > sequence.getLength()))
2165 stretchGroup = av.getSelectionGroup();
2167 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2169 stretchGroup = av.getAlignment().findGroup(sequence, res);
2170 if (stretchGroup != null)
2172 // only update the current selection if the popup menu has a group to
2174 av.setSelectionGroup(stretchGroup);
2178 if (evt.isPopupTrigger()) // Mac: mousePressed
2180 showPopupMenu(evt, pos);
2185 * defer right-mouse click handling to mouseReleased on Windows
2186 * (where isPopupTrigger() will answer true)
2187 * NB isRightMouseButton is also true for Cmd-click on Mac
2189 if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2196 seqCanvas.cursorX = res;
2197 seqCanvas.cursorY = seq;
2198 seqCanvas.repaint();
2202 if (stretchGroup == null)
2204 createStretchGroup(res, sequence);
2207 if (stretchGroup != null)
2209 stretchGroup.addPropertyChangeListener(seqCanvas);
2212 seqCanvas.repaint();
2215 private void createStretchGroup(int res, SequenceI sequence)
2217 // Only if left mouse button do we want to change group sizes
2218 // define a new group here
2219 SequenceGroup sg = new SequenceGroup();
2220 sg.setStartRes(res);
2222 sg.addSequence(sequence, false);
2223 av.setSelectionGroup(sg);
2226 if (av.getConservationSelected())
2228 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2232 if (av.getAbovePIDThreshold())
2234 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2237 // TODO: stretchGroup will always be not null. Is this a merge error ?
2238 // or is there a threading issue here?
2239 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2241 // Edit end res position of selected group
2242 changeEndRes = true;
2244 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2246 // Edit end res position of selected group
2247 changeStartRes = true;
2249 stretchGroup.getWidth();
2254 * Build and show a pop-up menu at the right-click mouse position
2259 void showPopupMenu(MouseEvent evt, MousePos pos)
2261 final int column = pos.column;
2262 final int seq = pos.seqIndex;
2263 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2264 if (sequence != null)
2266 PopupMenu pop = new PopupMenu(ap, sequence, column);
2267 pop.show(this, evt.getX(), evt.getY());
2272 * Update the display after mouse up on a selection or group
2275 * mouse released event details
2277 * true if this event is happening after a mouse drag (rather than a
2280 protected void doMouseReleasedDefineMode(MouseEvent evt,
2283 if (stretchGroup == null)
2288 stretchGroup.removePropertyChangeListener(seqCanvas);
2290 // always do this - annotation has own state
2291 // but defer colourscheme update until hidden sequences are passed in
2292 boolean vischange = stretchGroup.recalcConservation(true);
2293 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2295 if (stretchGroup.cs != null)
2299 stretchGroup.cs.alignmentChanged(stretchGroup,
2300 av.getHiddenRepSequences());
2303 ResidueShaderI groupColourScheme = stretchGroup
2304 .getGroupColourScheme();
2305 String name = stretchGroup.getName();
2306 if (stretchGroup.cs.conservationApplied())
2308 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2310 if (stretchGroup.cs.getThreshold() > 0)
2312 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2315 PaintRefresher.Refresh(this, av.getSequenceSetId());
2316 // TODO: structure colours only need updating if stretchGroup used to or now
2317 // does contain sequences with structure views
2318 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2319 updateOverviewAndStructs = false;
2320 changeEndRes = false;
2321 changeStartRes = false;
2322 stretchGroup = null;
2327 * Resizes the borders of a selection group depending on the direction of
2332 protected void dragStretchGroup(MouseEvent evt)
2334 if (stretchGroup == null)
2339 MousePos pos = findMousePosition(evt);
2340 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2345 int res = pos.column;
2346 int y = pos.seqIndex;
2348 if (wrappedBlock != startWrapBlock)
2353 res = Math.min(res, av.getAlignment().getWidth()-1);
2355 if (stretchGroup.getEndRes() == res)
2357 // Edit end res position of selected group
2358 changeEndRes = true;
2360 else if (stretchGroup.getStartRes() == res)
2362 // Edit start res position of selected group
2363 changeStartRes = true;
2366 if (res < av.getRanges().getStartRes())
2368 res = av.getRanges().getStartRes();
2373 if (res > (stretchGroup.getStartRes() - 1))
2375 stretchGroup.setEndRes(res);
2376 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2379 else if (changeStartRes)
2381 if (res < (stretchGroup.getEndRes() + 1))
2383 stretchGroup.setStartRes(res);
2384 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2388 int dragDirection = 0;
2394 else if (y < oldSeq)
2399 while ((y != oldSeq) && (oldSeq > -1)
2400 && (y < av.getAlignment().getHeight()))
2402 // This routine ensures we don't skip any sequences, as the
2403 // selection is quite slow.
2404 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2406 oldSeq += dragDirection;
2413 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2415 if (stretchGroup.getSequences(null).contains(nextSeq))
2417 stretchGroup.deleteSequence(seq, false);
2418 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2424 stretchGroup.addSequence(seq, false);
2427 stretchGroup.addSequence(nextSeq, false);
2428 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2437 mouseDragging = true;
2439 if (scrollThread != null)
2441 scrollThread.setMousePosition(evt.getPoint());
2445 * construct a status message showing the range of the selection
2447 StringBuilder status = new StringBuilder(64);
2448 List<SequenceI> seqs = stretchGroup.getSequences();
2449 String name = seqs.get(0).getName();
2450 if (name.length() > 20)
2452 name = name.substring(0, 20);
2454 status.append(name).append(" - ");
2455 name = seqs.get(seqs.size() - 1).getName();
2456 if (name.length() > 20)
2458 name = name.substring(0, 20);
2460 status.append(name).append(" ");
2461 int startRes = stretchGroup.getStartRes();
2462 status.append(" cols ").append(String.valueOf(startRes + 1))
2464 int endRes = stretchGroup.getEndRes();
2465 status.append(String.valueOf(endRes + 1));
2466 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2467 .append(String.valueOf(endRes - startRes + 1)).append(")");
2468 ap.alignFrame.setStatus(status.toString());
2472 * Stops the scroll thread if it is running
2474 void stopScrolling()
2476 if (scrollThread != null)
2478 scrollThread.stopScrolling();
2479 scrollThread = null;
2481 mouseDragging = false;
2485 * Starts a thread to scroll the alignment, towards a given mouse position
2486 * outside the panel bounds
2490 void startScrolling(Point mousePos)
2492 if (scrollThread == null)
2494 scrollThread = new ScrollThread();
2497 mouseDragging = true;
2498 scrollThread.setMousePosition(mousePos);
2502 * Performs scrolling of the visible alignment left, right, up or down
2504 class ScrollThread extends Thread
2506 private Point mousePos;
2508 private volatile boolean threadRunning = true;
2513 public ScrollThread()
2515 setName("SeqPanel$ScrollThread");
2520 * Sets the position of the mouse that determines the direction of the
2525 public void setMousePosition(Point p)
2531 * Sets a flag that will cause the thread to exit
2533 public void stopScrolling()
2535 threadRunning = false;
2539 * Scrolls the alignment left or right, and/or up or down, depending on the
2540 * last notified mouse position, until the limit of the alignment is
2541 * reached, or a flag is set to stop the scroll
2546 while (threadRunning && mouseDragging)
2548 if (mousePos != null)
2550 boolean scrolled = false;
2551 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2558 // mouse is above this panel - try scroll up
2559 scrolled = ranges.scrollUp(true);
2561 else if (mousePos.y >= getHeight())
2563 // mouse is below this panel - try scroll down
2564 scrolled = ranges.scrollUp(false);
2568 * scroll left or right
2572 scrolled |= ranges.scrollRight(false);
2574 else if (mousePos.x >= getWidth())
2576 scrolled |= ranges.scrollRight(true);
2581 * we have reached the limit of the visible alignment - quit
2583 threadRunning = false;
2584 SeqPanel.this.ap.repaint();
2591 } catch (Exception ex)
2599 * modify current selection according to a received message.
2602 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2603 HiddenColumns hidden, SelectionSource source)
2605 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2606 // handles selection messages...
2607 // TODO: extend config options to allow user to control if selections may be
2608 // shared between viewports.
2609 boolean iSentTheSelection = (av == source
2610 || (source instanceof AlignViewport
2611 && ((AlignmentViewport) source).getSequenceSetId()
2612 .equals(av.getSequenceSetId())));
2614 if (iSentTheSelection)
2616 // respond to our own event by updating dependent dialogs
2617 if (ap.getCalculationDialog() != null)
2619 ap.getCalculationDialog().validateCalcTypes();
2625 // process further ?
2626 if (!av.followSelection)
2632 * Ignore the selection if there is one of our own pending.
2634 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2640 * Check for selection in a view of which this one is a dna/protein
2643 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2648 // do we want to thread this ? (contention with seqsel and colsel locks, I
2651 * only copy colsel if there is a real intersection between
2652 * sequence selection and this panel's alignment
2654 boolean repaint = false;
2655 boolean copycolsel = false;
2657 SequenceGroup sgroup = null;
2658 if (seqsel != null && seqsel.getSize() > 0)
2660 if (av.getAlignment() == null)
2662 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2663 + " ViewId=" + av.getViewId()
2664 + " 's alignment is NULL! returning immediately.");
2667 sgroup = seqsel.intersect(av.getAlignment(),
2668 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2669 if ((sgroup != null && sgroup.getSize() > 0))
2674 if (sgroup != null && sgroup.getSize() > 0)
2676 av.setSelectionGroup(sgroup);
2680 av.setSelectionGroup(null);
2682 av.isSelectionGroupChanged(true);
2687 // the current selection is unset or from a previous message
2688 // so import the new colsel.
2689 if (colsel == null || colsel.isEmpty())
2691 if (av.getColumnSelection() != null)
2693 av.getColumnSelection().clear();
2699 // TODO: shift colSel according to the intersecting sequences
2700 if (av.getColumnSelection() == null)
2702 av.setColumnSelection(new ColumnSelection(colsel));
2706 av.getColumnSelection().setElementsFrom(colsel,
2707 av.getAlignment().getHiddenColumns());
2710 av.isColSelChanged(true);
2714 if (copycolsel && av.hasHiddenColumns()
2715 && (av.getAlignment().getHiddenColumns() == null))
2717 System.err.println("Bad things");
2719 if (repaint) // always true!
2721 // probably finessing with multiple redraws here
2722 PaintRefresher.Refresh(this, av.getSequenceSetId());
2723 // ap.paintAlignment(false);
2726 // lastly, update dependent dialogs
2727 if (ap.getCalculationDialog() != null)
2729 ap.getCalculationDialog().validateCalcTypes();
2735 * If this panel is a cdna/protein translation view of the selection source,
2736 * tries to map the source selection to a local one, and returns true. Else
2743 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2744 ColumnSelection colsel, HiddenColumns hidden,
2745 SelectionSource source)
2747 if (!(source instanceof AlignViewportI))
2751 final AlignViewportI sourceAv = (AlignViewportI) source;
2752 if (sourceAv.getCodingComplement() != av
2753 && av.getCodingComplement() != sourceAv)
2759 * Map sequence selection
2761 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2762 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2763 av.isSelectionGroupChanged(true);
2766 * Map column selection
2768 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2770 ColumnSelection cs = new ColumnSelection();
2771 HiddenColumns hs = new HiddenColumns();
2772 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2773 av.setColumnSelection(cs);
2774 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2776 // lastly, update any dependent dialogs
2777 if (ap.getCalculationDialog() != null)
2779 ap.getCalculationDialog().validateCalcTypes();
2783 * repaint alignment, and also Overview or Structure
2784 * if hidden column selection has changed
2786 ap.paintAlignment(hiddenChanged, hiddenChanged);
2793 * @return null or last search results handled by this panel
2795 public SearchResultsI getLastSearchResults()
2797 return lastSearchResults;