2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import java.awt.BorderLayout;
24 import java.awt.Color;
26 import java.awt.FontMetrics;
27 import java.awt.Point;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.awt.event.MouseEvent;
31 import java.awt.event.MouseListener;
32 import java.awt.event.MouseMotionListener;
33 import java.awt.event.MouseWheelEvent;
34 import java.awt.event.MouseWheelListener;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.List;
39 import javax.swing.JLabel;
40 import javax.swing.JPanel;
41 import javax.swing.JToolTip;
42 import javax.swing.SwingUtilities;
43 import javax.swing.Timer;
44 import javax.swing.ToolTipManager;
46 import jalview.api.AlignViewportI;
47 import jalview.bin.Cache;
48 import jalview.commands.EditCommand;
49 import jalview.commands.EditCommand.Action;
50 import jalview.commands.EditCommand.Edit;
51 import jalview.datamodel.AlignmentAnnotation;
52 import jalview.datamodel.AlignmentI;
53 import jalview.datamodel.ColumnSelection;
54 import jalview.datamodel.HiddenColumns;
55 import jalview.datamodel.MappedFeatures;
56 import jalview.datamodel.SearchResultMatchI;
57 import jalview.datamodel.SearchResults;
58 import jalview.datamodel.SearchResultsI;
59 import jalview.datamodel.Sequence;
60 import jalview.datamodel.SequenceFeature;
61 import jalview.datamodel.SequenceGroup;
62 import jalview.datamodel.SequenceI;
63 import jalview.io.SequenceAnnotationReport;
64 import jalview.renderer.ResidueShaderI;
65 import jalview.schemes.ResidueProperties;
66 import jalview.structure.SelectionListener;
67 import jalview.structure.SelectionSource;
68 import jalview.structure.SequenceListener;
69 import jalview.structure.StructureSelectionManager;
70 import jalview.structure.VamsasSource;
71 import jalview.util.Comparison;
72 import jalview.util.MappingUtils;
73 import jalview.util.MessageManager;
74 import jalview.util.Platform;
75 import jalview.viewmodel.AlignmentViewport;
76 import jalview.viewmodel.ViewportRanges;
77 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
83 * @version $Revision: 1.130 $
85 public class SeqPanel extends JPanel
86 implements MouseListener, MouseMotionListener, MouseWheelListener,
87 SequenceListener, SelectionListener
90 * a class that holds computed mouse position
91 * - column of the alignment (0...)
92 * - sequence offset (0...)
93 * - annotation row offset (0...)
94 * where annotation offset is -1 unless the alignment is shown
95 * in wrapped mode, annotations are shown, and the mouse is
96 * over an annnotation row
101 * alignment column position of cursor (0...)
106 * index in alignment of sequence under cursor,
107 * or nearest above if cursor is not over a sequence
112 * index in annotations array of annotation under the cursor
113 * (only possible in wrapped mode with annotations shown),
114 * or -1 if cursor is not over an annotation row
116 final int annotationIndex;
118 MousePos(int col, int seq, int ann)
122 annotationIndex = ann;
125 boolean isOverAnnotation()
127 return annotationIndex != -1;
131 public boolean equals(Object obj)
133 if (obj == null || !(obj instanceof MousePos))
137 MousePos o = (MousePos) obj;
138 boolean b = (column == o.column && seqIndex == o.seqIndex
139 && annotationIndex == o.annotationIndex);
140 // System.out.println(obj + (b ? "= " : "!= ") + this);
145 * A simple hashCode that ensures that instances that satisfy equals() have
149 public int hashCode()
151 return column + seqIndex + annotationIndex;
155 * toString method for debug output purposes only
158 public String toString()
160 return String.format("c%d:s%d:a%d", column, seqIndex,
165 private static final int MAX_TOOLTIP_LENGTH = 300;
167 public SeqCanvas seqCanvas;
169 public AlignmentPanel ap;
172 * last position for mouseMoved event
174 private MousePos lastMousePosition;
176 protected int editLastRes;
178 protected int editStartSeq;
180 protected AlignViewport av;
182 ScrollThread scrollThread = null;
184 boolean mouseDragging = false;
186 boolean editingSeqs = false;
188 boolean groupEditing = false;
190 // ////////////////////////////////////////
191 // ///Everything below this is for defining the boundary of the rubberband
192 // ////////////////////////////////////////
195 boolean changeEndSeq = false;
197 boolean changeStartSeq = false;
199 boolean changeEndRes = false;
201 boolean changeStartRes = false;
203 SequenceGroup stretchGroup = null;
205 boolean remove = false;
207 Point lastMousePress;
209 boolean mouseWheelPressed = false;
211 StringBuffer keyboardNo1;
213 StringBuffer keyboardNo2;
215 private final SequenceAnnotationReport seqARep;
218 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
219 * - the tooltip is not set again if unchanged
220 * - this is the tooltip text _before_ formatting as html
222 private String lastTooltip;
225 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
226 * - used to decide where to place the tooltip in getTooltipLocation()
227 * - this is the tooltip text _after_ formatting as html
229 private String lastFormattedTooltip;
231 EditCommand editCommand;
233 StructureSelectionManager ssm;
235 SearchResultsI lastSearchResults;
238 * Creates a new SeqPanel object
243 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
245 seqARep = new SequenceAnnotationReport(true);
246 ToolTipManager.sharedInstance().registerComponent(this);
247 ToolTipManager.sharedInstance().setInitialDelay(0);
248 ToolTipManager.sharedInstance().setDismissDelay(10000);
252 setBackground(Color.white);
254 seqCanvas = new SeqCanvas(alignPanel);
255 setLayout(new BorderLayout());
256 add(seqCanvas, BorderLayout.CENTER);
258 this.ap = alignPanel;
260 if (!viewport.isDataset())
262 addMouseMotionListener(this);
263 addMouseListener(this);
264 addMouseWheelListener(this);
265 ssm = viewport.getStructureSelectionManager();
266 ssm.addStructureViewerListener(this);
267 ssm.addSelectionListener(this);
271 int startWrapBlock = -1;
273 int wrappedBlock = -1;
276 * Computes the column and sequence row (and possibly annotation row when in
277 * wrapped mode) for the given mouse position
282 MousePos findMousePosition(MouseEvent evt)
284 int col = findColumn(evt);
289 int charHeight = av.getCharHeight();
290 int alignmentHeight = av.getAlignment().getHeight();
291 if (av.getWrapAlignment())
293 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
294 seqCanvas.getHeight());
297 * yPos modulo height of repeating width
299 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
302 * height of sequences plus space / scale above,
303 * plus gap between sequences and annotations
305 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
306 + alignmentHeight * charHeight
307 + SeqCanvas.SEQS_ANNOTATION_GAP;
308 if (yOffsetPx >= alignmentHeightPixels)
311 * mouse is over annotations; find annotation index, also set
312 * last sequence above (for backwards compatible behaviour)
314 AlignmentAnnotation[] anns = av.getAlignment()
315 .getAlignmentAnnotation();
316 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
317 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
318 seqIndex = alignmentHeight - 1;
323 * mouse is over sequence (or the space above sequences)
325 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
328 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
334 ViewportRanges ranges = av.getRanges();
335 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
336 alignmentHeight - 1);
337 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
340 return new MousePos(col, seqIndex, annIndex);
343 * Returns the aligned sequence position (base 0) at the mouse position, or
344 * the closest visible one
349 int findColumn(MouseEvent evt)
354 final int startRes = av.getRanges().getStartRes();
355 final int charWidth = av.getCharWidth();
357 if (av.getWrapAlignment())
359 int hgap = av.getCharHeight();
360 if (av.getScaleAboveWrapped())
362 hgap += av.getCharHeight();
365 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
366 + hgap + seqCanvas.getAnnotationHeight();
369 y = Math.max(0, y - hgap);
370 x -= seqCanvas.getLabelWidthWest();
373 // mouse is over left scale
377 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
382 if (x >= cwidth * charWidth)
384 // mouse is over right scale
388 wrappedBlock = y / cHeight;
389 wrappedBlock += startRes / cwidth;
390 // allow for wrapped view scrolled right (possible from Overview)
391 int startOffset = startRes % cwidth;
392 res = wrappedBlock * cwidth + startOffset
393 + Math.min(cwidth - 1, x / charWidth);
398 * make sure we calculate relative to visible alignment,
399 * rather than right-hand gutter
401 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
402 res = (x / charWidth) + startRes;
403 res = Math.min(res, av.getRanges().getEndRes());
406 if (av.hasHiddenColumns())
408 res = av.getAlignment().getHiddenColumns()
409 .visibleToAbsoluteColumn(res);
416 * When all of a sequence of edits are complete, put the resulting edit list
417 * on the history stack (undo list), and reset flags for editing in progress.
423 if (editCommand != null && editCommand.getSize() > 0)
425 ap.alignFrame.addHistoryItem(editCommand);
426 av.firePropertyChange("alignment", null,
427 av.getAlignment().getSequences());
432 * Tidy up come what may...
437 groupEditing = false;
446 seqCanvas.cursorY = getKeyboardNo1() - 1;
447 scrollToVisible(true);
450 void setCursorColumn()
452 seqCanvas.cursorX = getKeyboardNo1() - 1;
453 scrollToVisible(true);
456 void setCursorRowAndColumn()
458 if (keyboardNo2 == null)
460 keyboardNo2 = new StringBuffer();
464 seqCanvas.cursorX = getKeyboardNo1() - 1;
465 seqCanvas.cursorY = getKeyboardNo2() - 1;
466 scrollToVisible(true);
470 void setCursorPosition()
472 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
474 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
475 scrollToVisible(true);
478 void moveCursor(int dx, int dy)
480 moveCursor(dx, dy,false);
482 void moveCursor(int dx, int dy, boolean nextWord)
484 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
488 int maxWidth = av.getAlignment().getWidth();
489 int maxHeight=av.getAlignment().getHeight();
490 SequenceI seqAtRow = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
491 // look for next gap or residue
492 boolean isGap = Comparison.isGap(seqAtRow.getCharAt(seqCanvas.cursorX));
493 int p = seqCanvas.cursorX,lastP,r=seqCanvas.cursorY,lastR;
509 seqAtRow = av.getAlignment().getSequenceAt(r);
511 p = nextVisible(hidden, maxWidth, p, dx);
512 } while ((dx != 0 ? p != lastP : r != lastR)
513 && isGap == Comparison.isGap(seqAtRow.getCharAt(p)));
517 int maxWidth = av.getAlignment().getWidth();
518 seqCanvas.cursorX = nextVisible(hidden, maxWidth, seqCanvas.cursorX, dx);
519 seqCanvas.cursorY += dy;
521 scrollToVisible(false);
524 private int nextVisible(HiddenColumns hidden,int maxWidth, int original, int dx)
526 int newCursorX=original+dx;
527 if (av.hasHiddenColumns() && !hidden.isVisible(newCursorX))
529 int visx = hidden.absoluteToVisibleColumn(newCursorX - dx);
530 int[] region = hidden.getRegionWithEdgeAtRes(visx);
532 if (region != null) // just in case
537 newCursorX = region[1] + 1;
542 newCursorX = region[0] - 1;
546 newCursorX = (newCursorX < 0) ? 0 : newCursorX;
547 if (newCursorX >= maxWidth
548 || !hidden.isVisible(newCursorX))
550 newCursorX = original;
555 * Scroll to make the cursor visible in the viewport.
558 * just jump to the location rather than scrolling
560 void scrollToVisible(boolean jump)
562 if (seqCanvas.cursorX < 0)
564 seqCanvas.cursorX = 0;
566 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
568 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
571 if (seqCanvas.cursorY < 0)
573 seqCanvas.cursorY = 0;
575 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
577 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
582 boolean repaintNeeded = true;
585 // only need to repaint if the viewport did not move, as otherwise it will
587 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
592 if (av.getWrapAlignment())
594 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
595 int x = av.getAlignment().getHiddenColumns()
596 .absoluteToVisibleColumn(seqCanvas.cursorX);
597 av.getRanges().scrollToWrappedVisible(x);
601 av.getRanges().scrollToVisible(seqCanvas.cursorX,
606 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
608 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
609 seqCanvas.cursorX, seqCanvas.cursorY);
619 void setSelectionAreaAtCursor(boolean topLeft)
621 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
623 if (av.getSelectionGroup() != null)
625 SequenceGroup sg = av.getSelectionGroup();
626 // Find the top and bottom of this group
627 int min = av.getAlignment().getHeight(), max = 0;
628 for (int i = 0; i < sg.getSize(); i++)
630 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
645 sg.setStartRes(seqCanvas.cursorX);
646 if (sg.getEndRes() < seqCanvas.cursorX)
648 sg.setEndRes(seqCanvas.cursorX);
651 min = seqCanvas.cursorY;
655 sg.setEndRes(seqCanvas.cursorX);
656 if (sg.getStartRes() > seqCanvas.cursorX)
658 sg.setStartRes(seqCanvas.cursorX);
661 max = seqCanvas.cursorY + 1;
666 // Only the user can do this
667 av.setSelectionGroup(null);
671 // Now add any sequences between min and max
672 sg.getSequences(null).clear();
673 for (int i = min; i < max; i++)
675 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
680 if (av.getSelectionGroup() == null)
682 SequenceGroup sg = new SequenceGroup();
683 sg.setStartRes(seqCanvas.cursorX);
684 sg.setEndRes(seqCanvas.cursorX);
685 sg.addSequence(sequence, false);
686 av.setSelectionGroup(sg);
689 ap.paintAlignment(false, false);
693 void insertGapAtCursor(boolean group)
695 groupEditing = group;
696 editStartSeq = seqCanvas.cursorY;
697 editLastRes = seqCanvas.cursorX;
698 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
702 void deleteGapAtCursor(boolean group)
704 groupEditing = group;
705 editStartSeq = seqCanvas.cursorY;
706 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
707 editSequence(false, false, seqCanvas.cursorX);
711 void insertNucAtCursor(boolean group, String nuc)
713 // TODO not called - delete?
714 groupEditing = group;
715 editStartSeq = seqCanvas.cursorY;
716 editLastRes = seqCanvas.cursorX;
717 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
721 void numberPressed(char value)
723 if (keyboardNo1 == null)
725 keyboardNo1 = new StringBuffer();
728 if (keyboardNo2 != null)
730 keyboardNo2.append(value);
734 keyboardNo1.append(value);
742 if (keyboardNo1 != null)
744 int value = Integer.parseInt(keyboardNo1.toString());
748 } catch (Exception x)
759 if (keyboardNo2 != null)
761 int value = Integer.parseInt(keyboardNo2.toString());
765 } catch (Exception x)
779 public void mouseReleased(MouseEvent evt)
781 MousePos pos = findMousePosition(evt);
782 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
787 boolean didDrag = mouseDragging; // did we come here after a drag
788 mouseDragging = false;
789 mouseWheelPressed = false;
791 if (evt.isPopupTrigger()) // Windows: mouseReleased
793 showPopupMenu(evt, pos);
804 doMouseReleasedDefineMode(evt, didDrag);
815 public void mousePressed(MouseEvent evt)
817 lastMousePress = evt.getPoint();
818 MousePos pos = findMousePosition(evt);
819 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
824 if (SwingUtilities.isMiddleMouseButton(evt))
826 mouseWheelPressed = true;
830 boolean isControlDown = Platform.isControlDown(evt);
831 if (evt.isShiftDown() || isControlDown)
841 doMousePressedDefineMode(evt, pos);
845 int seq = pos.seqIndex;
846 int res = pos.column;
848 if ((seq < av.getAlignment().getHeight())
849 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
866 public void mouseOverSequence(SequenceI sequence, int index, int pos)
868 String tmp = sequence.hashCode() + " " + index + " " + pos;
870 if (lastMessage == null || !lastMessage.equals(tmp))
872 // System.err.println("mouseOver Sequence: "+tmp);
873 ssm.mouseOverSequence(sequence, index, pos, av);
879 * Highlight the mapped region described by the search results object (unless
880 * unchanged). This supports highlight of protein while mousing over linked
881 * cDNA and vice versa. The status bar is also updated to show the location of
882 * the start of the highlighted region.
885 public String highlightSequence(SearchResultsI results)
887 if (results == null || results.equals(lastSearchResults))
891 lastSearchResults = results;
893 boolean wasScrolled = false;
895 if (av.isFollowHighlight())
897 // don't allow highlight of protein/cDNA to also scroll a complementary
898 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
899 // over residue to change abruptly, causing highlighted residue in panel 2
900 // to change, causing a scroll in panel 1 etc)
901 ap.setToScrollComplementPanel(false);
902 wasScrolled = ap.scrollToPosition(results);
905 seqCanvas.revalidate();
907 ap.setToScrollComplementPanel(true);
910 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
911 if (seqCanvas.highlightSearchResults(results, fastPaint))
913 setStatusMessage(results);
915 return results.isEmpty() ? null : getHighlightInfo(results);
919 * temporary hack: answers a message suitable to show on structure hover
920 * label. This is normally null. It is a peptide variation description if
922 * <li>results are a single residue in a protein alignment</li>
923 * <li>there is a mapping to a coding sequence (codon)</li>
924 * <li>there are one or more SNP variant features on the codon</li>
926 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
931 private String getHighlightInfo(SearchResultsI results)
934 * ideally, just find mapped CDS (as we don't care about render style here);
935 * for now, go via split frame complement's FeatureRenderer
937 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
938 if (complement == null)
942 AlignFrame af = Desktop.getAlignFrameFor(complement);
943 FeatureRendererModel fr2 = af.getFeatureRenderer();
945 int j = results.getSize();
946 List<String> infos = new ArrayList<>();
947 for (int i = 0; i < j; i++)
949 SearchResultMatchI match = results.getResults().get(i);
950 int pos = match.getStart();
951 if (pos == match.getEnd())
953 SequenceI seq = match.getSequence();
954 SequenceI ds = seq.getDatasetSequence() == null ? seq
955 : seq.getDatasetSequence();
956 MappedFeatures mf = fr2
957 .findComplementFeaturesAtResidue(ds, pos);
960 for (SequenceFeature sf : mf.features)
962 String pv = mf.findProteinVariants(sf);
963 if (pv.length() > 0 && !infos.contains(pv))
976 StringBuilder sb = new StringBuilder();
977 for (String info : infos)
985 return sb.toString();
989 public VamsasSource getVamsasSource()
991 return this.ap == null ? null : this.ap.av;
995 public void updateColours(SequenceI seq, int index)
997 System.out.println("update the seqPanel colours");
1002 * Action on mouse movement is to update the status bar to show the current
1003 * sequence position, and (if features are shown) to show any features at the
1004 * position in a tooltip. Does nothing if the mouse move does not change
1010 public void mouseMoved(MouseEvent evt)
1014 // This is because MacOSX creates a mouseMoved
1015 // If control is down, other platforms will not.
1019 final MousePos mousePos = findMousePosition(evt);
1020 if (mousePos.equals(lastMousePosition))
1023 * just a pixel move without change of 'cell'
1025 moveTooltip = false;
1029 lastMousePosition = mousePos;
1031 if (mousePos.isOverAnnotation())
1033 mouseMovedOverAnnotation(mousePos);
1036 final int seq = mousePos.seqIndex;
1038 final int column = mousePos.column;
1039 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1041 lastMousePosition = null;
1042 setToolTipText(null);
1044 lastFormattedTooltip = null;
1045 ap.alignFrame.setStatus("");
1049 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1051 if (column >= sequence.getLength())
1057 * set status bar message, returning residue position in sequence
1059 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1060 final int pos = setStatusMessage(sequence, column, seq);
1061 if (ssm != null && !isGapped)
1063 mouseOverSequence(sequence, column, pos);
1066 StringBuilder tooltipText = new StringBuilder(64);
1068 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1071 for (int g = 0; g < groups.length; g++)
1073 if (groups[g].getStartRes() <= column
1074 && groups[g].getEndRes() >= column)
1076 if (!groups[g].getName().startsWith("JTreeGroup")
1077 && !groups[g].getName().startsWith("JGroup"))
1079 tooltipText.append(groups[g].getName());
1082 if (groups[g].getDescription() != null)
1084 tooltipText.append(": " + groups[g].getDescription());
1091 * add any features at the position to the tooltip; if over a gap, only
1092 * add features that straddle the gap (pos may be the residue before or
1095 int unshownFeatures = 0;
1096 if (av.isShowSequenceFeatures())
1098 List<SequenceFeature> features = ap.getFeatureRenderer()
1099 .findFeaturesAtColumn(sequence, column + 1);
1100 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1101 features, this.ap.getSeqPanel().seqCanvas.fr,
1102 MAX_TOOLTIP_LENGTH);
1105 * add features in CDS/protein complement at the corresponding
1106 * position if configured to do so
1108 if (av.isShowComplementFeatures())
1110 if (!Comparison.isGap(sequence.getCharAt(column)))
1112 AlignViewportI complement = ap.getAlignViewport()
1113 .getCodingComplement();
1114 AlignFrame af = Desktop.getAlignFrameFor(complement);
1115 FeatureRendererModel fr2 = af.getFeatureRenderer();
1116 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1120 unshownFeatures = seqARep.appendFeatures(tooltipText,
1121 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1126 if (tooltipText.length() == 0) // nothing added
1128 setToolTipText(null);
1133 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1135 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1136 tooltipText.append("...");
1138 if (unshownFeatures > 0)
1140 tooltipText.append("<br/>").append("... ").append("<i>")
1141 .append(MessageManager.formatMessage(
1142 "label.features_not_shown", unshownFeatures))
1145 String textString = tooltipText.toString();
1146 if (!textString.equals(lastTooltip))
1148 lastTooltip = textString;
1149 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1151 setToolTipText(lastFormattedTooltip);
1157 * When the view is in wrapped mode, and the mouse is over an annotation row,
1158 * shows the corresponding tooltip and status message (if any)
1163 protected void mouseMovedOverAnnotation(MousePos pos)
1165 final int column = pos.column;
1166 final int rowIndex = pos.annotationIndex;
1168 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1173 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1175 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1177 if (!tooltip.equals(lastTooltip))
1179 lastTooltip = tooltip;
1180 lastFormattedTooltip = tooltip == null ? null
1181 : JvSwingUtils.wrapTooltip(true, tooltip);
1182 setToolTipText(lastFormattedTooltip);
1185 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1187 ap.alignFrame.setStatus(msg);
1191 * if Shift key is held down while moving the mouse,
1192 * the tooltip location is not changed once shown
1194 private Point lastTooltipLocation = null;
1197 * this flag is false for pixel moves within a residue,
1198 * to reduce tooltip flicker
1200 private boolean moveTooltip = true;
1203 * a dummy tooltip used to estimate where to position tooltips
1205 private JToolTip tempTip = new JLabel().createToolTip();
1210 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1213 public Point getToolTipLocation(MouseEvent event)
1217 if (lastTooltip == null || !moveTooltip)
1222 if (lastTooltipLocation != null && event.isShiftDown())
1224 return lastTooltipLocation;
1227 int x = event.getX();
1228 int y = event.getY();
1231 tempTip.setTipText(lastFormattedTooltip);
1232 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1234 // was x += (w - x < 200) ? -(w / 2) : 5;
1235 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1236 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1238 return lastTooltipLocation = p;
1242 * set when the current UI interaction has resulted in a change that requires
1243 * shading in overviews and structures to be recalculated. this could be
1244 * changed to a something more expressive that indicates what actually has
1245 * changed, so selective redraws can be applied (ie. only structures, only
1248 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1251 * set if av.getSelectionGroup() refers to a group that is defined on the
1252 * alignment view, rather than a transient selection
1254 // private boolean editingDefinedGroup = false; // TODO: refactor to
1255 // avcontroller or viewModel
1258 * Sets the status message in alignment panel, showing the sequence number
1259 * (index) and id, and residue and residue position if not at a gap, for the
1260 * given sequence and column position. Returns the residue position returned
1261 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1262 * if at a gapped position.
1265 * aligned sequence object
1269 * index of sequence in alignment
1270 * @return sequence position of residue at column, or adjacent residue if at a
1273 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1275 char sequenceChar = sequence.getCharAt(column);
1276 int pos = sequence.findPosition(column);
1277 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1283 * Builds the status message for the current cursor location and writes it to
1284 * the status bar, for example
1287 * Sequence 3 ID: FER1_SOLLC
1288 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1289 * Sequence 5 ID: FER1_PEA Residue: B (3)
1290 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1295 * sequence position in the alignment (1..)
1296 * @param sequenceChar
1297 * the character under the cursor
1299 * the sequence residue position (if not over a gap)
1301 protected void setStatusMessage(String seqName, int seqIndex,
1302 char sequenceChar, int residuePos)
1304 StringBuilder text = new StringBuilder(32);
1307 * Sequence number (if known), and sequence name.
1309 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1310 text.append("Sequence").append(seqno).append(" ID: ")
1313 String residue = null;
1316 * Try to translate the display character to residue name (null for gap).
1318 boolean isGapped = Comparison.isGap(sequenceChar);
1322 boolean nucleotide = av.getAlignment().isNucleotide();
1323 String displayChar = String.valueOf(sequenceChar);
1326 residue = ResidueProperties.nucleotideName.get(displayChar);
1330 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1331 : ("*".equals(displayChar) ? "STOP"
1332 : ResidueProperties.aa2Triplet.get(displayChar));
1334 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1335 .append(": ").append(residue == null ? displayChar : residue);
1337 text.append(" (").append(Integer.toString(residuePos)).append(")");
1339 ap.alignFrame.setStatus(text.toString());
1343 * Set the status bar message to highlight the first matched position in
1348 private void setStatusMessage(SearchResultsI results)
1350 AlignmentI al = this.av.getAlignment();
1351 int sequenceIndex = al.findIndex(results);
1352 if (sequenceIndex == -1)
1356 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1357 SequenceI ds = alignedSeq.getDatasetSequence();
1358 for (SearchResultMatchI m : results.getResults())
1360 SequenceI seq = m.getSequence();
1361 if (seq.getDatasetSequence() != null)
1363 seq = seq.getDatasetSequence();
1368 int start = m.getStart();
1369 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1370 seq.getCharAt(start - 1), start);
1380 public void mouseDragged(MouseEvent evt)
1382 MousePos pos = findMousePosition(evt);
1383 if (pos.isOverAnnotation() || pos.column == -1)
1388 if (mouseWheelPressed)
1390 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1391 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1393 int oldWidth = av.getCharWidth();
1395 // Which is bigger, left-right or up-down?
1396 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1397 .abs(evt.getX() - lastMousePress.getX()))
1400 * on drag up or down, decrement or increment font size
1402 int fontSize = av.font.getSize();
1403 boolean fontChanged = false;
1405 if (evt.getY() < lastMousePress.getY())
1410 else if (evt.getY() > lastMousePress.getY())
1423 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1425 av.setFont(newFont, true);
1426 av.setCharWidth(oldWidth);
1430 ap.av.getCodingComplement().setFont(newFont, true);
1431 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1432 .getSplitViewContainer();
1433 splitFrame.adjustLayout();
1434 splitFrame.repaint();
1441 * on drag left or right, decrement or increment character width
1444 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1446 newWidth = av.getCharWidth() - 1;
1447 av.setCharWidth(newWidth);
1449 else if (evt.getX() > lastMousePress.getX())
1451 newWidth = av.getCharWidth() + 1;
1452 av.setCharWidth(newWidth);
1456 ap.paintAlignment(false, false);
1460 * need to ensure newWidth is set on cdna, regardless of which
1461 * panel the mouse drag happened in; protein will compute its
1462 * character width as 1:1 or 3:1
1464 av.getCodingComplement().setCharWidth(newWidth);
1465 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1466 .getSplitViewContainer();
1467 splitFrame.adjustLayout();
1468 splitFrame.repaint();
1473 FontMetrics fm = getFontMetrics(av.getFont());
1474 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1476 lastMousePress = evt.getPoint();
1483 dragStretchGroup(evt);
1487 int res = pos.column;
1494 if ((editLastRes == -1) || (editLastRes == res))
1499 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1501 // dragLeft, delete gap
1502 editSequence(false, false, res);
1506 editSequence(true, false, res);
1509 mouseDragging = true;
1510 if (scrollThread != null)
1512 scrollThread.setMousePosition(evt.getPoint());
1517 * Edits the sequence to insert or delete one or more gaps, in response to a
1518 * mouse drag or cursor mode command. The number of inserts/deletes may be
1519 * specified with the cursor command, or else depends on the mouse event
1520 * (normally one column, but potentially more for a fast mouse drag).
1522 * Delete gaps is limited to the number of gaps left of the cursor position
1523 * (mouse drag), or at or right of the cursor position (cursor mode).
1525 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1526 * the current selection group.
1528 * In locked editing mode (with a selection group present), inserts/deletions
1529 * within the selection group are limited to its boundaries (and edits outside
1530 * the group stop at its border).
1533 * true to insert gaps, false to delete gaps
1535 * (unused parameter)
1537 * the column at which to perform the action; the number of columns
1538 * affected depends on <code>this.editLastRes</code> (cursor column
1541 synchronized void editSequence(boolean insertGap, boolean editSeq,
1545 int fixedRight = -1;
1546 boolean fixedColumns = false;
1547 SequenceGroup sg = av.getSelectionGroup();
1549 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1551 // No group, but the sequence may represent a group
1552 if (!groupEditing && av.hasHiddenRows())
1554 if (av.isHiddenRepSequence(seq))
1556 sg = av.getRepresentedSequences(seq);
1557 groupEditing = true;
1561 StringBuilder message = new StringBuilder(64); // for status bar
1564 * make a name for the edit action, for
1565 * status bar message and Undo/Redo menu
1567 String label = null;
1570 message.append("Edit group:");
1571 label = MessageManager.getString("action.edit_group");
1575 message.append("Edit sequence: " + seq.getName());
1576 label = seq.getName();
1577 if (label.length() > 10)
1579 label = label.substring(0, 10);
1581 label = MessageManager.formatMessage("label.edit_params",
1587 * initialise the edit command if there is not
1588 * already one being extended
1590 if (editCommand == null)
1592 editCommand = new EditCommand(label);
1597 message.append(" insert ");
1601 message.append(" delete ");
1604 message.append(Math.abs(startres - editLastRes) + " gaps.");
1605 ap.alignFrame.setStatus(message.toString());
1608 * is there a selection group containing the sequence being edited?
1609 * if so the boundary of the group is the limit of the edit
1610 * (but the edit may be inside or outside the selection group)
1612 boolean inSelectionGroup = sg != null
1613 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1614 if (groupEditing || inSelectionGroup)
1616 fixedColumns = true;
1618 // sg might be null as the user may only see 1 sequence,
1619 // but the sequence represents a group
1622 if (!av.isHiddenRepSequence(seq))
1627 sg = av.getRepresentedSequences(seq);
1630 fixedLeft = sg.getStartRes();
1631 fixedRight = sg.getEndRes();
1633 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1634 || (startres >= fixedLeft && editLastRes < fixedLeft)
1635 || (startres > fixedRight && editLastRes <= fixedRight)
1636 || (startres <= fixedRight && editLastRes > fixedRight))
1642 if (fixedLeft > startres)
1644 fixedRight = fixedLeft - 1;
1647 else if (fixedRight < startres)
1649 fixedLeft = fixedRight;
1654 if (av.hasHiddenColumns())
1656 fixedColumns = true;
1657 int y1 = av.getAlignment().getHiddenColumns()
1658 .getNextHiddenBoundary(true, startres);
1659 int y2 = av.getAlignment().getHiddenColumns()
1660 .getNextHiddenBoundary(false, startres);
1662 if ((insertGap && startres > y1 && editLastRes < y1)
1663 || (!insertGap && startres < y2 && editLastRes > y2))
1669 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1670 // Selection spans a hidden region
1671 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1679 fixedRight = y2 - 1;
1684 boolean success = doEditSequence(insertGap, editSeq, startres,
1685 fixedRight, fixedColumns, sg);
1688 * report what actually happened (might be less than
1689 * what was requested), by inspecting the edit commands added
1691 String msg = getEditStatusMessage(editCommand);
1692 ap.alignFrame.setStatus(msg == null ? " " : msg);
1698 editLastRes = startres;
1699 seqCanvas.repaint();
1703 * A helper method that performs the requested editing to insert or delete
1704 * gaps (if possible). Answers true if the edit was successful, false if could
1705 * only be performed in part or not at all. Failure may occur in 'locked edit'
1706 * mode, when an insertion requires a matching gapped position (or column) to
1707 * delete, and deletion requires an adjacent gapped position (or column) to
1711 * true if inserting gap(s), false if deleting
1713 * (unused parameter, currently always false)
1715 * the column at which to perform the edit
1717 * fixed right boundary column of a locked edit (within or to the
1718 * left of a selection group)
1719 * @param fixedColumns
1720 * true if this is a locked edit
1722 * the sequence group (if group edit is being performed)
1725 protected boolean doEditSequence(final boolean insertGap,
1726 final boolean editSeq, final int startres, int fixedRight,
1727 final boolean fixedColumns, final SequenceGroup sg)
1729 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1730 SequenceI[] seqs = new SequenceI[] { seq };
1734 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1735 int g, groupSize = vseqs.size();
1736 SequenceI[] groupSeqs = new SequenceI[groupSize];
1737 for (g = 0; g < groupSeqs.length; g++)
1739 groupSeqs[g] = vseqs.get(g);
1745 // If the user has selected the whole sequence, and is dragging to
1746 // the right, we can still extend the alignment and selectionGroup
1747 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1748 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1751 av.getAlignment().getWidth() + startres - editLastRes);
1752 fixedRight = sg.getEndRes();
1755 // Is it valid with fixed columns??
1756 // Find the next gap before the end
1757 // of the visible region boundary
1758 boolean blank = false;
1759 for (; fixedRight > editLastRes; fixedRight--)
1763 for (g = 0; g < groupSize; g++)
1765 for (int j = 0; j < startres - editLastRes; j++)
1768 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1783 if (sg.getSize() == av.getAlignment().getHeight())
1785 if ((av.hasHiddenColumns()
1786 && startres < av.getAlignment().getHiddenColumns()
1787 .getNextHiddenBoundary(false, startres)))
1792 int alWidth = av.getAlignment().getWidth();
1793 if (av.hasHiddenRows())
1795 int hwidth = av.getAlignment().getHiddenSequences()
1797 if (hwidth > alWidth)
1802 // We can still insert gaps if the selectionGroup
1803 // contains all the sequences
1804 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1805 fixedRight = alWidth + startres - editLastRes;
1815 else if (!insertGap)
1817 // / Are we able to delete?
1818 // ie are all columns blank?
1820 for (g = 0; g < groupSize; g++)
1822 for (int j = startres; j < editLastRes; j++)
1824 if (groupSeqs[g].getLength() <= j)
1829 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1831 // Not a gap, block edit not valid
1840 // dragging to the right
1841 if (fixedColumns && fixedRight != -1)
1843 for (int j = editLastRes; j < startres; j++)
1845 insertGap(j, groupSeqs, fixedRight);
1850 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1851 startres - editLastRes, false);
1856 // dragging to the left
1857 if (fixedColumns && fixedRight != -1)
1859 for (int j = editLastRes; j > startres; j--)
1861 deleteChar(startres, groupSeqs, fixedRight);
1866 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1867 editLastRes - startres, false);
1874 * editing a single sequence
1878 // dragging to the right
1879 if (fixedColumns && fixedRight != -1)
1881 for (int j = editLastRes; j < startres; j++)
1883 if (!insertGap(j, seqs, fixedRight))
1886 * e.g. cursor mode command specified
1887 * more inserts than are possible
1895 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1896 startres - editLastRes, false);
1903 // dragging to the left
1904 if (fixedColumns && fixedRight != -1)
1906 for (int j = editLastRes; j > startres; j--)
1908 if (!Comparison.isGap(seq.getCharAt(startres)))
1912 deleteChar(startres, seqs, fixedRight);
1917 // could be a keyboard edit trying to delete none gaps
1919 for (int m = startres; m < editLastRes; m++)
1921 if (!Comparison.isGap(seq.getCharAt(m)))
1929 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1934 {// insertGap==false AND editSeq==TRUE;
1935 if (fixedColumns && fixedRight != -1)
1937 for (int j = editLastRes; j < startres; j++)
1939 insertGap(j, seqs, fixedRight);
1944 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1945 startres - editLastRes, false);
1955 * Constructs an informative status bar message while dragging to insert or
1956 * delete gaps. Answers null if inserts and deletes cancel out.
1958 * @param editCommand
1959 * a command containing the list of individual edits
1962 protected static String getEditStatusMessage(EditCommand editCommand)
1964 if (editCommand == null)
1970 * add any inserts, and subtract any deletes,
1971 * not counting those auto-inserted when doing a 'locked edit'
1972 * (so only counting edits 'under the cursor')
1975 for (Edit cmd : editCommand.getEdits())
1977 if (!cmd.isSystemGenerated())
1979 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1987 * inserts and deletes cancel out
1992 String msgKey = count > 1 ? "label.insert_gaps"
1993 : (count == 1 ? "label.insert_gap"
1994 : (count == -1 ? "label.delete_gap"
1995 : "label.delete_gaps"));
1996 count = Math.abs(count);
1998 return MessageManager.formatMessage(msgKey, String.valueOf(count));
2002 * Inserts one gap at column j, deleting the right-most gapped column up to
2003 * (and including) fixedColumn. Returns true if the edit is successful, false
2004 * if no blank column is available to allow the insertion to be balanced by a
2009 * @param fixedColumn
2012 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
2014 int blankColumn = fixedColumn;
2015 for (int s = 0; s < seq.length; s++)
2017 // Find the next gap before the end of the visible region boundary
2018 // If lastCol > j, theres a boundary after the gap insertion
2020 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
2022 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
2024 // Theres a space, so break and insert the gap
2029 if (blankColumn <= j)
2031 blankColumn = fixedColumn;
2037 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2039 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2045 * Helper method to add and perform one edit action
2051 * @param systemGenerated
2052 * true if the edit is a 'balancing' delete (or insert) to match a
2053 * user's insert (or delete) in a locked editing region
2055 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2056 int count, boolean systemGenerated)
2059 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2060 av.getAlignment().getGapCharacter());
2061 edit.setSystemGenerated(systemGenerated);
2063 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2067 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2068 * each of the given sequences. The caller should ensure that all sequences
2069 * are gapped in column j.
2073 * @param fixedColumn
2075 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2077 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2079 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2083 * On reentering the panel, stops any scrolling that was started on dragging
2089 public void mouseEntered(MouseEvent e)
2099 * On leaving the panel, if the mouse is being dragged, starts a thread to
2100 * scroll it until the mouse is released (in unwrapped mode only)
2105 public void mouseExited(MouseEvent e)
2107 lastMousePosition = null;
2108 ap.alignFrame.setStatus(" ");
2109 if (av.getWrapAlignment())
2114 if (mouseDragging && scrollThread == null)
2116 startScrolling(e.getPoint());
2121 * Handler for double-click on a position with one or more sequence features.
2122 * Opens the Amend Features dialog to allow feature details to be amended, or
2123 * the feature deleted.
2126 public void mouseClicked(MouseEvent evt)
2128 SequenceGroup sg = null;
2129 MousePos pos = findMousePosition(evt);
2130 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2135 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2137 sg = av.getSelectionGroup();
2138 if (sg != null && sg.getSize() == 1
2139 && sg.getEndRes() - sg.getStartRes() < 2)
2141 av.setSelectionGroup(null);
2144 int column = pos.column;
2147 * find features at the position (if not gapped), or straddling
2148 * the position (if at a gap)
2150 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2151 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2152 .findFeaturesAtColumn(sequence, column + 1);
2154 if (!features.isEmpty())
2157 * highlight the first feature at the position on the alignment
2159 SearchResultsI highlight = new SearchResults();
2160 highlight.addResult(sequence, features.get(0).getBegin(), features
2162 seqCanvas.highlightSearchResults(highlight, true);
2165 * open the Amend Features dialog
2167 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2168 false).showDialog();
2174 public void mouseWheelMoved(MouseWheelEvent e)
2177 double wheelRotation = e.getPreciseWheelRotation();
2178 if (wheelRotation > 0)
2180 if (e.isShiftDown())
2182 av.getRanges().scrollRight(true);
2187 av.getRanges().scrollUp(false);
2190 else if (wheelRotation < 0)
2192 if (e.isShiftDown())
2194 av.getRanges().scrollRight(false);
2198 av.getRanges().scrollUp(true);
2203 * update status bar and tooltip for new position
2204 * (need to synthesize a mouse movement to refresh tooltip)
2207 ToolTipManager.sharedInstance().mouseMoved(e);
2216 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2218 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2223 final int res = pos.column;
2224 final int seq = pos.seqIndex;
2226 updateOverviewAndStructs = false;
2228 startWrapBlock = wrappedBlock;
2230 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2232 if ((sequence == null) || (res > sequence.getLength()))
2237 stretchGroup = av.getSelectionGroup();
2239 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2241 stretchGroup = av.getAlignment().findGroup(sequence, res);
2242 if (stretchGroup != null)
2244 // only update the current selection if the popup menu has a group to
2246 av.setSelectionGroup(stretchGroup);
2251 * defer right-mouse click handling to mouseReleased on Windows
2252 * (where isPopupTrigger() will answer true)
2253 * NB isRightMouseButton is also true for Cmd-click on Mac
2255 if (Platform.isWinRightButton(evt))
2260 if (evt.isPopupTrigger()) // Mac: mousePressed
2262 showPopupMenu(evt, pos);
2268 seqCanvas.cursorX = res;
2269 seqCanvas.cursorY = seq;
2270 seqCanvas.repaint();
2274 if (stretchGroup == null)
2276 createStretchGroup(res, sequence);
2279 if (stretchGroup != null)
2281 stretchGroup.addPropertyChangeListener(seqCanvas);
2284 seqCanvas.repaint();
2287 private void createStretchGroup(int res, SequenceI sequence)
2289 // Only if left mouse button do we want to change group sizes
2290 // define a new group here
2291 SequenceGroup sg = new SequenceGroup();
2292 sg.setStartRes(res);
2294 sg.addSequence(sequence, false);
2295 av.setSelectionGroup(sg);
2298 if (av.getConservationSelected())
2300 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2304 if (av.getAbovePIDThreshold())
2306 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2309 // TODO: stretchGroup will always be not null. Is this a merge error ?
2310 // or is there a threading issue here?
2311 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2313 // Edit end res position of selected group
2314 changeEndRes = true;
2316 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2318 // Edit end res position of selected group
2319 changeStartRes = true;
2321 stretchGroup.getWidth();
2326 * Build and show a pop-up menu at the right-click mouse position
2331 void showPopupMenu(MouseEvent evt, MousePos pos)
2333 final int column = pos.column;
2334 final int seq = pos.seqIndex;
2335 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2336 if (sequence != null)
2338 PopupMenu pop = new PopupMenu(ap, sequence, column);
2339 pop.show(this, evt.getX(), evt.getY());
2344 * Update the display after mouse up on a selection or group
2347 * mouse released event details
2349 * true if this event is happening after a mouse drag (rather than a
2352 protected void doMouseReleasedDefineMode(MouseEvent evt,
2355 if (stretchGroup == null)
2360 stretchGroup.removePropertyChangeListener(seqCanvas);
2362 // always do this - annotation has own state
2363 // but defer colourscheme update until hidden sequences are passed in
2364 boolean vischange = stretchGroup.recalcConservation(true);
2365 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2367 if (stretchGroup.cs != null)
2371 stretchGroup.cs.alignmentChanged(stretchGroup,
2372 av.getHiddenRepSequences());
2375 ResidueShaderI groupColourScheme = stretchGroup
2376 .getGroupColourScheme();
2377 String name = stretchGroup.getName();
2378 if (stretchGroup.cs.conservationApplied())
2380 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2382 if (stretchGroup.cs.getThreshold() > 0)
2384 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2387 PaintRefresher.Refresh(this, av.getSequenceSetId());
2388 // TODO: structure colours only need updating if stretchGroup used to or now
2389 // does contain sequences with structure views
2390 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2391 updateOverviewAndStructs = false;
2392 changeEndRes = false;
2393 changeStartRes = false;
2394 stretchGroup = null;
2399 * Resizes the borders of a selection group depending on the direction of
2404 protected void dragStretchGroup(MouseEvent evt)
2406 if (stretchGroup == null)
2411 MousePos pos = findMousePosition(evt);
2412 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2417 int res = pos.column;
2418 int y = pos.seqIndex;
2420 if (wrappedBlock != startWrapBlock)
2425 res = Math.min(res, av.getAlignment().getWidth()-1);
2427 if (stretchGroup.getEndRes() == res)
2429 // Edit end res position of selected group
2430 changeEndRes = true;
2432 else if (stretchGroup.getStartRes() == res)
2434 // Edit start res position of selected group
2435 changeStartRes = true;
2438 if (res < av.getRanges().getStartRes())
2440 res = av.getRanges().getStartRes();
2445 if (res > (stretchGroup.getStartRes() - 1))
2447 stretchGroup.setEndRes(res);
2448 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2451 else if (changeStartRes)
2453 if (res < (stretchGroup.getEndRes() + 1))
2455 stretchGroup.setStartRes(res);
2456 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2460 int dragDirection = 0;
2466 else if (y < oldSeq)
2471 while ((y != oldSeq) && (oldSeq > -1)
2472 && (y < av.getAlignment().getHeight()))
2474 // This routine ensures we don't skip any sequences, as the
2475 // selection is quite slow.
2476 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2478 oldSeq += dragDirection;
2485 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2487 if (stretchGroup.getSequences(null).contains(nextSeq))
2489 stretchGroup.deleteSequence(seq, false);
2490 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2496 stretchGroup.addSequence(seq, false);
2499 stretchGroup.addSequence(nextSeq, false);
2500 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2509 mouseDragging = true;
2511 if (scrollThread != null)
2513 scrollThread.setMousePosition(evt.getPoint());
2517 * construct a status message showing the range of the selection
2519 StringBuilder status = new StringBuilder(64);
2520 List<SequenceI> seqs = stretchGroup.getSequences();
2521 String name = seqs.get(0).getName();
2522 if (name.length() > 20)
2524 name = name.substring(0, 20);
2526 status.append(name).append(" - ");
2527 name = seqs.get(seqs.size() - 1).getName();
2528 if (name.length() > 20)
2530 name = name.substring(0, 20);
2532 status.append(name).append(" ");
2533 int startRes = stretchGroup.getStartRes();
2534 status.append(" cols ").append(String.valueOf(startRes + 1))
2536 int endRes = stretchGroup.getEndRes();
2537 status.append(String.valueOf(endRes + 1));
2538 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2539 .append(String.valueOf(endRes - startRes + 1)).append(")");
2540 ap.alignFrame.setStatus(status.toString());
2544 * Stops the scroll thread if it is running
2546 void stopScrolling()
2548 if (scrollThread != null)
2550 scrollThread.stopScrolling();
2551 scrollThread = null;
2553 mouseDragging = false;
2557 * Starts a thread to scroll the alignment, towards a given mouse position
2558 * outside the panel bounds, unless the alignment is in wrapped mode
2562 void startScrolling(Point mousePos)
2565 * set this.mouseDragging in case this was called from
2566 * a drag in ScalePanel or AnnotationPanel
2568 mouseDragging = true;
2569 if (!av.getWrapAlignment() && scrollThread == null)
2571 scrollThread = new ScrollThread();
2572 scrollThread.setMousePosition(mousePos);
2573 if (Platform.isJS())
2576 * Javascript - run every 20ms until scrolling stopped
2577 * or reaches the limit of scrollable alignment
2579 Timer t = new Timer(20, new ActionListener()
2582 public void actionPerformed(ActionEvent e)
2584 if (scrollThread != null)
2586 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2587 scrollThread.scrollOnce();
2591 t.addActionListener(new ActionListener()
2594 public void actionPerformed(ActionEvent e)
2596 if (scrollThread == null)
2598 // SeqPanel.stopScrolling called
2608 * Java - run in a new thread
2610 scrollThread.start();
2616 * Performs scrolling of the visible alignment left, right, up or down, until
2617 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2618 * limit of the alignment is reached
2620 class ScrollThread extends Thread
2622 private Point mousePos;
2624 private volatile boolean keepRunning = true;
2629 public ScrollThread()
2631 setName("SeqPanel$ScrollThread");
2635 * Sets the position of the mouse that determines the direction of the
2636 * scroll to perform. If this is called as the mouse moves, scrolling should
2637 * respond accordingly. For example, if the mouse is dragged right, scroll
2638 * right should start; if the drag continues down, scroll down should also
2643 public void setMousePosition(Point p)
2649 * Sets a flag that will cause the thread to exit
2651 public void stopScrolling()
2653 keepRunning = false;
2657 * Scrolls the alignment left or right, and/or up or down, depending on the
2658 * last notified mouse position, until the limit of the alignment is
2659 * reached, or a flag is set to stop the scroll
2666 if (mousePos != null)
2668 keepRunning = scrollOnce();
2673 } catch (Exception ex)
2677 SeqPanel.this.scrollThread = null;
2683 * <li>one row up, if the mouse is above the panel</li>
2684 * <li>one row down, if the mouse is below the panel</li>
2685 * <li>one column left, if the mouse is left of the panel</li>
2686 * <li>one column right, if the mouse is right of the panel</li>
2688 * Answers true if a scroll was performed, false if not - meaning either
2689 * that the mouse position is within the panel, or the edge of the alignment
2692 boolean scrollOnce()
2695 * quit after mouseUp ensures interrupt in JalviewJS
2702 boolean scrolled = false;
2703 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2710 // mouse is above this panel - try scroll up
2711 scrolled = ranges.scrollUp(true);
2713 else if (mousePos.y >= getHeight())
2715 // mouse is below this panel - try scroll down
2716 scrolled = ranges.scrollUp(false);
2720 * scroll left or right
2724 scrolled |= ranges.scrollRight(false);
2726 else if (mousePos.x >= getWidth())
2728 scrolled |= ranges.scrollRight(true);
2735 * modify current selection according to a received message.
2738 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2739 HiddenColumns hidden, SelectionSource source)
2741 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2742 // handles selection messages...
2743 // TODO: extend config options to allow user to control if selections may be
2744 // shared between viewports.
2745 boolean iSentTheSelection = (av == source
2746 || (source instanceof AlignViewport
2747 && ((AlignmentViewport) source).getSequenceSetId()
2748 .equals(av.getSequenceSetId())));
2750 if (iSentTheSelection)
2752 // respond to our own event by updating dependent dialogs
2753 if (ap.getCalculationDialog() != null)
2755 ap.getCalculationDialog().validateCalcTypes();
2761 // process further ?
2762 if (!av.followSelection)
2768 * Ignore the selection if there is one of our own pending.
2770 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2776 * Check for selection in a view of which this one is a dna/protein
2779 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2784 // do we want to thread this ? (contention with seqsel and colsel locks, I
2787 * only copy colsel if there is a real intersection between
2788 * sequence selection and this panel's alignment
2790 boolean repaint = false;
2791 boolean copycolsel = false;
2793 SequenceGroup sgroup = null;
2794 if (seqsel != null && seqsel.getSize() > 0)
2796 if (av.getAlignment() == null)
2798 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2799 + " ViewId=" + av.getViewId()
2800 + " 's alignment is NULL! returning immediately.");
2803 sgroup = seqsel.intersect(av.getAlignment(),
2804 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2805 if ((sgroup != null && sgroup.getSize() > 0))
2810 if (sgroup != null && sgroup.getSize() > 0)
2812 av.setSelectionGroup(sgroup);
2816 av.setSelectionGroup(null);
2818 av.isSelectionGroupChanged(true);
2823 // the current selection is unset or from a previous message
2824 // so import the new colsel.
2825 if (colsel == null || colsel.isEmpty())
2827 if (av.getColumnSelection() != null)
2829 av.getColumnSelection().clear();
2835 // TODO: shift colSel according to the intersecting sequences
2836 if (av.getColumnSelection() == null)
2838 av.setColumnSelection(new ColumnSelection(colsel));
2842 av.getColumnSelection().setElementsFrom(colsel,
2843 av.getAlignment().getHiddenColumns());
2846 av.isColSelChanged(true);
2850 if (copycolsel && av.hasHiddenColumns()
2851 && (av.getAlignment().getHiddenColumns() == null))
2853 System.err.println("Bad things");
2855 if (repaint) // always true!
2857 // probably finessing with multiple redraws here
2858 PaintRefresher.Refresh(this, av.getSequenceSetId());
2859 // ap.paintAlignment(false);
2862 // lastly, update dependent dialogs
2863 if (ap.getCalculationDialog() != null)
2865 ap.getCalculationDialog().validateCalcTypes();
2871 * If this panel is a cdna/protein translation view of the selection source,
2872 * tries to map the source selection to a local one, and returns true. Else
2879 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2880 ColumnSelection colsel, HiddenColumns hidden,
2881 SelectionSource source)
2883 if (!(source instanceof AlignViewportI))
2887 final AlignViewportI sourceAv = (AlignViewportI) source;
2888 if (sourceAv.getCodingComplement() != av
2889 && av.getCodingComplement() != sourceAv)
2895 * Map sequence selection
2897 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2898 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2899 av.isSelectionGroupChanged(true);
2902 * Map column selection
2904 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2906 ColumnSelection cs = new ColumnSelection();
2907 HiddenColumns hs = new HiddenColumns();
2908 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2909 av.setColumnSelection(cs);
2910 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2912 // lastly, update any dependent dialogs
2913 if (ap.getCalculationDialog() != null)
2915 ap.getCalculationDialog().validateCalcTypes();
2919 * repaint alignment, and also Overview or Structure
2920 * if hidden column selection has changed
2922 ap.paintAlignment(hiddenChanged, hiddenChanged);
2929 * @return null or last search results handled by this panel
2931 public SearchResultsI getLastSearchResults()
2933 return lastSearchResults;