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.getName(), 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(String seqName, 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: ")
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 alignedSeq = al.getSequenceAt(sequenceIndex);
1282 SequenceI ds = alignedSeq.getDatasetSequence();
1283 for (SearchResultMatchI m : results.getResults())
1285 SequenceI seq = m.getSequence();
1286 if (seq.getDatasetSequence() != null)
1288 seq = seq.getDatasetSequence();
1293 int start = m.getStart();
1294 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1295 seq.getCharAt(start - 1), start);
1305 public void mouseDragged(MouseEvent evt)
1307 MousePos pos = findMousePosition(evt);
1308 if (pos.isOverAnnotation() || pos.column == -1)
1313 if (mouseWheelPressed)
1315 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1316 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1318 int oldWidth = av.getCharWidth();
1320 // Which is bigger, left-right or up-down?
1321 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1322 .abs(evt.getX() - lastMousePress.getX()))
1325 * on drag up or down, decrement or increment font size
1327 int fontSize = av.font.getSize();
1328 boolean fontChanged = false;
1330 if (evt.getY() < lastMousePress.getY())
1335 else if (evt.getY() > lastMousePress.getY())
1348 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1350 av.setFont(newFont, true);
1351 av.setCharWidth(oldWidth);
1355 ap.av.getCodingComplement().setFont(newFont, true);
1356 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1357 .getSplitViewContainer();
1358 splitFrame.adjustLayout();
1359 splitFrame.repaint();
1366 * on drag left or right, decrement or increment character width
1369 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1371 newWidth = av.getCharWidth() - 1;
1372 av.setCharWidth(newWidth);
1374 else if (evt.getX() > lastMousePress.getX())
1376 newWidth = av.getCharWidth() + 1;
1377 av.setCharWidth(newWidth);
1381 ap.paintAlignment(false, false);
1385 * need to ensure newWidth is set on cdna, regardless of which
1386 * panel the mouse drag happened in; protein will compute its
1387 * character width as 1:1 or 3:1
1389 av.getCodingComplement().setCharWidth(newWidth);
1390 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1391 .getSplitViewContainer();
1392 splitFrame.adjustLayout();
1393 splitFrame.repaint();
1398 FontMetrics fm = getFontMetrics(av.getFont());
1399 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1401 lastMousePress = evt.getPoint();
1408 dragStretchGroup(evt);
1412 int res = pos.column;
1419 if ((editLastRes == -1) || (editLastRes == res))
1424 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1426 // dragLeft, delete gap
1427 editSequence(false, false, res);
1431 editSequence(true, false, res);
1434 mouseDragging = true;
1435 if (scrollThread != null)
1437 scrollThread.setMousePosition(evt.getPoint());
1442 * Edits the sequence to insert or delete one or more gaps, in response to a
1443 * mouse drag or cursor mode command. The number of inserts/deletes may be
1444 * specified with the cursor command, or else depends on the mouse event
1445 * (normally one column, but potentially more for a fast mouse drag).
1447 * Delete gaps is limited to the number of gaps left of the cursor position
1448 * (mouse drag), or at or right of the cursor position (cursor mode).
1450 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1451 * the current selection group.
1453 * In locked editing mode (with a selection group present), inserts/deletions
1454 * within the selection group are limited to its boundaries (and edits outside
1455 * the group stop at its border).
1458 * true to insert gaps, false to delete gaps
1460 * (unused parameter)
1462 * the column at which to perform the action; the number of columns
1463 * affected depends on <code>this.editLastRes</code> (cursor column
1466 synchronized void editSequence(boolean insertGap, boolean editSeq,
1470 int fixedRight = -1;
1471 boolean fixedColumns = false;
1472 SequenceGroup sg = av.getSelectionGroup();
1474 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1476 // No group, but the sequence may represent a group
1477 if (!groupEditing && av.hasHiddenRows())
1479 if (av.isHiddenRepSequence(seq))
1481 sg = av.getRepresentedSequences(seq);
1482 groupEditing = true;
1486 StringBuilder message = new StringBuilder(64); // for status bar
1489 * make a name for the edit action, for
1490 * status bar message and Undo/Redo menu
1492 String label = null;
1495 message.append("Edit group:");
1496 label = MessageManager.getString("action.edit_group");
1500 message.append("Edit sequence: " + seq.getName());
1501 label = seq.getName();
1502 if (label.length() > 10)
1504 label = label.substring(0, 10);
1506 label = MessageManager.formatMessage("label.edit_params",
1512 * initialise the edit command if there is not
1513 * already one being extended
1515 if (editCommand == null)
1517 editCommand = new EditCommand(label);
1522 message.append(" insert ");
1526 message.append(" delete ");
1529 message.append(Math.abs(startres - editLastRes) + " gaps.");
1530 ap.alignFrame.setStatus(message.toString());
1533 * is there a selection group containing the sequence being edited?
1534 * if so the boundary of the group is the limit of the edit
1535 * (but the edit may be inside or outside the selection group)
1537 boolean inSelectionGroup = sg != null
1538 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1539 if (groupEditing || inSelectionGroup)
1541 fixedColumns = true;
1543 // sg might be null as the user may only see 1 sequence,
1544 // but the sequence represents a group
1547 if (!av.isHiddenRepSequence(seq))
1552 sg = av.getRepresentedSequences(seq);
1555 fixedLeft = sg.getStartRes();
1556 fixedRight = sg.getEndRes();
1558 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1559 || (startres >= fixedLeft && editLastRes < fixedLeft)
1560 || (startres > fixedRight && editLastRes <= fixedRight)
1561 || (startres <= fixedRight && editLastRes > fixedRight))
1567 if (fixedLeft > startres)
1569 fixedRight = fixedLeft - 1;
1572 else if (fixedRight < startres)
1574 fixedLeft = fixedRight;
1579 if (av.hasHiddenColumns())
1581 fixedColumns = true;
1582 int y1 = av.getAlignment().getHiddenColumns()
1583 .getNextHiddenBoundary(true, startres);
1584 int y2 = av.getAlignment().getHiddenColumns()
1585 .getNextHiddenBoundary(false, startres);
1587 if ((insertGap && startres > y1 && editLastRes < y1)
1588 || (!insertGap && startres < y2 && editLastRes > y2))
1594 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1595 // Selection spans a hidden region
1596 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1604 fixedRight = y2 - 1;
1609 boolean success = doEditSequence(insertGap, editSeq, startres,
1610 fixedRight, fixedColumns, sg);
1613 * report what actually happened (might be less than
1614 * what was requested), by inspecting the edit commands added
1616 String msg = getEditStatusMessage(editCommand);
1617 ap.alignFrame.setStatus(msg == null ? " " : msg);
1623 editLastRes = startres;
1624 seqCanvas.repaint();
1628 * A helper method that performs the requested editing to insert or delete
1629 * gaps (if possible). Answers true if the edit was successful, false if could
1630 * only be performed in part or not at all. Failure may occur in 'locked edit'
1631 * mode, when an insertion requires a matching gapped position (or column) to
1632 * delete, and deletion requires an adjacent gapped position (or column) to
1636 * true if inserting gap(s), false if deleting
1638 * (unused parameter, currently always false)
1640 * the column at which to perform the edit
1642 * fixed right boundary column of a locked edit (within or to the
1643 * left of a selection group)
1644 * @param fixedColumns
1645 * true if this is a locked edit
1647 * the sequence group (if group edit is being performed)
1650 protected boolean doEditSequence(final boolean insertGap,
1651 final boolean editSeq, final int startres, int fixedRight,
1652 final boolean fixedColumns, final SequenceGroup sg)
1654 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1655 SequenceI[] seqs = new SequenceI[] { seq };
1659 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1660 int g, groupSize = vseqs.size();
1661 SequenceI[] groupSeqs = new SequenceI[groupSize];
1662 for (g = 0; g < groupSeqs.length; g++)
1664 groupSeqs[g] = vseqs.get(g);
1670 // If the user has selected the whole sequence, and is dragging to
1671 // the right, we can still extend the alignment and selectionGroup
1672 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1673 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1676 av.getAlignment().getWidth() + startres - editLastRes);
1677 fixedRight = sg.getEndRes();
1680 // Is it valid with fixed columns??
1681 // Find the next gap before the end
1682 // of the visible region boundary
1683 boolean blank = false;
1684 for (; fixedRight > editLastRes; fixedRight--)
1688 for (g = 0; g < groupSize; g++)
1690 for (int j = 0; j < startres - editLastRes; j++)
1693 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1708 if (sg.getSize() == av.getAlignment().getHeight())
1710 if ((av.hasHiddenColumns()
1711 && startres < av.getAlignment().getHiddenColumns()
1712 .getNextHiddenBoundary(false, startres)))
1717 int alWidth = av.getAlignment().getWidth();
1718 if (av.hasHiddenRows())
1720 int hwidth = av.getAlignment().getHiddenSequences()
1722 if (hwidth > alWidth)
1727 // We can still insert gaps if the selectionGroup
1728 // contains all the sequences
1729 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1730 fixedRight = alWidth + startres - editLastRes;
1740 else if (!insertGap)
1742 // / Are we able to delete?
1743 // ie are all columns blank?
1745 for (g = 0; g < groupSize; g++)
1747 for (int j = startres; j < editLastRes; j++)
1749 if (groupSeqs[g].getLength() <= j)
1754 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1756 // Not a gap, block edit not valid
1765 // dragging to the right
1766 if (fixedColumns && fixedRight != -1)
1768 for (int j = editLastRes; j < startres; j++)
1770 insertGap(j, groupSeqs, fixedRight);
1775 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1776 startres - editLastRes, false);
1781 // dragging to the left
1782 if (fixedColumns && fixedRight != -1)
1784 for (int j = editLastRes; j > startres; j--)
1786 deleteChar(startres, groupSeqs, fixedRight);
1791 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1792 editLastRes - startres, false);
1799 * editing a single sequence
1803 // dragging to the right
1804 if (fixedColumns && fixedRight != -1)
1806 for (int j = editLastRes; j < startres; j++)
1808 if (!insertGap(j, seqs, fixedRight))
1811 * e.g. cursor mode command specified
1812 * more inserts than are possible
1820 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1821 startres - editLastRes, false);
1828 // dragging to the left
1829 if (fixedColumns && fixedRight != -1)
1831 for (int j = editLastRes; j > startres; j--)
1833 if (!Comparison.isGap(seq.getCharAt(startres)))
1837 deleteChar(startres, seqs, fixedRight);
1842 // could be a keyboard edit trying to delete none gaps
1844 for (int m = startres; m < editLastRes; m++)
1846 if (!Comparison.isGap(seq.getCharAt(m)))
1854 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1859 {// insertGap==false AND editSeq==TRUE;
1860 if (fixedColumns && fixedRight != -1)
1862 for (int j = editLastRes; j < startres; j++)
1864 insertGap(j, seqs, fixedRight);
1869 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1870 startres - editLastRes, false);
1880 * Constructs an informative status bar message while dragging to insert or
1881 * delete gaps. Answers null if inserts and deletes cancel out.
1883 * @param editCommand
1884 * a command containing the list of individual edits
1887 protected static String getEditStatusMessage(EditCommand editCommand)
1889 if (editCommand == null)
1895 * add any inserts, and subtract any deletes,
1896 * not counting those auto-inserted when doing a 'locked edit'
1897 * (so only counting edits 'under the cursor')
1900 for (Edit cmd : editCommand.getEdits())
1902 if (!cmd.isSystemGenerated())
1904 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1912 * inserts and deletes cancel out
1917 String msgKey = count > 1 ? "label.insert_gaps"
1918 : (count == 1 ? "label.insert_gap"
1919 : (count == -1 ? "label.delete_gap"
1920 : "label.delete_gaps"));
1921 count = Math.abs(count);
1923 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1927 * Inserts one gap at column j, deleting the right-most gapped column up to
1928 * (and including) fixedColumn. Returns true if the edit is successful, false
1929 * if no blank column is available to allow the insertion to be balanced by a
1934 * @param fixedColumn
1937 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1939 int blankColumn = fixedColumn;
1940 for (int s = 0; s < seq.length; s++)
1942 // Find the next gap before the end of the visible region boundary
1943 // If lastCol > j, theres a boundary after the gap insertion
1945 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1947 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1949 // Theres a space, so break and insert the gap
1954 if (blankColumn <= j)
1956 blankColumn = fixedColumn;
1962 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1964 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1970 * Helper method to add and perform one edit action
1976 * @param systemGenerated
1977 * true if the edit is a 'balancing' delete (or insert) to match a
1978 * user's insert (or delete) in a locked editing region
1980 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1981 int count, boolean systemGenerated)
1984 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1985 av.getAlignment().getGapCharacter());
1986 edit.setSystemGenerated(systemGenerated);
1988 editCommand.appendEdit(edit, av.getAlignment(), true, null);
1992 * Deletes the character at column j, and inserts a gap at fixedColumn, in
1993 * each of the given sequences. The caller should ensure that all sequences
1994 * are gapped in column j.
1998 * @param fixedColumn
2000 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2002 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2004 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2008 * On reentering the panel, stops any scrolling that was started on dragging
2014 public void mouseEntered(MouseEvent e)
2024 * On leaving the panel, if the mouse is being dragged, starts a thread to
2025 * scroll it until the mouse is released (in unwrapped mode only)
2030 public void mouseExited(MouseEvent e)
2032 lastMousePosition = null;
2033 ap.alignFrame.setStatus(" ");
2034 if (av.getWrapAlignment())
2039 if (mouseDragging && scrollThread == null)
2041 scrollThread = new ScrollThread();
2046 * Handler for double-click on a position with one or more sequence features.
2047 * Opens the Amend Features dialog to allow feature details to be amended, or
2048 * the feature deleted.
2051 public void mouseClicked(MouseEvent evt)
2053 SequenceGroup sg = null;
2054 MousePos pos = findMousePosition(evt);
2055 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2060 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2062 sg = av.getSelectionGroup();
2063 if (sg != null && sg.getSize() == 1
2064 && sg.getEndRes() - sg.getStartRes() < 2)
2066 av.setSelectionGroup(null);
2069 int column = pos.column;
2072 * find features at the position (if not gapped), or straddling
2073 * the position (if at a gap)
2075 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2076 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2077 .findFeaturesAtColumn(sequence, column + 1);
2079 if (!features.isEmpty())
2082 * highlight the first feature at the position on the alignment
2084 SearchResultsI highlight = new SearchResults();
2085 highlight.addResult(sequence, features.get(0).getBegin(), features
2087 seqCanvas.highlightSearchResults(highlight, false);
2090 * open the Amend Features dialog; clear highlighting afterwards,
2091 * whether changes were made or not
2093 List<SequenceI> seqs = Collections.singletonList(sequence);
2094 seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2096 av.setSearchResults(null); // clear highlighting
2097 seqCanvas.repaint(); // draw new/amended features
2103 public void mouseWheelMoved(MouseWheelEvent e)
2106 double wheelRotation = e.getPreciseWheelRotation();
2107 if (wheelRotation > 0)
2109 if (e.isShiftDown())
2111 av.getRanges().scrollRight(true);
2116 av.getRanges().scrollUp(false);
2119 else if (wheelRotation < 0)
2121 if (e.isShiftDown())
2123 av.getRanges().scrollRight(false);
2127 av.getRanges().scrollUp(true);
2132 * update status bar and tooltip for new position
2133 * (need to synthesize a mouse movement to refresh tooltip)
2136 ToolTipManager.sharedInstance().mouseMoved(e);
2145 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2147 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2152 final int res = pos.column;
2153 final int seq = pos.seqIndex;
2155 updateOverviewAndStructs = false;
2157 startWrapBlock = wrappedBlock;
2159 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2161 if ((sequence == null) || (res > sequence.getLength()))
2166 stretchGroup = av.getSelectionGroup();
2168 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2170 stretchGroup = av.getAlignment().findGroup(sequence, res);
2171 if (stretchGroup != null)
2173 // only update the current selection if the popup menu has a group to
2175 av.setSelectionGroup(stretchGroup);
2179 if (evt.isPopupTrigger()) // Mac: mousePressed
2181 showPopupMenu(evt, pos);
2186 * defer right-mouse click handling to mouseReleased on Windows
2187 * (where isPopupTrigger() will answer true)
2188 * NB isRightMouseButton is also true for Cmd-click on Mac
2190 if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2197 seqCanvas.cursorX = res;
2198 seqCanvas.cursorY = seq;
2199 seqCanvas.repaint();
2203 if (stretchGroup == null)
2205 createStretchGroup(res, sequence);
2208 if (stretchGroup != null)
2210 stretchGroup.addPropertyChangeListener(seqCanvas);
2213 seqCanvas.repaint();
2216 private void createStretchGroup(int res, SequenceI sequence)
2218 // Only if left mouse button do we want to change group sizes
2219 // define a new group here
2220 SequenceGroup sg = new SequenceGroup();
2221 sg.setStartRes(res);
2223 sg.addSequence(sequence, false);
2224 av.setSelectionGroup(sg);
2227 if (av.getConservationSelected())
2229 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2233 if (av.getAbovePIDThreshold())
2235 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2238 // TODO: stretchGroup will always be not null. Is this a merge error ?
2239 // or is there a threading issue here?
2240 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2242 // Edit end res position of selected group
2243 changeEndRes = true;
2245 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2247 // Edit end res position of selected group
2248 changeStartRes = true;
2250 stretchGroup.getWidth();
2255 * Build and show a pop-up menu at the right-click mouse position
2260 void showPopupMenu(MouseEvent evt, MousePos pos)
2262 final int column = pos.column;
2263 final int seq = pos.seqIndex;
2264 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2265 if (sequence != null)
2267 PopupMenu pop = new PopupMenu(ap, sequence, column);
2268 pop.show(this, evt.getX(), evt.getY());
2273 * Update the display after mouse up on a selection or group
2276 * mouse released event details
2278 * true if this event is happening after a mouse drag (rather than a
2281 protected void doMouseReleasedDefineMode(MouseEvent evt,
2284 if (stretchGroup == null)
2289 stretchGroup.removePropertyChangeListener(seqCanvas);
2291 // always do this - annotation has own state
2292 // but defer colourscheme update until hidden sequences are passed in
2293 boolean vischange = stretchGroup.recalcConservation(true);
2294 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2296 if (stretchGroup.cs != null)
2300 stretchGroup.cs.alignmentChanged(stretchGroup,
2301 av.getHiddenRepSequences());
2304 ResidueShaderI groupColourScheme = stretchGroup
2305 .getGroupColourScheme();
2306 String name = stretchGroup.getName();
2307 if (stretchGroup.cs.conservationApplied())
2309 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2311 if (stretchGroup.cs.getThreshold() > 0)
2313 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2316 PaintRefresher.Refresh(this, av.getSequenceSetId());
2317 // TODO: structure colours only need updating if stretchGroup used to or now
2318 // does contain sequences with structure views
2319 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2320 updateOverviewAndStructs = false;
2321 changeEndRes = false;
2322 changeStartRes = false;
2323 stretchGroup = null;
2328 * Resizes the borders of a selection group depending on the direction of
2333 protected void dragStretchGroup(MouseEvent evt)
2335 if (stretchGroup == null)
2340 MousePos pos = findMousePosition(evt);
2341 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2346 int res = pos.column;
2347 int y = pos.seqIndex;
2349 if (wrappedBlock != startWrapBlock)
2354 res = Math.min(res, av.getAlignment().getWidth()-1);
2356 if (stretchGroup.getEndRes() == res)
2358 // Edit end res position of selected group
2359 changeEndRes = true;
2361 else if (stretchGroup.getStartRes() == res)
2363 // Edit start res position of selected group
2364 changeStartRes = true;
2367 if (res < av.getRanges().getStartRes())
2369 res = av.getRanges().getStartRes();
2374 if (res > (stretchGroup.getStartRes() - 1))
2376 stretchGroup.setEndRes(res);
2377 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2380 else if (changeStartRes)
2382 if (res < (stretchGroup.getEndRes() + 1))
2384 stretchGroup.setStartRes(res);
2385 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2389 int dragDirection = 0;
2395 else if (y < oldSeq)
2400 while ((y != oldSeq) && (oldSeq > -1)
2401 && (y < av.getAlignment().getHeight()))
2403 // This routine ensures we don't skip any sequences, as the
2404 // selection is quite slow.
2405 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2407 oldSeq += dragDirection;
2414 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2416 if (stretchGroup.getSequences(null).contains(nextSeq))
2418 stretchGroup.deleteSequence(seq, false);
2419 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2425 stretchGroup.addSequence(seq, false);
2428 stretchGroup.addSequence(nextSeq, false);
2429 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2438 mouseDragging = true;
2440 if (scrollThread != null)
2442 scrollThread.setMousePosition(evt.getPoint());
2446 * construct a status message showing the range of the selection
2448 StringBuilder status = new StringBuilder(64);
2449 List<SequenceI> seqs = stretchGroup.getSequences();
2450 String name = seqs.get(0).getName();
2451 if (name.length() > 20)
2453 name = name.substring(0, 20);
2455 status.append(name).append(" - ");
2456 name = seqs.get(seqs.size() - 1).getName();
2457 if (name.length() > 20)
2459 name = name.substring(0, 20);
2461 status.append(name).append(" ");
2462 int startRes = stretchGroup.getStartRes();
2463 status.append(" cols ").append(String.valueOf(startRes + 1))
2465 int endRes = stretchGroup.getEndRes();
2466 status.append(String.valueOf(endRes + 1));
2467 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2468 .append(String.valueOf(endRes - startRes + 1)).append(")");
2469 ap.alignFrame.setStatus(status.toString());
2473 * Stops the scroll thread if it is running
2475 void stopScrolling()
2477 if (scrollThread != null)
2479 scrollThread.stopScrolling();
2480 scrollThread = null;
2482 mouseDragging = false;
2486 * Starts a thread to scroll the alignment, towards a given mouse position
2487 * outside the panel bounds
2491 void startScrolling(Point mousePos)
2493 if (scrollThread == null)
2495 scrollThread = new ScrollThread();
2498 mouseDragging = true;
2499 scrollThread.setMousePosition(mousePos);
2503 * Performs scrolling of the visible alignment left, right, up or down
2505 class ScrollThread extends Thread
2507 private Point mousePos;
2509 private volatile boolean threadRunning = true;
2514 public ScrollThread()
2516 setName("SeqPanel$ScrollThread");
2521 * Sets the position of the mouse that determines the direction of the
2526 public void setMousePosition(Point p)
2532 * Sets a flag that will cause the thread to exit
2534 public void stopScrolling()
2536 threadRunning = false;
2540 * Scrolls the alignment left or right, and/or up or down, depending on the
2541 * last notified mouse position, until the limit of the alignment is
2542 * reached, or a flag is set to stop the scroll
2547 while (threadRunning && mouseDragging)
2549 if (mousePos != null)
2551 boolean scrolled = false;
2552 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2559 // mouse is above this panel - try scroll up
2560 scrolled = ranges.scrollUp(true);
2562 else if (mousePos.y >= getHeight())
2564 // mouse is below this panel - try scroll down
2565 scrolled = ranges.scrollUp(false);
2569 * scroll left or right
2573 scrolled |= ranges.scrollRight(false);
2575 else if (mousePos.x >= getWidth())
2577 scrolled |= ranges.scrollRight(true);
2582 * we have reached the limit of the visible alignment - quit
2584 threadRunning = false;
2585 SeqPanel.this.ap.repaint();
2592 } catch (Exception ex)
2600 * modify current selection according to a received message.
2603 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2604 HiddenColumns hidden, SelectionSource source)
2606 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2607 // handles selection messages...
2608 // TODO: extend config options to allow user to control if selections may be
2609 // shared between viewports.
2610 boolean iSentTheSelection = (av == source
2611 || (source instanceof AlignViewport
2612 && ((AlignmentViewport) source).getSequenceSetId()
2613 .equals(av.getSequenceSetId())));
2615 if (iSentTheSelection)
2617 // respond to our own event by updating dependent dialogs
2618 if (ap.getCalculationDialog() != null)
2620 ap.getCalculationDialog().validateCalcTypes();
2626 // process further ?
2627 if (!av.followSelection)
2633 * Ignore the selection if there is one of our own pending.
2635 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2641 * Check for selection in a view of which this one is a dna/protein
2644 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2649 // do we want to thread this ? (contention with seqsel and colsel locks, I
2652 * only copy colsel if there is a real intersection between
2653 * sequence selection and this panel's alignment
2655 boolean repaint = false;
2656 boolean copycolsel = false;
2658 SequenceGroup sgroup = null;
2659 if (seqsel != null && seqsel.getSize() > 0)
2661 if (av.getAlignment() == null)
2663 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2664 + " ViewId=" + av.getViewId()
2665 + " 's alignment is NULL! returning immediately.");
2668 sgroup = seqsel.intersect(av.getAlignment(),
2669 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2670 if ((sgroup != null && sgroup.getSize() > 0))
2675 if (sgroup != null && sgroup.getSize() > 0)
2677 av.setSelectionGroup(sgroup);
2681 av.setSelectionGroup(null);
2683 av.isSelectionGroupChanged(true);
2688 // the current selection is unset or from a previous message
2689 // so import the new colsel.
2690 if (colsel == null || colsel.isEmpty())
2692 if (av.getColumnSelection() != null)
2694 av.getColumnSelection().clear();
2700 // TODO: shift colSel according to the intersecting sequences
2701 if (av.getColumnSelection() == null)
2703 av.setColumnSelection(new ColumnSelection(colsel));
2707 av.getColumnSelection().setElementsFrom(colsel,
2708 av.getAlignment().getHiddenColumns());
2711 av.isColSelChanged(true);
2715 if (copycolsel && av.hasHiddenColumns()
2716 && (av.getAlignment().getHiddenColumns() == null))
2718 System.err.println("Bad things");
2720 if (repaint) // always true!
2722 // probably finessing with multiple redraws here
2723 PaintRefresher.Refresh(this, av.getSequenceSetId());
2724 // ap.paintAlignment(false);
2727 // lastly, update dependent dialogs
2728 if (ap.getCalculationDialog() != null)
2730 ap.getCalculationDialog().validateCalcTypes();
2736 * If this panel is a cdna/protein translation view of the selection source,
2737 * tries to map the source selection to a local one, and returns true. Else
2744 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2745 ColumnSelection colsel, HiddenColumns hidden,
2746 SelectionSource source)
2748 if (!(source instanceof AlignViewportI))
2752 final AlignViewportI sourceAv = (AlignViewportI) source;
2753 if (sourceAv.getCodingComplement() != av
2754 && av.getCodingComplement() != sourceAv)
2760 * Map sequence selection
2762 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2763 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2764 av.isSelectionGroupChanged(true);
2767 * Map column selection
2769 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2771 ColumnSelection cs = new ColumnSelection();
2772 HiddenColumns hs = new HiddenColumns();
2773 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2774 av.setColumnSelection(cs);
2775 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2777 // lastly, update any dependent dialogs
2778 if (ap.getCalculationDialog() != null)
2780 ap.getCalculationDialog().validateCalcTypes();
2784 * repaint alignment, and also Overview or Structure
2785 * if hidden column selection has changed
2787 ap.paintAlignment(hiddenChanged, hiddenChanged);
2794 * @return null or last search results handled by this panel
2796 public SearchResultsI getLastSearchResults()
2798 return lastSearchResults;