0046206c010b7940cd5d598c3d1fd955d3bffc0c
[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.renderer.ResidueShaderI;
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 || !stretchGroup.contains(sequence, res))
1591     {
1592       stretchGroup = av.getAlignment().findGroup(sequence, res);
1593       av.setSelectionGroup(stretchGroup);
1594     }
1595
1596     if (evt.isPopupTrigger()) // Mac: mousePressed
1597     {
1598       showPopupMenu(evt);
1599       return;
1600     }
1601
1602     /*
1603      * defer right-mouse click handling to mouseReleased on Windows
1604      * (where isPopupTrigger() will answer true)
1605      * NB isRightMouseButton is also true for Cmd-click on Mac
1606      */
1607     if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
1608     {
1609       return;
1610     }
1611
1612     if (av.cursorMode)
1613     {
1614       seqCanvas.cursorX = findRes(evt);
1615       seqCanvas.cursorY = findSeq(evt);
1616       seqCanvas.repaint();
1617       return;
1618     }
1619
1620     if (stretchGroup == null)
1621     {
1622       // Only if left mouse button do we want to change group sizes
1623
1624       // define a new group here
1625       SequenceGroup sg = new SequenceGroup();
1626       sg.setStartRes(res);
1627       sg.setEndRes(res);
1628       sg.addSequence(sequence, false);
1629       av.setSelectionGroup(sg);
1630       stretchGroup = sg;
1631
1632       if (av.getConservationSelected())
1633       {
1634         SliderPanel.setConservationSlider(ap, av.getResidueShading(),
1635                 ap.getViewName());
1636       }
1637
1638       if (av.getAbovePIDThreshold())
1639       {
1640         SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
1641                 ap.getViewName());
1642       }
1643       if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
1644       {
1645         // Edit end res position of selected group
1646         changeEndRes = true;
1647       }
1648       else if ((stretchGroup != null)
1649               && (stretchGroup.getStartRes() == res))
1650       {
1651         // Edit end res position of selected group
1652         changeStartRes = true;
1653       }
1654       stretchGroup.getWidth();
1655     }
1656
1657     seqCanvas.repaint();
1658   }
1659
1660   /**
1661    * Build and show a pop-up menu at the right-click mouse position
1662    * 
1663    * @param evt
1664    * @param res
1665    * @param sequence
1666    */
1667   void showPopupMenu(MouseEvent evt)
1668   {
1669     final int res = findRes(evt);
1670     final int seq = findSeq(evt);
1671     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1672     List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
1673             .findFeaturesAtRes(sequence.getDatasetSequence(),
1674                     sequence.findPosition(res));
1675     List<String> links = new ArrayList<String>();
1676     for (SequenceFeature sf : allFeatures)
1677     {
1678       if (sf.links != null)
1679       {
1680         for (String link : sf.links)
1681         {
1682           links.add(link);
1683         }
1684       }
1685     }
1686
1687     PopupMenu pop = new PopupMenu(ap, null, links);
1688     pop.show(this, evt.getX(), evt.getY());
1689   }
1690
1691   /**
1692    * DOCUMENT ME!
1693    * 
1694    * @param evt
1695    *          DOCUMENT ME!
1696    */
1697   public void doMouseReleasedDefineMode(MouseEvent evt)
1698   {
1699     if (stretchGroup == null)
1700     {
1701       return;
1702     }
1703     // always do this - annotation has own state
1704     // but defer colourscheme update until hidden sequences are passed in
1705     boolean vischange = stretchGroup.recalcConservation(true);
1706     needOverviewUpdate |= vischange && av.isSelectionDefinedGroup();
1707     if (stretchGroup.cs != null)
1708     {
1709       stretchGroup.cs.alignmentChanged(stretchGroup,
1710               av.getHiddenRepSequences());
1711
1712       ResidueShaderI groupColourScheme = stretchGroup.getGroupColourScheme();
1713       String name = stretchGroup.getName();
1714       if (stretchGroup.cs.conservationApplied())
1715       {
1716         SliderPanel.setConservationSlider(ap, groupColourScheme, name);
1717       }
1718       if (stretchGroup.cs.getThreshold() > 0)
1719       {
1720         SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
1721       }
1722     }
1723     PaintRefresher.Refresh(this, av.getSequenceSetId());
1724     ap.paintAlignment(needOverviewUpdate);
1725     needOverviewUpdate = false;
1726     changeEndRes = false;
1727     changeStartRes = false;
1728     stretchGroup = null;
1729     av.sendSelection();
1730   }
1731
1732   /**
1733    * DOCUMENT ME!
1734    * 
1735    * @param evt
1736    *          DOCUMENT ME!
1737    */
1738   public void doMouseDraggedDefineMode(MouseEvent evt)
1739   {
1740     int res = findRes(evt);
1741     int y = findSeq(evt);
1742
1743     if (wrappedBlock != startWrapBlock)
1744     {
1745       return;
1746     }
1747
1748     if (stretchGroup == null)
1749     {
1750       return;
1751     }
1752
1753     if (res >= av.getAlignment().getWidth())
1754     {
1755       res = av.getAlignment().getWidth() - 1;
1756     }
1757
1758     if (stretchGroup.getEndRes() == res)
1759     {
1760       // Edit end res position of selected group
1761       changeEndRes = true;
1762     }
1763     else if (stretchGroup.getStartRes() == res)
1764     {
1765       // Edit start res position of selected group
1766       changeStartRes = true;
1767     }
1768
1769     if (res < av.getStartRes())
1770     {
1771       res = av.getStartRes();
1772     }
1773
1774     if (changeEndRes)
1775     {
1776       if (res > (stretchGroup.getStartRes() - 1))
1777       {
1778         stretchGroup.setEndRes(res);
1779         needOverviewUpdate |= av.isSelectionDefinedGroup();
1780       }
1781     }
1782     else if (changeStartRes)
1783     {
1784       if (res < (stretchGroup.getEndRes() + 1))
1785       {
1786         stretchGroup.setStartRes(res);
1787         needOverviewUpdate |= av.isSelectionDefinedGroup();
1788       }
1789     }
1790
1791     int dragDirection = 0;
1792
1793     if (y > oldSeq)
1794     {
1795       dragDirection = 1;
1796     }
1797     else if (y < oldSeq)
1798     {
1799       dragDirection = -1;
1800     }
1801
1802     while ((y != oldSeq) && (oldSeq > -1)
1803             && (y < av.getAlignment().getHeight()))
1804     {
1805       // This routine ensures we don't skip any sequences, as the
1806       // selection is quite slow.
1807       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1808
1809       oldSeq += dragDirection;
1810
1811       if (oldSeq < 0)
1812       {
1813         break;
1814       }
1815
1816       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1817
1818       if (stretchGroup.getSequences(null).contains(nextSeq))
1819       {
1820         stretchGroup.deleteSequence(seq, false);
1821         needOverviewUpdate |= av.isSelectionDefinedGroup();
1822       }
1823       else
1824       {
1825         if (seq != null)
1826         {
1827           stretchGroup.addSequence(seq, false);
1828         }
1829
1830         stretchGroup.addSequence(nextSeq, false);
1831         needOverviewUpdate |= av.isSelectionDefinedGroup();
1832       }
1833     }
1834
1835     if (oldSeq < 0)
1836     {
1837       oldSeq = -1;
1838     }
1839
1840     mouseDragging = true;
1841
1842     if (scrollThread != null)
1843     {
1844       scrollThread.setEvent(evt);
1845     }
1846
1847     seqCanvas.repaint();
1848   }
1849
1850   void scrollCanvas(MouseEvent evt)
1851   {
1852     if (evt == null)
1853     {
1854       if (scrollThread != null)
1855       {
1856         scrollThread.running = false;
1857         scrollThread = null;
1858       }
1859       mouseDragging = false;
1860     }
1861     else
1862     {
1863       if (scrollThread == null)
1864       {
1865         scrollThread = new ScrollThread();
1866       }
1867
1868       mouseDragging = true;
1869       scrollThread.setEvent(evt);
1870     }
1871
1872   }
1873
1874   // this class allows scrolling off the bottom of the visible alignment
1875   class ScrollThread extends Thread
1876   {
1877     MouseEvent evt;
1878
1879     boolean running = false;
1880
1881     public ScrollThread()
1882     {
1883       start();
1884     }
1885
1886     public void setEvent(MouseEvent e)
1887     {
1888       evt = e;
1889     }
1890
1891     public void stopScrolling()
1892     {
1893       running = false;
1894     }
1895
1896     @Override
1897     public void run()
1898     {
1899       running = true;
1900
1901       while (running)
1902       {
1903         if (evt != null)
1904         {
1905           if (mouseDragging && (evt.getY() < 0) && (av.getStartSeq() > 0))
1906           {
1907             running = ap.scrollUp(true);
1908           }
1909
1910           if (mouseDragging && (evt.getY() >= getHeight())
1911                   && (av.getAlignment().getHeight() > av.getEndSeq()))
1912           {
1913             running = ap.scrollUp(false);
1914           }
1915
1916           if (mouseDragging && (evt.getX() < 0))
1917           {
1918             running = ap.scrollRight(false);
1919           }
1920           else if (mouseDragging && (evt.getX() >= getWidth()))
1921           {
1922             running = ap.scrollRight(true);
1923           }
1924         }
1925
1926         try
1927         {
1928           Thread.sleep(20);
1929         } catch (Exception ex)
1930         {
1931         }
1932       }
1933     }
1934   }
1935
1936   /**
1937    * modify current selection according to a received message.
1938    */
1939   @Override
1940   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
1941           SelectionSource source)
1942   {
1943     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
1944     // handles selection messages...
1945     // TODO: extend config options to allow user to control if selections may be
1946     // shared between viewports.
1947     boolean iSentTheSelection = (av == source || (source instanceof AlignViewport && ((AlignmentViewport) source)
1948             .getSequenceSetId().equals(av.getSequenceSetId())));
1949     if (iSentTheSelection || !av.followSelection)
1950     {
1951       return;
1952     }
1953
1954     /*
1955      * Ignore the selection if there is one of our own pending.
1956      */
1957     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
1958     {
1959       return;
1960     }
1961
1962     /*
1963      * Check for selection in a view of which this one is a dna/protein
1964      * complement.
1965      */
1966     if (selectionFromTranslation(seqsel, colsel, source))
1967     {
1968       return;
1969     }
1970
1971     // do we want to thread this ? (contention with seqsel and colsel locks, I
1972     // suspect)
1973     /*
1974      * only copy colsel if there is a real intersection between
1975      * sequence selection and this panel's alignment
1976      */
1977     boolean repaint = false;
1978     boolean copycolsel = false;
1979
1980     SequenceGroup sgroup = null;
1981     if (seqsel != null && seqsel.getSize() > 0)
1982     {
1983       if (av.getAlignment() == null)
1984       {
1985         Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
1986                 + " ViewId=" + av.getViewId()
1987                 + " 's alignment is NULL! returning immediately.");
1988         return;
1989       }
1990       sgroup = seqsel.intersect(av.getAlignment(),
1991               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
1992       if ((sgroup != null && sgroup.getSize() > 0))
1993       {
1994         copycolsel = true;
1995       }
1996     }
1997     if (sgroup != null && sgroup.getSize() > 0)
1998     {
1999       av.setSelectionGroup(sgroup);
2000     }
2001     else
2002     {
2003       av.setSelectionGroup(null);
2004     }
2005     av.isSelectionGroupChanged(true);
2006     repaint = true;
2007
2008     if (copycolsel)
2009     {
2010       // the current selection is unset or from a previous message
2011       // so import the new colsel.
2012       if (colsel == null || colsel.isEmpty())
2013       {
2014         if (av.getColumnSelection() != null)
2015         {
2016           av.getColumnSelection().clear();
2017           repaint = true;
2018         }
2019       }
2020       else
2021       {
2022         // TODO: shift colSel according to the intersecting sequences
2023         if (av.getColumnSelection() == null)
2024         {
2025           av.setColumnSelection(new ColumnSelection(colsel));
2026         }
2027         else
2028         {
2029           av.getColumnSelection().setElementsFrom(colsel);
2030         }
2031       }
2032       av.isColSelChanged(true);
2033       repaint = true;
2034     }
2035
2036     if (copycolsel
2037             && av.hasHiddenColumns()
2038             && (av.getColumnSelection() == null || av.getColumnSelection()
2039                     .getHiddenColumns() == null))
2040     {
2041       System.err.println("Bad things");
2042     }
2043     if (repaint) // always true!
2044     {
2045       // probably finessing with multiple redraws here
2046       PaintRefresher.Refresh(this, av.getSequenceSetId());
2047       // ap.paintAlignment(false);
2048     }
2049   }
2050
2051   /**
2052    * If this panel is a cdna/protein translation view of the selection source,
2053    * tries to map the source selection to a local one, and returns true. Else
2054    * returns false.
2055    * 
2056    * @param seqsel
2057    * @param colsel
2058    * @param source
2059    */
2060   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2061           ColumnSelection colsel, SelectionSource source)
2062   {
2063     if (!(source instanceof AlignViewportI))
2064     {
2065       return false;
2066     }
2067     final AlignViewportI sourceAv = (AlignViewportI) source;
2068     if (sourceAv.getCodingComplement() != av
2069             && av.getCodingComplement() != sourceAv)
2070     {
2071       return false;
2072     }
2073
2074     /*
2075      * Map sequence selection
2076      */
2077     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2078     av.setSelectionGroup(sg);
2079     av.isSelectionGroupChanged(true);
2080
2081     /*
2082      * Map column selection
2083      */
2084     ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2085             av);
2086     av.setColumnSelection(cs);
2087
2088     PaintRefresher.Refresh(this, av.getSequenceSetId());
2089
2090     return true;
2091   }
2092 }