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