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 seqCanvas.cursorX += dx;
481 seqCanvas.cursorY += dy;
483 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
485 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
487 int original = seqCanvas.cursorX - dx;
488 int maxWidth = av.getAlignment().getWidth();
490 if (!hidden.isVisible(seqCanvas.cursorX))
492 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
493 int[] region = hidden.getRegionWithEdgeAtRes(visx);
495 if (region != null) // just in case
500 seqCanvas.cursorX = region[1] + 1;
505 seqCanvas.cursorX = region[0] - 1;
508 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
511 if (seqCanvas.cursorX >= maxWidth
512 || !hidden.isVisible(seqCanvas.cursorX))
514 seqCanvas.cursorX = original;
518 scrollToVisible(false);
522 * Scroll to make the cursor visible in the viewport.
525 * just jump to the location rather than scrolling
527 void scrollToVisible(boolean jump)
529 if (seqCanvas.cursorX < 0)
531 seqCanvas.cursorX = 0;
533 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
535 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
538 if (seqCanvas.cursorY < 0)
540 seqCanvas.cursorY = 0;
542 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
544 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
549 boolean repaintNeeded = true;
552 // only need to repaint if the viewport did not move, as otherwise it will
554 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
559 if (av.getWrapAlignment())
561 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
562 int x = av.getAlignment().getHiddenColumns()
563 .absoluteToVisibleColumn(seqCanvas.cursorX);
564 av.getRanges().scrollToWrappedVisible(x);
568 av.getRanges().scrollToVisible(seqCanvas.cursorX,
573 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
575 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
576 seqCanvas.cursorX, seqCanvas.cursorY);
586 void setSelectionAreaAtCursor(boolean topLeft)
588 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
590 if (av.getSelectionGroup() != null)
592 SequenceGroup sg = av.getSelectionGroup();
593 // Find the top and bottom of this group
594 int min = av.getAlignment().getHeight(), max = 0;
595 for (int i = 0; i < sg.getSize(); i++)
597 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
612 sg.setStartRes(seqCanvas.cursorX);
613 if (sg.getEndRes() < seqCanvas.cursorX)
615 sg.setEndRes(seqCanvas.cursorX);
618 min = seqCanvas.cursorY;
622 sg.setEndRes(seqCanvas.cursorX);
623 if (sg.getStartRes() > seqCanvas.cursorX)
625 sg.setStartRes(seqCanvas.cursorX);
628 max = seqCanvas.cursorY + 1;
633 // Only the user can do this
634 av.setSelectionGroup(null);
638 // Now add any sequences between min and max
639 sg.getSequences(null).clear();
640 for (int i = min; i < max; i++)
642 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
647 if (av.getSelectionGroup() == null)
649 SequenceGroup sg = new SequenceGroup();
650 sg.setStartRes(seqCanvas.cursorX);
651 sg.setEndRes(seqCanvas.cursorX);
652 sg.addSequence(sequence, false);
653 av.setSelectionGroup(sg);
656 ap.paintAlignment(false, false);
660 void insertGapAtCursor(boolean group)
662 groupEditing = group;
663 editStartSeq = seqCanvas.cursorY;
664 editLastRes = seqCanvas.cursorX;
665 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
669 void deleteGapAtCursor(boolean group)
671 groupEditing = group;
672 editStartSeq = seqCanvas.cursorY;
673 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
674 editSequence(false, false, seqCanvas.cursorX);
678 void insertNucAtCursor(boolean group, String nuc)
680 // TODO not called - delete?
681 groupEditing = group;
682 editStartSeq = seqCanvas.cursorY;
683 editLastRes = seqCanvas.cursorX;
684 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
688 void numberPressed(char value)
690 if (keyboardNo1 == null)
692 keyboardNo1 = new StringBuffer();
695 if (keyboardNo2 != null)
697 keyboardNo2.append(value);
701 keyboardNo1.append(value);
709 if (keyboardNo1 != null)
711 int value = Integer.parseInt(keyboardNo1.toString());
715 } catch (Exception x)
726 if (keyboardNo2 != null)
728 int value = Integer.parseInt(keyboardNo2.toString());
732 } catch (Exception x)
746 public void mouseReleased(MouseEvent evt)
748 MousePos pos = findMousePosition(evt);
749 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
754 boolean didDrag = mouseDragging; // did we come here after a drag
755 mouseDragging = false;
756 mouseWheelPressed = false;
758 if (evt.isPopupTrigger()) // Windows: mouseReleased
760 showPopupMenu(evt, pos);
771 doMouseReleasedDefineMode(evt, didDrag);
782 public void mousePressed(MouseEvent evt)
784 lastMousePress = evt.getPoint();
785 MousePos pos = findMousePosition(evt);
786 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
791 if (SwingUtilities.isMiddleMouseButton(evt))
793 mouseWheelPressed = true;
797 boolean isControlDown = Platform.isControlDown(evt);
798 if (evt.isShiftDown() || isControlDown)
808 doMousePressedDefineMode(evt, pos);
812 int seq = pos.seqIndex;
813 int res = pos.column;
815 if ((seq < av.getAlignment().getHeight())
816 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
833 public void mouseOverSequence(SequenceI sequence, int index, int pos)
835 String tmp = sequence.hashCode() + " " + index + " " + pos;
837 if (lastMessage == null || !lastMessage.equals(tmp))
839 // System.err.println("mouseOver Sequence: "+tmp);
840 ssm.mouseOverSequence(sequence, index, pos, av);
846 * Highlight the mapped region described by the search results object (unless
847 * unchanged). This supports highlight of protein while mousing over linked
848 * cDNA and vice versa. The status bar is also updated to show the location of
849 * the start of the highlighted region.
852 public String highlightSequence(SearchResultsI results)
854 if (results == null || results.equals(lastSearchResults))
858 lastSearchResults = results;
860 boolean wasScrolled = false;
862 if (av.isFollowHighlight())
864 // don't allow highlight of protein/cDNA to also scroll a complementary
865 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
866 // over residue to change abruptly, causing highlighted residue in panel 2
867 // to change, causing a scroll in panel 1 etc)
868 ap.setToScrollComplementPanel(false);
869 wasScrolled = ap.scrollToPosition(results);
872 seqCanvas.revalidate();
874 ap.setToScrollComplementPanel(true);
877 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
878 if (seqCanvas.highlightSearchResults(results, fastPaint))
880 setStatusMessage(results);
882 return results.isEmpty() ? null : getHighlightInfo(results);
886 * temporary hack: answers a message suitable to show on structure hover
887 * label. This is normally null. It is a peptide variation description if
889 * <li>results are a single residue in a protein alignment</li>
890 * <li>there is a mapping to a coding sequence (codon)</li>
891 * <li>there are one or more SNP variant features on the codon</li>
893 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
898 private String getHighlightInfo(SearchResultsI results)
901 * ideally, just find mapped CDS (as we don't care about render style here);
902 * for now, go via split frame complement's FeatureRenderer
904 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
905 if (complement == null)
909 AlignFrame af = Desktop.getAlignFrameFor(complement);
910 FeatureRendererModel fr2 = af.getFeatureRenderer();
912 int j = results.getSize();
913 List<String> infos = new ArrayList<>();
914 for (int i = 0; i < j; i++)
916 SearchResultMatchI match = results.getResults().get(i);
917 int pos = match.getStart();
918 if (pos == match.getEnd())
920 SequenceI seq = match.getSequence();
921 SequenceI ds = seq.getDatasetSequence() == null ? seq
922 : seq.getDatasetSequence();
923 MappedFeatures mf = fr2
924 .findComplementFeaturesAtResidue(ds, pos);
927 for (SequenceFeature sf : mf.features)
929 String pv = mf.findProteinVariants(sf);
930 if (pv.length() > 0 && !infos.contains(pv))
943 StringBuilder sb = new StringBuilder();
944 for (String info : infos)
952 return sb.toString();
956 public VamsasSource getVamsasSource()
958 return this.ap == null ? null : this.ap.av;
962 public void updateColours(SequenceI seq, int index)
964 System.out.println("update the seqPanel colours");
969 * Action on mouse movement is to update the status bar to show the current
970 * sequence position, and (if features are shown) to show any features at the
971 * position in a tooltip. Does nothing if the mouse move does not change
977 public void mouseMoved(MouseEvent evt)
981 // This is because MacOSX creates a mouseMoved
982 // If control is down, other platforms will not.
986 final MousePos mousePos = findMousePosition(evt);
987 if (mousePos.equals(lastMousePosition))
990 * just a pixel move without change of 'cell'
996 lastMousePosition = mousePos;
998 if (mousePos.isOverAnnotation())
1000 mouseMovedOverAnnotation(mousePos);
1003 final int seq = mousePos.seqIndex;
1005 final int column = mousePos.column;
1006 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1008 lastMousePosition = null;
1009 setToolTipText(null);
1011 lastFormattedTooltip = null;
1012 ap.alignFrame.setStatus("");
1016 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1018 if (column >= sequence.getLength())
1024 * set status bar message, returning residue position in sequence
1026 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1027 final int pos = setStatusMessage(sequence, column, seq);
1028 if (ssm != null && !isGapped)
1030 mouseOverSequence(sequence, column, pos);
1033 StringBuilder tooltipText = new StringBuilder(64);
1035 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1038 for (int g = 0; g < groups.length; g++)
1040 if (groups[g].getStartRes() <= column
1041 && groups[g].getEndRes() >= column)
1043 if (!groups[g].getName().startsWith("JTreeGroup")
1044 && !groups[g].getName().startsWith("JGroup"))
1046 tooltipText.append(groups[g].getName());
1049 if (groups[g].getDescription() != null)
1051 tooltipText.append(": " + groups[g].getDescription());
1058 * add any features at the position to the tooltip; if over a gap, only
1059 * add features that straddle the gap (pos may be the residue before or
1062 int unshownFeatures = 0;
1063 if (av.isShowSequenceFeatures())
1065 List<SequenceFeature> features = ap.getFeatureRenderer()
1066 .findFeaturesAtColumn(sequence, column + 1);
1067 unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1068 features, this.ap.getSeqPanel().seqCanvas.fr,
1069 MAX_TOOLTIP_LENGTH);
1072 * add features in CDS/protein complement at the corresponding
1073 * position if configured to do so
1075 if (av.isShowComplementFeatures())
1077 if (!Comparison.isGap(sequence.getCharAt(column)))
1079 AlignViewportI complement = ap.getAlignViewport()
1080 .getCodingComplement();
1081 AlignFrame af = Desktop.getAlignFrameFor(complement);
1082 FeatureRendererModel fr2 = af.getFeatureRenderer();
1083 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1087 unshownFeatures += seqARep.appendFeatures(tooltipText,
1088 pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1093 if (tooltipText.length() == 0) // nothing added
1095 setToolTipText(null);
1100 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1102 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1103 tooltipText.append("...");
1105 if (unshownFeatures > 0)
1107 tooltipText.append("<br/>").append("... ").append("<i>")
1108 .append(MessageManager.formatMessage(
1109 "label.features_not_shown", unshownFeatures))
1112 String textString = tooltipText.toString();
1113 if (!textString.equals(lastTooltip))
1115 lastTooltip = textString;
1116 lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1118 setToolTipText(lastFormattedTooltip);
1124 * When the view is in wrapped mode, and the mouse is over an annotation row,
1125 * shows the corresponding tooltip and status message (if any)
1130 protected void mouseMovedOverAnnotation(MousePos pos)
1132 final int column = pos.column;
1133 final int rowIndex = pos.annotationIndex;
1135 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1140 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1142 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1144 if (!tooltip.equals(lastTooltip))
1146 lastTooltip = tooltip;
1147 lastFormattedTooltip = tooltip == null ? null
1148 : JvSwingUtils.wrapTooltip(true, tooltip);
1149 setToolTipText(lastFormattedTooltip);
1152 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1154 ap.alignFrame.setStatus(msg);
1158 * if Shift key is held down while moving the mouse,
1159 * the tooltip location is not changed once shown
1161 private Point lastTooltipLocation = null;
1164 * this flag is false for pixel moves within a residue,
1165 * to reduce tooltip flicker
1167 private boolean moveTooltip = true;
1170 * a dummy tooltip used to estimate where to position tooltips
1172 private JToolTip tempTip = new JLabel().createToolTip();
1177 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1180 public Point getToolTipLocation(MouseEvent event)
1184 if (lastTooltip == null || !moveTooltip)
1189 if (lastTooltipLocation != null && event.isShiftDown())
1191 return lastTooltipLocation;
1194 int x = event.getX();
1195 int y = event.getY();
1198 tempTip.setTipText(lastFormattedTooltip);
1199 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1201 // was x += (w - x < 200) ? -(w / 2) : 5;
1202 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1203 Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1205 return lastTooltipLocation = p;
1209 * set when the current UI interaction has resulted in a change that requires
1210 * shading in overviews and structures to be recalculated. this could be
1211 * changed to a something more expressive that indicates what actually has
1212 * changed, so selective redraws can be applied (ie. only structures, only
1215 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1218 * set if av.getSelectionGroup() refers to a group that is defined on the
1219 * alignment view, rather than a transient selection
1221 // private boolean editingDefinedGroup = false; // TODO: refactor to
1222 // avcontroller or viewModel
1225 * Sets the status message in alignment panel, showing the sequence number
1226 * (index) and id, and residue and residue position if not at a gap, for the
1227 * given sequence and column position. Returns the residue position returned
1228 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1229 * if at a gapped position.
1232 * aligned sequence object
1236 * index of sequence in alignment
1237 * @return sequence position of residue at column, or adjacent residue if at a
1240 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1242 char sequenceChar = sequence.getCharAt(column);
1243 int pos = sequence.findPosition(column);
1244 setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1250 * Builds the status message for the current cursor location and writes it to
1251 * the status bar, for example
1254 * Sequence 3 ID: FER1_SOLLC
1255 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1256 * Sequence 5 ID: FER1_PEA Residue: B (3)
1257 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1262 * sequence position in the alignment (1..)
1263 * @param sequenceChar
1264 * the character under the cursor
1266 * the sequence residue position (if not over a gap)
1268 protected void setStatusMessage(String seqName, int seqIndex,
1269 char sequenceChar, int residuePos)
1271 StringBuilder text = new StringBuilder(32);
1274 * Sequence number (if known), and sequence name.
1276 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1277 text.append("Sequence").append(seqno).append(" ID: ")
1280 String residue = null;
1283 * Try to translate the display character to residue name (null for gap).
1285 boolean isGapped = Comparison.isGap(sequenceChar);
1289 boolean nucleotide = av.getAlignment().isNucleotide();
1290 String displayChar = String.valueOf(sequenceChar);
1293 residue = ResidueProperties.nucleotideName.get(displayChar);
1297 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1298 : ("*".equals(displayChar) ? "STOP"
1299 : ResidueProperties.aa2Triplet.get(displayChar));
1301 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1302 .append(": ").append(residue == null ? displayChar : residue);
1304 text.append(" (").append(Integer.toString(residuePos)).append(")");
1306 ap.alignFrame.setStatus(text.toString());
1310 * Set the status bar message to highlight the first matched position in
1315 private void setStatusMessage(SearchResultsI results)
1317 AlignmentI al = this.av.getAlignment();
1318 int sequenceIndex = al.findIndex(results);
1319 if (sequenceIndex == -1)
1323 SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1324 SequenceI ds = alignedSeq.getDatasetSequence();
1325 for (SearchResultMatchI m : results.getResults())
1327 SequenceI seq = m.getSequence();
1328 if (seq.getDatasetSequence() != null)
1330 seq = seq.getDatasetSequence();
1335 int start = m.getStart();
1336 setStatusMessage(alignedSeq.getName(), sequenceIndex,
1337 seq.getCharAt(start - 1), start);
1347 public void mouseDragged(MouseEvent evt)
1349 MousePos pos = findMousePosition(evt);
1350 if (pos.isOverAnnotation() || pos.column == -1)
1355 if (mouseWheelPressed)
1357 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1358 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1360 int oldWidth = av.getCharWidth();
1362 // Which is bigger, left-right or up-down?
1363 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1364 .abs(evt.getX() - lastMousePress.getX()))
1367 * on drag up or down, decrement or increment font size
1369 int fontSize = av.font.getSize();
1370 boolean fontChanged = false;
1372 if (evt.getY() < lastMousePress.getY())
1377 else if (evt.getY() > lastMousePress.getY())
1390 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1392 av.setFont(newFont, true);
1393 av.setCharWidth(oldWidth);
1397 ap.av.getCodingComplement().setFont(newFont, true);
1398 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1399 .getSplitViewContainer();
1400 splitFrame.adjustLayout();
1401 splitFrame.repaint();
1408 * on drag left or right, decrement or increment character width
1411 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1413 newWidth = av.getCharWidth() - 1;
1414 av.setCharWidth(newWidth);
1416 else if (evt.getX() > lastMousePress.getX())
1418 newWidth = av.getCharWidth() + 1;
1419 av.setCharWidth(newWidth);
1423 ap.paintAlignment(false, false);
1427 * need to ensure newWidth is set on cdna, regardless of which
1428 * panel the mouse drag happened in; protein will compute its
1429 * character width as 1:1 or 3:1
1431 av.getCodingComplement().setCharWidth(newWidth);
1432 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1433 .getSplitViewContainer();
1434 splitFrame.adjustLayout();
1435 splitFrame.repaint();
1440 FontMetrics fm = getFontMetrics(av.getFont());
1441 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1443 lastMousePress = evt.getPoint();
1450 dragStretchGroup(evt);
1454 int res = pos.column;
1461 if ((editLastRes == -1) || (editLastRes == res))
1466 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1468 // dragLeft, delete gap
1469 editSequence(false, false, res);
1473 editSequence(true, false, res);
1476 mouseDragging = true;
1477 if (scrollThread != null)
1479 scrollThread.setMousePosition(evt.getPoint());
1484 * Edits the sequence to insert or delete one or more gaps, in response to a
1485 * mouse drag or cursor mode command. The number of inserts/deletes may be
1486 * specified with the cursor command, or else depends on the mouse event
1487 * (normally one column, but potentially more for a fast mouse drag).
1489 * Delete gaps is limited to the number of gaps left of the cursor position
1490 * (mouse drag), or at or right of the cursor position (cursor mode).
1492 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1493 * the current selection group.
1495 * In locked editing mode (with a selection group present), inserts/deletions
1496 * within the selection group are limited to its boundaries (and edits outside
1497 * the group stop at its border).
1500 * true to insert gaps, false to delete gaps
1502 * (unused parameter)
1504 * the column at which to perform the action; the number of columns
1505 * affected depends on <code>this.editLastRes</code> (cursor column
1508 synchronized void editSequence(boolean insertGap, boolean editSeq,
1512 int fixedRight = -1;
1513 boolean fixedColumns = false;
1514 SequenceGroup sg = av.getSelectionGroup();
1516 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1518 // No group, but the sequence may represent a group
1519 if (!groupEditing && av.hasHiddenRows())
1521 if (av.isHiddenRepSequence(seq))
1523 sg = av.getRepresentedSequences(seq);
1524 groupEditing = true;
1528 StringBuilder message = new StringBuilder(64); // for status bar
1531 * make a name for the edit action, for
1532 * status bar message and Undo/Redo menu
1534 String label = null;
1537 message.append("Edit group:");
1538 label = MessageManager.getString("action.edit_group");
1542 message.append("Edit sequence: " + seq.getName());
1543 label = seq.getName();
1544 if (label.length() > 10)
1546 label = label.substring(0, 10);
1548 label = MessageManager.formatMessage("label.edit_params",
1554 * initialise the edit command if there is not
1555 * already one being extended
1557 if (editCommand == null)
1559 editCommand = new EditCommand(label);
1564 message.append(" insert ");
1568 message.append(" delete ");
1571 message.append(Math.abs(startres - editLastRes) + " gaps.");
1572 ap.alignFrame.setStatus(message.toString());
1575 * is there a selection group containing the sequence being edited?
1576 * if so the boundary of the group is the limit of the edit
1577 * (but the edit may be inside or outside the selection group)
1579 boolean inSelectionGroup = sg != null
1580 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1581 if (groupEditing || inSelectionGroup)
1583 fixedColumns = true;
1585 // sg might be null as the user may only see 1 sequence,
1586 // but the sequence represents a group
1589 if (!av.isHiddenRepSequence(seq))
1594 sg = av.getRepresentedSequences(seq);
1597 fixedLeft = sg.getStartRes();
1598 fixedRight = sg.getEndRes();
1600 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1601 || (startres >= fixedLeft && editLastRes < fixedLeft)
1602 || (startres > fixedRight && editLastRes <= fixedRight)
1603 || (startres <= fixedRight && editLastRes > fixedRight))
1609 if (fixedLeft > startres)
1611 fixedRight = fixedLeft - 1;
1614 else if (fixedRight < startres)
1616 fixedLeft = fixedRight;
1621 if (av.hasHiddenColumns())
1623 fixedColumns = true;
1624 int y1 = av.getAlignment().getHiddenColumns()
1625 .getNextHiddenBoundary(true, startres);
1626 int y2 = av.getAlignment().getHiddenColumns()
1627 .getNextHiddenBoundary(false, startres);
1629 if ((insertGap && startres > y1 && editLastRes < y1)
1630 || (!insertGap && startres < y2 && editLastRes > y2))
1636 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1637 // Selection spans a hidden region
1638 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1646 fixedRight = y2 - 1;
1651 boolean success = doEditSequence(insertGap, editSeq, startres,
1652 fixedRight, fixedColumns, sg);
1655 * report what actually happened (might be less than
1656 * what was requested), by inspecting the edit commands added
1658 String msg = getEditStatusMessage(editCommand);
1659 ap.alignFrame.setStatus(msg == null ? " " : msg);
1665 editLastRes = startres;
1666 seqCanvas.repaint();
1670 * A helper method that performs the requested editing to insert or delete
1671 * gaps (if possible). Answers true if the edit was successful, false if could
1672 * only be performed in part or not at all. Failure may occur in 'locked edit'
1673 * mode, when an insertion requires a matching gapped position (or column) to
1674 * delete, and deletion requires an adjacent gapped position (or column) to
1678 * true if inserting gap(s), false if deleting
1680 * (unused parameter, currently always false)
1682 * the column at which to perform the edit
1684 * fixed right boundary column of a locked edit (within or to the
1685 * left of a selection group)
1686 * @param fixedColumns
1687 * true if this is a locked edit
1689 * the sequence group (if group edit is being performed)
1692 protected boolean doEditSequence(final boolean insertGap,
1693 final boolean editSeq, final int startres, int fixedRight,
1694 final boolean fixedColumns, final SequenceGroup sg)
1696 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1697 SequenceI[] seqs = new SequenceI[] { seq };
1701 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1702 int g, groupSize = vseqs.size();
1703 SequenceI[] groupSeqs = new SequenceI[groupSize];
1704 for (g = 0; g < groupSeqs.length; g++)
1706 groupSeqs[g] = vseqs.get(g);
1712 // If the user has selected the whole sequence, and is dragging to
1713 // the right, we can still extend the alignment and selectionGroup
1714 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1715 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1718 av.getAlignment().getWidth() + startres - editLastRes);
1719 fixedRight = sg.getEndRes();
1722 // Is it valid with fixed columns??
1723 // Find the next gap before the end
1724 // of the visible region boundary
1725 boolean blank = false;
1726 for (; fixedRight > editLastRes; fixedRight--)
1730 for (g = 0; g < groupSize; g++)
1732 for (int j = 0; j < startres - editLastRes; j++)
1735 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1750 if (sg.getSize() == av.getAlignment().getHeight())
1752 if ((av.hasHiddenColumns()
1753 && startres < av.getAlignment().getHiddenColumns()
1754 .getNextHiddenBoundary(false, startres)))
1759 int alWidth = av.getAlignment().getWidth();
1760 if (av.hasHiddenRows())
1762 int hwidth = av.getAlignment().getHiddenSequences()
1764 if (hwidth > alWidth)
1769 // We can still insert gaps if the selectionGroup
1770 // contains all the sequences
1771 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1772 fixedRight = alWidth + startres - editLastRes;
1782 else if (!insertGap)
1784 // / Are we able to delete?
1785 // ie are all columns blank?
1787 for (g = 0; g < groupSize; g++)
1789 for (int j = startres; j < editLastRes; j++)
1791 if (groupSeqs[g].getLength() <= j)
1796 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1798 // Not a gap, block edit not valid
1807 // dragging to the right
1808 if (fixedColumns && fixedRight != -1)
1810 for (int j = editLastRes; j < startres; j++)
1812 insertGap(j, groupSeqs, fixedRight);
1817 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1818 startres - editLastRes, false);
1823 // dragging to the left
1824 if (fixedColumns && fixedRight != -1)
1826 for (int j = editLastRes; j > startres; j--)
1828 deleteChar(startres, groupSeqs, fixedRight);
1833 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1834 editLastRes - startres, false);
1841 * editing a single sequence
1845 // dragging to the right
1846 if (fixedColumns && fixedRight != -1)
1848 for (int j = editLastRes; j < startres; j++)
1850 if (!insertGap(j, seqs, fixedRight))
1853 * e.g. cursor mode command specified
1854 * more inserts than are possible
1862 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1863 startres - editLastRes, false);
1870 // dragging to the left
1871 if (fixedColumns && fixedRight != -1)
1873 for (int j = editLastRes; j > startres; j--)
1875 if (!Comparison.isGap(seq.getCharAt(startres)))
1879 deleteChar(startres, seqs, fixedRight);
1884 // could be a keyboard edit trying to delete none gaps
1886 for (int m = startres; m < editLastRes; m++)
1888 if (!Comparison.isGap(seq.getCharAt(m)))
1896 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1901 {// insertGap==false AND editSeq==TRUE;
1902 if (fixedColumns && fixedRight != -1)
1904 for (int j = editLastRes; j < startres; j++)
1906 insertGap(j, seqs, fixedRight);
1911 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1912 startres - editLastRes, false);
1922 * Constructs an informative status bar message while dragging to insert or
1923 * delete gaps. Answers null if inserts and deletes cancel out.
1925 * @param editCommand
1926 * a command containing the list of individual edits
1929 protected static String getEditStatusMessage(EditCommand editCommand)
1931 if (editCommand == null)
1937 * add any inserts, and subtract any deletes,
1938 * not counting those auto-inserted when doing a 'locked edit'
1939 * (so only counting edits 'under the cursor')
1942 for (Edit cmd : editCommand.getEdits())
1944 if (!cmd.isSystemGenerated())
1946 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1954 * inserts and deletes cancel out
1959 String msgKey = count > 1 ? "label.insert_gaps"
1960 : (count == 1 ? "label.insert_gap"
1961 : (count == -1 ? "label.delete_gap"
1962 : "label.delete_gaps"));
1963 count = Math.abs(count);
1965 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1969 * Inserts one gap at column j, deleting the right-most gapped column up to
1970 * (and including) fixedColumn. Returns true if the edit is successful, false
1971 * if no blank column is available to allow the insertion to be balanced by a
1976 * @param fixedColumn
1979 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1981 int blankColumn = fixedColumn;
1982 for (int s = 0; s < seq.length; s++)
1984 // Find the next gap before the end of the visible region boundary
1985 // If lastCol > j, theres a boundary after the gap insertion
1987 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1989 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1991 // Theres a space, so break and insert the gap
1996 if (blankColumn <= j)
1998 blankColumn = fixedColumn;
2004 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2006 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2012 * Helper method to add and perform one edit action
2018 * @param systemGenerated
2019 * true if the edit is a 'balancing' delete (or insert) to match a
2020 * user's insert (or delete) in a locked editing region
2022 protected void appendEdit(Action action, SequenceI[] seq, int pos,
2023 int count, boolean systemGenerated)
2026 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2027 av.getAlignment().getGapCharacter());
2028 edit.setSystemGenerated(systemGenerated);
2030 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2034 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2035 * each of the given sequences. The caller should ensure that all sequences
2036 * are gapped in column j.
2040 * @param fixedColumn
2042 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2044 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2046 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2050 * On reentering the panel, stops any scrolling that was started on dragging
2056 public void mouseEntered(MouseEvent e)
2066 * On leaving the panel, if the mouse is being dragged, starts a thread to
2067 * scroll it until the mouse is released (in unwrapped mode only)
2072 public void mouseExited(MouseEvent e)
2074 lastMousePosition = null;
2075 ap.alignFrame.setStatus(" ");
2076 if (av.getWrapAlignment())
2081 if (mouseDragging && scrollThread == null)
2083 startScrolling(e.getPoint());
2088 * Handler for double-click on a position with one or more sequence features.
2089 * Opens the Amend Features dialog to allow feature details to be amended, or
2090 * the feature deleted.
2093 public void mouseClicked(MouseEvent evt)
2095 SequenceGroup sg = null;
2096 MousePos pos = findMousePosition(evt);
2097 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2102 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2104 sg = av.getSelectionGroup();
2105 if (sg != null && sg.getSize() == 1
2106 && sg.getEndRes() - sg.getStartRes() < 2)
2108 av.setSelectionGroup(null);
2111 int column = pos.column;
2114 * find features at the position (if not gapped), or straddling
2115 * the position (if at a gap)
2117 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2118 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2119 .findFeaturesAtColumn(sequence, column + 1);
2121 if (!features.isEmpty())
2124 * highlight the first feature at the position on the alignment
2126 SearchResultsI highlight = new SearchResults();
2127 highlight.addResult(sequence, features.get(0).getBegin(), features
2129 seqCanvas.highlightSearchResults(highlight, true);
2132 * open the Amend Features dialog
2134 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2135 false).showDialog();
2141 public void mouseWheelMoved(MouseWheelEvent e)
2144 double wheelRotation = e.getPreciseWheelRotation();
2145 if (wheelRotation > 0)
2147 if (e.isShiftDown())
2149 av.getRanges().scrollRight(true);
2154 av.getRanges().scrollUp(false);
2157 else if (wheelRotation < 0)
2159 if (e.isShiftDown())
2161 av.getRanges().scrollRight(false);
2165 av.getRanges().scrollUp(true);
2170 * update status bar and tooltip for new position
2171 * (need to synthesize a mouse movement to refresh tooltip)
2174 ToolTipManager.sharedInstance().mouseMoved(e);
2183 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2185 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2190 final int res = pos.column;
2191 final int seq = pos.seqIndex;
2193 updateOverviewAndStructs = false;
2195 startWrapBlock = wrappedBlock;
2197 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2199 if ((sequence == null) || (res > sequence.getLength()))
2204 stretchGroup = av.getSelectionGroup();
2206 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2208 stretchGroup = av.getAlignment().findGroup(sequence, res);
2209 if (stretchGroup != null)
2211 // only update the current selection if the popup menu has a group to
2213 av.setSelectionGroup(stretchGroup);
2218 * defer right-mouse click handling to mouseReleased on Windows
2219 * (where isPopupTrigger() will answer true)
2220 * NB isRightMouseButton is also true for Cmd-click on Mac
2222 if (Platform.isWinRightButton(evt))
2227 if (evt.isPopupTrigger()) // Mac: mousePressed
2229 showPopupMenu(evt, pos);
2235 seqCanvas.cursorX = res;
2236 seqCanvas.cursorY = seq;
2237 seqCanvas.repaint();
2241 if (stretchGroup == null)
2243 createStretchGroup(res, sequence);
2246 if (stretchGroup != null)
2248 stretchGroup.addPropertyChangeListener(seqCanvas);
2251 seqCanvas.repaint();
2254 private void createStretchGroup(int res, SequenceI sequence)
2256 // Only if left mouse button do we want to change group sizes
2257 // define a new group here
2258 SequenceGroup sg = new SequenceGroup();
2259 sg.setStartRes(res);
2261 sg.addSequence(sequence, false);
2262 av.setSelectionGroup(sg);
2265 if (av.getConservationSelected())
2267 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2271 if (av.getAbovePIDThreshold())
2273 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2276 // TODO: stretchGroup will always be not null. Is this a merge error ?
2277 // or is there a threading issue here?
2278 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2280 // Edit end res position of selected group
2281 changeEndRes = true;
2283 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2285 // Edit end res position of selected group
2286 changeStartRes = true;
2288 stretchGroup.getWidth();
2293 * Build and show a pop-up menu at the right-click mouse position
2298 void showPopupMenu(MouseEvent evt, MousePos pos)
2300 final int column = pos.column;
2301 final int seq = pos.seqIndex;
2302 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2303 if (sequence != null)
2305 PopupMenu pop = new PopupMenu(ap, sequence, column);
2306 pop.show(this, evt.getX(), evt.getY());
2311 * Update the display after mouse up on a selection or group
2314 * mouse released event details
2316 * true if this event is happening after a mouse drag (rather than a
2319 protected void doMouseReleasedDefineMode(MouseEvent evt,
2322 if (stretchGroup == null)
2327 stretchGroup.removePropertyChangeListener(seqCanvas);
2329 // always do this - annotation has own state
2330 // but defer colourscheme update until hidden sequences are passed in
2331 boolean vischange = stretchGroup.recalcConservation(true);
2332 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2334 if (stretchGroup.cs != null)
2338 stretchGroup.cs.alignmentChanged(stretchGroup,
2339 av.getHiddenRepSequences());
2342 ResidueShaderI groupColourScheme = stretchGroup
2343 .getGroupColourScheme();
2344 String name = stretchGroup.getName();
2345 if (stretchGroup.cs.conservationApplied())
2347 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2349 if (stretchGroup.cs.getThreshold() > 0)
2351 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2354 PaintRefresher.Refresh(this, av.getSequenceSetId());
2355 // TODO: structure colours only need updating if stretchGroup used to or now
2356 // does contain sequences with structure views
2357 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2358 updateOverviewAndStructs = false;
2359 changeEndRes = false;
2360 changeStartRes = false;
2361 stretchGroup = null;
2366 * Resizes the borders of a selection group depending on the direction of
2371 protected void dragStretchGroup(MouseEvent evt)
2373 if (stretchGroup == null)
2378 MousePos pos = findMousePosition(evt);
2379 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2384 int res = pos.column;
2385 int y = pos.seqIndex;
2387 if (wrappedBlock != startWrapBlock)
2392 res = Math.min(res, av.getAlignment().getWidth()-1);
2394 if (stretchGroup.getEndRes() == res)
2396 // Edit end res position of selected group
2397 changeEndRes = true;
2399 else if (stretchGroup.getStartRes() == res)
2401 // Edit start res position of selected group
2402 changeStartRes = true;
2405 if (res < av.getRanges().getStartRes())
2407 res = av.getRanges().getStartRes();
2412 if (res > (stretchGroup.getStartRes() - 1))
2414 stretchGroup.setEndRes(res);
2415 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2418 else if (changeStartRes)
2420 if (res < (stretchGroup.getEndRes() + 1))
2422 stretchGroup.setStartRes(res);
2423 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2427 int dragDirection = 0;
2433 else if (y < oldSeq)
2438 while ((y != oldSeq) && (oldSeq > -1)
2439 && (y < av.getAlignment().getHeight()))
2441 // This routine ensures we don't skip any sequences, as the
2442 // selection is quite slow.
2443 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2445 oldSeq += dragDirection;
2452 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2454 if (stretchGroup.getSequences(null).contains(nextSeq))
2456 stretchGroup.deleteSequence(seq, false);
2457 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2463 stretchGroup.addSequence(seq, false);
2466 stretchGroup.addSequence(nextSeq, false);
2467 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2476 mouseDragging = true;
2478 if (scrollThread != null)
2480 scrollThread.setMousePosition(evt.getPoint());
2484 * construct a status message showing the range of the selection
2486 StringBuilder status = new StringBuilder(64);
2487 List<SequenceI> seqs = stretchGroup.getSequences();
2488 String name = seqs.get(0).getName();
2489 if (name.length() > 20)
2491 name = name.substring(0, 20);
2493 status.append(name).append(" - ");
2494 name = seqs.get(seqs.size() - 1).getName();
2495 if (name.length() > 20)
2497 name = name.substring(0, 20);
2499 status.append(name).append(" ");
2500 int startRes = stretchGroup.getStartRes();
2501 status.append(" cols ").append(String.valueOf(startRes + 1))
2503 int endRes = stretchGroup.getEndRes();
2504 status.append(String.valueOf(endRes + 1));
2505 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2506 .append(String.valueOf(endRes - startRes + 1)).append(")");
2507 ap.alignFrame.setStatus(status.toString());
2511 * Stops the scroll thread if it is running
2513 void stopScrolling()
2515 if (scrollThread != null)
2517 scrollThread.stopScrolling();
2518 scrollThread = null;
2520 mouseDragging = false;
2524 * Starts a thread to scroll the alignment, towards a given mouse position
2525 * outside the panel bounds, unless the alignment is in wrapped mode
2529 void startScrolling(Point mousePos)
2532 * set this.mouseDragging in case this was called from
2533 * a drag in ScalePanel or AnnotationPanel
2535 mouseDragging = true;
2536 if (!av.getWrapAlignment() && scrollThread == null)
2538 scrollThread = new ScrollThread();
2539 scrollThread.setMousePosition(mousePos);
2540 if (Platform.isJS())
2543 * Javascript - run every 20ms until scrolling stopped
2544 * or reaches the limit of scrollable alignment
2546 Timer t = new Timer(20, new ActionListener()
2549 public void actionPerformed(ActionEvent e)
2551 if (scrollThread != null)
2553 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2554 scrollThread.scrollOnce();
2558 t.addActionListener(new ActionListener()
2561 public void actionPerformed(ActionEvent e)
2563 if (scrollThread == null)
2565 // SeqPanel.stopScrolling called
2575 * Java - run in a new thread
2577 scrollThread.start();
2583 * Performs scrolling of the visible alignment left, right, up or down, until
2584 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2585 * limit of the alignment is reached
2587 class ScrollThread extends Thread
2589 private Point mousePos;
2591 private volatile boolean keepRunning = true;
2596 public ScrollThread()
2598 setName("SeqPanel$ScrollThread");
2602 * Sets the position of the mouse that determines the direction of the
2603 * scroll to perform. If this is called as the mouse moves, scrolling should
2604 * respond accordingly. For example, if the mouse is dragged right, scroll
2605 * right should start; if the drag continues down, scroll down should also
2610 public void setMousePosition(Point p)
2616 * Sets a flag that will cause the thread to exit
2618 public void stopScrolling()
2620 keepRunning = false;
2624 * Scrolls the alignment left or right, and/or up or down, depending on the
2625 * last notified mouse position, until the limit of the alignment is
2626 * reached, or a flag is set to stop the scroll
2633 if (mousePos != null)
2635 keepRunning = scrollOnce();
2640 } catch (Exception ex)
2644 SeqPanel.this.scrollThread = null;
2650 * <li>one row up, if the mouse is above the panel</li>
2651 * <li>one row down, if the mouse is below the panel</li>
2652 * <li>one column left, if the mouse is left of the panel</li>
2653 * <li>one column right, if the mouse is right of the panel</li>
2655 * Answers true if a scroll was performed, false if not - meaning either
2656 * that the mouse position is within the panel, or the edge of the alignment
2659 boolean scrollOnce()
2662 * quit after mouseUp ensures interrupt in JalviewJS
2669 boolean scrolled = false;
2670 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2677 // mouse is above this panel - try scroll up
2678 scrolled = ranges.scrollUp(true);
2680 else if (mousePos.y >= getHeight())
2682 // mouse is below this panel - try scroll down
2683 scrolled = ranges.scrollUp(false);
2687 * scroll left or right
2691 scrolled |= ranges.scrollRight(false);
2693 else if (mousePos.x >= getWidth())
2695 scrolled |= ranges.scrollRight(true);
2702 * modify current selection according to a received message.
2705 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2706 HiddenColumns hidden, SelectionSource source)
2708 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2709 // handles selection messages...
2710 // TODO: extend config options to allow user to control if selections may be
2711 // shared between viewports.
2712 boolean iSentTheSelection = (av == source
2713 || (source instanceof AlignViewport
2714 && ((AlignmentViewport) source).getSequenceSetId()
2715 .equals(av.getSequenceSetId())));
2717 if (iSentTheSelection)
2719 // respond to our own event by updating dependent dialogs
2720 if (ap.getCalculationDialog() != null)
2722 ap.getCalculationDialog().validateCalcTypes();
2728 // process further ?
2729 if (!av.followSelection)
2735 * Ignore the selection if there is one of our own pending.
2737 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2743 * Check for selection in a view of which this one is a dna/protein
2746 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2751 // do we want to thread this ? (contention with seqsel and colsel locks, I
2754 * only copy colsel if there is a real intersection between
2755 * sequence selection and this panel's alignment
2757 boolean repaint = false;
2758 boolean copycolsel = false;
2760 SequenceGroup sgroup = null;
2761 if (seqsel != null && seqsel.getSize() > 0)
2763 if (av.getAlignment() == null)
2765 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2766 + " ViewId=" + av.getViewId()
2767 + " 's alignment is NULL! returning immediately.");
2770 sgroup = seqsel.intersect(av.getAlignment(),
2771 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2772 if ((sgroup != null && sgroup.getSize() > 0))
2777 if (sgroup != null && sgroup.getSize() > 0)
2779 av.setSelectionGroup(sgroup);
2783 av.setSelectionGroup(null);
2785 av.isSelectionGroupChanged(true);
2790 // the current selection is unset or from a previous message
2791 // so import the new colsel.
2792 if (colsel == null || colsel.isEmpty())
2794 if (av.getColumnSelection() != null)
2796 av.getColumnSelection().clear();
2802 // TODO: shift colSel according to the intersecting sequences
2803 if (av.getColumnSelection() == null)
2805 av.setColumnSelection(new ColumnSelection(colsel));
2809 av.getColumnSelection().setElementsFrom(colsel,
2810 av.getAlignment().getHiddenColumns());
2813 av.isColSelChanged(true);
2817 if (copycolsel && av.hasHiddenColumns()
2818 && (av.getAlignment().getHiddenColumns() == null))
2820 System.err.println("Bad things");
2822 if (repaint) // always true!
2824 // probably finessing with multiple redraws here
2825 PaintRefresher.Refresh(this, av.getSequenceSetId());
2826 // ap.paintAlignment(false);
2829 // lastly, update dependent dialogs
2830 if (ap.getCalculationDialog() != null)
2832 ap.getCalculationDialog().validateCalcTypes();
2838 * If this panel is a cdna/protein translation view of the selection source,
2839 * tries to map the source selection to a local one, and returns true. Else
2846 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2847 ColumnSelection colsel, HiddenColumns hidden,
2848 SelectionSource source)
2850 if (!(source instanceof AlignViewportI))
2854 final AlignViewportI sourceAv = (AlignViewportI) source;
2855 if (sourceAv.getCodingComplement() != av
2856 && av.getCodingComplement() != sourceAv)
2862 * Map sequence selection
2864 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2865 av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2866 av.isSelectionGroupChanged(true);
2869 * Map column selection
2871 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2873 ColumnSelection cs = new ColumnSelection();
2874 HiddenColumns hs = new HiddenColumns();
2875 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2876 av.setColumnSelection(cs);
2877 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2879 // lastly, update any dependent dialogs
2880 if (ap.getCalculationDialog() != null)
2882 ap.getCalculationDialog().validateCalcTypes();
2886 * repaint alignment, and also Overview or Structure
2887 * if hidden column selection has changed
2889 ap.paintAlignment(hiddenChanged, hiddenChanged);
2896 * @return null or last search results handled by this panel
2898 public SearchResultsI getLastSearchResults()
2900 return lastSearchResults;