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