2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
23 import jalview.api.AlignViewportI;
24 import jalview.bin.Cache;
25 import jalview.commands.EditCommand;
26 import jalview.commands.EditCommand.Action;
27 import jalview.commands.EditCommand.Edit;
28 import jalview.datamodel.AlignmentAnnotation;
29 import jalview.datamodel.AlignmentI;
30 import jalview.datamodel.ColumnSelection;
31 import jalview.datamodel.HiddenColumns;
32 import jalview.datamodel.MappedFeatures;
33 import jalview.datamodel.SearchResultMatchI;
34 import jalview.datamodel.SearchResults;
35 import jalview.datamodel.SearchResultsI;
36 import jalview.datamodel.Sequence;
37 import jalview.datamodel.SequenceFeature;
38 import jalview.datamodel.SequenceGroup;
39 import jalview.datamodel.SequenceI;
40 import jalview.io.SequenceAnnotationReport;
41 import jalview.renderer.ResidueShaderI;
42 import jalview.schemes.ResidueProperties;
43 import jalview.structure.SelectionListener;
44 import jalview.structure.SelectionSource;
45 import jalview.structure.SequenceListener;
46 import jalview.structure.StructureSelectionManager;
47 import jalview.structure.VamsasSource;
48 import jalview.util.Comparison;
49 import jalview.util.MappingUtils;
50 import jalview.util.MessageManager;
51 import jalview.util.Platform;
52 import jalview.viewmodel.AlignmentViewport;
53 import jalview.viewmodel.ViewportRanges;
54 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
56 import java.awt.BorderLayout;
57 import java.awt.Color;
59 import java.awt.FontMetrics;
60 import java.awt.Point;
61 import java.awt.event.ActionEvent;
62 import java.awt.event.ActionListener;
63 import java.awt.event.MouseEvent;
64 import java.awt.event.MouseListener;
65 import java.awt.event.MouseMotionListener;
66 import java.awt.event.MouseWheelEvent;
67 import java.awt.event.MouseWheelListener;
68 import java.util.ArrayList;
69 import java.util.Collections;
70 import java.util.List;
72 import javax.swing.JLabel;
73 import javax.swing.JPanel;
74 import javax.swing.JToolTip;
75 import javax.swing.SwingUtilities;
76 import javax.swing.Timer;
77 import javax.swing.ToolTipManager;
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;
219 StringBuilder tooltipText = new StringBuilder();
223 EditCommand editCommand;
225 StructureSelectionManager ssm;
227 SearchResultsI lastSearchResults;
230 * Creates a new SeqPanel object
235 public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
237 linkImageURL = getClass().getResource("/images/link.gif");
238 seqARep = new SequenceAnnotationReport(linkImageURL.toString());
239 ToolTipManager.sharedInstance().registerComponent(this);
240 ToolTipManager.sharedInstance().setInitialDelay(0);
241 ToolTipManager.sharedInstance().setDismissDelay(10000);
245 setBackground(Color.white);
247 seqCanvas = new SeqCanvas(alignPanel);
248 setLayout(new BorderLayout());
249 add(seqCanvas, BorderLayout.CENTER);
251 this.ap = alignPanel;
253 if (!viewport.isDataset())
255 addMouseMotionListener(this);
256 addMouseListener(this);
257 addMouseWheelListener(this);
258 ssm = viewport.getStructureSelectionManager();
259 ssm.addStructureViewerListener(this);
260 ssm.addSelectionListener(this);
264 int startWrapBlock = -1;
266 int wrappedBlock = -1;
269 * Computes the column and sequence row (and possibly annotation row when in
270 * wrapped mode) for the given mouse position
275 MousePos findMousePosition(MouseEvent evt)
277 int col = findColumn(evt);
282 int charHeight = av.getCharHeight();
283 int alignmentHeight = av.getAlignment().getHeight();
284 if (av.getWrapAlignment())
286 seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
287 seqCanvas.getHeight());
290 * yPos modulo height of repeating width
292 int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
295 * height of sequences plus space / scale above,
296 * plus gap between sequences and annotations
298 int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
299 + alignmentHeight * charHeight
300 + SeqCanvas.SEQS_ANNOTATION_GAP;
301 if (yOffsetPx >= alignmentHeightPixels)
304 * mouse is over annotations; find annotation index, also set
305 * last sequence above (for backwards compatible behaviour)
307 AlignmentAnnotation[] anns = av.getAlignment()
308 .getAlignmentAnnotation();
309 int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
310 annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
311 seqIndex = alignmentHeight - 1;
316 * mouse is over sequence (or the space above sequences)
318 yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
321 seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
327 ViewportRanges ranges = av.getRanges();
328 seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
329 alignmentHeight - 1);
330 seqIndex = Math.min(seqIndex, ranges.getEndSeq());
333 return new MousePos(col, seqIndex, annIndex);
336 * Returns the aligned sequence position (base 0) at the mouse position, or
337 * the closest visible one
342 int findColumn(MouseEvent evt)
347 final int startRes = av.getRanges().getStartRes();
348 final int charWidth = av.getCharWidth();
350 if (av.getWrapAlignment())
352 int hgap = av.getCharHeight();
353 if (av.getScaleAboveWrapped())
355 hgap += av.getCharHeight();
358 int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
359 + hgap + seqCanvas.getAnnotationHeight();
362 y = Math.max(0, y - hgap);
363 x -= seqCanvas.getLabelWidthWest();
366 // mouse is over left scale
370 int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
375 if (x >= cwidth * charWidth)
377 // mouse is over right scale
381 wrappedBlock = y / cHeight;
382 wrappedBlock += startRes / cwidth;
383 // allow for wrapped view scrolled right (possible from Overview)
384 int startOffset = startRes % cwidth;
385 res = wrappedBlock * cwidth + startOffset
386 + Math.min(cwidth - 1, x / charWidth);
391 * make sure we calculate relative to visible alignment,
392 * rather than right-hand gutter
394 x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
395 res = (x / charWidth) + startRes;
396 res = Math.min(res, av.getRanges().getEndRes());
399 if (av.hasHiddenColumns())
401 res = av.getAlignment().getHiddenColumns()
402 .visibleToAbsoluteColumn(res);
409 * When all of a sequence of edits are complete, put the resulting edit list
410 * on the history stack (undo list), and reset flags for editing in progress.
416 if (editCommand != null && editCommand.getSize() > 0)
418 ap.alignFrame.addHistoryItem(editCommand);
419 av.firePropertyChange("alignment", null,
420 av.getAlignment().getSequences());
425 * Tidy up come what may...
430 groupEditing = false;
439 seqCanvas.cursorY = getKeyboardNo1() - 1;
440 scrollToVisible(true);
443 void setCursorColumn()
445 seqCanvas.cursorX = getKeyboardNo1() - 1;
446 scrollToVisible(true);
449 void setCursorRowAndColumn()
451 if (keyboardNo2 == null)
453 keyboardNo2 = new StringBuffer();
457 seqCanvas.cursorX = getKeyboardNo1() - 1;
458 seqCanvas.cursorY = getKeyboardNo2() - 1;
459 scrollToVisible(true);
463 void setCursorPosition()
465 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
467 seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
468 scrollToVisible(true);
471 void moveCursor(int dx, int dy)
473 seqCanvas.cursorX += dx;
474 seqCanvas.cursorY += dy;
476 HiddenColumns hidden = av.getAlignment().getHiddenColumns();
478 if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
480 int original = seqCanvas.cursorX - dx;
481 int maxWidth = av.getAlignment().getWidth();
483 if (!hidden.isVisible(seqCanvas.cursorX))
485 int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
486 int[] region = hidden.getRegionWithEdgeAtRes(visx);
488 if (region != null) // just in case
493 seqCanvas.cursorX = region[1] + 1;
498 seqCanvas.cursorX = region[0] - 1;
501 seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
504 if (seqCanvas.cursorX >= maxWidth
505 || !hidden.isVisible(seqCanvas.cursorX))
507 seqCanvas.cursorX = original;
511 scrollToVisible(false);
515 * Scroll to make the cursor visible in the viewport.
518 * just jump to the location rather than scrolling
520 void scrollToVisible(boolean jump)
522 if (seqCanvas.cursorX < 0)
524 seqCanvas.cursorX = 0;
526 else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
528 seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
531 if (seqCanvas.cursorY < 0)
533 seqCanvas.cursorY = 0;
535 else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
537 seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
542 boolean repaintNeeded = true;
545 // only need to repaint if the viewport did not move, as otherwise it will
547 repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
552 if (av.getWrapAlignment())
554 // scrollToWrappedVisible expects x-value to have hidden cols subtracted
555 int x = av.getAlignment().getHiddenColumns()
556 .absoluteToVisibleColumn(seqCanvas.cursorX);
557 av.getRanges().scrollToWrappedVisible(x);
561 av.getRanges().scrollToVisible(seqCanvas.cursorX,
566 if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
568 setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
569 seqCanvas.cursorX, seqCanvas.cursorY);
579 void setSelectionAreaAtCursor(boolean topLeft)
581 SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
583 if (av.getSelectionGroup() != null)
585 SequenceGroup sg = av.getSelectionGroup();
586 // Find the top and bottom of this group
587 int min = av.getAlignment().getHeight(), max = 0;
588 for (int i = 0; i < sg.getSize(); i++)
590 int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
605 sg.setStartRes(seqCanvas.cursorX);
606 if (sg.getEndRes() < seqCanvas.cursorX)
608 sg.setEndRes(seqCanvas.cursorX);
611 min = seqCanvas.cursorY;
615 sg.setEndRes(seqCanvas.cursorX);
616 if (sg.getStartRes() > seqCanvas.cursorX)
618 sg.setStartRes(seqCanvas.cursorX);
621 max = seqCanvas.cursorY + 1;
626 // Only the user can do this
627 av.setSelectionGroup(null);
631 // Now add any sequences between min and max
632 sg.getSequences(null).clear();
633 for (int i = min; i < max; i++)
635 sg.addSequence(av.getAlignment().getSequenceAt(i), false);
640 if (av.getSelectionGroup() == null)
642 SequenceGroup sg = new SequenceGroup();
643 sg.setStartRes(seqCanvas.cursorX);
644 sg.setEndRes(seqCanvas.cursorX);
645 sg.addSequence(sequence, false);
646 av.setSelectionGroup(sg);
649 ap.paintAlignment(false, false);
653 void insertGapAtCursor(boolean group)
655 groupEditing = group;
656 editStartSeq = seqCanvas.cursorY;
657 editLastRes = seqCanvas.cursorX;
658 editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
662 void deleteGapAtCursor(boolean group)
664 groupEditing = group;
665 editStartSeq = seqCanvas.cursorY;
666 editLastRes = seqCanvas.cursorX + getKeyboardNo1();
667 editSequence(false, false, seqCanvas.cursorX);
671 void insertNucAtCursor(boolean group, String nuc)
673 // TODO not called - delete?
674 groupEditing = group;
675 editStartSeq = seqCanvas.cursorY;
676 editLastRes = seqCanvas.cursorX;
677 editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
681 void numberPressed(char value)
683 if (keyboardNo1 == null)
685 keyboardNo1 = new StringBuffer();
688 if (keyboardNo2 != null)
690 keyboardNo2.append(value);
694 keyboardNo1.append(value);
702 if (keyboardNo1 != null)
704 int value = Integer.parseInt(keyboardNo1.toString());
708 } catch (Exception x)
719 if (keyboardNo2 != null)
721 int value = Integer.parseInt(keyboardNo2.toString());
725 } catch (Exception x)
739 public void mouseReleased(MouseEvent evt)
741 MousePos pos = findMousePosition(evt);
742 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
747 boolean didDrag = mouseDragging; // did we come here after a drag
748 mouseDragging = false;
749 mouseWheelPressed = false;
751 if (evt.isPopupTrigger()) // Windows: mouseReleased
753 showPopupMenu(evt, pos);
764 doMouseReleasedDefineMode(evt, didDrag);
775 public void mousePressed(MouseEvent evt)
777 lastMousePress = evt.getPoint();
778 MousePos pos = findMousePosition(evt);
779 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
784 if (SwingUtilities.isMiddleMouseButton(evt))
786 mouseWheelPressed = true;
790 boolean isControlDown = Platform.isControlDown(evt);
791 if (evt.isShiftDown() || isControlDown)
801 doMousePressedDefineMode(evt, pos);
805 int seq = pos.seqIndex;
806 int res = pos.column;
808 if ((seq < av.getAlignment().getHeight())
809 && (res < av.getAlignment().getSequenceAt(seq).getLength()))
825 private String formattedTooltipText;
828 public void mouseOverSequence(SequenceI sequence, int index, int pos)
830 String tmp = sequence.hashCode() + " " + index + " " + pos;
832 if (lastMessage == null || !lastMessage.equals(tmp))
834 // System.err.println("mouseOver Sequence: "+tmp);
835 ssm.mouseOverSequence(sequence, index, pos, av);
841 * Highlight the mapped region described by the search results object (unless
842 * unchanged). This supports highlight of protein while mousing over linked
843 * cDNA and vice versa. The status bar is also updated to show the location of
844 * the start of the highlighted region.
847 public String highlightSequence(SearchResultsI results)
849 if (results == null || results.equals(lastSearchResults))
853 lastSearchResults = results;
855 boolean wasScrolled = false;
857 if (av.isFollowHighlight())
859 // don't allow highlight of protein/cDNA to also scroll a complementary
860 // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
861 // over residue to change abruptly, causing highlighted residue in panel 2
862 // to change, causing a scroll in panel 1 etc)
863 ap.setToScrollComplementPanel(false);
864 wasScrolled = ap.scrollToPosition(results);
867 seqCanvas.revalidate();
869 ap.setToScrollComplementPanel(true);
872 boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
873 if (seqCanvas.highlightSearchResults(results, fastPaint))
875 setStatusMessage(results);
877 return results.isEmpty() ? null : getHighlightInfo(results);
881 * temporary hack: answers a message suitable to show on structure hover
882 * label. This is normally null. It is a peptide variation description if
884 * <li>results are a single residue in a protein alignment</li>
885 * <li>there is a mapping to a coding sequence (codon)</li>
886 * <li>there are one or more SNP variant features on the codon</li>
888 * in which case the answer is of the format (e.g.) "p.Glu388Asp"
893 private String getHighlightInfo(SearchResultsI results)
896 * ideally, just find mapped CDS (as we don't care about render style here);
897 * for now, go via split frame complement's FeatureRenderer
899 AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
900 if (complement == null)
904 AlignFrame af = Desktop.getAlignFrameFor(complement);
905 FeatureRendererModel fr2 = af.getFeatureRenderer();
907 int j = results.getSize();
908 List<String> infos = new ArrayList<>();
909 for (int i = 0; i < j; i++)
911 SearchResultMatchI match = results.getResults().get(i);
912 int pos = match.getStart();
913 if (pos == match.getEnd())
915 SequenceI seq = match.getSequence();
916 SequenceI ds = seq.getDatasetSequence() == null ? seq
917 : seq.getDatasetSequence();
918 MappedFeatures mf = fr2
919 .findComplementFeaturesAtResidue(ds, pos);
922 for (SequenceFeature sf : mf.features)
924 String pv = mf.findProteinVariants(sf);
925 if (pv.length() > 0 && !infos.contains(pv))
938 StringBuilder sb = new StringBuilder();
939 for (String info : infos)
947 return sb.toString();
951 public VamsasSource getVamsasSource()
953 return this.ap == null ? null : this.ap.av;
957 public void updateColours(SequenceI seq, int index)
959 System.out.println("update the seqPanel colours");
964 * Action on mouse movement is to update the status bar to show the current
965 * sequence position, and (if features are shown) to show any features at the
966 * position in a tooltip. Does nothing if the mouse move does not change
972 public void mouseMoved(MouseEvent evt)
976 // This is because MacOSX creates a mouseMoved
977 // If control is down, other platforms will not.
981 final MousePos mousePos = findMousePosition(evt);
982 if (mousePos.equals(lastMousePosition))
985 * just a pixel move without change of 'cell'
989 lastMousePosition = mousePos;
991 if (mousePos.isOverAnnotation())
993 mouseMovedOverAnnotation(mousePos);
996 final int seq = mousePos.seqIndex;
998 final int column = mousePos.column;
999 if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1001 lastMousePosition = null;
1002 setToolTipText(null);
1004 ap.alignFrame.setStatus("");
1008 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1010 if (column >= sequence.getLength())
1016 * set status bar message, returning residue position in sequence
1018 boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1019 final int pos = setStatusMessage(sequence, column, seq);
1020 if (ssm != null && !isGapped)
1022 mouseOverSequence(sequence, column, pos);
1025 tooltipText.setLength(6); // "<html>"
1027 SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1030 for (int g = 0; g < groups.length; g++)
1032 if (groups[g].getStartRes() <= column
1033 && groups[g].getEndRes() >= column)
1035 if (!groups[g].getName().startsWith("JTreeGroup")
1036 && !groups[g].getName().startsWith("JGroup"))
1038 tooltipText.append(groups[g].getName());
1041 if (groups[g].getDescription() != null)
1043 tooltipText.append(": " + groups[g].getDescription());
1050 * add any features at the position to the tooltip; if over a gap, only
1051 * add features that straddle the gap (pos may be the residue before or
1054 if (av.isShowSequenceFeatures())
1056 List<SequenceFeature> features = ap.getFeatureRenderer()
1057 .findFeaturesAtColumn(sequence, column + 1);
1058 seqARep.appendFeatures(tooltipText, pos, features,
1059 this.ap.getSeqPanel().seqCanvas.fr);
1062 * add features in CDS/protein complement at the corresponding
1063 * position if configured to do so
1065 if (av.isShowComplementFeatures())
1067 if (!Comparison.isGap(sequence.getCharAt(column)))
1069 AlignViewportI complement = ap.getAlignViewport()
1070 .getCodingComplement();
1071 AlignFrame af = Desktop.getAlignFrameFor(complement);
1072 FeatureRendererModel fr2 = af.getFeatureRenderer();
1073 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1077 seqARep.appendFeatures(tooltipText, pos, mf, fr2);
1082 if (tooltipText.length() == 6) // <html>
1084 setToolTipText(null);
1089 if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
1091 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1092 tooltipText.append("...");
1094 String textString = tooltipText.toString();
1095 if (lastTooltip == null || !lastTooltip.equals(textString))
1097 formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1099 setToolTipText(formattedTooltipText);
1100 lastTooltip = textString;
1106 * When the view is in wrapped mode, and the mouse is over an annotation row,
1107 * shows the corresponding tooltip and status message (if any)
1112 protected void mouseMovedOverAnnotation(MousePos pos)
1114 final int column = pos.column;
1115 final int rowIndex = pos.annotationIndex;
1117 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1122 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1124 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1126 setToolTipText(tooltip);
1127 lastTooltip = tooltip;
1129 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1131 ap.alignFrame.setStatus(msg);
1134 private Point lastp = null;
1136 private JToolTip tempTip = new JLabel().createToolTip();
1141 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1144 public Point getToolTipLocation(MouseEvent event)
1148 if (tooltipText == null || tooltipText.length() <= 6)
1153 if (lastp != null && event.isShiftDown())
1159 int x = event.getX();
1160 int y = event.getY();
1163 tempTip.setTipText(formattedTooltipText);
1164 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1166 // was x += (w - x < 200) ? -(w / 2) : 5;
1167 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1168 p = new Point(x, y + 20); // BH 2018 was - 20?
1176 * set when the current UI interaction has resulted in a change that requires
1177 * shading in overviews and structures to be recalculated. this could be
1178 * changed to a something more expressive that indicates what actually has
1179 * changed, so selective redraws can be applied (ie. only structures, only
1182 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1185 * set if av.getSelectionGroup() refers to a group that is defined on the
1186 * alignment view, rather than a transient selection
1188 // private boolean editingDefinedGroup = false; // TODO: refactor to
1189 // avcontroller or viewModel
1192 * Sets the status message in alignment panel, showing the sequence number
1193 * (index) and id, and residue and residue position if not at a gap, for the
1194 * given sequence and column position. Returns the residue position returned
1195 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1196 * if at a gapped position.
1199 * aligned sequence object
1203 * index of sequence in alignment
1204 * @return sequence position of residue at column, or adjacent residue if at a
1207 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1209 char sequenceChar = sequence.getCharAt(column);
1210 int pos = sequence.findPosition(column);
1211 setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1217 * Builds the status message for the current cursor location and writes it to
1218 * the status bar, for example
1221 * Sequence 3 ID: FER1_SOLLC
1222 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1223 * Sequence 5 ID: FER1_PEA Residue: B (3)
1224 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1229 * sequence position in the alignment (1..)
1230 * @param sequenceChar
1231 * the character under the cursor
1233 * the sequence residue position (if not over a gap)
1235 protected void setStatusMessage(SequenceI sequence, int seqIndex,
1236 char sequenceChar, int residuePos)
1238 StringBuilder text = new StringBuilder(32);
1241 * Sequence number (if known), and sequence name.
1243 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1244 text.append("Sequence").append(seqno).append(" ID: ")
1245 .append(sequence.getName());
1247 String residue = null;
1250 * Try to translate the display character to residue name (null for gap).
1252 boolean isGapped = Comparison.isGap(sequenceChar);
1256 boolean nucleotide = av.getAlignment().isNucleotide();
1257 String displayChar = String.valueOf(sequenceChar);
1260 residue = ResidueProperties.nucleotideName.get(displayChar);
1264 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1265 : ("*".equals(displayChar) ? "STOP"
1266 : ResidueProperties.aa2Triplet.get(displayChar));
1268 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1269 .append(": ").append(residue == null ? displayChar : residue);
1271 text.append(" (").append(Integer.toString(residuePos)).append(")");
1273 ap.alignFrame.setStatus(text.toString());
1277 * Set the status bar message to highlight the first matched position in
1282 private void setStatusMessage(SearchResultsI results)
1284 AlignmentI al = this.av.getAlignment();
1285 int sequenceIndex = al.findIndex(results);
1286 if (sequenceIndex == -1)
1290 SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1291 for (SearchResultMatchI m : results.getResults())
1293 SequenceI seq = m.getSequence();
1294 if (seq.getDatasetSequence() != null)
1296 seq = seq.getDatasetSequence();
1301 int start = m.getStart();
1302 setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1313 public void mouseDragged(MouseEvent evt)
1315 MousePos pos = findMousePosition(evt);
1316 if (pos.isOverAnnotation() || pos.column == -1)
1321 if (mouseWheelPressed)
1323 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1324 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1326 int oldWidth = av.getCharWidth();
1328 // Which is bigger, left-right or up-down?
1329 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1330 .abs(evt.getX() - lastMousePress.getX()))
1333 * on drag up or down, decrement or increment font size
1335 int fontSize = av.font.getSize();
1336 boolean fontChanged = false;
1338 if (evt.getY() < lastMousePress.getY())
1343 else if (evt.getY() > lastMousePress.getY())
1356 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1358 av.setFont(newFont, true);
1359 av.setCharWidth(oldWidth);
1363 ap.av.getCodingComplement().setFont(newFont, true);
1364 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1365 .getSplitViewContainer();
1366 splitFrame.adjustLayout();
1367 splitFrame.repaint();
1374 * on drag left or right, decrement or increment character width
1377 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1379 newWidth = av.getCharWidth() - 1;
1380 av.setCharWidth(newWidth);
1382 else if (evt.getX() > lastMousePress.getX())
1384 newWidth = av.getCharWidth() + 1;
1385 av.setCharWidth(newWidth);
1389 ap.paintAlignment(false, false);
1393 * need to ensure newWidth is set on cdna, regardless of which
1394 * panel the mouse drag happened in; protein will compute its
1395 * character width as 1:1 or 3:1
1397 av.getCodingComplement().setCharWidth(newWidth);
1398 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1399 .getSplitViewContainer();
1400 splitFrame.adjustLayout();
1401 splitFrame.repaint();
1406 FontMetrics fm = getFontMetrics(av.getFont());
1407 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1409 lastMousePress = evt.getPoint();
1416 dragStretchGroup(evt);
1420 int res = pos.column;
1427 if ((editLastRes == -1) || (editLastRes == res))
1432 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1434 // dragLeft, delete gap
1435 editSequence(false, false, res);
1439 editSequence(true, false, res);
1442 mouseDragging = true;
1443 if (scrollThread != null)
1445 scrollThread.setMousePosition(evt.getPoint());
1450 * Edits the sequence to insert or delete one or more gaps, in response to a
1451 * mouse drag or cursor mode command. The number of inserts/deletes may be
1452 * specified with the cursor command, or else depends on the mouse event
1453 * (normally one column, but potentially more for a fast mouse drag).
1455 * Delete gaps is limited to the number of gaps left of the cursor position
1456 * (mouse drag), or at or right of the cursor position (cursor mode).
1458 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1459 * the current selection group.
1461 * In locked editing mode (with a selection group present), inserts/deletions
1462 * within the selection group are limited to its boundaries (and edits outside
1463 * the group stop at its border).
1466 * true to insert gaps, false to delete gaps
1468 * (unused parameter)
1470 * the column at which to perform the action; the number of columns
1471 * affected depends on <code>this.editLastRes</code> (cursor column
1474 synchronized void editSequence(boolean insertGap, boolean editSeq,
1478 int fixedRight = -1;
1479 boolean fixedColumns = false;
1480 SequenceGroup sg = av.getSelectionGroup();
1482 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1484 // No group, but the sequence may represent a group
1485 if (!groupEditing && av.hasHiddenRows())
1487 if (av.isHiddenRepSequence(seq))
1489 sg = av.getRepresentedSequences(seq);
1490 groupEditing = true;
1494 StringBuilder message = new StringBuilder(64); // for status bar
1497 * make a name for the edit action, for
1498 * status bar message and Undo/Redo menu
1500 String label = null;
1503 message.append("Edit group:");
1504 label = MessageManager.getString("action.edit_group");
1508 message.append("Edit sequence: " + seq.getName());
1509 label = seq.getName();
1510 if (label.length() > 10)
1512 label = label.substring(0, 10);
1514 label = MessageManager.formatMessage("label.edit_params",
1520 * initialise the edit command if there is not
1521 * already one being extended
1523 if (editCommand == null)
1525 editCommand = new EditCommand(label);
1530 message.append(" insert ");
1534 message.append(" delete ");
1537 message.append(Math.abs(startres - editLastRes) + " gaps.");
1538 ap.alignFrame.setStatus(message.toString());
1541 * is there a selection group containing the sequence being edited?
1542 * if so the boundary of the group is the limit of the edit
1543 * (but the edit may be inside or outside the selection group)
1545 boolean inSelectionGroup = sg != null
1546 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1547 if (groupEditing || inSelectionGroup)
1549 fixedColumns = true;
1551 // sg might be null as the user may only see 1 sequence,
1552 // but the sequence represents a group
1555 if (!av.isHiddenRepSequence(seq))
1560 sg = av.getRepresentedSequences(seq);
1563 fixedLeft = sg.getStartRes();
1564 fixedRight = sg.getEndRes();
1566 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1567 || (startres >= fixedLeft && editLastRes < fixedLeft)
1568 || (startres > fixedRight && editLastRes <= fixedRight)
1569 || (startres <= fixedRight && editLastRes > fixedRight))
1575 if (fixedLeft > startres)
1577 fixedRight = fixedLeft - 1;
1580 else if (fixedRight < startres)
1582 fixedLeft = fixedRight;
1587 if (av.hasHiddenColumns())
1589 fixedColumns = true;
1590 int y1 = av.getAlignment().getHiddenColumns()
1591 .getNextHiddenBoundary(true, startres);
1592 int y2 = av.getAlignment().getHiddenColumns()
1593 .getNextHiddenBoundary(false, startres);
1595 if ((insertGap && startres > y1 && editLastRes < y1)
1596 || (!insertGap && startres < y2 && editLastRes > y2))
1602 // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1603 // Selection spans a hidden region
1604 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1612 fixedRight = y2 - 1;
1617 boolean success = doEditSequence(insertGap, editSeq, startres,
1618 fixedRight, fixedColumns, sg);
1621 * report what actually happened (might be less than
1622 * what was requested), by inspecting the edit commands added
1624 String msg = getEditStatusMessage(editCommand);
1625 ap.alignFrame.setStatus(msg == null ? " " : msg);
1631 editLastRes = startres;
1632 seqCanvas.repaint();
1636 * A helper method that performs the requested editing to insert or delete
1637 * gaps (if possible). Answers true if the edit was successful, false if could
1638 * only be performed in part or not at all. Failure may occur in 'locked edit'
1639 * mode, when an insertion requires a matching gapped position (or column) to
1640 * delete, and deletion requires an adjacent gapped position (or column) to
1644 * true if inserting gap(s), false if deleting
1646 * (unused parameter, currently always false)
1648 * the column at which to perform the edit
1650 * fixed right boundary column of a locked edit (within or to the
1651 * left of a selection group)
1652 * @param fixedColumns
1653 * true if this is a locked edit
1655 * the sequence group (if group edit is being performed)
1658 protected boolean doEditSequence(final boolean insertGap,
1659 final boolean editSeq, final int startres, int fixedRight,
1660 final boolean fixedColumns, final SequenceGroup sg)
1662 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1663 SequenceI[] seqs = new SequenceI[] { seq };
1667 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1668 int g, groupSize = vseqs.size();
1669 SequenceI[] groupSeqs = new SequenceI[groupSize];
1670 for (g = 0; g < groupSeqs.length; g++)
1672 groupSeqs[g] = vseqs.get(g);
1678 // If the user has selected the whole sequence, and is dragging to
1679 // the right, we can still extend the alignment and selectionGroup
1680 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1681 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1684 av.getAlignment().getWidth() + startres - editLastRes);
1685 fixedRight = sg.getEndRes();
1688 // Is it valid with fixed columns??
1689 // Find the next gap before the end
1690 // of the visible region boundary
1691 boolean blank = false;
1692 for (; fixedRight > editLastRes; fixedRight--)
1696 for (g = 0; g < groupSize; g++)
1698 for (int j = 0; j < startres - editLastRes; j++)
1701 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1716 if (sg.getSize() == av.getAlignment().getHeight())
1718 if ((av.hasHiddenColumns()
1719 && startres < av.getAlignment().getHiddenColumns()
1720 .getNextHiddenBoundary(false, startres)))
1725 int alWidth = av.getAlignment().getWidth();
1726 if (av.hasHiddenRows())
1728 int hwidth = av.getAlignment().getHiddenSequences()
1730 if (hwidth > alWidth)
1735 // We can still insert gaps if the selectionGroup
1736 // contains all the sequences
1737 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1738 fixedRight = alWidth + startres - editLastRes;
1748 else if (!insertGap)
1750 // / Are we able to delete?
1751 // ie are all columns blank?
1753 for (g = 0; g < groupSize; g++)
1755 for (int j = startres; j < editLastRes; j++)
1757 if (groupSeqs[g].getLength() <= j)
1762 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1764 // Not a gap, block edit not valid
1773 // dragging to the right
1774 if (fixedColumns && fixedRight != -1)
1776 for (int j = editLastRes; j < startres; j++)
1778 insertGap(j, groupSeqs, fixedRight);
1783 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1784 startres - editLastRes, false);
1789 // dragging to the left
1790 if (fixedColumns && fixedRight != -1)
1792 for (int j = editLastRes; j > startres; j--)
1794 deleteChar(startres, groupSeqs, fixedRight);
1799 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1800 editLastRes - startres, false);
1807 * editing a single sequence
1811 // dragging to the right
1812 if (fixedColumns && fixedRight != -1)
1814 for (int j = editLastRes; j < startres; j++)
1816 if (!insertGap(j, seqs, fixedRight))
1819 * e.g. cursor mode command specified
1820 * more inserts than are possible
1828 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1829 startres - editLastRes, false);
1836 // dragging to the left
1837 if (fixedColumns && fixedRight != -1)
1839 for (int j = editLastRes; j > startres; j--)
1841 if (!Comparison.isGap(seq.getCharAt(startres)))
1845 deleteChar(startres, seqs, fixedRight);
1850 // could be a keyboard edit trying to delete none gaps
1852 for (int m = startres; m < editLastRes; m++)
1854 if (!Comparison.isGap(seq.getCharAt(m)))
1862 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1867 {// insertGap==false AND editSeq==TRUE;
1868 if (fixedColumns && fixedRight != -1)
1870 for (int j = editLastRes; j < startres; j++)
1872 insertGap(j, seqs, fixedRight);
1877 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1878 startres - editLastRes, false);
1888 * Constructs an informative status bar message while dragging to insert or
1889 * delete gaps. Answers null if inserts and deletes cancel out.
1891 * @param editCommand
1892 * a command containing the list of individual edits
1895 protected static String getEditStatusMessage(EditCommand editCommand)
1897 if (editCommand == null)
1903 * add any inserts, and subtract any deletes,
1904 * not counting those auto-inserted when doing a 'locked edit'
1905 * (so only counting edits 'under the cursor')
1908 for (Edit cmd : editCommand.getEdits())
1910 if (!cmd.isSystemGenerated())
1912 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1920 * inserts and deletes cancel out
1925 String msgKey = count > 1 ? "label.insert_gaps"
1926 : (count == 1 ? "label.insert_gap"
1927 : (count == -1 ? "label.delete_gap"
1928 : "label.delete_gaps"));
1929 count = Math.abs(count);
1931 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1935 * Inserts one gap at column j, deleting the right-most gapped column up to
1936 * (and including) fixedColumn. Returns true if the edit is successful, false
1937 * if no blank column is available to allow the insertion to be balanced by a
1942 * @param fixedColumn
1945 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1947 int blankColumn = fixedColumn;
1948 for (int s = 0; s < seq.length; s++)
1950 // Find the next gap before the end of the visible region boundary
1951 // If lastCol > j, theres a boundary after the gap insertion
1953 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1955 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1957 // Theres a space, so break and insert the gap
1962 if (blankColumn <= j)
1964 blankColumn = fixedColumn;
1970 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1972 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1978 * Helper method to add and perform one edit action
1984 * @param systemGenerated
1985 * true if the edit is a 'balancing' delete (or insert) to match a
1986 * user's insert (or delete) in a locked editing region
1988 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1989 int count, boolean systemGenerated)
1992 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1993 av.getAlignment().getGapCharacter());
1994 edit.setSystemGenerated(systemGenerated);
1996 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2000 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2001 * each of the given sequences. The caller should ensure that all sequences
2002 * are gapped in column j.
2006 * @param fixedColumn
2008 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2010 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2012 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2016 * On reentering the panel, stops any scrolling that was started on dragging
2022 public void mouseEntered(MouseEvent e)
2032 * On leaving the panel, if the mouse is being dragged, starts a thread to
2033 * scroll it until the mouse is released (in unwrapped mode only)
2038 public void mouseExited(MouseEvent e)
2040 lastMousePosition = null;
2041 ap.alignFrame.setStatus(" ");
2042 if (av.getWrapAlignment())
2047 if (mouseDragging && scrollThread == null)
2049 startScrolling(e.getPoint());
2054 * Handler for double-click on a position with one or more sequence features.
2055 * Opens the Amend Features dialog to allow feature details to be amended, or
2056 * the feature deleted.
2059 public void mouseClicked(MouseEvent evt)
2061 SequenceGroup sg = null;
2062 MousePos pos = findMousePosition(evt);
2063 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2068 if (evt.getClickCount() > 1)
2070 sg = av.getSelectionGroup();
2071 if (sg != null && sg.getSize() == 1
2072 && sg.getEndRes() - sg.getStartRes() < 2)
2074 av.setSelectionGroup(null);
2077 int column = pos.column;
2080 * find features at the position (if not gapped), or straddling
2081 * the position (if at a gap)
2083 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2084 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2085 .findFeaturesAtColumn(sequence, column + 1);
2087 if (!features.isEmpty())
2090 * highlight the first feature at the position on the alignment
2092 SearchResultsI highlight = new SearchResults();
2093 highlight.addResult(sequence, features.get(0).getBegin(), features
2095 seqCanvas.highlightSearchResults(highlight, true);
2098 * open the Amend Features dialog
2100 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2101 false).showDialog();
2107 public void mouseWheelMoved(MouseWheelEvent e)
2110 double wheelRotation = e.getPreciseWheelRotation();
2111 if (wheelRotation > 0)
2113 if (e.isShiftDown())
2115 av.getRanges().scrollRight(true);
2120 av.getRanges().scrollUp(false);
2123 else if (wheelRotation < 0)
2125 if (e.isShiftDown())
2127 av.getRanges().scrollRight(false);
2131 av.getRanges().scrollUp(true);
2136 * update status bar and tooltip for new position
2137 * (need to synthesize a mouse movement to refresh tooltip)
2140 ToolTipManager.sharedInstance().mouseMoved(e);
2149 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2151 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2156 final int res = pos.column;
2157 final int seq = pos.seqIndex;
2159 updateOverviewAndStructs = false;
2161 startWrapBlock = wrappedBlock;
2163 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2165 if ((sequence == null) || (res > sequence.getLength()))
2170 stretchGroup = av.getSelectionGroup();
2172 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2174 stretchGroup = av.getAlignment().findGroup(sequence, res);
2175 if (stretchGroup != null)
2177 // only update the current selection if the popup menu has a group to
2179 av.setSelectionGroup(stretchGroup);
2184 * defer right-mouse click handling to mouseReleased on Windows
2185 * (where isPopupTrigger() will answer true)
2186 * NB isRightMouseButton is also true for Cmd-click on Mac
2188 if (Platform.isWinRightButton(evt))
2193 if (evt.isPopupTrigger()) // Mac: mousePressed
2195 showPopupMenu(evt, pos);
2201 seqCanvas.cursorX = res;
2202 seqCanvas.cursorY = seq;
2203 seqCanvas.repaint();
2207 if (stretchGroup == null)
2209 createStretchGroup(res, sequence);
2212 if (stretchGroup != null)
2214 stretchGroup.addPropertyChangeListener(seqCanvas);
2217 seqCanvas.repaint();
2220 private void createStretchGroup(int res, SequenceI sequence)
2222 // Only if left mouse button do we want to change group sizes
2223 // define a new group here
2224 SequenceGroup sg = new SequenceGroup();
2225 sg.setStartRes(res);
2227 sg.addSequence(sequence, false);
2228 av.setSelectionGroup(sg);
2231 if (av.getConservationSelected())
2233 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2237 if (av.getAbovePIDThreshold())
2239 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2242 // TODO: stretchGroup will always be not null. Is this a merge error ?
2243 // or is there a threading issue here?
2244 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2246 // Edit end res position of selected group
2247 changeEndRes = true;
2249 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2251 // Edit end res position of selected group
2252 changeStartRes = true;
2254 stretchGroup.getWidth();
2259 * Build and show a pop-up menu at the right-click mouse position
2264 void showPopupMenu(MouseEvent evt, MousePos pos)
2266 final int column = pos.column;
2267 final int seq = pos.seqIndex;
2268 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2269 if (sequence != null)
2271 PopupMenu pop = new PopupMenu(ap, sequence, column);
2272 pop.show(this, evt.getX(), evt.getY());
2277 * Update the display after mouse up on a selection or group
2280 * mouse released event details
2282 * true if this event is happening after a mouse drag (rather than a
2285 protected void doMouseReleasedDefineMode(MouseEvent evt,
2288 if (stretchGroup == null)
2293 stretchGroup.removePropertyChangeListener(seqCanvas);
2295 // always do this - annotation has own state
2296 // but defer colourscheme update until hidden sequences are passed in
2297 boolean vischange = stretchGroup.recalcConservation(true);
2298 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2300 if (stretchGroup.cs != null)
2304 stretchGroup.cs.alignmentChanged(stretchGroup,
2305 av.getHiddenRepSequences());
2308 ResidueShaderI groupColourScheme = stretchGroup
2309 .getGroupColourScheme();
2310 String name = stretchGroup.getName();
2311 if (stretchGroup.cs.conservationApplied())
2313 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2315 if (stretchGroup.cs.getThreshold() > 0)
2317 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2320 PaintRefresher.Refresh(this, av.getSequenceSetId());
2321 // TODO: structure colours only need updating if stretchGroup used to or now
2322 // does contain sequences with structure views
2323 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2324 updateOverviewAndStructs = false;
2325 changeEndRes = false;
2326 changeStartRes = false;
2327 stretchGroup = null;
2332 * Resizes the borders of a selection group depending on the direction of
2337 protected void dragStretchGroup(MouseEvent evt)
2339 if (stretchGroup == null)
2344 MousePos pos = findMousePosition(evt);
2345 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2350 int res = pos.column;
2351 int y = pos.seqIndex;
2353 if (wrappedBlock != startWrapBlock)
2358 res = Math.min(res, av.getAlignment().getWidth()-1);
2360 if (stretchGroup.getEndRes() == res)
2362 // Edit end res position of selected group
2363 changeEndRes = true;
2365 else if (stretchGroup.getStartRes() == res)
2367 // Edit start res position of selected group
2368 changeStartRes = true;
2371 if (res < av.getRanges().getStartRes())
2373 res = av.getRanges().getStartRes();
2378 if (res > (stretchGroup.getStartRes() - 1))
2380 stretchGroup.setEndRes(res);
2381 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2384 else if (changeStartRes)
2386 if (res < (stretchGroup.getEndRes() + 1))
2388 stretchGroup.setStartRes(res);
2389 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2393 int dragDirection = 0;
2399 else if (y < oldSeq)
2404 while ((y != oldSeq) && (oldSeq > -1)
2405 && (y < av.getAlignment().getHeight()))
2407 // This routine ensures we don't skip any sequences, as the
2408 // selection is quite slow.
2409 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2411 oldSeq += dragDirection;
2418 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2420 if (stretchGroup.getSequences(null).contains(nextSeq))
2422 stretchGroup.deleteSequence(seq, false);
2423 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2429 stretchGroup.addSequence(seq, false);
2432 stretchGroup.addSequence(nextSeq, false);
2433 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2442 mouseDragging = true;
2444 if (scrollThread != null)
2446 scrollThread.setMousePosition(evt.getPoint());
2450 * construct a status message showing the range of the selection
2452 StringBuilder status = new StringBuilder(64);
2453 List<SequenceI> seqs = stretchGroup.getSequences();
2454 String name = seqs.get(0).getName();
2455 if (name.length() > 20)
2457 name = name.substring(0, 20);
2459 status.append(name).append(" - ");
2460 name = seqs.get(seqs.size() - 1).getName();
2461 if (name.length() > 20)
2463 name = name.substring(0, 20);
2465 status.append(name).append(" ");
2466 int startRes = stretchGroup.getStartRes();
2467 status.append(" cols ").append(String.valueOf(startRes + 1))
2469 int endRes = stretchGroup.getEndRes();
2470 status.append(String.valueOf(endRes + 1));
2471 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2472 .append(String.valueOf(endRes - startRes + 1)).append(")");
2473 ap.alignFrame.setStatus(status.toString());
2477 * Stops the scroll thread if it is running
2479 void stopScrolling()
2481 if (scrollThread != null)
2483 scrollThread.stopScrolling();
2484 scrollThread = null;
2486 mouseDragging = false;
2490 * Starts a thread to scroll the alignment, towards a given mouse position
2491 * outside the panel bounds, unless the alignment is in wrapped mode
2495 void startScrolling(Point mousePos)
2498 * set this.mouseDragging in case this was called from
2499 * a drag in ScalePanel or AnnotationPanel
2501 mouseDragging = true;
2502 if (!av.getWrapAlignment() && scrollThread == null)
2504 scrollThread = new ScrollThread();
2505 scrollThread.setMousePosition(mousePos);
2506 if (Platform.isJS())
2509 * Javascript - run every 20ms until scrolling stopped
2510 * or reaches the limit of scrollable alignment
2512 Timer t = new Timer(20, new ActionListener()
2515 public void actionPerformed(ActionEvent e)
2517 if (scrollThread != null)
2519 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2520 scrollThread.scrollOnce();
2524 t.addActionListener(new ActionListener()
2527 public void actionPerformed(ActionEvent e)
2529 if (scrollThread == null)
2531 // SeqPanel.stopScrolling called
2541 * Java - run in a new thread
2543 scrollThread.start();
2549 * Performs scrolling of the visible alignment left, right, up or down, until
2550 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2551 * limit of the alignment is reached
2553 class ScrollThread extends Thread
2555 private Point mousePos;
2557 private volatile boolean keepRunning = true;
2562 public ScrollThread()
2564 setName("SeqPanel$ScrollThread");
2568 * Sets the position of the mouse that determines the direction of the
2569 * scroll to perform. If this is called as the mouse moves, scrolling should
2570 * respond accordingly. For example, if the mouse is dragged right, scroll
2571 * right should start; if the drag continues down, scroll down should also
2576 public void setMousePosition(Point p)
2582 * Sets a flag that will cause the thread to exit
2584 public void stopScrolling()
2586 keepRunning = false;
2590 * Scrolls the alignment left or right, and/or up or down, depending on the
2591 * last notified mouse position, until the limit of the alignment is
2592 * reached, or a flag is set to stop the scroll
2599 if (mousePos != null)
2601 keepRunning = scrollOnce();
2606 } catch (Exception ex)
2610 SeqPanel.this.scrollThread = null;
2616 * <li>one row up, if the mouse is above the panel</li>
2617 * <li>one row down, if the mouse is below the panel</li>
2618 * <li>one column left, if the mouse is left of the panel</li>
2619 * <li>one column right, if the mouse is right of the panel</li>
2621 * Answers true if a scroll was performed, false if not - meaning either
2622 * that the mouse position is within the panel, or the edge of the alignment
2625 boolean scrollOnce()
2628 * quit after mouseUp ensures interrupt in JalviewJS
2635 boolean scrolled = false;
2636 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2643 // mouse is above this panel - try scroll up
2644 scrolled = ranges.scrollUp(true);
2646 else if (mousePos.y >= getHeight())
2648 // mouse is below this panel - try scroll down
2649 scrolled = ranges.scrollUp(false);
2653 * scroll left or right
2657 scrolled |= ranges.scrollRight(false);
2659 else if (mousePos.x >= getWidth())
2661 scrolled |= ranges.scrollRight(true);
2668 * modify current selection according to a received message.
2671 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2672 HiddenColumns hidden, SelectionSource source)
2674 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2675 // handles selection messages...
2676 // TODO: extend config options to allow user to control if selections may be
2677 // shared between viewports.
2678 boolean iSentTheSelection = (av == source
2679 || (source instanceof AlignViewport
2680 && ((AlignmentViewport) source).getSequenceSetId()
2681 .equals(av.getSequenceSetId())));
2683 if (iSentTheSelection)
2685 // respond to our own event by updating dependent dialogs
2686 if (ap.getCalculationDialog() != null)
2688 ap.getCalculationDialog().validateCalcTypes();
2694 // process further ?
2695 if (!av.followSelection)
2701 * Ignore the selection if there is one of our own pending.
2703 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2709 * Check for selection in a view of which this one is a dna/protein
2712 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2717 // do we want to thread this ? (contention with seqsel and colsel locks, I
2720 * only copy colsel if there is a real intersection between
2721 * sequence selection and this panel's alignment
2723 boolean repaint = false;
2724 boolean copycolsel = false;
2726 SequenceGroup sgroup = null;
2727 if (seqsel != null && seqsel.getSize() > 0)
2729 if (av.getAlignment() == null)
2731 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2732 + " ViewId=" + av.getViewId()
2733 + " 's alignment is NULL! returning immediately.");
2736 sgroup = seqsel.intersect(av.getAlignment(),
2737 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2738 if ((sgroup != null && sgroup.getSize() > 0))
2743 if (sgroup != null && sgroup.getSize() > 0)
2745 av.setSelectionGroup(sgroup);
2749 av.setSelectionGroup(null);
2751 av.isSelectionGroupChanged(true);
2756 // the current selection is unset or from a previous message
2757 // so import the new colsel.
2758 if (colsel == null || colsel.isEmpty())
2760 if (av.getColumnSelection() != null)
2762 av.getColumnSelection().clear();
2768 // TODO: shift colSel according to the intersecting sequences
2769 if (av.getColumnSelection() == null)
2771 av.setColumnSelection(new ColumnSelection(colsel));
2775 av.getColumnSelection().setElementsFrom(colsel,
2776 av.getAlignment().getHiddenColumns());
2779 av.isColSelChanged(true);
2783 if (copycolsel && av.hasHiddenColumns()
2784 && (av.getAlignment().getHiddenColumns() == null))
2786 System.err.println("Bad things");
2788 if (repaint) // always true!
2790 // probably finessing with multiple redraws here
2791 PaintRefresher.Refresh(this, av.getSequenceSetId());
2792 // ap.paintAlignment(false);
2795 // lastly, update dependent dialogs
2796 if (ap.getCalculationDialog() != null)
2798 ap.getCalculationDialog().validateCalcTypes();
2804 * If this panel is a cdna/protein translation view of the selection source,
2805 * tries to map the source selection to a local one, and returns true. Else
2812 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2813 ColumnSelection colsel, HiddenColumns hidden,
2814 SelectionSource source)
2816 if (!(source instanceof AlignViewportI))
2820 final AlignViewportI sourceAv = (AlignViewportI) source;
2821 if (sourceAv.getCodingComplement() != av
2822 && av.getCodingComplement() != sourceAv)
2828 * Map sequence selection
2830 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2831 av.setSelectionGroup(sg);
2832 av.isSelectionGroupChanged(true);
2835 * Map column selection
2837 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2839 ColumnSelection cs = new ColumnSelection();
2840 HiddenColumns hs = new HiddenColumns();
2841 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2842 av.setColumnSelection(cs);
2843 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2845 // lastly, update any dependent dialogs
2846 if (ap.getCalculationDialog() != null)
2848 ap.getCalculationDialog().validateCalcTypes();
2852 * repaint alignment, and also Overview or Structure
2853 * if hidden column selection has changed
2855 ap.paintAlignment(hiddenChanged, hiddenChanged);
2862 * @return null or last search results handled by this panel
2864 public SearchResultsI getLastSearchResults()
2866 return lastSearchResults;