JAL-2089 patch broken merge to master for Release 2.10.0b1
[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.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     boolean isControlDown = Platform.isControlDown(evt);
620     if (evt.isShiftDown() || isControlDown)
621     {
622       editingSeqs = true;
623       if (isControlDown)
624       {
625         groupEditing = true;
626       }
627     }
628     else
629     {
630       doMousePressedDefineMode(evt);
631       return;
632     }
633
634     int seq = findSeq(evt);
635     int res = findRes(evt);
636
637     if (seq < 0 || res < 0)
638     {
639       return;
640     }
641
642     if ((seq < av.getAlignment().getHeight())
643             && (res < av.getAlignment().getSequenceAt(seq).getLength()))
644     {
645       startseq = seq;
646       lastres = res;
647     }
648     else
649     {
650       startseq = -1;
651       lastres = -1;
652     }
653
654     return;
655   }
656
657   String lastMessage;
658
659   @Override
660   public void mouseOverSequence(SequenceI sequence, int index, int pos)
661   {
662     String tmp = sequence.hashCode() + " " + index + " " + pos;
663
664     if (lastMessage == null || !lastMessage.equals(tmp))
665     {
666       // System.err.println("mouseOver Sequence: "+tmp);
667       ssm.mouseOverSequence(sequence, index, pos, av);
668     }
669     lastMessage = tmp;
670   }
671
672   /**
673    * Highlight the mapped region described by the search results object (unless
674    * unchanged). This supports highlight of protein while mousing over linked
675    * cDNA and vice versa. The status bar is also updated to show the location of
676    * the start of the highlighted region.
677    */
678   @Override
679   public void highlightSequence(SearchResults results)
680   {
681     if (results == null || results.equals(lastSearchResults))
682     {
683       return;
684     }
685     lastSearchResults = results;
686
687     if (av.isFollowHighlight())
688     {
689       /*
690        * if scrollToPosition requires a scroll adjustment, this flag prevents
691        * another scroll event being propagated back to the originator
692        * 
693        * @see AlignmentPanel#adjustmentValueChanged
694        */
695       ap.setDontScrollComplement(true);
696       if (ap.scrollToPosition(results, false))
697       {
698         seqCanvas.revalidate();
699       }
700     }
701     setStatusMessage(results);
702     seqCanvas.highlightSearchResults(results);
703   }
704
705   @Override
706   public VamsasSource getVamsasSource()
707   {
708     return this.ap == null ? null : this.ap.av;
709   }
710
711   @Override
712   public void updateColours(SequenceI seq, int index)
713   {
714     System.out.println("update the seqPanel colours");
715     // repaint();
716   }
717
718   /**
719    * DOCUMENT ME!
720    * 
721    * @param evt
722    *          DOCUMENT ME!
723    */
724   @Override
725   public void mouseMoved(MouseEvent evt)
726   {
727     if (editingSeqs)
728     {
729       // This is because MacOSX creates a mouseMoved
730       // If control is down, other platforms will not.
731       mouseDragged(evt);
732     }
733
734     int res = findRes(evt);
735     int seq = findSeq(evt);
736     int pos;
737     if (res < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
738     {
739       return;
740     }
741
742     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
743
744     if (res >= sequence.getLength())
745     {
746       return;
747     }
748
749     pos = setStatusMessage(sequence, res, seq);
750     if (ssm != null && pos > -1)
751     {
752       mouseOverSequence(sequence, res, pos);
753     }
754
755     tooltipText.setLength(6); // Cuts the buffer back to <html>
756
757     SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
758     if (groups != null)
759     {
760       for (int g = 0; g < groups.length; g++)
761       {
762         if (groups[g].getStartRes() <= res && groups[g].getEndRes() >= res)
763         {
764           if (!groups[g].getName().startsWith("JTreeGroup")
765                   && !groups[g].getName().startsWith("JGroup"))
766           {
767             tooltipText.append(groups[g].getName());
768           }
769
770           if (groups[g].getDescription() != null)
771           {
772             tooltipText.append(": " + groups[g].getDescription());
773           }
774         }
775       }
776     }
777
778     // use aa to see if the mouse pointer is on a
779     if (av.isShowSequenceFeatures())
780     {
781       int rpos;
782       List<SequenceFeature> features = ap.getFeatureRenderer()
783               .findFeaturesAtRes(sequence.getDatasetSequence(),
784                       rpos = sequence.findPosition(res));
785       seqARep.appendFeatures(tooltipText, rpos, features,
786               this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
787     }
788     if (tooltipText.length() == 6) // <html></html>
789     {
790       setToolTipText(null);
791       lastTooltip = null;
792     }
793     else
794     {
795       if (lastTooltip == null
796               || !lastTooltip.equals(tooltipText.toString()))
797       {
798         String formatedTooltipText = JvSwingUtils.wrapTooltip(true,
799                 tooltipText.toString());
800         // String formatedTooltipText = tooltipText.toString();
801         setToolTipText(formatedTooltipText);
802         lastTooltip = tooltipText.toString();
803       }
804
805     }
806
807   }
808
809   private Point lastp = null;
810
811   /*
812    * (non-Javadoc)
813    * 
814    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
815    */
816   @Override
817   public Point getToolTipLocation(MouseEvent event)
818   {
819     int x = event.getX(), w = getWidth();
820     int wdth = (w - x < 200) ? -(w / 2) : 5; // switch sides when tooltip is too
821     // close to edge
822     Point p = lastp;
823     if (!event.isShiftDown() || p == null)
824     {
825       p = (tooltipText != null && tooltipText.length() > 6) ? new Point(
826               event.getX() + wdth, event.getY() - 20) : null;
827     }
828     /*
829      * TODO: try to modify position region is not obcured by tooltip
830      */
831     return lastp = p;
832   }
833
834   String lastTooltip;
835
836   /**
837    * set when the current UI interaction has resulted in a change that requires
838    * overview shading to be recalculated. this could be changed to something
839    * more expressive that indicates what actually has changed, so selective
840    * redraws can be applied
841    */
842   private boolean needOverviewUpdate = false; // TODO: refactor to avcontroller
843
844   /**
845    * set if av.getSelectionGroup() refers to a group that is defined on the
846    * alignment view, rather than a transient selection
847    */
848   // private boolean editingDefinedGroup = false; // TODO: refactor to
849   // avcontroller or viewModel
850
851   /**
852    * Set status message in alignment panel
853    * 
854    * @param sequence
855    *          aligned sequence object
856    * @param res
857    *          alignment column
858    * @param seq
859    *          index of sequence in alignment
860    * @return position of res in sequence
861    */
862   int setStatusMessage(SequenceI sequence, int res, int seq)
863   {
864     StringBuilder text = new StringBuilder(32);
865
866     /*
867      * Sequence number (if known), and sequence name.
868      */
869     String seqno = seq == -1 ? "" : " " + (seq + 1);
870     text.append("Sequence").append(seqno).append(" ID: ")
871             .append(sequence.getName());
872
873     String residue = null;
874     /*
875      * Try to translate the display character to residue name (null for gap).
876      */
877     final String displayChar = String.valueOf(sequence.getCharAt(res));
878     if (av.getAlignment().isNucleotide())
879     {
880       residue = ResidueProperties.nucleotideName.get(displayChar);
881       if (residue != null)
882       {
883         text.append(" Nucleotide: ").append(residue);
884       }
885     }
886     else
887     {
888       residue = "X".equalsIgnoreCase(displayChar) ? "X" : ("*"
889               .equals(displayChar) ? "STOP" : ResidueProperties.aa2Triplet
890               .get(displayChar));
891       if (residue != null)
892       {
893         text.append(" Residue: ").append(residue);
894       }
895     }
896
897     int pos = -1;
898     if (residue != null)
899     {
900       pos = sequence.findPosition(res);
901       text.append(" (").append(Integer.toString(pos)).append(")");
902     }
903     ap.alignFrame.statusBar.setText(text.toString());
904     return pos;
905   }
906
907   /**
908    * Set the status bar message to highlight the first matched position in
909    * search results.
910    * 
911    * @param results
912    */
913   private void setStatusMessage(SearchResults results)
914   {
915     AlignmentI al = this.av.getAlignment();
916     int sequenceIndex = al.findIndex(results);
917     if (sequenceIndex == -1)
918     {
919       return;
920     }
921     SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
922     for (Match m : results.getResults())
923     {
924       SequenceI seq = m.getSequence();
925       if (seq.getDatasetSequence() != null)
926       {
927         seq = seq.getDatasetSequence();
928       }
929
930       if (seq == ds)
931       {
932         /*
933          * Convert position in sequence (base 1) to sequence character array
934          * index (base 0)
935          */
936         int start = m.getStart() - m.getSequence().getStart();
937         setStatusMessage(seq, start, sequenceIndex);
938         return;
939       }
940     }
941   }
942
943   /**
944    * DOCUMENT ME!
945    * 
946    * @param evt
947    *          DOCUMENT ME!
948    */
949   @Override
950   public void mouseDragged(MouseEvent evt)
951   {
952     if (mouseWheelPressed)
953     {
954       int oldWidth = av.getCharWidth();
955
956       // Which is bigger, left-right or up-down?
957       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math.abs(evt
958               .getX() - lastMousePress.getX()))
959       {
960         int fontSize = av.font.getSize();
961
962         if (evt.getY() < lastMousePress.getY())
963         {
964           fontSize--;
965         }
966         else if (evt.getY() > lastMousePress.getY())
967         {
968           fontSize++;
969         }
970
971         if (fontSize < 1)
972         {
973           fontSize = 1;
974         }
975
976         av.setFont(
977                 new Font(av.font.getName(), av.font.getStyle(), fontSize),
978                 true);
979         av.setCharWidth(oldWidth);
980         ap.fontChanged();
981       }
982       else
983       {
984         if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
985         {
986           av.setCharWidth(av.getCharWidth() - 1);
987         }
988         else if (evt.getX() > lastMousePress.getX())
989         {
990           av.setCharWidth(av.getCharWidth() + 1);
991         }
992
993         ap.paintAlignment(false);
994       }
995
996       FontMetrics fm = getFontMetrics(av.getFont());
997       av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
998
999       lastMousePress = evt.getPoint();
1000
1001       return;
1002     }
1003
1004     if (!editingSeqs)
1005     {
1006       doMouseDraggedDefineMode(evt);
1007       return;
1008     }
1009
1010     int res = findRes(evt);
1011
1012     if (res < 0)
1013     {
1014       res = 0;
1015     }
1016
1017     if ((lastres == -1) || (lastres == res))
1018     {
1019       return;
1020     }
1021
1022     if ((res < av.getAlignment().getWidth()) && (res < lastres))
1023     {
1024       // dragLeft, delete gap
1025       editSequence(false, false, res);
1026     }
1027     else
1028     {
1029       editSequence(true, false, res);
1030     }
1031
1032     mouseDragging = true;
1033     if (scrollThread != null)
1034     {
1035       scrollThread.setEvent(evt);
1036     }
1037   }
1038
1039   // TODO: Make it more clever than many booleans
1040   synchronized void editSequence(boolean insertGap, boolean editSeq,
1041           int startres)
1042   {
1043     int fixedLeft = -1;
1044     int fixedRight = -1;
1045     boolean fixedColumns = false;
1046     SequenceGroup sg = av.getSelectionGroup();
1047
1048     SequenceI seq = av.getAlignment().getSequenceAt(startseq);
1049
1050     // No group, but the sequence may represent a group
1051     if (!groupEditing && av.hasHiddenRows())
1052     {
1053       if (av.isHiddenRepSequence(seq))
1054       {
1055         sg = av.getRepresentedSequences(seq);
1056         groupEditing = true;
1057       }
1058     }
1059
1060     StringBuilder message = new StringBuilder(64);
1061     if (groupEditing)
1062     {
1063       message.append("Edit group:");
1064       if (editCommand == null)
1065       {
1066         editCommand = new EditCommand(
1067                 MessageManager.getString("action.edit_group"));
1068       }
1069     }
1070     else
1071     {
1072       message.append("Edit sequence: " + seq.getName());
1073       String label = seq.getName();
1074       if (label.length() > 10)
1075       {
1076         label = label.substring(0, 10);
1077       }
1078       if (editCommand == null)
1079       {
1080         editCommand = new EditCommand(MessageManager.formatMessage(
1081                 "label.edit_params", new String[] { label }));
1082       }
1083     }
1084
1085     if (insertGap)
1086     {
1087       message.append(" insert ");
1088     }
1089     else
1090     {
1091       message.append(" delete ");
1092     }
1093
1094     message.append(Math.abs(startres - lastres) + " gaps.");
1095     ap.alignFrame.statusBar.setText(message.toString());
1096
1097     // Are we editing within a selection group?
1098     if (groupEditing
1099             || (sg != null && sg.getSequences(av.getHiddenRepSequences())
1100                     .contains(seq)))
1101     {
1102       fixedColumns = true;
1103
1104       // sg might be null as the user may only see 1 sequence,
1105       // but the sequence represents a group
1106       if (sg == null)
1107       {
1108         if (!av.isHiddenRepSequence(seq))
1109         {
1110           endEditing();
1111           return;
1112         }
1113         sg = av.getRepresentedSequences(seq);
1114       }
1115
1116       fixedLeft = sg.getStartRes();
1117       fixedRight = sg.getEndRes();
1118
1119       if ((startres < fixedLeft && lastres >= fixedLeft)
1120               || (startres >= fixedLeft && lastres < fixedLeft)
1121               || (startres > fixedRight && lastres <= fixedRight)
1122               || (startres <= fixedRight && lastres > fixedRight))
1123       {
1124         endEditing();
1125         return;
1126       }
1127
1128       if (fixedLeft > startres)
1129       {
1130         fixedRight = fixedLeft - 1;
1131         fixedLeft = 0;
1132       }
1133       else if (fixedRight < startres)
1134       {
1135         fixedLeft = fixedRight;
1136         fixedRight = -1;
1137       }
1138     }
1139
1140     if (av.hasHiddenColumns())
1141     {
1142       fixedColumns = true;
1143       int y1 = av.getColumnSelection().getHiddenBoundaryLeft(startres);
1144       int y2 = av.getColumnSelection().getHiddenBoundaryRight(startres);
1145
1146       if ((insertGap && startres > y1 && lastres < y1)
1147               || (!insertGap && startres < y2 && lastres > y2))
1148       {
1149         endEditing();
1150         return;
1151       }
1152
1153       // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1154       // Selection spans a hidden region
1155       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1156       {
1157         if (startres >= y2)
1158         {
1159           fixedLeft = y2;
1160         }
1161         else
1162         {
1163           fixedRight = y2 - 1;
1164         }
1165       }
1166     }
1167
1168     if (groupEditing)
1169     {
1170       List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1171       int g, groupSize = vseqs.size();
1172       SequenceI[] groupSeqs = new SequenceI[groupSize];
1173       for (g = 0; g < groupSeqs.length; g++)
1174       {
1175         groupSeqs[g] = vseqs.get(g);
1176       }
1177
1178       // drag to right
1179       if (insertGap)
1180       {
1181         // If the user has selected the whole sequence, and is dragging to
1182         // the right, we can still extend the alignment and selectionGroup
1183         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1184                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1185         {
1186           sg.setEndRes(av.getAlignment().getWidth() + startres - lastres);
1187           fixedRight = sg.getEndRes();
1188         }
1189
1190         // Is it valid with fixed columns??
1191         // Find the next gap before the end
1192         // of the visible region boundary
1193         boolean blank = false;
1194         for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--)
1195         {
1196           blank = true;
1197
1198           for (g = 0; g < groupSize; g++)
1199           {
1200             for (int j = 0; j < startres - lastres; j++)
1201             {
1202               if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1203               {
1204                 blank = false;
1205                 break;
1206               }
1207             }
1208           }
1209           if (blank)
1210           {
1211             break;
1212           }
1213         }
1214
1215         if (!blank)
1216         {
1217           if (sg.getSize() == av.getAlignment().getHeight())
1218           {
1219             if ((av.hasHiddenColumns() && startres < av
1220                     .getColumnSelection().getHiddenBoundaryRight(startres)))
1221             {
1222               endEditing();
1223               return;
1224             }
1225
1226             int alWidth = av.getAlignment().getWidth();
1227             if (av.hasHiddenRows())
1228             {
1229               int hwidth = av.getAlignment().getHiddenSequences()
1230                       .getWidth();
1231               if (hwidth > alWidth)
1232               {
1233                 alWidth = hwidth;
1234               }
1235             }
1236             // We can still insert gaps if the selectionGroup
1237             // contains all the sequences
1238             sg.setEndRes(sg.getEndRes() + startres - lastres);
1239             fixedRight = alWidth + startres - lastres;
1240           }
1241           else
1242           {
1243             endEditing();
1244             return;
1245           }
1246         }
1247       }
1248
1249       // drag to left
1250       else if (!insertGap)
1251       {
1252         // / Are we able to delete?
1253         // ie are all columns blank?
1254
1255         for (g = 0; g < groupSize; g++)
1256         {
1257           for (int j = startres; j < lastres; j++)
1258           {
1259             if (groupSeqs[g].getLength() <= j)
1260             {
1261               continue;
1262             }
1263
1264             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1265             {
1266               // Not a gap, block edit not valid
1267               endEditing();
1268               return;
1269             }
1270           }
1271         }
1272       }
1273
1274       if (insertGap)
1275       {
1276         // dragging to the right
1277         if (fixedColumns && fixedRight != -1)
1278         {
1279           for (int j = lastres; j < startres; j++)
1280           {
1281             insertChar(j, groupSeqs, fixedRight);
1282           }
1283         }
1284         else
1285         {
1286           appendEdit(Action.INSERT_GAP, groupSeqs, startres, startres
1287                   - lastres);
1288         }
1289       }
1290       else
1291       {
1292         // dragging to the left
1293         if (fixedColumns && fixedRight != -1)
1294         {
1295           for (int j = lastres; j > startres; j--)
1296           {
1297             deleteChar(startres, groupSeqs, fixedRight);
1298           }
1299         }
1300         else
1301         {
1302           appendEdit(Action.DELETE_GAP, groupSeqs, startres, lastres
1303                   - startres);
1304         }
1305
1306       }
1307     }
1308     else
1309     // ///Editing a single sequence///////////
1310     {
1311       if (insertGap)
1312       {
1313         // dragging to the right
1314         if (fixedColumns && fixedRight != -1)
1315         {
1316           for (int j = lastres; j < startres; j++)
1317           {
1318             insertChar(j, new SequenceI[] { seq }, fixedRight);
1319           }
1320         }
1321         else
1322         {
1323           appendEdit(Action.INSERT_GAP, new SequenceI[] { seq }, lastres,
1324                   startres - lastres);
1325         }
1326       }
1327       else
1328       {
1329         if (!editSeq)
1330         {
1331           // dragging to the left
1332           if (fixedColumns && fixedRight != -1)
1333           {
1334             for (int j = lastres; j > startres; j--)
1335             {
1336               if (!Comparison.isGap(seq.getCharAt(startres)))
1337               {
1338                 endEditing();
1339                 break;
1340               }
1341               deleteChar(startres, new SequenceI[] { seq }, fixedRight);
1342             }
1343           }
1344           else
1345           {
1346             // could be a keyboard edit trying to delete none gaps
1347             int max = 0;
1348             for (int m = startres; m < lastres; m++)
1349             {
1350               if (!Comparison.isGap(seq.getCharAt(m)))
1351               {
1352                 break;
1353               }
1354               max++;
1355             }
1356
1357             if (max > 0)
1358             {
1359               appendEdit(Action.DELETE_GAP, new SequenceI[] { seq },
1360                       startres, max);
1361             }
1362           }
1363         }
1364         else
1365         {// insertGap==false AND editSeq==TRUE;
1366           if (fixedColumns && fixedRight != -1)
1367           {
1368             for (int j = lastres; j < startres; j++)
1369             {
1370               insertChar(j, new SequenceI[] { seq }, fixedRight);
1371             }
1372           }
1373           else
1374           {
1375             appendEdit(Action.INSERT_NUC, new SequenceI[] { seq }, lastres,
1376                     startres - lastres);
1377           }
1378         }
1379       }
1380     }
1381
1382     lastres = startres;
1383     seqCanvas.repaint();
1384   }
1385
1386   void insertChar(int j, SequenceI[] seq, int fixedColumn)
1387   {
1388     int blankColumn = fixedColumn;
1389     for (int s = 0; s < seq.length; s++)
1390     {
1391       // Find the next gap before the end of the visible region boundary
1392       // If lastCol > j, theres a boundary after the gap insertion
1393
1394       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1395       {
1396         if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1397         {
1398           // Theres a space, so break and insert the gap
1399           break;
1400         }
1401       }
1402
1403       if (blankColumn <= j)
1404       {
1405         blankColumn = fixedColumn;
1406         endEditing();
1407         return;
1408       }
1409     }
1410
1411     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1);
1412
1413     appendEdit(Action.INSERT_GAP, seq, j, 1);
1414
1415   }
1416
1417   /**
1418    * Helper method to add and perform one edit action.
1419    * 
1420    * @param action
1421    * @param seq
1422    * @param pos
1423    * @param count
1424    */
1425   protected void appendEdit(Action action, SequenceI[] seq, int pos,
1426           int count)
1427   {
1428
1429     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1430             av.getAlignment().getGapCharacter());
1431
1432     editCommand.appendEdit(edit, av.getAlignment(), true, null);
1433   }
1434
1435   void deleteChar(int j, SequenceI[] seq, int fixedColumn)
1436   {
1437
1438     appendEdit(Action.DELETE_GAP, seq, j, 1);
1439
1440     appendEdit(Action.INSERT_GAP, seq, fixedColumn, 1);
1441   }
1442
1443   /**
1444    * DOCUMENT ME!
1445    * 
1446    * @param e
1447    *          DOCUMENT ME!
1448    */
1449   @Override
1450   public void mouseEntered(MouseEvent e)
1451   {
1452     if (oldSeq < 0)
1453     {
1454       oldSeq = 0;
1455     }
1456
1457     if (scrollThread != null)
1458     {
1459       scrollThread.running = false;
1460       scrollThread = null;
1461     }
1462   }
1463
1464   /**
1465    * DOCUMENT ME!
1466    * 
1467    * @param e
1468    *          DOCUMENT ME!
1469    */
1470   @Override
1471   public void mouseExited(MouseEvent e)
1472   {
1473     if (av.getWrapAlignment())
1474     {
1475       return;
1476     }
1477
1478     if (mouseDragging)
1479     {
1480       scrollThread = new ScrollThread();
1481     }
1482   }
1483
1484   @Override
1485   public void mouseClicked(MouseEvent evt)
1486   {
1487     SequenceGroup sg = null;
1488     SequenceI sequence = av.getAlignment().getSequenceAt(findSeq(evt));
1489     if (evt.getClickCount() > 1)
1490     {
1491       sg = av.getSelectionGroup();
1492       if (sg != null && sg.getSize() == 1
1493               && sg.getEndRes() - sg.getStartRes() < 2)
1494       {
1495         av.setSelectionGroup(null);
1496       }
1497
1498       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
1499               .findFeaturesAtRes(sequence.getDatasetSequence(),
1500                       sequence.findPosition(findRes(evt)));
1501
1502       if (features != null && features.size() > 0)
1503       {
1504         SearchResults highlight = new SearchResults();
1505         highlight.addResult(sequence, features.get(0).getBegin(), features
1506                 .get(0).getEnd());
1507         seqCanvas.highlightSearchResults(highlight);
1508       }
1509       if (features != null && features.size() > 0)
1510       {
1511         seqCanvas.getFeatureRenderer().amendFeatures(
1512                 new SequenceI[] { sequence },
1513                 features.toArray(new SequenceFeature[features.size()]),
1514                 false, ap);
1515
1516         seqCanvas.highlightSearchResults(null);
1517       }
1518     }
1519   }
1520
1521   @Override
1522   public void mouseWheelMoved(MouseWheelEvent e)
1523   {
1524     e.consume();
1525     if (e.getWheelRotation() > 0)
1526     {
1527       if (e.isShiftDown())
1528       {
1529         ap.scrollRight(true);
1530
1531       }
1532       else
1533       {
1534         ap.scrollUp(false);
1535       }
1536     }
1537     else
1538     {
1539       if (e.isShiftDown())
1540       {
1541         ap.scrollRight(false);
1542       }
1543       else
1544       {
1545         ap.scrollUp(true);
1546       }
1547     }
1548     // TODO Update tooltip for new position.
1549   }
1550
1551   /**
1552    * DOCUMENT ME!
1553    * 
1554    * @param evt
1555    *          DOCUMENT ME!
1556    */
1557   public void doMousePressedDefineMode(MouseEvent evt)
1558   {
1559     final int res = findRes(evt);
1560     final int seq = findSeq(evt);
1561     oldSeq = seq;
1562     needOverviewUpdate = false;
1563
1564     startWrapBlock = wrappedBlock;
1565
1566     if (av.getWrapAlignment() && seq > av.getAlignment().getHeight())
1567     {
1568       JOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
1569               .getString("label.cannot_edit_annotations_in_wrapped_view"),
1570               MessageManager.getString("label.wrapped_view_no_edit"),
1571               JOptionPane.WARNING_MESSAGE);
1572       return;
1573     }
1574
1575     if (seq < 0 || res < 0)
1576     {
1577       return;
1578     }
1579
1580     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1581
1582     if ((sequence == null) || (res > sequence.getLength()))
1583     {
1584       return;
1585     }
1586
1587     stretchGroup = av.getSelectionGroup();
1588
1589     if (stretchGroup == null)
1590     {
1591       stretchGroup = av.getAlignment().findGroup(sequence);
1592
1593       if ((stretchGroup != null) && (res > stretchGroup.getStartRes())
1594               && (res < stretchGroup.getEndRes()))
1595       {
1596         av.setSelectionGroup(stretchGroup);
1597       }
1598       else
1599       {
1600         stretchGroup = null;
1601       }
1602     }
1603     else if (!stretchGroup.getSequences(null).contains(sequence)
1604             || (stretchGroup.getStartRes() > res)
1605             || (stretchGroup.getEndRes() < res))
1606     {
1607       stretchGroup = null;
1608
1609       SequenceGroup[] allGroups = av.getAlignment().findAllGroups(sequence);
1610
1611       if (allGroups != null)
1612       {
1613         for (int i = 0; i < allGroups.length; i++)
1614         {
1615           if ((allGroups[i].getStartRes() <= res)
1616                   && (allGroups[i].getEndRes() >= res))
1617           {
1618             stretchGroup = allGroups[i];
1619             break;
1620           }
1621         }
1622       }
1623
1624       av.setSelectionGroup(stretchGroup);
1625     }
1626
1627     if (evt.isPopupTrigger()) // Mac: mousePressed
1628     {
1629       showPopupMenu(evt);
1630       return;
1631     }
1632
1633     /*
1634      * defer right-mouse click handling to mouseReleased on Windows
1635      * (where isPopupTrigger() will answer true)
1636      * NB isRightMouseButton is also true for Cmd-click on Mac
1637      */
1638     if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
1639     {
1640       return;
1641     }
1642
1643     if (av.cursorMode)
1644     {
1645       seqCanvas.cursorX = findRes(evt);
1646       seqCanvas.cursorY = findSeq(evt);
1647       seqCanvas.repaint();
1648       return;
1649     }
1650
1651     if (stretchGroup == null)
1652     {
1653       // Only if left mouse button do we want to change group sizes
1654
1655       // define a new group here
1656       SequenceGroup sg = new SequenceGroup();
1657       sg.setStartRes(res);
1658       sg.setEndRes(res);
1659       sg.addSequence(sequence, false);
1660       av.setSelectionGroup(sg);
1661       stretchGroup = sg;
1662
1663       if (av.getConservationSelected())
1664       {
1665         SliderPanel.setConservationSlider(ap, av.getGlobalColourScheme(),
1666                 "Background");
1667       }
1668
1669       if (av.getAbovePIDThreshold())
1670       {
1671         SliderPanel.setPIDSliderSource(ap, av.getGlobalColourScheme(),
1672                 "Background");
1673       }
1674       if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
1675       {
1676         // Edit end res position of selected group
1677         changeEndRes = true;
1678       }
1679       else if ((stretchGroup != null)
1680               && (stretchGroup.getStartRes() == res))
1681       {
1682         // Edit end res position of selected group
1683         changeStartRes = true;
1684       }
1685       stretchGroup.getWidth();
1686     }
1687
1688     seqCanvas.repaint();
1689   }
1690
1691   /**
1692    * Build and show a pop-up menu at the right-click mouse position
1693    * 
1694    * @param evt
1695    * @param res
1696    * @param sequence
1697    */
1698   void showPopupMenu(MouseEvent evt)
1699   {
1700     final int res = findRes(evt);
1701     final int seq = findSeq(evt);
1702     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1703     List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
1704             .findFeaturesAtRes(sequence.getDatasetSequence(),
1705                     sequence.findPosition(res));
1706     List<String> links = new ArrayList<String>();
1707     for (SequenceFeature sf : allFeatures)
1708     {
1709       if (sf.links != null)
1710       {
1711         for (String link : sf.links)
1712         {
1713           links.add(link);
1714         }
1715       }
1716     }
1717
1718     PopupMenu pop = new PopupMenu(ap, null, links);
1719     pop.show(this, evt.getX(), evt.getY());
1720   }
1721
1722   /**
1723    * DOCUMENT ME!
1724    * 
1725    * @param evt
1726    *          DOCUMENT ME!
1727    */
1728   public void doMouseReleasedDefineMode(MouseEvent evt)
1729   {
1730     if (stretchGroup == null)
1731     {
1732       return;
1733     }
1734     // always do this - annotation has own state
1735     // but defer colourscheme update until hidden sequences are passed in
1736     boolean vischange = stretchGroup.recalcConservation(true);
1737     needOverviewUpdate |= vischange && av.isSelectionDefinedGroup();
1738     if (stretchGroup.cs != null)
1739     {
1740       stretchGroup.cs.alignmentChanged(stretchGroup,
1741               av.getHiddenRepSequences());
1742
1743       if (stretchGroup.cs.conservationApplied())
1744       {
1745         SliderPanel.setConservationSlider(ap, stretchGroup.cs,
1746                 stretchGroup.getName());
1747       }
1748       else
1749       {
1750         SliderPanel.setPIDSliderSource(ap, stretchGroup.cs,
1751                 stretchGroup.getName());
1752       }
1753     }
1754     PaintRefresher.Refresh(this, av.getSequenceSetId());
1755     ap.paintAlignment(needOverviewUpdate);
1756     needOverviewUpdate = false;
1757     changeEndRes = false;
1758     changeStartRes = false;
1759     stretchGroup = null;
1760     av.sendSelection();
1761   }
1762
1763   /**
1764    * DOCUMENT ME!
1765    * 
1766    * @param evt
1767    *          DOCUMENT ME!
1768    */
1769   public void doMouseDraggedDefineMode(MouseEvent evt)
1770   {
1771     int res = findRes(evt);
1772     int y = findSeq(evt);
1773
1774     if (wrappedBlock != startWrapBlock)
1775     {
1776       return;
1777     }
1778
1779     if (stretchGroup == null)
1780     {
1781       return;
1782     }
1783
1784     if (res >= av.getAlignment().getWidth())
1785     {
1786       res = av.getAlignment().getWidth() - 1;
1787     }
1788
1789     if (stretchGroup.getEndRes() == res)
1790     {
1791       // Edit end res position of selected group
1792       changeEndRes = true;
1793     }
1794     else if (stretchGroup.getStartRes() == res)
1795     {
1796       // Edit start res position of selected group
1797       changeStartRes = true;
1798     }
1799
1800     if (res < av.getStartRes())
1801     {
1802       res = av.getStartRes();
1803     }
1804
1805     if (changeEndRes)
1806     {
1807       if (res > (stretchGroup.getStartRes() - 1))
1808       {
1809         stretchGroup.setEndRes(res);
1810         needOverviewUpdate |= av.isSelectionDefinedGroup();
1811       }
1812     }
1813     else if (changeStartRes)
1814     {
1815       if (res < (stretchGroup.getEndRes() + 1))
1816       {
1817         stretchGroup.setStartRes(res);
1818         needOverviewUpdate |= av.isSelectionDefinedGroup();
1819       }
1820     }
1821
1822     int dragDirection = 0;
1823
1824     if (y > oldSeq)
1825     {
1826       dragDirection = 1;
1827     }
1828     else if (y < oldSeq)
1829     {
1830       dragDirection = -1;
1831     }
1832
1833     while ((y != oldSeq) && (oldSeq > -1)
1834             && (y < av.getAlignment().getHeight()))
1835     {
1836       // This routine ensures we don't skip any sequences, as the
1837       // selection is quite slow.
1838       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1839
1840       oldSeq += dragDirection;
1841
1842       if (oldSeq < 0)
1843       {
1844         break;
1845       }
1846
1847       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1848
1849       if (stretchGroup.getSequences(null).contains(nextSeq))
1850       {
1851         stretchGroup.deleteSequence(seq, false);
1852         needOverviewUpdate |= av.isSelectionDefinedGroup();
1853       }
1854       else
1855       {
1856         if (seq != null)
1857         {
1858           stretchGroup.addSequence(seq, false);
1859         }
1860
1861         stretchGroup.addSequence(nextSeq, false);
1862         needOverviewUpdate |= av.isSelectionDefinedGroup();
1863       }
1864     }
1865
1866     if (oldSeq < 0)
1867     {
1868       oldSeq = -1;
1869     }
1870
1871     mouseDragging = true;
1872
1873     if (scrollThread != null)
1874     {
1875       scrollThread.setEvent(evt);
1876     }
1877
1878     seqCanvas.repaint();
1879   }
1880
1881   void scrollCanvas(MouseEvent evt)
1882   {
1883     if (evt == null)
1884     {
1885       if (scrollThread != null)
1886       {
1887         scrollThread.running = false;
1888         scrollThread = null;
1889       }
1890       mouseDragging = false;
1891     }
1892     else
1893     {
1894       if (scrollThread == null)
1895       {
1896         scrollThread = new ScrollThread();
1897       }
1898
1899       mouseDragging = true;
1900       scrollThread.setEvent(evt);
1901     }
1902
1903   }
1904
1905   // this class allows scrolling off the bottom of the visible alignment
1906   class ScrollThread extends Thread
1907   {
1908     MouseEvent evt;
1909
1910     boolean running = false;
1911
1912     public ScrollThread()
1913     {
1914       start();
1915     }
1916
1917     public void setEvent(MouseEvent e)
1918     {
1919       evt = e;
1920     }
1921
1922     public void stopScrolling()
1923     {
1924       running = false;
1925     }
1926
1927     @Override
1928     public void run()
1929     {
1930       running = true;
1931
1932       while (running)
1933       {
1934         if (evt != null)
1935         {
1936           if (mouseDragging && (evt.getY() < 0) && (av.getStartSeq() > 0))
1937           {
1938             running = ap.scrollUp(true);
1939           }
1940
1941           if (mouseDragging && (evt.getY() >= getHeight())
1942                   && (av.getAlignment().getHeight() > av.getEndSeq()))
1943           {
1944             running = ap.scrollUp(false);
1945           }
1946
1947           if (mouseDragging && (evt.getX() < 0))
1948           {
1949             running = ap.scrollRight(false);
1950           }
1951           else if (mouseDragging && (evt.getX() >= getWidth()))
1952           {
1953             running = ap.scrollRight(true);
1954           }
1955         }
1956
1957         try
1958         {
1959           Thread.sleep(20);
1960         } catch (Exception ex)
1961         {
1962         }
1963       }
1964     }
1965   }
1966
1967   /**
1968    * modify current selection according to a received message.
1969    */
1970   @Override
1971   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
1972           SelectionSource source)
1973   {
1974     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
1975     // handles selection messages...
1976     // TODO: extend config options to allow user to control if selections may be
1977     // shared between viewports.
1978     boolean iSentTheSelection = (av == source || (source instanceof AlignViewport && ((AlignmentViewport) source)
1979             .getSequenceSetId().equals(av.getSequenceSetId())));
1980     if (iSentTheSelection || !av.followSelection)
1981     {
1982       return;
1983     }
1984
1985     /*
1986      * Ignore the selection if there is one of our own pending.
1987      */
1988     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
1989     {
1990       return;
1991     }
1992
1993     /*
1994      * Check for selection in a view of which this one is a dna/protein
1995      * complement.
1996      */
1997     if (selectionFromTranslation(seqsel, colsel, source))
1998     {
1999       return;
2000     }
2001
2002     // do we want to thread this ? (contention with seqsel and colsel locks, I
2003     // suspect)
2004     /*
2005      * only copy colsel if there is a real intersection between
2006      * sequence selection and this panel's alignment
2007      */
2008     boolean repaint = false;
2009     boolean copycolsel = false;
2010
2011     SequenceGroup sgroup = null;
2012     if (seqsel != null && seqsel.getSize() > 0)
2013     {
2014       if (av.getAlignment() == null)
2015       {
2016         Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2017                 + " ViewId=" + av.getViewId()
2018                 + " 's alignment is NULL! returning immediately.");
2019         return;
2020       }
2021       sgroup = seqsel.intersect(av.getAlignment(),
2022               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2023       if ((sgroup != null && sgroup.getSize() > 0))
2024       {
2025         copycolsel = true;
2026       }
2027     }
2028     if (sgroup != null && sgroup.getSize() > 0)
2029     {
2030       av.setSelectionGroup(sgroup);
2031     }
2032     else
2033     {
2034       av.setSelectionGroup(null);
2035     }
2036     av.isSelectionGroupChanged(true);
2037     repaint = true;
2038
2039     if (copycolsel)
2040     {
2041       // the current selection is unset or from a previous message
2042       // so import the new colsel.
2043       if (colsel == null || colsel.isEmpty())
2044       {
2045         if (av.getColumnSelection() != null)
2046         {
2047           av.getColumnSelection().clear();
2048           repaint = true;
2049         }
2050       }
2051       else
2052       {
2053         // TODO: shift colSel according to the intersecting sequences
2054         if (av.getColumnSelection() == null)
2055         {
2056           av.setColumnSelection(new ColumnSelection(colsel));
2057         }
2058         else
2059         {
2060           av.getColumnSelection().setElementsFrom(colsel);
2061         }
2062       }
2063       av.isColSelChanged(true);
2064       repaint = true;
2065     }
2066
2067     if (copycolsel
2068             && av.hasHiddenColumns()
2069             && (av.getColumnSelection() == null || av.getColumnSelection()
2070                     .getHiddenColumns() == null))
2071     {
2072       System.err.println("Bad things");
2073     }
2074     if (repaint) // always true!
2075     {
2076       // probably finessing with multiple redraws here
2077       PaintRefresher.Refresh(this, av.getSequenceSetId());
2078       // ap.paintAlignment(false);
2079     }
2080   }
2081
2082   /**
2083    * If this panel is a cdna/protein translation view of the selection source,
2084    * tries to map the source selection to a local one, and returns true. Else
2085    * returns false.
2086    * 
2087    * @param seqsel
2088    * @param colsel
2089    * @param source
2090    */
2091   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2092           ColumnSelection colsel, SelectionSource source)
2093   {
2094     if (!(source instanceof AlignViewportI))
2095     {
2096       return false;
2097     }
2098     final AlignViewportI sourceAv = (AlignViewportI) source;
2099     if (sourceAv.getCodingComplement() != av
2100             && av.getCodingComplement() != sourceAv)
2101     {
2102       return false;
2103     }
2104
2105     /*
2106      * Map sequence selection
2107      */
2108     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2109     av.setSelectionGroup(sg);
2110     av.isSelectionGroupChanged(true);
2111
2112     /*
2113      * Map column selection
2114      */
2115     ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2116             av);
2117     av.setColumnSelection(cs);
2118
2119     PaintRefresher.Refresh(this, av.getSequenceSetId());
2120
2121     return true;
2122   }
2123 }