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