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