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