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