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