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