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