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