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