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