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