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;
80 * The main scrollable region containing the alignment and just to the right of
84 * @version $Revision: 1.130 $
86 public class SeqPanel extends JPanel
87 implements MouseListener, MouseMotionListener, MouseWheelListener,
88 SequenceListener, SelectionListener
91 * a class that holds computed mouse position
92 * - column of the alignment (0...)
93 * - sequence offset (0...)
94 * - annotation row offset (0...)
95 * where annotation offset is -1 unless the alignment is shown
96 * in wrapped mode, annotations are shown, and the mouse is
97 * over an annnotation row
102 * alignment column position of cursor (0...)
107 * index in alignment of sequence under cursor,
108 * or nearest above if cursor is not over a sequence
113 * index in annotations array of annotation under the cursor
114 * (only possible in wrapped mode with annotations shown),
115 * or -1 if cursor is not over an annotation row
117 final int annotationIndex;
119 MousePos(int col, int seq, int ann)
123 annotationIndex = ann;
126 boolean isOverAnnotation()
128 return annotationIndex != -1;
132 public boolean equals(Object obj)
134 if (obj == null || !(obj instanceof MousePos))
138 MousePos o = (MousePos) obj;
139 boolean b = (column == o.column && seqIndex == o.seqIndex
140 && annotationIndex == o.annotationIndex);
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 int unshownFeatures = 0;
1055 if (av.isShowSequenceFeatures())
1057 List<SequenceFeature> features = ap.getFeatureRenderer()
1058 .findFeaturesAtColumn(sequence, column + 1);
1059 unshownFeatures = seqARep.appendFeaturesLengthLimit(tooltipText, pos,
1061 this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
1064 * add features in CDS/protein complement at the corresponding
1065 * position if configured to do so
1067 if (av.isShowComplementFeatures())
1069 if (!Comparison.isGap(sequence.getCharAt(column)))
1071 AlignViewportI complement = ap.getAlignViewport()
1072 .getCodingComplement();
1073 AlignFrame af = Desktop.getAlignFrameFor(complement);
1074 FeatureRendererModel fr2 = af.getFeatureRenderer();
1075 MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1079 unshownFeatures = seqARep.appendFeaturesLengthLimit(
1080 tooltipText, pos, mf, fr2,
1081 MAX_TOOLTIP_LENGTH);
1086 if (tooltipText.length() == 6) // "<html>"
1088 setToolTipText(null);
1093 if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1095 tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1096 tooltipText.append("...");
1098 if (unshownFeatures > 0)
1100 tooltipText.append("<br/>").append("... ").append("<i>")
1101 .append(MessageManager.formatMessage(
1102 "label.features_not_shown", unshownFeatures))
1105 String textString = tooltipText.toString();
1106 if (lastTooltip == null || !lastTooltip.equals(textString))
1108 formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1110 setToolTipText(formattedTooltipText);
1111 lastTooltip = textString;
1117 * When the view is in wrapped mode, and the mouse is over an annotation row,
1118 * shows the corresponding tooltip and status message (if any)
1123 protected void mouseMovedOverAnnotation(MousePos pos)
1125 final int column = pos.column;
1126 final int rowIndex = pos.annotationIndex;
1128 if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1133 AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1135 String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1137 setToolTipText(tooltip);
1138 lastTooltip = tooltip;
1140 String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1142 ap.alignFrame.setStatus(msg);
1145 private Point lastp = null;
1147 private JToolTip tempTip = new JLabel().createToolTip();
1152 * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1155 public Point getToolTipLocation(MouseEvent event)
1159 if (tooltipText == null || tooltipText.length() <= 6)
1164 if (lastp != null && event.isShiftDown())
1170 int x = event.getX();
1171 int y = event.getY();
1174 tempTip.setTipText(formattedTooltipText);
1175 int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1177 // was x += (w - x < 200) ? -(w / 2) : 5;
1178 x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1179 p = new Point(x, y + 20); // BH 2018 was - 20?
1187 * set when the current UI interaction has resulted in a change that requires
1188 * shading in overviews and structures to be recalculated. this could be
1189 * changed to a something more expressive that indicates what actually has
1190 * changed, so selective redraws can be applied (ie. only structures, only
1193 private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1196 * set if av.getSelectionGroup() refers to a group that is defined on the
1197 * alignment view, rather than a transient selection
1199 // private boolean editingDefinedGroup = false; // TODO: refactor to
1200 // avcontroller or viewModel
1203 * Sets the status message in alignment panel, showing the sequence number
1204 * (index) and id, and residue and residue position if not at a gap, for the
1205 * given sequence and column position. Returns the residue position returned
1206 * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1207 * if at a gapped position.
1210 * aligned sequence object
1214 * index of sequence in alignment
1215 * @return sequence position of residue at column, or adjacent residue if at a
1218 int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1220 char sequenceChar = sequence.getCharAt(column);
1221 int pos = sequence.findPosition(column);
1222 setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1228 * Builds the status message for the current cursor location and writes it to
1229 * the status bar, for example
1232 * Sequence 3 ID: FER1_SOLLC
1233 * Sequence 5 ID: FER1_PEA Residue: THR (4)
1234 * Sequence 5 ID: FER1_PEA Residue: B (3)
1235 * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1240 * sequence position in the alignment (1..)
1241 * @param sequenceChar
1242 * the character under the cursor
1244 * the sequence residue position (if not over a gap)
1246 protected void setStatusMessage(SequenceI sequence, int seqIndex,
1247 char sequenceChar, int residuePos)
1249 StringBuilder text = new StringBuilder(32);
1252 * Sequence number (if known), and sequence name.
1254 String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1255 text.append("Sequence").append(seqno).append(" ID: ")
1256 .append(sequence.getName());
1258 String residue = null;
1261 * Try to translate the display character to residue name (null for gap).
1263 boolean isGapped = Comparison.isGap(sequenceChar);
1267 boolean nucleotide = av.getAlignment().isNucleotide();
1268 String displayChar = String.valueOf(sequenceChar);
1271 residue = ResidueProperties.nucleotideName.get(displayChar);
1275 residue = "X".equalsIgnoreCase(displayChar) ? "X"
1276 : ("*".equals(displayChar) ? "STOP"
1277 : ResidueProperties.aa2Triplet.get(displayChar));
1279 text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1280 .append(": ").append(residue == null ? displayChar : residue);
1282 text.append(" (").append(Integer.toString(residuePos)).append(")");
1284 ap.alignFrame.setStatus(text.toString());
1288 * Set the status bar message to highlight the first matched position in
1293 private void setStatusMessage(SearchResultsI results)
1295 AlignmentI al = this.av.getAlignment();
1296 int sequenceIndex = al.findIndex(results);
1297 if (sequenceIndex == -1)
1301 SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1302 for (SearchResultMatchI m : results.getResults())
1304 SequenceI seq = m.getSequence();
1305 if (seq.getDatasetSequence() != null)
1307 seq = seq.getDatasetSequence();
1312 int start = m.getStart();
1313 setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1324 public void mouseDragged(MouseEvent evt)
1326 MousePos pos = findMousePosition(evt);
1327 if (pos.isOverAnnotation() || pos.column == -1)
1332 if (mouseWheelPressed)
1334 boolean inSplitFrame = ap.av.getCodingComplement() != null;
1335 boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1337 int oldWidth = av.getCharWidth();
1339 // Which is bigger, left-right or up-down?
1340 if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1341 .abs(evt.getX() - lastMousePress.getX()))
1344 * on drag up or down, decrement or increment font size
1346 int fontSize = av.font.getSize();
1347 boolean fontChanged = false;
1349 if (evt.getY() < lastMousePress.getY())
1354 else if (evt.getY() > lastMousePress.getY())
1367 Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1369 av.setFont(newFont, true);
1370 av.setCharWidth(oldWidth);
1374 ap.av.getCodingComplement().setFont(newFont, true);
1375 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1376 .getSplitViewContainer();
1377 splitFrame.adjustLayout();
1378 splitFrame.repaint();
1385 * on drag left or right, decrement or increment character width
1388 if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1390 newWidth = av.getCharWidth() - 1;
1391 av.setCharWidth(newWidth);
1393 else if (evt.getX() > lastMousePress.getX())
1395 newWidth = av.getCharWidth() + 1;
1396 av.setCharWidth(newWidth);
1400 ap.paintAlignment(false, false);
1404 * need to ensure newWidth is set on cdna, regardless of which
1405 * panel the mouse drag happened in; protein will compute its
1406 * character width as 1:1 or 3:1
1408 av.getCodingComplement().setCharWidth(newWidth);
1409 SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1410 .getSplitViewContainer();
1411 splitFrame.adjustLayout();
1412 splitFrame.repaint();
1417 FontMetrics fm = getFontMetrics(av.getFont());
1418 av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1420 lastMousePress = evt.getPoint();
1427 dragStretchGroup(evt);
1431 int res = pos.column;
1438 if ((editLastRes == -1) || (editLastRes == res))
1443 if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1445 // dragLeft, delete gap
1446 editSequence(false, false, res);
1450 editSequence(true, false, res);
1453 mouseDragging = true;
1454 if (scrollThread != null)
1456 scrollThread.setMousePosition(evt.getPoint());
1461 * Edits the sequence to insert or delete one or more gaps, in response to a
1462 * mouse drag or cursor mode command. The number of inserts/deletes may be
1463 * specified with the cursor command, or else depends on the mouse event
1464 * (normally one column, but potentially more for a fast mouse drag).
1466 * Delete gaps is limited to the number of gaps left of the cursor position
1467 * (mouse drag), or at or right of the cursor position (cursor mode).
1469 * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1470 * the current selection group.
1472 * In locked editing mode (with a selection group present), inserts/deletions
1473 * within the selection group are limited to its boundaries (and edits outside
1474 * the group stop at its border).
1477 * true to insert gaps, false to delete gaps
1479 * (unused parameter)
1481 * the column at which to perform the action; the number of columns
1482 * affected depends on <code>this.editLastRes</code> (cursor column
1485 synchronized void editSequence(boolean insertGap, boolean editSeq,
1489 int fixedRight = -1;
1490 boolean fixedColumns = false;
1491 SequenceGroup sg = av.getSelectionGroup();
1493 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1495 // No group, but the sequence may represent a group
1496 if (!groupEditing && av.hasHiddenRows())
1498 if (av.isHiddenRepSequence(seq))
1500 sg = av.getRepresentedSequences(seq);
1501 groupEditing = true;
1505 StringBuilder message = new StringBuilder(64); // for status bar
1508 * make a name for the edit action, for
1509 * status bar message and Undo/Redo menu
1511 String label = null;
1514 message.append("Edit group:");
1515 label = MessageManager.getString("action.edit_group");
1519 message.append("Edit sequence: " + seq.getName());
1520 label = seq.getName();
1521 if (label.length() > 10)
1523 label = label.substring(0, 10);
1525 label = MessageManager.formatMessage("label.edit_params",
1531 * initialise the edit command if there is not
1532 * already one being extended
1534 if (editCommand == null)
1536 editCommand = new EditCommand(label);
1541 message.append(" insert ");
1545 message.append(" delete ");
1548 message.append(Math.abs(startres - editLastRes) + " gaps.");
1549 ap.alignFrame.setStatus(message.toString());
1552 * is there a selection group containing the sequence being edited?
1553 * if so the boundary of the group is the limit of the edit
1554 * (but the edit may be inside or outside the selection group)
1556 boolean inSelectionGroup = sg != null
1557 && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1558 if (groupEditing || inSelectionGroup)
1560 fixedColumns = true;
1562 // sg might be null as the user may only see 1 sequence,
1563 // but the sequence represents a group
1566 if (!av.isHiddenRepSequence(seq))
1571 sg = av.getRepresentedSequences(seq);
1574 fixedLeft = sg.getStartRes();
1575 fixedRight = sg.getEndRes();
1577 if ((startres < fixedLeft && editLastRes >= fixedLeft)
1578 || (startres >= fixedLeft && editLastRes < fixedLeft)
1579 || (startres > fixedRight && editLastRes <= fixedRight)
1580 || (startres <= fixedRight && editLastRes > fixedRight))
1586 if (fixedLeft > startres)
1588 fixedRight = fixedLeft - 1;
1591 else if (fixedRight < startres)
1593 fixedLeft = fixedRight;
1598 if (av.hasHiddenColumns())
1600 fixedColumns = true;
1601 int y1 = av.getAlignment().getHiddenColumns()
1602 .getNextHiddenBoundary(true, startres);
1603 int y2 = av.getAlignment().getHiddenColumns()
1604 .getNextHiddenBoundary(false, startres);
1606 if ((insertGap && startres > y1 && editLastRes < y1)
1607 || (!insertGap && startres < y2 && editLastRes > y2))
1613 // Selection spans a hidden region
1614 if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1622 fixedRight = y2 - 1;
1627 boolean success = doEditSequence(insertGap, editSeq, startres,
1628 fixedRight, fixedColumns, sg);
1631 * report what actually happened (might be less than
1632 * what was requested), by inspecting the edit commands added
1634 String msg = getEditStatusMessage(editCommand);
1635 ap.alignFrame.setStatus(msg == null ? " " : msg);
1641 editLastRes = startres;
1642 seqCanvas.repaint();
1646 * A helper method that performs the requested editing to insert or delete
1647 * gaps (if possible). Answers true if the edit was successful, false if could
1648 * only be performed in part or not at all. Failure may occur in 'locked edit'
1649 * mode, when an insertion requires a matching gapped position (or column) to
1650 * delete, and deletion requires an adjacent gapped position (or column) to
1654 * true if inserting gap(s), false if deleting
1656 * (unused parameter, currently always false)
1658 * the column at which to perform the edit
1660 * fixed right boundary column of a locked edit (within or to the
1661 * left of a selection group)
1662 * @param fixedColumns
1663 * true if this is a locked edit
1665 * the sequence group (if group edit is being performed)
1668 protected boolean doEditSequence(final boolean insertGap,
1669 final boolean editSeq, final int startres, int fixedRight,
1670 final boolean fixedColumns, final SequenceGroup sg)
1672 final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1673 SequenceI[] seqs = new SequenceI[] { seq };
1677 List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1678 int g, groupSize = vseqs.size();
1679 SequenceI[] groupSeqs = new SequenceI[groupSize];
1680 for (g = 0; g < groupSeqs.length; g++)
1682 groupSeqs[g] = vseqs.get(g);
1688 // If the user has selected the whole sequence, and is dragging to
1689 // the right, we can still extend the alignment and selectionGroup
1690 if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1691 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1694 av.getAlignment().getWidth() + startres - editLastRes);
1695 fixedRight = sg.getEndRes();
1698 // Is it valid with fixed columns??
1699 // Find the next gap before the end
1700 // of the visible region boundary
1701 boolean blank = false;
1702 for (; fixedRight > editLastRes; fixedRight--)
1706 for (g = 0; g < groupSize; g++)
1708 for (int j = 0; j < startres - editLastRes; j++)
1711 .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1726 if (sg.getSize() == av.getAlignment().getHeight())
1728 if ((av.hasHiddenColumns()
1729 && startres < av.getAlignment().getHiddenColumns()
1730 .getNextHiddenBoundary(false, startres)))
1735 int alWidth = av.getAlignment().getWidth();
1736 if (av.hasHiddenRows())
1738 int hwidth = av.getAlignment().getHiddenSequences()
1740 if (hwidth > alWidth)
1745 // We can still insert gaps if the selectionGroup
1746 // contains all the sequences
1747 sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1748 fixedRight = alWidth + startres - editLastRes;
1758 else if (!insertGap)
1760 // / Are we able to delete?
1761 // ie are all columns blank?
1763 for (g = 0; g < groupSize; g++)
1765 for (int j = startres; j < editLastRes; j++)
1767 if (groupSeqs[g].getLength() <= j)
1772 if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1774 // Not a gap, block edit not valid
1783 // dragging to the right
1784 if (fixedColumns && fixedRight != -1)
1786 for (int j = editLastRes; j < startres; j++)
1788 insertGap(j, groupSeqs, fixedRight);
1793 appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1794 startres - editLastRes, false);
1799 // dragging to the left
1800 if (fixedColumns && fixedRight != -1)
1802 for (int j = editLastRes; j > startres; j--)
1804 deleteChar(startres, groupSeqs, fixedRight);
1809 appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1810 editLastRes - startres, false);
1817 * editing a single sequence
1821 // dragging to the right
1822 if (fixedColumns && fixedRight != -1)
1824 for (int j = editLastRes; j < startres; j++)
1826 if (!insertGap(j, seqs, fixedRight))
1829 * e.g. cursor mode command specified
1830 * more inserts than are possible
1838 appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1839 startres - editLastRes, false);
1846 // dragging to the left
1847 if (fixedColumns && fixedRight != -1)
1849 for (int j = editLastRes; j > startres; j--)
1851 if (!Comparison.isGap(seq.getCharAt(startres)))
1855 deleteChar(startres, seqs, fixedRight);
1860 // could be a keyboard edit trying to delete none gaps
1862 for (int m = startres; m < editLastRes; m++)
1864 if (!Comparison.isGap(seq.getCharAt(m)))
1872 appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1877 {// insertGap==false AND editSeq==TRUE;
1878 if (fixedColumns && fixedRight != -1)
1880 for (int j = editLastRes; j < startres; j++)
1882 insertGap(j, seqs, fixedRight);
1887 appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1888 startres - editLastRes, false);
1898 * Constructs an informative status bar message while dragging to insert or
1899 * delete gaps. Answers null if inserts and deletes cancel out.
1901 * @param editCommand
1902 * a command containing the list of individual edits
1905 protected static String getEditStatusMessage(EditCommand editCommand)
1907 if (editCommand == null)
1913 * add any inserts, and subtract any deletes,
1914 * not counting those auto-inserted when doing a 'locked edit'
1915 * (so only counting edits 'under the cursor')
1918 for (Edit cmd : editCommand.getEdits())
1920 if (!cmd.isSystemGenerated())
1922 count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1930 * inserts and deletes cancel out
1935 String msgKey = count > 1 ? "label.insert_gaps"
1936 : (count == 1 ? "label.insert_gap"
1937 : (count == -1 ? "label.delete_gap"
1938 : "label.delete_gaps"));
1939 count = Math.abs(count);
1941 return MessageManager.formatMessage(msgKey, String.valueOf(count));
1945 * Inserts one gap at column j, deleting the right-most gapped column up to
1946 * (and including) fixedColumn. Returns true if the edit is successful, false
1947 * if no blank column is available to allow the insertion to be balanced by a
1952 * @param fixedColumn
1955 boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1957 int blankColumn = fixedColumn;
1958 for (int s = 0; s < seq.length; s++)
1960 // Find the next gap before the end of the visible region boundary
1961 // If lastCol > j, theres a boundary after the gap insertion
1963 for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1965 if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1967 // Theres a space, so break and insert the gap
1972 if (blankColumn <= j)
1974 blankColumn = fixedColumn;
1980 appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1982 appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1988 * Helper method to add and perform one edit action
1994 * @param systemGenerated
1995 * true if the edit is a 'balancing' delete (or insert) to match a
1996 * user's insert (or delete) in a locked editing region
1998 protected void appendEdit(Action action, SequenceI[] seq, int pos,
1999 int count, boolean systemGenerated)
2002 final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2003 av.getAlignment().getGapCharacter());
2004 edit.setSystemGenerated(systemGenerated);
2006 editCommand.appendEdit(edit, av.getAlignment(), true, null);
2010 * Deletes the character at column j, and inserts a gap at fixedColumn, in
2011 * each of the given sequences. The caller should ensure that all sequences
2012 * are gapped in column j.
2016 * @param fixedColumn
2018 void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2020 appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2022 appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2026 * On reentering the panel, stops any scrolling that was started on dragging
2032 public void mouseEntered(MouseEvent e)
2042 * On leaving the panel, if the mouse is being dragged, starts a thread to
2043 * scroll it until the mouse is released (in unwrapped mode only)
2048 public void mouseExited(MouseEvent e)
2050 lastMousePosition = null;
2051 ap.alignFrame.setStatus(" ");
2053 if (av.getWrapAlignment())
2060 // * start scrolling if mouse dragging, whether the drag started
2061 // * in the scale panel or this panel
2063 // if (mouseDragging || ap.getScalePanel().isMouseDragging())
2065 // startScrolling(new Point(e.getX(), 0));
2068 if (mouseDragging && scrollThread == null)
2070 startScrolling(e.getPoint());
2075 * Handler for double-click on a position with one or more sequence features.
2076 * Opens the Amend Features dialog to allow feature details to be amended, or
2077 * the feature deleted.
2080 public void mouseClicked(MouseEvent evt)
2082 SequenceGroup sg = null;
2083 MousePos pos = findMousePosition(evt);
2084 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2089 if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2091 sg = av.getSelectionGroup();
2092 if (sg != null && sg.getSize() == 1
2093 && sg.getEndRes() - sg.getStartRes() < 2)
2095 av.setSelectionGroup(null);
2098 int column = pos.column;
2101 * find features at the position (if not gapped), or straddling
2102 * the position (if at a gap)
2104 SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2105 List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2106 .findFeaturesAtColumn(sequence, column + 1);
2108 if (!features.isEmpty())
2111 * highlight the first feature at the position on the alignment
2113 SearchResultsI highlight = new SearchResults();
2114 highlight.addResult(sequence, features.get(0).getBegin(), features
2116 seqCanvas.highlightSearchResults(highlight, true);
2119 * open the Amend Features dialog
2121 new FeatureEditor(ap, Collections.singletonList(sequence), features,
2122 false).showDialog();
2128 public void mouseWheelMoved(MouseWheelEvent e)
2131 double wheelRotation = e.getPreciseWheelRotation();
2132 if (wheelRotation > 0)
2134 if (e.isShiftDown())
2136 av.getRanges().scrollRight(true);
2141 av.getRanges().scrollUp(false);
2144 else if (wheelRotation < 0)
2146 if (e.isShiftDown())
2148 av.getRanges().scrollRight(false);
2152 av.getRanges().scrollUp(true);
2157 * update status bar and tooltip for new position
2158 * (need to synthesize a mouse movement to refresh tooltip)
2161 ToolTipManager.sharedInstance().mouseMoved(e);
2170 protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2172 if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2177 final int res = pos.column;
2178 final int seq = pos.seqIndex;
2180 updateOverviewAndStructs = false;
2182 startWrapBlock = wrappedBlock;
2184 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2186 if ((sequence == null) || (res > sequence.getLength()))
2191 stretchGroup = av.getSelectionGroup();
2193 if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2195 stretchGroup = av.getAlignment().findGroup(sequence, res);
2196 if (stretchGroup != null)
2198 // only update the current selection if the popup menu has a group to
2200 av.setSelectionGroup(stretchGroup);
2205 * defer right-mouse click handling to mouseReleased on Windows
2206 * (where isPopupTrigger() will answer true)
2207 * NB isRightMouseButton is also true for Cmd-click on Mac
2209 if (Platform.isWinRightButton(evt))
2214 if (evt.isPopupTrigger()) // Mac: mousePressed
2216 showPopupMenu(evt, pos);
2222 seqCanvas.cursorX = res;
2223 seqCanvas.cursorY = seq;
2224 seqCanvas.repaint();
2228 if (stretchGroup == null)
2230 createStretchGroup(res, sequence);
2233 if (stretchGroup != null)
2235 stretchGroup.addPropertyChangeListener(seqCanvas);
2238 seqCanvas.repaint();
2241 private void createStretchGroup(int res, SequenceI sequence)
2243 // Only if left mouse button do we want to change group sizes
2244 // define a new group here
2245 SequenceGroup sg = new SequenceGroup();
2246 sg.setStartRes(res);
2248 sg.addSequence(sequence, false);
2249 av.setSelectionGroup(sg);
2252 if (av.getConservationSelected())
2254 SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2258 if (av.getAbovePIDThreshold())
2260 SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2263 // TODO: stretchGroup will always be not null. Is this a merge error ?
2264 // or is there a threading issue here?
2265 if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2267 // Edit end res position of selected group
2268 changeEndRes = true;
2270 else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2272 // Edit end res position of selected group
2273 changeStartRes = true;
2275 stretchGroup.getWidth();
2280 * Build and show a pop-up menu at the right-click mouse position
2285 void showPopupMenu(MouseEvent evt, MousePos pos)
2287 final int column = pos.column;
2288 final int seq = pos.seqIndex;
2289 SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2290 if (sequence != null)
2292 PopupMenu pop = new PopupMenu(ap, sequence, column);
2293 pop.show(this, evt.getX(), evt.getY());
2298 * Update the display after mouse up on a selection or group
2301 * mouse released event details
2303 * true if this event is happening after a mouse drag (rather than a
2306 protected void doMouseReleasedDefineMode(MouseEvent evt,
2309 if (stretchGroup == null)
2314 stretchGroup.removePropertyChangeListener(seqCanvas);
2316 // always do this - annotation has own state
2317 // but defer colourscheme update until hidden sequences are passed in
2318 boolean vischange = stretchGroup.recalcConservation(true);
2319 updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2321 if (stretchGroup.cs != null)
2325 stretchGroup.cs.alignmentChanged(stretchGroup,
2326 av.getHiddenRepSequences());
2329 ResidueShaderI groupColourScheme = stretchGroup
2330 .getGroupColourScheme();
2331 String name = stretchGroup.getName();
2332 if (stretchGroup.cs.conservationApplied())
2334 SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2336 if (stretchGroup.cs.getThreshold() > 0)
2338 SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2341 PaintRefresher.Refresh(this, av.getSequenceSetId());
2342 // TODO: structure colours only need updating if stretchGroup used to or now
2343 // does contain sequences with structure views
2344 ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2345 updateOverviewAndStructs = false;
2346 changeEndRes = false;
2347 changeStartRes = false;
2348 stretchGroup = null;
2353 * Resizes the borders of a selection group depending on the direction of
2358 protected void dragStretchGroup(MouseEvent evt)
2360 if (stretchGroup == null)
2365 MousePos pos = findMousePosition(evt);
2366 if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2371 int res = pos.column;
2372 int y = pos.seqIndex;
2374 if (wrappedBlock != startWrapBlock)
2379 res = Math.min(res, av.getAlignment().getWidth()-1);
2381 if (stretchGroup.getEndRes() == res)
2383 // Edit end res position of selected group
2384 changeEndRes = true;
2386 else if (stretchGroup.getStartRes() == res)
2388 // Edit start res position of selected group
2389 changeStartRes = true;
2392 if (res < av.getRanges().getStartRes())
2394 res = av.getRanges().getStartRes();
2399 if (res > (stretchGroup.getStartRes() - 1))
2401 stretchGroup.setEndRes(res);
2402 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2405 else if (changeStartRes)
2407 if (res < (stretchGroup.getEndRes() + 1))
2409 stretchGroup.setStartRes(res);
2410 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2414 int dragDirection = 0;
2420 else if (y < oldSeq)
2425 while ((y != oldSeq) && (oldSeq > -1)
2426 && (y < av.getAlignment().getHeight()))
2428 // This routine ensures we don't skip any sequences, as the
2429 // selection is quite slow.
2430 Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2432 oldSeq += dragDirection;
2439 Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2441 if (stretchGroup.getSequences(null).contains(nextSeq))
2443 stretchGroup.deleteSequence(seq, false);
2444 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2450 stretchGroup.addSequence(seq, false);
2453 stretchGroup.addSequence(nextSeq, false);
2454 updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2463 mouseDragging = true;
2465 if (scrollThread != null)
2467 scrollThread.setMousePosition(evt.getPoint());
2471 * construct a status message showing the range of the selection
2473 StringBuilder status = new StringBuilder(64);
2474 List<SequenceI> seqs = stretchGroup.getSequences();
2475 String name = seqs.get(0).getName();
2476 if (name.length() > 20)
2478 name = name.substring(0, 20);
2480 status.append(name).append(" - ");
2481 name = seqs.get(seqs.size() - 1).getName();
2482 if (name.length() > 20)
2484 name = name.substring(0, 20);
2486 status.append(name).append(" ");
2487 int startRes = stretchGroup.getStartRes();
2488 status.append(" cols ").append(String.valueOf(startRes + 1))
2490 int endRes = stretchGroup.getEndRes();
2491 status.append(String.valueOf(endRes + 1));
2492 status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2493 .append(String.valueOf(endRes - startRes + 1)).append(")");
2494 ap.alignFrame.setStatus(status.toString());
2498 * Stops the scroll thread if it is running
2500 void stopScrolling()
2502 if (scrollThread != null)
2504 scrollThread.stopScrolling();
2505 scrollThread = null;
2507 mouseDragging = false;
2511 * Starts a thread to scroll the alignment, towards a given mouse position
2512 * outside the panel bounds, unless the alignment is in wrapped mode
2516 void startScrolling(Point mousePos)
2519 * set this.mouseDragging in case this was called from
2520 * a drag in ScalePanel or AnnotationPanel
2522 mouseDragging = true;
2523 if (!av.getWrapAlignment() && scrollThread == null)
2525 scrollThread = new ScrollThread();
2526 scrollThread.setMousePosition(mousePos);
2527 if (Platform.isJS())
2530 * Javascript - run every 20ms until scrolling stopped
2531 * or reaches the limit of scrollable alignment
2533 Timer t = new Timer(20, new ActionListener()
2536 public void actionPerformed(ActionEvent e)
2538 if (scrollThread != null)
2540 // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2541 scrollThread.scrollOnce();
2545 t.addActionListener(new ActionListener()
2548 public void actionPerformed(ActionEvent e)
2550 if (scrollThread == null)
2552 // SeqPanel.stopScrolling called
2562 * Java - run in a new thread
2564 scrollThread.start();
2570 * Performs scrolling of the visible alignment left, right, up or down, until
2571 * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2572 * limit of the alignment is reached
2574 class ScrollThread extends Thread
2576 private Point mousePos;
2578 private volatile boolean keepRunning = true;
2583 public ScrollThread()
2585 setName("SeqPanel$ScrollThread");
2589 * Sets the position of the mouse that determines the direction of the
2590 * scroll to perform. If this is called as the mouse moves, scrolling should
2591 * respond accordingly. For example, if the mouse is dragged right, scroll
2592 * right should start; if the drag continues down, scroll down should also
2597 public void setMousePosition(Point p)
2603 * Sets a flag that will cause the thread to exit
2605 public void stopScrolling()
2607 keepRunning = false;
2611 * Scrolls the alignment left or right, and/or up or down, depending on the
2612 * last notified mouse position, until the limit of the alignment is
2613 * reached, or a flag is set to stop the scroll
2620 if (mousePos != null)
2622 keepRunning = scrollOnce();
2627 } catch (Exception ex)
2631 SeqPanel.this.scrollThread = null;
2637 * <li>one row up, if the mouse is above the panel</li>
2638 * <li>one row down, if the mouse is below the panel</li>
2639 * <li>one column left, if the mouse is left of the panel</li>
2640 * <li>one column right, if the mouse is right of the panel</li>
2642 * Answers true if a scroll was performed, false if not - meaning either
2643 * that the mouse position is within the panel, or the edge of the alignment
2646 boolean scrollOnce()
2649 * quit after mouseUp ensures interrupt in JalviewJS
2656 boolean scrolled = false;
2657 ViewportRanges ranges = SeqPanel.this.av.getRanges();
2664 // mouse is above this panel - try scroll up
2665 scrolled = ranges.scrollUp(true);
2667 else if (mousePos.y >= getHeight())
2669 // mouse is below this panel - try scroll down
2670 scrolled = ranges.scrollUp(false);
2674 * scroll left or right
2678 scrolled |= ranges.scrollRight(false);
2680 else if (mousePos.x >= getWidth())
2682 scrolled |= ranges.scrollRight(true);
2689 * modify current selection according to a received message.
2692 public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2693 HiddenColumns hidden, SelectionSource source)
2695 // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2696 // handles selection messages...
2697 // TODO: extend config options to allow user to control if selections may be
2698 // shared between viewports.
2699 boolean iSentTheSelection = (av == source
2700 || (source instanceof AlignViewport
2701 && ((AlignmentViewport) source).getSequenceSetId()
2702 .equals(av.getSequenceSetId())));
2704 if (iSentTheSelection)
2706 // respond to our own event by updating dependent dialogs
2707 if (ap.getCalculationDialog() != null)
2709 ap.getCalculationDialog().validateCalcTypes();
2715 // process further ?
2716 if (!av.followSelection)
2722 * Ignore the selection if there is one of our own pending.
2724 if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2730 * Check for selection in a view of which this one is a dna/protein
2733 if (selectionFromTranslation(seqsel, colsel, hidden, source))
2738 // do we want to thread this ? (contention with seqsel and colsel locks, I
2741 * only copy colsel if there is a real intersection between
2742 * sequence selection and this panel's alignment
2744 boolean repaint = false;
2745 boolean copycolsel = false;
2747 SequenceGroup sgroup = null;
2748 if (seqsel != null && seqsel.getSize() > 0)
2750 if (av.getAlignment() == null)
2752 Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2753 + " ViewId=" + av.getViewId()
2754 + " 's alignment is NULL! returning immediately.");
2757 sgroup = seqsel.intersect(av.getAlignment(),
2758 (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2759 if ((sgroup != null && sgroup.getSize() > 0))
2764 if (sgroup != null && sgroup.getSize() > 0)
2766 av.setSelectionGroup(sgroup);
2770 av.setSelectionGroup(null);
2772 av.isSelectionGroupChanged(true);
2777 // the current selection is unset or from a previous message
2778 // so import the new colsel.
2779 if (colsel == null || colsel.isEmpty())
2781 if (av.getColumnSelection() != null)
2783 av.getColumnSelection().clear();
2789 // TODO: shift colSel according to the intersecting sequences
2790 if (av.getColumnSelection() == null)
2792 av.setColumnSelection(new ColumnSelection(colsel));
2796 av.getColumnSelection().setElementsFrom(colsel,
2797 av.getAlignment().getHiddenColumns());
2800 av.isColSelChanged(true);
2804 if (copycolsel && av.hasHiddenColumns()
2805 && (av.getAlignment().getHiddenColumns() == null))
2807 System.err.println("Bad things");
2809 if (repaint) // always true!
2811 // probably finessing with multiple redraws here
2812 PaintRefresher.Refresh(this, av.getSequenceSetId());
2813 // ap.paintAlignment(false);
2816 // lastly, update dependent dialogs
2817 if (ap.getCalculationDialog() != null)
2819 ap.getCalculationDialog().validateCalcTypes();
2825 * If this panel is a cdna/protein translation view of the selection source,
2826 * tries to map the source selection to a local one, and returns true. Else
2833 protected boolean selectionFromTranslation(SequenceGroup seqsel,
2834 ColumnSelection colsel, HiddenColumns hidden,
2835 SelectionSource source)
2837 if (!(source instanceof AlignViewportI))
2841 final AlignViewportI sourceAv = (AlignViewportI) source;
2842 if (sourceAv.getCodingComplement() != av
2843 && av.getCodingComplement() != sourceAv)
2849 * Map sequence selection
2851 SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2852 av.setSelectionGroup(sg);
2853 av.isSelectionGroupChanged(true);
2856 * Map column selection
2858 // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2860 ColumnSelection cs = new ColumnSelection();
2861 HiddenColumns hs = new HiddenColumns();
2862 MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2863 av.setColumnSelection(cs);
2864 boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2866 // lastly, update any dependent dialogs
2867 if (ap.getCalculationDialog() != null)
2869 ap.getCalculationDialog().validateCalcTypes();
2873 * repaint alignment, and also Overview or Structure
2874 * if hidden column selection has changed
2876 ap.paintAlignment(hiddenChanged, hiddenChanged);
2883 * @return null or last search results handled by this panel
2885 public SearchResultsI getLastSearchResults()
2887 return lastSearchResults;
2891 * scroll to the given row/column - or nearest visible location
2896 public void scrollTo(int row, int column)
2899 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2900 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2901 ap.scrollTo(column, column, row, true, true);
2905 * scroll to the given row - or nearest visible location
2909 public void scrollToRow(int row)
2912 row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2913 ap.scrollTo(ap.av.getRanges().getStartRes(),
2914 ap.av.getRanges().getStartRes(), row, true, true);
2918 * scroll to the given column - or nearest visible location
2922 public void scrollToColumn(int column)
2925 column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2926 ap.scrollTo(column, column, ap.av.getRanges().getStartSeq(), true,