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