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