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