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 return results.isEmpty() ? null : getHighlightInfo(results);
872 * temporary hack: answers a message suitable to show on structure hover
873 * label. This is normally null. It is a peptide variation description if
875 * <li>results are a single residue in a protein alignment</li>
876 * <li>there is a mapping to a coding sequence (codon)</li>
877 * <li>there are one or more SNP variant features on the codon</li>
879 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
884 private String getHighlightInfo(SearchResultsI results)
887 * ideally, just find mapped CDS (as we don't care about render style here);
888 * for now, go via split frame complement's FeatureRenderer
890 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
891 if (complement == null)
895 AlignFrame af = Desktop.getAlignFrameFor(complement);
896 FeatureRendererModel fr2 = af.getFeatureRenderer();
898 int j = results.getSize();
899 List<String> infos = new ArrayList<>();
900 for (int i = 0; i < j; i++)
902 SearchResultMatchI match = results.getResults().get(i);
903 int pos = match.getStart();
904 if (pos == match.getEnd())
906 SequenceI seq = match.getSequence();
907 SequenceI ds = seq.getDatasetSequence() == null ? seq
908 : seq.getDatasetSequence();
909 MappedFeatures mf = fr2
910 .findComplementFeaturesAtResidue(ds, pos);
913 for (SequenceFeature sf : mf.features)
915 String pv = mf.findProteinVariants(sf);
916 if (pv.length() > 0 && !infos.contains(pv))
929 StringBuilder sb = new StringBuilder();
930 for (String info : infos)
938 return sb.toString();
942 public VamsasSource getVamsasSource()
944 return this.ap == null ? null : this.ap.av;
948 public void updateColours(SequenceI seq, int index)
950 System.out.println("update the seqPanel colours");
955 * Action on mouse movement is to update the status bar to show the current
956 * sequence position, and (if features are shown) to show any features at the
957 * position in a tooltip. Does nothing if the mouse move does not change
963 public void mouseMoved(MouseEvent evt)
967 // This is because MacOSX creates a mouseMoved
968 // If control is down, other platforms will not.
972 final MousePos mousePos = findMousePosition(evt);
973 if (mousePos.equals(lastMousePosition))
976 * just a pixel move without change of 'cell'
980 lastMousePosition = mousePos;
982 if (mousePos.isOverAnnotation())
984 mouseMovedOverAnnotation(mousePos);
987 final int seq = mousePos.seqIndex;
989 final int column = mousePos.column;
990 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
992 lastMousePosition = null;
993 setToolTipText(null);
995 ap.alignFrame.setStatus("");
999 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1001 if (column >= sequence.getLength())
1007 * set status bar message, returning residue position in sequence
1009 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1010 final int pos = setStatusMessage(sequence, column, seq);
1011 if (ssm != null && !isGapped)
1013 mouseOverSequence(sequence, column, pos);
1016 tooltipText.setLength(6); // Cuts the buffer back to <html>
1018 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1021 for (int g = 0; g < groups.length; g++)
1023 if (groups[g].getStartRes() <= column
1024 && groups[g].getEndRes() >= column)
1026 if (!groups[g].getName().startsWith("JTreeGroup")
1027 && !groups[g].getName().startsWith("JGroup"))
1029 tooltipText.append(groups[g].getName());
1032 if (groups[g].getDescription() != null)
1034 tooltipText.append(": " + groups[g].getDescription());
1041 * add any features at the position to the tooltip; if over a gap, only
1042 * add features that straddle the gap (pos may be the residue before or
1045 int unshownFeatures = 0;
1046 if (av.isShowSequenceFeatures())
1048 List<SequenceFeature> features = ap.getFeatureRenderer()
1049 .findFeaturesAtColumn(sequence, column + 1);
1050 unshownFeatures = seqARep.appendFeaturesLengthLimit(tooltipText, pos,
1052 this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
1055 * add features in CDS/protein complement at the corresponding
1056 * position if configured to do so
1058 if (av.isShowComplementFeatures())
1060 if (!Comparison.isGap(sequence.getCharAt(column)))
1062 AlignViewportI complement = ap.getAlignViewport()
1063 .getCodingComplement();
1064 AlignFrame af = Desktop.getAlignFrameFor(complement);
1065 FeatureRendererModel fr2 = af.getFeatureRenderer();
1066 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1070 unshownFeatures = seqARep.appendFeaturesLengthLimit(
1071 tooltipText, pos, mf, fr2,
1072 MAX_TOOLTIP_LENGTH);
1077 if (tooltipText.length() == 6) // "<html>"
1079 setToolTipText(null);
1084 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1086 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1087 tooltipText.append("...");
1089 if (unshownFeatures > 0)
1091 tooltipText.append("<br/>").append("... ").append("<i>")
1092 .append(MessageManager.formatMessage(
1093 "label.features_not_shown", unshownFeatures))
1096 String textString = tooltipText.toString();
1097 if (lastTooltip == null || !lastTooltip.equals(textString))
1099 String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1101 setToolTipText(formattedTooltipText);
1102 lastTooltip = textString;
1108 * When the view is in wrapped mode, and the mouse is over an annotation row,
1109 * shows the corresponding tooltip and status message (if any)
1114 protected void mouseMovedOverAnnotation(MousePos pos)
1116 final int column = pos.column;
1117 final int rowIndex = pos.annotationIndex;
1119 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1124 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1126 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1128 setToolTipText(tooltip);
1129 lastTooltip = tooltip;
1131 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1133 ap.alignFrame.setStatus(msg);
1136 private Point lastp = null;
1141 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1144 public Point getToolTipLocation(MouseEvent event)
1146 if (tooltipText == null || tooltipText.length() <= 6)
1152 int x = event.getX();
1154 // switch sides when tooltip is too close to edge
1155 int wdth = (w - x < 200) ? -(w / 2) : 5;
1157 if (!event.isShiftDown() || p == null)
1159 p = new Point(event.getX() + wdth, event.getY() - 20);
1163 * TODO: try to set position so region is not obscured by tooltip
1171 * set when the current UI interaction has resulted in a change that requires
1172 * shading in overviews and structures to be recalculated. this could be
1173 * changed to a something more expressive that indicates what actually has
1174 * changed, so selective redraws can be applied (ie. only structures, only
1177 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1180 * set if av.getSelectionGroup() refers to a group that is defined on the
1181 * alignment view, rather than a transient selection
1183 // private boolean editingDefinedGroup = false; // TODO: refactor to
1184 // avcontroller or viewModel
1187 * Sets the status message in alignment panel, showing the sequence number
1188 * (index) and id, and residue and residue position if not at a gap, for the
1189 * given sequence and column position. Returns the residue position returned
1190 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1191 * if at a gapped position.
1194 * aligned sequence object
1198 * index of sequence in alignment
1199 * @return sequence position of residue at column, or adjacent residue if at a
1202 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1204 char sequenceChar = sequence.getCharAt(column);
1205 int pos = sequence.findPosition(column);
1206 setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1212 * Builds the status message for the current cursor location and writes it to
1213 * the status bar, for example
1216 * Sequence 3 ID: FER1_SOLLC
1217 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1218 * Sequence 5 ID: FER1_PEA Residue: B (3)
1219 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1224 * sequence position in the alignment (1..)
1225 * @param sequenceChar
1226 * the character under the cursor
1228 * the sequence residue position (if not over a gap)
1230 protected void setStatusMessage(SequenceI sequence, int seqIndex,
1231 char sequenceChar, int residuePos)
1233 StringBuilder text = new StringBuilder(32);
1236 * Sequence number (if known), and sequence name.
1238 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1239 text.append("Sequence").append(seqno).append(" ID: ")
1240 .append(sequence.getName());
1242 String residue = null;
1245 * Try to translate the display character to residue name (null for gap).
1247 boolean isGapped = Comparison.isGap(sequenceChar);
1251 boolean nucleotide = av.getAlignment().isNucleotide();
1252 String displayChar = String.valueOf(sequenceChar);
1255 residue = ResidueProperties.nucleotideName.get(displayChar);
1259 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1260 : ("*".equals(displayChar) ? "STOP"
1261 : ResidueProperties.aa2Triplet.get(displayChar));
1263 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1264 .append(": ").append(residue == null ? displayChar : residue);
1266 text.append(" (").append(Integer.toString(residuePos)).append(")");
1268 ap.alignFrame.setStatus(text.toString());
1272 * Set the status bar message to highlight the first matched position in
1277 private void setStatusMessage(SearchResultsI results)
1279 AlignmentI al = this.av.getAlignment();
1280 int sequenceIndex = al.findIndex(results);
1281 if (sequenceIndex == -1)
1285 SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1286 for (SearchResultMatchI m : results.getResults())
1288 SequenceI seq = m.getSequence();
1289 if (seq.getDatasetSequence() != null)
1291 seq = seq.getDatasetSequence();
1296 int start = m.getStart();
1297 setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1308 public void mouseDragged(MouseEvent evt)
1310 MousePos pos = findMousePosition(evt);
1311 if (pos.isOverAnnotation() || pos.column == -1)
1316 if (mouseWheelPressed)
1318 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1319 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1321 int oldWidth = av.getCharWidth();
1323 // Which is bigger, left-right or up-down?
1324 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1325 .abs(evt.getX() - lastMousePress.getX()))
1328 * on drag up or down, decrement or increment font size
1330 int fontSize = av.font.getSize();
1331 boolean fontChanged = false;
1333 if (evt.getY() < lastMousePress.getY())
1338 else if (evt.getY() > lastMousePress.getY())
1351 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1353 av.setFont(newFont, true);
1354 av.setCharWidth(oldWidth);
1358 ap.av.getCodingComplement().setFont(newFont, true);
1359 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1360 .getSplitViewContainer();
1361 splitFrame.adjustLayout();
1362 splitFrame.repaint();
1369 * on drag left or right, decrement or increment character width
1372 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1374 newWidth = av.getCharWidth() - 1;
1375 av.setCharWidth(newWidth);
1377 else if (evt.getX() > lastMousePress.getX())
1379 newWidth = av.getCharWidth() + 1;
1380 av.setCharWidth(newWidth);
1384 ap.paintAlignment(false, false);
1388 * need to ensure newWidth is set on cdna, regardless of which
1389 * panel the mouse drag happened in; protein will compute its
1390 * character width as 1:1 or 3:1
1392 av.getCodingComplement().setCharWidth(newWidth);
1393 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1394 .getSplitViewContainer();
1395 splitFrame.adjustLayout();
1396 splitFrame.repaint();
1401 FontMetrics fm = getFontMetrics(av.getFont());
1402 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1404 lastMousePress = evt.getPoint();
1411 dragStretchGroup(evt);
1415 int res = pos.column;
1422 if ((editLastRes == -1) || (editLastRes == res))
1427 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1429 // dragLeft, delete gap
1430 editSequence(false, false, res);
1434 editSequence(true, false, res);
1437 mouseDragging = true;
1438 if (scrollThread != null)
1440 scrollThread.setMousePosition(evt.getPoint());
1445 * Edits the sequence to insert or delete one or more gaps, in response to a
1446 * mouse drag or cursor mode command. The number of inserts/deletes may be
1447 * specified with the cursor command, or else depends on the mouse event
1448 * (normally one column, but potentially more for a fast mouse drag).
1450 * Delete gaps is limited to the number of gaps left of the cursor position
1451 * (mouse drag), or at or right of the cursor position (cursor mode).
1453 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1454 * the current selection group.
1456 * In locked editing mode (with a selection group present), inserts/deletions
1457 * within the selection group are limited to its boundaries (and edits outside
1458 * the group stop at its border).
1461 * true to insert gaps, false to delete gaps
1463 * (unused parameter)
1465 * the column at which to perform the action; the number of columns
1466 * affected depends on <code>this.editLastRes</code> (cursor column
1469 synchronized void editSequence(boolean insertGap, boolean editSeq,
1473 int fixedRight = -1;
1474 boolean fixedColumns = false;
1475 SequenceGroup sg = av.getSelectionGroup();
1477 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1479 // No group, but the sequence may represent a group
1480 if (!groupEditing && av.hasHiddenRows())
1482 if (av.isHiddenRepSequence(seq))
1484 sg = av.getRepresentedSequences(seq);
1485 groupEditing = true;
1489 StringBuilder message = new StringBuilder(64); // for status bar
1492 * make a name for the edit action, for
1493 * status bar message and Undo/Redo menu
1495 String label = null;
1498 message.append("Edit group:");
1499 label = MessageManager.getString("action.edit_group");
1503 message.append("Edit sequence: " + seq.getName());
1504 label = seq.getName();
1505 if (label.length() > 10)
1507 label = label.substring(0, 10);
1509 label = MessageManager.formatMessage("label.edit_params",
1515 * initialise the edit command if there is not
1516 * already one being extended
1518 if (editCommand == null)
1520 editCommand = new EditCommand(label);
1525 message.append(" insert ");
1529 message.append(" delete ");
1532 message.append(Math.abs(startres - editLastRes) + " gaps.");
1533 ap.alignFrame.setStatus(message.toString());
1536 * is there a selection group containing the sequence being edited?
1537 * if so the boundary of the group is the limit of the edit
1538 * (but the edit may be inside or outside the selection group)
1540 boolean inSelectionGroup = sg != null
1541 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1542 if (groupEditing || inSelectionGroup)
1544 fixedColumns = true;
1546 // sg might be null as the user may only see 1 sequence,
1547 // but the sequence represents a group
1550 if (!av.isHiddenRepSequence(seq))
1555 sg = av.getRepresentedSequences(seq);
1558 fixedLeft = sg.getStartRes();
1559 fixedRight = sg.getEndRes();
1561 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1562 || (startres >= fixedLeft && editLastRes < fixedLeft)
1563 || (startres > fixedRight && editLastRes <= fixedRight)
1564 || (startres <= fixedRight && editLastRes > fixedRight))
1570 if (fixedLeft > startres)
1572 fixedRight = fixedLeft - 1;
1575 else if (fixedRight < startres)
1577 fixedLeft = fixedRight;
1582 if (av.hasHiddenColumns())
1584 fixedColumns = true;
1585 int y1 = av.getAlignment().getHiddenColumns()
1586 .getNextHiddenBoundary(true, startres);
1587 int y2 = av.getAlignment().getHiddenColumns()
1588 .getNextHiddenBoundary(false, startres);
1590 if ((insertGap && startres > y1 && editLastRes < y1)
1591 || (!insertGap && startres < y2 && editLastRes > y2))
1597 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1598 // Selection spans a hidden region
1599 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1607 fixedRight = y2 - 1;
1612 boolean success = doEditSequence(insertGap, editSeq, startres,
1613 fixedRight, fixedColumns, sg);
1616 * report what actually happened (might be less than
1617 * what was requested), by inspecting the edit commands added
1619 String msg = getEditStatusMessage(editCommand);
1620 ap.alignFrame.setStatus(msg == null ? " " : msg);
1626 editLastRes = startres;
1627 seqCanvas.repaint();
1631 * A helper method that performs the requested editing to insert or delete
1632 * gaps (if possible). Answers true if the edit was successful, false if could
1633 * only be performed in part or not at all. Failure may occur in 'locked edit'
1634 * mode, when an insertion requires a matching gapped position (or column) to
1635 * delete, and deletion requires an adjacent gapped position (or column) to
1639 * true if inserting gap(s), false if deleting
1641 * (unused parameter, currently always false)
1643 * the column at which to perform the edit
1645 * fixed right boundary column of a locked edit (within or to the
1646 * left of a selection group)
1647 * @param fixedColumns
1648 * true if this is a locked edit
1650 * the sequence group (if group edit is being performed)
1653 protected boolean doEditSequence(final boolean insertGap,
1654 final boolean editSeq, final int startres, int fixedRight,
1655 final boolean fixedColumns, final SequenceGroup sg)
1657 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1658 SequenceI[] seqs = new SequenceI[] { seq };
1662 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1663 int g, groupSize = vseqs.size();
1664 SequenceI[] groupSeqs = new SequenceI[groupSize];
1665 for (g = 0; g < groupSeqs.length; g++)
1667 groupSeqs[g] = vseqs.get(g);
1673 // If the user has selected the whole sequence, and is dragging to
1674 // the right, we can still extend the alignment and selectionGroup
1675 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1676 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1679 av.getAlignment().getWidth() + startres - editLastRes);
1680 fixedRight = sg.getEndRes();
1683 // Is it valid with fixed columns??
1684 // Find the next gap before the end
1685 // of the visible region boundary
1686 boolean blank = false;
1687 for (; fixedRight > editLastRes; fixedRight--)
1691 for (g = 0; g < groupSize; g++)
1693 for (int j = 0; j < startres - editLastRes; j++)
1696 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1711 if (sg.getSize() == av.getAlignment().getHeight())
1713 if ((av.hasHiddenColumns()
1714 && startres < av.getAlignment().getHiddenColumns()
1715 .getNextHiddenBoundary(false, startres)))
1720 int alWidth = av.getAlignment().getWidth();
1721 if (av.hasHiddenRows())
1723 int hwidth = av.getAlignment().getHiddenSequences()
1725 if (hwidth > alWidth)
1730 // We can still insert gaps if the selectionGroup
1731 // contains all the sequences
1732 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1733 fixedRight = alWidth + startres - editLastRes;
1743 else if (!insertGap)
1745 // / Are we able to delete?
1746 // ie are all columns blank?
1748 for (g = 0; g < groupSize; g++)
1750 for (int j = startres; j < editLastRes; j++)
1752 if (groupSeqs[g].getLength() <= j)
1757 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1759 // Not a gap, block edit not valid
1768 // dragging to the right
1769 if (fixedColumns && fixedRight != -1)
1771 for (int j = editLastRes; j < startres; j++)
1773 insertGap(j, groupSeqs, fixedRight);
1778 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1779 startres - editLastRes, false);
1784 // dragging to the left
1785 if (fixedColumns && fixedRight != -1)
1787 for (int j = editLastRes; j > startres; j--)
1789 deleteChar(startres, groupSeqs, fixedRight);
1794 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1795 editLastRes - startres, false);
1802 * editing a single sequence
1806 // dragging to the right
1807 if (fixedColumns && fixedRight != -1)
1809 for (int j = editLastRes; j < startres; j++)
1811 if (!insertGap(j, seqs, fixedRight))
1814 * e.g. cursor mode command specified
1815 * more inserts than are possible
1823 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1824 startres - editLastRes, false);
1831 // dragging to the left
1832 if (fixedColumns && fixedRight != -1)
1834 for (int j = editLastRes; j > startres; j--)
1836 if (!Comparison.isGap(seq.getCharAt(startres)))
1840 deleteChar(startres, seqs, fixedRight);
1845 // could be a keyboard edit trying to delete none gaps
1847 for (int m = startres; m < editLastRes; m++)
1849 if (!Comparison.isGap(seq.getCharAt(m)))
1857 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1862 {// insertGap==false AND editSeq==TRUE;
1863 if (fixedColumns && fixedRight != -1)
1865 for (int j = editLastRes; j < startres; j++)
1867 insertGap(j, seqs, fixedRight);
1872 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1873 startres - editLastRes, false);
1883 * Constructs an informative status bar message while dragging to insert or
1884 * delete gaps. Answers null if inserts and deletes cancel out.
1886 * @param editCommand
1887 * a command containing the list of individual edits
1890 protected static String getEditStatusMessage(EditCommand editCommand)
1892 if (editCommand == null)
1898 * add any inserts, and subtract any deletes,
1899 * not counting those auto-inserted when doing a 'locked edit'
1900 * (so only counting edits 'under the cursor')
1903 for (Edit cmd : editCommand.getEdits())
1905 if (!cmd.isSystemGenerated())
1907 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1915 * inserts and deletes cancel out
1920 String msgKey = count > 1 ? "label.insert_gaps"
1921 : (count == 1 ? "label.insert_gap"
1922 : (count == -1 ? "label.delete_gap"
1923 : "label.delete_gaps"));
1924 count = Math.abs(count);
1926 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1930 * Inserts one gap at column j, deleting the right-most gapped column up to
1931 * (and including) fixedColumn. Returns true if the edit is successful, false
1932 * if no blank column is available to allow the insertion to be balanced by a
1937 * @param fixedColumn
1940 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1942 int blankColumn = fixedColumn;
1943 for (int s = 0; s < seq.length; s++)
1945 // Find the next gap before the end of the visible region boundary
1946 // If lastCol > j, theres a boundary after the gap insertion
1948 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1950 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1952 // Theres a space, so break and insert the gap
1957 if (blankColumn <= j)
1959 blankColumn = fixedColumn;
1965 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1967 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1973 * Helper method to add and perform one edit action
1979 * @param systemGenerated
1980 * true if the edit is a 'balancing' delete (or insert) to match a
1981 * user's insert (or delete) in a locked editing region
1983 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1984 int count, boolean systemGenerated)
1987 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1988 av.getAlignment().getGapCharacter());
1989 edit.setSystemGenerated(systemGenerated);
1991 editCommand.appendEdit(edit, av.getAlignment(), true, null);
1995 * Deletes the character at column j, and inserts a gap at fixedColumn, in
1996 * each of the given sequences. The caller should ensure that all sequences
1997 * are gapped in column j.
2001 * @param fixedColumn
2003 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2005 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2007 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2011 * On reentering the panel, stops any scrolling that was started on dragging
2017 public void mouseEntered(MouseEvent e)
2027 * On leaving the panel, if the mouse is being dragged, starts a thread to
2028 * scroll it until the mouse is released (in unwrapped mode only)
2033 public void mouseExited(MouseEvent e)
2035 lastMousePosition = null;
2036 ap.alignFrame.setStatus(" ");
2037 if (av.getWrapAlignment())
2042 if (mouseDragging && scrollThread == null)
2044 scrollThread = new ScrollThread();
2049 * Handler for double-click on a position with one or more sequence features.
2050 * Opens the Amend Features dialog to allow feature details to be amended, or
2051 * the feature deleted.
2054 public void mouseClicked(MouseEvent evt)
2056 SequenceGroup sg = null;
2057 MousePos pos = findMousePosition(evt);
2058 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2063 if (evt.getClickCount() > 1)
2065 sg = av.getSelectionGroup();
2066 if (sg != null && sg.getSize() == 1
2067 && sg.getEndRes() - sg.getStartRes() < 2)
2069 av.setSelectionGroup(null);
2072 int column = pos.column;
2075 * find features at the position (if not gapped), or straddling
2076 * the position (if at a gap)
2078 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2079 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2080 .findFeaturesAtColumn(sequence, column + 1);
2082 if (!features.isEmpty())
2085 * highlight the first feature at the position on the alignment
2087 SearchResultsI highlight = new SearchResults();
2088 highlight.addResult(sequence, features.get(0).getBegin(), features
2090 seqCanvas.highlightSearchResults(highlight, false);
2093 * open the Amend Features dialog; clear highlighting afterwards,
2094 * whether changes were made or not
2096 List<SequenceI> seqs = Collections.singletonList(sequence);
2097 seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2099 av.setSearchResults(null); // clear highlighting
2100 seqCanvas.repaint(); // draw new/amended features
2106 public void mouseWheelMoved(MouseWheelEvent e)
2109 double wheelRotation = e.getPreciseWheelRotation();
2110 if (wheelRotation > 0)
2112 if (e.isShiftDown())
2114 av.getRanges().scrollRight(true);
2119 av.getRanges().scrollUp(false);
2122 else if (wheelRotation < 0)
2124 if (e.isShiftDown())
2126 av.getRanges().scrollRight(false);
2130 av.getRanges().scrollUp(true);
2135 * update status bar and tooltip for new position
2136 * (need to synthesize a mouse movement to refresh tooltip)
2139 ToolTipManager.sharedInstance().mouseMoved(e);
2148 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2150 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2155 final int res = pos.column;
2156 final int seq = pos.seqIndex;
2158 updateOverviewAndStructs = false;
2160 startWrapBlock = wrappedBlock;
2162 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2164 if ((sequence == null) || (res > sequence.getLength()))
2169 stretchGroup = av.getSelectionGroup();
2171 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2173 stretchGroup = av.getAlignment().findGroup(sequence, res);
2174 if (stretchGroup != null)
2176 // only update the current selection if the popup menu has a group to
2178 av.setSelectionGroup(stretchGroup);
2182 if (evt.isPopupTrigger()) // Mac: mousePressed
2184 showPopupMenu(evt, pos);
2189 * defer right-mouse click handling to mouseReleased on Windows
2190 * (where isPopupTrigger() will answer true)
2191 * NB isRightMouseButton is also true for Cmd-click on Mac
2193 if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2200 seqCanvas.cursorX = res;
2201 seqCanvas.cursorY = seq;
2202 seqCanvas.repaint();
2206 if (stretchGroup == null)
2208 createStretchGroup(res, sequence);
2211 if (stretchGroup != null)
2213 stretchGroup.addPropertyChangeListener(seqCanvas);
2216 seqCanvas.repaint();
2219 private void createStretchGroup(int res, SequenceI sequence)
2221 // Only if left mouse button do we want to change group sizes
2222 // define a new group here
2223 SequenceGroup sg = new SequenceGroup();
2224 sg.setStartRes(res);
2226 sg.addSequence(sequence, false);
2227 av.setSelectionGroup(sg);
2230 if (av.getConservationSelected())
2232 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2236 if (av.getAbovePIDThreshold())
2238 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2241 // TODO: stretchGroup will always be not null. Is this a merge error ?
2242 // or is there a threading issue here?
2243 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2245 // Edit end res position of selected group
2246 changeEndRes = true;
2248 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2250 // Edit end res position of selected group
2251 changeStartRes = true;
2253 stretchGroup.getWidth();
2258 * Build and show a pop-up menu at the right-click mouse position
2263 void showPopupMenu(MouseEvent evt, MousePos pos)
2265 final int column = pos.column;
2266 final int seq = pos.seqIndex;
2267 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2268 if (sequence != null)
2270 PopupMenu pop = new PopupMenu(ap, sequence, column);
2271 pop.show(this, evt.getX(), evt.getY());
2276 * Update the display after mouse up on a selection or group
2279 * mouse released event details
2281 * true if this event is happening after a mouse drag (rather than a
2284 protected void doMouseReleasedDefineMode(MouseEvent evt,
2287 if (stretchGroup == null)
2292 stretchGroup.removePropertyChangeListener(seqCanvas);
2294 // always do this - annotation has own state
2295 // but defer colourscheme update until hidden sequences are passed in
2296 boolean vischange = stretchGroup.recalcConservation(true);
2297 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2299 if (stretchGroup.cs != null)
2303 stretchGroup.cs.alignmentChanged(stretchGroup,
2304 av.getHiddenRepSequences());
2307 ResidueShaderI groupColourScheme = stretchGroup
2308 .getGroupColourScheme();
2309 String name = stretchGroup.getName();
2310 if (stretchGroup.cs.conservationApplied())
2312 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2314 if (stretchGroup.cs.getThreshold() > 0)
2316 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2319 PaintRefresher.Refresh(this, av.getSequenceSetId());
2320 // TODO: structure colours only need updating if stretchGroup used to or now
2321 // does contain sequences with structure views
2322 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2323 updateOverviewAndStructs = false;
2324 changeEndRes = false;
2325 changeStartRes = false;
2326 stretchGroup = null;
2331 * Resizes the borders of a selection group depending on the direction of
2336 protected void dragStretchGroup(MouseEvent evt)
2338 if (stretchGroup == null)
2343 MousePos pos = findMousePosition(evt);
2344 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2349 int res = pos.column;
2350 int y = pos.seqIndex;
2352 if (wrappedBlock != startWrapBlock)
2357 res = Math.min(res, av.getAlignment().getWidth()-1);
2359 if (stretchGroup.getEndRes() == res)
2361 // Edit end res position of selected group
2362 changeEndRes = true;
2364 else if (stretchGroup.getStartRes() == res)
2366 // Edit start res position of selected group
2367 changeStartRes = true;
2370 if (res < av.getRanges().getStartRes())
2372 res = av.getRanges().getStartRes();
2377 if (res > (stretchGroup.getStartRes() - 1))
2379 stretchGroup.setEndRes(res);
2380 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2383 else if (changeStartRes)
2385 if (res < (stretchGroup.getEndRes() + 1))
2387 stretchGroup.setStartRes(res);
2388 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2392 int dragDirection = 0;
2398 else if (y < oldSeq)
2403 while ((y != oldSeq) && (oldSeq > -1)
2404 && (y < av.getAlignment().getHeight()))
2406 // This routine ensures we don't skip any sequences, as the
2407 // selection is quite slow.
2408 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2410 oldSeq += dragDirection;
2417 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2419 if (stretchGroup.getSequences(null).contains(nextSeq))
2421 stretchGroup.deleteSequence(seq, false);
2422 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2428 stretchGroup.addSequence(seq, false);
2431 stretchGroup.addSequence(nextSeq, false);
2432 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2441 mouseDragging = true;
2443 if (scrollThread != null)
2445 scrollThread.setMousePosition(evt.getPoint());
2449 * construct a status message showing the range of the selection
2451 StringBuilder status = new StringBuilder(64);
2452 List<SequenceI> seqs = stretchGroup.getSequences();
2453 String name = seqs.get(0).getName();
2454 if (name.length() > 20)
2456 name = name.substring(0, 20);
2458 status.append(name).append(" - ");
2459 name = seqs.get(seqs.size() - 1).getName();
2460 if (name.length() > 20)
2462 name = name.substring(0, 20);
2464 status.append(name).append(" ");
2465 int startRes = stretchGroup.getStartRes();
2466 status.append(" cols ").append(String.valueOf(startRes + 1))
2468 int endRes = stretchGroup.getEndRes();
2469 status.append(String.valueOf(endRes + 1));
2470 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2471 .append(String.valueOf(endRes - startRes + 1)).append(")");
2472 ap.alignFrame.setStatus(status.toString());
2476 * Stops the scroll thread if it is running
2478 void stopScrolling()
2480 if (scrollThread != null)
2482 scrollThread.stopScrolling();
2483 scrollThread = null;
2485 mouseDragging = false;
2489 * Starts a thread to scroll the alignment, towards a given mouse position
2490 * outside the panel bounds
2494 void startScrolling(Point mousePos)
2496 if (scrollThread == null)
2498 scrollThread = new ScrollThread();
2501 mouseDragging = true;
2502 scrollThread.setMousePosition(mousePos);
2506 * Performs scrolling of the visible alignment left, right, up or down
2508 class ScrollThread extends Thread
2510 private Point mousePos;
2512 private volatile boolean threadRunning = true;
2517 public ScrollThread()
2519 setName("SeqPanel$ScrollThread");
2524 * Sets the position of the mouse that determines the direction of the
2529 public void setMousePosition(Point p)
2535 * Sets a flag that will cause the thread to exit
2537 public void stopScrolling()
2539 threadRunning = false;
2543 * Scrolls the alignment left or right, and/or up or down, depending on the
2544 * last notified mouse position, until the limit of the alignment is
2545 * reached, or a flag is set to stop the scroll
2550 while (threadRunning && mouseDragging)
2552 if (mousePos != null)
2554 boolean scrolled = false;
2555 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2562 // mouse is above this panel - try scroll up
2563 scrolled = ranges.scrollUp(true);
2565 else if (mousePos.y >= getHeight())
2567 // mouse is below this panel - try scroll down
2568 scrolled = ranges.scrollUp(false);
2572 * scroll left or right
2576 scrolled |= ranges.scrollRight(false);
2578 else if (mousePos.x >= getWidth())
2580 scrolled |= ranges.scrollRight(true);
2585 * we have reached the limit of the visible alignment - quit
2587 threadRunning = false;
2588 SeqPanel.this.ap.repaint();
2595 } catch (Exception ex)
2603 * modify current selection according to a received message.
2606 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2607 HiddenColumns hidden, SelectionSource source)
2609 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2610 // handles selection messages...
2611 // TODO: extend config options to allow user to control if selections may be
2612 // shared between viewports.
2613 boolean iSentTheSelection = (av == source
2614 || (source instanceof AlignViewport
2615 && ((AlignmentViewport) source).getSequenceSetId()
2616 .equals(av.getSequenceSetId())));
2618 if (iSentTheSelection)
2620 // respond to our own event by updating dependent dialogs
2621 if (ap.getCalculationDialog() != null)
2623 ap.getCalculationDialog().validateCalcTypes();
2629 // process further ?
2630 if (!av.followSelection)
2636 * Ignore the selection if there is one of our own pending.
2638 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2644 * Check for selection in a view of which this one is a dna/protein
2647 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2652 // do we want to thread this ? (contention with seqsel and colsel locks, I
2655 * only copy colsel if there is a real intersection between
2656 * sequence selection and this panel's alignment
2658 boolean repaint = false;
2659 boolean copycolsel = false;
2661 SequenceGroup sgroup = null;
2662 if (seqsel != null && seqsel.getSize() > 0)
2664 if (av.getAlignment() == null)
2666 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2667 + " ViewId=" + av.getViewId()
2668 + " 's alignment is NULL! returning immediately.");
2671 sgroup = seqsel.intersect(av.getAlignment(),
2672 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2673 if ((sgroup != null && sgroup.getSize() > 0))
2678 if (sgroup != null && sgroup.getSize() > 0)
2680 av.setSelectionGroup(sgroup);
2684 av.setSelectionGroup(null);
2686 av.isSelectionGroupChanged(true);
2691 // the current selection is unset or from a previous message
2692 // so import the new colsel.
2693 if (colsel == null || colsel.isEmpty())
2695 if (av.getColumnSelection() != null)
2697 av.getColumnSelection().clear();
2703 // TODO: shift colSel according to the intersecting sequences
2704 if (av.getColumnSelection() == null)
2706 av.setColumnSelection(new ColumnSelection(colsel));
2710 av.getColumnSelection().setElementsFrom(colsel,
2711 av.getAlignment().getHiddenColumns());
2714 av.isColSelChanged(true);
2718 if (copycolsel && av.hasHiddenColumns()
2719 && (av.getAlignment().getHiddenColumns() == null))
2721 System.err.println("Bad things");
2723 if (repaint) // always true!
2725 // probably finessing with multiple redraws here
2726 PaintRefresher.Refresh(this, av.getSequenceSetId());
2727 // ap.paintAlignment(false);
2730 // lastly, update dependent dialogs
2731 if (ap.getCalculationDialog() != null)
2733 ap.getCalculationDialog().validateCalcTypes();
2739 * If this panel is a cdna/protein translation view of the selection source,
2740 * tries to map the source selection to a local one, and returns true. Else
2747 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2748 ColumnSelection colsel, HiddenColumns hidden,
2749 SelectionSource source)
2751 if (!(source instanceof AlignViewportI))
2755 final AlignViewportI sourceAv = (AlignViewportI) source;
2756 if (sourceAv.getCodingComplement() != av
2757 && av.getCodingComplement() != sourceAv)
2763 * Map sequence selection
2765 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2766 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2767 av.isSelectionGroupChanged(true);
2770 * Map column selection
2772 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2774 ColumnSelection cs = new ColumnSelection();
2775 HiddenColumns hs = new HiddenColumns();
2776 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2777 av.setColumnSelection(cs);
2778 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2780 // lastly, update any dependent dialogs
2781 if (ap.getCalculationDialog() != null)
2783 ap.getCalculationDialog().validateCalcTypes();
2787 * repaint alignment, and also Overview or Structure
2788 * if hidden column selection has changed
2790 ap.paintAlignment(hiddenChanged, hiddenChanged);
2797 * @return null or last search results handled by this panel
2799 public SearchResultsI getLastSearchResults()
2801 return lastSearchResults;