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