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