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