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