875b3a708ccf899bc7ed78a91b64b06a82184720
[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.ColumnSelection;
29 import jalview.datamodel.SearchResults;
30 import jalview.datamodel.SearchResults.Match;
31 import jalview.datamodel.Sequence;
32 import jalview.datamodel.SequenceFeature;
33 import jalview.datamodel.SequenceGroup;
34 import jalview.datamodel.SequenceI;
35 import jalview.io.SequenceAnnotationReport;
36 import jalview.schemes.ResidueProperties;
37 import jalview.structure.SelectionListener;
38 import jalview.structure.SelectionSource;
39 import jalview.structure.SequenceListener;
40 import jalview.structure.StructureSelectionManager;
41 import jalview.structure.VamsasSource;
42 import jalview.util.Comparison;
43 import jalview.util.MappingUtils;
44 import jalview.util.MessageManager;
45 import jalview.viewmodel.AlignmentViewport;
46
47 import java.awt.BorderLayout;
48 import java.awt.Color;
49 import java.awt.Font;
50 import java.awt.FontMetrics;
51 import java.awt.Point;
52 import java.awt.event.MouseEvent;
53 import java.awt.event.MouseListener;
54 import java.awt.event.MouseMotionListener;
55 import java.awt.event.MouseWheelEvent;
56 import java.awt.event.MouseWheelListener;
57 import java.util.List;
58 import java.util.Vector;
59
60 import javax.swing.JOptionPane;
61 import javax.swing.JPanel;
62 import javax.swing.ToolTipManager;
63
64 /**
65  * DOCUMENT ME!
66  * 
67  * @author $author$
68  * @version $Revision: 1.130 $
69  */
70 public class SeqPanel extends JPanel implements MouseListener,
71         MouseMotionListener, MouseWheelListener, SequenceListener,
72         SelectionListener
73
74 {
75   /** DOCUMENT ME!! */
76   public SeqCanvas seqCanvas;
77
78   /** DOCUMENT ME!! */
79   public AlignmentPanel ap;
80
81   protected int lastres;
82
83   protected int startseq;
84
85   protected AlignViewport av;
86
87   ScrollThread scrollThread = null;
88
89   boolean mouseDragging = false;
90
91   boolean editingSeqs = false;
92
93   boolean groupEditing = false;
94
95   // ////////////////////////////////////////
96   // ///Everything below this is for defining the boundary of the rubberband
97   // ////////////////////////////////////////
98   int oldSeq = -1;
99
100   boolean changeEndSeq = false;
101
102   boolean changeStartSeq = false;
103
104   boolean changeEndRes = false;
105
106   boolean changeStartRes = false;
107
108   SequenceGroup stretchGroup = null;
109
110   boolean remove = false;
111
112   Point lastMousePress;
113
114   boolean mouseWheelPressed = false;
115
116   StringBuffer keyboardNo1;
117
118   StringBuffer keyboardNo2;
119
120   java.net.URL linkImageURL;
121
122   private final SequenceAnnotationReport seqARep;
123
124   StringBuffer tooltipText = new StringBuffer();
125
126   String tmpString;
127
128   EditCommand editCommand;
129
130   StructureSelectionManager ssm;
131
132   /**
133    * Creates a new SeqPanel object.
134    * 
135    * @param avp
136    *          DOCUMENT ME!
137    * @param p
138    *          DOCUMENT ME!
139    */
140   public SeqPanel(AlignViewport av, AlignmentPanel ap)
141   {
142     linkImageURL = getClass().getResource("/images/link.gif");
143     seqARep = new SequenceAnnotationReport(linkImageURL.toString());
144     ToolTipManager.sharedInstance().registerComponent(this);
145     ToolTipManager.sharedInstance().setInitialDelay(0);
146     ToolTipManager.sharedInstance().setDismissDelay(10000);
147     this.av = av;
148     setBackground(Color.white);
149
150     seqCanvas = new SeqCanvas(ap);
151     setLayout(new BorderLayout());
152     add(seqCanvas, BorderLayout.CENTER);
153
154     this.ap = ap;
155
156     if (!av.isDataset())
157     {
158       addMouseMotionListener(this);
159       addMouseListener(this);
160       addMouseWheelListener(this);
161       ssm = av.getStructureSelectionManager();
162       ssm.addStructureViewerListener(this);
163       ssm.addSelectionListener(this);
164     }
165   }
166
167   int startWrapBlock = -1;
168
169   int wrappedBlock = -1;
170
171   /**
172    * Returns the aligned sequence position (base 0) at the mouse position, or
173    * the closest visible one
174    * 
175    * @param evt
176    * @return
177    */
178   int findRes(MouseEvent evt)
179   {
180     int res = 0;
181     int x = evt.getX();
182
183     if (av.getWrapAlignment())
184     {
185
186       int hgap = av.getCharHeight();
187       if (av.getScaleAboveWrapped())
188       {
189         hgap += av.getCharHeight();
190       }
191
192       int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
193               + hgap
194               + seqCanvas.getAnnotationHeight();
195
196       int y = evt.getY();
197       y -= hgap;
198       x -= seqCanvas.LABEL_WEST;
199
200       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
201       if (cwidth < 1)
202       {
203         return 0;
204       }
205
206       wrappedBlock = y / cHeight;
207       wrappedBlock += av.getStartRes() / cwidth;
208
209       res = wrappedBlock * cwidth + x / av.getCharWidth();
210
211     }
212     else
213     {
214       if (x > seqCanvas.getX() + seqCanvas.getWidth())
215       {
216         // make sure we calculate relative to visible alignment, rather than
217         // right-hand gutter
218         x = seqCanvas.getX() + seqCanvas.getWidth();
219       }
220       res = (x / av.getCharWidth()) + av.getStartRes();
221       if (res > av.getEndRes())
222       {
223         // moused off right
224         res = av.getEndRes();
225       }
226     }
227
228     if (av.hasHiddenColumns())
229     {
230       res = av.getColumnSelection().adjustForHiddenColumns(res);
231     }
232
233     return res;
234
235   }
236
237   int findSeq(MouseEvent evt)
238   {
239     int seq = 0;
240     int y = evt.getY();
241
242     if (av.getWrapAlignment())
243     {
244       int hgap = av.getCharHeight();
245       if (av.getScaleAboveWrapped())
246       {
247         hgap += av.getCharHeight();
248       }
249
250       int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
251               + hgap
252               + seqCanvas.getAnnotationHeight();
253
254       y -= hgap;
255
256       seq = Math.min((y % cHeight) / av.getCharHeight(), av.getAlignment()
257               .getHeight() - 1);
258     }
259     else
260     {
261       seq = Math.min((y / av.getCharHeight()) + av.getStartSeq(), av
262               .getAlignment().getHeight() - 1);
263     }
264
265     return seq;
266   }
267
268   /**
269    * When all of a sequence of edits are complete, put the resulting edit list
270    * on the history stack (undo list), and reset flags for editing in progress.
271    */
272   void endEditing()
273   {
274     try
275     {
276       if (editCommand != null && editCommand.getSize() > 0)
277       {
278         ap.alignFrame.addHistoryItem(editCommand);
279         av.firePropertyChange("alignment", null, av.getAlignment()
280                 .getSequences());
281       }
282     } finally
283     {
284       /*
285        * Tidy up come what may...
286        */
287       startseq = -1;
288       lastres = -1;
289       editingSeqs = false;
290       groupEditing = false;
291       keyboardNo1 = null;
292       keyboardNo2 = null;
293       editCommand = null;
294     }
295   }
296
297   void setCursorRow()
298   {
299     seqCanvas.cursorY = getKeyboardNo1() - 1;
300     scrollToVisible();
301   }
302
303   void setCursorColumn()
304   {
305     seqCanvas.cursorX = getKeyboardNo1() - 1;
306     scrollToVisible();
307   }
308
309   void setCursorRowAndColumn()
310   {
311     if (keyboardNo2 == null)
312     {
313       keyboardNo2 = new StringBuffer();
314     }
315     else
316     {
317       seqCanvas.cursorX = getKeyboardNo1() - 1;
318       seqCanvas.cursorY = getKeyboardNo2() - 1;
319       scrollToVisible();
320     }
321   }
322
323   void setCursorPosition()
324   {
325     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
326
327     seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
328     scrollToVisible();
329   }
330
331   void moveCursor(int dx, int dy)
332   {
333     seqCanvas.cursorX += dx;
334     seqCanvas.cursorY += dy;
335     if (av.hasHiddenColumns()
336             && !av.getColumnSelection().isVisible(seqCanvas.cursorX))
337     {
338       int original = seqCanvas.cursorX - dx;
339       int maxWidth = av.getAlignment().getWidth();
340
341       while (!av.getColumnSelection().isVisible(seqCanvas.cursorX)
342               && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0)
343       {
344         seqCanvas.cursorX += dx;
345       }
346
347       if (seqCanvas.cursorX >= maxWidth
348               || !av.getColumnSelection().isVisible(seqCanvas.cursorX))
349       {
350         seqCanvas.cursorX = original;
351       }
352     }
353
354     scrollToVisible();
355   }
356
357   void scrollToVisible()
358   {
359     if (seqCanvas.cursorX < 0)
360     {
361       seqCanvas.cursorX = 0;
362     }
363     else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
364     {
365       seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
366     }
367
368     if (seqCanvas.cursorY < 0)
369     {
370       seqCanvas.cursorY = 0;
371     }
372     else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
373     {
374       seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
375     }
376
377     endEditing();
378     if (av.getWrapAlignment())
379     {
380       ap.scrollToWrappedVisible(seqCanvas.cursorX);
381     }
382     else
383     {
384       while (seqCanvas.cursorY < av.startSeq)
385       {
386         ap.scrollUp(true);
387       }
388       while (seqCanvas.cursorY + 1 > av.endSeq)
389       {
390         ap.scrollUp(false);
391       }
392       if (!av.getWrapAlignment())
393       {
394         while (seqCanvas.cursorX < av.getColumnSelection()
395                 .adjustForHiddenColumns(av.startRes))
396         {
397           if (!ap.scrollRight(false))
398           {
399             break;
400           }
401         }
402         while (seqCanvas.cursorX > av.getColumnSelection()
403                 .adjustForHiddenColumns(av.endRes))
404         {
405           if (!ap.scrollRight(true))
406           {
407             break;
408           }
409         }
410       }
411     }
412     setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
413             seqCanvas.cursorX, seqCanvas.cursorY);
414
415     seqCanvas.repaint();
416   }
417
418   void setSelectionAreaAtCursor(boolean topLeft)
419   {
420     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
421
422     if (av.getSelectionGroup() != null)
423     {
424       SequenceGroup sg = av.getSelectionGroup();
425       // Find the top and bottom of this group
426       int min = av.getAlignment().getHeight(), max = 0;
427       for (int i = 0; i < sg.getSize(); i++)
428       {
429         int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
430         if (index > max)
431         {
432           max = index;
433         }
434         if (index < min)
435         {
436           min = index;
437         }
438       }
439
440       max++;
441
442       if (topLeft)
443       {
444         sg.setStartRes(seqCanvas.cursorX);
445         if (sg.getEndRes() < seqCanvas.cursorX)
446         {
447           sg.setEndRes(seqCanvas.cursorX);
448         }
449
450         min = seqCanvas.cursorY;
451       }
452       else
453       {
454         sg.setEndRes(seqCanvas.cursorX);
455         if (sg.getStartRes() > seqCanvas.cursorX)
456         {
457           sg.setStartRes(seqCanvas.cursorX);
458         }
459
460         max = seqCanvas.cursorY + 1;
461       }
462
463       if (min > max)
464       {
465         // Only the user can do this
466         av.setSelectionGroup(null);
467       }
468       else
469       {
470         // Now add any sequences between min and max
471         sg.getSequences(null).clear();
472         for (int i = min; i < max; i++)
473         {
474           sg.addSequence(av.getAlignment().getSequenceAt(i), false);
475         }
476       }
477     }
478
479     if (av.getSelectionGroup() == null)
480     {
481       SequenceGroup sg = new SequenceGroup();
482       sg.setStartRes(seqCanvas.cursorX);
483       sg.setEndRes(seqCanvas.cursorX);
484       sg.addSequence(sequence, false);
485       av.setSelectionGroup(sg);
486     }
487
488     ap.paintAlignment(false);
489     av.sendSelection();
490   }
491
492   void insertGapAtCursor(boolean group)
493   {
494     groupEditing = group;
495     startseq = seqCanvas.cursorY;
496     lastres = seqCanvas.cursorX;
497     editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
498     endEditing();
499   }
500
501   void deleteGapAtCursor(boolean group)
502   {
503     groupEditing = group;
504     startseq = seqCanvas.cursorY;
505     lastres = seqCanvas.cursorX + getKeyboardNo1();
506     editSequence(false, false, seqCanvas.cursorX);
507     endEditing();
508   }
509
510   void insertNucAtCursor(boolean group, String nuc)
511   {
512     groupEditing = group;
513     startseq = seqCanvas.cursorY;
514     lastres = seqCanvas.cursorX;
515     editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
516     endEditing();
517   }
518
519   void numberPressed(char value)
520   {
521     if (keyboardNo1 == null)
522     {
523       keyboardNo1 = new StringBuffer();
524     }
525
526     if (keyboardNo2 != null)
527     {
528       keyboardNo2.append(value);
529     }
530     else
531     {
532       keyboardNo1.append(value);
533     }
534   }
535
536   int getKeyboardNo1()
537   {
538     try
539     {
540       if (keyboardNo1 != null)
541       {
542         int value = Integer.parseInt(keyboardNo1.toString());
543         keyboardNo1 = null;
544         return value;
545       }
546     } catch (Exception x)
547     {
548     }
549     keyboardNo1 = null;
550     return 1;
551   }
552
553   int getKeyboardNo2()
554   {
555     try
556     {
557       if (keyboardNo2 != null)
558       {
559         int value = Integer.parseInt(keyboardNo2.toString());
560         keyboardNo2 = null;
561         return value;
562       }
563     } catch (Exception x)
564     {
565     }
566     keyboardNo2 = null;
567     return 1;
568   }
569
570   /**
571    * DOCUMENT ME!
572    * 
573    * @param evt
574    *          DOCUMENT ME!
575    */
576   @Override
577   public void mouseReleased(MouseEvent evt)
578   {
579     mouseDragging = false;
580     mouseWheelPressed = false;
581
582     if (!editingSeqs)
583     {
584       doMouseReleasedDefineMode(evt);
585       return;
586     }
587
588     endEditing();
589   }
590
591   /**
592    * DOCUMENT ME!
593    * 
594    * @param evt
595    *          DOCUMENT ME!
596    */
597   @Override
598   public void mousePressed(MouseEvent evt)
599   {
600     lastMousePress = evt.getPoint();
601
602     if (javax.swing.SwingUtilities.isMiddleMouseButton(evt))
603     {
604       mouseWheelPressed = true;
605       return;
606     }
607
608     if (evt.isShiftDown() || evt.isAltDown() || evt.isControlDown())
609     {
610       if (evt.isAltDown() || evt.isControlDown())
611       {
612         groupEditing = true;
613       }
614       editingSeqs = true;
615     }
616     else
617     {
618       doMousePressedDefineMode(evt);
619       return;
620     }
621
622     int seq = findSeq(evt);
623     int res = findRes(evt);
624
625     if (seq < 0 || res < 0)
626     {
627       return;
628     }
629
630     if ((seq < av.getAlignment().getHeight())
631             && (res < av.getAlignment().getSequenceAt(seq).getLength()))
632     {
633       startseq = seq;
634       lastres = res;
635     }
636     else
637     {
638       startseq = -1;
639       lastres = -1;
640     }
641
642     return;
643   }
644
645   String lastMessage;
646
647   @Override
648   public void mouseOverSequence(SequenceI sequence, int index, int pos)
649   {
650     String tmp = sequence.hashCode() + " " + index + " " + pos;
651
652     if (lastMessage == null || !lastMessage.equals(tmp))
653     {
654       // System.err.println("mouseOver Sequence: "+tmp);
655       ssm.mouseOverSequence(sequence, index, pos, av);
656     }
657     lastMessage = tmp;
658   }
659
660   @Override
661   public void highlightSequence(SearchResults results)
662   {
663     if (av.isFollowHighlight())
664     {
665       /*
666        * if scrollToPosition requires a scroll adjustment, this flag prevents
667        * another scroll event being propagated back to the originator
668        * 
669        * @see AlignmentPanel#adjustmentValueChanged
670        */
671       ap.setFollowingComplementScroll(true);
672       if (ap.scrollToPosition(results, false))
673       {
674         seqCanvas.revalidate();
675       }
676     }
677     setStatusMessage(results);
678     seqCanvas.highlightSearchResults(results);
679   }
680
681   @Override
682   public VamsasSource getVamsasSource()
683   {
684     return this.ap == null ? null : this.ap.av;
685   }
686   @Override
687   public void updateColours(SequenceI seq, int index)
688   {
689     System.out.println("update the seqPanel colours");
690     // repaint();
691   }
692
693   /**
694    * DOCUMENT ME!
695    * 
696    * @param evt
697    *          DOCUMENT ME!
698    */
699   @Override
700   public void mouseMoved(MouseEvent evt)
701   {
702     if (editingSeqs)
703     {
704       // This is because MacOSX creates a mouseMoved
705       // If control is down, other platforms will not.
706       mouseDragged(evt);
707     }
708
709     int res = findRes(evt);
710     int seq = findSeq(evt);
711     int pos;
712     if (res < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
713     {
714       return;
715     }
716
717     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
718
719     if (res >= sequence.getLength())
720     {
721       return;
722     }
723
724     pos = setStatusMessage(sequence, res, seq);
725     if (ssm != null && pos > -1)
726     {
727       mouseOverSequence(sequence, res, pos);
728     }
729
730     tooltipText.setLength(6); // Cuts the buffer back to <html>
731
732     SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
733     if (groups != null)
734     {
735       for (int g = 0; g < groups.length; g++)
736       {
737         if (groups[g].getStartRes() <= res && groups[g].getEndRes() >= res)
738         {
739           if (!groups[g].getName().startsWith("JTreeGroup")
740                   && !groups[g].getName().startsWith("JGroup"))
741           {
742             tooltipText.append(groups[g].getName());
743           }
744
745           if (groups[g].getDescription() != null)
746           {
747             tooltipText.append(": " + groups[g].getDescription());
748           }
749         }
750       }
751     }
752
753     // use aa to see if the mouse pointer is on a
754     if (av.isShowSequenceFeatures())
755     {
756       int rpos;
757       List<SequenceFeature> features = ap.getFeatureRenderer().findFeaturesAtRes(
758               sequence.getDatasetSequence(),
759               rpos = sequence.findPosition(res));
760       seqARep.appendFeatures(tooltipText, rpos, features,
761               this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
762     }
763     if (tooltipText.length() == 6) // <html></html>
764     {
765       setToolTipText(null);
766       lastTooltip = null;
767     }
768     else
769     {
770       if (lastTooltip == null
771               || !lastTooltip.equals(tooltipText.toString()))
772       {
773         String formatedTooltipText = JvSwingUtils.wrapTooltip(true,
774                 tooltipText.toString());
775         // String formatedTooltipText = tooltipText.toString();
776         setToolTipText(formatedTooltipText);
777         lastTooltip = tooltipText.toString();
778       }
779
780     }
781
782   }
783
784   private Point lastp = null;
785
786   /*
787    * (non-Javadoc)
788    * 
789    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
790    */
791   public Point getToolTipLocation(MouseEvent event)
792   {
793     int x = event.getX(), w = getWidth();
794     int wdth = (w - x < 200) ? -(w / 2) : 5; // switch sides when tooltip is too
795     // close to edge
796     Point p = lastp;
797     if (!event.isShiftDown() || p == null)
798     {
799       p = (tooltipText != null && tooltipText.length() > 6) ? new Point(
800               event.getX() + wdth, event.getY() - 20) : null;
801     }
802     /*
803      * TODO: try to modify position region is not obcured by tooltip
804      */
805     return lastp = p;
806   }
807
808   String lastTooltip;
809
810   /**
811    * Set status message in alignment panel
812    * 
813    * @param sequence
814    *          aligned sequence object
815    * @param res
816    *          alignment column
817    * @param seq
818    *          index of sequence in alignment
819    * @return position of res in sequence
820    */
821   int setStatusMessage(SequenceI sequence, int res, int seq)
822   {
823     StringBuilder text = new StringBuilder(32);
824
825     /*
826      * Sequence number (if known), and sequence name.
827      */
828     String seqno = seq == -1 ? "" : " " + (seq + 1);
829     text.append("Sequence" + seqno + " ID: " + sequence.getName());
830
831     String residue = null;
832     /*
833      * Try to translate the display character to residue name (null for gap).
834      */
835     final String displayChar = String.valueOf(sequence.getCharAt(res));
836     if (av.getAlignment().isNucleotide())
837     {
838       residue = ResidueProperties.nucleotideName.get(displayChar);
839       if (residue != null)
840       {
841         text.append(" Nucleotide: ").append(residue);
842       }
843     }
844     else
845     {
846       residue = "X".equalsIgnoreCase(displayChar) ? "X"
847               : ResidueProperties.aa2Triplet.get(displayChar);
848       if (residue != null)
849       {
850         text.append(" Residue: ").append(residue);
851       }
852     }
853
854     int pos = -1;
855     if (residue != null)
856     {
857       pos = sequence.findPosition(res);
858       text.append(" (").append(Integer.toString(pos)).append(")");
859     }
860     ap.alignFrame.statusBar.setText(text.toString());
861     return pos;
862   }
863
864   /**
865    * Set the status bar message to highlight the first matched position in
866    * search results.
867    * 
868    * @param results
869    */
870   private void setStatusMessage(SearchResults results)
871   {
872     List<Match> matches = results.getResults();
873     if (!matches.isEmpty())
874     {
875       Match m = matches.get(0);
876       SequenceI seq = m.getSequence();
877       int sequenceIndex = this.av.getAlignment().findIndex(seq);
878
879       /*
880        * Convert position in sequence (base 1) to sequence character array index
881        * (base 0)
882        */
883       int start = m.getStart() - 1;
884       setStatusMessage(seq, start, sequenceIndex);
885     }
886   }
887
888   /**
889    * DOCUMENT ME!
890    * 
891    * @param evt
892    *          DOCUMENT ME!
893    */
894   @Override
895   public void mouseDragged(MouseEvent evt)
896   {
897     if (mouseWheelPressed)
898     {
899       int oldWidth = av.getCharWidth();
900
901       // Which is bigger, left-right or up-down?
902       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math.abs(evt
903               .getX() - lastMousePress.getX()))
904       {
905         int fontSize = av.font.getSize();
906
907         if (evt.getY() < lastMousePress.getY())
908         {
909           fontSize--;
910         }
911         else if (evt.getY() > lastMousePress.getY())
912         {
913           fontSize++;
914         }
915
916         if (fontSize < 1)
917         {
918           fontSize = 1;
919         }
920
921         av.setFont(
922                 new Font(av.font.getName(), av.font.getStyle(), fontSize),
923                 true);
924         av.setCharWidth(oldWidth);
925         ap.fontChanged();
926       }
927       else
928       {
929         if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
930         {
931           av.setCharWidth(av.getCharWidth() - 1);
932         }
933         else if (evt.getX() > lastMousePress.getX())
934         {
935           av.setCharWidth(av.getCharWidth() + 1);
936         }
937
938         ap.paintAlignment(false);
939       }
940
941       FontMetrics fm = getFontMetrics(av.getFont());
942       av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
943
944       lastMousePress = evt.getPoint();
945
946       return;
947     }
948
949     if (!editingSeqs)
950     {
951       doMouseDraggedDefineMode(evt);
952       return;
953     }
954
955     int res = findRes(evt);
956
957     if (res < 0)
958     {
959       res = 0;
960     }
961
962     if ((lastres == -1) || (lastres == res))
963     {
964       return;
965     }
966
967     if ((res < av.getAlignment().getWidth()) && (res < lastres))
968     {
969       // dragLeft, delete gap
970       editSequence(false, false, res);
971     }
972     else
973     {
974       editSequence(true, false, res);
975     }
976
977     mouseDragging = true;
978     if (scrollThread != null)
979     {
980       scrollThread.setEvent(evt);
981     }
982   }
983
984   // TODO: Make it more clever than many booleans
985   synchronized void editSequence(boolean insertGap, boolean editSeq,
986           int startres)
987   {
988     int fixedLeft = -1;
989     int fixedRight = -1;
990     boolean fixedColumns = false;
991     SequenceGroup sg = av.getSelectionGroup();
992
993     SequenceI seq = av.getAlignment().getSequenceAt(startseq);
994
995     // No group, but the sequence may represent a group
996     if (!groupEditing && av.hasHiddenRows())
997     {
998       if (av.isHiddenRepSequence(seq))
999       {
1000         sg = av.getRepresentedSequences(seq);
1001         groupEditing = true;
1002       }
1003     }
1004
1005     StringBuilder message = new StringBuilder(64);
1006     if (groupEditing)
1007     {
1008       message.append("Edit group:");
1009       if (editCommand == null)
1010       {
1011         editCommand = new EditCommand(MessageManager.getString("action.edit_group"));
1012       }
1013     }
1014     else
1015     {
1016       message.append("Edit sequence: " + seq.getName());
1017       String label = seq.getName();
1018       if (label.length() > 10)
1019       {
1020         label = label.substring(0, 10);
1021       }
1022       if (editCommand == null)
1023       {
1024         editCommand = new EditCommand(MessageManager.formatMessage("label.edit_params", new String[]{label}));
1025       }
1026     }
1027
1028     if (insertGap)
1029     {
1030       message.append(" insert ");
1031     }
1032     else
1033     {
1034       message.append(" delete ");
1035     }
1036
1037     message.append(Math.abs(startres - lastres) + " gaps.");
1038     ap.alignFrame.statusBar.setText(message.toString());
1039
1040     // Are we editing within a selection group?
1041     if (groupEditing
1042             || (sg != null && sg.getSequences(av.getHiddenRepSequences())
1043                     .contains(seq)))
1044     {
1045       fixedColumns = true;
1046
1047       // sg might be null as the user may only see 1 sequence,
1048       // but the sequence represents a group
1049       if (sg == null)
1050       {
1051         if (!av.isHiddenRepSequence(seq))
1052         {
1053           endEditing();
1054           return;
1055         }
1056         sg = av.getRepresentedSequences(seq);
1057       }
1058
1059       fixedLeft = sg.getStartRes();
1060       fixedRight = sg.getEndRes();
1061
1062       if ((startres < fixedLeft && lastres >= fixedLeft)
1063               || (startres >= fixedLeft && lastres < fixedLeft)
1064               || (startres > fixedRight && lastres <= fixedRight)
1065               || (startres <= fixedRight && lastres > fixedRight))
1066       {
1067         endEditing();
1068         return;
1069       }
1070
1071       if (fixedLeft > startres)
1072       {
1073         fixedRight = fixedLeft - 1;
1074         fixedLeft = 0;
1075       }
1076       else if (fixedRight < startres)
1077       {
1078         fixedLeft = fixedRight;
1079         fixedRight = -1;
1080       }
1081     }
1082
1083     if (av.hasHiddenColumns())
1084     {
1085       fixedColumns = true;
1086       int y1 = av.getColumnSelection().getHiddenBoundaryLeft(startres);
1087       int y2 = av.getColumnSelection().getHiddenBoundaryRight(startres);
1088
1089       if ((insertGap && startres > y1 && lastres < y1)
1090               || (!insertGap && startres < y2 && lastres > y2))
1091       {
1092         endEditing();
1093         return;
1094       }
1095
1096       // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1097       // Selection spans a hidden region
1098       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1099       {
1100         if (startres >= y2)
1101         {
1102           fixedLeft = y2;
1103         }
1104         else
1105         {
1106           fixedRight = y2 - 1;
1107         }
1108       }
1109     }
1110
1111     if (groupEditing)
1112     {
1113       List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1114       int g, groupSize = vseqs.size();
1115       SequenceI[] groupSeqs = new SequenceI[groupSize];
1116       for (g = 0; g < groupSeqs.length; g++)
1117       {
1118         groupSeqs[g] = vseqs.get(g);
1119       }
1120
1121       // drag to right
1122       if (insertGap)
1123       {
1124         // If the user has selected the whole sequence, and is dragging to
1125         // the right, we can still extend the alignment and selectionGroup
1126         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1127                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1128         {
1129           sg.setEndRes(av.getAlignment().getWidth() + startres - lastres);
1130           fixedRight = sg.getEndRes();
1131         }
1132
1133         // Is it valid with fixed columns??
1134         // Find the next gap before the end
1135         // of the visible region boundary
1136         boolean blank = false;
1137         for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--)
1138         {
1139           blank = true;
1140
1141           for (g = 0; g < groupSize; g++)
1142           {
1143             for (int j = 0; j < startres - lastres; j++)
1144             {
1145               if (!Comparison.isGap(groupSeqs[g]
1146                       .getCharAt(fixedRight - j)))
1147               {
1148                 blank = false;
1149                 break;
1150               }
1151             }
1152           }
1153           if (blank)
1154           {
1155             break;
1156           }
1157         }
1158
1159         if (!blank)
1160         {
1161           if (sg.getSize() == av.getAlignment().getHeight())
1162           {
1163             if ((av.hasHiddenColumns() && startres < av
1164                     .getColumnSelection().getHiddenBoundaryRight(startres)))
1165             {
1166               endEditing();
1167               return;
1168             }
1169
1170             int alWidth = av.getAlignment().getWidth();
1171             if (av.hasHiddenRows())
1172             {
1173               int hwidth = av.getAlignment().getHiddenSequences()
1174                       .getWidth();
1175               if (hwidth > alWidth)
1176               {
1177                 alWidth = hwidth;
1178               }
1179             }
1180             // We can still insert gaps if the selectionGroup
1181             // contains all the sequences
1182             sg.setEndRes(sg.getEndRes() + startres - lastres);
1183             fixedRight = alWidth + startres - lastres;
1184           }
1185           else
1186           {
1187             endEditing();
1188             return;
1189           }
1190         }
1191       }
1192
1193       // drag to left
1194       else if (!insertGap)
1195       {
1196         // / Are we able to delete?
1197         // ie are all columns blank?
1198
1199         for (g = 0; g < groupSize; g++)
1200         {
1201           for (int j = startres; j < lastres; j++)
1202           {
1203             if (groupSeqs[g].getLength() <= j)
1204             {
1205               continue;
1206             }
1207
1208             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1209             {
1210               // Not a gap, block edit not valid
1211               endEditing();
1212               return;
1213             }
1214           }
1215         }
1216       }
1217
1218       if (insertGap)
1219       {
1220         // dragging to the right
1221         if (fixedColumns && fixedRight != -1)
1222         {
1223           for (int j = lastres; j < startres; j++)
1224           {
1225             insertChar(j, groupSeqs, fixedRight);
1226           }
1227         }
1228         else
1229         {
1230           appendEdit(Action.INSERT_GAP, groupSeqs, startres, startres
1231                   - lastres);
1232         }
1233       }
1234       else
1235       {
1236         // dragging to the left
1237         if (fixedColumns && fixedRight != -1)
1238         {
1239           for (int j = lastres; j > startres; j--)
1240           {
1241             deleteChar(startres, groupSeqs, fixedRight);
1242           }
1243         }
1244         else
1245         {
1246           appendEdit(Action.DELETE_GAP, groupSeqs, startres, lastres
1247                   - startres);
1248         }
1249
1250       }
1251     }
1252     else
1253     // ///Editing a single sequence///////////
1254     {
1255       if (insertGap)
1256       {
1257         // dragging to the right
1258         if (fixedColumns && fixedRight != -1)
1259         {
1260           for (int j = lastres; j < startres; j++)
1261           {
1262             insertChar(j, new SequenceI[]
1263             { seq }, fixedRight);
1264           }
1265         }
1266         else
1267         {
1268           appendEdit(Action.INSERT_GAP, new SequenceI[]
1269           { seq }, lastres, startres - lastres);
1270         }
1271       }
1272       else
1273       {
1274         if (!editSeq)
1275         {
1276           // dragging to the left
1277           if (fixedColumns && fixedRight != -1)
1278           {
1279             for (int j = lastres; j > startres; j--)
1280             {
1281               if (!Comparison.isGap(seq.getCharAt(startres)))
1282               {
1283                 endEditing();
1284                 break;
1285               }
1286               deleteChar(startres, new SequenceI[]
1287               { seq }, fixedRight);
1288             }
1289           }
1290           else
1291           {
1292             // could be a keyboard edit trying to delete none gaps
1293             int max = 0;
1294             for (int m = startres; m < lastres; m++)
1295             {
1296               if (!Comparison.isGap(seq.getCharAt(m)))
1297               {
1298                 break;
1299               }
1300               max++;
1301             }
1302
1303             if (max > 0)
1304             {
1305               appendEdit(Action.DELETE_GAP, new SequenceI[]
1306               { seq }, startres, max);
1307             }
1308           }
1309         }
1310         else
1311         {// insertGap==false AND editSeq==TRUE;
1312           if (fixedColumns && fixedRight != -1)
1313           {
1314             for (int j = lastres; j < startres; j++)
1315             {
1316               insertChar(j, new SequenceI[]
1317               { seq }, fixedRight);
1318             }
1319           }
1320           else
1321           {
1322             appendEdit(Action.INSERT_NUC, new SequenceI[]
1323             { seq }, lastres, startres - lastres);
1324           }
1325         }
1326       }
1327     }
1328
1329     lastres = startres;
1330     seqCanvas.repaint();
1331   }
1332
1333   void insertChar(int j, SequenceI[] seq, int fixedColumn)
1334   {
1335     int blankColumn = fixedColumn;
1336     for (int s = 0; s < seq.length; s++)
1337     {
1338       // Find the next gap before the end of the visible region boundary
1339       // If lastCol > j, theres a boundary after the gap insertion
1340
1341       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1342       {
1343         if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1344         {
1345           // Theres a space, so break and insert the gap
1346           break;
1347         }
1348       }
1349
1350       if (blankColumn <= j)
1351       {
1352         blankColumn = fixedColumn;
1353         endEditing();
1354         return;
1355       }
1356     }
1357
1358     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1);
1359
1360     appendEdit(Action.INSERT_GAP, seq, j, 1);
1361
1362   }
1363
1364   /**
1365    * Helper method to add and perform one edit action.
1366    * 
1367    * @param action
1368    * @param seq
1369    * @param pos
1370    * @param count
1371    */
1372   protected void appendEdit(Action action, SequenceI[] seq, int pos,
1373           int count)
1374   {
1375
1376     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1377             av.getAlignment().getGapCharacter());
1378
1379     editCommand.appendEdit(edit, av.getAlignment(),
1380             true, null);
1381   }
1382
1383   void deleteChar(int j, SequenceI[] seq, int fixedColumn)
1384   {
1385
1386     appendEdit(Action.DELETE_GAP, seq, j, 1);
1387
1388     appendEdit(Action.INSERT_GAP, seq, fixedColumn, 1);
1389   }
1390
1391   /**
1392    * DOCUMENT ME!
1393    * 
1394    * @param e
1395    *          DOCUMENT ME!
1396    */
1397   @Override
1398   public void mouseEntered(MouseEvent e)
1399   {
1400     if (oldSeq < 0)
1401     {
1402       oldSeq = 0;
1403     }
1404
1405     if (scrollThread != null)
1406     {
1407       scrollThread.running = false;
1408       scrollThread = null;
1409     }
1410   }
1411
1412   /**
1413    * DOCUMENT ME!
1414    * 
1415    * @param e
1416    *          DOCUMENT ME!
1417    */
1418   @Override
1419   public void mouseExited(MouseEvent e)
1420   {
1421     if (av.getWrapAlignment())
1422     {
1423       return;
1424     }
1425
1426     if (mouseDragging)
1427     {
1428       scrollThread = new ScrollThread();
1429     }
1430   }
1431
1432   @Override
1433   public void mouseClicked(MouseEvent evt)
1434   {
1435     SequenceGroup sg = null;
1436     SequenceI sequence = av.getAlignment().getSequenceAt(findSeq(evt));
1437     if (evt.getClickCount() > 1)
1438     {
1439       sg = av.getSelectionGroup();
1440       if (sg != null && sg.getSize() == 1
1441               && sg.getEndRes() - sg.getStartRes() < 2)
1442       {
1443         av.setSelectionGroup(null);
1444       }
1445
1446       List<SequenceFeature> features = seqCanvas.getFeatureRenderer().findFeaturesAtRes(
1447               sequence.getDatasetSequence(),
1448               sequence.findPosition(findRes(evt)));
1449
1450       if (features != null && features.size()> 0)
1451       {
1452         SearchResults highlight = new SearchResults();
1453         highlight.addResult(sequence, features.get(0).getBegin(),
1454                 features.get(0).getEnd());
1455         seqCanvas.highlightSearchResults(highlight);
1456       }
1457       if (features != null && features.size()> 0)
1458       {
1459         seqCanvas.getFeatureRenderer().amendFeatures(new SequenceI[]
1460         { sequence }, features.toArray(new SequenceFeature[features.size()]), false, ap);
1461
1462         seqCanvas.highlightSearchResults(null);
1463       }
1464     }
1465   }
1466
1467   @Override
1468   public void mouseWheelMoved(MouseWheelEvent e)
1469   {
1470     e.consume();
1471     if (e.getWheelRotation() > 0)
1472     {
1473       if (e.isShiftDown())
1474       {
1475         ap.scrollRight(true);
1476
1477       }
1478       else
1479       {
1480         ap.scrollUp(false);
1481       }
1482     }
1483     else
1484     {
1485       if (e.isShiftDown())
1486       {
1487         ap.scrollRight(false);
1488       }
1489       else
1490       {
1491         ap.scrollUp(true);
1492       }
1493     }
1494     // TODO Update tooltip for new position.
1495   }
1496
1497   /**
1498    * DOCUMENT ME!
1499    * 
1500    * @param evt
1501    *          DOCUMENT ME!
1502    */
1503   public void doMousePressedDefineMode(MouseEvent evt)
1504   {
1505     int res = findRes(evt);
1506     int seq = findSeq(evt);
1507     oldSeq = seq;
1508
1509     startWrapBlock = wrappedBlock;
1510
1511     if (av.getWrapAlignment() && seq > av.getAlignment().getHeight())
1512     {
1513       JOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
1514               .getString("label.cannot_edit_annotations_in_wrapped_view"),
1515               MessageManager.getString("label.wrapped_view_no_edit"),
1516               JOptionPane.WARNING_MESSAGE);
1517       return;
1518     }
1519
1520     if (seq < 0 || res < 0)
1521     {
1522       return;
1523     }
1524
1525     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1526
1527     if ((sequence == null) || (res > sequence.getLength()))
1528     {
1529       return;
1530     }
1531
1532     stretchGroup = av.getSelectionGroup();
1533
1534     if (stretchGroup == null)
1535     {
1536       stretchGroup = av.getAlignment().findGroup(sequence);
1537
1538       if ((stretchGroup != null) && (res > stretchGroup.getStartRes())
1539               && (res < stretchGroup.getEndRes()))
1540       {
1541         av.setSelectionGroup(stretchGroup);
1542       }
1543       else
1544       {
1545         stretchGroup = null;
1546       }
1547     }
1548     else if (!stretchGroup.getSequences(null).contains(sequence)
1549             || (stretchGroup.getStartRes() > res)
1550             || (stretchGroup.getEndRes() < res))
1551     {
1552       stretchGroup = null;
1553
1554       SequenceGroup[] allGroups = av.getAlignment().findAllGroups(sequence);
1555
1556       if (allGroups != null)
1557       {
1558         for (int i = 0; i < allGroups.length; i++)
1559         {
1560           if ((allGroups[i].getStartRes() <= res)
1561                   && (allGroups[i].getEndRes() >= res))
1562           {
1563             stretchGroup = allGroups[i];
1564             break;
1565           }
1566         }
1567       }
1568
1569       av.setSelectionGroup(stretchGroup);
1570
1571     }
1572
1573     if (javax.swing.SwingUtilities.isRightMouseButton(evt))
1574     {
1575       List<SequenceFeature> allFeatures = ap.getFeatureRenderer().findFeaturesAtRes(
1576               sequence.getDatasetSequence(), sequence.findPosition(res));
1577       Vector links = new Vector();
1578       for (SequenceFeature sf:allFeatures)
1579       {
1580         if (sf.links != null)
1581         {
1582           for (int j = 0; j < sf.links.size(); j++)
1583           {
1584             links.addElement(sf.links.elementAt(j));
1585           }
1586         }
1587       }
1588
1589       PopupMenu pop = new PopupMenu(ap, null, links);
1590       pop.show(this, evt.getX(), evt.getY());
1591       return;
1592     }
1593
1594     if (av.cursorMode)
1595     {
1596       seqCanvas.cursorX = findRes(evt);
1597       seqCanvas.cursorY = findSeq(evt);
1598       seqCanvas.repaint();
1599       return;
1600     }
1601
1602     if (stretchGroup == null)
1603     {
1604       // Only if left mouse button do we want to change group sizes
1605
1606       // define a new group here
1607       SequenceGroup sg = new SequenceGroup();
1608       sg.setStartRes(res);
1609       sg.setEndRes(res);
1610       sg.addSequence(sequence, false);
1611       av.setSelectionGroup(sg);
1612
1613       stretchGroup = sg;
1614
1615       if (av.getConservationSelected())
1616       {
1617         SliderPanel.setConservationSlider(ap, av.getGlobalColourScheme(),
1618                 "Background");
1619       }
1620
1621       if (av.getAbovePIDThreshold())
1622       {
1623         SliderPanel.setPIDSliderSource(ap, av.getGlobalColourScheme(),
1624                 "Background");
1625       }
1626       if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
1627       {
1628         // Edit end res position of selected group
1629         changeEndRes = true;
1630       }
1631       else if ((stretchGroup != null)
1632               && (stretchGroup.getStartRes() == res))
1633       {
1634         // Edit end res position of selected group
1635         changeStartRes = true;
1636       }
1637       stretchGroup.getWidth();
1638     }
1639
1640     seqCanvas.repaint();
1641   }
1642
1643   /**
1644    * DOCUMENT ME!
1645    * 
1646    * @param evt
1647    *          DOCUMENT ME!
1648    */
1649   public void doMouseReleasedDefineMode(MouseEvent evt)
1650   {
1651     if (stretchGroup == null)
1652     {
1653       return;
1654     }
1655
1656     stretchGroup.recalcConservation(); // always do this - annotation has own
1657                                        // state
1658     if (stretchGroup.cs != null)
1659     {
1660       stretchGroup.cs.alignmentChanged(stretchGroup,
1661               av.getHiddenRepSequences());
1662
1663       if (stretchGroup.cs.conservationApplied())
1664       {
1665         SliderPanel.setConservationSlider(ap, stretchGroup.cs,
1666                 stretchGroup.getName());
1667       }
1668       else
1669       {
1670         SliderPanel.setPIDSliderSource(ap, stretchGroup.cs,
1671                 stretchGroup.getName());
1672       }
1673     }
1674     PaintRefresher.Refresh(this, av.getSequenceSetId());
1675     ap.paintAlignment(true);
1676
1677     changeEndRes = false;
1678     changeStartRes = false;
1679     stretchGroup = null;
1680     av.sendSelection();
1681   }
1682
1683   /**
1684    * DOCUMENT ME!
1685    * 
1686    * @param evt
1687    *          DOCUMENT ME!
1688    */
1689   public void doMouseDraggedDefineMode(MouseEvent evt)
1690   {
1691     int res = findRes(evt);
1692     int y = findSeq(evt);
1693
1694     if (wrappedBlock != startWrapBlock)
1695     {
1696       return;
1697     }
1698
1699     if (stretchGroup == null)
1700     {
1701       return;
1702     }
1703
1704     if (res >= av.getAlignment().getWidth())
1705     {
1706       res = av.getAlignment().getWidth() - 1;
1707     }
1708
1709     if (stretchGroup.getEndRes() == res)
1710     {
1711       // Edit end res position of selected group
1712       changeEndRes = true;
1713     }
1714     else if (stretchGroup.getStartRes() == res)
1715     {
1716       // Edit start res position of selected group
1717       changeStartRes = true;
1718     }
1719
1720     if (res < av.getStartRes())
1721     {
1722       res = av.getStartRes();
1723     }
1724
1725     if (changeEndRes)
1726     {
1727       if (res > (stretchGroup.getStartRes() - 1))
1728       {
1729         stretchGroup.setEndRes(res);
1730       }
1731     }
1732     else if (changeStartRes)
1733     {
1734       if (res < (stretchGroup.getEndRes() + 1))
1735       {
1736         stretchGroup.setStartRes(res);
1737       }
1738     }
1739
1740     int dragDirection = 0;
1741
1742     if (y > oldSeq)
1743     {
1744       dragDirection = 1;
1745     }
1746     else if (y < oldSeq)
1747     {
1748       dragDirection = -1;
1749     }
1750
1751     while ((y != oldSeq) && (oldSeq > -1)
1752             && (y < av.getAlignment().getHeight()))
1753     {
1754       // This routine ensures we don't skip any sequences, as the
1755       // selection is quite slow.
1756       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1757
1758       oldSeq += dragDirection;
1759
1760       if (oldSeq < 0)
1761       {
1762         break;
1763       }
1764
1765       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1766
1767       if (stretchGroup.getSequences(null).contains(nextSeq))
1768       {
1769         stretchGroup.deleteSequence(seq, false);
1770       }
1771       else
1772       {
1773         if (seq != null)
1774         {
1775           stretchGroup.addSequence(seq, false);
1776         }
1777
1778         stretchGroup.addSequence(nextSeq, false);
1779       }
1780     }
1781
1782     if (oldSeq < 0)
1783     {
1784       oldSeq = -1;
1785     }
1786
1787     mouseDragging = true;
1788
1789     if (scrollThread != null)
1790     {
1791       scrollThread.setEvent(evt);
1792     }
1793
1794     seqCanvas.repaint();
1795   }
1796
1797   void scrollCanvas(MouseEvent evt)
1798   {
1799     if (evt == null)
1800     {
1801       if (scrollThread != null)
1802       {
1803         scrollThread.running = false;
1804         scrollThread = null;
1805       }
1806       mouseDragging = false;
1807     }
1808     else
1809     {
1810       if (scrollThread == null)
1811       {
1812         scrollThread = new ScrollThread();
1813       }
1814
1815       mouseDragging = true;
1816       scrollThread.setEvent(evt);
1817     }
1818
1819   }
1820
1821   // this class allows scrolling off the bottom of the visible alignment
1822   class ScrollThread extends Thread
1823   {
1824     MouseEvent evt;
1825
1826     boolean running = false;
1827
1828     public ScrollThread()
1829     {
1830       start();
1831     }
1832
1833     public void setEvent(MouseEvent e)
1834     {
1835       evt = e;
1836     }
1837
1838     public void stopScrolling()
1839     {
1840       running = false;
1841     }
1842
1843     @Override
1844     public void run()
1845     {
1846       running = true;
1847
1848       while (running)
1849       {
1850         if (evt != null)
1851         {
1852           if (mouseDragging && (evt.getY() < 0) && (av.getStartSeq() > 0))
1853           {
1854             running = ap.scrollUp(true);
1855           }
1856
1857           if (mouseDragging && (evt.getY() >= getHeight())
1858                   && (av.getAlignment().getHeight() > av.getEndSeq()))
1859           {
1860             running = ap.scrollUp(false);
1861           }
1862
1863           if (mouseDragging && (evt.getX() < 0))
1864           {
1865             running = ap.scrollRight(false);
1866           }
1867           else if (mouseDragging && (evt.getX() >= getWidth()))
1868           {
1869             running = ap.scrollRight(true);
1870           }
1871         }
1872
1873         try
1874         {
1875           Thread.sleep(20);
1876         } catch (Exception ex)
1877         {
1878         }
1879       }
1880     }
1881   }
1882
1883   /**
1884    * modify current selection according to a received message.
1885    */
1886   @Override
1887   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
1888           SelectionSource source)
1889   {
1890     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
1891     // handles selection messages...
1892     // TODO: extend config options to allow user to control if selections may be
1893     // shared between viewports.
1894     boolean iSentTheSelection = (av == source
1895             || (source instanceof AlignViewport && ((AlignmentViewport) source)
1896             .getSequenceSetId().equals(av.getSequenceSetId())));
1897     if (iSentTheSelection || !av.followSelection)
1898     {
1899       return;
1900     }
1901
1902     /*
1903      * Ignore the selection if there is one of our own pending.
1904      */
1905     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
1906     {
1907       return;
1908     }
1909
1910     /*
1911      * Check for selection in a view of which this one is a dna/protein
1912      * complement.
1913      */
1914     if (selectionFromTranslation(seqsel, colsel, source))
1915     {
1916       return;
1917     }
1918
1919     // do we want to thread this ? (contention with seqsel and colsel locks, I
1920     // suspect)
1921     // rules are: colsel is copied if there is a real intersection between
1922     // sequence selection
1923     boolean repaint = false;
1924     boolean copycolsel = true;
1925
1926     SequenceGroup sgroup = null;
1927     if (seqsel != null && seqsel.getSize() > 0)
1928     {
1929       if (av.getAlignment() == null)
1930       {
1931         Cache.log.warn("alignviewport av SeqSetId="
1932                 + av.getSequenceSetId() + " ViewId=" + av.getViewId()
1933                 + " 's alignment is NULL! returning immediately.");
1934         return;
1935       }
1936       sgroup = seqsel.intersect(av.getAlignment(),
1937               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
1938       if ((sgroup == null || sgroup.getSize() == 0)
1939               || (colsel == null || colsel.size() == 0))
1940       {
1941         // don't copy columns if the region didn't intersect.
1942         copycolsel = false;
1943       }
1944     }
1945     if (sgroup != null && sgroup.getSize() > 0)
1946     {
1947       av.setSelectionGroup(sgroup);
1948     }
1949     else
1950     {
1951       av.setSelectionGroup(null);
1952     }
1953     av.isSelectionGroupChanged(true);
1954     repaint = true;
1955
1956     if (copycolsel)
1957     {
1958       // the current selection is unset or from a previous message
1959       // so import the new colsel.
1960       if (colsel == null || colsel.size() == 0)
1961       {
1962         if (av.getColumnSelection() != null)
1963         {
1964           av.getColumnSelection().clear();
1965           repaint = true;
1966         }
1967       }
1968       else
1969       {
1970         // TODO: shift colSel according to the intersecting sequences
1971         if (av.getColumnSelection() == null)
1972         {
1973           av.setColumnSelection(new ColumnSelection(colsel));
1974         }
1975         else
1976         {
1977           av.getColumnSelection().setElementsFrom(colsel);
1978         }
1979       }
1980       av.isColSelChanged(true);
1981       repaint = true;
1982     }
1983
1984     if (copycolsel
1985             && av.hasHiddenColumns()
1986             && (av.getColumnSelection() == null || av.getColumnSelection()
1987                     .getHiddenColumns() == null))
1988     {
1989       System.err.println("Bad things");
1990     }
1991     if (repaint) // always true!
1992     {
1993       // probably finessing with multiple redraws here
1994       PaintRefresher.Refresh(this, av.getSequenceSetId());
1995       // ap.paintAlignment(false);
1996     }
1997   }
1998
1999   /**
2000    * If this panel is a cdna/protein translation view of the selection source,
2001    * tries to map the source selection to a local one, and returns true. Else
2002    * returns false.
2003    * 
2004    * @param seqsel
2005    * @param colsel
2006    * @param source
2007    */
2008   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2009           ColumnSelection colsel, SelectionSource source)
2010   {
2011     if (!(source instanceof AlignViewportI)) {
2012       return false;
2013     }
2014     final AlignViewportI sourceAv = (AlignViewportI) source;
2015     if (sourceAv.getCodingComplement() != av && av.getCodingComplement() != sourceAv)
2016     {
2017       return false;
2018     }
2019
2020     /*
2021      * Map sequence selection
2022      */
2023     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2024     av.setSelectionGroup(sg);
2025     av.isSelectionGroupChanged(true);
2026
2027     /*
2028      * Map column selection
2029      */
2030     ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2031             av);
2032     av.setColumnSelection(cs);
2033     av.isColSelChanged(true);
2034
2035     PaintRefresher.Refresh(this, av.getSequenceSetId());
2036
2037     return true;
2038   }
2039 }