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