JAL-3490 merged with 2.11.2 develop
[jalview.git] / src / jalview / gui / SeqPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
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.
11  *  
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.
16  * 
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.
20  */
21 package jalview.gui;
22
23 import java.awt.BorderLayout;
24 import java.awt.Color;
25 import java.awt.Font;
26 import java.awt.FontMetrics;
27 import java.awt.Point;
28 import java.awt.event.ActionEvent;
29 import java.awt.event.ActionListener;
30 import java.awt.event.MouseEvent;
31 import java.awt.event.MouseListener;
32 import java.awt.event.MouseMotionListener;
33 import java.awt.event.MouseWheelEvent;
34 import java.awt.event.MouseWheelListener;
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.List;
38
39 import javax.swing.JLabel;
40 import javax.swing.JPanel;
41 import javax.swing.JToolTip;
42 import javax.swing.SwingUtilities;
43 import javax.swing.Timer;
44 import javax.swing.ToolTipManager;
45
46 import jalview.api.AlignViewportI;
47 import jalview.bin.Cache;
48 import jalview.commands.EditCommand;
49 import jalview.commands.EditCommand.Action;
50 import jalview.commands.EditCommand.Edit;
51 import jalview.datamodel.AlignmentAnnotation;
52 import jalview.datamodel.AlignmentI;
53 import jalview.datamodel.ColumnSelection;
54 import jalview.datamodel.HiddenColumns;
55 import jalview.datamodel.MappedFeatures;
56 import jalview.datamodel.SearchResultMatchI;
57 import jalview.datamodel.SearchResults;
58 import jalview.datamodel.SearchResultsI;
59 import jalview.datamodel.Sequence;
60 import jalview.datamodel.SequenceFeature;
61 import jalview.datamodel.SequenceGroup;
62 import jalview.datamodel.SequenceI;
63 import jalview.io.SequenceAnnotationReport;
64 import jalview.renderer.ResidueShaderI;
65 import jalview.schemes.ResidueProperties;
66 import jalview.structure.SelectionListener;
67 import jalview.structure.SelectionSource;
68 import jalview.structure.SequenceListener;
69 import jalview.structure.StructureSelectionManager;
70 import jalview.structure.VamsasSource;
71 import jalview.util.Comparison;
72 import jalview.util.MappingUtils;
73 import jalview.util.MessageManager;
74 import jalview.util.Platform;
75 import jalview.viewmodel.AlignmentViewport;
76 import jalview.viewmodel.ViewportRanges;
77 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
78
79 /**
80  * DOCUMENT ME!
81  * 
82  * @author $author$
83  * @version $Revision: 1.130 $
84  */
85 public class SeqPanel extends JPanel
86         implements MouseListener, MouseMotionListener, MouseWheelListener,
87         SequenceListener, SelectionListener
88 {
89   /*
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
97    */
98   static class MousePos
99   {
100     /*
101      * alignment column position of cursor (0...)
102      */
103     final int column;
104
105     /*
106      * index in alignment of sequence under cursor,
107      * or nearest above if cursor is not over a sequence
108      */
109     final int seqIndex;
110
111     /*
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
115      */
116     final int annotationIndex;
117
118     MousePos(int col, int seq, int ann)
119     {
120       column = col;
121       seqIndex = seq;
122       annotationIndex = ann;
123     }
124
125     boolean isOverAnnotation()
126     {
127       return annotationIndex != -1;
128     }
129
130     @Override
131     public boolean equals(Object obj)
132     {
133       if (obj == null || !(obj instanceof MousePos))
134       {
135         return false;
136       }
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);
141       return b;
142     }
143
144     /**
145      * A simple hashCode that ensures that instances that satisfy equals() have
146      * the same hashCode
147      */
148     @Override
149     public int hashCode()
150     {
151       return column + seqIndex + annotationIndex;
152     }
153
154     /**
155      * toString method for debug output purposes only
156      */
157     @Override
158     public String toString()
159     {
160       return String.format("c%d:s%d:a%d", column, seqIndex,
161               annotationIndex);
162     }
163   }
164
165   private static final int MAX_TOOLTIP_LENGTH = 300;
166
167   public SeqCanvas seqCanvas;
168
169   public AlignmentPanel ap;
170
171   /*
172    * last position for mouseMoved event
173    */
174   private MousePos lastMousePosition;
175
176   protected int editLastRes;
177
178   protected int editStartSeq;
179
180   protected AlignViewport av;
181
182   ScrollThread scrollThread = null;
183
184   boolean mouseDragging = false;
185
186   boolean editingSeqs = false;
187
188   boolean groupEditing = false;
189
190   // ////////////////////////////////////////
191   // ///Everything below this is for defining the boundary of the rubberband
192   // ////////////////////////////////////////
193   int oldSeq = -1;
194
195   boolean changeEndSeq = false;
196
197   boolean changeStartSeq = false;
198
199   boolean changeEndRes = false;
200
201   boolean changeStartRes = false;
202
203   SequenceGroup stretchGroup = null;
204
205   boolean remove = false;
206
207   Point lastMousePress;
208
209   boolean mouseWheelPressed = false;
210
211   StringBuffer keyboardNo1;
212
213   StringBuffer keyboardNo2;
214
215   private final SequenceAnnotationReport seqARep;
216
217   /*
218    * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
219    * - the tooltip is not set again if unchanged
220    * - this is the tooltip text _before_ formatting as html
221    */
222   private String lastTooltip;
223
224   /*
225    * the last tooltip on mousing over the alignment (or annotation in wrapped mode)
226    * - used to decide where to place the tooltip in getTooltipLocation() 
227    * - this is the tooltip text _after_ formatting as html
228    */
229   private String lastFormattedTooltip;
230
231   EditCommand editCommand;
232
233   StructureSelectionManager ssm;
234
235   SearchResultsI lastSearchResults;
236
237   /**
238    * Creates a new SeqPanel object
239    * 
240    * @param viewport
241    * @param alignPanel
242    */
243   public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
244   {
245     seqARep = new SequenceAnnotationReport(true);
246     ToolTipManager.sharedInstance().registerComponent(this);
247     ToolTipManager.sharedInstance().setInitialDelay(0);
248     ToolTipManager.sharedInstance().setDismissDelay(10000);
249     
250     
251     this.av = viewport;
252     setBackground(Color.white);
253
254     seqCanvas = new SeqCanvas(alignPanel);
255     setLayout(new BorderLayout());
256     add(seqCanvas, BorderLayout.CENTER);
257
258     this.ap = alignPanel;
259
260     if (!viewport.isDataset())
261     {
262       addMouseMotionListener(this);
263       addMouseListener(this);
264       addMouseWheelListener(this);
265       ssm = viewport.getStructureSelectionManager();
266       ssm.addStructureViewerListener(this);
267       ssm.addSelectionListener(this);
268     }
269   }
270
271   int startWrapBlock = -1;
272
273   int wrappedBlock = -1;
274
275   /**
276    * Computes the column and sequence row (and possibly annotation row when in
277    * wrapped mode) for the given mouse position
278    * 
279    * @param evt
280    * @return
281    */
282   MousePos findMousePosition(MouseEvent evt)
283   {
284     int col = findColumn(evt);
285     int seqIndex = -1;
286     int annIndex = -1;
287     int y = evt.getY();
288
289     int charHeight = av.getCharHeight();
290     int alignmentHeight = av.getAlignment().getHeight();
291     if (av.getWrapAlignment())
292     {
293       seqCanvas.calculateWrappedGeometry(seqCanvas.getWidth(),
294               seqCanvas.getHeight());
295
296       /*
297        * yPos modulo height of repeating width
298        */
299       int yOffsetPx = y % seqCanvas.wrappedRepeatHeightPx;
300
301       /*
302        * height of sequences plus space / scale above,
303        * plus gap between sequences and annotations
304        */
305       int alignmentHeightPixels = seqCanvas.wrappedSpaceAboveAlignment
306               + alignmentHeight * charHeight
307               + SeqCanvas.SEQS_ANNOTATION_GAP;
308       if (yOffsetPx >= alignmentHeightPixels)
309       {
310         /*
311          * mouse is over annotations; find annotation index, also set
312          * last sequence above (for backwards compatible behaviour)
313          */
314         AlignmentAnnotation[] anns = av.getAlignment()
315                 .getAlignmentAnnotation();
316         int rowOffsetPx = yOffsetPx - alignmentHeightPixels;
317         annIndex = AnnotationPanel.getRowIndex(rowOffsetPx, anns);
318         seqIndex = alignmentHeight - 1;
319       }
320       else
321       {
322         /*
323          * mouse is over sequence (or the space above sequences)
324          */
325         yOffsetPx -= seqCanvas.wrappedSpaceAboveAlignment;
326         if (yOffsetPx >= 0)
327         {
328           seqIndex = Math.min(yOffsetPx / charHeight, alignmentHeight - 1);
329         }
330       }
331     }
332     else
333     {
334       ViewportRanges ranges = av.getRanges();
335       seqIndex = Math.min((y / charHeight) + ranges.getStartSeq(),
336               alignmentHeight - 1);
337       seqIndex = Math.min(seqIndex, ranges.getEndSeq());
338     }
339
340     return new MousePos(col, seqIndex, annIndex);
341   }
342   /**
343    * Returns the aligned sequence position (base 0) at the mouse position, or
344    * the closest visible one
345    * 
346    * @param evt
347    * @return
348    */
349   int findColumn(MouseEvent evt)
350   {
351     int res = 0;
352     int x = evt.getX();
353
354     final int startRes = av.getRanges().getStartRes();
355     final int charWidth = av.getCharWidth();
356
357     if (av.getWrapAlignment())
358     {
359       int hgap = av.getCharHeight();
360       if (av.getScaleAboveWrapped())
361       {
362         hgap += av.getCharHeight();
363       }
364
365       int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
366               + hgap + seqCanvas.getAnnotationHeight();
367
368       int y = evt.getY();
369       y = Math.max(0, y - hgap);
370       x -= seqCanvas.getLabelWidthWest();
371       if (x < 0)
372       {
373         // mouse is over left scale
374         return -1;
375       }
376
377       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
378       if (cwidth < 1)
379       {
380         return 0;
381       }
382       if (x >= cwidth * charWidth)
383       {
384         // mouse is over right scale
385         return -1;
386       }
387
388       wrappedBlock = y / cHeight;
389       wrappedBlock += startRes / cwidth;
390       // allow for wrapped view scrolled right (possible from Overview)
391       int startOffset = startRes % cwidth;
392       res = wrappedBlock * cwidth + startOffset
393               + Math.min(cwidth - 1, x / charWidth);
394     }
395     else
396     {
397       /*
398        * make sure we calculate relative to visible alignment, 
399        * rather than right-hand gutter
400        */
401       x = Math.min(x, seqCanvas.getX() + seqCanvas.getWidth());
402       res = (x / charWidth) + startRes;
403       res = Math.min(res, av.getRanges().getEndRes());
404     }
405
406     if (av.hasHiddenColumns())
407     {
408       res = av.getAlignment().getHiddenColumns()
409               .visibleToAbsoluteColumn(res);
410     }
411
412     return res;
413   }
414
415   /**
416    * When all of a sequence of edits are complete, put the resulting edit list
417    * on the history stack (undo list), and reset flags for editing in progress.
418    */
419   void endEditing()
420   {
421     try
422     {
423       if (editCommand != null && editCommand.getSize() > 0)
424       {
425         ap.alignFrame.addHistoryItem(editCommand);
426         av.firePropertyChange("alignment", null,
427                 av.getAlignment().getSequences());
428       }
429     } finally
430     {
431       /*
432        * Tidy up come what may...
433        */
434       editStartSeq = -1;
435       editLastRes = -1;
436       editingSeqs = false;
437       groupEditing = false;
438       keyboardNo1 = null;
439       keyboardNo2 = null;
440       editCommand = null;
441     }
442   }
443
444   void setCursorRow()
445   {
446     seqCanvas.cursorY = getKeyboardNo1() - 1;
447     scrollToVisible(true);
448   }
449
450   void setCursorColumn()
451   {
452     seqCanvas.cursorX = getKeyboardNo1() - 1;
453     scrollToVisible(true);
454   }
455
456   void setCursorRowAndColumn()
457   {
458     if (keyboardNo2 == null)
459     {
460       keyboardNo2 = new StringBuffer();
461     }
462     else
463     {
464       seqCanvas.cursorX = getKeyboardNo1() - 1;
465       seqCanvas.cursorY = getKeyboardNo2() - 1;
466       scrollToVisible(true);
467     }
468   }
469
470   void setCursorPosition()
471   {
472     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
473
474     seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
475     scrollToVisible(true);
476   }
477
478   void moveCursor(int dx, int dy)
479   {
480     seqCanvas.cursorX += dx;
481     seqCanvas.cursorY += dy;
482
483     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
484
485     if (av.hasHiddenColumns() && !hidden.isVisible(seqCanvas.cursorX))
486     {
487       int original = seqCanvas.cursorX - dx;
488       int maxWidth = av.getAlignment().getWidth();
489
490       if (!hidden.isVisible(seqCanvas.cursorX))
491       {
492         int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
493         int[] region = hidden.getRegionWithEdgeAtRes(visx);
494
495         if (region != null) // just in case
496         {
497           if (dx == 1)
498           {
499             // moving right
500             seqCanvas.cursorX = region[1] + 1;
501           }
502           else if (dx == -1)
503           {
504             // moving left
505             seqCanvas.cursorX = region[0] - 1;
506           }
507         }
508         seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
509       }
510
511       if (seqCanvas.cursorX >= maxWidth
512               || !hidden.isVisible(seqCanvas.cursorX))
513       {
514         seqCanvas.cursorX = original;
515       }
516     }
517
518     scrollToVisible(false);
519   }
520
521   /**
522    * Scroll to make the cursor visible in the viewport.
523    * 
524    * @param jump
525    *          just jump to the location rather than scrolling
526    */
527   void scrollToVisible(boolean jump)
528   {
529     if (seqCanvas.cursorX < 0)
530     {
531       seqCanvas.cursorX = 0;
532     }
533     else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
534     {
535       seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
536     }
537
538     if (seqCanvas.cursorY < 0)
539     {
540       seqCanvas.cursorY = 0;
541     }
542     else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
543     {
544       seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
545     }
546
547     endEditing();
548
549     boolean repaintNeeded = true;
550     if (jump)
551     {
552       // only need to repaint if the viewport did not move, as otherwise it will
553       // get a repaint
554       repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
555               seqCanvas.cursorY);
556     }
557     else
558     {
559       if (av.getWrapAlignment())
560       {
561         // scrollToWrappedVisible expects x-value to have hidden cols subtracted
562         int x = av.getAlignment().getHiddenColumns()
563                 .absoluteToVisibleColumn(seqCanvas.cursorX);
564         av.getRanges().scrollToWrappedVisible(x);
565       }
566       else
567       {
568         av.getRanges().scrollToVisible(seqCanvas.cursorX,
569                 seqCanvas.cursorY);
570       }
571     }
572
573     if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
574     {
575       setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
576             seqCanvas.cursorX, seqCanvas.cursorY);
577     }
578
579     if (repaintNeeded)
580     {
581       seqCanvas.repaint();
582     }
583   }
584
585
586   void setSelectionAreaAtCursor(boolean topLeft)
587   {
588     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
589
590     if (av.getSelectionGroup() != null)
591     {
592       SequenceGroup sg = av.getSelectionGroup();
593       // Find the top and bottom of this group
594       int min = av.getAlignment().getHeight(), max = 0;
595       for (int i = 0; i < sg.getSize(); i++)
596       {
597         int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
598         if (index > max)
599         {
600           max = index;
601         }
602         if (index < min)
603         {
604           min = index;
605         }
606       }
607
608       max++;
609
610       if (topLeft)
611       {
612         sg.setStartRes(seqCanvas.cursorX);
613         if (sg.getEndRes() < seqCanvas.cursorX)
614         {
615           sg.setEndRes(seqCanvas.cursorX);
616         }
617
618         min = seqCanvas.cursorY;
619       }
620       else
621       {
622         sg.setEndRes(seqCanvas.cursorX);
623         if (sg.getStartRes() > seqCanvas.cursorX)
624         {
625           sg.setStartRes(seqCanvas.cursorX);
626         }
627
628         max = seqCanvas.cursorY + 1;
629       }
630
631       if (min > max)
632       {
633         // Only the user can do this
634         av.setSelectionGroup(null);
635       }
636       else
637       {
638         // Now add any sequences between min and max
639         sg.getSequences(null).clear();
640         for (int i = min; i < max; i++)
641         {
642           sg.addSequence(av.getAlignment().getSequenceAt(i), false);
643         }
644       }
645     }
646
647     if (av.getSelectionGroup() == null)
648     {
649       SequenceGroup sg = new SequenceGroup();
650       sg.setStartRes(seqCanvas.cursorX);
651       sg.setEndRes(seqCanvas.cursorX);
652       sg.addSequence(sequence, false);
653       av.setSelectionGroup(sg);
654     }
655
656     ap.paintAlignment(false, false);
657     av.sendSelection();
658   }
659
660   void insertGapAtCursor(boolean group)
661   {
662     groupEditing = group;
663     editStartSeq = seqCanvas.cursorY;
664     editLastRes = seqCanvas.cursorX;
665     editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
666     endEditing();
667   }
668
669   void deleteGapAtCursor(boolean group)
670   {
671     groupEditing = group;
672     editStartSeq = seqCanvas.cursorY;
673     editLastRes = seqCanvas.cursorX + getKeyboardNo1();
674     editSequence(false, false, seqCanvas.cursorX);
675     endEditing();
676   }
677
678   void insertNucAtCursor(boolean group, String nuc)
679   {
680     // TODO not called - delete?
681     groupEditing = group;
682     editStartSeq = seqCanvas.cursorY;
683     editLastRes = seqCanvas.cursorX;
684     editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
685     endEditing();
686   }
687
688   void numberPressed(char value)
689   {
690     if (keyboardNo1 == null)
691     {
692       keyboardNo1 = new StringBuffer();
693     }
694
695     if (keyboardNo2 != null)
696     {
697       keyboardNo2.append(value);
698     }
699     else
700     {
701       keyboardNo1.append(value);
702     }
703   }
704
705   int getKeyboardNo1()
706   {
707     try
708     {
709       if (keyboardNo1 != null)
710       {
711         int value = Integer.parseInt(keyboardNo1.toString());
712         keyboardNo1 = null;
713         return value;
714       }
715     } catch (Exception x)
716     {
717     }
718     keyboardNo1 = null;
719     return 1;
720   }
721
722   int getKeyboardNo2()
723   {
724     try
725     {
726       if (keyboardNo2 != null)
727       {
728         int value = Integer.parseInt(keyboardNo2.toString());
729         keyboardNo2 = null;
730         return value;
731       }
732     } catch (Exception x)
733     {
734     }
735     keyboardNo2 = null;
736     return 1;
737   }
738
739   /**
740    * DOCUMENT ME!
741    * 
742    * @param evt
743    *          DOCUMENT ME!
744    */
745   @Override
746   public void mouseReleased(MouseEvent evt)
747   {
748     MousePos pos = findMousePosition(evt);
749     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
750     {
751       return;
752     }
753
754     boolean didDrag = mouseDragging; // did we come here after a drag
755     mouseDragging = false;
756     mouseWheelPressed = false;
757
758     if (evt.isPopupTrigger()) // Windows: mouseReleased
759     {
760       showPopupMenu(evt, pos);
761       evt.consume();
762       return;
763     }
764
765     if (editingSeqs)
766     {
767       endEditing();
768     }
769     else
770     {
771       doMouseReleasedDefineMode(evt, didDrag);
772     }
773   }
774
775   /**
776    * DOCUMENT ME!
777    * 
778    * @param evt
779    *          DOCUMENT ME!
780    */
781   @Override
782   public void mousePressed(MouseEvent evt)
783   {
784     lastMousePress = evt.getPoint();
785     MousePos pos = findMousePosition(evt);
786     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
787     {
788       return;
789     }
790
791     if (SwingUtilities.isMiddleMouseButton(evt))
792     {
793       mouseWheelPressed = true;
794       return;
795     }
796
797     boolean isControlDown = Platform.isControlDown(evt);
798     if (evt.isShiftDown() || isControlDown)
799     {
800       editingSeqs = true;
801       if (isControlDown)
802       {
803         groupEditing = true;
804       }
805     }
806     else
807     {
808       doMousePressedDefineMode(evt, pos);
809       return;
810     }
811
812     int seq = pos.seqIndex;
813     int res = pos.column;
814
815     if ((seq < av.getAlignment().getHeight())
816             && (res < av.getAlignment().getSequenceAt(seq).getLength()))
817     {
818       editStartSeq = seq;
819       editLastRes = res;
820     }
821     else
822     {
823       editStartSeq = -1;
824       editLastRes = -1;
825     }
826
827     return;
828   }
829
830   String lastMessage;
831
832   @Override
833   public void mouseOverSequence(SequenceI sequence, int index, int pos)
834   {
835     String tmp = sequence.hashCode() + " " + index + " " + pos;
836
837     if (lastMessage == null || !lastMessage.equals(tmp))
838     {
839       // System.err.println("mouseOver Sequence: "+tmp);
840       ssm.mouseOverSequence(sequence, index, pos, av);
841     }
842     lastMessage = tmp;
843   }
844
845   /**
846    * Highlight the mapped region described by the search results object (unless
847    * unchanged). This supports highlight of protein while mousing over linked
848    * cDNA and vice versa. The status bar is also updated to show the location of
849    * the start of the highlighted region.
850    */
851   @Override
852   public String highlightSequence(SearchResultsI results)
853   {
854     if (results == null || results.equals(lastSearchResults))
855     {
856       return null;
857     }
858     lastSearchResults = results;
859
860     boolean wasScrolled = false;
861
862     if (av.isFollowHighlight())
863     {
864       // don't allow highlight of protein/cDNA to also scroll a complementary
865       // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
866       // over residue to change abruptly, causing highlighted residue in panel 2
867       // to change, causing a scroll in panel 1 etc)
868       ap.setToScrollComplementPanel(false);
869       wasScrolled = ap.scrollToPosition(results);
870       if (wasScrolled)
871       {
872         seqCanvas.revalidate();
873       }
874       ap.setToScrollComplementPanel(true);
875     }
876
877     boolean fastPaint = !(wasScrolled && av.getWrapAlignment());
878     if (seqCanvas.highlightSearchResults(results, fastPaint))
879     {
880       setStatusMessage(results);
881     }
882     return results.isEmpty() ? null : getHighlightInfo(results);
883   }
884
885   /**
886    * temporary hack: answers a message suitable to show on structure hover
887    * label. This is normally null. It is a peptide variation description if
888    * <ul>
889    * <li>results are a single residue in a protein alignment</li>
890    * <li>there is a mapping to a coding sequence (codon)</li>
891    * <li>there are one or more SNP variant features on the codon</li>
892    * </ul>
893    * in which case the answer is of the format (e.g.) "p.Glu388Asp"
894    * 
895    * @param results
896    * @return
897    */
898   private String getHighlightInfo(SearchResultsI results)
899   {
900     /*
901      * ideally, just find mapped CDS (as we don't care about render style here);
902      * for now, go via split frame complement's FeatureRenderer
903      */
904     AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
905     if (complement == null)
906     {
907       return null;
908     }
909     AlignFrame af = Desktop.getAlignFrameFor(complement);
910     FeatureRendererModel fr2 = af.getFeatureRenderer();
911
912     List<SearchResultMatchI> matches = results.getResults();
913     int j = matches.size();
914     List<String> infos = new ArrayList<>();
915     for (int i = 0; i < j; i++)
916     {
917       SearchResultMatchI match = matches.get(i);
918       int pos = match.getStart();
919       if (pos == match.getEnd())
920       {
921         SequenceI seq = match.getSequence();
922         SequenceI ds = seq.getDatasetSequence() == null ? seq
923                 : seq.getDatasetSequence();
924         MappedFeatures mf = fr2
925                 .findComplementFeaturesAtResidue(ds, pos);
926         if (mf != null)
927         {
928           for (SequenceFeature sf : mf.features)
929           {
930             String pv = mf.findProteinVariants(sf);
931             if (pv.length() > 0 && !infos.contains(pv))
932             {
933               infos.add(pv);
934             }
935           }
936         }
937       }
938     }
939
940     if (infos.isEmpty())
941     {
942       return null;
943     }
944     StringBuilder sb = new StringBuilder();
945     for (String info : infos)
946     {
947       if (sb.length() > 0)
948       {
949         sb.append("|");
950       }
951       sb.append(info);
952     }
953     return sb.toString();
954   }
955
956   @Override
957   public VamsasSource getVamsasSource()
958   {
959     return this.ap == null ? null : this.ap.av;
960   }
961
962   @Override
963   public void updateColours(SequenceI seq, int index)
964   {
965     System.out.println("update the seqPanel colours");
966     // repaint();
967   }
968
969   /**
970    * Action on mouse movement is to update the status bar to show the current
971    * sequence position, and (if features are shown) to show any features at the
972    * position in a tooltip. Does nothing if the mouse move does not change
973    * residue position.
974    * 
975    * @param evt
976    */
977   @Override
978   public void mouseMoved(MouseEvent evt)
979   {
980     if (editingSeqs)
981     {
982       // This is because MacOSX creates a mouseMoved
983       // If control is down, other platforms will not.
984       mouseDragged(evt);
985     }
986
987     final MousePos mousePos = findMousePosition(evt);
988     if (mousePos.equals(lastMousePosition))
989     {
990       /*
991        * just a pixel move without change of 'cell'
992        */
993       moveTooltip = false;
994       return;
995     }
996     moveTooltip = true;
997     lastMousePosition = mousePos;
998
999     if (mousePos.isOverAnnotation())
1000     {
1001       mouseMovedOverAnnotation(mousePos);
1002       return;
1003     }
1004     final int seq = mousePos.seqIndex;
1005
1006     final int column = mousePos.column;
1007     if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1008     {
1009       lastMousePosition = null;
1010       setToolTipText(null);
1011       lastTooltip = null;
1012       lastFormattedTooltip = null;
1013       ap.alignFrame.setStatus("");
1014       return;
1015     }
1016
1017     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1018
1019     if (column >= sequence.getLength())
1020     {
1021       return;
1022     }
1023
1024     /*
1025      * set status bar message, returning residue position in sequence
1026      */
1027     boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1028     final int pos = setStatusMessage(sequence, column, seq);
1029     if (ssm != null && !isGapped)
1030     {
1031       mouseOverSequence(sequence, column, pos);
1032     }
1033
1034     StringBuilder tooltipText = new StringBuilder(64);
1035
1036     SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1037     if (groups != null)
1038     {
1039       for (int g = 0; g < groups.length; g++)
1040       {
1041         if (groups[g].getStartRes() <= column
1042                 && groups[g].getEndRes() >= column)
1043         {
1044           if (!groups[g].getName().startsWith("JTreeGroup")
1045                   && !groups[g].getName().startsWith("JGroup"))
1046           {
1047             tooltipText.append(groups[g].getName());
1048           }
1049
1050           if (groups[g].getDescription() != null)
1051           {
1052             tooltipText.append(": " + groups[g].getDescription());
1053           }
1054         }
1055       }
1056     }
1057
1058     /*
1059      * add any features at the position to the tooltip; if over a gap, only
1060      * add features that straddle the gap (pos may be the residue before or
1061      * after the gap)
1062      */
1063     int unshownFeatures = 0;
1064     if (av.isShowSequenceFeatures())
1065     {
1066       List<SequenceFeature> features = ap.getFeatureRenderer()
1067               .findFeaturesAtColumn(sequence, column + 1);
1068       unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1069               features, this.ap.getSeqPanel().seqCanvas.fr,
1070               MAX_TOOLTIP_LENGTH);
1071
1072       /*
1073        * add features in CDS/protein complement at the corresponding
1074        * position if configured to do so
1075        */
1076       if (av.isShowComplementFeatures())
1077       {
1078         if (!Comparison.isGap(sequence.getCharAt(column)))
1079         {
1080           AlignViewportI complement = ap.getAlignViewport()
1081                   .getCodingComplement();
1082           AlignFrame af = Desktop.getAlignFrameFor(complement);
1083           FeatureRendererModel fr2 = af.getFeatureRenderer();
1084           MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1085                   pos);
1086           if (mf != null)
1087           {
1088             unshownFeatures += seqARep.appendFeatures(tooltipText,
1089                     pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1090           }
1091         }
1092       }
1093     }
1094     if (tooltipText.length() == 0) // nothing added
1095     {
1096       setToolTipText(null);
1097       lastTooltip = null;
1098     }
1099     else
1100     {
1101       if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1102       {
1103         tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1104         tooltipText.append("...");
1105       }
1106       if (unshownFeatures > 0)
1107       {
1108         tooltipText.append("<br/>").append("... ").append("<i>")
1109                 .append(MessageManager.formatMessage(
1110                         "label.features_not_shown", unshownFeatures))
1111                 .append("</i>");
1112       }
1113       String textString = tooltipText.toString();
1114       if (!textString.equals(lastTooltip))
1115       {
1116         lastTooltip = textString;
1117         lastFormattedTooltip = JvSwingUtils.wrapTooltip(true,
1118                 textString);
1119         setToolTipText(lastFormattedTooltip);
1120       }
1121     }
1122   }
1123
1124   /**
1125    * When the view is in wrapped mode, and the mouse is over an annotation row,
1126    * shows the corresponding tooltip and status message (if any)
1127    * 
1128    * @param pos
1129    * @param column
1130    */
1131   protected void mouseMovedOverAnnotation(MousePos pos)
1132   {
1133     final int column = pos.column;
1134     final int rowIndex = pos.annotationIndex;
1135
1136     if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1137             || rowIndex < 0)
1138     {
1139       return;
1140     }
1141     AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1142
1143     String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1144             anns);
1145     if (!tooltip.equals(lastTooltip))
1146     {
1147       lastTooltip = tooltip;
1148       lastFormattedTooltip = tooltip == null ? null
1149               : JvSwingUtils.wrapTooltip(true, tooltip);
1150       setToolTipText(lastFormattedTooltip);
1151     }
1152
1153     String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1154             anns[rowIndex]);
1155     ap.alignFrame.setStatus(msg);
1156   }
1157
1158   /*
1159    * if Shift key is held down while moving the mouse, 
1160    * the tooltip location is not changed once shown
1161    */
1162   private Point lastTooltipLocation = null;
1163
1164   /*
1165    * this flag is false for pixel moves within a residue,
1166    * to reduce tooltip flicker
1167    */
1168   private boolean moveTooltip = true;
1169
1170   /*
1171    * a dummy tooltip used to estimate where to position tooltips
1172    */
1173   private JToolTip tempTip = new JLabel().createToolTip();
1174
1175   /*
1176    * (non-Javadoc)
1177    * 
1178    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1179    */
1180   @Override
1181   public Point getToolTipLocation(MouseEvent event)
1182   {
1183     // BH 2018
1184
1185     if (lastTooltip == null || !moveTooltip)
1186     {
1187       return null;
1188     }
1189
1190     if (lastTooltipLocation != null && event.isShiftDown())
1191     {
1192       return lastTooltipLocation;
1193     }
1194
1195     int x = event.getX();
1196     int y = event.getY();
1197     int w = getWidth();
1198
1199     tempTip.setTipText(lastFormattedTooltip);
1200     int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1201     
1202     // was      x += (w - x < 200) ? -(w / 2) : 5;
1203     x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1204     Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1205
1206     return lastTooltipLocation = p;
1207   }
1208
1209   /**
1210    * set when the current UI interaction has resulted in a change that requires
1211    * shading in overviews and structures to be recalculated. this could be
1212    * changed to a something more expressive that indicates what actually has
1213    * changed, so selective redraws can be applied (ie. only structures, only
1214    * overview, etc)
1215    */
1216   private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1217
1218   /**
1219    * set if av.getSelectionGroup() refers to a group that is defined on the
1220    * alignment view, rather than a transient selection
1221    */
1222   // private boolean editingDefinedGroup = false; // TODO: refactor to
1223   // avcontroller or viewModel
1224
1225   /**
1226    * Sets the status message in alignment panel, showing the sequence number
1227    * (index) and id, and residue and residue position if not at a gap, for the
1228    * given sequence and column position. Returns the residue position returned
1229    * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1230    * if at a gapped position.
1231    * 
1232    * @param sequence
1233    *          aligned sequence object
1234    * @param column
1235    *          alignment column
1236    * @param seqIndex
1237    *          index of sequence in alignment
1238    * @return sequence position of residue at column, or adjacent residue if at a
1239    *         gap
1240    */
1241   int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1242   {
1243     char sequenceChar = sequence.getCharAt(column);
1244     int pos = sequence.findPosition(column);
1245     setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1246
1247     return pos;
1248   }
1249
1250   /**
1251    * Builds the status message for the current cursor location and writes it to
1252    * the status bar, for example
1253    * 
1254    * <pre>
1255    * Sequence 3 ID: FER1_SOLLC
1256    * Sequence 5 ID: FER1_PEA Residue: THR (4)
1257    * Sequence 5 ID: FER1_PEA Residue: B (3)
1258    * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1259    * </pre>
1260    * 
1261    * @param seqName
1262    * @param seqIndex
1263    *          sequence position in the alignment (1..)
1264    * @param sequenceChar
1265    *          the character under the cursor
1266    * @param residuePos
1267    *          the sequence residue position (if not over a gap)
1268    */
1269   protected void setStatusMessage(String seqName, int seqIndex,
1270           char sequenceChar, int residuePos)
1271   {
1272     StringBuilder text = new StringBuilder(32);
1273
1274     /*
1275      * Sequence number (if known), and sequence name.
1276      */
1277     String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1278     text.append("Sequence").append(seqno).append(" ID: ")
1279             .append(seqName);
1280
1281     String residue = null;
1282
1283     /*
1284      * Try to translate the display character to residue name (null for gap).
1285      */
1286     boolean isGapped = Comparison.isGap(sequenceChar);
1287
1288     if (!isGapped)
1289     {
1290       boolean nucleotide = av.getAlignment().isNucleotide();
1291       String displayChar = String.valueOf(sequenceChar);
1292       if (nucleotide)
1293       {
1294         residue = ResidueProperties.nucleotideName.get(displayChar);
1295       }
1296       else
1297       {
1298         residue = "X".equalsIgnoreCase(displayChar) ? "X"
1299                 : ("*".equals(displayChar) ? "STOP"
1300                         : ResidueProperties.aa2Triplet.get(displayChar));
1301       }
1302       text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1303               .append(": ").append(residue == null ? displayChar : residue);
1304
1305       text.append(" (").append(Integer.toString(residuePos)).append(")");
1306     }
1307     ap.alignFrame.setStatus(text.toString());
1308   }
1309
1310   /**
1311    * Set the status bar message to highlight the first matched position in
1312    * search results.
1313    * 
1314    * @param results
1315    */
1316   private void setStatusMessage(SearchResultsI results)
1317   {
1318     AlignmentI al = this.av.getAlignment();
1319     int sequenceIndex = al.findIndex(results);
1320     if (sequenceIndex == -1)
1321     {
1322       return;
1323     }
1324     SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1325     SequenceI ds = alignedSeq.getDatasetSequence();
1326     for (SearchResultMatchI m : results.getResults())
1327     {
1328       SequenceI seq = m.getSequence();
1329       if (seq.getDatasetSequence() != null)
1330       {
1331         seq = seq.getDatasetSequence();
1332       }
1333
1334       if (seq == ds)
1335       {
1336         int start = m.getStart();
1337         setStatusMessage(alignedSeq.getName(), sequenceIndex,
1338                 seq.getCharAt(start - 1), start);
1339         return;
1340       }
1341     }
1342   }
1343
1344   /**
1345    * {@inheritDoc}
1346    */
1347   @Override
1348   public void mouseDragged(MouseEvent evt)
1349   {
1350     MousePos pos = findMousePosition(evt);
1351     if (pos.isOverAnnotation() || pos.column == -1)
1352     {
1353       return;
1354     }
1355
1356     if (mouseWheelPressed)
1357     {
1358       boolean inSplitFrame = ap.av.getCodingComplement() != null;
1359       boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1360
1361       int oldWidth = av.getCharWidth();
1362
1363       // Which is bigger, left-right or up-down?
1364       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1365               .abs(evt.getX() - lastMousePress.getX()))
1366       {
1367         /*
1368          * on drag up or down, decrement or increment font size
1369          */
1370         int fontSize = av.font.getSize();
1371         boolean fontChanged = false;
1372
1373         if (evt.getY() < lastMousePress.getY())
1374         {
1375           fontChanged = true;
1376           fontSize--;
1377         }
1378         else if (evt.getY() > lastMousePress.getY())
1379         {
1380           fontChanged = true;
1381           fontSize++;
1382         }
1383
1384         if (fontSize < 1)
1385         {
1386           fontSize = 1;
1387         }
1388
1389         if (fontChanged)
1390         {
1391           Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1392                   fontSize);
1393           av.setFont(newFont, true);
1394           av.setCharWidth(oldWidth);
1395           ap.fontChanged();
1396           if (copyChanges)
1397           {
1398             ap.av.getCodingComplement().setFont(newFont, true);
1399             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1400                     .getSplitViewContainer();
1401             splitFrame.adjustLayout();
1402             splitFrame.repaint();
1403           }
1404         }
1405       }
1406       else
1407       {
1408         /*
1409          * on drag left or right, decrement or increment character width
1410          */
1411         int newWidth = 0;
1412         if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1413         {
1414           newWidth = av.getCharWidth() - 1;
1415           av.setCharWidth(newWidth);
1416         }
1417         else if (evt.getX() > lastMousePress.getX())
1418         {
1419           newWidth = av.getCharWidth() + 1;
1420           av.setCharWidth(newWidth);
1421         }
1422         if (newWidth > 0)
1423         {
1424           ap.paintAlignment(false, false);
1425           if (copyChanges)
1426           {
1427             /*
1428              * need to ensure newWidth is set on cdna, regardless of which
1429              * panel the mouse drag happened in; protein will compute its 
1430              * character width as 1:1 or 3:1
1431              */
1432             av.getCodingComplement().setCharWidth(newWidth);
1433             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1434                     .getSplitViewContainer();
1435             splitFrame.adjustLayout();
1436             splitFrame.repaint();
1437           }
1438         }
1439       }
1440
1441       FontMetrics fm = getFontMetrics(av.getFont());
1442       av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1443
1444       lastMousePress = evt.getPoint();
1445
1446       return;
1447     }
1448
1449     if (!editingSeqs)
1450     {
1451       dragStretchGroup(evt);
1452       return;
1453     }
1454
1455     int res = pos.column;
1456
1457     if (res < 0)
1458     {
1459       res = 0;
1460     }
1461
1462     if ((editLastRes == -1) || (editLastRes == res))
1463     {
1464       return;
1465     }
1466
1467     if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1468     {
1469       // dragLeft, delete gap
1470       editSequence(false, false, res);
1471     }
1472     else
1473     {
1474       editSequence(true, false, res);
1475     }
1476
1477     mouseDragging = true;
1478     if (scrollThread != null)
1479     {
1480       scrollThread.setMousePosition(evt.getPoint());
1481     }
1482   }
1483
1484   /**
1485    * Edits the sequence to insert or delete one or more gaps, in response to a
1486    * mouse drag or cursor mode command. The number of inserts/deletes may be
1487    * specified with the cursor command, or else depends on the mouse event
1488    * (normally one column, but potentially more for a fast mouse drag).
1489    * <p>
1490    * Delete gaps is limited to the number of gaps left of the cursor position
1491    * (mouse drag), or at or right of the cursor position (cursor mode).
1492    * <p>
1493    * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1494    * the current selection group.
1495    * <p>
1496    * In locked editing mode (with a selection group present), inserts/deletions
1497    * within the selection group are limited to its boundaries (and edits outside
1498    * the group stop at its border).
1499    * 
1500    * @param insertGap
1501    *          true to insert gaps, false to delete gaps
1502    * @param editSeq
1503    *          (unused parameter)
1504    * @param startres
1505    *          the column at which to perform the action; the number of columns
1506    *          affected depends on <code>this.editLastRes</code> (cursor column
1507    *          position)
1508    */
1509   synchronized void editSequence(boolean insertGap, boolean editSeq,
1510           final int startres)
1511   {
1512     int fixedLeft = -1;
1513     int fixedRight = -1;
1514     boolean fixedColumns = false;
1515     SequenceGroup sg = av.getSelectionGroup();
1516
1517     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1518
1519     // No group, but the sequence may represent a group
1520     if (!groupEditing && av.hasHiddenRows())
1521     {
1522       if (av.isHiddenRepSequence(seq))
1523       {
1524         sg = av.getRepresentedSequences(seq);
1525         groupEditing = true;
1526       }
1527     }
1528
1529     StringBuilder message = new StringBuilder(64); // for status bar
1530
1531     /*
1532      * make a name for the edit action, for
1533      * status bar message and Undo/Redo menu
1534      */
1535     String label = null;
1536     if (groupEditing)
1537     {
1538         message.append("Edit group:");
1539       label = MessageManager.getString("action.edit_group");
1540     }
1541     else
1542     {
1543         message.append("Edit sequence: " + seq.getName());
1544       label = seq.getName();
1545       if (label.length() > 10)
1546       {
1547         label = label.substring(0, 10);
1548       }
1549       label = MessageManager.formatMessage("label.edit_params",
1550               new String[]
1551               { label });
1552     }
1553
1554     /*
1555      * initialise the edit command if there is not
1556      * already one being extended
1557      */
1558     if (editCommand == null)
1559     {
1560       editCommand = new EditCommand(label);
1561     }
1562
1563     if (insertGap)
1564     {
1565       message.append(" insert ");
1566     }
1567     else
1568     {
1569       message.append(" delete ");
1570     }
1571
1572     message.append(Math.abs(startres - editLastRes) + " gaps.");
1573     ap.alignFrame.setStatus(message.toString());
1574
1575     /*
1576      * is there a selection group containing the sequence being edited?
1577      * if so the boundary of the group is the limit of the edit
1578      * (but the edit may be inside or outside the selection group)
1579      */
1580     boolean inSelectionGroup = sg != null
1581             && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1582     if (groupEditing || inSelectionGroup)
1583     {
1584       fixedColumns = true;
1585
1586       // sg might be null as the user may only see 1 sequence,
1587       // but the sequence represents a group
1588       if (sg == null)
1589       {
1590         if (!av.isHiddenRepSequence(seq))
1591         {
1592           endEditing();
1593           return;
1594         }
1595         sg = av.getRepresentedSequences(seq);
1596       }
1597
1598       fixedLeft = sg.getStartRes();
1599       fixedRight = sg.getEndRes();
1600
1601       if ((startres < fixedLeft && editLastRes >= fixedLeft)
1602               || (startres >= fixedLeft && editLastRes < fixedLeft)
1603               || (startres > fixedRight && editLastRes <= fixedRight)
1604               || (startres <= fixedRight && editLastRes > fixedRight))
1605       {
1606         endEditing();
1607         return;
1608       }
1609
1610       if (fixedLeft > startres)
1611       {
1612         fixedRight = fixedLeft - 1;
1613         fixedLeft = 0;
1614       }
1615       else if (fixedRight < startres)
1616       {
1617         fixedLeft = fixedRight;
1618         fixedRight = -1;
1619       }
1620     }
1621
1622     if (av.hasHiddenColumns())
1623     {
1624       fixedColumns = true;
1625       int y1 = av.getAlignment().getHiddenColumns()
1626               .getNextHiddenBoundary(true, startres);
1627       int y2 = av.getAlignment().getHiddenColumns()
1628               .getNextHiddenBoundary(false, startres);
1629
1630       if ((insertGap && startres > y1 && editLastRes < y1)
1631               || (!insertGap && startres < y2 && editLastRes > y2))
1632       {
1633         endEditing();
1634         return;
1635       }
1636
1637       // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1638       // Selection spans a hidden region
1639       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1640       {
1641         if (startres >= y2)
1642         {
1643           fixedLeft = y2;
1644         }
1645         else
1646         {
1647           fixedRight = y2 - 1;
1648         }
1649       }
1650     }
1651
1652     boolean success = doEditSequence(insertGap, editSeq, startres,
1653             fixedRight, fixedColumns, sg);
1654
1655     /*
1656      * report what actually happened (might be less than
1657      * what was requested), by inspecting the edit commands added
1658      */
1659     String msg = getEditStatusMessage(editCommand);
1660     ap.alignFrame.setStatus(msg == null ? " " : msg);
1661     if (!success)
1662     {
1663       endEditing();
1664     }
1665
1666     editLastRes = startres;
1667     seqCanvas.repaint();
1668   }
1669
1670   /**
1671    * A helper method that performs the requested editing to insert or delete
1672    * gaps (if possible). Answers true if the edit was successful, false if could
1673    * only be performed in part or not at all. Failure may occur in 'locked edit'
1674    * mode, when an insertion requires a matching gapped position (or column) to
1675    * delete, and deletion requires an adjacent gapped position (or column) to
1676    * remove.
1677    * 
1678    * @param insertGap
1679    *          true if inserting gap(s), false if deleting
1680    * @param editSeq
1681    *          (unused parameter, currently always false)
1682    * @param startres
1683    *          the column at which to perform the edit
1684    * @param fixedRight
1685    *          fixed right boundary column of a locked edit (within or to the
1686    *          left of a selection group)
1687    * @param fixedColumns
1688    *          true if this is a locked edit
1689    * @param sg
1690    *          the sequence group (if group edit is being performed)
1691    * @return
1692    */
1693   protected boolean doEditSequence(final boolean insertGap,
1694           final boolean editSeq, final int startres, int fixedRight,
1695           final boolean fixedColumns, final SequenceGroup sg)
1696   {
1697     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1698     SequenceI[] seqs = new SequenceI[] { seq };
1699
1700     if (groupEditing)
1701     {
1702       List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1703       int g, groupSize = vseqs.size();
1704       SequenceI[] groupSeqs = new SequenceI[groupSize];
1705       for (g = 0; g < groupSeqs.length; g++)
1706       {
1707         groupSeqs[g] = vseqs.get(g);
1708       }
1709
1710       // drag to right
1711       if (insertGap)
1712       {
1713         // If the user has selected the whole sequence, and is dragging to
1714         // the right, we can still extend the alignment and selectionGroup
1715         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1716                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1717         {
1718           sg.setEndRes(
1719                   av.getAlignment().getWidth() + startres - editLastRes);
1720           fixedRight = sg.getEndRes();
1721         }
1722
1723         // Is it valid with fixed columns??
1724         // Find the next gap before the end
1725         // of the visible region boundary
1726         boolean blank = false;
1727         for (; fixedRight > editLastRes; fixedRight--)
1728         {
1729           blank = true;
1730
1731           for (g = 0; g < groupSize; g++)
1732           {
1733             for (int j = 0; j < startres - editLastRes; j++)
1734             {
1735               if (!Comparison
1736                       .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1737               {
1738                 blank = false;
1739                 break;
1740               }
1741             }
1742           }
1743           if (blank)
1744           {
1745             break;
1746           }
1747         }
1748
1749         if (!blank)
1750         {
1751           if (sg.getSize() == av.getAlignment().getHeight())
1752           {
1753             if ((av.hasHiddenColumns()
1754                     && startres < av.getAlignment().getHiddenColumns()
1755                             .getNextHiddenBoundary(false, startres)))
1756             {
1757               return false;
1758             }
1759
1760             int alWidth = av.getAlignment().getWidth();
1761             if (av.hasHiddenRows())
1762             {
1763               int hwidth = av.getAlignment().getHiddenSequences()
1764                       .getWidth();
1765               if (hwidth > alWidth)
1766               {
1767                 alWidth = hwidth;
1768               }
1769             }
1770             // We can still insert gaps if the selectionGroup
1771             // contains all the sequences
1772             sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1773             fixedRight = alWidth + startres - editLastRes;
1774           }
1775           else
1776           {
1777             return false;
1778           }
1779         }
1780       }
1781
1782       // drag to left
1783       else if (!insertGap)
1784       {
1785         // / Are we able to delete?
1786         // ie are all columns blank?
1787
1788         for (g = 0; g < groupSize; g++)
1789         {
1790           for (int j = startres; j < editLastRes; j++)
1791           {
1792             if (groupSeqs[g].getLength() <= j)
1793             {
1794               continue;
1795             }
1796
1797             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1798             {
1799               // Not a gap, block edit not valid
1800               return false;
1801             }
1802           }
1803         }
1804       }
1805
1806       if (insertGap)
1807       {
1808         // dragging to the right
1809         if (fixedColumns && fixedRight != -1)
1810         {
1811           for (int j = editLastRes; j < startres; j++)
1812           {
1813             insertGap(j, groupSeqs, fixedRight);
1814           }
1815         }
1816         else
1817         {
1818           appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1819                   startres - editLastRes, false);
1820         }
1821       }
1822       else
1823       {
1824         // dragging to the left
1825         if (fixedColumns && fixedRight != -1)
1826         {
1827           for (int j = editLastRes; j > startres; j--)
1828           {
1829             deleteChar(startres, groupSeqs, fixedRight);
1830           }
1831         }
1832         else
1833         {
1834           appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1835                   editLastRes - startres, false);
1836         }
1837       }
1838     }
1839     else
1840     {
1841       /*
1842        * editing a single sequence
1843        */
1844       if (insertGap)
1845       {
1846         // dragging to the right
1847         if (fixedColumns && fixedRight != -1)
1848         {
1849           for (int j = editLastRes; j < startres; j++)
1850           {
1851             if (!insertGap(j, seqs, fixedRight))
1852             {
1853               /*
1854                * e.g. cursor mode command specified 
1855                * more inserts than are possible
1856                */
1857               return false;
1858             }
1859           }
1860         }
1861         else
1862         {
1863           appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1864                   startres - editLastRes, false);
1865         }
1866       }
1867       else
1868       {
1869         if (!editSeq)
1870         {
1871           // dragging to the left
1872           if (fixedColumns && fixedRight != -1)
1873           {
1874             for (int j = editLastRes; j > startres; j--)
1875             {
1876               if (!Comparison.isGap(seq.getCharAt(startres)))
1877               {
1878                 return false;
1879               }
1880               deleteChar(startres, seqs, fixedRight);
1881             }
1882           }
1883           else
1884           {
1885             // could be a keyboard edit trying to delete none gaps
1886             int max = 0;
1887             for (int m = startres; m < editLastRes; m++)
1888             {
1889               if (!Comparison.isGap(seq.getCharAt(m)))
1890               {
1891                 break;
1892               }
1893               max++;
1894             }
1895             if (max > 0)
1896             {
1897               appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1898             }
1899           }
1900         }
1901         else
1902         {// insertGap==false AND editSeq==TRUE;
1903           if (fixedColumns && fixedRight != -1)
1904           {
1905             for (int j = editLastRes; j < startres; j++)
1906             {
1907               insertGap(j, seqs, fixedRight);
1908             }
1909           }
1910           else
1911           {
1912             appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1913                     startres - editLastRes, false);
1914           }
1915         }
1916       }
1917     }
1918
1919     return true;
1920   }
1921
1922   /**
1923    * Constructs an informative status bar message while dragging to insert or
1924    * delete gaps. Answers null if inserts and deletes cancel out.
1925    * 
1926    * @param editCommand
1927    *          a command containing the list of individual edits
1928    * @return
1929    */
1930   protected static String getEditStatusMessage(EditCommand editCommand)
1931   {
1932     if (editCommand == null)
1933     {
1934       return null;
1935     }
1936
1937     /*
1938      * add any inserts, and subtract any deletes,  
1939      * not counting those auto-inserted when doing a 'locked edit'
1940      * (so only counting edits 'under the cursor')
1941      */
1942     int count = 0;
1943     for (Edit cmd : editCommand.getEdits())
1944     {
1945       if (!cmd.isSystemGenerated())
1946       {
1947         count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1948                 : -cmd.getNumber();
1949       }
1950     }
1951
1952     if (count == 0)
1953     {
1954       /*
1955        * inserts and deletes cancel out
1956        */
1957       return null;
1958     }
1959
1960     String msgKey = count > 1 ? "label.insert_gaps"
1961             : (count == 1 ? "label.insert_gap"
1962                     : (count == -1 ? "label.delete_gap"
1963                             : "label.delete_gaps"));
1964     count = Math.abs(count);
1965
1966     return MessageManager.formatMessage(msgKey, String.valueOf(count));
1967   }
1968
1969   /**
1970    * Inserts one gap at column j, deleting the right-most gapped column up to
1971    * (and including) fixedColumn. Returns true if the edit is successful, false
1972    * if no blank column is available to allow the insertion to be balanced by a
1973    * deletion.
1974    * 
1975    * @param j
1976    * @param seq
1977    * @param fixedColumn
1978    * @return
1979    */
1980   boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1981   {
1982     int blankColumn = fixedColumn;
1983     for (int s = 0; s < seq.length; s++)
1984     {
1985       // Find the next gap before the end of the visible region boundary
1986       // If lastCol > j, theres a boundary after the gap insertion
1987
1988       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1989       {
1990         if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1991         {
1992           // Theres a space, so break and insert the gap
1993           break;
1994         }
1995       }
1996
1997       if (blankColumn <= j)
1998       {
1999         blankColumn = fixedColumn;
2000         endEditing();
2001         return false;
2002       }
2003     }
2004
2005     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2006
2007     appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2008
2009     return true;
2010   }
2011
2012   /**
2013    * Helper method to add and perform one edit action
2014    * 
2015    * @param action
2016    * @param seq
2017    * @param pos
2018    * @param count
2019    * @param systemGenerated
2020    *          true if the edit is a 'balancing' delete (or insert) to match a
2021    *          user's insert (or delete) in a locked editing region
2022    */
2023   protected void appendEdit(Action action, SequenceI[] seq, int pos,
2024           int count, boolean systemGenerated)
2025   {
2026
2027     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2028             av.getAlignment().getGapCharacter());
2029     edit.setSystemGenerated(systemGenerated);
2030
2031     editCommand.appendEdit(edit, av.getAlignment(), true, null);
2032   }
2033
2034   /**
2035    * Deletes the character at column j, and inserts a gap at fixedColumn, in
2036    * each of the given sequences. The caller should ensure that all sequences
2037    * are gapped in column j.
2038    * 
2039    * @param j
2040    * @param seqs
2041    * @param fixedColumn
2042    */
2043   void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2044   {
2045     appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2046
2047     appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2048   }
2049
2050   /**
2051    * On reentering the panel, stops any scrolling that was started on dragging
2052    * out of the panel
2053    * 
2054    * @param e
2055    */
2056   @Override
2057   public void mouseEntered(MouseEvent e)
2058   {
2059     if (oldSeq < 0)
2060     {
2061       oldSeq = 0;
2062     }
2063     stopScrolling();
2064   }
2065
2066   /**
2067    * On leaving the panel, if the mouse is being dragged, starts a thread to
2068    * scroll it until the mouse is released (in unwrapped mode only)
2069    * 
2070    * @param e
2071    */
2072   @Override
2073   public void mouseExited(MouseEvent e)
2074   {
2075     lastMousePosition = null;
2076     ap.alignFrame.setStatus(" ");
2077     if (av.getWrapAlignment())
2078     {
2079       return;
2080     }
2081
2082     if (mouseDragging && scrollThread == null)
2083     {
2084       startScrolling(e.getPoint());
2085     }
2086   }
2087
2088   /**
2089    * Handler for double-click on a position with one or more sequence features.
2090    * Opens the Amend Features dialog to allow feature details to be amended, or
2091    * the feature deleted.
2092    */
2093   @Override
2094   public void mouseClicked(MouseEvent evt)
2095   {
2096     SequenceGroup sg = null;
2097     MousePos pos = findMousePosition(evt);
2098     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2099     {
2100       return;
2101     }
2102
2103     if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2104     {
2105       sg = av.getSelectionGroup();
2106       if (sg != null && sg.getSize() == 1
2107               && sg.getEndRes() - sg.getStartRes() < 2)
2108       {
2109         av.setSelectionGroup(null);
2110       }
2111
2112       int column = pos.column;
2113
2114       /*
2115        * find features at the position (if not gapped), or straddling
2116        * the position (if at a gap)
2117        */
2118       SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2119       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2120               .findFeaturesAtColumn(sequence, column + 1);
2121
2122       if (!features.isEmpty())
2123       {
2124         /*
2125          * highlight the first feature at the position on the alignment
2126          */
2127         SearchResultsI highlight = new SearchResults();
2128         highlight.addResult(sequence, features.get(0).getBegin(), features
2129                 .get(0).getEnd());
2130         seqCanvas.highlightSearchResults(highlight, true);
2131
2132         /*
2133          * open the Amend Features dialog
2134          */
2135         new FeatureEditor(ap, Collections.singletonList(sequence), features,
2136                 false).showDialog();
2137       }
2138     }
2139   }
2140
2141   @Override
2142   public void mouseWheelMoved(MouseWheelEvent e)
2143   {
2144     e.consume();
2145     double wheelRotation = e.getPreciseWheelRotation();
2146     if (wheelRotation > 0)
2147     {
2148       if (e.isShiftDown())
2149       {
2150         av.getRanges().scrollRight(true);
2151
2152       }
2153       else
2154       {
2155         av.getRanges().scrollUp(false);
2156       }
2157     }
2158     else if (wheelRotation < 0)
2159     {
2160       if (e.isShiftDown())
2161       {
2162         av.getRanges().scrollRight(false);
2163       }
2164       else
2165       {
2166         av.getRanges().scrollUp(true);
2167       }
2168     }
2169
2170     /*
2171      * update status bar and tooltip for new position
2172      * (need to synthesize a mouse movement to refresh tooltip)
2173      */
2174     mouseMoved(e);
2175     ToolTipManager.sharedInstance().mouseMoved(e);
2176   }
2177
2178   /**
2179    * DOCUMENT ME!
2180    * 
2181    * @param pos
2182    *          DOCUMENT ME!
2183    */
2184   protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2185   {
2186     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2187     {
2188       return;
2189     }
2190
2191     final int res = pos.column;
2192     final int seq = pos.seqIndex;
2193     oldSeq = seq;
2194     updateOverviewAndStructs = false;
2195
2196     startWrapBlock = wrappedBlock;
2197
2198     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2199
2200     if ((sequence == null) || (res > sequence.getLength()))
2201     {
2202       return;
2203     }
2204
2205     stretchGroup = av.getSelectionGroup();
2206
2207     if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2208     {
2209       stretchGroup = av.getAlignment().findGroup(sequence, res);
2210       if (stretchGroup != null)
2211       {
2212         // only update the current selection if the popup menu has a group to
2213         // focus on
2214         av.setSelectionGroup(stretchGroup);
2215       }
2216     }
2217
2218     /*
2219      * defer right-mouse click handling to mouseReleased on Windows
2220      * (where isPopupTrigger() will answer true)
2221      * NB isRightMouseButton is also true for Cmd-click on Mac
2222      */
2223     if (Platform.isWinRightButton(evt))
2224     {
2225       return;
2226     }
2227
2228     if (evt.isPopupTrigger()) // Mac: mousePressed
2229     {
2230       showPopupMenu(evt, pos);
2231       return;
2232     }
2233
2234     if (av.cursorMode)
2235     {
2236       seqCanvas.cursorX = res;
2237       seqCanvas.cursorY = seq;
2238       seqCanvas.repaint();
2239       return;
2240     }
2241
2242     if (stretchGroup == null)
2243     {
2244       createStretchGroup(res, sequence);
2245     }
2246
2247     if (stretchGroup != null)
2248     {
2249       stretchGroup.addPropertyChangeListener(seqCanvas);
2250     }
2251
2252     seqCanvas.repaint();
2253   }
2254
2255   private void createStretchGroup(int res, SequenceI sequence)
2256   {
2257     // Only if left mouse button do we want to change group sizes
2258     // define a new group here
2259     SequenceGroup sg = new SequenceGroup();
2260     sg.setStartRes(res);
2261     sg.setEndRes(res);
2262     sg.addSequence(sequence, false);
2263     av.setSelectionGroup(sg);
2264     stretchGroup = sg;
2265
2266     if (av.getConservationSelected())
2267     {
2268       SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2269               ap.getViewName());
2270     }
2271
2272     if (av.getAbovePIDThreshold())
2273     {
2274       SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2275               ap.getViewName());
2276     }
2277     // TODO: stretchGroup will always be not null. Is this a merge error ?
2278     // or is there a threading issue here?
2279     if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2280     {
2281       // Edit end res position of selected group
2282       changeEndRes = true;
2283     }
2284     else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2285     {
2286       // Edit end res position of selected group
2287       changeStartRes = true;
2288     }
2289     stretchGroup.getWidth();
2290
2291   }
2292
2293   /**
2294    * Build and show a pop-up menu at the right-click mouse position
2295    *
2296    * @param evt
2297    * @param pos
2298    */
2299   void showPopupMenu(MouseEvent evt, MousePos pos)
2300   {
2301     final int column = pos.column;
2302     final int seq = pos.seqIndex;
2303     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2304     if (sequence != null)
2305     {
2306       PopupMenu pop = new PopupMenu(ap, sequence, column);
2307       pop.show(this, evt.getX(), evt.getY());
2308     }
2309   }
2310
2311   /**
2312    * Update the display after mouse up on a selection or group
2313    * 
2314    * @param evt
2315    *          mouse released event details
2316    * @param afterDrag
2317    *          true if this event is happening after a mouse drag (rather than a
2318    *          mouse down)
2319    */
2320   protected void doMouseReleasedDefineMode(MouseEvent evt,
2321           boolean afterDrag)
2322   {
2323     if (stretchGroup == null)
2324     {
2325       return;
2326     }
2327
2328     stretchGroup.removePropertyChangeListener(seqCanvas);
2329
2330     // always do this - annotation has own state
2331     // but defer colourscheme update until hidden sequences are passed in
2332     boolean vischange = stretchGroup.recalcConservation(true);
2333     updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2334             && afterDrag;
2335     if (stretchGroup.cs != null)
2336     {
2337       if (afterDrag)
2338       {
2339         stretchGroup.cs.alignmentChanged(stretchGroup,
2340                 av.getHiddenRepSequences());
2341       }
2342
2343       ResidueShaderI groupColourScheme = stretchGroup
2344               .getGroupColourScheme();
2345       String name = stretchGroup.getName();
2346       if (stretchGroup.cs.conservationApplied())
2347       {
2348         SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2349       }
2350       if (stretchGroup.cs.getThreshold() > 0)
2351       {
2352         SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2353       }
2354     }
2355     PaintRefresher.Refresh(this, av.getSequenceSetId());
2356     // TODO: structure colours only need updating if stretchGroup used to or now
2357     // does contain sequences with structure views
2358     ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2359     updateOverviewAndStructs = false;
2360     changeEndRes = false;
2361     changeStartRes = false;
2362     stretchGroup = null;
2363     av.sendSelection();
2364   }
2365
2366   /**
2367    * Resizes the borders of a selection group depending on the direction of
2368    * mouse drag
2369    * 
2370    * @param evt
2371    */
2372   protected void dragStretchGroup(MouseEvent evt)
2373   {
2374     if (stretchGroup == null)
2375     {
2376       return;
2377     }
2378
2379     MousePos pos = findMousePosition(evt);
2380     if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2381     {
2382       return;
2383     }
2384
2385     int res = pos.column;
2386     int y = pos.seqIndex;
2387
2388     if (wrappedBlock != startWrapBlock)
2389     {
2390       return;
2391     }
2392
2393     res = Math.min(res, av.getAlignment().getWidth()-1);
2394
2395     if (stretchGroup.getEndRes() == res)
2396     {
2397       // Edit end res position of selected group
2398       changeEndRes = true;
2399     }
2400     else if (stretchGroup.getStartRes() == res)
2401     {
2402       // Edit start res position of selected group
2403       changeStartRes = true;
2404     }
2405
2406     if (res < av.getRanges().getStartRes())
2407     {
2408       res = av.getRanges().getStartRes();
2409     }
2410
2411     if (changeEndRes)
2412     {
2413       if (res > (stretchGroup.getStartRes() - 1))
2414       {
2415         stretchGroup.setEndRes(res);
2416         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2417       }
2418     }
2419     else if (changeStartRes)
2420     {
2421       if (res < (stretchGroup.getEndRes() + 1))
2422       {
2423         stretchGroup.setStartRes(res);
2424         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2425       }
2426     }
2427
2428     int dragDirection = 0;
2429
2430     if (y > oldSeq)
2431     {
2432       dragDirection = 1;
2433     }
2434     else if (y < oldSeq)
2435     {
2436       dragDirection = -1;
2437     }
2438
2439     while ((y != oldSeq) && (oldSeq > -1)
2440             && (y < av.getAlignment().getHeight()))
2441     {
2442       // This routine ensures we don't skip any sequences, as the
2443       // selection is quite slow.
2444       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2445
2446       oldSeq += dragDirection;
2447
2448       if (oldSeq < 0)
2449       {
2450         break;
2451       }
2452
2453       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2454
2455       if (stretchGroup.getSequences(null).contains(nextSeq))
2456       {
2457         stretchGroup.deleteSequence(seq, false);
2458         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2459       }
2460       else
2461       {
2462         if (seq != null)
2463         {
2464           stretchGroup.addSequence(seq, false);
2465         }
2466
2467         stretchGroup.addSequence(nextSeq, false);
2468         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2469       }
2470     }
2471
2472     if (oldSeq < 0)
2473     {
2474       oldSeq = -1;
2475     }
2476
2477     mouseDragging = true;
2478
2479     if (scrollThread != null)
2480     {
2481       scrollThread.setMousePosition(evt.getPoint());
2482     }
2483
2484     /*
2485      * construct a status message showing the range of the selection
2486      */
2487     StringBuilder status = new StringBuilder(64);
2488     List<SequenceI> seqs = stretchGroup.getSequences();
2489     String name = seqs.get(0).getName();
2490     if (name.length() > 20)
2491     {
2492       name = name.substring(0, 20);
2493     }
2494     status.append(name).append(" - ");
2495     name = seqs.get(seqs.size() - 1).getName();
2496     if (name.length() > 20)
2497     {
2498       name = name.substring(0, 20);
2499     }
2500     status.append(name).append(" ");
2501     int startRes = stretchGroup.getStartRes();
2502     status.append(" cols ").append(String.valueOf(startRes + 1))
2503             .append("-");
2504     int endRes = stretchGroup.getEndRes();
2505     status.append(String.valueOf(endRes + 1));
2506     status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2507             .append(String.valueOf(endRes - startRes + 1)).append(")");
2508     ap.alignFrame.setStatus(status.toString());
2509   }
2510
2511   /**
2512    * Stops the scroll thread if it is running
2513    */
2514   void stopScrolling()
2515   {
2516     if (scrollThread != null)
2517     {
2518       scrollThread.stopScrolling();
2519       scrollThread = null;
2520     }
2521     mouseDragging = false;
2522   }
2523
2524   /**
2525    * Starts a thread to scroll the alignment, towards a given mouse position
2526    * outside the panel bounds, unless the alignment is in wrapped mode
2527    * 
2528    * @param mousePos
2529    */
2530   void startScrolling(Point mousePos)
2531   {
2532     /*
2533      * set this.mouseDragging in case this was called from 
2534      * a drag in ScalePanel or AnnotationPanel
2535      */
2536     mouseDragging = true;
2537     if (!av.getWrapAlignment() && scrollThread == null)
2538     {
2539       scrollThread = new ScrollThread();
2540       scrollThread.setMousePosition(mousePos);
2541       if (Platform.isJS())
2542       {
2543         /*
2544          * Javascript - run every 20ms until scrolling stopped
2545          * or reaches the limit of scrollable alignment
2546          */
2547         Timer t = new Timer(20, new ActionListener()
2548         {
2549           @Override
2550           public void actionPerformed(ActionEvent e)
2551           {
2552             if (scrollThread != null)
2553             {
2554               // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2555               scrollThread.scrollOnce();
2556             }
2557           }
2558         });
2559         t.addActionListener(new ActionListener()
2560         {
2561           @Override
2562           public void actionPerformed(ActionEvent e)
2563           {
2564             if (scrollThread == null)
2565             {
2566               // SeqPanel.stopScrolling called
2567               t.stop();
2568             }
2569           }
2570         });
2571         t.start();
2572       }
2573       else
2574       {
2575         /*
2576          * Java - run in a new thread
2577          */
2578         scrollThread.start();
2579       }
2580     }
2581   }
2582
2583   /**
2584    * Performs scrolling of the visible alignment left, right, up or down, until
2585    * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2586    * limit of the alignment is reached
2587    */
2588   class ScrollThread extends Thread
2589   {
2590     private Point mousePos;
2591
2592     private volatile boolean keepRunning = true;
2593
2594     /**
2595      * Constructor
2596      */
2597     public ScrollThread()
2598     {
2599       setName("SeqPanel$ScrollThread");
2600     }
2601
2602     /**
2603      * Sets the position of the mouse that determines the direction of the
2604      * scroll to perform. If this is called as the mouse moves, scrolling should
2605      * respond accordingly. For example, if the mouse is dragged right, scroll
2606      * right should start; if the drag continues down, scroll down should also
2607      * happen.
2608      * 
2609      * @param p
2610      */
2611     public void setMousePosition(Point p)
2612     {
2613       mousePos = p;
2614     }
2615
2616     /**
2617      * Sets a flag that will cause the thread to exit
2618      */
2619     public void stopScrolling()
2620     {
2621       keepRunning = false;
2622     }
2623
2624     /**
2625      * Scrolls the alignment left or right, and/or up or down, depending on the
2626      * last notified mouse position, until the limit of the alignment is
2627      * reached, or a flag is set to stop the scroll
2628      */
2629     @Override
2630     public void run()
2631     {
2632       while (keepRunning)
2633       {
2634         if (mousePos != null)
2635         {
2636           keepRunning = scrollOnce();
2637         }
2638         try
2639         {
2640           Thread.sleep(20);
2641         } catch (Exception ex)
2642         {
2643         }
2644       }
2645       SeqPanel.this.scrollThread = null;
2646     }
2647
2648     /**
2649      * Scrolls
2650      * <ul>
2651      * <li>one row up, if the mouse is above the panel</li>
2652      * <li>one row down, if the mouse is below the panel</li>
2653      * <li>one column left, if the mouse is left of the panel</li>
2654      * <li>one column right, if the mouse is right of the panel</li>
2655      * </ul>
2656      * Answers true if a scroll was performed, false if not - meaning either
2657      * that the mouse position is within the panel, or the edge of the alignment
2658      * has been reached.
2659      */
2660     boolean scrollOnce()
2661     {
2662       /*
2663        * quit after mouseUp ensures interrupt in JalviewJS
2664        */
2665       if (!mouseDragging)
2666       {
2667         return false;
2668       }
2669
2670       boolean scrolled = false;
2671       ViewportRanges ranges = SeqPanel.this.av.getRanges();
2672
2673       /*
2674        * scroll up or down
2675        */
2676       if (mousePos.y < 0)
2677       {
2678         // mouse is above this panel - try scroll up
2679         scrolled = ranges.scrollUp(true);
2680       }
2681       else if (mousePos.y >= getHeight())
2682       {
2683         // mouse is below this panel - try scroll down
2684         scrolled = ranges.scrollUp(false);
2685       }
2686
2687       /*
2688        * scroll left or right
2689        */
2690       if (mousePos.x < 0)
2691       {
2692         scrolled |= ranges.scrollRight(false);
2693       }
2694       else if (mousePos.x >= getWidth())
2695       {
2696         scrolled |= ranges.scrollRight(true);
2697       }
2698       return scrolled;
2699     }
2700   }
2701
2702   /**
2703    * modify current selection according to a received message.
2704    */
2705   @Override
2706   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2707           HiddenColumns hidden, SelectionSource source)
2708   {
2709     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2710     // handles selection messages...
2711     // TODO: extend config options to allow user to control if selections may be
2712     // shared between viewports.
2713     boolean iSentTheSelection = (av == source
2714             || (source instanceof AlignViewport
2715                     && ((AlignmentViewport) source).getSequenceSetId()
2716                             .equals(av.getSequenceSetId())));
2717
2718     if (iSentTheSelection)
2719     {
2720       // respond to our own event by updating dependent dialogs
2721       if (ap.getCalculationDialog() != null)
2722       {
2723         ap.getCalculationDialog().validateCalcTypes();
2724       }
2725
2726       return;
2727     }
2728
2729     // process further ?
2730     if (!av.followSelection)
2731     {
2732       return;
2733     }
2734
2735     /*
2736      * Ignore the selection if there is one of our own pending.
2737      */
2738     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2739     {
2740       return;
2741     }
2742
2743     /*
2744      * Check for selection in a view of which this one is a dna/protein
2745      * complement.
2746      */
2747     if (selectionFromTranslation(seqsel, colsel, hidden, source))
2748     {
2749       return;
2750     }
2751
2752     // do we want to thread this ? (contention with seqsel and colsel locks, I
2753     // suspect)
2754     /*
2755      * only copy colsel if there is a real intersection between
2756      * sequence selection and this panel's alignment
2757      */
2758     boolean repaint = false;
2759     boolean copycolsel = false;
2760
2761     SequenceGroup sgroup = null;
2762     if (seqsel != null && seqsel.getSize() > 0)
2763     {
2764       if (av.getAlignment() == null)
2765       {
2766         Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2767                 + " ViewId=" + av.getViewId()
2768                 + " 's alignment is NULL! returning immediately.");
2769         return;
2770       }
2771       sgroup = seqsel.intersect(av.getAlignment(),
2772               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2773       if ((sgroup != null && sgroup.getSize() > 0))
2774       {
2775         copycolsel = true;
2776       }
2777     }
2778     if (sgroup != null && sgroup.getSize() > 0)
2779     {
2780       av.setSelectionGroup(sgroup);
2781     }
2782     else
2783     {
2784       av.setSelectionGroup(null);
2785     }
2786     av.isSelectionGroupChanged(true);
2787     repaint = true;
2788
2789     if (copycolsel)
2790     {
2791       // the current selection is unset or from a previous message
2792       // so import the new colsel.
2793       if (colsel == null || colsel.isEmpty())
2794       {
2795         if (av.getColumnSelection() != null)
2796         {
2797           av.getColumnSelection().clear();
2798           repaint = true;
2799         }
2800       }
2801       else
2802       {
2803         // TODO: shift colSel according to the intersecting sequences
2804         if (av.getColumnSelection() == null)
2805         {
2806           av.setColumnSelection(new ColumnSelection(colsel));
2807         }
2808         else
2809         {
2810           av.getColumnSelection().setElementsFrom(colsel,
2811                   av.getAlignment().getHiddenColumns());
2812         }
2813       }
2814       av.isColSelChanged(true);
2815       repaint = true;
2816     }
2817
2818     if (copycolsel && av.hasHiddenColumns()
2819             && (av.getAlignment().getHiddenColumns() == null))
2820     {
2821       System.err.println("Bad things");
2822     }
2823     if (repaint) // always true!
2824     {
2825       // probably finessing with multiple redraws here
2826       PaintRefresher.Refresh(this, av.getSequenceSetId());
2827       // ap.paintAlignment(false);
2828     }
2829
2830     // lastly, update dependent dialogs
2831     if (ap.getCalculationDialog() != null)
2832     {
2833       ap.getCalculationDialog().validateCalcTypes();
2834     }
2835
2836   }
2837
2838   /**
2839    * If this panel is a cdna/protein translation view of the selection source,
2840    * tries to map the source selection to a local one, and returns true. Else
2841    * returns false.
2842    * 
2843    * @param seqsel
2844    * @param colsel
2845    * @param source
2846    */
2847   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2848           ColumnSelection colsel, HiddenColumns hidden,
2849           SelectionSource source)
2850   {
2851     if (!(source instanceof AlignViewportI))
2852     {
2853       return false;
2854     }
2855     final AlignViewportI sourceAv = (AlignViewportI) source;
2856     if (sourceAv.getCodingComplement() != av
2857             && av.getCodingComplement() != sourceAv)
2858     {
2859       return false;
2860     }
2861
2862     /*
2863      * Map sequence selection
2864      */
2865     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2866     av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2867     av.isSelectionGroupChanged(true);
2868
2869     /*
2870      * Map column selection
2871      */
2872     // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2873     // av);
2874     ColumnSelection cs = new ColumnSelection();
2875     HiddenColumns hs = new HiddenColumns();
2876     MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2877     av.setColumnSelection(cs);
2878     boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2879
2880     // lastly, update any dependent dialogs
2881     if (ap.getCalculationDialog() != null)
2882     {
2883       ap.getCalculationDialog().validateCalcTypes();
2884     }
2885
2886     /*
2887      * repaint alignment, and also Overview or Structure
2888      * if hidden column selection has changed
2889      */
2890     ap.paintAlignment(hiddenChanged, hiddenChanged);
2891
2892     return true;
2893   }
2894
2895   /**
2896    * 
2897    * @return null or last search results handled by this panel
2898    */
2899   public SearchResultsI getLastSearchResults()
2900   {
2901     return lastSearchResults;
2902   }
2903 }