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