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