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