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