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 jalview.api.AlignViewportI;
24 import jalview.bin.Cache;
25 import jalview.commands.EditCommand;
26 import jalview.commands.EditCommand.Action;
27 import jalview.commands.EditCommand.Edit;
28 import jalview.datamodel.AlignmentAnnotation;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.ColumnSelection;
31 import jalview.datamodel.HiddenColumns;
32 import jalview.datamodel.MappedFeatures;
33 import jalview.datamodel.SearchResultMatchI;
34 import jalview.datamodel.SearchResults;
35 import jalview.datamodel.SearchResultsI;
36 import jalview.datamodel.Sequence;
37 import jalview.datamodel.SequenceFeature;
38 import jalview.datamodel.SequenceGroup;
39 import jalview.datamodel.SequenceI;
40 import jalview.io.SequenceAnnotationReport;
41 import jalview.renderer.ResidueShaderI;
42 import jalview.schemes.ResidueProperties;
43 import jalview.structure.SelectionListener;
44 import jalview.structure.SelectionSource;
45 import jalview.structure.SequenceListener;
46 import jalview.structure.StructureSelectionManager;
47 import jalview.structure.VamsasSource;
48 import jalview.util.Comparison;
49 import jalview.util.MappingUtils;
50 import jalview.util.MessageManager;
51 import jalview.util.Platform;
52 import jalview.viewmodel.AlignmentViewport;
53 import jalview.viewmodel.ViewportRanges;
54 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
56 import java.awt.BorderLayout;
57 import java.awt.Color;
59 import java.awt.FontMetrics;
60 import java.awt.Point;
61 import java.awt.event.MouseEvent;
62 import java.awt.event.MouseListener;
63 import java.awt.event.MouseMotionListener;
64 import java.awt.event.MouseWheelEvent;
65 import java.awt.event.MouseWheelListener;
66 import java.util.ArrayList;
67 import java.util.Collections;
68 import java.util.List;
70 import javax.swing.JPanel;
71 import javax.swing.SwingUtilities;
72 import javax.swing.ToolTipManager;
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 java.net.URL linkImageURL;
212 private final SequenceAnnotationReport seqARep;
214 StringBuilder tooltipText = new StringBuilder();
218 EditCommand editCommand;
220 StructureSelectionManager ssm;
222 SearchResultsI lastSearchResults;
225 * Creates a new SeqPanel object
230 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
232 linkImageURL = getClass().getResource("/images/link.gif");
233 seqARep = new SequenceAnnotationReport(linkImageURL.toString());
234 ToolTipManager.sharedInstance().registerComponent(this);
235 ToolTipManager.sharedInstance().setInitialDelay(0);
236 ToolTipManager.sharedInstance().setDismissDelay(10000);
238 setBackground(Color.white);
240 seqCanvas = new SeqCanvas(alignPanel);
241 setLayout(new BorderLayout());
242 add(seqCanvas, BorderLayout.CENTER);
244 this.ap = alignPanel;
246 if (!viewport.isDataset())
248 addMouseMotionListener(this);
249 addMouseListener(this);
250 addMouseWheelListener(this);
251 ssm = viewport.getStructureSelectionManager();
252 ssm.addStructureViewerListener(this);
253 ssm.addSelectionListener(this);
257 int startWrapBlock = -1;
259 int wrappedBlock = -1;
262 * Computes the column and sequence row (and possibly annotation row when in
263 * wrapped mode) for the given mouse position
268 MousePos findMousePosition(MouseEvent evt)
270 int col = findColumn(evt);
275 int charHeight = av.getCharHeight();
276 int alignmentHeight = av.getAlignment().getHeight();
277 if (av.getWrapAlignment())
279 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
280 seqCanvas.getHeight());
283 * yPos modulo height of repeating width
285 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
288 * height of sequences plus space / scale above,
289 * plus gap between sequences and annotations
291 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
292 + alignmentHeight * charHeight
293 + SeqCanvas.SEQS_ANNOTATION_GAP;
294 if (yOffsetPx >= alignmentHeightPixels)
297 * mouse is over annotations; find annotation index, also set
298 * last sequence above (for backwards compatible behaviour)
300 AlignmentAnnotation[] anns = av.getAlignment()
301 .getAlignmentAnnotation();
302 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
303 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
304 seqIndex = alignmentHeight - 1;
309 * mouse is over sequence (or the space above sequences)
311 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
314 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
320 ViewportRanges ranges = av.getRanges();
321 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
322 alignmentHeight - 1);
323 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
326 return new MousePos(col, seqIndex, annIndex);
329 * Returns the aligned sequence position (base 0) at the mouse position, or
330 * the closest visible one
335 int findColumn(MouseEvent evt)
340 final int startRes = av.getRanges().getStartRes();
341 final int charWidth = av.getCharWidth();
343 if (av.getWrapAlignment())
345 int hgap = av.getCharHeight();
346 if (av.getScaleAboveWrapped())
348 hgap += av.getCharHeight();
351 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
352 + hgap + seqCanvas.getAnnotationHeight();
355 y = Math.max(0, y - hgap);
356 x -= seqCanvas.getLabelWidthWest();
359 // mouse is over left scale
363 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
368 if (x >= cwidth * charWidth)
370 // mouse is over right scale
374 wrappedBlock = y / cHeight;
375 wrappedBlock += startRes / cwidth;
376 // allow for wrapped view scrolled right (possible from Overview)
377 int startOffset = startRes % cwidth;
378 res = wrappedBlock * cwidth + startOffset
379 + Math.min(cwidth - 1, x / charWidth);
384 * make sure we calculate relative to visible alignment,
385 * rather than right-hand gutter
387 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
388 res = (x / charWidth) + startRes;
389 res = Math.min(res, av.getRanges().getEndRes());
392 if (av.hasHiddenColumns())
394 res = av.getAlignment().getHiddenColumns()
395 .visibleToAbsoluteColumn(res);
402 * When all of a sequence of edits are complete, put the resulting edit list
403 * on the history stack (undo list), and reset flags for editing in progress.
409 if (editCommand != null && editCommand.getSize() > 0)
411 ap.alignFrame.addHistoryItem(editCommand);
412 av.firePropertyChange("alignment", null,
413 av.getAlignment().getSequences());
418 * Tidy up come what may...
423 groupEditing = false;
432 seqCanvas.cursorY = getKeyboardNo1() - 1;
433 scrollToVisible(true);
436 void setCursorColumn()
438 seqCanvas.cursorX = getKeyboardNo1() - 1;
439 scrollToVisible(true);
442 void setCursorRowAndColumn()
444 if (keyboardNo2 == null)
446 keyboardNo2 = new StringBuffer();
450 seqCanvas.cursorX = getKeyboardNo1() - 1;
451 seqCanvas.cursorY = getKeyboardNo2() - 1;
452 scrollToVisible(true);
456 void setCursorPosition()
458 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
460 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
461 scrollToVisible(true);
464 void moveCursor(int dx, int dy)
466 seqCanvas.cursorX += dx;
467 seqCanvas.cursorY += dy;
469 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
471 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
473 int original = seqCanvas.cursorX - dx;
474 int maxWidth = av.getAlignment().getWidth();
476 if (!hidden.isVisible(seqCanvas.cursorX))
478 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
479 int[] region = hidden.getRegionWithEdgeAtRes(visx);
481 if (region != null) // just in case
486 seqCanvas.cursorX = region[1] + 1;
491 seqCanvas.cursorX = region[0] - 1;
494 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
497 if (seqCanvas.cursorX >= maxWidth
498 || !hidden.isVisible(seqCanvas.cursorX))
500 seqCanvas.cursorX = original;
504 scrollToVisible(false);
508 * Scroll to make the cursor visible in the viewport.
511 * just jump to the location rather than scrolling
513 void scrollToVisible(boolean jump)
515 if (seqCanvas.cursorX < 0)
517 seqCanvas.cursorX = 0;
519 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
521 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
524 if (seqCanvas.cursorY < 0)
526 seqCanvas.cursorY = 0;
528 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
530 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
535 boolean repaintNeeded = true;
538 // only need to repaint if the viewport did not move, as otherwise it will
540 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
545 if (av.getWrapAlignment())
547 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
548 int x = av.getAlignment().getHiddenColumns()
549 .absoluteToVisibleColumn(seqCanvas.cursorX);
550 av.getRanges().scrollToWrappedVisible(x);
554 av.getRanges().scrollToVisible(seqCanvas.cursorX,
559 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
561 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
562 seqCanvas.cursorX, seqCanvas.cursorY);
572 void setSelectionAreaAtCursor(boolean topLeft)
574 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
576 if (av.getSelectionGroup() != null)
578 SequenceGroup sg = av.getSelectionGroup();
579 // Find the top and bottom of this group
580 int min = av.getAlignment().getHeight(), max = 0;
581 for (int i = 0; i < sg.getSize(); i++)
583 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
598 sg.setStartRes(seqCanvas.cursorX);
599 if (sg.getEndRes() < seqCanvas.cursorX)
601 sg.setEndRes(seqCanvas.cursorX);
604 min = seqCanvas.cursorY;
608 sg.setEndRes(seqCanvas.cursorX);
609 if (sg.getStartRes() > seqCanvas.cursorX)
611 sg.setStartRes(seqCanvas.cursorX);
614 max = seqCanvas.cursorY + 1;
619 // Only the user can do this
620 av.setSelectionGroup(null);
624 // Now add any sequences between min and max
625 sg.getSequences(null).clear();
626 for (int i = min; i < max; i++)
628 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
633 if (av.getSelectionGroup() == null)
635 SequenceGroup sg = new SequenceGroup();
636 sg.setStartRes(seqCanvas.cursorX);
637 sg.setEndRes(seqCanvas.cursorX);
638 sg.addSequence(sequence, false);
639 av.setSelectionGroup(sg);
642 ap.paintAlignment(false, false);
646 void insertGapAtCursor(boolean group)
648 groupEditing = group;
649 editStartSeq = seqCanvas.cursorY;
650 editLastRes = seqCanvas.cursorX;
651 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
655 void deleteGapAtCursor(boolean group)
657 groupEditing = group;
658 editStartSeq = seqCanvas.cursorY;
659 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
660 editSequence(false, false, seqCanvas.cursorX);
664 void insertNucAtCursor(boolean group, String nuc)
666 // TODO not called - delete?
667 groupEditing = group;
668 editStartSeq = seqCanvas.cursorY;
669 editLastRes = seqCanvas.cursorX;
670 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
674 void numberPressed(char value)
676 if (keyboardNo1 == null)
678 keyboardNo1 = new StringBuffer();
681 if (keyboardNo2 != null)
683 keyboardNo2.append(value);
687 keyboardNo1.append(value);
695 if (keyboardNo1 != null)
697 int value = Integer.parseInt(keyboardNo1.toString());
701 } catch (Exception x)
712 if (keyboardNo2 != null)
714 int value = Integer.parseInt(keyboardNo2.toString());
718 } catch (Exception x)
732 public void mouseReleased(MouseEvent evt)
734 MousePos pos = findMousePosition(evt);
735 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
740 boolean didDrag = mouseDragging; // did we come here after a drag
741 mouseDragging = false;
742 mouseWheelPressed = false;
744 if (evt.isPopupTrigger()) // Windows: mouseReleased
746 showPopupMenu(evt, pos);
757 doMouseReleasedDefineMode(evt, didDrag);
768 public void mousePressed(MouseEvent evt)
770 lastMousePress = evt.getPoint();
771 MousePos pos = findMousePosition(evt);
772 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
777 if (SwingUtilities.isMiddleMouseButton(evt))
779 mouseWheelPressed = true;
783 boolean isControlDown = Platform.isControlDown(evt);
784 if (evt.isShiftDown() || isControlDown)
794 doMousePressedDefineMode(evt, pos);
798 int seq = pos.seqIndex;
799 int res = pos.column;
801 if ((seq < av.getAlignment().getHeight())
802 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
819 public void mouseOverSequence(SequenceI sequence, int index, int pos)
821 String tmp = sequence.hashCode() + " " + index + " " + pos;
823 if (lastMessage == null || !lastMessage.equals(tmp))
825 // System.err.println("mouseOver Sequence: "+tmp);
826 ssm.mouseOverSequence(sequence, index, pos, av);
832 * Highlight the mapped region described by the search results object (unless
833 * unchanged). This supports highlight of protein while mousing over linked
834 * cDNA and vice versa. The status bar is also updated to show the location of
835 * the start of the highlighted region.
838 public String highlightSequence(SearchResultsI results)
840 if (results == null || results.equals(lastSearchResults))
844 lastSearchResults = results;
846 boolean wasScrolled = false;
848 if (av.isFollowHighlight())
850 // don't allow highlight of protein/cDNA to also scroll a complementary
851 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
852 // over residue to change abruptly, causing highlighted residue in panel 2
853 // to change, causing a scroll in panel 1 etc)
854 ap.setToScrollComplementPanel(false);
855 wasScrolled = ap.scrollToPosition(results);
858 seqCanvas.revalidate();
860 ap.setToScrollComplementPanel(true);
863 boolean noFastPaint = wasScrolled && av.getWrapAlignment();
864 if (seqCanvas.highlightSearchResults(results, noFastPaint))
866 setStatusMessage(results);
868 // JAL-3303 feature suppressed for now pending review
869 return null; // results.isEmpty() ? null : getHighlightInfo(results);
873 * temporary hack: answers a message suitable to show on structure hover
874 * label. This is normally null. It is a peptide variation description if
876 * <li>results are a single residue in a protein alignment</li>
877 * <li>there is a mapping to a coding sequence (codon)</li>
878 * <li>there are one or more SNP variant features on the codon</li>
880 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
885 private String getHighlightInfo(SearchResultsI results)
888 * ideally, just find mapped CDS (as we don't care about render style here);
889 * for now, go via split frame complement's FeatureRenderer
891 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
892 if (complement == null)
896 AlignFrame af = Desktop.getAlignFrameFor(complement);
897 FeatureRendererModel fr2 = af.getFeatureRenderer();
899 int j = results.getSize();
900 List<String> infos = new ArrayList<>();
901 for (int i = 0; i < j; i++)
903 SearchResultMatchI match = results.getResults().get(i);
904 int pos = match.getStart();
905 if (pos == match.getEnd())
907 SequenceI seq = match.getSequence();
908 SequenceI ds = seq.getDatasetSequence() == null ? seq
909 : seq.getDatasetSequence();
910 MappedFeatures mf = fr2
911 .findComplementFeaturesAtResidue(ds, pos);
914 for (SequenceFeature sf : mf.features)
916 String pv = mf.findProteinVariants(sf);
917 if (pv.length() > 0 && !infos.contains(pv))
930 StringBuilder sb = new StringBuilder();
931 for (String info : infos)
939 return sb.toString();
943 public VamsasSource getVamsasSource()
945 return this.ap == null ? null : this.ap.av;
949 public void updateColours(SequenceI seq, int index)
951 System.out.println("update the seqPanel colours");
956 * Action on mouse movement is to update the status bar to show the current
957 * sequence position, and (if features are shown) to show any features at the
958 * position in a tooltip. Does nothing if the mouse move does not change
964 public void mouseMoved(MouseEvent evt)
968 // This is because MacOSX creates a mouseMoved
969 // If control is down, other platforms will not.
973 final MousePos mousePos = findMousePosition(evt);
974 if (mousePos.equals(lastMousePosition))
977 * just a pixel move without change of 'cell'
981 lastMousePosition = mousePos;
983 if (mousePos.isOverAnnotation())
985 mouseMovedOverAnnotation(mousePos);
988 final int seq = mousePos.seqIndex;
990 final int column = mousePos.column;
991 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
993 lastMousePosition = null;
994 setToolTipText(null);
996 ap.alignFrame.setStatus("");
1000 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1002 if (column >= sequence.getLength())
1008 * set status bar message, returning residue position in sequence
1010 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1011 final int pos = setStatusMessage(sequence, column, seq);
1012 if (ssm != null && !isGapped)
1014 mouseOverSequence(sequence, column, pos);
1017 tooltipText.setLength(6); // Cuts the buffer back to <html>
1019 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1022 for (int g = 0; g < groups.length; g++)
1024 if (groups[g].getStartRes() <= column
1025 && groups[g].getEndRes() >= column)
1027 if (!groups[g].getName().startsWith("JTreeGroup")
1028 && !groups[g].getName().startsWith("JGroup"))
1030 tooltipText.append(groups[g].getName());
1033 if (groups[g].getDescription() != null)
1035 tooltipText.append(": " + groups[g].getDescription());
1042 * add any features at the position to the tooltip; if over a gap, only
1043 * add features that straddle the gap (pos may be the residue before or
1046 int unshownFeatures = 0;
1047 if (av.isShowSequenceFeatures())
1049 List<SequenceFeature> features = ap.getFeatureRenderer()
1050 .findFeaturesAtColumn(sequence, column + 1);
1051 unshownFeatures = seqARep.appendFeaturesLengthLimit(tooltipText, pos,
1053 this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
1056 * add features in CDS/protein complement at the corresponding
1057 * position if configured to do so
1059 if (av.isShowComplementFeatures())
1061 if (!Comparison.isGap(sequence.getCharAt(column)))
1063 AlignViewportI complement = ap.getAlignViewport()
1064 .getCodingComplement();
1065 AlignFrame af = Desktop.getAlignFrameFor(complement);
1066 FeatureRendererModel fr2 = af.getFeatureRenderer();
1067 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1071 unshownFeatures = seqARep.appendFeaturesLengthLimit(
1072 tooltipText, pos, mf, fr2,
1073 MAX_TOOLTIP_LENGTH);
1078 if (tooltipText.length() == 6) // "<html>"
1080 setToolTipText(null);
1085 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1087 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1088 tooltipText.append("...");
1090 if (unshownFeatures > 0)
1092 tooltipText.append("<br/>").append("... ").append("<i>")
1093 .append(MessageManager.formatMessage(
1094 "label.features_not_shown", unshownFeatures))
1097 String textString = tooltipText.toString();
1098 if (lastTooltip == null || !lastTooltip.equals(textString))
1100 String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1102 setToolTipText(formattedTooltipText);
1103 lastTooltip = textString;
1109 * When the view is in wrapped mode, and the mouse is over an annotation row,
1110 * shows the corresponding tooltip and status message (if any)
1115 protected void mouseMovedOverAnnotation(MousePos pos)
1117 final int column = pos.column;
1118 final int rowIndex = pos.annotationIndex;
1120 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1125 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1127 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1129 setToolTipText(tooltip);
1130 lastTooltip = tooltip;
1132 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1134 ap.alignFrame.setStatus(msg);
1137 private Point lastp = null;
1142 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1145 public Point getToolTipLocation(MouseEvent event)
1147 if (tooltipText == null || tooltipText.length() <= 6)
1153 int x = event.getX();
1155 // switch sides when tooltip is too close to edge
1156 int wdth = (w - x < 200) ? -(w / 2) : 5;
1158 if (!event.isShiftDown() || p == null)
1160 p = new Point(event.getX() + wdth, event.getY() - 20);
1164 * TODO: try to set position so region is not obscured by tooltip
1172 * set when the current UI interaction has resulted in a change that requires
1173 * shading in overviews and structures to be recalculated. this could be
1174 * changed to a something more expressive that indicates what actually has
1175 * changed, so selective redraws can be applied (ie. only structures, only
1178 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1181 * set if av.getSelectionGroup() refers to a group that is defined on the
1182 * alignment view, rather than a transient selection
1184 // private boolean editingDefinedGroup = false; // TODO: refactor to
1185 // avcontroller or viewModel
1188 * Sets the status message in alignment panel, showing the sequence number
1189 * (index) and id, and residue and residue position if not at a gap, for the
1190 * given sequence and column position. Returns the residue position returned
1191 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1192 * if at a gapped position.
1195 * aligned sequence object
1199 * index of sequence in alignment
1200 * @return sequence position of residue at column, or adjacent residue if at a
1203 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1205 char sequenceChar = sequence.getCharAt(column);
1206 int pos = sequence.findPosition(column);
1207 setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1213 * Builds the status message for the current cursor location and writes it to
1214 * the status bar, for example
1217 * Sequence 3 ID: FER1_SOLLC
1218 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1219 * Sequence 5 ID: FER1_PEA Residue: B (3)
1220 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1225 * sequence position in the alignment (1..)
1226 * @param sequenceChar
1227 * the character under the cursor
1229 * the sequence residue position (if not over a gap)
1231 protected void setStatusMessage(SequenceI sequence, int seqIndex,
1232 char sequenceChar, int residuePos)
1234 StringBuilder text = new StringBuilder(32);
1237 * Sequence number (if known), and sequence name.
1239 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1240 text.append("Sequence").append(seqno).append(" ID: ")
1241 .append(sequence.getName());
1243 String residue = null;
1246 * Try to translate the display character to residue name (null for gap).
1248 boolean isGapped = Comparison.isGap(sequenceChar);
1252 boolean nucleotide = av.getAlignment().isNucleotide();
1253 String displayChar = String.valueOf(sequenceChar);
1256 residue = ResidueProperties.nucleotideName.get(displayChar);
1260 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1261 : ("*".equals(displayChar) ? "STOP"
1262 : ResidueProperties.aa2Triplet.get(displayChar));
1264 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1265 .append(": ").append(residue == null ? displayChar : residue);
1267 text.append(" (").append(Integer.toString(residuePos)).append(")");
1269 ap.alignFrame.setStatus(text.toString());
1273 * Set the status bar message to highlight the first matched position in
1278 private void setStatusMessage(SearchResultsI results)
1280 AlignmentI al = this.av.getAlignment();
1281 int sequenceIndex = al.findIndex(results);
1282 if (sequenceIndex == -1)
1286 SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1287 for (SearchResultMatchI m : results.getResults())
1289 SequenceI seq = m.getSequence();
1290 if (seq.getDatasetSequence() != null)
1292 seq = seq.getDatasetSequence();
1297 int start = m.getStart();
1298 setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1309 public void mouseDragged(MouseEvent evt)
1311 MousePos pos = findMousePosition(evt);
1312 if (pos.isOverAnnotation() || pos.column == -1)
1317 if (mouseWheelPressed)
1319 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1320 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1322 int oldWidth = av.getCharWidth();
1324 // Which is bigger, left-right or up-down?
1325 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1326 .abs(evt.getX() - lastMousePress.getX()))
1329 * on drag up or down, decrement or increment font size
1331 int fontSize = av.font.getSize();
1332 boolean fontChanged = false;
1334 if (evt.getY() < lastMousePress.getY())
1339 else if (evt.getY() > lastMousePress.getY())
1352 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1354 av.setFont(newFont, true);
1355 av.setCharWidth(oldWidth);
1359 ap.av.getCodingComplement().setFont(newFont, true);
1360 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1361 .getSplitViewContainer();
1362 splitFrame.adjustLayout();
1363 splitFrame.repaint();
1370 * on drag left or right, decrement or increment character width
1373 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1375 newWidth = av.getCharWidth() - 1;
1376 av.setCharWidth(newWidth);
1378 else if (evt.getX() > lastMousePress.getX())
1380 newWidth = av.getCharWidth() + 1;
1381 av.setCharWidth(newWidth);
1385 ap.paintAlignment(false, false);
1389 * need to ensure newWidth is set on cdna, regardless of which
1390 * panel the mouse drag happened in; protein will compute its
1391 * character width as 1:1 or 3:1
1393 av.getCodingComplement().setCharWidth(newWidth);
1394 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1395 .getSplitViewContainer();
1396 splitFrame.adjustLayout();
1397 splitFrame.repaint();
1402 FontMetrics fm = getFontMetrics(av.getFont());
1403 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1405 lastMousePress = evt.getPoint();
1412 dragStretchGroup(evt);
1416 int res = pos.column;
1423 if ((editLastRes == -1) || (editLastRes == res))
1428 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1430 // dragLeft, delete gap
1431 editSequence(false, false, res);
1435 editSequence(true, false, res);
1438 mouseDragging = true;
1439 if (scrollThread != null)
1441 scrollThread.setMousePosition(evt.getPoint());
1446 * Edits the sequence to insert or delete one or more gaps, in response to a
1447 * mouse drag or cursor mode command. The number of inserts/deletes may be
1448 * specified with the cursor command, or else depends on the mouse event
1449 * (normally one column, but potentially more for a fast mouse drag).
1451 * Delete gaps is limited to the number of gaps left of the cursor position
1452 * (mouse drag), or at or right of the cursor position (cursor mode).
1454 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1455 * the current selection group.
1457 * In locked editing mode (with a selection group present), inserts/deletions
1458 * within the selection group are limited to its boundaries (and edits outside
1459 * the group stop at its border).
1462 * true to insert gaps, false to delete gaps
1464 * (unused parameter)
1466 * the column at which to perform the action; the number of columns
1467 * affected depends on <code>this.editLastRes</code> (cursor column
1470 synchronized void editSequence(boolean insertGap, boolean editSeq,
1474 int fixedRight = -1;
1475 boolean fixedColumns = false;
1476 SequenceGroup sg = av.getSelectionGroup();
1478 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1480 // No group, but the sequence may represent a group
1481 if (!groupEditing && av.hasHiddenRows())
1483 if (av.isHiddenRepSequence(seq))
1485 sg = av.getRepresentedSequences(seq);
1486 groupEditing = true;
1490 StringBuilder message = new StringBuilder(64); // for status bar
1493 * make a name for the edit action, for
1494 * status bar message and Undo/Redo menu
1496 String label = null;
1499 message.append("Edit group:");
1500 label = MessageManager.getString("action.edit_group");
1504 message.append("Edit sequence: " + seq.getName());
1505 label = seq.getName();
1506 if (label.length() > 10)
1508 label = label.substring(0, 10);
1510 label = MessageManager.formatMessage("label.edit_params",
1516 * initialise the edit command if there is not
1517 * already one being extended
1519 if (editCommand == null)
1521 editCommand = new EditCommand(label);
1526 message.append(" insert ");
1530 message.append(" delete ");
1533 message.append(Math.abs(startres - editLastRes) + " gaps.");
1534 ap.alignFrame.setStatus(message.toString());
1537 * is there a selection group containing the sequence being edited?
1538 * if so the boundary of the group is the limit of the edit
1539 * (but the edit may be inside or outside the selection group)
1541 boolean inSelectionGroup = sg != null
1542 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1543 if (groupEditing || inSelectionGroup)
1545 fixedColumns = true;
1547 // sg might be null as the user may only see 1 sequence,
1548 // but the sequence represents a group
1551 if (!av.isHiddenRepSequence(seq))
1556 sg = av.getRepresentedSequences(seq);
1559 fixedLeft = sg.getStartRes();
1560 fixedRight = sg.getEndRes();
1562 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1563 || (startres >= fixedLeft && editLastRes < fixedLeft)
1564 || (startres > fixedRight && editLastRes <= fixedRight)
1565 || (startres <= fixedRight && editLastRes > fixedRight))
1571 if (fixedLeft > startres)
1573 fixedRight = fixedLeft - 1;
1576 else if (fixedRight < startres)
1578 fixedLeft = fixedRight;
1583 if (av.hasHiddenColumns())
1585 fixedColumns = true;
1586 int y1 = av.getAlignment().getHiddenColumns()
1587 .getNextHiddenBoundary(true, startres);
1588 int y2 = av.getAlignment().getHiddenColumns()
1589 .getNextHiddenBoundary(false, startres);
1591 if ((insertGap && startres > y1 && editLastRes < y1)
1592 || (!insertGap && startres < y2 && editLastRes > y2))
1598 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1599 // Selection spans a hidden region
1600 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1608 fixedRight = y2 - 1;
1613 boolean success = doEditSequence(insertGap, editSeq, startres,
1614 fixedRight, fixedColumns, sg);
1617 * report what actually happened (might be less than
1618 * what was requested), by inspecting the edit commands added
1620 String msg = getEditStatusMessage(editCommand);
1621 ap.alignFrame.setStatus(msg == null ? " " : msg);
1627 editLastRes = startres;
1628 seqCanvas.repaint();
1632 * A helper method that performs the requested editing to insert or delete
1633 * gaps (if possible). Answers true if the edit was successful, false if could
1634 * only be performed in part or not at all. Failure may occur in 'locked edit'
1635 * mode, when an insertion requires a matching gapped position (or column) to
1636 * delete, and deletion requires an adjacent gapped position (or column) to
1640 * true if inserting gap(s), false if deleting
1642 * (unused parameter, currently always false)
1644 * the column at which to perform the edit
1646 * fixed right boundary column of a locked edit (within or to the
1647 * left of a selection group)
1648 * @param fixedColumns
1649 * true if this is a locked edit
1651 * the sequence group (if group edit is being performed)
1654 protected boolean doEditSequence(final boolean insertGap,
1655 final boolean editSeq, final int startres, int fixedRight,
1656 final boolean fixedColumns, final SequenceGroup sg)
1658 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1659 SequenceI[] seqs = new SequenceI[] { seq };
1663 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1664 int g, groupSize = vseqs.size();
1665 SequenceI[] groupSeqs = new SequenceI[groupSize];
1666 for (g = 0; g < groupSeqs.length; g++)
1668 groupSeqs[g] = vseqs.get(g);
1674 // If the user has selected the whole sequence, and is dragging to
1675 // the right, we can still extend the alignment and selectionGroup
1676 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1677 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1680 av.getAlignment().getWidth() + startres - editLastRes);
1681 fixedRight = sg.getEndRes();
1684 // Is it valid with fixed columns??
1685 // Find the next gap before the end
1686 // of the visible region boundary
1687 boolean blank = false;
1688 for (; fixedRight > editLastRes; fixedRight--)
1692 for (g = 0; g < groupSize; g++)
1694 for (int j = 0; j < startres - editLastRes; j++)
1697 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1712 if (sg.getSize() == av.getAlignment().getHeight())
1714 if ((av.hasHiddenColumns()
1715 && startres < av.getAlignment().getHiddenColumns()
1716 .getNextHiddenBoundary(false, startres)))
1721 int alWidth = av.getAlignment().getWidth();
1722 if (av.hasHiddenRows())
1724 int hwidth = av.getAlignment().getHiddenSequences()
1726 if (hwidth > alWidth)
1731 // We can still insert gaps if the selectionGroup
1732 // contains all the sequences
1733 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1734 fixedRight = alWidth + startres - editLastRes;
1744 else if (!insertGap)
1746 // / Are we able to delete?
1747 // ie are all columns blank?
1749 for (g = 0; g < groupSize; g++)
1751 for (int j = startres; j < editLastRes; j++)
1753 if (groupSeqs[g].getLength() <= j)
1758 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1760 // Not a gap, block edit not valid
1769 // dragging to the right
1770 if (fixedColumns && fixedRight != -1)
1772 for (int j = editLastRes; j < startres; j++)
1774 insertGap(j, groupSeqs, fixedRight);
1779 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1780 startres - editLastRes, false);
1785 // dragging to the left
1786 if (fixedColumns && fixedRight != -1)
1788 for (int j = editLastRes; j > startres; j--)
1790 deleteChar(startres, groupSeqs, fixedRight);
1795 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1796 editLastRes - startres, false);
1803 * editing a single sequence
1807 // dragging to the right
1808 if (fixedColumns && fixedRight != -1)
1810 for (int j = editLastRes; j < startres; j++)
1812 if (!insertGap(j, seqs, fixedRight))
1815 * e.g. cursor mode command specified
1816 * more inserts than are possible
1824 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1825 startres - editLastRes, false);
1832 // dragging to the left
1833 if (fixedColumns && fixedRight != -1)
1835 for (int j = editLastRes; j > startres; j--)
1837 if (!Comparison.isGap(seq.getCharAt(startres)))
1841 deleteChar(startres, seqs, fixedRight);
1846 // could be a keyboard edit trying to delete none gaps
1848 for (int m = startres; m < editLastRes; m++)
1850 if (!Comparison.isGap(seq.getCharAt(m)))
1858 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1863 {// insertGap==false AND editSeq==TRUE;
1864 if (fixedColumns && fixedRight != -1)
1866 for (int j = editLastRes; j < startres; j++)
1868 insertGap(j, seqs, fixedRight);
1873 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1874 startres - editLastRes, false);
1884 * Constructs an informative status bar message while dragging to insert or
1885 * delete gaps. Answers null if inserts and deletes cancel out.
1887 * @param editCommand
1888 * a command containing the list of individual edits
1891 protected static String getEditStatusMessage(EditCommand editCommand)
1893 if (editCommand == null)
1899 * add any inserts, and subtract any deletes,
1900 * not counting those auto-inserted when doing a 'locked edit'
1901 * (so only counting edits 'under the cursor')
1904 for (Edit cmd : editCommand.getEdits())
1906 if (!cmd.isSystemGenerated())
1908 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1916 * inserts and deletes cancel out
1921 String msgKey = count > 1 ? "label.insert_gaps"
1922 : (count == 1 ? "label.insert_gap"
1923 : (count == -1 ? "label.delete_gap"
1924 : "label.delete_gaps"));
1925 count = Math.abs(count);
1927 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1931 * Inserts one gap at column j, deleting the right-most gapped column up to
1932 * (and including) fixedColumn. Returns true if the edit is successful, false
1933 * if no blank column is available to allow the insertion to be balanced by a
1938 * @param fixedColumn
1941 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1943 int blankColumn = fixedColumn;
1944 for (int s = 0; s < seq.length; s++)
1946 // Find the next gap before the end of the visible region boundary
1947 // If lastCol > j, theres a boundary after the gap insertion
1949 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1951 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1953 // Theres a space, so break and insert the gap
1958 if (blankColumn <= j)
1960 blankColumn = fixedColumn;
1966 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1968 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1974 * Helper method to add and perform one edit action
1980 * @param systemGenerated
1981 * true if the edit is a 'balancing' delete (or insert) to match a
1982 * user's insert (or delete) in a locked editing region
1984 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1985 int count, boolean systemGenerated)
1988 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1989 av.getAlignment().getGapCharacter());
1990 edit.setSystemGenerated(systemGenerated);
1992 editCommand.appendEdit(edit, av.getAlignment(), true, null);
1996 * Deletes the character at column j, and inserts a gap at fixedColumn, in
1997 * each of the given sequences. The caller should ensure that all sequences
1998 * are gapped in column j.
2002 * @param fixedColumn
2004 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2006 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2008 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2012 * On reentering the panel, stops any scrolling that was started on dragging
2018 public void mouseEntered(MouseEvent e)
2028 * On leaving the panel, if the mouse is being dragged, starts a thread to
2029 * scroll it until the mouse is released (in unwrapped mode only)
2034 public void mouseExited(MouseEvent e)
2036 lastMousePosition = null;
2037 ap.alignFrame.setStatus(" ");
2038 if (av.getWrapAlignment())
2043 if (mouseDragging && scrollThread == null)
2045 scrollThread = new ScrollThread();
2050 * Handler for double-click on a position with one or more sequence features.
2051 * Opens the Amend Features dialog to allow feature details to be amended, or
2052 * the feature deleted.
2055 public void mouseClicked(MouseEvent evt)
2057 SequenceGroup sg = null;
2058 MousePos pos = findMousePosition(evt);
2059 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2064 if (evt.getClickCount() > 1)
2066 sg = av.getSelectionGroup();
2067 if (sg != null && sg.getSize() == 1
2068 && sg.getEndRes() - sg.getStartRes() < 2)
2070 av.setSelectionGroup(null);
2073 int column = pos.column;
2076 * find features at the position (if not gapped), or straddling
2077 * the position (if at a gap)
2079 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2080 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2081 .findFeaturesAtColumn(sequence, column + 1);
2083 if (!features.isEmpty())
2086 * highlight the first feature at the position on the alignment
2088 SearchResultsI highlight = new SearchResults();
2089 highlight.addResult(sequence, features.get(0).getBegin(), features
2091 seqCanvas.highlightSearchResults(highlight, false);
2094 * open the Amend Features dialog; clear highlighting afterwards,
2095 * whether changes were made or not
2097 List<SequenceI> seqs = Collections.singletonList(sequence);
2098 seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2100 av.setSearchResults(null); // clear highlighting
2101 seqCanvas.repaint(); // draw new/amended features
2107 public void mouseWheelMoved(MouseWheelEvent e)
2110 double wheelRotation = e.getPreciseWheelRotation();
2111 if (wheelRotation > 0)
2113 if (e.isShiftDown())
2115 av.getRanges().scrollRight(true);
2120 av.getRanges().scrollUp(false);
2123 else if (wheelRotation < 0)
2125 if (e.isShiftDown())
2127 av.getRanges().scrollRight(false);
2131 av.getRanges().scrollUp(true);
2136 * update status bar and tooltip for new position
2137 * (need to synthesize a mouse movement to refresh tooltip)
2140 ToolTipManager.sharedInstance().mouseMoved(e);
2149 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2151 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2156 final int res = pos.column;
2157 final int seq = pos.seqIndex;
2159 updateOverviewAndStructs = false;
2161 startWrapBlock = wrappedBlock;
2163 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2165 if ((sequence == null) || (res > sequence.getLength()))
2170 stretchGroup = av.getSelectionGroup();
2172 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2174 stretchGroup = av.getAlignment().findGroup(sequence, res);
2175 if (stretchGroup != null)
2177 // only update the current selection if the popup menu has a group to
2179 av.setSelectionGroup(stretchGroup);
2183 if (evt.isPopupTrigger()) // Mac: mousePressed
2185 showPopupMenu(evt, pos);
2190 * defer right-mouse click handling to mouseReleased on Windows
2191 * (where isPopupTrigger() will answer true)
2192 * NB isRightMouseButton is also true for Cmd-click on Mac
2194 if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2201 seqCanvas.cursorX = res;
2202 seqCanvas.cursorY = seq;
2203 seqCanvas.repaint();
2207 if (stretchGroup == null)
2209 createStretchGroup(res, sequence);
2212 if (stretchGroup != null)
2214 stretchGroup.addPropertyChangeListener(seqCanvas);
2217 seqCanvas.repaint();
2220 private void createStretchGroup(int res, SequenceI sequence)
2222 // Only if left mouse button do we want to change group sizes
2223 // define a new group here
2224 SequenceGroup sg = new SequenceGroup();
2225 sg.setStartRes(res);
2227 sg.addSequence(sequence, false);
2228 av.setSelectionGroup(sg);
2231 if (av.getConservationSelected())
2233 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2237 if (av.getAbovePIDThreshold())
2239 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2242 // TODO: stretchGroup will always be not null. Is this a merge error ?
2243 // or is there a threading issue here?
2244 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2246 // Edit end res position of selected group
2247 changeEndRes = true;
2249 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2251 // Edit end res position of selected group
2252 changeStartRes = true;
2254 stretchGroup.getWidth();
2259 * Build and show a pop-up menu at the right-click mouse position
2264 void showPopupMenu(MouseEvent evt, MousePos pos)
2266 final int column = pos.column;
2267 final int seq = pos.seqIndex;
2268 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2269 if (sequence != null)
2271 PopupMenu pop = new PopupMenu(ap, sequence, column);
2272 pop.show(this, evt.getX(), evt.getY());
2277 * Update the display after mouse up on a selection or group
2280 * mouse released event details
2282 * true if this event is happening after a mouse drag (rather than a
2285 protected void doMouseReleasedDefineMode(MouseEvent evt,
2288 if (stretchGroup == null)
2293 stretchGroup.removePropertyChangeListener(seqCanvas);
2295 // always do this - annotation has own state
2296 // but defer colourscheme update until hidden sequences are passed in
2297 boolean vischange = stretchGroup.recalcConservation(true);
2298 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2300 if (stretchGroup.cs != null)
2304 stretchGroup.cs.alignmentChanged(stretchGroup,
2305 av.getHiddenRepSequences());
2308 ResidueShaderI groupColourScheme = stretchGroup
2309 .getGroupColourScheme();
2310 String name = stretchGroup.getName();
2311 if (stretchGroup.cs.conservationApplied())
2313 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2315 if (stretchGroup.cs.getThreshold() > 0)
2317 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2320 PaintRefresher.Refresh(this, av.getSequenceSetId());
2321 // TODO: structure colours only need updating if stretchGroup used to or now
2322 // does contain sequences with structure views
2323 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2324 updateOverviewAndStructs = false;
2325 changeEndRes = false;
2326 changeStartRes = false;
2327 stretchGroup = null;
2332 * Resizes the borders of a selection group depending on the direction of
2337 protected void dragStretchGroup(MouseEvent evt)
2339 if (stretchGroup == null)
2344 MousePos pos = findMousePosition(evt);
2345 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2350 int res = pos.column;
2351 int y = pos.seqIndex;
2353 if (wrappedBlock != startWrapBlock)
2358 res = Math.min(res, av.getAlignment().getWidth()-1);
2360 if (stretchGroup.getEndRes() == res)
2362 // Edit end res position of selected group
2363 changeEndRes = true;
2365 else if (stretchGroup.getStartRes() == res)
2367 // Edit start res position of selected group
2368 changeStartRes = true;
2371 if (res < av.getRanges().getStartRes())
2373 res = av.getRanges().getStartRes();
2378 if (res > (stretchGroup.getStartRes() - 1))
2380 stretchGroup.setEndRes(res);
2381 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2384 else if (changeStartRes)
2386 if (res < (stretchGroup.getEndRes() + 1))
2388 stretchGroup.setStartRes(res);
2389 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2393 int dragDirection = 0;
2399 else if (y < oldSeq)
2404 while ((y != oldSeq) && (oldSeq > -1)
2405 && (y < av.getAlignment().getHeight()))
2407 // This routine ensures we don't skip any sequences, as the
2408 // selection is quite slow.
2409 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2411 oldSeq += dragDirection;
2418 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2420 if (stretchGroup.getSequences(null).contains(nextSeq))
2422 stretchGroup.deleteSequence(seq, false);
2423 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2429 stretchGroup.addSequence(seq, false);
2432 stretchGroup.addSequence(nextSeq, false);
2433 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2442 mouseDragging = true;
2444 if (scrollThread != null)
2446 scrollThread.setMousePosition(evt.getPoint());
2450 * construct a status message showing the range of the selection
2452 StringBuilder status = new StringBuilder(64);
2453 List<SequenceI> seqs = stretchGroup.getSequences();
2454 String name = seqs.get(0).getName();
2455 if (name.length() > 20)
2457 name = name.substring(0, 20);
2459 status.append(name).append(" - ");
2460 name = seqs.get(seqs.size() - 1).getName();
2461 if (name.length() > 20)
2463 name = name.substring(0, 20);
2465 status.append(name).append(" ");
2466 int startRes = stretchGroup.getStartRes();
2467 status.append(" cols ").append(String.valueOf(startRes + 1))
2469 int endRes = stretchGroup.getEndRes();
2470 status.append(String.valueOf(endRes + 1));
2471 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2472 .append(String.valueOf(endRes - startRes + 1)).append(")");
2473 ap.alignFrame.setStatus(status.toString());
2477 * Stops the scroll thread if it is running
2479 void stopScrolling()
2481 if (scrollThread != null)
2483 scrollThread.stopScrolling();
2484 scrollThread = null;
2486 mouseDragging = false;
2490 * Starts a thread to scroll the alignment, towards a given mouse position
2491 * outside the panel bounds
2495 void startScrolling(Point mousePos)
2497 if (scrollThread == null)
2499 scrollThread = new ScrollThread();
2502 mouseDragging = true;
2503 scrollThread.setMousePosition(mousePos);
2507 * Performs scrolling of the visible alignment left, right, up or down
2509 class ScrollThread extends Thread
2511 private Point mousePos;
2513 private volatile boolean threadRunning = true;
2518 public ScrollThread()
2520 setName("SeqPanel$ScrollThread");
2525 * Sets the position of the mouse that determines the direction of the
2530 public void setMousePosition(Point p)
2536 * Sets a flag that will cause the thread to exit
2538 public void stopScrolling()
2540 threadRunning = false;
2544 * Scrolls the alignment left or right, and/or up or down, depending on the
2545 * last notified mouse position, until the limit of the alignment is
2546 * reached, or a flag is set to stop the scroll
2551 while (threadRunning && mouseDragging)
2553 if (mousePos != null)
2555 boolean scrolled = false;
2556 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2563 // mouse is above this panel - try scroll up
2564 scrolled = ranges.scrollUp(true);
2566 else if (mousePos.y >= getHeight())
2568 // mouse is below this panel - try scroll down
2569 scrolled = ranges.scrollUp(false);
2573 * scroll left or right
2577 scrolled |= ranges.scrollRight(false);
2579 else if (mousePos.x >= getWidth())
2581 scrolled |= ranges.scrollRight(true);
2586 * we have reached the limit of the visible alignment - quit
2588 threadRunning = false;
2589 SeqPanel.this.ap.repaint();
2596 } catch (Exception ex)
2604 * modify current selection according to a received message.
2607 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2608 HiddenColumns hidden, SelectionSource source)
2610 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2611 // handles selection messages...
2612 // TODO: extend config options to allow user to control if selections may be
2613 // shared between viewports.
2614 boolean iSentTheSelection = (av == source
2615 || (source instanceof AlignViewport
2616 && ((AlignmentViewport) source).getSequenceSetId()
2617 .equals(av.getSequenceSetId())));
2619 if (iSentTheSelection)
2621 // respond to our own event by updating dependent dialogs
2622 if (ap.getCalculationDialog() != null)
2624 ap.getCalculationDialog().validateCalcTypes();
2630 // process further ?
2631 if (!av.followSelection)
2637 * Ignore the selection if there is one of our own pending.
2639 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2645 * Check for selection in a view of which this one is a dna/protein
2648 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2653 // do we want to thread this ? (contention with seqsel and colsel locks, I
2656 * only copy colsel if there is a real intersection between
2657 * sequence selection and this panel's alignment
2659 boolean repaint = false;
2660 boolean copycolsel = false;
2662 SequenceGroup sgroup = null;
2663 if (seqsel != null && seqsel.getSize() > 0)
2665 if (av.getAlignment() == null)
2667 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2668 + " ViewId=" + av.getViewId()
2669 + " 's alignment is NULL! returning immediately.");
2672 sgroup = seqsel.intersect(av.getAlignment(),
2673 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2674 if ((sgroup != null && sgroup.getSize() > 0))
2679 if (sgroup != null && sgroup.getSize() > 0)
2681 av.setSelectionGroup(sgroup);
2685 av.setSelectionGroup(null);
2687 av.isSelectionGroupChanged(true);
2692 // the current selection is unset or from a previous message
2693 // so import the new colsel.
2694 if (colsel == null || colsel.isEmpty())
2696 if (av.getColumnSelection() != null)
2698 av.getColumnSelection().clear();
2704 // TODO: shift colSel according to the intersecting sequences
2705 if (av.getColumnSelection() == null)
2707 av.setColumnSelection(new ColumnSelection(colsel));
2711 av.getColumnSelection().setElementsFrom(colsel,
2712 av.getAlignment().getHiddenColumns());
2715 av.isColSelChanged(true);
2719 if (copycolsel && av.hasHiddenColumns()
2720 && (av.getAlignment().getHiddenColumns() == null))
2722 System.err.println("Bad things");
2724 if (repaint) // always true!
2726 // probably finessing with multiple redraws here
2727 PaintRefresher.Refresh(this, av.getSequenceSetId());
2728 // ap.paintAlignment(false);
2731 // lastly, update dependent dialogs
2732 if (ap.getCalculationDialog() != null)
2734 ap.getCalculationDialog().validateCalcTypes();
2740 * If this panel is a cdna/protein translation view of the selection source,
2741 * tries to map the source selection to a local one, and returns true. Else
2748 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2749 ColumnSelection colsel, HiddenColumns hidden,
2750 SelectionSource source)
2752 if (!(source instanceof AlignViewportI))
2756 final AlignViewportI sourceAv = (AlignViewportI) source;
2757 if (sourceAv.getCodingComplement() != av
2758 && av.getCodingComplement() != sourceAv)
2764 * Map sequence selection
2766 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2767 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2768 av.isSelectionGroupChanged(true);
2771 * Map column selection
2773 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2775 ColumnSelection cs = new ColumnSelection();
2776 HiddenColumns hs = new HiddenColumns();
2777 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2778 av.setColumnSelection(cs);
2779 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2781 // lastly, update any dependent dialogs
2782 if (ap.getCalculationDialog() != null)
2784 ap.getCalculationDialog().validateCalcTypes();
2788 * repaint alignment, and also Overview or Structure
2789 * if hidden column selection has changed
2791 ap.paintAlignment(hiddenChanged, hiddenChanged);
2798 * @return null or last search results handled by this panel
2800 public SearchResultsI getLastSearchResults()
2802 return lastSearchResults;