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