2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import java.awt.BorderLayout;
24 import java.awt.Color;
26 import java.awt.FontMetrics;
27 import java.awt.Point;
28 import java.awt.event.MouseEvent;
29 import java.awt.event.MouseListener;
30 import java.awt.event.MouseMotionListener;
31 import java.awt.event.MouseWheelEvent;
32 import java.awt.event.MouseWheelListener;
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.List;
37 import javax.swing.JPanel;
38 import javax.swing.SwingUtilities;
39 import javax.swing.ToolTipManager;
41 import jalview.api.AlignViewportI;
42 import jalview.bin.Cache;
43 import jalview.commands.EditCommand;
44 import jalview.commands.EditCommand.Action;
45 import jalview.commands.EditCommand.Edit;
46 import jalview.datamodel.AlignmentAnnotation;
47 import jalview.datamodel.AlignmentI;
48 import jalview.datamodel.ColumnSelection;
49 import jalview.datamodel.HiddenColumns;
50 import jalview.datamodel.MappedFeatures;
51 import jalview.datamodel.SearchResultMatchI;
52 import jalview.datamodel.SearchResults;
53 import jalview.datamodel.SearchResultsI;
54 import jalview.datamodel.Sequence;
55 import jalview.datamodel.SequenceFeature;
56 import jalview.datamodel.SequenceGroup;
57 import jalview.datamodel.SequenceI;
58 import jalview.io.SequenceAnnotationReport;
59 import jalview.renderer.ResidueShaderI;
60 import jalview.schemes.ResidueProperties;
61 import jalview.structure.SelectionListener;
62 import jalview.structure.SelectionSource;
63 import jalview.structure.SequenceListener;
64 import jalview.structure.StructureSelectionManager;
65 import jalview.structure.VamsasSource;
66 import jalview.util.Comparison;
67 import jalview.util.MappingUtils;
68 import jalview.util.MessageManager;
69 import jalview.util.Platform;
70 import jalview.viewmodel.AlignmentViewport;
71 import jalview.viewmodel.ViewportRanges;
72 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
78 * @version $Revision: 1.130 $
80 public class SeqPanel extends JPanel
81 implements MouseListener, MouseMotionListener, MouseWheelListener,
82 SequenceListener, SelectionListener
85 * a class that holds computed mouse position
86 * - column of the alignment (0...)
87 * - sequence offset (0...)
88 * - annotation row offset (0...)
89 * where annotation offset is -1 unless the alignment is shown
90 * in wrapped mode, annotations are shown, and the mouse is
91 * over an annnotation row
96 * alignment column position of cursor (0...)
101 * index in alignment of sequence under cursor,
102 * or nearest above if cursor is not over a sequence
107 * index in annotations array of annotation under the cursor
108 * (only possible in wrapped mode with annotations shown),
109 * or -1 if cursor is not over an annotation row
111 final int annotationIndex;
113 MousePos(int col, int seq, int ann)
117 annotationIndex = ann;
120 boolean isOverAnnotation()
122 return annotationIndex != -1;
126 public boolean equals(Object obj)
128 if (obj == null || !(obj instanceof MousePos))
132 MousePos o = (MousePos) obj;
133 boolean b = (column == o.column && seqIndex == o.seqIndex
134 && annotationIndex == o.annotationIndex);
135 // System.out.println(obj + (b ? "= " : "!= ") + this);
140 * A simple hashCode that ensures that instances that satisfy equals() have
144 public int hashCode()
146 return column + seqIndex + annotationIndex;
150 * toString method for debug output purposes only
153 public String toString()
155 return String.format("c%d:s%d:a%d", column, seqIndex,
160 private static final int MAX_TOOLTIP_LENGTH = 300;
162 public SeqCanvas seqCanvas;
164 public AlignmentPanel ap;
167 * last position for mouseMoved event
169 private MousePos lastMousePosition;
171 protected int editLastRes;
173 protected int editStartSeq;
175 protected AlignViewport av;
177 ScrollThread scrollThread = null;
179 boolean mouseDragging = false;
181 boolean editingSeqs = false;
183 boolean groupEditing = false;
185 // ////////////////////////////////////////
186 // ///Everything below this is for defining the boundary of the rubberband
187 // ////////////////////////////////////////
190 boolean changeEndSeq = false;
192 boolean changeStartSeq = false;
194 boolean changeEndRes = false;
196 boolean changeStartRes = false;
198 SequenceGroup stretchGroup = null;
200 boolean remove = false;
202 Point lastMousePress;
204 boolean mouseWheelPressed = false;
206 StringBuffer keyboardNo1;
208 StringBuffer keyboardNo2;
210 private final SequenceAnnotationReport seqARep;
212 StringBuilder tooltipText = new StringBuilder();
216 EditCommand editCommand;
218 StructureSelectionManager ssm;
220 SearchResultsI lastSearchResults;
223 * Creates a new SeqPanel object
228 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
230 seqARep = new SequenceAnnotationReport(true);
231 ToolTipManager.sharedInstance().registerComponent(this);
232 ToolTipManager.sharedInstance().setInitialDelay(0);
233 ToolTipManager.sharedInstance().setDismissDelay(10000);
235 setBackground(Color.white);
237 seqCanvas = new SeqCanvas(alignPanel);
238 setLayout(new BorderLayout());
239 add(seqCanvas, BorderLayout.CENTER);
241 this.ap = alignPanel;
243 if (!viewport.isDataset())
245 addMouseMotionListener(this);
246 addMouseListener(this);
247 addMouseWheelListener(this);
248 ssm = viewport.getStructureSelectionManager();
249 ssm.addStructureViewerListener(this);
250 ssm.addSelectionListener(this);
254 int startWrapBlock = -1;
256 int wrappedBlock = -1;
259 * Computes the column and sequence row (and possibly annotation row when in
260 * wrapped mode) for the given mouse position
265 MousePos findMousePosition(MouseEvent evt)
267 int col = findColumn(evt);
272 int charHeight = av.getCharHeight();
273 int alignmentHeight = av.getAlignment().getHeight();
274 if (av.getWrapAlignment())
276 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
277 seqCanvas.getHeight());
280 * yPos modulo height of repeating width
282 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
285 * height of sequences plus space / scale above,
286 * plus gap between sequences and annotations
288 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
289 + alignmentHeight * charHeight
290 + SeqCanvas.SEQS_ANNOTATION_GAP;
291 if (yOffsetPx >= alignmentHeightPixels)
294 * mouse is over annotations; find annotation index, also set
295 * last sequence above (for backwards compatible behaviour)
297 AlignmentAnnotation[] anns = av.getAlignment()
298 .getAlignmentAnnotation();
299 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
300 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
301 seqIndex = alignmentHeight - 1;
306 * mouse is over sequence (or the space above sequences)
308 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
311 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
317 ViewportRanges ranges = av.getRanges();
318 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
319 alignmentHeight - 1);
320 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
323 return new MousePos(col, seqIndex, annIndex);
326 * Returns the aligned sequence position (base 0) at the mouse position, or
327 * the closest visible one
332 int findColumn(MouseEvent evt)
337 final int startRes = av.getRanges().getStartRes();
338 final int charWidth = av.getCharWidth();
340 if (av.getWrapAlignment())
342 int hgap = av.getCharHeight();
343 if (av.getScaleAboveWrapped())
345 hgap += av.getCharHeight();
348 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
349 + hgap + seqCanvas.getAnnotationHeight();
352 y = Math.max(0, y - hgap);
353 x -= seqCanvas.getLabelWidthWest();
356 // mouse is over left scale
360 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
365 if (x >= cwidth * charWidth)
367 // mouse is over right scale
371 wrappedBlock = y / cHeight;
372 wrappedBlock += startRes / cwidth;
373 // allow for wrapped view scrolled right (possible from Overview)
374 int startOffset = startRes % cwidth;
375 res = wrappedBlock * cwidth + startOffset
376 + Math.min(cwidth - 1, x / charWidth);
381 * make sure we calculate relative to visible alignment,
382 * rather than right-hand gutter
384 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
385 res = (x / charWidth) + startRes;
386 res = Math.min(res, av.getRanges().getEndRes());
389 if (av.hasHiddenColumns())
391 res = av.getAlignment().getHiddenColumns()
392 .visibleToAbsoluteColumn(res);
399 * When all of a sequence of edits are complete, put the resulting edit list
400 * on the history stack (undo list), and reset flags for editing in progress.
406 if (editCommand != null && editCommand.getSize() > 0)
408 ap.alignFrame.addHistoryItem(editCommand);
409 av.firePropertyChange("alignment", null,
410 av.getAlignment().getSequences());
415 * Tidy up come what may...
420 groupEditing = false;
429 seqCanvas.cursorY = getKeyboardNo1() - 1;
430 scrollToVisible(true);
433 void setCursorColumn()
435 seqCanvas.cursorX = getKeyboardNo1() - 1;
436 scrollToVisible(true);
439 void setCursorRowAndColumn()
441 if (keyboardNo2 == null)
443 keyboardNo2 = new StringBuffer();
447 seqCanvas.cursorX = getKeyboardNo1() - 1;
448 seqCanvas.cursorY = getKeyboardNo2() - 1;
449 scrollToVisible(true);
453 void setCursorPosition()
455 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
457 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
458 scrollToVisible(true);
461 void moveCursor(int dx, int dy)
463 moveCursor(dx, dy,false);
465 void moveCursor(int dx, int dy, boolean nextWord)
467 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
471 int maxWidth = av.getAlignment().getWidth();
472 int maxHeight=av.getAlignment().getHeight();
473 SequenceI seqAtRow = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
474 // look for next gap or residue
475 boolean isGap = Comparison.isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
476 int p = seqCanvas.cursorX,lastP,r=seqCanvas.cursorY,lastR;
492 seqAtRow = av.getAlignment().getSequenceAt(r);
494 p = nextVisible(hidden, maxWidth, p, dx);
495 } while ((dx != 0 ? p != lastP : r != lastR)
496 && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
500 int maxWidth = av.getAlignment().getWidth();
501 seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX, dx);
502 seqCanvas.cursorY += dy;
504 scrollToVisible(false);
507 private int nextVisible(HiddenColumns hidden,int maxWidth, int original, int dx)
509 int newCursorX=original+dx;
510 if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
512 int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
513 int[] region = hidden.getRegionWithEdgeAtRes(visx);
515 if (region != null) // just in case
520 newCursorX = region[1] + 1;
525 newCursorX = region[0] - 1;
529 newCursorX = (newCursorX < 0) ? 0 : newCursorX;
530 if (newCursorX >= maxWidth
531 || !hidden.isVisible(newCursorX))
533 newCursorX = original;
538 * Scroll to make the cursor visible in the viewport.
541 * just jump to the location rather than scrolling
543 void scrollToVisible(boolean jump)
545 if (seqCanvas.cursorX < 0)
547 seqCanvas.cursorX = 0;
549 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
551 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
554 if (seqCanvas.cursorY < 0)
556 seqCanvas.cursorY = 0;
558 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
560 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
565 boolean repaintNeeded = true;
568 // only need to repaint if the viewport did not move, as otherwise it will
570 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
575 if (av.getWrapAlignment())
577 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
578 int x = av.getAlignment().getHiddenColumns()
579 .absoluteToVisibleColumn(seqCanvas.cursorX);
580 av.getRanges().scrollToWrappedVisible(x);
584 av.getRanges().scrollToVisible(seqCanvas.cursorX,
589 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
591 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
592 seqCanvas.cursorX, seqCanvas.cursorY);
602 void setSelectionAreaAtCursor(boolean topLeft)
604 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
606 if (av.getSelectionGroup() != null)
608 SequenceGroup sg = av.getSelectionGroup();
609 // Find the top and bottom of this group
610 int min = av.getAlignment().getHeight(), max = 0;
611 for (int i = 0; i < sg.getSize(); i++)
613 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
628 sg.setStartRes(seqCanvas.cursorX);
629 if (sg.getEndRes() < seqCanvas.cursorX)
631 sg.setEndRes(seqCanvas.cursorX);
634 min = seqCanvas.cursorY;
638 sg.setEndRes(seqCanvas.cursorX);
639 if (sg.getStartRes() > seqCanvas.cursorX)
641 sg.setStartRes(seqCanvas.cursorX);
644 max = seqCanvas.cursorY + 1;
649 // Only the user can do this
650 av.setSelectionGroup(null);
654 // Now add any sequences between min and max
655 sg.getSequences(null).clear();
656 for (int i = min; i < max; i++)
658 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
663 if (av.getSelectionGroup() == null)
665 SequenceGroup sg = new SequenceGroup();
666 sg.setStartRes(seqCanvas.cursorX);
667 sg.setEndRes(seqCanvas.cursorX);
668 sg.addSequence(sequence, false);
669 av.setSelectionGroup(sg);
672 ap.paintAlignment(false, false);
676 void insertGapAtCursor(boolean group)
678 groupEditing = group;
679 editStartSeq = seqCanvas.cursorY;
680 editLastRes = seqCanvas.cursorX;
681 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
685 void deleteGapAtCursor(boolean group)
687 groupEditing = group;
688 editStartSeq = seqCanvas.cursorY;
689 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
690 editSequence(false, false, seqCanvas.cursorX);
694 void insertNucAtCursor(boolean group, String nuc)
696 // TODO not called - delete?
697 groupEditing = group;
698 editStartSeq = seqCanvas.cursorY;
699 editLastRes = seqCanvas.cursorX;
700 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
704 void numberPressed(char value)
706 if (keyboardNo1 == null)
708 keyboardNo1 = new StringBuffer();
711 if (keyboardNo2 != null)
713 keyboardNo2.append(value);
717 keyboardNo1.append(value);
725 if (keyboardNo1 != null)
727 int value = Integer.parseInt(keyboardNo1.toString());
731 } catch (Exception x)
742 if (keyboardNo2 != null)
744 int value = Integer.parseInt(keyboardNo2.toString());
748 } catch (Exception x)
762 public void mouseReleased(MouseEvent evt)
764 MousePos pos = findMousePosition(evt);
765 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
770 boolean didDrag = mouseDragging; // did we come here after a drag
771 mouseDragging = false;
772 mouseWheelPressed = false;
774 if (evt.isPopupTrigger()) // Windows: mouseReleased
776 showPopupMenu(evt, pos);
787 doMouseReleasedDefineMode(evt, didDrag);
798 public void mousePressed(MouseEvent evt)
800 lastMousePress = evt.getPoint();
801 MousePos pos = findMousePosition(evt);
802 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
807 if (SwingUtilities.isMiddleMouseButton(evt))
809 mouseWheelPressed = true;
813 boolean isControlDown = Platform.isControlDown(evt);
814 if (evt.isShiftDown() || isControlDown)
824 doMousePressedDefineMode(evt, pos);
828 int seq = pos.seqIndex;
829 int res = pos.column;
831 if ((seq < av.getAlignment().getHeight())
832 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
849 public void mouseOverSequence(SequenceI sequence, int index, int pos)
851 String tmp = sequence.hashCode() + " " + index + " " + pos;
853 if (lastMessage == null || !lastMessage.equals(tmp))
855 // System.err.println("mouseOver Sequence: "+tmp);
856 ssm.mouseOverSequence(sequence, index, pos, av);
862 * Highlight the mapped region described by the search results object (unless
863 * unchanged). This supports highlight of protein while mousing over linked
864 * cDNA and vice versa. The status bar is also updated to show the location of
865 * the start of the highlighted region.
868 public String highlightSequence(SearchResultsI results)
870 if (results == null || results.equals(lastSearchResults))
874 lastSearchResults = results;
876 boolean wasScrolled = false;
878 if (av.isFollowHighlight())
880 // don't allow highlight of protein/cDNA to also scroll a complementary
881 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
882 // over residue to change abruptly, causing highlighted residue in panel 2
883 // to change, causing a scroll in panel 1 etc)
884 ap.setToScrollComplementPanel(false);
885 wasScrolled = ap.scrollToPosition(results);
888 seqCanvas.revalidate();
890 ap.setToScrollComplementPanel(true);
893 boolean noFastPaint = wasScrolled && av.getWrapAlignment();
894 if (seqCanvas.highlightSearchResults(results, noFastPaint))
896 setStatusMessage(results);
898 // JAL-3303 feature suppressed for now pending review
899 return null; // results.isEmpty() ? null : getHighlightInfo(results);
903 * temporary hack: answers a message suitable to show on structure hover
904 * label. This is normally null. It is a peptide variation description if
906 * <li>results are a single residue in a protein alignment</li>
907 * <li>there is a mapping to a coding sequence (codon)</li>
908 * <li>there are one or more SNP variant features on the codon</li>
910 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
915 private String getHighlightInfo(SearchResultsI results)
918 * ideally, just find mapped CDS (as we don't care about render style here);
919 * for now, go via split frame complement's FeatureRenderer
921 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
922 if (complement == null)
926 AlignFrame af = Desktop.getAlignFrameFor(complement);
927 FeatureRendererModel fr2 = af.getFeatureRenderer();
929 List<SearchResultMatchI> matches = results.getResults();
930 int j = matches.size();
931 List<String> infos = new ArrayList<>();
932 for (int i = 0; i < j; i++)
934 SearchResultMatchI match = matches.get(i);
935 int pos = match.getStart();
936 if (pos == match.getEnd())
938 SequenceI seq = match.getSequence();
939 SequenceI ds = seq.getDatasetSequence() == null ? seq
940 : seq.getDatasetSequence();
941 MappedFeatures mf = fr2
942 .findComplementFeaturesAtResidue(ds, pos);
945 for (SequenceFeature sf : mf.features)
947 String pv = mf.findProteinVariants(sf);
948 if (pv.length() > 0 && !infos.contains(pv))
961 StringBuilder sb = new StringBuilder();
962 for (String info : infos)
970 return sb.toString();
974 public VamsasSource getVamsasSource()
976 return this.ap == null ? null : this.ap.av;
980 public void updateColours(SequenceI seq, int index)
982 System.out.println("update the seqPanel colours");
987 * Action on mouse movement is to update the status bar to show the current
988 * sequence position, and (if features are shown) to show any features at the
989 * position in a tooltip. Does nothing if the mouse move does not change
995 public void mouseMoved(MouseEvent evt)
999 // This is because MacOSX creates a mouseMoved
1000 // If control is down, other platforms will not.
1004 final MousePos mousePos = findMousePosition(evt);
1005 if (mousePos.equals(lastMousePosition))
1008 * just a pixel move without change of 'cell'
1012 lastMousePosition = mousePos;
1014 if (mousePos.isOverAnnotation())
1016 mouseMovedOverAnnotation(mousePos);
1019 final int seq = mousePos.seqIndex;
1021 final int column = mousePos.column;
1022 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1024 lastMousePosition = null;
1025 setToolTipText(null);
1027 ap.alignFrame.setStatus("");
1031 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1033 if (column >= sequence.getLength())
1039 * set status bar message, returning residue position in sequence
1041 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1042 final int pos = setStatusMessage(sequence, column, seq);
1043 if (ssm != null && !isGapped)
1045 mouseOverSequence(sequence, column, pos);
1048 tooltipText.setLength(6); // Cuts the buffer back to <html>
1050 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1053 for (int g = 0; g < groups.length; g++)
1055 if (groups[g].getStartRes() <= column
1056 && groups[g].getEndRes() >= column)
1058 if (!groups[g].getName().startsWith("JTreeGroup")
1059 && !groups[g].getName().startsWith("JGroup"))
1061 tooltipText.append(groups[g].getName());
1064 if (groups[g].getDescription() != null)
1066 tooltipText.append(": " + groups[g].getDescription());
1073 * add any features at the position to the tooltip; if over a gap, only
1074 * add features that straddle the gap (pos may be the residue before or
1077 int unshownFeatures = 0;
1078 if (av.isShowSequenceFeatures())
1080 List<SequenceFeature> features = ap.getFeatureRenderer()
1081 .findFeaturesAtColumn(sequence, column + 1);
1082 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1083 features, this.ap.getSeqPanel().seqCanvas.fr,
1084 MAX_TOOLTIP_LENGTH);
1087 * add features in CDS/protein complement at the corresponding
1088 * position if configured to do so
1090 if (av.isShowComplementFeatures())
1092 if (!Comparison.isGap(sequence.getCharAt(column)))
1094 AlignViewportI complement = ap.getAlignViewport()
1095 .getCodingComplement();
1096 AlignFrame af = Desktop.getAlignFrameFor(complement);
1097 FeatureRendererModel fr2 = af.getFeatureRenderer();
1098 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1102 unshownFeatures += seqARep.appendFeatures(tooltipText,
1103 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1108 if (tooltipText.length() == 6) // "<html>"
1110 setToolTipText(null);
1115 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1117 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1118 tooltipText.append("...");
1120 if (unshownFeatures > 0)
1122 tooltipText.append("<br/>").append("... ").append("<i>")
1123 .append(MessageManager.formatMessage(
1124 "label.features_not_shown", unshownFeatures))
1127 String textString = tooltipText.toString();
1128 if (lastTooltip == null || !lastTooltip.equals(textString))
1130 String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1132 setToolTipText(formattedTooltipText);
1133 lastTooltip = textString;
1139 * When the view is in wrapped mode, and the mouse is over an annotation row,
1140 * shows the corresponding tooltip and status message (if any)
1145 protected void mouseMovedOverAnnotation(MousePos pos)
1147 final int column = pos.column;
1148 final int rowIndex = pos.annotationIndex;
1150 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1155 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1157 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1159 setToolTipText(tooltip);
1160 lastTooltip = tooltip;
1162 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1164 ap.alignFrame.setStatus(msg);
1167 private Point lastp = null;
1172 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1175 public Point getToolTipLocation(MouseEvent event)
1177 if (tooltipText == null || tooltipText.length() <= 6)
1183 int x = event.getX();
1185 // switch sides when tooltip is too close to edge
1186 int wdth = (w - x < 200) ? -(w / 2) : 5;
1188 if (!event.isShiftDown() || p == null)
1190 p = new Point(event.getX() + wdth, event.getY() - 20);
1194 * TODO: try to set position so region is not obscured by tooltip
1202 * set when the current UI interaction has resulted in a change that requires
1203 * shading in overviews and structures to be recalculated. this could be
1204 * changed to a something more expressive that indicates what actually has
1205 * changed, so selective redraws can be applied (ie. only structures, only
1208 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1211 * set if av.getSelectionGroup() refers to a group that is defined on the
1212 * alignment view, rather than a transient selection
1214 // private boolean editingDefinedGroup = false; // TODO: refactor to
1215 // avcontroller or viewModel
1218 * Sets the status message in alignment panel, showing the sequence number
1219 * (index) and id, and residue and residue position if not at a gap, for the
1220 * given sequence and column position. Returns the residue position returned
1221 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1222 * if at a gapped position.
1225 * aligned sequence object
1229 * index of sequence in alignment
1230 * @return sequence position of residue at column, or adjacent residue if at a
1233 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1235 char sequenceChar = sequence.getCharAt(column);
1236 int pos = sequence.findPosition(column);
1237 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1243 * Builds the status message for the current cursor location and writes it to
1244 * the status bar, for example
1247 * Sequence 3 ID: FER1_SOLLC
1248 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1249 * Sequence 5 ID: FER1_PEA Residue: B (3)
1250 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1255 * sequence position in the alignment (1..)
1256 * @param sequenceChar
1257 * the character under the cursor
1259 * the sequence residue position (if not over a gap)
1261 protected void setStatusMessage(String seqName, int seqIndex,
1262 char sequenceChar, int residuePos)
1264 StringBuilder text = new StringBuilder(32);
1267 * Sequence number (if known), and sequence name.
1269 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1270 text.append("Sequence").append(seqno).append(" ID: ")
1273 String residue = null;
1276 * Try to translate the display character to residue name (null for gap).
1278 boolean isGapped = Comparison.isGap(sequenceChar);
1282 boolean nucleotide = av.getAlignment().isNucleotide();
1283 String displayChar = String.valueOf(sequenceChar);
1286 residue = ResidueProperties.nucleotideName.get(displayChar);
1290 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1291 : ("*".equals(displayChar) ? "STOP"
1292 : ResidueProperties.aa2Triplet.get(displayChar));
1294 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1295 .append(": ").append(residue == null ? displayChar : residue);
1297 text.append(" (").append(Integer.toString(residuePos)).append(")");
1299 ap.alignFrame.setStatus(text.toString());
1303 * Set the status bar message to highlight the first matched position in
1308 private void setStatusMessage(SearchResultsI results)
1310 AlignmentI al = this.av.getAlignment();
1311 int sequenceIndex = al.findIndex(results);
1312 if (sequenceIndex == -1)
1316 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1317 SequenceI ds = alignedSeq.getDatasetSequence();
1318 for (SearchResultMatchI m : results.getResults())
1320 SequenceI seq = m.getSequence();
1321 if (seq.getDatasetSequence() != null)
1323 seq = seq.getDatasetSequence();
1328 int start = m.getStart();
1329 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1330 seq.getCharAt(start - 1), start);
1340 public void mouseDragged(MouseEvent evt)
1342 MousePos pos = findMousePosition(evt);
1343 if (pos.isOverAnnotation() || pos.column == -1)
1348 if (mouseWheelPressed)
1350 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1351 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1353 int oldWidth = av.getCharWidth();
1355 // Which is bigger, left-right or up-down?
1356 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1357 .abs(evt.getX() - lastMousePress.getX()))
1360 * on drag up or down, decrement or increment font size
1362 int fontSize = av.font.getSize();
1363 boolean fontChanged = false;
1365 if (evt.getY() < lastMousePress.getY())
1370 else if (evt.getY() > lastMousePress.getY())
1383 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1385 av.setFont(newFont, true);
1386 av.setCharWidth(oldWidth);
1390 ap.av.getCodingComplement().setFont(newFont, true);
1391 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1392 .getSplitViewContainer();
1393 splitFrame.adjustLayout();
1394 splitFrame.repaint();
1401 * on drag left or right, decrement or increment character width
1404 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1406 newWidth = av.getCharWidth() - 1;
1407 av.setCharWidth(newWidth);
1409 else if (evt.getX() > lastMousePress.getX())
1411 newWidth = av.getCharWidth() + 1;
1412 av.setCharWidth(newWidth);
1416 ap.paintAlignment(false, false);
1420 * need to ensure newWidth is set on cdna, regardless of which
1421 * panel the mouse drag happened in; protein will compute its
1422 * character width as 1:1 or 3:1
1424 av.getCodingComplement().setCharWidth(newWidth);
1425 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1426 .getSplitViewContainer();
1427 splitFrame.adjustLayout();
1428 splitFrame.repaint();
1433 FontMetrics fm = getFontMetrics(av.getFont());
1434 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1436 lastMousePress = evt.getPoint();
1443 dragStretchGroup(evt);
1447 int res = pos.column;
1454 if ((editLastRes == -1) || (editLastRes == res))
1459 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1461 // dragLeft, delete gap
1462 editSequence(false, false, res);
1466 editSequence(true, false, res);
1469 mouseDragging = true;
1470 if (scrollThread != null)
1472 scrollThread.setMousePosition(evt.getPoint());
1477 * Edits the sequence to insert or delete one or more gaps, in response to a
1478 * mouse drag or cursor mode command. The number of inserts/deletes may be
1479 * specified with the cursor command, or else depends on the mouse event
1480 * (normally one column, but potentially more for a fast mouse drag).
1482 * Delete gaps is limited to the number of gaps left of the cursor position
1483 * (mouse drag), or at or right of the cursor position (cursor mode).
1485 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1486 * the current selection group.
1488 * In locked editing mode (with a selection group present), inserts/deletions
1489 * within the selection group are limited to its boundaries (and edits outside
1490 * the group stop at its border).
1493 * true to insert gaps, false to delete gaps
1495 * (unused parameter)
1497 * the column at which to perform the action; the number of columns
1498 * affected depends on <code>this.editLastRes</code> (cursor column
1501 synchronized void editSequence(boolean insertGap, boolean editSeq,
1505 int fixedRight = -1;
1506 boolean fixedColumns = false;
1507 SequenceGroup sg = av.getSelectionGroup();
1509 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1511 // No group, but the sequence may represent a group
1512 if (!groupEditing && av.hasHiddenRows())
1514 if (av.isHiddenRepSequence(seq))
1516 sg = av.getRepresentedSequences(seq);
1517 groupEditing = true;
1521 StringBuilder message = new StringBuilder(64); // for status bar
1524 * make a name for the edit action, for
1525 * status bar message and Undo/Redo menu
1527 String label = null;
1530 message.append("Edit group:");
1531 label = MessageManager.getString("action.edit_group");
1535 message.append("Edit sequence: " + seq.getName());
1536 label = seq.getName();
1537 if (label.length() > 10)
1539 label = label.substring(0, 10);
1541 label = MessageManager.formatMessage("label.edit_params",
1547 * initialise the edit command if there is not
1548 * already one being extended
1550 if (editCommand == null)
1552 editCommand = new EditCommand(label);
1557 message.append(" insert ");
1561 message.append(" delete ");
1564 message.append(Math.abs(startres - editLastRes) + " gaps.");
1565 ap.alignFrame.setStatus(message.toString());
1568 * is there a selection group containing the sequence being edited?
1569 * if so the boundary of the group is the limit of the edit
1570 * (but the edit may be inside or outside the selection group)
1572 boolean inSelectionGroup = sg != null
1573 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1574 if (groupEditing || inSelectionGroup)
1576 fixedColumns = true;
1578 // sg might be null as the user may only see 1 sequence,
1579 // but the sequence represents a group
1582 if (!av.isHiddenRepSequence(seq))
1587 sg = av.getRepresentedSequences(seq);
1590 fixedLeft = sg.getStartRes();
1591 fixedRight = sg.getEndRes();
1593 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1594 || (startres >= fixedLeft && editLastRes < fixedLeft)
1595 || (startres > fixedRight && editLastRes <= fixedRight)
1596 || (startres <= fixedRight && editLastRes > fixedRight))
1602 if (fixedLeft > startres)
1604 fixedRight = fixedLeft - 1;
1607 else if (fixedRight < startres)
1609 fixedLeft = fixedRight;
1614 if (av.hasHiddenColumns())
1616 fixedColumns = true;
1617 int y1 = av.getAlignment().getHiddenColumns()
1618 .getNextHiddenBoundary(true, startres);
1619 int y2 = av.getAlignment().getHiddenColumns()
1620 .getNextHiddenBoundary(false, startres);
1622 if ((insertGap && startres > y1 && editLastRes < y1)
1623 || (!insertGap && startres < y2 && editLastRes > y2))
1629 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1630 // Selection spans a hidden region
1631 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1639 fixedRight = y2 - 1;
1644 boolean success = doEditSequence(insertGap, editSeq, startres,
1645 fixedRight, fixedColumns, sg);
1648 * report what actually happened (might be less than
1649 * what was requested), by inspecting the edit commands added
1651 String msg = getEditStatusMessage(editCommand);
1652 ap.alignFrame.setStatus(msg == null ? " " : msg);
1658 editLastRes = startres;
1659 seqCanvas.repaint();
1663 * A helper method that performs the requested editing to insert or delete
1664 * gaps (if possible). Answers true if the edit was successful, false if could
1665 * only be performed in part or not at all. Failure may occur in 'locked edit'
1666 * mode, when an insertion requires a matching gapped position (or column) to
1667 * delete, and deletion requires an adjacent gapped position (or column) to
1671 * true if inserting gap(s), false if deleting
1673 * (unused parameter, currently always false)
1675 * the column at which to perform the edit
1677 * fixed right boundary column of a locked edit (within or to the
1678 * left of a selection group)
1679 * @param fixedColumns
1680 * true if this is a locked edit
1682 * the sequence group (if group edit is being performed)
1685 protected boolean doEditSequence(final boolean insertGap,
1686 final boolean editSeq, final int startres, int fixedRight,
1687 final boolean fixedColumns, final SequenceGroup sg)
1689 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1690 SequenceI[] seqs = new SequenceI[] { seq };
1694 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1695 int g, groupSize = vseqs.size();
1696 SequenceI[] groupSeqs = new SequenceI[groupSize];
1697 for (g = 0; g < groupSeqs.length; g++)
1699 groupSeqs[g] = vseqs.get(g);
1705 // If the user has selected the whole sequence, and is dragging to
1706 // the right, we can still extend the alignment and selectionGroup
1707 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1708 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1711 av.getAlignment().getWidth() + startres - editLastRes);
1712 fixedRight = sg.getEndRes();
1715 // Is it valid with fixed columns??
1716 // Find the next gap before the end
1717 // of the visible region boundary
1718 boolean blank = false;
1719 for (; fixedRight > editLastRes; fixedRight--)
1723 for (g = 0; g < groupSize; g++)
1725 for (int j = 0; j < startres - editLastRes; j++)
1728 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1743 if (sg.getSize() == av.getAlignment().getHeight())
1745 if ((av.hasHiddenColumns()
1746 && startres < av.getAlignment().getHiddenColumns()
1747 .getNextHiddenBoundary(false, startres)))
1752 int alWidth = av.getAlignment().getWidth();
1753 if (av.hasHiddenRows())
1755 int hwidth = av.getAlignment().getHiddenSequences()
1757 if (hwidth > alWidth)
1762 // We can still insert gaps if the selectionGroup
1763 // contains all the sequences
1764 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1765 fixedRight = alWidth + startres - editLastRes;
1775 else if (!insertGap)
1777 // / Are we able to delete?
1778 // ie are all columns blank?
1780 for (g = 0; g < groupSize; g++)
1782 for (int j = startres; j < editLastRes; j++)
1784 if (groupSeqs[g].getLength() <= j)
1789 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1791 // Not a gap, block edit not valid
1800 // dragging to the right
1801 if (fixedColumns && fixedRight != -1)
1803 for (int j = editLastRes; j < startres; j++)
1805 insertGap(j, groupSeqs, fixedRight);
1810 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1811 startres - editLastRes, false);
1816 // dragging to the left
1817 if (fixedColumns && fixedRight != -1)
1819 for (int j = editLastRes; j > startres; j--)
1821 deleteChar(startres, groupSeqs, fixedRight);
1826 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1827 editLastRes - startres, false);
1834 * editing a single sequence
1838 // dragging to the right
1839 if (fixedColumns && fixedRight != -1)
1841 for (int j = editLastRes; j < startres; j++)
1843 if (!insertGap(j, seqs, fixedRight))
1846 * e.g. cursor mode command specified
1847 * more inserts than are possible
1855 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1856 startres - editLastRes, false);
1863 // dragging to the left
1864 if (fixedColumns && fixedRight != -1)
1866 for (int j = editLastRes; j > startres; j--)
1868 if (!Comparison.isGap(seq.getCharAt(startres)))
1872 deleteChar(startres, seqs, fixedRight);
1877 // could be a keyboard edit trying to delete none gaps
1879 for (int m = startres; m < editLastRes; m++)
1881 if (!Comparison.isGap(seq.getCharAt(m)))
1889 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1894 {// insertGap==false AND editSeq==TRUE;
1895 if (fixedColumns && fixedRight != -1)
1897 for (int j = editLastRes; j < startres; j++)
1899 insertGap(j, seqs, fixedRight);
1904 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1905 startres - editLastRes, false);
1915 * Constructs an informative status bar message while dragging to insert or
1916 * delete gaps. Answers null if inserts and deletes cancel out.
1918 * @param editCommand
1919 * a command containing the list of individual edits
1922 protected static String getEditStatusMessage(EditCommand editCommand)
1924 if (editCommand == null)
1930 * add any inserts, and subtract any deletes,
1931 * not counting those auto-inserted when doing a 'locked edit'
1932 * (so only counting edits 'under the cursor')
1935 for (Edit cmd : editCommand.getEdits())
1937 if (!cmd.isSystemGenerated())
1939 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1947 * inserts and deletes cancel out
1952 String msgKey = count > 1 ? "label.insert_gaps"
1953 : (count == 1 ? "label.insert_gap"
1954 : (count == -1 ? "label.delete_gap"
1955 : "label.delete_gaps"));
1956 count = Math.abs(count);
1958 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1962 * Inserts one gap at column j, deleting the right-most gapped column up to
1963 * (and including) fixedColumn. Returns true if the edit is successful, false
1964 * if no blank column is available to allow the insertion to be balanced by a
1969 * @param fixedColumn
1972 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1974 int blankColumn = fixedColumn;
1975 for (int s = 0; s < seq.length; s++)
1977 // Find the next gap before the end of the visible region boundary
1978 // If lastCol > j, theres a boundary after the gap insertion
1980 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1982 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1984 // Theres a space, so break and insert the gap
1989 if (blankColumn <= j)
1991 blankColumn = fixedColumn;
1997 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1999 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2005 * Helper method to add and perform one edit action
2011 * @param systemGenerated
2012 * true if the edit is a 'balancing' delete (or insert) to match a
2013 * user's insert (or delete) in a locked editing region
2015 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2016 int count, boolean systemGenerated)
2019 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2020 av.getAlignment().getGapCharacter());
2021 edit.setSystemGenerated(systemGenerated);
2023 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2027 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2028 * each of the given sequences. The caller should ensure that all sequences
2029 * are gapped in column j.
2033 * @param fixedColumn
2035 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2037 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2039 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2043 * On reentering the panel, stops any scrolling that was started on dragging
2049 public void mouseEntered(MouseEvent e)
2059 * On leaving the panel, if the mouse is being dragged, starts a thread to
2060 * scroll it until the mouse is released (in unwrapped mode only)
2065 public void mouseExited(MouseEvent e)
2067 lastMousePosition = null;
2068 ap.alignFrame.setStatus(" ");
2069 if (av.getWrapAlignment())
2074 if (mouseDragging && scrollThread == null)
2076 scrollThread = new ScrollThread();
2081 * Handler for double-click on a position with one or more sequence features.
2082 * Opens the Amend Features dialog to allow feature details to be amended, or
2083 * the feature deleted.
2086 public void mouseClicked(MouseEvent evt)
2088 SequenceGroup sg = null;
2089 MousePos pos = findMousePosition(evt);
2090 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2095 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2097 sg = av.getSelectionGroup();
2098 if (sg != null && sg.getSize() == 1
2099 && sg.getEndRes() - sg.getStartRes() < 2)
2101 av.setSelectionGroup(null);
2104 int column = pos.column;
2107 * find features at the position (if not gapped), or straddling
2108 * the position (if at a gap)
2110 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2111 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2112 .findFeaturesAtColumn(sequence, column + 1);
2114 if (!features.isEmpty())
2117 * highlight the first feature at the position on the alignment
2119 SearchResultsI highlight = new SearchResults();
2120 highlight.addResult(sequence, features.get(0).getBegin(), features
2122 seqCanvas.highlightSearchResults(highlight, false);
2125 * open the Amend Features dialog; clear highlighting afterwards,
2126 * whether changes were made or not
2128 List<SequenceI> seqs = Collections.singletonList(sequence);
2129 seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2131 av.setSearchResults(null); // clear highlighting
2132 seqCanvas.repaint(); // draw new/amended features
2138 public void mouseWheelMoved(MouseWheelEvent e)
2141 double wheelRotation = e.getPreciseWheelRotation();
2142 if (wheelRotation > 0)
2144 if (e.isShiftDown())
2146 av.getRanges().scrollRight(true);
2151 av.getRanges().scrollUp(false);
2154 else if (wheelRotation < 0)
2156 if (e.isShiftDown())
2158 av.getRanges().scrollRight(false);
2162 av.getRanges().scrollUp(true);
2167 * update status bar and tooltip for new position
2168 * (need to synthesize a mouse movement to refresh tooltip)
2171 ToolTipManager.sharedInstance().mouseMoved(e);
2180 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2182 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2187 final int res = pos.column;
2188 final int seq = pos.seqIndex;
2190 updateOverviewAndStructs = false;
2192 startWrapBlock = wrappedBlock;
2194 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2196 if ((sequence == null) || (res > sequence.getLength()))
2201 stretchGroup = av.getSelectionGroup();
2203 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2205 stretchGroup = av.getAlignment().findGroup(sequence, res);
2206 if (stretchGroup != null)
2208 // only update the current selection if the popup menu has a group to
2210 av.setSelectionGroup(stretchGroup);
2214 if (evt.isPopupTrigger()) // Mac: mousePressed
2216 showPopupMenu(evt, pos);
2221 * defer right-mouse click handling to mouseReleased on Windows
2222 * (where isPopupTrigger() will answer true)
2223 * NB isRightMouseButton is also true for Cmd-click on Mac
2225 if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2232 seqCanvas.cursorX = res;
2233 seqCanvas.cursorY = seq;
2234 seqCanvas.repaint();
2238 if (stretchGroup == null)
2240 createStretchGroup(res, sequence);
2243 if (stretchGroup != null)
2245 stretchGroup.addPropertyChangeListener(seqCanvas);
2248 seqCanvas.repaint();
2251 private void createStretchGroup(int res, SequenceI sequence)
2253 // Only if left mouse button do we want to change group sizes
2254 // define a new group here
2255 SequenceGroup sg = new SequenceGroup();
2256 sg.setStartRes(res);
2258 sg.addSequence(sequence, false);
2259 av.setSelectionGroup(sg);
2262 if (av.getConservationSelected())
2264 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2268 if (av.getAbovePIDThreshold())
2270 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2273 // TODO: stretchGroup will always be not null. Is this a merge error ?
2274 // or is there a threading issue here?
2275 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2277 // Edit end res position of selected group
2278 changeEndRes = true;
2280 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2282 // Edit end res position of selected group
2283 changeStartRes = true;
2285 stretchGroup.getWidth();
2290 * Build and show a pop-up menu at the right-click mouse position
2295 void showPopupMenu(MouseEvent evt, MousePos pos)
2297 final int column = pos.column;
2298 final int seq = pos.seqIndex;
2299 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2300 if (sequence != null)
2302 PopupMenu pop = new PopupMenu(ap, sequence, column);
2303 pop.show(this, evt.getX(), evt.getY());
2308 * Update the display after mouse up on a selection or group
2311 * mouse released event details
2313 * true if this event is happening after a mouse drag (rather than a
2316 protected void doMouseReleasedDefineMode(MouseEvent evt,
2319 if (stretchGroup == null)
2324 stretchGroup.removePropertyChangeListener(seqCanvas);
2326 // always do this - annotation has own state
2327 // but defer colourscheme update until hidden sequences are passed in
2328 boolean vischange = stretchGroup.recalcConservation(true);
2329 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2331 if (stretchGroup.cs != null)
2335 stretchGroup.cs.alignmentChanged(stretchGroup,
2336 av.getHiddenRepSequences());
2339 ResidueShaderI groupColourScheme = stretchGroup
2340 .getGroupColourScheme();
2341 String name = stretchGroup.getName();
2342 if (stretchGroup.cs.conservationApplied())
2344 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2346 if (stretchGroup.cs.getThreshold() > 0)
2348 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2351 PaintRefresher.Refresh(this, av.getSequenceSetId());
2352 // TODO: structure colours only need updating if stretchGroup used to or now
2353 // does contain sequences with structure views
2354 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2355 updateOverviewAndStructs = false;
2356 changeEndRes = false;
2357 changeStartRes = false;
2358 stretchGroup = null;
2363 * Resizes the borders of a selection group depending on the direction of
2368 protected void dragStretchGroup(MouseEvent evt)
2370 if (stretchGroup == null)
2375 MousePos pos = findMousePosition(evt);
2376 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2381 int res = pos.column;
2382 int y = pos.seqIndex;
2384 if (wrappedBlock != startWrapBlock)
2389 res = Math.min(res, av.getAlignment().getWidth()-1);
2391 if (stretchGroup.getEndRes() == res)
2393 // Edit end res position of selected group
2394 changeEndRes = true;
2396 else if (stretchGroup.getStartRes() == res)
2398 // Edit start res position of selected group
2399 changeStartRes = true;
2402 if (res < av.getRanges().getStartRes())
2404 res = av.getRanges().getStartRes();
2409 if (res > (stretchGroup.getStartRes() - 1))
2411 stretchGroup.setEndRes(res);
2412 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2415 else if (changeStartRes)
2417 if (res < (stretchGroup.getEndRes() + 1))
2419 stretchGroup.setStartRes(res);
2420 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2424 int dragDirection = 0;
2430 else if (y < oldSeq)
2435 while ((y != oldSeq) && (oldSeq > -1)
2436 && (y < av.getAlignment().getHeight()))
2438 // This routine ensures we don't skip any sequences, as the
2439 // selection is quite slow.
2440 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2442 oldSeq += dragDirection;
2449 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2451 if (stretchGroup.getSequences(null).contains(nextSeq))
2453 stretchGroup.deleteSequence(seq, false);
2454 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2460 stretchGroup.addSequence(seq, false);
2463 stretchGroup.addSequence(nextSeq, false);
2464 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2473 mouseDragging = true;
2475 if (scrollThread != null)
2477 scrollThread.setMousePosition(evt.getPoint());
2481 * construct a status message showing the range of the selection
2483 StringBuilder status = new StringBuilder(64);
2484 List<SequenceI> seqs = stretchGroup.getSequences();
2485 String name = seqs.get(0).getName();
2486 if (name.length() > 20)
2488 name = name.substring(0, 20);
2490 status.append(name).append(" - ");
2491 name = seqs.get(seqs.size() - 1).getName();
2492 if (name.length() > 20)
2494 name = name.substring(0, 20);
2496 status.append(name).append(" ");
2497 int startRes = stretchGroup.getStartRes();
2498 status.append(" cols ").append(String.valueOf(startRes + 1))
2500 int endRes = stretchGroup.getEndRes();
2501 status.append(String.valueOf(endRes + 1));
2502 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2503 .append(String.valueOf(endRes - startRes + 1)).append(")");
2504 ap.alignFrame.setStatus(status.toString());
2508 * Stops the scroll thread if it is running
2510 void stopScrolling()
2512 if (scrollThread != null)
2514 scrollThread.stopScrolling();
2515 scrollThread = null;
2517 mouseDragging = false;
2521 * Starts a thread to scroll the alignment, towards a given mouse position
2522 * outside the panel bounds
2526 void startScrolling(Point mousePos)
2528 if (scrollThread == null)
2530 scrollThread = new ScrollThread();
2533 mouseDragging = true;
2534 scrollThread.setMousePosition(mousePos);
2538 * Performs scrolling of the visible alignment left, right, up or down
2540 class ScrollThread extends Thread
2542 private Point mousePos;
2544 private volatile boolean threadRunning = true;
2549 public ScrollThread()
2551 setName("SeqPanel$ScrollThread");
2556 * Sets the position of the mouse that determines the direction of the
2561 public void setMousePosition(Point p)
2567 * Sets a flag that will cause the thread to exit
2569 public void stopScrolling()
2571 threadRunning = false;
2575 * Scrolls the alignment left or right, and/or up or down, depending on the
2576 * last notified mouse position, until the limit of the alignment is
2577 * reached, or a flag is set to stop the scroll
2582 while (threadRunning && mouseDragging)
2584 if (mousePos != null)
2586 boolean scrolled = false;
2587 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2594 // mouse is above this panel - try scroll up
2595 scrolled = ranges.scrollUp(true);
2597 else if (mousePos.y >= getHeight())
2599 // mouse is below this panel - try scroll down
2600 scrolled = ranges.scrollUp(false);
2604 * scroll left or right
2608 scrolled |= ranges.scrollRight(false);
2610 else if (mousePos.x >= getWidth())
2612 scrolled |= ranges.scrollRight(true);
2617 * we have reached the limit of the visible alignment - quit
2619 threadRunning = false;
2620 SeqPanel.this.ap.repaint();
2627 } catch (Exception ex)
2635 * modify current selection according to a received message.
2638 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2639 HiddenColumns hidden, SelectionSource source)
2641 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2642 // handles selection messages...
2643 // TODO: extend config options to allow user to control if selections may be
2644 // shared between viewports.
2645 boolean iSentTheSelection = (av == source
2646 || (source instanceof AlignViewport
2647 && ((AlignmentViewport) source).getSequenceSetId()
2648 .equals(av.getSequenceSetId())));
2650 if (iSentTheSelection)
2652 // respond to our own event by updating dependent dialogs
2653 if (ap.getCalculationDialog() != null)
2655 ap.getCalculationDialog().validateCalcTypes();
2661 // process further ?
2662 if (!av.followSelection)
2668 * Ignore the selection if there is one of our own pending.
2670 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2676 * Check for selection in a view of which this one is a dna/protein
2679 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2684 // do we want to thread this ? (contention with seqsel and colsel locks, I
2687 * only copy colsel if there is a real intersection between
2688 * sequence selection and this panel's alignment
2690 boolean repaint = false;
2691 boolean copycolsel = false;
2693 SequenceGroup sgroup = null;
2694 if (seqsel != null && seqsel.getSize() > 0)
2696 if (av.getAlignment() == null)
2698 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2699 + " ViewId=" + av.getViewId()
2700 + " 's alignment is NULL! returning immediately.");
2703 sgroup = seqsel.intersect(av.getAlignment(),
2704 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2705 if ((sgroup != null && sgroup.getSize() > 0))
2710 if (sgroup != null && sgroup.getSize() > 0)
2712 av.setSelectionGroup(sgroup);
2716 av.setSelectionGroup(null);
2718 av.isSelectionGroupChanged(true);
2723 // the current selection is unset or from a previous message
2724 // so import the new colsel.
2725 if (colsel == null || colsel.isEmpty())
2727 if (av.getColumnSelection() != null)
2729 av.getColumnSelection().clear();
2735 // TODO: shift colSel according to the intersecting sequences
2736 if (av.getColumnSelection() == null)
2738 av.setColumnSelection(new ColumnSelection(colsel));
2742 av.getColumnSelection().setElementsFrom(colsel,
2743 av.getAlignment().getHiddenColumns());
2746 av.isColSelChanged(true);
2750 if (copycolsel && av.hasHiddenColumns()
2751 && (av.getAlignment().getHiddenColumns() == null))
2753 System.err.println("Bad things");
2755 if (repaint) // always true!
2757 // probably finessing with multiple redraws here
2758 PaintRefresher.Refresh(this, av.getSequenceSetId());
2759 // ap.paintAlignment(false);
2762 // lastly, update dependent dialogs
2763 if (ap.getCalculationDialog() != null)
2765 ap.getCalculationDialog().validateCalcTypes();
2771 * If this panel is a cdna/protein translation view of the selection source,
2772 * tries to map the source selection to a local one, and returns true. Else
2779 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2780 ColumnSelection colsel, HiddenColumns hidden,
2781 SelectionSource source)
2783 if (!(source instanceof AlignViewportI))
2787 final AlignViewportI sourceAv = (AlignViewportI) source;
2788 if (sourceAv.getCodingComplement() != av
2789 && av.getCodingComplement() != sourceAv)
2795 * Map sequence selection
2797 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2798 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2799 av.isSelectionGroupChanged(true);
2802 * Map column selection
2804 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2806 ColumnSelection cs = new ColumnSelection();
2807 HiddenColumns hs = new HiddenColumns();
2808 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2809 av.setColumnSelection(cs);
2810 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2812 // lastly, update any dependent dialogs
2813 if (ap.getCalculationDialog() != null)
2815 ap.getCalculationDialog().validateCalcTypes();
2819 * repaint alignment, and also Overview or Structure
2820 * if hidden column selection has changed
2822 ap.paintAlignment(hiddenChanged, hiddenChanged);
2829 * @return null or last search results handled by this panel
2831 public SearchResultsI getLastSearchResults()
2833 return lastSearchResults;