Merge branch 'patch/Release_2_11_1_Branch_patch_JAL-3490' into releases/Release_2_11_...
[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     List<SearchResultMatchI> matches = results.getResults();
930     int j = matches.size();
931     List<String> infos = new ArrayList<>();
932     for (int i = 0; i < j; i++)
933     {
934       SearchResultMatchI match = matches.get(i);
935       int pos = match.getStart();
936       if (pos == match.getEnd())
937       {
938         SequenceI seq = match.getSequence();
939         SequenceI ds = seq.getDatasetSequence() == null ? seq
940                 : seq.getDatasetSequence();
941         MappedFeatures mf = fr2
942                 .findComplementFeaturesAtResidue(ds, pos);
943         if (mf != null)
944         {
945           for (SequenceFeature sf : mf.features)
946           {
947             String pv = mf.findProteinVariants(sf);
948             if (pv.length() > 0 && !infos.contains(pv))
949             {
950               infos.add(pv);
951             }
952           }
953         }
954       }
955     }
956
957     if (infos.isEmpty())
958     {
959       return null;
960     }
961     StringBuilder sb = new StringBuilder();
962     for (String info : infos)
963     {
964       if (sb.length() > 0)
965       {
966         sb.append("|");
967       }
968       sb.append(info);
969     }
970     return sb.toString();
971   }
972
973   @Override
974   public VamsasSource getVamsasSource()
975   {
976     return this.ap == null ? null : this.ap.av;
977   }
978
979   @Override
980   public void updateColours(SequenceI seq, int index)
981   {
982     System.out.println("update the seqPanel colours");
983     // repaint();
984   }
985
986   /**
987    * Action on mouse movement is to update the status bar to show the current
988    * sequence position, and (if features are shown) to show any features at the
989    * position in a tooltip. Does nothing if the mouse move does not change
990    * residue position.
991    * 
992    * @param evt
993    */
994   @Override
995   public void mouseMoved(MouseEvent evt)
996   {
997     if (editingSeqs)
998     {
999       // This is because MacOSX creates a mouseMoved
1000       // If control is down, other platforms will not.
1001       mouseDragged(evt);
1002     }
1003
1004     final MousePos mousePos = findMousePosition(evt);
1005     if (mousePos.equals(lastMousePosition))
1006     {
1007       /*
1008        * just a pixel move without change of 'cell'
1009        */
1010       return;
1011     }
1012     lastMousePosition = mousePos;
1013
1014     if (mousePos.isOverAnnotation())
1015     {
1016       mouseMovedOverAnnotation(mousePos);
1017       return;
1018     }
1019     final int seq = mousePos.seqIndex;
1020
1021     final int column = mousePos.column;
1022     if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
1023     {
1024       lastMousePosition = null;
1025       setToolTipText(null);
1026       lastTooltip = null;
1027       ap.alignFrame.setStatus("");
1028       return;
1029     }
1030
1031     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1032
1033     if (column >= sequence.getLength())
1034     {
1035       return;
1036     }
1037
1038     /*
1039      * set status bar message, returning residue position in sequence
1040      */
1041     boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1042     final int pos = setStatusMessage(sequence, column, seq);
1043     if (ssm != null && !isGapped)
1044     {
1045       mouseOverSequence(sequence, column, pos);
1046     }
1047
1048     tooltipText.setLength(6); // Cuts the buffer back to <html>
1049
1050     SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1051     if (groups != null)
1052     {
1053       for (int g = 0; g < groups.length; g++)
1054       {
1055         if (groups[g].getStartRes() <= column
1056                 && groups[g].getEndRes() >= column)
1057         {
1058           if (!groups[g].getName().startsWith("JTreeGroup")
1059                   && !groups[g].getName().startsWith("JGroup"))
1060           {
1061             tooltipText.append(groups[g].getName());
1062           }
1063
1064           if (groups[g].getDescription() != null)
1065           {
1066             tooltipText.append(": " + groups[g].getDescription());
1067           }
1068         }
1069       }
1070     }
1071
1072     /*
1073      * add any features at the position to the tooltip; if over a gap, only
1074      * add features that straddle the gap (pos may be the residue before or
1075      * after the gap)
1076      */
1077     int unshownFeatures = 0;
1078     if (av.isShowSequenceFeatures())
1079     {
1080       List<SequenceFeature> features = ap.getFeatureRenderer()
1081               .findFeaturesAtColumn(sequence, column + 1);
1082       unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
1083               features, this.ap.getSeqPanel().seqCanvas.fr,
1084               MAX_TOOLTIP_LENGTH);
1085
1086       /*
1087        * add features in CDS/protein complement at the corresponding
1088        * position if configured to do so
1089        */
1090       if (av.isShowComplementFeatures())
1091       {
1092         if (!Comparison.isGap(sequence.getCharAt(column)))
1093         {
1094           AlignViewportI complement = ap.getAlignViewport()
1095                   .getCodingComplement();
1096           AlignFrame af = Desktop.getAlignFrameFor(complement);
1097           FeatureRendererModel fr2 = af.getFeatureRenderer();
1098           MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1099                   pos);
1100           if (mf != null)
1101           {
1102             unshownFeatures += seqARep.appendFeatures(tooltipText,
1103                     pos, mf, fr2, MAX_TOOLTIP_LENGTH);
1104           }
1105         }
1106       }
1107     }
1108     if (tooltipText.length() == 6) // "<html>"
1109     {
1110       setToolTipText(null);
1111       lastTooltip = null;
1112     }
1113     else
1114     {
1115       if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1116       {
1117         tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1118         tooltipText.append("...");
1119       }
1120       if (unshownFeatures > 0)
1121       {
1122         tooltipText.append("<br/>").append("... ").append("<i>")
1123                 .append(MessageManager.formatMessage(
1124                         "label.features_not_shown", unshownFeatures))
1125                 .append("</i>");
1126       }
1127       String textString = tooltipText.toString();
1128       if (lastTooltip == null || !lastTooltip.equals(textString))
1129       {
1130         String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1131                 textString);
1132         setToolTipText(formattedTooltipText);
1133         lastTooltip = textString;
1134       }
1135     }
1136   }
1137
1138   /**
1139    * When the view is in wrapped mode, and the mouse is over an annotation row,
1140    * shows the corresponding tooltip and status message (if any)
1141    * 
1142    * @param pos
1143    * @param column
1144    */
1145   protected void mouseMovedOverAnnotation(MousePos pos)
1146   {
1147     final int column = pos.column;
1148     final int rowIndex = pos.annotationIndex;
1149
1150     if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1151             || rowIndex < 0)
1152     {
1153       return;
1154     }
1155     AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1156
1157     String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1158             anns);
1159     setToolTipText(tooltip);
1160     lastTooltip = tooltip;
1161
1162     String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1163             anns[rowIndex]);
1164     ap.alignFrame.setStatus(msg);
1165   }
1166
1167   private Point lastp = null;
1168
1169   /*
1170    * (non-Javadoc)
1171    * 
1172    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1173    */
1174   @Override
1175   public Point getToolTipLocation(MouseEvent event)
1176   {
1177     if (tooltipText == null || tooltipText.length() <= 6)
1178     {
1179       lastp = null;
1180       return null;
1181     }
1182
1183     int x = event.getX();
1184     int w = getWidth();
1185     // switch sides when tooltip is too close to edge
1186     int wdth = (w - x < 200) ? -(w / 2) : 5;
1187     Point p = lastp;
1188     if (!event.isShiftDown() || p == null)
1189     {
1190       p = new Point(event.getX() + wdth, event.getY() - 20);
1191       lastp = p;
1192     }
1193     /*
1194      * TODO: try to set position so region is not obscured by tooltip
1195      */
1196     return p;
1197   }
1198
1199   String lastTooltip;
1200
1201   /**
1202    * set when the current UI interaction has resulted in a change that requires
1203    * shading in overviews and structures to be recalculated. this could be
1204    * changed to a something more expressive that indicates what actually has
1205    * changed, so selective redraws can be applied (ie. only structures, only
1206    * overview, etc)
1207    */
1208   private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1209
1210   /**
1211    * set if av.getSelectionGroup() refers to a group that is defined on the
1212    * alignment view, rather than a transient selection
1213    */
1214   // private boolean editingDefinedGroup = false; // TODO: refactor to
1215   // avcontroller or viewModel
1216
1217   /**
1218    * Sets the status message in alignment panel, showing the sequence number
1219    * (index) and id, and residue and residue position if not at a gap, for the
1220    * given sequence and column position. Returns the residue position returned
1221    * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1222    * if at a gapped position.
1223    * 
1224    * @param sequence
1225    *          aligned sequence object
1226    * @param column
1227    *          alignment column
1228    * @param seqIndex
1229    *          index of sequence in alignment
1230    * @return sequence position of residue at column, or adjacent residue if at a
1231    *         gap
1232    */
1233   int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1234   {
1235     char sequenceChar = sequence.getCharAt(column);
1236     int pos = sequence.findPosition(column);
1237     setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1238
1239     return pos;
1240   }
1241
1242   /**
1243    * Builds the status message for the current cursor location and writes it to
1244    * the status bar, for example
1245    * 
1246    * <pre>
1247    * Sequence 3 ID: FER1_SOLLC
1248    * Sequence 5 ID: FER1_PEA Residue: THR (4)
1249    * Sequence 5 ID: FER1_PEA Residue: B (3)
1250    * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1251    * </pre>
1252    * 
1253    * @param seqName
1254    * @param seqIndex
1255    *          sequence position in the alignment (1..)
1256    * @param sequenceChar
1257    *          the character under the cursor
1258    * @param residuePos
1259    *          the sequence residue position (if not over a gap)
1260    */
1261   protected void setStatusMessage(String seqName, int seqIndex,
1262           char sequenceChar, int residuePos)
1263   {
1264     StringBuilder text = new StringBuilder(32);
1265
1266     /*
1267      * Sequence number (if known), and sequence name.
1268      */
1269     String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1270     text.append("Sequence").append(seqno).append(" ID: ")
1271             .append(seqName);
1272
1273     String residue = null;
1274
1275     /*
1276      * Try to translate the display character to residue name (null for gap).
1277      */
1278     boolean isGapped = Comparison.isGap(sequenceChar);
1279
1280     if (!isGapped)
1281     {
1282       boolean nucleotide = av.getAlignment().isNucleotide();
1283       String displayChar = String.valueOf(sequenceChar);
1284       if (nucleotide)
1285       {
1286         residue = ResidueProperties.nucleotideName.get(displayChar);
1287       }
1288       else
1289       {
1290         residue = "X".equalsIgnoreCase(displayChar) ? "X"
1291                 : ("*".equals(displayChar) ? "STOP"
1292                         : ResidueProperties.aa2Triplet.get(displayChar));
1293       }
1294       text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1295               .append(": ").append(residue == null ? displayChar : residue);
1296
1297       text.append(" (").append(Integer.toString(residuePos)).append(")");
1298     }
1299     ap.alignFrame.setStatus(text.toString());
1300   }
1301
1302   /**
1303    * Set the status bar message to highlight the first matched position in
1304    * search results.
1305    * 
1306    * @param results
1307    */
1308   private void setStatusMessage(SearchResultsI results)
1309   {
1310     AlignmentI al = this.av.getAlignment();
1311     int sequenceIndex = al.findIndex(results);
1312     if (sequenceIndex == -1)
1313     {
1314       return;
1315     }
1316     SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1317     SequenceI ds = alignedSeq.getDatasetSequence();
1318     for (SearchResultMatchI m : results.getResults())
1319     {
1320       SequenceI seq = m.getSequence();
1321       if (seq.getDatasetSequence() != null)
1322       {
1323         seq = seq.getDatasetSequence();
1324       }
1325
1326       if (seq == ds)
1327       {
1328         int start = m.getStart();
1329         setStatusMessage(alignedSeq.getName(), sequenceIndex,
1330                 seq.getCharAt(start - 1), start);
1331         return;
1332       }
1333     }
1334   }
1335
1336   /**
1337    * {@inheritDoc}
1338    */
1339   @Override
1340   public void mouseDragged(MouseEvent evt)
1341   {
1342     MousePos pos = findMousePosition(evt);
1343     if (pos.isOverAnnotation() || pos.column == -1)
1344     {
1345       return;
1346     }
1347
1348     if (mouseWheelPressed)
1349     {
1350       boolean inSplitFrame = ap.av.getCodingComplement() != null;
1351       boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1352
1353       int oldWidth = av.getCharWidth();
1354
1355       // Which is bigger, left-right or up-down?
1356       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1357               .abs(evt.getX() - lastMousePress.getX()))
1358       {
1359         /*
1360          * on drag up or down, decrement or increment font size
1361          */
1362         int fontSize = av.font.getSize();
1363         boolean fontChanged = false;
1364
1365         if (evt.getY() < lastMousePress.getY())
1366         {
1367           fontChanged = true;
1368           fontSize--;
1369         }
1370         else if (evt.getY() > lastMousePress.getY())
1371         {
1372           fontChanged = true;
1373           fontSize++;
1374         }
1375
1376         if (fontSize < 1)
1377         {
1378           fontSize = 1;
1379         }
1380
1381         if (fontChanged)
1382         {
1383           Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1384                   fontSize);
1385           av.setFont(newFont, true);
1386           av.setCharWidth(oldWidth);
1387           ap.fontChanged();
1388           if (copyChanges)
1389           {
1390             ap.av.getCodingComplement().setFont(newFont, true);
1391             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1392                     .getSplitViewContainer();
1393             splitFrame.adjustLayout();
1394             splitFrame.repaint();
1395           }
1396         }
1397       }
1398       else
1399       {
1400         /*
1401          * on drag left or right, decrement or increment character width
1402          */
1403         int newWidth = 0;
1404         if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1405         {
1406           newWidth = av.getCharWidth() - 1;
1407           av.setCharWidth(newWidth);
1408         }
1409         else if (evt.getX() > lastMousePress.getX())
1410         {
1411           newWidth = av.getCharWidth() + 1;
1412           av.setCharWidth(newWidth);
1413         }
1414         if (newWidth > 0)
1415         {
1416           ap.paintAlignment(false, false);
1417           if (copyChanges)
1418           {
1419             /*
1420              * need to ensure newWidth is set on cdna, regardless of which
1421              * panel the mouse drag happened in; protein will compute its 
1422              * character width as 1:1 or 3:1
1423              */
1424             av.getCodingComplement().setCharWidth(newWidth);
1425             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1426                     .getSplitViewContainer();
1427             splitFrame.adjustLayout();
1428             splitFrame.repaint();
1429           }
1430         }
1431       }
1432
1433       FontMetrics fm = getFontMetrics(av.getFont());
1434       av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1435
1436       lastMousePress = evt.getPoint();
1437
1438       return;
1439     }
1440
1441     if (!editingSeqs)
1442     {
1443       dragStretchGroup(evt);
1444       return;
1445     }
1446
1447     int res = pos.column;
1448
1449     if (res < 0)
1450     {
1451       res = 0;
1452     }
1453
1454     if ((editLastRes == -1) || (editLastRes == res))
1455     {
1456       return;
1457     }
1458
1459     if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1460     {
1461       // dragLeft, delete gap
1462       editSequence(false, false, res);
1463     }
1464     else
1465     {
1466       editSequence(true, false, res);
1467     }
1468
1469     mouseDragging = true;
1470     if (scrollThread != null)
1471     {
1472       scrollThread.setMousePosition(evt.getPoint());
1473     }
1474   }
1475
1476   /**
1477    * Edits the sequence to insert or delete one or more gaps, in response to a
1478    * mouse drag or cursor mode command. The number of inserts/deletes may be
1479    * specified with the cursor command, or else depends on the mouse event
1480    * (normally one column, but potentially more for a fast mouse drag).
1481    * <p>
1482    * Delete gaps is limited to the number of gaps left of the cursor position
1483    * (mouse drag), or at or right of the cursor position (cursor mode).
1484    * <p>
1485    * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1486    * the current selection group.
1487    * <p>
1488    * In locked editing mode (with a selection group present), inserts/deletions
1489    * within the selection group are limited to its boundaries (and edits outside
1490    * the group stop at its border).
1491    * 
1492    * @param insertGap
1493    *          true to insert gaps, false to delete gaps
1494    * @param editSeq
1495    *          (unused parameter)
1496    * @param startres
1497    *          the column at which to perform the action; the number of columns
1498    *          affected depends on <code>this.editLastRes</code> (cursor column
1499    *          position)
1500    */
1501   synchronized void editSequence(boolean insertGap, boolean editSeq,
1502           final int startres)
1503   {
1504     int fixedLeft = -1;
1505     int fixedRight = -1;
1506     boolean fixedColumns = false;
1507     SequenceGroup sg = av.getSelectionGroup();
1508
1509     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1510
1511     // No group, but the sequence may represent a group
1512     if (!groupEditing && av.hasHiddenRows())
1513     {
1514       if (av.isHiddenRepSequence(seq))
1515       {
1516         sg = av.getRepresentedSequences(seq);
1517         groupEditing = true;
1518       }
1519     }
1520
1521     StringBuilder message = new StringBuilder(64); // for status bar
1522
1523     /*
1524      * make a name for the edit action, for
1525      * status bar message and Undo/Redo menu
1526      */
1527     String label = null;
1528     if (groupEditing)
1529     {
1530         message.append("Edit group:");
1531       label = MessageManager.getString("action.edit_group");
1532     }
1533     else
1534     {
1535         message.append("Edit sequence: " + seq.getName());
1536       label = seq.getName();
1537       if (label.length() > 10)
1538       {
1539         label = label.substring(0, 10);
1540       }
1541       label = MessageManager.formatMessage("label.edit_params",
1542               new String[]
1543               { label });
1544     }
1545
1546     /*
1547      * initialise the edit command if there is not
1548      * already one being extended
1549      */
1550     if (editCommand == null)
1551     {
1552       editCommand = new EditCommand(label);
1553     }
1554
1555     if (insertGap)
1556     {
1557       message.append(" insert ");
1558     }
1559     else
1560     {
1561       message.append(" delete ");
1562     }
1563
1564     message.append(Math.abs(startres - editLastRes) + " gaps.");
1565     ap.alignFrame.setStatus(message.toString());
1566
1567     /*
1568      * is there a selection group containing the sequence being edited?
1569      * if so the boundary of the group is the limit of the edit
1570      * (but the edit may be inside or outside the selection group)
1571      */
1572     boolean inSelectionGroup = sg != null
1573             && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1574     if (groupEditing || inSelectionGroup)
1575     {
1576       fixedColumns = true;
1577
1578       // sg might be null as the user may only see 1 sequence,
1579       // but the sequence represents a group
1580       if (sg == null)
1581       {
1582         if (!av.isHiddenRepSequence(seq))
1583         {
1584           endEditing();
1585           return;
1586         }
1587         sg = av.getRepresentedSequences(seq);
1588       }
1589
1590       fixedLeft = sg.getStartRes();
1591       fixedRight = sg.getEndRes();
1592
1593       if ((startres < fixedLeft && editLastRes >= fixedLeft)
1594               || (startres >= fixedLeft && editLastRes < fixedLeft)
1595               || (startres > fixedRight && editLastRes <= fixedRight)
1596               || (startres <= fixedRight && editLastRes > fixedRight))
1597       {
1598         endEditing();
1599         return;
1600       }
1601
1602       if (fixedLeft > startres)
1603       {
1604         fixedRight = fixedLeft - 1;
1605         fixedLeft = 0;
1606       }
1607       else if (fixedRight < startres)
1608       {
1609         fixedLeft = fixedRight;
1610         fixedRight = -1;
1611       }
1612     }
1613
1614     if (av.hasHiddenColumns())
1615     {
1616       fixedColumns = true;
1617       int y1 = av.getAlignment().getHiddenColumns()
1618               .getNextHiddenBoundary(true, startres);
1619       int y2 = av.getAlignment().getHiddenColumns()
1620               .getNextHiddenBoundary(false, startres);
1621
1622       if ((insertGap && startres > y1 && editLastRes < y1)
1623               || (!insertGap && startres < y2 && editLastRes > y2))
1624       {
1625         endEditing();
1626         return;
1627       }
1628
1629       // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1630       // Selection spans a hidden region
1631       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1632       {
1633         if (startres >= y2)
1634         {
1635           fixedLeft = y2;
1636         }
1637         else
1638         {
1639           fixedRight = y2 - 1;
1640         }
1641       }
1642     }
1643
1644     boolean success = doEditSequence(insertGap, editSeq, startres,
1645             fixedRight, fixedColumns, sg);
1646
1647     /*
1648      * report what actually happened (might be less than
1649      * what was requested), by inspecting the edit commands added
1650      */
1651     String msg = getEditStatusMessage(editCommand);
1652     ap.alignFrame.setStatus(msg == null ? " " : msg);
1653     if (!success)
1654     {
1655       endEditing();
1656     }
1657
1658     editLastRes = startres;
1659     seqCanvas.repaint();
1660   }
1661
1662   /**
1663    * A helper method that performs the requested editing to insert or delete
1664    * gaps (if possible). Answers true if the edit was successful, false if could
1665    * only be performed in part or not at all. Failure may occur in 'locked edit'
1666    * mode, when an insertion requires a matching gapped position (or column) to
1667    * delete, and deletion requires an adjacent gapped position (or column) to
1668    * remove.
1669    * 
1670    * @param insertGap
1671    *          true if inserting gap(s), false if deleting
1672    * @param editSeq
1673    *          (unused parameter, currently always false)
1674    * @param startres
1675    *          the column at which to perform the edit
1676    * @param fixedRight
1677    *          fixed right boundary column of a locked edit (within or to the
1678    *          left of a selection group)
1679    * @param fixedColumns
1680    *          true if this is a locked edit
1681    * @param sg
1682    *          the sequence group (if group edit is being performed)
1683    * @return
1684    */
1685   protected boolean doEditSequence(final boolean insertGap,
1686           final boolean editSeq, final int startres, int fixedRight,
1687           final boolean fixedColumns, final SequenceGroup sg)
1688   {
1689     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1690     SequenceI[] seqs = new SequenceI[] { seq };
1691
1692     if (groupEditing)
1693     {
1694       List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1695       int g, groupSize = vseqs.size();
1696       SequenceI[] groupSeqs = new SequenceI[groupSize];
1697       for (g = 0; g < groupSeqs.length; g++)
1698       {
1699         groupSeqs[g] = vseqs.get(g);
1700       }
1701
1702       // drag to right
1703       if (insertGap)
1704       {
1705         // If the user has selected the whole sequence, and is dragging to
1706         // the right, we can still extend the alignment and selectionGroup
1707         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1708                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1709         {
1710           sg.setEndRes(
1711                   av.getAlignment().getWidth() + startres - editLastRes);
1712           fixedRight = sg.getEndRes();
1713         }
1714
1715         // Is it valid with fixed columns??
1716         // Find the next gap before the end
1717         // of the visible region boundary
1718         boolean blank = false;
1719         for (; fixedRight > editLastRes; fixedRight--)
1720         {
1721           blank = true;
1722
1723           for (g = 0; g < groupSize; g++)
1724           {
1725             for (int j = 0; j < startres - editLastRes; j++)
1726             {
1727               if (!Comparison
1728                       .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1729               {
1730                 blank = false;
1731                 break;
1732               }
1733             }
1734           }
1735           if (blank)
1736           {
1737             break;
1738           }
1739         }
1740
1741         if (!blank)
1742         {
1743           if (sg.getSize() == av.getAlignment().getHeight())
1744           {
1745             if ((av.hasHiddenColumns()
1746                     && startres < av.getAlignment().getHiddenColumns()
1747                             .getNextHiddenBoundary(false, startres)))
1748             {
1749               return false;
1750             }
1751
1752             int alWidth = av.getAlignment().getWidth();
1753             if (av.hasHiddenRows())
1754             {
1755               int hwidth = av.getAlignment().getHiddenSequences()
1756                       .getWidth();
1757               if (hwidth > alWidth)
1758               {
1759                 alWidth = hwidth;
1760               }
1761             }
1762             // We can still insert gaps if the selectionGroup
1763             // contains all the sequences
1764             sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1765             fixedRight = alWidth + startres - editLastRes;
1766           }
1767           else
1768           {
1769             return false;
1770           }
1771         }
1772       }
1773
1774       // drag to left
1775       else if (!insertGap)
1776       {
1777         // / Are we able to delete?
1778         // ie are all columns blank?
1779
1780         for (g = 0; g < groupSize; g++)
1781         {
1782           for (int j = startres; j < editLastRes; j++)
1783           {
1784             if (groupSeqs[g].getLength() <= j)
1785             {
1786               continue;
1787             }
1788
1789             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1790             {
1791               // Not a gap, block edit not valid
1792               return false;
1793             }
1794           }
1795         }
1796       }
1797
1798       if (insertGap)
1799       {
1800         // dragging to the right
1801         if (fixedColumns && fixedRight != -1)
1802         {
1803           for (int j = editLastRes; j < startres; j++)
1804           {
1805             insertGap(j, groupSeqs, fixedRight);
1806           }
1807         }
1808         else
1809         {
1810           appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1811                   startres - editLastRes, false);
1812         }
1813       }
1814       else
1815       {
1816         // dragging to the left
1817         if (fixedColumns && fixedRight != -1)
1818         {
1819           for (int j = editLastRes; j > startres; j--)
1820           {
1821             deleteChar(startres, groupSeqs, fixedRight);
1822           }
1823         }
1824         else
1825         {
1826           appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1827                   editLastRes - startres, false);
1828         }
1829       }
1830     }
1831     else
1832     {
1833       /*
1834        * editing a single sequence
1835        */
1836       if (insertGap)
1837       {
1838         // dragging to the right
1839         if (fixedColumns && fixedRight != -1)
1840         {
1841           for (int j = editLastRes; j < startres; j++)
1842           {
1843             if (!insertGap(j, seqs, fixedRight))
1844             {
1845               /*
1846                * e.g. cursor mode command specified 
1847                * more inserts than are possible
1848                */
1849               return false;
1850             }
1851           }
1852         }
1853         else
1854         {
1855           appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1856                   startres - editLastRes, false);
1857         }
1858       }
1859       else
1860       {
1861         if (!editSeq)
1862         {
1863           // dragging to the left
1864           if (fixedColumns && fixedRight != -1)
1865           {
1866             for (int j = editLastRes; j > startres; j--)
1867             {
1868               if (!Comparison.isGap(seq.getCharAt(startres)))
1869               {
1870                 return false;
1871               }
1872               deleteChar(startres, seqs, fixedRight);
1873             }
1874           }
1875           else
1876           {
1877             // could be a keyboard edit trying to delete none gaps
1878             int max = 0;
1879             for (int m = startres; m < editLastRes; m++)
1880             {
1881               if (!Comparison.isGap(seq.getCharAt(m)))
1882               {
1883                 break;
1884               }
1885               max++;
1886             }
1887             if (max > 0)
1888             {
1889               appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1890             }
1891           }
1892         }
1893         else
1894         {// insertGap==false AND editSeq==TRUE;
1895           if (fixedColumns && fixedRight != -1)
1896           {
1897             for (int j = editLastRes; j < startres; j++)
1898             {
1899               insertGap(j, seqs, fixedRight);
1900             }
1901           }
1902           else
1903           {
1904             appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1905                     startres - editLastRes, false);
1906           }
1907         }
1908       }
1909     }
1910
1911     return true;
1912   }
1913
1914   /**
1915    * Constructs an informative status bar message while dragging to insert or
1916    * delete gaps. Answers null if inserts and deletes cancel out.
1917    * 
1918    * @param editCommand
1919    *          a command containing the list of individual edits
1920    * @return
1921    */
1922   protected static String getEditStatusMessage(EditCommand editCommand)
1923   {
1924     if (editCommand == null)
1925     {
1926       return null;
1927     }
1928
1929     /*
1930      * add any inserts, and subtract any deletes,  
1931      * not counting those auto-inserted when doing a 'locked edit'
1932      * (so only counting edits 'under the cursor')
1933      */
1934     int count = 0;
1935     for (Edit cmd : editCommand.getEdits())
1936     {
1937       if (!cmd.isSystemGenerated())
1938       {
1939         count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1940                 : -cmd.getNumber();
1941       }
1942     }
1943
1944     if (count == 0)
1945     {
1946       /*
1947        * inserts and deletes cancel out
1948        */
1949       return null;
1950     }
1951
1952     String msgKey = count > 1 ? "label.insert_gaps"
1953             : (count == 1 ? "label.insert_gap"
1954                     : (count == -1 ? "label.delete_gap"
1955                             : "label.delete_gaps"));
1956     count = Math.abs(count);
1957
1958     return MessageManager.formatMessage(msgKey, String.valueOf(count));
1959   }
1960
1961   /**
1962    * Inserts one gap at column j, deleting the right-most gapped column up to
1963    * (and including) fixedColumn. Returns true if the edit is successful, false
1964    * if no blank column is available to allow the insertion to be balanced by a
1965    * deletion.
1966    * 
1967    * @param j
1968    * @param seq
1969    * @param fixedColumn
1970    * @return
1971    */
1972   boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1973   {
1974     int blankColumn = fixedColumn;
1975     for (int s = 0; s < seq.length; s++)
1976     {
1977       // Find the next gap before the end of the visible region boundary
1978       // If lastCol > j, theres a boundary after the gap insertion
1979
1980       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1981       {
1982         if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1983         {
1984           // Theres a space, so break and insert the gap
1985           break;
1986         }
1987       }
1988
1989       if (blankColumn <= j)
1990       {
1991         blankColumn = fixedColumn;
1992         endEditing();
1993         return false;
1994       }
1995     }
1996
1997     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1998
1999     appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2000
2001     return true;
2002   }
2003
2004   /**
2005    * Helper method to add and perform one edit action
2006    * 
2007    * @param action
2008    * @param seq
2009    * @param pos
2010    * @param count
2011    * @param systemGenerated
2012    *          true if the edit is a 'balancing' delete (or insert) to match a
2013    *          user's insert (or delete) in a locked editing region
2014    */
2015   protected void appendEdit(Action action, SequenceI[] seq, int pos,
2016           int count, boolean systemGenerated)
2017   {
2018
2019     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2020             av.getAlignment().getGapCharacter());
2021     edit.setSystemGenerated(systemGenerated);
2022
2023     editCommand.appendEdit(edit, av.getAlignment(), true, null);
2024   }
2025
2026   /**
2027    * Deletes the character at column j, and inserts a gap at fixedColumn, in
2028    * each of the given sequences. The caller should ensure that all sequences
2029    * are gapped in column j.
2030    * 
2031    * @param j
2032    * @param seqs
2033    * @param fixedColumn
2034    */
2035   void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2036   {
2037     appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2038
2039     appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2040   }
2041
2042   /**
2043    * On reentering the panel, stops any scrolling that was started on dragging
2044    * out of the panel
2045    * 
2046    * @param e
2047    */
2048   @Override
2049   public void mouseEntered(MouseEvent e)
2050   {
2051     if (oldSeq < 0)
2052     {
2053       oldSeq = 0;
2054     }
2055     stopScrolling();
2056   }
2057
2058   /**
2059    * On leaving the panel, if the mouse is being dragged, starts a thread to
2060    * scroll it until the mouse is released (in unwrapped mode only)
2061    * 
2062    * @param e
2063    */
2064   @Override
2065   public void mouseExited(MouseEvent e)
2066   {
2067     lastMousePosition = null;
2068     ap.alignFrame.setStatus(" ");
2069     if (av.getWrapAlignment())
2070     {
2071       return;
2072     }
2073
2074     if (mouseDragging && scrollThread == null)
2075     {
2076       scrollThread = new ScrollThread();
2077     }
2078   }
2079
2080   /**
2081    * Handler for double-click on a position with one or more sequence features.
2082    * Opens the Amend Features dialog to allow feature details to be amended, or
2083    * the feature deleted.
2084    */
2085   @Override
2086   public void mouseClicked(MouseEvent evt)
2087   {
2088     SequenceGroup sg = null;
2089     MousePos pos = findMousePosition(evt);
2090     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2091     {
2092       return;
2093     }
2094
2095     if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2096     {
2097       sg = av.getSelectionGroup();
2098       if (sg != null && sg.getSize() == 1
2099               && sg.getEndRes() - sg.getStartRes() < 2)
2100       {
2101         av.setSelectionGroup(null);
2102       }
2103
2104       int column = pos.column;
2105
2106       /*
2107        * find features at the position (if not gapped), or straddling
2108        * the position (if at a gap)
2109        */
2110       SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2111       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2112               .findFeaturesAtColumn(sequence, column + 1);
2113
2114       if (!features.isEmpty())
2115       {
2116         /*
2117          * highlight the first feature at the position on the alignment
2118          */
2119         SearchResultsI highlight = new SearchResults();
2120         highlight.addResult(sequence, features.get(0).getBegin(), features
2121                 .get(0).getEnd());
2122         seqCanvas.highlightSearchResults(highlight, false);
2123
2124         /*
2125          * open the Amend Features dialog; clear highlighting afterwards,
2126          * whether changes were made or not
2127          */
2128         List<SequenceI> seqs = Collections.singletonList(sequence);
2129         seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2130                 ap);
2131         av.setSearchResults(null); // clear highlighting
2132         seqCanvas.repaint(); // draw new/amended features
2133       }
2134     }
2135   }
2136
2137   @Override
2138   public void mouseWheelMoved(MouseWheelEvent e)
2139   {
2140     e.consume();
2141     double wheelRotation = e.getPreciseWheelRotation();
2142     if (wheelRotation > 0)
2143     {
2144       if (e.isShiftDown())
2145       {
2146         av.getRanges().scrollRight(true);
2147
2148       }
2149       else
2150       {
2151         av.getRanges().scrollUp(false);
2152       }
2153     }
2154     else if (wheelRotation < 0)
2155     {
2156       if (e.isShiftDown())
2157       {
2158         av.getRanges().scrollRight(false);
2159       }
2160       else
2161       {
2162         av.getRanges().scrollUp(true);
2163       }
2164     }
2165
2166     /*
2167      * update status bar and tooltip for new position
2168      * (need to synthesize a mouse movement to refresh tooltip)
2169      */
2170     mouseMoved(e);
2171     ToolTipManager.sharedInstance().mouseMoved(e);
2172   }
2173
2174   /**
2175    * DOCUMENT ME!
2176    * 
2177    * @param pos
2178    *          DOCUMENT ME!
2179    */
2180   protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2181   {
2182     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2183     {
2184       return;
2185     }
2186
2187     final int res = pos.column;
2188     final int seq = pos.seqIndex;
2189     oldSeq = seq;
2190     updateOverviewAndStructs = false;
2191
2192     startWrapBlock = wrappedBlock;
2193
2194     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2195
2196     if ((sequence == null) || (res > sequence.getLength()))
2197     {
2198       return;
2199     }
2200
2201     stretchGroup = av.getSelectionGroup();
2202
2203     if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2204     {
2205       stretchGroup = av.getAlignment().findGroup(sequence, res);
2206       if (stretchGroup != null)
2207       {
2208         // only update the current selection if the popup menu has a group to
2209         // focus on
2210         av.setSelectionGroup(stretchGroup);
2211       }
2212     }
2213
2214     if (evt.isPopupTrigger()) // Mac: mousePressed
2215     {
2216       showPopupMenu(evt, pos);
2217       return;
2218     }
2219
2220     /*
2221      * defer right-mouse click handling to mouseReleased on Windows
2222      * (where isPopupTrigger() will answer true)
2223      * NB isRightMouseButton is also true for Cmd-click on Mac
2224      */
2225     if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2226     {
2227       return;
2228     }
2229
2230     if (av.cursorMode)
2231     {
2232       seqCanvas.cursorX = res;
2233       seqCanvas.cursorY = seq;
2234       seqCanvas.repaint();
2235       return;
2236     }
2237
2238     if (stretchGroup == null)
2239     {
2240       createStretchGroup(res, sequence);
2241     }
2242
2243     if (stretchGroup != null)
2244     {
2245       stretchGroup.addPropertyChangeListener(seqCanvas);
2246     }
2247
2248     seqCanvas.repaint();
2249   }
2250
2251   private void createStretchGroup(int res, SequenceI sequence)
2252   {
2253     // Only if left mouse button do we want to change group sizes
2254     // define a new group here
2255     SequenceGroup sg = new SequenceGroup();
2256     sg.setStartRes(res);
2257     sg.setEndRes(res);
2258     sg.addSequence(sequence, false);
2259     av.setSelectionGroup(sg);
2260     stretchGroup = sg;
2261
2262     if (av.getConservationSelected())
2263     {
2264       SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2265               ap.getViewName());
2266     }
2267
2268     if (av.getAbovePIDThreshold())
2269     {
2270       SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2271               ap.getViewName());
2272     }
2273     // TODO: stretchGroup will always be not null. Is this a merge error ?
2274     // or is there a threading issue here?
2275     if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2276     {
2277       // Edit end res position of selected group
2278       changeEndRes = true;
2279     }
2280     else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2281     {
2282       // Edit end res position of selected group
2283       changeStartRes = true;
2284     }
2285     stretchGroup.getWidth();
2286
2287   }
2288
2289   /**
2290    * Build and show a pop-up menu at the right-click mouse position
2291    *
2292    * @param evt
2293    * @param pos
2294    */
2295   void showPopupMenu(MouseEvent evt, MousePos pos)
2296   {
2297     final int column = pos.column;
2298     final int seq = pos.seqIndex;
2299     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2300     if (sequence != null)
2301     {
2302       PopupMenu pop = new PopupMenu(ap, sequence, column);
2303       pop.show(this, evt.getX(), evt.getY());
2304     }
2305   }
2306
2307   /**
2308    * Update the display after mouse up on a selection or group
2309    * 
2310    * @param evt
2311    *          mouse released event details
2312    * @param afterDrag
2313    *          true if this event is happening after a mouse drag (rather than a
2314    *          mouse down)
2315    */
2316   protected void doMouseReleasedDefineMode(MouseEvent evt,
2317           boolean afterDrag)
2318   {
2319     if (stretchGroup == null)
2320     {
2321       return;
2322     }
2323
2324     stretchGroup.removePropertyChangeListener(seqCanvas);
2325
2326     // always do this - annotation has own state
2327     // but defer colourscheme update until hidden sequences are passed in
2328     boolean vischange = stretchGroup.recalcConservation(true);
2329     updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2330             && afterDrag;
2331     if (stretchGroup.cs != null)
2332     {
2333       if (afterDrag)
2334       {
2335         stretchGroup.cs.alignmentChanged(stretchGroup,
2336                 av.getHiddenRepSequences());
2337       }
2338
2339       ResidueShaderI groupColourScheme = stretchGroup
2340               .getGroupColourScheme();
2341       String name = stretchGroup.getName();
2342       if (stretchGroup.cs.conservationApplied())
2343       {
2344         SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2345       }
2346       if (stretchGroup.cs.getThreshold() > 0)
2347       {
2348         SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2349       }
2350     }
2351     PaintRefresher.Refresh(this, av.getSequenceSetId());
2352     // TODO: structure colours only need updating if stretchGroup used to or now
2353     // does contain sequences with structure views
2354     ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2355     updateOverviewAndStructs = false;
2356     changeEndRes = false;
2357     changeStartRes = false;
2358     stretchGroup = null;
2359     av.sendSelection();
2360   }
2361
2362   /**
2363    * Resizes the borders of a selection group depending on the direction of
2364    * mouse drag
2365    * 
2366    * @param evt
2367    */
2368   protected void dragStretchGroup(MouseEvent evt)
2369   {
2370     if (stretchGroup == null)
2371     {
2372       return;
2373     }
2374
2375     MousePos pos = findMousePosition(evt);
2376     if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2377     {
2378       return;
2379     }
2380
2381     int res = pos.column;
2382     int y = pos.seqIndex;
2383
2384     if (wrappedBlock != startWrapBlock)
2385     {
2386       return;
2387     }
2388
2389     res = Math.min(res, av.getAlignment().getWidth()-1);
2390
2391     if (stretchGroup.getEndRes() == res)
2392     {
2393       // Edit end res position of selected group
2394       changeEndRes = true;
2395     }
2396     else if (stretchGroup.getStartRes() == res)
2397     {
2398       // Edit start res position of selected group
2399       changeStartRes = true;
2400     }
2401
2402     if (res < av.getRanges().getStartRes())
2403     {
2404       res = av.getRanges().getStartRes();
2405     }
2406
2407     if (changeEndRes)
2408     {
2409       if (res > (stretchGroup.getStartRes() - 1))
2410       {
2411         stretchGroup.setEndRes(res);
2412         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2413       }
2414     }
2415     else if (changeStartRes)
2416     {
2417       if (res < (stretchGroup.getEndRes() + 1))
2418       {
2419         stretchGroup.setStartRes(res);
2420         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2421       }
2422     }
2423
2424     int dragDirection = 0;
2425
2426     if (y > oldSeq)
2427     {
2428       dragDirection = 1;
2429     }
2430     else if (y < oldSeq)
2431     {
2432       dragDirection = -1;
2433     }
2434
2435     while ((y != oldSeq) && (oldSeq > -1)
2436             && (y < av.getAlignment().getHeight()))
2437     {
2438       // This routine ensures we don't skip any sequences, as the
2439       // selection is quite slow.
2440       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2441
2442       oldSeq += dragDirection;
2443
2444       if (oldSeq < 0)
2445       {
2446         break;
2447       }
2448
2449       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2450
2451       if (stretchGroup.getSequences(null).contains(nextSeq))
2452       {
2453         stretchGroup.deleteSequence(seq, false);
2454         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2455       }
2456       else
2457       {
2458         if (seq != null)
2459         {
2460           stretchGroup.addSequence(seq, false);
2461         }
2462
2463         stretchGroup.addSequence(nextSeq, false);
2464         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2465       }
2466     }
2467
2468     if (oldSeq < 0)
2469     {
2470       oldSeq = -1;
2471     }
2472
2473     mouseDragging = true;
2474
2475     if (scrollThread != null)
2476     {
2477       scrollThread.setMousePosition(evt.getPoint());
2478     }
2479
2480     /*
2481      * construct a status message showing the range of the selection
2482      */
2483     StringBuilder status = new StringBuilder(64);
2484     List<SequenceI> seqs = stretchGroup.getSequences();
2485     String name = seqs.get(0).getName();
2486     if (name.length() > 20)
2487     {
2488       name = name.substring(0, 20);
2489     }
2490     status.append(name).append(" - ");
2491     name = seqs.get(seqs.size() - 1).getName();
2492     if (name.length() > 20)
2493     {
2494       name = name.substring(0, 20);
2495     }
2496     status.append(name).append(" ");
2497     int startRes = stretchGroup.getStartRes();
2498     status.append(" cols ").append(String.valueOf(startRes + 1))
2499             .append("-");
2500     int endRes = stretchGroup.getEndRes();
2501     status.append(String.valueOf(endRes + 1));
2502     status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2503             .append(String.valueOf(endRes - startRes + 1)).append(")");
2504     ap.alignFrame.setStatus(status.toString());
2505   }
2506
2507   /**
2508    * Stops the scroll thread if it is running
2509    */
2510   void stopScrolling()
2511   {
2512     if (scrollThread != null)
2513     {
2514       scrollThread.stopScrolling();
2515       scrollThread = null;
2516     }
2517     mouseDragging = false;
2518   }
2519
2520   /**
2521    * Starts a thread to scroll the alignment, towards a given mouse position
2522    * outside the panel bounds
2523    * 
2524    * @param mousePos
2525    */
2526   void startScrolling(Point mousePos)
2527   {
2528     if (scrollThread == null)
2529     {
2530       scrollThread = new ScrollThread();
2531     }
2532
2533     mouseDragging = true;
2534     scrollThread.setMousePosition(mousePos);
2535   }
2536
2537   /**
2538    * Performs scrolling of the visible alignment left, right, up or down
2539    */
2540   class ScrollThread extends Thread
2541   {
2542     private Point mousePos;
2543
2544     private volatile boolean threadRunning = true;
2545
2546     /**
2547      * Constructor
2548      */
2549     public ScrollThread()
2550     {
2551       setName("SeqPanel$ScrollThread");
2552       start();
2553     }
2554
2555     /**
2556      * Sets the position of the mouse that determines the direction of the
2557      * scroll to perform
2558      * 
2559      * @param p
2560      */
2561     public void setMousePosition(Point p)
2562     {
2563       mousePos = p;
2564     }
2565
2566     /**
2567      * Sets a flag that will cause the thread to exit
2568      */
2569     public void stopScrolling()
2570     {
2571       threadRunning = false;
2572     }
2573
2574     /**
2575      * Scrolls the alignment left or right, and/or up or down, depending on the
2576      * last notified mouse position, until the limit of the alignment is
2577      * reached, or a flag is set to stop the scroll
2578      */
2579     @Override
2580     public void run()
2581     {
2582       while (threadRunning && mouseDragging)
2583       {
2584         if (mousePos != null)
2585         {
2586           boolean scrolled = false;
2587           ViewportRanges ranges = SeqPanel.this.av.getRanges();
2588
2589           /*
2590            * scroll up or down
2591            */
2592           if (mousePos.y < 0)
2593           {
2594             // mouse is above this panel - try scroll up
2595             scrolled = ranges.scrollUp(true);
2596           }
2597           else if (mousePos.y >= getHeight())
2598           {
2599             // mouse is below this panel - try scroll down
2600             scrolled = ranges.scrollUp(false);
2601           }
2602
2603           /*
2604            * scroll left or right
2605            */
2606           if (mousePos.x < 0)
2607           {
2608             scrolled |= ranges.scrollRight(false);
2609           }
2610           else if (mousePos.x >= getWidth())
2611           {
2612             scrolled |= ranges.scrollRight(true);
2613           }
2614           if (!scrolled)
2615           {
2616             /*
2617              * we have reached the limit of the visible alignment - quit
2618              */
2619             threadRunning = false;
2620             SeqPanel.this.ap.repaint();
2621           }
2622         }
2623
2624         try
2625         {
2626           Thread.sleep(20);
2627         } catch (Exception ex)
2628         {
2629         }
2630       }
2631     }
2632   }
2633
2634   /**
2635    * modify current selection according to a received message.
2636    */
2637   @Override
2638   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2639           HiddenColumns hidden, SelectionSource source)
2640   {
2641     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2642     // handles selection messages...
2643     // TODO: extend config options to allow user to control if selections may be
2644     // shared between viewports.
2645     boolean iSentTheSelection = (av == source
2646             || (source instanceof AlignViewport
2647                     && ((AlignmentViewport) source).getSequenceSetId()
2648                             .equals(av.getSequenceSetId())));
2649
2650     if (iSentTheSelection)
2651     {
2652       // respond to our own event by updating dependent dialogs
2653       if (ap.getCalculationDialog() != null)
2654       {
2655         ap.getCalculationDialog().validateCalcTypes();
2656       }
2657
2658       return;
2659     }
2660
2661     // process further ?
2662     if (!av.followSelection)
2663     {
2664       return;
2665     }
2666
2667     /*
2668      * Ignore the selection if there is one of our own pending.
2669      */
2670     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2671     {
2672       return;
2673     }
2674
2675     /*
2676      * Check for selection in a view of which this one is a dna/protein
2677      * complement.
2678      */
2679     if (selectionFromTranslation(seqsel, colsel, hidden, source))
2680     {
2681       return;
2682     }
2683
2684     // do we want to thread this ? (contention with seqsel and colsel locks, I
2685     // suspect)
2686     /*
2687      * only copy colsel if there is a real intersection between
2688      * sequence selection and this panel's alignment
2689      */
2690     boolean repaint = false;
2691     boolean copycolsel = false;
2692
2693     SequenceGroup sgroup = null;
2694     if (seqsel != null && seqsel.getSize() > 0)
2695     {
2696       if (av.getAlignment() == null)
2697       {
2698         Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2699                 + " ViewId=" + av.getViewId()
2700                 + " 's alignment is NULL! returning immediately.");
2701         return;
2702       }
2703       sgroup = seqsel.intersect(av.getAlignment(),
2704               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2705       if ((sgroup != null && sgroup.getSize() > 0))
2706       {
2707         copycolsel = true;
2708       }
2709     }
2710     if (sgroup != null && sgroup.getSize() > 0)
2711     {
2712       av.setSelectionGroup(sgroup);
2713     }
2714     else
2715     {
2716       av.setSelectionGroup(null);
2717     }
2718     av.isSelectionGroupChanged(true);
2719     repaint = true;
2720
2721     if (copycolsel)
2722     {
2723       // the current selection is unset or from a previous message
2724       // so import the new colsel.
2725       if (colsel == null || colsel.isEmpty())
2726       {
2727         if (av.getColumnSelection() != null)
2728         {
2729           av.getColumnSelection().clear();
2730           repaint = true;
2731         }
2732       }
2733       else
2734       {
2735         // TODO: shift colSel according to the intersecting sequences
2736         if (av.getColumnSelection() == null)
2737         {
2738           av.setColumnSelection(new ColumnSelection(colsel));
2739         }
2740         else
2741         {
2742           av.getColumnSelection().setElementsFrom(colsel,
2743                   av.getAlignment().getHiddenColumns());
2744         }
2745       }
2746       av.isColSelChanged(true);
2747       repaint = true;
2748     }
2749
2750     if (copycolsel && av.hasHiddenColumns()
2751             && (av.getAlignment().getHiddenColumns() == null))
2752     {
2753       System.err.println("Bad things");
2754     }
2755     if (repaint) // always true!
2756     {
2757       // probably finessing with multiple redraws here
2758       PaintRefresher.Refresh(this, av.getSequenceSetId());
2759       // ap.paintAlignment(false);
2760     }
2761
2762     // lastly, update dependent dialogs
2763     if (ap.getCalculationDialog() != null)
2764     {
2765       ap.getCalculationDialog().validateCalcTypes();
2766     }
2767
2768   }
2769
2770   /**
2771    * If this panel is a cdna/protein translation view of the selection source,
2772    * tries to map the source selection to a local one, and returns true. Else
2773    * returns false.
2774    * 
2775    * @param seqsel
2776    * @param colsel
2777    * @param source
2778    */
2779   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2780           ColumnSelection colsel, HiddenColumns hidden,
2781           SelectionSource source)
2782   {
2783     if (!(source instanceof AlignViewportI))
2784     {
2785       return false;
2786     }
2787     final AlignViewportI sourceAv = (AlignViewportI) source;
2788     if (sourceAv.getCodingComplement() != av
2789             && av.getCodingComplement() != sourceAv)
2790     {
2791       return false;
2792     }
2793
2794     /*
2795      * Map sequence selection
2796      */
2797     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2798     av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2799     av.isSelectionGroupChanged(true);
2800
2801     /*
2802      * Map column selection
2803      */
2804     // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2805     // av);
2806     ColumnSelection cs = new ColumnSelection();
2807     HiddenColumns hs = new HiddenColumns();
2808     MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2809     av.setColumnSelection(cs);
2810     boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2811
2812     // lastly, update any dependent dialogs
2813     if (ap.getCalculationDialog() != null)
2814     {
2815       ap.getCalculationDialog().validateCalcTypes();
2816     }
2817
2818     /*
2819      * repaint alignment, and also Overview or Structure
2820      * if hidden column selection has changed
2821      */
2822     ap.paintAlignment(hiddenChanged, hiddenChanged);
2823
2824     return true;
2825   }
2826
2827   /**
2828    * 
2829    * @return null or last search results handled by this panel
2830    */
2831   public SearchResultsI getLastSearchResults()
2832   {
2833     return lastSearchResults;
2834   }
2835 }