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 java.net.URL linkImageURL;
217 private final SequenceAnnotationReport seqARep;
220 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
221 * - the tooltip is not set again if unchanged
222 * - this is the tooltip text _before_ formatting as html
224 private String lastTooltip;
227 * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
228 * - used to decide where to place the tooltip in getTooltipLocation()
229 * - this is the tooltip text _after_ formatting as html
231 private String lastFormattedTooltip;
233 EditCommand editCommand;
235 StructureSelectionManager ssm;
237 SearchResultsI lastSearchResults;
240 * Creates a new SeqPanel object
245 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
247 linkImageURL = getClass().getResource("/images/link.gif");
248 seqARep = new SequenceAnnotationReport(linkImageURL.toString());
249 ToolTipManager.sharedInstance().registerComponent(this);
250 ToolTipManager.sharedInstance().setInitialDelay(0);
251 ToolTipManager.sharedInstance().setDismissDelay(10000);
255 setBackground(Color.white);
257 seqCanvas = new SeqCanvas(alignPanel);
258 setLayout(new BorderLayout());
259 add(seqCanvas, BorderLayout.CENTER);
261 this.ap = alignPanel;
263 if (!viewport.isDataset())
265 addMouseMotionListener(this);
266 addMouseListener(this);
267 addMouseWheelListener(this);
268 ssm = viewport.getStructureSelectionManager();
269 ssm.addStructureViewerListener(this);
270 ssm.addSelectionListener(this);
274 int startWrapBlock = -1;
276 int wrappedBlock = -1;
279 * Computes the column and sequence row (and possibly annotation row when in
280 * wrapped mode) for the given mouse position
285 MousePos findMousePosition(MouseEvent evt)
287 int col = findColumn(evt);
292 int charHeight = av.getCharHeight();
293 int alignmentHeight = av.getAlignment().getHeight();
294 if (av.getWrapAlignment())
296 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
297 seqCanvas.getHeight());
300 * yPos modulo height of repeating width
302 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
305 * height of sequences plus space / scale above,
306 * plus gap between sequences and annotations
308 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
309 + alignmentHeight * charHeight
310 + SeqCanvas.SEQS_ANNOTATION_GAP;
311 if (yOffsetPx >= alignmentHeightPixels)
314 * mouse is over annotations; find annotation index, also set
315 * last sequence above (for backwards compatible behaviour)
317 AlignmentAnnotation[] anns = av.getAlignment()
318 .getAlignmentAnnotation();
319 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
320 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
321 seqIndex = alignmentHeight - 1;
326 * mouse is over sequence (or the space above sequences)
328 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
331 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
337 ViewportRanges ranges = av.getRanges();
338 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
339 alignmentHeight - 1);
340 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
343 return new MousePos(col, seqIndex, annIndex);
346 * Returns the aligned sequence position (base 0) at the mouse position, or
347 * the closest visible one
352 int findColumn(MouseEvent evt)
357 final int startRes = av.getRanges().getStartRes();
358 final int charWidth = av.getCharWidth();
360 if (av.getWrapAlignment())
362 int hgap = av.getCharHeight();
363 if (av.getScaleAboveWrapped())
365 hgap += av.getCharHeight();
368 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
369 + hgap + seqCanvas.getAnnotationHeight();
372 y = Math.max(0, y - hgap);
373 x -= seqCanvas.getLabelWidthWest();
376 // mouse is over left scale
380 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
385 if (x >= cwidth * charWidth)
387 // mouse is over right scale
391 wrappedBlock = y / cHeight;
392 wrappedBlock += startRes / cwidth;
393 // allow for wrapped view scrolled right (possible from Overview)
394 int startOffset = startRes % cwidth;
395 res = wrappedBlock * cwidth + startOffset
396 + Math.min(cwidth - 1, x / charWidth);
401 * make sure we calculate relative to visible alignment,
402 * rather than right-hand gutter
404 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
405 res = (x / charWidth) + startRes;
406 res = Math.min(res, av.getRanges().getEndRes());
409 if (av.hasHiddenColumns())
411 res = av.getAlignment().getHiddenColumns()
412 .visibleToAbsoluteColumn(res);
419 * When all of a sequence of edits are complete, put the resulting edit list
420 * on the history stack (undo list), and reset flags for editing in progress.
426 if (editCommand != null && editCommand.getSize() > 0)
428 ap.alignFrame.addHistoryItem(editCommand);
429 av.firePropertyChange("alignment", null,
430 av.getAlignment().getSequences());
435 * Tidy up come what may...
440 groupEditing = false;
449 seqCanvas.cursorY = getKeyboardNo1() - 1;
450 scrollToVisible(true);
453 void setCursorColumn()
455 seqCanvas.cursorX = getKeyboardNo1() - 1;
456 scrollToVisible(true);
459 void setCursorRowAndColumn()
461 if (keyboardNo2 == null)
463 keyboardNo2 = new StringBuffer();
467 seqCanvas.cursorX = getKeyboardNo1() - 1;
468 seqCanvas.cursorY = getKeyboardNo2() - 1;
469 scrollToVisible(true);
473 void setCursorPosition()
475 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
477 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
478 scrollToVisible(true);
481 void moveCursor(int dx, int dy)
483 seqCanvas.cursorX += dx;
484 seqCanvas.cursorY += dy;
486 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
488 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
490 int original = seqCanvas.cursorX - dx;
491 int maxWidth = av.getAlignment().getWidth();
493 if (!hidden.isVisible(seqCanvas.cursorX))
495 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
496 int[] region = hidden.getRegionWithEdgeAtRes(visx);
498 if (region != null) // just in case
503 seqCanvas.cursorX = region[1] + 1;
508 seqCanvas.cursorX = region[0] - 1;
511 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
514 if (seqCanvas.cursorX >= maxWidth
515 || !hidden.isVisible(seqCanvas.cursorX))
517 seqCanvas.cursorX = original;
521 scrollToVisible(false);
525 * Scroll to make the cursor visible in the viewport.
528 * just jump to the location rather than scrolling
530 void scrollToVisible(boolean jump)
532 if (seqCanvas.cursorX < 0)
534 seqCanvas.cursorX = 0;
536 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
538 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
541 if (seqCanvas.cursorY < 0)
543 seqCanvas.cursorY = 0;
545 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
547 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
552 boolean repaintNeeded = true;
555 // only need to repaint if the viewport did not move, as otherwise it will
557 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
562 if (av.getWrapAlignment())
564 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
565 int x = av.getAlignment().getHiddenColumns()
566 .absoluteToVisibleColumn(seqCanvas.cursorX);
567 av.getRanges().scrollToWrappedVisible(x);
571 av.getRanges().scrollToVisible(seqCanvas.cursorX,
576 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
578 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
579 seqCanvas.cursorX, seqCanvas.cursorY);
589 void setSelectionAreaAtCursor(boolean topLeft)
591 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
593 if (av.getSelectionGroup() != null)
595 SequenceGroup sg = av.getSelectionGroup();
596 // Find the top and bottom of this group
597 int min = av.getAlignment().getHeight(), max = 0;
598 for (int i = 0; i < sg.getSize(); i++)
600 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
615 sg.setStartRes(seqCanvas.cursorX);
616 if (sg.getEndRes() < seqCanvas.cursorX)
618 sg.setEndRes(seqCanvas.cursorX);
621 min = seqCanvas.cursorY;
625 sg.setEndRes(seqCanvas.cursorX);
626 if (sg.getStartRes() > seqCanvas.cursorX)
628 sg.setStartRes(seqCanvas.cursorX);
631 max = seqCanvas.cursorY + 1;
636 // Only the user can do this
637 av.setSelectionGroup(null);
641 // Now add any sequences between min and max
642 sg.getSequences(null).clear();
643 for (int i = min; i < max; i++)
645 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
650 if (av.getSelectionGroup() == null)
652 SequenceGroup sg = new SequenceGroup();
653 sg.setStartRes(seqCanvas.cursorX);
654 sg.setEndRes(seqCanvas.cursorX);
655 sg.addSequence(sequence, false);
656 av.setSelectionGroup(sg);
659 ap.paintAlignment(false, false);
663 void insertGapAtCursor(boolean group)
665 groupEditing = group;
666 editStartSeq = seqCanvas.cursorY;
667 editLastRes = seqCanvas.cursorX;
668 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
672 void deleteGapAtCursor(boolean group)
674 groupEditing = group;
675 editStartSeq = seqCanvas.cursorY;
676 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
677 editSequence(false, false, seqCanvas.cursorX);
681 void insertNucAtCursor(boolean group, String nuc)
683 // TODO not called - delete?
684 groupEditing = group;
685 editStartSeq = seqCanvas.cursorY;
686 editLastRes = seqCanvas.cursorX;
687 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
691 void numberPressed(char value)
693 if (keyboardNo1 == null)
695 keyboardNo1 = new StringBuffer();
698 if (keyboardNo2 != null)
700 keyboardNo2.append(value);
704 keyboardNo1.append(value);
712 if (keyboardNo1 != null)
714 int value = Integer.parseInt(keyboardNo1.toString());
718 } catch (Exception x)
729 if (keyboardNo2 != null)
731 int value = Integer.parseInt(keyboardNo2.toString());
735 } catch (Exception x)
749 public void mouseReleased(MouseEvent evt)
751 MousePos pos = findMousePosition(evt);
752 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
757 boolean didDrag = mouseDragging; // did we come here after a drag
758 mouseDragging = false;
759 mouseWheelPressed = false;
761 if (evt.isPopupTrigger()) // Windows: mouseReleased
763 showPopupMenu(evt, pos);
774 doMouseReleasedDefineMode(evt, didDrag);
785 public void mousePressed(MouseEvent evt)
787 lastMousePress = evt.getPoint();
788 MousePos pos = findMousePosition(evt);
789 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
794 if (SwingUtilities.isMiddleMouseButton(evt))
796 mouseWheelPressed = true;
800 boolean isControlDown = Platform.isControlDown(evt);
801 if (evt.isShiftDown() || isControlDown)
811 doMousePressedDefineMode(evt, pos);
815 int seq = pos.seqIndex;
816 int res = pos.column;
818 if ((seq < av.getAlignment().getHeight())
819 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
836 public void mouseOverSequence(SequenceI sequence, int index, int pos)
838 String tmp = sequence.hashCode() + " " + index + " " + pos;
840 if (lastMessage == null || !lastMessage.equals(tmp))
842 // System.err.println("mouseOver Sequence: "+tmp);
843 ssm.mouseOverSequence(sequence, index, pos, av);
849 * Highlight the mapped region described by the search results object (unless
850 * unchanged). This supports highlight of protein while mousing over linked
851 * cDNA and vice versa. The status bar is also updated to show the location of
852 * the start of the highlighted region.
855 public String highlightSequence(SearchResultsI results)
857 if (results == null || results.equals(lastSearchResults))
861 lastSearchResults = results;
863 boolean wasScrolled = false;
865 if (av.isFollowHighlight())
867 // don't allow highlight of protein/cDNA to also scroll a complementary
868 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
869 // over residue to change abruptly, causing highlighted residue in panel 2
870 // to change, causing a scroll in panel 1 etc)
871 ap.setToScrollComplementPanel(false);
872 wasScrolled = ap.scrollToPosition(results);
875 seqCanvas.revalidate();
877 ap.setToScrollComplementPanel(true);
880 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
881 if (seqCanvas.highlightSearchResults(results, fastPaint))
883 setStatusMessage(results);
885 return results.isEmpty() ? null : getHighlightInfo(results);
889 * temporary hack: answers a message suitable to show on structure hover
890 * label. This is normally null. It is a peptide variation description if
892 * <li>results are a single residue in a protein alignment</li>
893 * <li>there is a mapping to a coding sequence (codon)</li>
894 * <li>there are one or more SNP variant features on the codon</li>
896 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
901 private String getHighlightInfo(SearchResultsI results)
904 * ideally, just find mapped CDS (as we don't care about render style here);
905 * for now, go via split frame complement's FeatureRenderer
907 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
908 if (complement == null)
912 AlignFrame af = Desktop.getAlignFrameFor(complement);
913 FeatureRendererModel fr2 = af.getFeatureRenderer();
915 int j = results.getSize();
916 List<String> infos = new ArrayList<>();
917 for (int i = 0; i < j; i++)
919 SearchResultMatchI match = results.getResults().get(i);
920 int pos = match.getStart();
921 if (pos == match.getEnd())
923 SequenceI seq = match.getSequence();
924 SequenceI ds = seq.getDatasetSequence() == null ? seq
925 : seq.getDatasetSequence();
926 MappedFeatures mf = fr2
927 .findComplementFeaturesAtResidue(ds, pos);
930 for (SequenceFeature sf : mf.features)
932 String pv = mf.findProteinVariants(sf);
933 if (pv.length() > 0 && !infos.contains(pv))
946 StringBuilder sb = new StringBuilder();
947 for (String info : infos)
955 return sb.toString();
959 public VamsasSource getVamsasSource()
961 return this.ap == null ? null : this.ap.av;
965 public void updateColours(SequenceI seq, int index)
967 System.out.println("update the seqPanel colours");
972 * Action on mouse movement is to update the status bar to show the current
973 * sequence position, and (if features are shown) to show any features at the
974 * position in a tooltip. Does nothing if the mouse move does not change
980 public void mouseMoved(MouseEvent evt)
984 // This is because MacOSX creates a mouseMoved
985 // If control is down, other platforms will not.
989 final MousePos mousePos = findMousePosition(evt);
990 if (mousePos.equals(lastMousePosition))
993 * just a pixel move without change of 'cell'
999 lastMousePosition = mousePos;
1001 if (mousePos.isOverAnnotation())
1003 mouseMovedOverAnnotation(mousePos);
1006 final int seq = mousePos.seqIndex;
1008 final int column = mousePos.column;
1009 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1011 lastMousePosition = null;
1012 setToolTipText(null);
1014 lastFormattedTooltip = null;
1015 ap.alignFrame.setStatus("");
1019 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1021 if (column >= sequence.getLength())
1027 * set status bar message, returning residue position in sequence
1029 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1030 final int pos = setStatusMessage(sequence, column, seq);
1031 if (ssm != null && !isGapped)
1033 mouseOverSequence(sequence, column, pos);
1036 StringBuilder tooltipText = new StringBuilder(64);
1038 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1041 for (int g = 0; g < groups.length; g++)
1043 if (groups[g].getStartRes() <= column
1044 && groups[g].getEndRes() >= column)
1046 if (!groups[g].getName().startsWith("JTreeGroup")
1047 && !groups[g].getName().startsWith("JGroup"))
1049 tooltipText.append(groups[g].getName());
1052 if (groups[g].getDescription() != null)
1054 tooltipText.append(": " + groups[g].getDescription());
1061 * add any features at the position to the tooltip; if over a gap, only
1062 * add features that straddle the gap (pos may be the residue before or
1065 int unshownFeatures = 0;
1066 if (av.isShowSequenceFeatures())
1068 List<SequenceFeature> features = ap.getFeatureRenderer()
1069 .findFeaturesAtColumn(sequence, column + 1);
1070 unshownFeatures = seqARep.appendFeaturesLengthLimit(tooltipText, pos,
1072 this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
1075 * add features in CDS/protein complement at the corresponding
1076 * position if configured to do so
1078 if (av.isShowComplementFeatures())
1080 if (!Comparison.isGap(sequence.getCharAt(column)))
1082 AlignViewportI complement = ap.getAlignViewport()
1083 .getCodingComplement();
1084 AlignFrame af = Desktop.getAlignFrameFor(complement);
1085 FeatureRendererModel fr2 = af.getFeatureRenderer();
1086 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1090 unshownFeatures = seqARep.appendFeaturesLengthLimit(
1091 tooltipText, pos, mf, fr2,
1092 MAX_TOOLTIP_LENGTH);
1097 if (tooltipText.length() == 0) // nothing added
1099 setToolTipText(null);
1104 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1106 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1107 tooltipText.append("...");
1109 if (unshownFeatures > 0)
1111 tooltipText.append("<br/>").append("... ").append("<i>")
1112 .append(MessageManager.formatMessage(
1113 "label.features_not_shown", unshownFeatures))
1116 String textString = tooltipText.toString();
1117 if (!textString.equals(lastTooltip))
1119 lastTooltip = textString;
1120 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1122 setToolTipText(lastFormattedTooltip);
1128 * When the view is in wrapped mode, and the mouse is over an annotation row,
1129 * shows the corresponding tooltip and status message (if any)
1134 protected void mouseMovedOverAnnotation(MousePos pos)
1136 final int column = pos.column;
1137 final int rowIndex = pos.annotationIndex;
1139 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1144 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1146 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1148 if (true || !tooltip.equals(lastTooltip))
1150 System.out.println("wrapped tooltip set");
1151 lastTooltip = tooltip;
1152 lastFormattedTooltip = tooltip == null ? null
1153 : JvSwingUtils.wrapTooltip(true, tooltip);
1154 setToolTipText(lastFormattedTooltip);
1157 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1159 ap.alignFrame.setStatus(msg);
1163 * if Shift key is held down while moving the mouse,
1164 * the tooltip location is not changed once shown
1166 private Point lastTooltipLocation = null;
1169 * this flag is false for pixel moves within a residue,
1170 * to reduce tooltip flicker
1172 private boolean moveTooltip = true;
1175 * a dummy tooltip used to estimate where to position tooltips
1177 private JToolTip tempTip = new JLabel().createToolTip();
1182 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1185 public Point getToolTipLocation(MouseEvent event)
1189 if (lastTooltip == null || !moveTooltip)
1194 if (lastTooltipLocation != null && event.isShiftDown())
1196 return lastTooltipLocation;
1199 int x = event.getX();
1200 int y = event.getY();
1203 tempTip.setTipText(lastFormattedTooltip);
1204 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1206 // was x += (w - x < 200) ? -(w / 2) : 5;
1207 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1208 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1210 return lastTooltipLocation = p;
1214 * set when the current UI interaction has resulted in a change that requires
1215 * shading in overviews and structures to be recalculated. this could be
1216 * changed to a something more expressive that indicates what actually has
1217 * changed, so selective redraws can be applied (ie. only structures, only
1220 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1223 * set if av.getSelectionGroup() refers to a group that is defined on the
1224 * alignment view, rather than a transient selection
1226 // private boolean editingDefinedGroup = false; // TODO: refactor to
1227 // avcontroller or viewModel
1230 * Sets the status message in alignment panel, showing the sequence number
1231 * (index) and id, and residue and residue position if not at a gap, for the
1232 * given sequence and column position. Returns the residue position returned
1233 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1234 * if at a gapped position.
1237 * aligned sequence object
1241 * index of sequence in alignment
1242 * @return sequence position of residue at column, or adjacent residue if at a
1245 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1247 char sequenceChar = sequence.getCharAt(column);
1248 int pos = sequence.findPosition(column);
1249 setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1255 * Builds the status message for the current cursor location and writes it to
1256 * the status bar, for example
1259 * Sequence 3 ID: FER1_SOLLC
1260 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1261 * Sequence 5 ID: FER1_PEA Residue: B (3)
1262 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1267 * sequence position in the alignment (1..)
1268 * @param sequenceChar
1269 * the character under the cursor
1271 * the sequence residue position (if not over a gap)
1273 protected void setStatusMessage(SequenceI sequence, int seqIndex,
1274 char sequenceChar, int residuePos)
1276 StringBuilder text = new StringBuilder(32);
1279 * Sequence number (if known), and sequence name.
1281 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1282 text.append("Sequence").append(seqno).append(" ID: ")
1283 .append(sequence.getName());
1285 String residue = null;
1288 * Try to translate the display character to residue name (null for gap).
1290 boolean isGapped = Comparison.isGap(sequenceChar);
1294 boolean nucleotide = av.getAlignment().isNucleotide();
1295 String displayChar = String.valueOf(sequenceChar);
1298 residue = ResidueProperties.nucleotideName.get(displayChar);
1302 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1303 : ("*".equals(displayChar) ? "STOP"
1304 : ResidueProperties.aa2Triplet.get(displayChar));
1306 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1307 .append(": ").append(residue == null ? displayChar : residue);
1309 text.append(" (").append(Integer.toString(residuePos)).append(")");
1311 ap.alignFrame.setStatus(text.toString());
1315 * Set the status bar message to highlight the first matched position in
1320 private void setStatusMessage(SearchResultsI results)
1322 AlignmentI al = this.av.getAlignment();
1323 int sequenceIndex = al.findIndex(results);
1324 if (sequenceIndex == -1)
1328 SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1329 for (SearchResultMatchI m : results.getResults())
1331 SequenceI seq = m.getSequence();
1332 if (seq.getDatasetSequence() != null)
1334 seq = seq.getDatasetSequence();
1339 int start = m.getStart();
1340 setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1351 public void mouseDragged(MouseEvent evt)
1353 MousePos pos = findMousePosition(evt);
1354 if (pos.isOverAnnotation() || pos.column == -1)
1359 if (mouseWheelPressed)
1361 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1362 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1364 int oldWidth = av.getCharWidth();
1366 // Which is bigger, left-right or up-down?
1367 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1368 .abs(evt.getX() - lastMousePress.getX()))
1371 * on drag up or down, decrement or increment font size
1373 int fontSize = av.font.getSize();
1374 boolean fontChanged = false;
1376 if (evt.getY() < lastMousePress.getY())
1381 else if (evt.getY() > lastMousePress.getY())
1394 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1396 av.setFont(newFont, true);
1397 av.setCharWidth(oldWidth);
1401 ap.av.getCodingComplement().setFont(newFont, true);
1402 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1403 .getSplitViewContainer();
1404 splitFrame.adjustLayout();
1405 splitFrame.repaint();
1412 * on drag left or right, decrement or increment character width
1415 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1417 newWidth = av.getCharWidth() - 1;
1418 av.setCharWidth(newWidth);
1420 else if (evt.getX() > lastMousePress.getX())
1422 newWidth = av.getCharWidth() + 1;
1423 av.setCharWidth(newWidth);
1427 ap.paintAlignment(false, false);
1431 * need to ensure newWidth is set on cdna, regardless of which
1432 * panel the mouse drag happened in; protein will compute its
1433 * character width as 1:1 or 3:1
1435 av.getCodingComplement().setCharWidth(newWidth);
1436 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1437 .getSplitViewContainer();
1438 splitFrame.adjustLayout();
1439 splitFrame.repaint();
1444 FontMetrics fm = getFontMetrics(av.getFont());
1445 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1447 lastMousePress = evt.getPoint();
1454 dragStretchGroup(evt);
1458 int res = pos.column;
1465 if ((editLastRes == -1) || (editLastRes == res))
1470 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1472 // dragLeft, delete gap
1473 editSequence(false, false, res);
1477 editSequence(true, false, res);
1480 mouseDragging = true;
1481 if (scrollThread != null)
1483 scrollThread.setMousePosition(evt.getPoint());
1488 * Edits the sequence to insert or delete one or more gaps, in response to a
1489 * mouse drag or cursor mode command. The number of inserts/deletes may be
1490 * specified with the cursor command, or else depends on the mouse event
1491 * (normally one column, but potentially more for a fast mouse drag).
1493 * Delete gaps is limited to the number of gaps left of the cursor position
1494 * (mouse drag), or at or right of the cursor position (cursor mode).
1496 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1497 * the current selection group.
1499 * In locked editing mode (with a selection group present), inserts/deletions
1500 * within the selection group are limited to its boundaries (and edits outside
1501 * the group stop at its border).
1504 * true to insert gaps, false to delete gaps
1506 * (unused parameter)
1508 * the column at which to perform the action; the number of columns
1509 * affected depends on <code>this.editLastRes</code> (cursor column
1512 synchronized void editSequence(boolean insertGap, boolean editSeq,
1516 int fixedRight = -1;
1517 boolean fixedColumns = false;
1518 SequenceGroup sg = av.getSelectionGroup();
1520 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1522 // No group, but the sequence may represent a group
1523 if (!groupEditing && av.hasHiddenRows())
1525 if (av.isHiddenRepSequence(seq))
1527 sg = av.getRepresentedSequences(seq);
1528 groupEditing = true;
1532 StringBuilder message = new StringBuilder(64); // for status bar
1535 * make a name for the edit action, for
1536 * status bar message and Undo/Redo menu
1538 String label = null;
1541 message.append("Edit group:");
1542 label = MessageManager.getString("action.edit_group");
1546 message.append("Edit sequence: " + seq.getName());
1547 label = seq.getName();
1548 if (label.length() > 10)
1550 label = label.substring(0, 10);
1552 label = MessageManager.formatMessage("label.edit_params",
1558 * initialise the edit command if there is not
1559 * already one being extended
1561 if (editCommand == null)
1563 editCommand = new EditCommand(label);
1568 message.append(" insert ");
1572 message.append(" delete ");
1575 message.append(Math.abs(startres - editLastRes) + " gaps.");
1576 ap.alignFrame.setStatus(message.toString());
1579 * is there a selection group containing the sequence being edited?
1580 * if so the boundary of the group is the limit of the edit
1581 * (but the edit may be inside or outside the selection group)
1583 boolean inSelectionGroup = sg != null
1584 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1585 if (groupEditing || inSelectionGroup)
1587 fixedColumns = true;
1589 // sg might be null as the user may only see 1 sequence,
1590 // but the sequence represents a group
1593 if (!av.isHiddenRepSequence(seq))
1598 sg = av.getRepresentedSequences(seq);
1601 fixedLeft = sg.getStartRes();
1602 fixedRight = sg.getEndRes();
1604 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1605 || (startres >= fixedLeft && editLastRes < fixedLeft)
1606 || (startres > fixedRight && editLastRes <= fixedRight)
1607 || (startres <= fixedRight && editLastRes > fixedRight))
1613 if (fixedLeft > startres)
1615 fixedRight = fixedLeft - 1;
1618 else if (fixedRight < startres)
1620 fixedLeft = fixedRight;
1625 if (av.hasHiddenColumns())
1627 fixedColumns = true;
1628 int y1 = av.getAlignment().getHiddenColumns()
1629 .getNextHiddenBoundary(true, startres);
1630 int y2 = av.getAlignment().getHiddenColumns()
1631 .getNextHiddenBoundary(false, startres);
1633 if ((insertGap && startres > y1 && editLastRes < y1)
1634 || (!insertGap && startres < y2 && editLastRes > y2))
1640 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1641 // Selection spans a hidden region
1642 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1650 fixedRight = y2 - 1;
1655 boolean success = doEditSequence(insertGap, editSeq, startres,
1656 fixedRight, fixedColumns, sg);
1659 * report what actually happened (might be less than
1660 * what was requested), by inspecting the edit commands added
1662 String msg = getEditStatusMessage(editCommand);
1663 ap.alignFrame.setStatus(msg == null ? " " : msg);
1669 editLastRes = startres;
1670 seqCanvas.repaint();
1674 * A helper method that performs the requested editing to insert or delete
1675 * gaps (if possible). Answers true if the edit was successful, false if could
1676 * only be performed in part or not at all. Failure may occur in 'locked edit'
1677 * mode, when an insertion requires a matching gapped position (or column) to
1678 * delete, and deletion requires an adjacent gapped position (or column) to
1682 * true if inserting gap(s), false if deleting
1684 * (unused parameter, currently always false)
1686 * the column at which to perform the edit
1688 * fixed right boundary column of a locked edit (within or to the
1689 * left of a selection group)
1690 * @param fixedColumns
1691 * true if this is a locked edit
1693 * the sequence group (if group edit is being performed)
1696 protected boolean doEditSequence(final boolean insertGap,
1697 final boolean editSeq, final int startres, int fixedRight,
1698 final boolean fixedColumns, final SequenceGroup sg)
1700 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1701 SequenceI[] seqs = new SequenceI[] { seq };
1705 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1706 int g, groupSize = vseqs.size();
1707 SequenceI[] groupSeqs = new SequenceI[groupSize];
1708 for (g = 0; g < groupSeqs.length; g++)
1710 groupSeqs[g] = vseqs.get(g);
1716 // If the user has selected the whole sequence, and is dragging to
1717 // the right, we can still extend the alignment and selectionGroup
1718 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1719 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1722 av.getAlignment().getWidth() + startres - editLastRes);
1723 fixedRight = sg.getEndRes();
1726 // Is it valid with fixed columns??
1727 // Find the next gap before the end
1728 // of the visible region boundary
1729 boolean blank = false;
1730 for (; fixedRight > editLastRes; fixedRight--)
1734 for (g = 0; g < groupSize; g++)
1736 for (int j = 0; j < startres - editLastRes; j++)
1739 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1754 if (sg.getSize() == av.getAlignment().getHeight())
1756 if ((av.hasHiddenColumns()
1757 && startres < av.getAlignment().getHiddenColumns()
1758 .getNextHiddenBoundary(false, startres)))
1763 int alWidth = av.getAlignment().getWidth();
1764 if (av.hasHiddenRows())
1766 int hwidth = av.getAlignment().getHiddenSequences()
1768 if (hwidth > alWidth)
1773 // We can still insert gaps if the selectionGroup
1774 // contains all the sequences
1775 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1776 fixedRight = alWidth + startres - editLastRes;
1786 else if (!insertGap)
1788 // / Are we able to delete?
1789 // ie are all columns blank?
1791 for (g = 0; g < groupSize; g++)
1793 for (int j = startres; j < editLastRes; j++)
1795 if (groupSeqs[g].getLength() <= j)
1800 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1802 // Not a gap, block edit not valid
1811 // dragging to the right
1812 if (fixedColumns && fixedRight != -1)
1814 for (int j = editLastRes; j < startres; j++)
1816 insertGap(j, groupSeqs, fixedRight);
1821 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1822 startres - editLastRes, false);
1827 // dragging to the left
1828 if (fixedColumns && fixedRight != -1)
1830 for (int j = editLastRes; j > startres; j--)
1832 deleteChar(startres, groupSeqs, fixedRight);
1837 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1838 editLastRes - startres, false);
1845 * editing a single sequence
1849 // dragging to the right
1850 if (fixedColumns && fixedRight != -1)
1852 for (int j = editLastRes; j < startres; j++)
1854 if (!insertGap(j, seqs, fixedRight))
1857 * e.g. cursor mode command specified
1858 * more inserts than are possible
1866 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1867 startres - editLastRes, false);
1874 // dragging to the left
1875 if (fixedColumns && fixedRight != -1)
1877 for (int j = editLastRes; j > startres; j--)
1879 if (!Comparison.isGap(seq.getCharAt(startres)))
1883 deleteChar(startres, seqs, fixedRight);
1888 // could be a keyboard edit trying to delete none gaps
1890 for (int m = startres; m < editLastRes; m++)
1892 if (!Comparison.isGap(seq.getCharAt(m)))
1900 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1905 {// insertGap==false AND editSeq==TRUE;
1906 if (fixedColumns && fixedRight != -1)
1908 for (int j = editLastRes; j < startres; j++)
1910 insertGap(j, seqs, fixedRight);
1915 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1916 startres - editLastRes, false);
1926 * Constructs an informative status bar message while dragging to insert or
1927 * delete gaps. Answers null if inserts and deletes cancel out.
1929 * @param editCommand
1930 * a command containing the list of individual edits
1933 protected static String getEditStatusMessage(EditCommand editCommand)
1935 if (editCommand == null)
1941 * add any inserts, and subtract any deletes,
1942 * not counting those auto-inserted when doing a 'locked edit'
1943 * (so only counting edits 'under the cursor')
1946 for (Edit cmd : editCommand.getEdits())
1948 if (!cmd.isSystemGenerated())
1950 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1958 * inserts and deletes cancel out
1963 String msgKey = count > 1 ? "label.insert_gaps"
1964 : (count == 1 ? "label.insert_gap"
1965 : (count == -1 ? "label.delete_gap"
1966 : "label.delete_gaps"));
1967 count = Math.abs(count);
1969 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1973 * Inserts one gap at column j, deleting the right-most gapped column up to
1974 * (and including) fixedColumn. Returns true if the edit is successful, false
1975 * if no blank column is available to allow the insertion to be balanced by a
1980 * @param fixedColumn
1983 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1985 int blankColumn = fixedColumn;
1986 for (int s = 0; s < seq.length; s++)
1988 // Find the next gap before the end of the visible region boundary
1989 // If lastCol > j, theres a boundary after the gap insertion
1991 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1993 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1995 // Theres a space, so break and insert the gap
2000 if (blankColumn <= j)
2002 blankColumn = fixedColumn;
2008 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2010 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2016 * Helper method to add and perform one edit action
2022 * @param systemGenerated
2023 * true if the edit is a 'balancing' delete (or insert) to match a
2024 * user's insert (or delete) in a locked editing region
2026 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2027 int count, boolean systemGenerated)
2030 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2031 av.getAlignment().getGapCharacter());
2032 edit.setSystemGenerated(systemGenerated);
2034 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2038 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2039 * each of the given sequences. The caller should ensure that all sequences
2040 * are gapped in column j.
2044 * @param fixedColumn
2046 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2048 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2050 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2054 * On reentering the panel, stops any scrolling that was started on dragging
2060 public void mouseEntered(MouseEvent e)
2070 * On leaving the panel, if the mouse is being dragged, starts a thread to
2071 * scroll it until the mouse is released (in unwrapped mode only)
2076 public void mouseExited(MouseEvent e)
2078 lastMousePosition = null;
2079 ap.alignFrame.setStatus(" ");
2080 if (av.getWrapAlignment())
2085 if (mouseDragging && scrollThread == null)
2087 startScrolling(e.getPoint());
2092 * Handler for double-click on a position with one or more sequence features.
2093 * Opens the Amend Features dialog to allow feature details to be amended, or
2094 * the feature deleted.
2097 public void mouseClicked(MouseEvent evt)
2099 SequenceGroup sg = null;
2100 MousePos pos = findMousePosition(evt);
2101 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2106 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2108 sg = av.getSelectionGroup();
2109 if (sg != null && sg.getSize() == 1
2110 && sg.getEndRes() - sg.getStartRes() < 2)
2112 av.setSelectionGroup(null);
2115 int column = pos.column;
2118 * find features at the position (if not gapped), or straddling
2119 * the position (if at a gap)
2121 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2122 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2123 .findFeaturesAtColumn(sequence, column + 1);
2125 if (!features.isEmpty())
2128 * highlight the first feature at the position on the alignment
2130 SearchResultsI highlight = new SearchResults();
2131 highlight.addResult(sequence, features.get(0).getBegin(), features
2133 seqCanvas.highlightSearchResults(highlight, true);
2136 * open the Amend Features dialog
2138 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2139 false).showDialog();
2145 public void mouseWheelMoved(MouseWheelEvent e)
2148 double wheelRotation = e.getPreciseWheelRotation();
2149 if (wheelRotation > 0)
2151 if (e.isShiftDown())
2153 av.getRanges().scrollRight(true);
2158 av.getRanges().scrollUp(false);
2161 else if (wheelRotation < 0)
2163 if (e.isShiftDown())
2165 av.getRanges().scrollRight(false);
2169 av.getRanges().scrollUp(true);
2174 * update status bar and tooltip for new position
2175 * (need to synthesize a mouse movement to refresh tooltip)
2178 ToolTipManager.sharedInstance().mouseMoved(e);
2187 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2189 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2194 final int res = pos.column;
2195 final int seq = pos.seqIndex;
2197 updateOverviewAndStructs = false;
2199 startWrapBlock = wrappedBlock;
2201 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2203 if ((sequence == null) || (res > sequence.getLength()))
2208 stretchGroup = av.getSelectionGroup();
2210 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2212 stretchGroup = av.getAlignment().findGroup(sequence, res);
2213 if (stretchGroup != null)
2215 // only update the current selection if the popup menu has a group to
2217 av.setSelectionGroup(stretchGroup);
2222 * defer right-mouse click handling to mouseReleased on Windows
2223 * (where isPopupTrigger() will answer true)
2224 * NB isRightMouseButton is also true for Cmd-click on Mac
2226 if (Platform.isWinRightButton(evt))
2231 if (evt.isPopupTrigger()) // Mac: mousePressed
2233 showPopupMenu(evt, pos);
2239 seqCanvas.cursorX = res;
2240 seqCanvas.cursorY = seq;
2241 seqCanvas.repaint();
2245 if (stretchGroup == null)
2247 createStretchGroup(res, sequence);
2250 if (stretchGroup != null)
2252 stretchGroup.addPropertyChangeListener(seqCanvas);
2255 seqCanvas.repaint();
2258 private void createStretchGroup(int res, SequenceI sequence)
2260 // Only if left mouse button do we want to change group sizes
2261 // define a new group here
2262 SequenceGroup sg = new SequenceGroup();
2263 sg.setStartRes(res);
2265 sg.addSequence(sequence, false);
2266 av.setSelectionGroup(sg);
2269 if (av.getConservationSelected())
2271 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2275 if (av.getAbovePIDThreshold())
2277 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2280 // TODO: stretchGroup will always be not null. Is this a merge error ?
2281 // or is there a threading issue here?
2282 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2284 // Edit end res position of selected group
2285 changeEndRes = true;
2287 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2289 // Edit end res position of selected group
2290 changeStartRes = true;
2292 stretchGroup.getWidth();
2297 * Build and show a pop-up menu at the right-click mouse position
2302 void showPopupMenu(MouseEvent evt, MousePos pos)
2304 final int column = pos.column;
2305 final int seq = pos.seqIndex;
2306 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2307 if (sequence != null)
2309 PopupMenu pop = new PopupMenu(ap, sequence, column);
2310 pop.show(this, evt.getX(), evt.getY());
2315 * Update the display after mouse up on a selection or group
2318 * mouse released event details
2320 * true if this event is happening after a mouse drag (rather than a
2323 protected void doMouseReleasedDefineMode(MouseEvent evt,
2326 if (stretchGroup == null)
2331 stretchGroup.removePropertyChangeListener(seqCanvas);
2333 // always do this - annotation has own state
2334 // but defer colourscheme update until hidden sequences are passed in
2335 boolean vischange = stretchGroup.recalcConservation(true);
2336 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2338 if (stretchGroup.cs != null)
2342 stretchGroup.cs.alignmentChanged(stretchGroup,
2343 av.getHiddenRepSequences());
2346 ResidueShaderI groupColourScheme = stretchGroup
2347 .getGroupColourScheme();
2348 String name = stretchGroup.getName();
2349 if (stretchGroup.cs.conservationApplied())
2351 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2353 if (stretchGroup.cs.getThreshold() > 0)
2355 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2358 PaintRefresher.Refresh(this, av.getSequenceSetId());
2359 // TODO: structure colours only need updating if stretchGroup used to or now
2360 // does contain sequences with structure views
2361 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2362 updateOverviewAndStructs = false;
2363 changeEndRes = false;
2364 changeStartRes = false;
2365 stretchGroup = null;
2370 * Resizes the borders of a selection group depending on the direction of
2375 protected void dragStretchGroup(MouseEvent evt)
2377 if (stretchGroup == null)
2382 MousePos pos = findMousePosition(evt);
2383 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2388 int res = pos.column;
2389 int y = pos.seqIndex;
2391 if (wrappedBlock != startWrapBlock)
2396 res = Math.min(res, av.getAlignment().getWidth()-1);
2398 if (stretchGroup.getEndRes() == res)
2400 // Edit end res position of selected group
2401 changeEndRes = true;
2403 else if (stretchGroup.getStartRes() == res)
2405 // Edit start res position of selected group
2406 changeStartRes = true;
2409 if (res < av.getRanges().getStartRes())
2411 res = av.getRanges().getStartRes();
2416 if (res > (stretchGroup.getStartRes() - 1))
2418 stretchGroup.setEndRes(res);
2419 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2422 else if (changeStartRes)
2424 if (res < (stretchGroup.getEndRes() + 1))
2426 stretchGroup.setStartRes(res);
2427 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2431 int dragDirection = 0;
2437 else if (y < oldSeq)
2442 while ((y != oldSeq) && (oldSeq > -1)
2443 && (y < av.getAlignment().getHeight()))
2445 // This routine ensures we don't skip any sequences, as the
2446 // selection is quite slow.
2447 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2449 oldSeq += dragDirection;
2456 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2458 if (stretchGroup.getSequences(null).contains(nextSeq))
2460 stretchGroup.deleteSequence(seq, false);
2461 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2467 stretchGroup.addSequence(seq, false);
2470 stretchGroup.addSequence(nextSeq, false);
2471 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2480 mouseDragging = true;
2482 if (scrollThread != null)
2484 scrollThread.setMousePosition(evt.getPoint());
2488 * construct a status message showing the range of the selection
2490 StringBuilder status = new StringBuilder(64);
2491 List<SequenceI> seqs = stretchGroup.getSequences();
2492 String name = seqs.get(0).getName();
2493 if (name.length() > 20)
2495 name = name.substring(0, 20);
2497 status.append(name).append(" - ");
2498 name = seqs.get(seqs.size() - 1).getName();
2499 if (name.length() > 20)
2501 name = name.substring(0, 20);
2503 status.append(name).append(" ");
2504 int startRes = stretchGroup.getStartRes();
2505 status.append(" cols ").append(String.valueOf(startRes + 1))
2507 int endRes = stretchGroup.getEndRes();
2508 status.append(String.valueOf(endRes + 1));
2509 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2510 .append(String.valueOf(endRes - startRes + 1)).append(")");
2511 ap.alignFrame.setStatus(status.toString());
2515 * Stops the scroll thread if it is running
2517 void stopScrolling()
2519 if (scrollThread != null)
2521 scrollThread.stopScrolling();
2522 scrollThread = null;
2524 mouseDragging = false;
2528 * Starts a thread to scroll the alignment, towards a given mouse position
2529 * outside the panel bounds, unless the alignment is in wrapped mode
2533 void startScrolling(Point mousePos)
2536 * set this.mouseDragging in case this was called from
2537 * a drag in ScalePanel or AnnotationPanel
2539 mouseDragging = true;
2540 if (!av.getWrapAlignment() && scrollThread == null)
2542 scrollThread = new ScrollThread();
2543 scrollThread.setMousePosition(mousePos);
2544 if (Platform.isJS())
2547 * Javascript - run every 20ms until scrolling stopped
2548 * or reaches the limit of scrollable alignment
2550 Timer t = new Timer(20, new ActionListener()
2553 public void actionPerformed(ActionEvent e)
2555 if (scrollThread != null)
2557 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2558 scrollThread.scrollOnce();
2562 t.addActionListener(new ActionListener()
2565 public void actionPerformed(ActionEvent e)
2567 if (scrollThread == null)
2569 // SeqPanel.stopScrolling called
2579 * Java - run in a new thread
2581 scrollThread.start();
2587 * Performs scrolling of the visible alignment left, right, up or down, until
2588 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2589 * limit of the alignment is reached
2591 class ScrollThread extends Thread
2593 private Point mousePos;
2595 private volatile boolean keepRunning = true;
2600 public ScrollThread()
2602 setName("SeqPanel$ScrollThread");
2606 * Sets the position of the mouse that determines the direction of the
2607 * scroll to perform. If this is called as the mouse moves, scrolling should
2608 * respond accordingly. For example, if the mouse is dragged right, scroll
2609 * right should start; if the drag continues down, scroll down should also
2614 public void setMousePosition(Point p)
2620 * Sets a flag that will cause the thread to exit
2622 public void stopScrolling()
2624 keepRunning = false;
2628 * Scrolls the alignment left or right, and/or up or down, depending on the
2629 * last notified mouse position, until the limit of the alignment is
2630 * reached, or a flag is set to stop the scroll
2637 if (mousePos != null)
2639 keepRunning = scrollOnce();
2644 } catch (Exception ex)
2648 SeqPanel.this.scrollThread = null;
2654 * <li>one row up, if the mouse is above the panel</li>
2655 * <li>one row down, if the mouse is below the panel</li>
2656 * <li>one column left, if the mouse is left of the panel</li>
2657 * <li>one column right, if the mouse is right of the panel</li>
2659 * Answers true if a scroll was performed, false if not - meaning either
2660 * that the mouse position is within the panel, or the edge of the alignment
2663 boolean scrollOnce()
2666 * quit after mouseUp ensures interrupt in JalviewJS
2673 boolean scrolled = false;
2674 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2681 // mouse is above this panel - try scroll up
2682 scrolled = ranges.scrollUp(true);
2684 else if (mousePos.y >= getHeight())
2686 // mouse is below this panel - try scroll down
2687 scrolled = ranges.scrollUp(false);
2691 * scroll left or right
2695 scrolled |= ranges.scrollRight(false);
2697 else if (mousePos.x >= getWidth())
2699 scrolled |= ranges.scrollRight(true);
2706 * modify current selection according to a received message.
2709 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2710 HiddenColumns hidden, SelectionSource source)
2712 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2713 // handles selection messages...
2714 // TODO: extend config options to allow user to control if selections may be
2715 // shared between viewports.
2716 boolean iSentTheSelection = (av == source
2717 || (source instanceof AlignViewport
2718 && ((AlignmentViewport) source).getSequenceSetId()
2719 .equals(av.getSequenceSetId())));
2721 if (iSentTheSelection)
2723 // respond to our own event by updating dependent dialogs
2724 if (ap.getCalculationDialog() != null)
2726 ap.getCalculationDialog().validateCalcTypes();
2732 // process further ?
2733 if (!av.followSelection)
2739 * Ignore the selection if there is one of our own pending.
2741 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2747 * Check for selection in a view of which this one is a dna/protein
2750 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2755 // do we want to thread this ? (contention with seqsel and colsel locks, I
2758 * only copy colsel if there is a real intersection between
2759 * sequence selection and this panel's alignment
2761 boolean repaint = false;
2762 boolean copycolsel = false;
2764 SequenceGroup sgroup = null;
2765 if (seqsel != null && seqsel.getSize() > 0)
2767 if (av.getAlignment() == null)
2769 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2770 + " ViewId=" + av.getViewId()
2771 + " 's alignment is NULL! returning immediately.");
2774 sgroup = seqsel.intersect(av.getAlignment(),
2775 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2776 if ((sgroup != null && sgroup.getSize() > 0))
2781 if (sgroup != null && sgroup.getSize() > 0)
2783 av.setSelectionGroup(sgroup);
2787 av.setSelectionGroup(null);
2789 av.isSelectionGroupChanged(true);
2794 // the current selection is unset or from a previous message
2795 // so import the new colsel.
2796 if (colsel == null || colsel.isEmpty())
2798 if (av.getColumnSelection() != null)
2800 av.getColumnSelection().clear();
2806 // TODO: shift colSel according to the intersecting sequences
2807 if (av.getColumnSelection() == null)
2809 av.setColumnSelection(new ColumnSelection(colsel));
2813 av.getColumnSelection().setElementsFrom(colsel,
2814 av.getAlignment().getHiddenColumns());
2817 av.isColSelChanged(true);
2821 if (copycolsel && av.hasHiddenColumns()
2822 && (av.getAlignment().getHiddenColumns() == null))
2824 System.err.println("Bad things");
2826 if (repaint) // always true!
2828 // probably finessing with multiple redraws here
2829 PaintRefresher.Refresh(this, av.getSequenceSetId());
2830 // ap.paintAlignment(false);
2833 // lastly, update dependent dialogs
2834 if (ap.getCalculationDialog() != null)
2836 ap.getCalculationDialog().validateCalcTypes();
2842 * If this panel is a cdna/protein translation view of the selection source,
2843 * tries to map the source selection to a local one, and returns true. Else
2850 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2851 ColumnSelection colsel, HiddenColumns hidden,
2852 SelectionSource source)
2854 if (!(source instanceof AlignViewportI))
2858 final AlignViewportI sourceAv = (AlignViewportI) source;
2859 if (sourceAv.getCodingComplement() != av
2860 && av.getCodingComplement() != sourceAv)
2866 * Map sequence selection
2868 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2869 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2870 av.isSelectionGroupChanged(true);
2873 * Map column selection
2875 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2877 ColumnSelection cs = new ColumnSelection();
2878 HiddenColumns hs = new HiddenColumns();
2879 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2880 av.setColumnSelection(cs);
2881 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2883 // lastly, update any dependent dialogs
2884 if (ap.getCalculationDialog() != null)
2886 ap.getCalculationDialog().validateCalcTypes();
2890 * repaint alignment, and also Overview or Structure
2891 * if hidden column selection has changed
2893 ap.paintAlignment(hiddenChanged, hiddenChanged);
2900 * @return null or last search results handled by this panel
2902 public SearchResultsI getLastSearchResults()
2904 return lastSearchResults;