5eb97e0c0291736916a0a94f27ef919d435fa531
[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(6); // Cuts the buffer back to <html>
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() == 6) // <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         
882         lastTooltip = textString;
883       }
884     }
885   }
886
887   
888   private Point lastp = null;
889
890   private JToolTip tempTip = new JLabel().createToolTip();
891
892   /*
893    * (non-Javadoc)
894    * 
895    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
896    */
897   @Override
898   public Point getToolTipLocation(MouseEvent event)
899   {
900     // BH 2018
901
902     if (tooltipText == null || tooltipText.length() == 6)
903       return null;
904
905     if (lastp != null && event.isShiftDown())
906       return lastp;
907
908     Point p = lastp;
909     int x = event.getX();
910     int y = event.getY();
911     int w = getWidth();
912
913     tempTip.setTipText(formattedTooltipText);
914     int tipWidth = (int) tempTip.getPreferredSize().getWidth();
915     
916     // was      x += (w - x < 200) ? -(w / 2) : 5;
917     x = (x + tipWidth < w ? x + 10 : w - tipWidth);
918     p = new Point(x, y + 20); // BH 2018 was - 20?
919     /*
920      * TODO: try to modify position region is not obcured by tooltip
921      * 
922      * Done? 
923      */
924
925     return lastp = p;
926   }
927
928   String lastTooltip;
929
930   /**
931    * set when the current UI interaction has resulted in a change that requires
932    * shading in overviews and structures to be recalculated. this could be
933    * changed to a something more expressive that indicates what actually has
934    * changed, so selective redraws can be applied (ie. only structures, only
935    * overview, etc)
936    */
937   private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
938
939   /**
940    * set if av.getSelectionGroup() refers to a group that is defined on the
941    * alignment view, rather than a transient selection
942    */
943   // private boolean editingDefinedGroup = false; // TODO: refactor to
944   // avcontroller or viewModel
945
946   /**
947    * Sets the status message in alignment panel, showing the sequence number
948    * (index) and id, and residue and residue position if not at a gap, for the
949    * given sequence and column position. Returns the residue position returned
950    * by Sequence.findPosition. Note this may be for the nearest adjacent residue
951    * if at a gapped position.
952    * 
953    * @param sequence
954    *          aligned sequence object
955    * @param column
956    *          alignment column
957    * @param seqIndex
958    *          index of sequence in alignment
959    * @return sequence position of residue at column, or adjacent residue if at a
960    *         gap
961    */
962   int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
963   {
964     char sequenceChar = sequence.getCharAt(column);
965     int pos = sequence.findPosition(column);
966     setStatusMessage(sequence, seqIndex, sequenceChar, pos);
967
968     return pos;
969   }
970
971   /**
972    * Builds the status message for the current cursor location and writes it to
973    * the status bar, for example
974    * 
975    * <pre>
976    * Sequence 3 ID: FER1_SOLLC
977    * Sequence 5 ID: FER1_PEA Residue: THR (4)
978    * Sequence 5 ID: FER1_PEA Residue: B (3)
979    * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
980    * </pre>
981    * 
982    * @param sequence
983    * @param seqIndex
984    *          sequence position in the alignment (1..)
985    * @param sequenceChar
986    *          the character under the cursor
987    * @param residuePos
988    *          the sequence residue position (if not over a gap)
989    */
990   protected void setStatusMessage(SequenceI sequence, int seqIndex,
991           char sequenceChar, int residuePos)
992   {
993     StringBuilder text = new StringBuilder(32);
994
995     /*
996      * Sequence number (if known), and sequence name.
997      */
998     String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
999     text.append("Sequence").append(seqno).append(" ID: ")
1000             .append(sequence.getName());
1001
1002     String residue = null;
1003
1004     /*
1005      * Try to translate the display character to residue name (null for gap).
1006      */
1007     boolean isGapped = Comparison.isGap(sequenceChar);
1008
1009     if (!isGapped)
1010     {
1011       boolean nucleotide = av.getAlignment().isNucleotide();
1012       String displayChar = String.valueOf(sequenceChar);
1013       if (nucleotide)
1014       {
1015         residue = ResidueProperties.nucleotideName.get(displayChar);
1016       }
1017       else
1018       {
1019         residue = "X".equalsIgnoreCase(displayChar) ? "X"
1020                 : ("*".equals(displayChar) ? "STOP"
1021                         : ResidueProperties.aa2Triplet.get(displayChar));
1022       }
1023       text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1024               .append(": ").append(residue == null ? displayChar : residue);
1025
1026       text.append(" (").append(Integer.toString(residuePos)).append(")");
1027     }
1028     ap.alignFrame.setStatus(text.toString());
1029   }
1030
1031   /**
1032    * Set the status bar message to highlight the first matched position in
1033    * search results.
1034    * 
1035    * @param results
1036    */
1037   private void setStatusMessage(SearchResultsI results)
1038   {
1039     AlignmentI al = this.av.getAlignment();
1040     int sequenceIndex = al.findIndex(results);
1041     if (sequenceIndex == -1)
1042     {
1043       return;
1044     }
1045     SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1046     for (SearchResultMatchI m : results.getResults())
1047     {
1048       SequenceI seq = m.getSequence();
1049       if (seq.getDatasetSequence() != null)
1050       {
1051         seq = seq.getDatasetSequence();
1052       }
1053
1054       if (seq == ds)
1055       {
1056         int start = m.getStart();
1057         setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1058                 start);
1059         return;
1060       }
1061     }
1062   }
1063
1064   /**
1065    * {@inheritDoc}
1066    */
1067   @Override
1068   public void mouseDragged(MouseEvent evt)
1069   {
1070     if (mouseWheelPressed)
1071     {
1072       boolean inSplitFrame = ap.av.getCodingComplement() != null;
1073       boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1074
1075       int oldWidth = av.getCharWidth();
1076
1077       // Which is bigger, left-right or up-down?
1078       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1079               .abs(evt.getX() - lastMousePress.getX()))
1080       {
1081         /*
1082          * on drag up or down, decrement or increment font size
1083          */
1084         int fontSize = av.font.getSize();
1085         boolean fontChanged = false;
1086
1087         if (evt.getY() < lastMousePress.getY())
1088         {
1089           fontChanged = true;
1090           fontSize--;
1091         }
1092         else if (evt.getY() > lastMousePress.getY())
1093         {
1094           fontChanged = true;
1095           fontSize++;
1096         }
1097
1098         if (fontSize < 1)
1099         {
1100           fontSize = 1;
1101         }
1102
1103         if (fontChanged)
1104         {
1105           Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1106                   fontSize);
1107           av.setFont(newFont, true);
1108           av.setCharWidth(oldWidth);
1109           ap.fontChanged();
1110           if (copyChanges)
1111           {
1112             ap.av.getCodingComplement().setFont(newFont, true);
1113             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1114                     .getSplitViewContainer();
1115             splitFrame.adjustLayout();
1116             splitFrame.repaint();
1117           }
1118         }
1119       }
1120       else
1121       {
1122         /*
1123          * on drag left or right, decrement or increment character width
1124          */
1125         int newWidth = 0;
1126         if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1127         {
1128           newWidth = av.getCharWidth() - 1;
1129           av.setCharWidth(newWidth);
1130         }
1131         else if (evt.getX() > lastMousePress.getX())
1132         {
1133           newWidth = av.getCharWidth() + 1;
1134           av.setCharWidth(newWidth);
1135         }
1136         if (newWidth > 0)
1137         {
1138           ap.paintAlignment(false, false);
1139           if (copyChanges)
1140           {
1141             /*
1142              * need to ensure newWidth is set on cdna, regardless of which
1143              * panel the mouse drag happened in; protein will compute its 
1144              * character width as 1:1 or 3:1
1145              */
1146             av.getCodingComplement().setCharWidth(newWidth);
1147             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1148                     .getSplitViewContainer();
1149             splitFrame.adjustLayout();
1150             splitFrame.repaint();
1151           }
1152         }
1153       }
1154
1155       FontMetrics fm = getFontMetrics(av.getFont());
1156       av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1157
1158       lastMousePress = evt.getPoint();
1159
1160       return;
1161     }
1162
1163     if (!editingSeqs)
1164     {
1165       doMouseDraggedDefineMode(evt);
1166       return;
1167     }
1168
1169     int res = findColumn(evt);
1170
1171     if (res < 0)
1172     {
1173       res = 0;
1174     }
1175
1176     if ((lastres == -1) || (lastres == res))
1177     {
1178       return;
1179     }
1180
1181     if ((res < av.getAlignment().getWidth()) && (res < lastres))
1182     {
1183       // dragLeft, delete gap
1184       editSequence(false, false, res);
1185     }
1186     else
1187     {
1188       editSequence(true, false, res);
1189     }
1190
1191     mouseDragging = true;
1192     if (scrollThread != null)
1193     {
1194       scrollThread.setMousePosition(evt.getPoint());
1195     }
1196   }
1197
1198   // TODO: Make it more clever than many booleans
1199   synchronized void editSequence(boolean insertGap, boolean editSeq,
1200           int startres)
1201   {
1202     int fixedLeft = -1;
1203     int fixedRight = -1;
1204     boolean fixedColumns = false;
1205     SequenceGroup sg = av.getSelectionGroup();
1206
1207     SequenceI seq = av.getAlignment().getSequenceAt(startseq);
1208
1209     // No group, but the sequence may represent a group
1210     if (!groupEditing && av.hasHiddenRows())
1211     {
1212       if (av.isHiddenRepSequence(seq))
1213       {
1214         sg = av.getRepresentedSequences(seq);
1215         groupEditing = true;
1216       }
1217     }
1218
1219     StringBuilder message = new StringBuilder(64);
1220     if (groupEditing)
1221     {
1222       message.append("Edit group:");
1223       if (editCommand == null)
1224       {
1225         editCommand = new EditCommand(
1226                 MessageManager.getString("action.edit_group"));
1227       }
1228     }
1229     else
1230     {
1231       message.append("Edit sequence: " + seq.getName());
1232       String label = seq.getName();
1233       if (label.length() > 10)
1234       {
1235         label = label.substring(0, 10);
1236       }
1237       if (editCommand == null)
1238       {
1239         editCommand = new EditCommand(MessageManager
1240                 .formatMessage("label.edit_params", new String[]
1241                 { label }));
1242       }
1243     }
1244
1245     if (insertGap)
1246     {
1247       message.append(" insert ");
1248     }
1249     else
1250     {
1251       message.append(" delete ");
1252     }
1253
1254     message.append(Math.abs(startres - lastres) + " gaps.");
1255     ap.alignFrame.setStatus(message.toString());
1256
1257     // Are we editing within a selection group?
1258     if (groupEditing || (sg != null
1259             && sg.getSequences(av.getHiddenRepSequences()).contains(seq)))
1260     {
1261       fixedColumns = true;
1262
1263       // sg might be null as the user may only see 1 sequence,
1264       // but the sequence represents a group
1265       if (sg == null)
1266       {
1267         if (!av.isHiddenRepSequence(seq))
1268         {
1269           endEditing();
1270           return;
1271         }
1272         sg = av.getRepresentedSequences(seq);
1273       }
1274
1275       fixedLeft = sg.getStartRes();
1276       fixedRight = sg.getEndRes();
1277
1278       if ((startres < fixedLeft && lastres >= fixedLeft)
1279               || (startres >= fixedLeft && lastres < fixedLeft)
1280               || (startres > fixedRight && lastres <= fixedRight)
1281               || (startres <= fixedRight && lastres > fixedRight))
1282       {
1283         endEditing();
1284         return;
1285       }
1286
1287       if (fixedLeft > startres)
1288       {
1289         fixedRight = fixedLeft - 1;
1290         fixedLeft = 0;
1291       }
1292       else if (fixedRight < startres)
1293       {
1294         fixedLeft = fixedRight;
1295         fixedRight = -1;
1296       }
1297     }
1298
1299     if (av.hasHiddenColumns())
1300     {
1301       fixedColumns = true;
1302       int y1 = av.getAlignment().getHiddenColumns()
1303               .getNextHiddenBoundary(true, startres);
1304       int y2 = av.getAlignment().getHiddenColumns()
1305               .getNextHiddenBoundary(false, startres);
1306
1307       if ((insertGap && startres > y1 && lastres < y1)
1308               || (!insertGap && startres < y2 && lastres > y2))
1309       {
1310         endEditing();
1311         return;
1312       }
1313
1314       // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1315       // Selection spans a hidden region
1316       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1317       {
1318         if (startres >= y2)
1319         {
1320           fixedLeft = y2;
1321         }
1322         else
1323         {
1324           fixedRight = y2 - 1;
1325         }
1326       }
1327     }
1328
1329     if (groupEditing)
1330     {
1331       List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1332       int g, groupSize = vseqs.size();
1333       SequenceI[] groupSeqs = new SequenceI[groupSize];
1334       for (g = 0; g < groupSeqs.length; g++)
1335       {
1336         groupSeqs[g] = vseqs.get(g);
1337       }
1338
1339       // drag to right
1340       if (insertGap)
1341       {
1342         // If the user has selected the whole sequence, and is dragging to
1343         // the right, we can still extend the alignment and selectionGroup
1344         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1345                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1346         {
1347           sg.setEndRes(av.getAlignment().getWidth() + startres - lastres);
1348           fixedRight = sg.getEndRes();
1349         }
1350
1351         // Is it valid with fixed columns??
1352         // Find the next gap before the end
1353         // of the visible region boundary
1354         boolean blank = false;
1355         for (; fixedRight > lastres; fixedRight--)
1356         {
1357           blank = true;
1358
1359           for (g = 0; g < groupSize; g++)
1360           {
1361             for (int j = 0; j < startres - lastres; j++)
1362             {
1363               if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1364               {
1365                 blank = false;
1366                 break;
1367               }
1368             }
1369           }
1370           if (blank)
1371           {
1372             break;
1373           }
1374         }
1375
1376         if (!blank)
1377         {
1378           if (sg.getSize() == av.getAlignment().getHeight())
1379           {
1380             if ((av.hasHiddenColumns() && startres < av.getAlignment()
1381                     .getHiddenColumns()
1382                     .getNextHiddenBoundary(false, startres)))
1383             {
1384               endEditing();
1385               return;
1386             }
1387
1388             int alWidth = av.getAlignment().getWidth();
1389             if (av.hasHiddenRows())
1390             {
1391               int hwidth = av.getAlignment().getHiddenSequences()
1392                       .getWidth();
1393               if (hwidth > alWidth)
1394               {
1395                 alWidth = hwidth;
1396               }
1397             }
1398             // We can still insert gaps if the selectionGroup
1399             // contains all the sequences
1400             sg.setEndRes(sg.getEndRes() + startres - lastres);
1401             fixedRight = alWidth + startres - lastres;
1402           }
1403           else
1404           {
1405             endEditing();
1406             return;
1407           }
1408         }
1409       }
1410
1411       // drag to left
1412       else if (!insertGap)
1413       {
1414         // / Are we able to delete?
1415         // ie are all columns blank?
1416
1417         for (g = 0; g < groupSize; g++)
1418         {
1419           for (int j = startres; j < lastres; j++)
1420           {
1421             if (groupSeqs[g].getLength() <= j)
1422             {
1423               continue;
1424             }
1425
1426             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1427             {
1428               // Not a gap, block edit not valid
1429               endEditing();
1430               return;
1431             }
1432           }
1433         }
1434       }
1435
1436       if (insertGap)
1437       {
1438         // dragging to the right
1439         if (fixedColumns && fixedRight != -1)
1440         {
1441           for (int j = lastres; j < startres; j++)
1442           {
1443             insertChar(j, groupSeqs, fixedRight);
1444           }
1445         }
1446         else
1447         {
1448           appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1449                   startres - lastres);
1450         }
1451       }
1452       else
1453       {
1454         // dragging to the left
1455         if (fixedColumns && fixedRight != -1)
1456         {
1457           for (int j = lastres; j > startres; j--)
1458           {
1459             deleteChar(startres, groupSeqs, fixedRight);
1460           }
1461         }
1462         else
1463         {
1464           appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1465                   lastres - startres);
1466         }
1467
1468       }
1469     }
1470     else
1471     // ///Editing a single sequence///////////
1472     {
1473       if (insertGap)
1474       {
1475         // dragging to the right
1476         if (fixedColumns && fixedRight != -1)
1477         {
1478           for (int j = lastres; j < startres; j++)
1479           {
1480             insertChar(j, new SequenceI[] { seq }, fixedRight);
1481           }
1482         }
1483         else
1484         {
1485           appendEdit(Action.INSERT_GAP, new SequenceI[] { seq }, lastres,
1486                   startres - lastres);
1487         }
1488       }
1489       else
1490       {
1491         if (!editSeq)
1492         {
1493           // dragging to the left
1494           if (fixedColumns && fixedRight != -1)
1495           {
1496             for (int j = lastres; j > startres; j--)
1497             {
1498               if (!Comparison.isGap(seq.getCharAt(startres)))
1499               {
1500                 endEditing();
1501                 break;
1502               }
1503               deleteChar(startres, new SequenceI[] { seq }, fixedRight);
1504             }
1505           }
1506           else
1507           {
1508             // could be a keyboard edit trying to delete none gaps
1509             int max = 0;
1510             for (int m = startres; m < lastres; m++)
1511             {
1512               if (!Comparison.isGap(seq.getCharAt(m)))
1513               {
1514                 break;
1515               }
1516               max++;
1517             }
1518
1519             if (max > 0)
1520             {
1521               appendEdit(Action.DELETE_GAP, new SequenceI[] { seq },
1522                       startres, max);
1523             }
1524           }
1525         }
1526         else
1527         {// insertGap==false AND editSeq==TRUE;
1528           if (fixedColumns && fixedRight != -1)
1529           {
1530             for (int j = lastres; j < startres; j++)
1531             {
1532               insertChar(j, new SequenceI[] { seq }, fixedRight);
1533             }
1534           }
1535           else
1536           {
1537             appendEdit(Action.INSERT_NUC, new SequenceI[] { seq }, lastres,
1538                     startres - lastres);
1539           }
1540         }
1541       }
1542     }
1543
1544     lastres = startres;
1545     seqCanvas.repaint();
1546   }
1547
1548   void insertChar(int j, SequenceI[] seq, int fixedColumn)
1549   {
1550     int blankColumn = fixedColumn;
1551     for (int s = 0; s < seq.length; s++)
1552     {
1553       // Find the next gap before the end of the visible region boundary
1554       // If lastCol > j, theres a boundary after the gap insertion
1555
1556       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1557       {
1558         if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1559         {
1560           // Theres a space, so break and insert the gap
1561           break;
1562         }
1563       }
1564
1565       if (blankColumn <= j)
1566       {
1567         blankColumn = fixedColumn;
1568         endEditing();
1569         return;
1570       }
1571     }
1572
1573     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1);
1574
1575     appendEdit(Action.INSERT_GAP, seq, j, 1);
1576
1577   }
1578
1579   /**
1580    * Helper method to add and perform one edit action.
1581    * 
1582    * @param action
1583    * @param seq
1584    * @param pos
1585    * @param count
1586    */
1587   protected void appendEdit(Action action, SequenceI[] seq, int pos,
1588           int count)
1589   {
1590
1591     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1592             av.getAlignment().getGapCharacter());
1593
1594     editCommand.appendEdit(edit, av.getAlignment(), true, null);
1595   }
1596
1597   void deleteChar(int j, SequenceI[] seq, int fixedColumn)
1598   {
1599
1600     appendEdit(Action.DELETE_GAP, seq, j, 1);
1601
1602     appendEdit(Action.INSERT_GAP, seq, fixedColumn, 1);
1603   }
1604
1605   /**
1606    * On reentering the panel, stops any scrolling that was started on dragging
1607    * out of the panel
1608    * 
1609    * @param e
1610    */
1611   @Override
1612   public void mouseEntered(MouseEvent e)
1613   {
1614     if (oldSeq < 0)
1615     {
1616       oldSeq = 0;
1617     }
1618     stopScrolling();
1619   }
1620
1621   /**
1622    * On leaving the panel, if the mouse is being dragged, starts a thread to
1623    * scroll it until the mouse is released (in unwrapped mode only)
1624    * 
1625    * @param e
1626    */
1627   @Override
1628   public void mouseExited(MouseEvent e)
1629   {
1630     if (mouseDragging)
1631     {
1632       startScrolling(e.getPoint());
1633     }
1634   }
1635
1636   /**
1637    * Handler for double-click on a position with one or more sequence features.
1638    * Opens the Amend Features dialog to allow feature details to be amended, or
1639    * the feature deleted.
1640    */
1641   @Override
1642   public void mouseClicked(MouseEvent evt)
1643   {
1644     SequenceGroup sg = null;
1645     SequenceI sequence = av.getAlignment().getSequenceAt(findSeq(evt));
1646     if (evt.getClickCount() > 1)
1647     {
1648       sg = av.getSelectionGroup();
1649       if (sg != null && sg.getSize() == 1
1650               && sg.getEndRes() - sg.getStartRes() < 2)
1651       {
1652         av.setSelectionGroup(null);
1653       }
1654
1655       int column = findColumn(evt);
1656
1657       /*
1658        * find features at the position (if not gapped), or straddling
1659        * the position (if at a gap)
1660        */
1661       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
1662               .findFeaturesAtColumn(sequence, column + 1);
1663
1664       if (!features.isEmpty())
1665       {
1666         /*
1667          * highlight the first feature at the position on the alignment
1668          */
1669         SearchResultsI highlight = new SearchResults();
1670         highlight.addResult(sequence, features.get(0).getBegin(), features
1671                 .get(0).getEnd());
1672         seqCanvas.highlightSearchResults(highlight, true);
1673
1674         /*
1675          * open the Amend Features dialog
1676          */
1677         new FeatureEditor(ap, Collections.singletonList(sequence), features,
1678                 false).showDialog();
1679       }
1680     }
1681   }
1682
1683   @Override
1684   public void mouseWheelMoved(MouseWheelEvent e)
1685   {
1686     e.consume();
1687     double wheelRotation = e.getPreciseWheelRotation();
1688     if (wheelRotation > 0)
1689     {
1690       if (e.isShiftDown())
1691       {
1692         av.getRanges().scrollRight(true);
1693
1694       }
1695       else
1696       {
1697         av.getRanges().scrollUp(false);
1698       }
1699     }
1700     else if (wheelRotation < 0)
1701     {
1702       if (e.isShiftDown())
1703       {
1704         av.getRanges().scrollRight(false);
1705       }
1706       else
1707       {
1708         av.getRanges().scrollUp(true);
1709       }
1710     }
1711
1712     /*
1713      * update status bar and tooltip for new position
1714      * (need to synthesize a mouse movement to refresh tooltip)
1715      */
1716     mouseMoved(e);
1717     ToolTipManager.sharedInstance().mouseMoved(e);
1718   }
1719
1720   /**
1721    * DOCUMENT ME!
1722    * 
1723    * @param evt
1724    *          DOCUMENT ME!
1725    */
1726   public void doMousePressedDefineMode(MouseEvent evt)
1727   {
1728     final int res = findColumn(evt);
1729     final int seq = findSeq(evt);
1730     oldSeq = seq;
1731     updateOverviewAndStructs = false;
1732
1733     startWrapBlock = wrappedBlock;
1734
1735     if (av.getWrapAlignment() && seq > av.getAlignment().getHeight())
1736     {
1737       JvOptionPane.showInternalMessageDialog(Desktop.desktop,
1738               MessageManager.getString(
1739                       "label.cannot_edit_annotations_in_wrapped_view"),
1740               MessageManager.getString("label.wrapped_view_no_edit"),
1741               JvOptionPane.WARNING_MESSAGE);
1742       return;
1743     }
1744
1745     if (seq < 0 || res < 0)
1746     {
1747       return;
1748     }
1749
1750     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1751
1752     if ((sequence == null) || (res > sequence.getLength()))
1753     {
1754       return;
1755     }
1756
1757     stretchGroup = av.getSelectionGroup();
1758
1759     if (stretchGroup == null || !stretchGroup.contains(sequence, res))
1760     {
1761       stretchGroup = av.getAlignment().findGroup(sequence, res);
1762       if (stretchGroup != null)
1763       {
1764         // only update the current selection if the popup menu has a group to
1765         // focus on
1766         av.setSelectionGroup(stretchGroup);
1767       }
1768     }
1769
1770     if (evt.isPopupTrigger()) // Mac: mousePressed
1771     {
1772       showPopupMenu(evt);
1773       return;
1774     }
1775
1776     /*
1777      * defer right-mouse click handling to mouseReleased on Windows
1778      * (where isPopupTrigger() will answer true)
1779      * NB isRightMouseButton is also true for Cmd-click on Mac
1780      */
1781     if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
1782     {
1783       return;
1784     }
1785
1786     if (av.cursorMode)
1787     {
1788       seqCanvas.cursorX = findColumn(evt);
1789       seqCanvas.cursorY = findSeq(evt);
1790       seqCanvas.repaint();
1791       return;
1792     }
1793
1794     if (stretchGroup == null)
1795     {
1796       createStretchGroup(res, sequence);
1797     }
1798
1799     if (stretchGroup != null)
1800     {
1801       stretchGroup.addPropertyChangeListener(seqCanvas);
1802     }
1803
1804     seqCanvas.repaint();
1805   }
1806
1807   private void createStretchGroup(int res, SequenceI sequence)
1808   {
1809     // Only if left mouse button do we want to change group sizes
1810     // define a new group here
1811     SequenceGroup sg = new SequenceGroup();
1812     sg.setStartRes(res);
1813     sg.setEndRes(res);
1814     sg.addSequence(sequence, false);
1815     av.setSelectionGroup(sg);
1816     stretchGroup = sg;
1817
1818     if (av.getConservationSelected())
1819     {
1820       SliderPanel.setConservationSlider(ap, av.getResidueShading(),
1821               ap.getViewName());
1822     }
1823
1824     if (av.getAbovePIDThreshold())
1825     {
1826       SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
1827               ap.getViewName());
1828     }
1829     // TODO: stretchGroup will always be not null. Is this a merge error ?
1830     // or is there a threading issue here?
1831     if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
1832     {
1833       // Edit end res position of selected group
1834       changeEndRes = true;
1835     }
1836     else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
1837     {
1838       // Edit end res position of selected group
1839       changeStartRes = true;
1840     }
1841     stretchGroup.getWidth();
1842
1843   }
1844
1845   /**
1846    * Build and show a pop-up menu at the right-click mouse position
1847    * 
1848    * @param evt
1849    * @param res
1850    * @param sequences
1851    */
1852   void showPopupMenu(MouseEvent evt)
1853   {
1854     final int column = findColumn(evt);
1855     final int seq = findSeq(evt);
1856     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1857     List<SequenceFeature> features = ap.getFeatureRenderer()
1858             .findFeaturesAtColumn(sequence, column + 1);
1859
1860     PopupMenu pop = new PopupMenu(ap, null, features);
1861     pop.show(this, evt.getX(), evt.getY());
1862   }
1863
1864   /**
1865    * Update the display after mouse up on a selection or group
1866    * 
1867    * @param evt
1868    *          mouse released event details
1869    * @param afterDrag
1870    *          true if this event is happening after a mouse drag (rather than a
1871    *          mouse down)
1872    */
1873   public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
1874   {
1875     if (stretchGroup == null)
1876     {
1877       return;
1878     }
1879
1880     stretchGroup.removePropertyChangeListener(seqCanvas);
1881
1882     // always do this - annotation has own state
1883     // but defer colourscheme update until hidden sequences are passed in
1884     boolean vischange = stretchGroup.recalcConservation(true);
1885     updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
1886             && afterDrag;
1887     if (stretchGroup.cs != null)
1888     {
1889       stretchGroup.cs.alignmentChanged(stretchGroup,
1890               av.getHiddenRepSequences());
1891
1892       ResidueShaderI groupColourScheme = stretchGroup
1893               .getGroupColourScheme();
1894       String name = stretchGroup.getName();
1895       if (stretchGroup.cs.conservationApplied())
1896       {
1897         SliderPanel.setConservationSlider(ap, groupColourScheme, name);
1898       }
1899       if (stretchGroup.cs.getThreshold() > 0)
1900       {
1901         SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
1902       }
1903     }
1904     PaintRefresher.Refresh(this, av.getSequenceSetId());
1905     // TODO: structure colours only need updating if stretchGroup used to or now
1906     // does contain sequences with structure views
1907     ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
1908     updateOverviewAndStructs = false;
1909     changeEndRes = false;
1910     changeStartRes = false;
1911     stretchGroup = null;
1912     av.sendSelection();
1913   }
1914
1915   /**
1916    * DOCUMENT ME!
1917    * 
1918    * @param evt
1919    *          DOCUMENT ME!
1920    */
1921   public void doMouseDraggedDefineMode(MouseEvent evt)
1922   {
1923     int res = findColumn(evt);
1924     int y = findSeq(evt);
1925
1926     if (wrappedBlock != startWrapBlock)
1927     {
1928       return;
1929     }
1930
1931     if (stretchGroup == null)
1932     {
1933       return;
1934     }
1935
1936     res = Math.min(res, av.getAlignment().getWidth()-1);
1937
1938     if (stretchGroup.getEndRes() == res)
1939     {
1940       // Edit end res position of selected group
1941       changeEndRes = true;
1942     }
1943     else if (stretchGroup.getStartRes() == res)
1944     {
1945       // Edit start res position of selected group
1946       changeStartRes = true;
1947     }
1948
1949     if (res < av.getRanges().getStartRes())
1950     {
1951       res = av.getRanges().getStartRes();
1952     }
1953
1954     if (changeEndRes)
1955     {
1956       if (res > (stretchGroup.getStartRes() - 1))
1957       {
1958         stretchGroup.setEndRes(res);
1959         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
1960       }
1961     }
1962     else if (changeStartRes)
1963     {
1964       if (res < (stretchGroup.getEndRes() + 1))
1965       {
1966         stretchGroup.setStartRes(res);
1967         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
1968       }
1969     }
1970
1971     int dragDirection = 0;
1972
1973     if (y > oldSeq)
1974     {
1975       dragDirection = 1;
1976     }
1977     else if (y < oldSeq)
1978     {
1979       dragDirection = -1;
1980     }
1981
1982     while ((y != oldSeq) && (oldSeq > -1)
1983             && (y < av.getAlignment().getHeight()))
1984     {
1985       // This routine ensures we don't skip any sequences, as the
1986       // selection is quite slow.
1987       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1988
1989       oldSeq += dragDirection;
1990
1991       if (oldSeq < 0)
1992       {
1993         break;
1994       }
1995
1996       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1997
1998       if (stretchGroup.getSequences(null).contains(nextSeq))
1999       {
2000         stretchGroup.deleteSequence(seq, false);
2001         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2002       }
2003       else
2004       {
2005         if (seq != null)
2006         {
2007           stretchGroup.addSequence(seq, false);
2008         }
2009
2010         stretchGroup.addSequence(nextSeq, false);
2011         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2012       }
2013     }
2014
2015     if (oldSeq < 0)
2016     {
2017       oldSeq = -1;
2018     }
2019
2020     mouseDragging = true;
2021
2022     if (scrollThread != null)
2023     {
2024       scrollThread.setMousePosition(evt.getPoint());
2025     }
2026   }
2027
2028   /**
2029    * Stops the scroll thread if it is running
2030    */
2031   void stopScrolling()
2032   {
2033     if (scrollThread != null)
2034     {
2035       scrollThread.stopScrolling();
2036       scrollThread = null;
2037     }
2038     mouseDragging = false;
2039   }
2040
2041   /**
2042    * Starts a thread to scroll the alignment, towards a given mouse position
2043    * outside the panel bounds, unless the alignment is in wrapped mode
2044    * 
2045    * @param mousePos
2046    */
2047   void startScrolling(Point mousePos)
2048   {
2049     /*
2050      * set this.mouseDragging in case this was called from 
2051      * a drag in ScalePanel or AnnotationPanel
2052      */
2053     mouseDragging = true;
2054     if (!av.getWrapAlignment() && scrollThread == null)
2055     {
2056       scrollThread = new ScrollThread();
2057       scrollThread.setMousePosition(mousePos);
2058       if (!Jalview.isJS())
2059       {
2060         /*
2061          * Java - run in a new thread
2062          */
2063         scrollThread.start();
2064       }
2065       else
2066       {
2067         /*
2068          * Javascript - run every 20ms until scrolling stopped
2069          * or reaches the limit of scrollable alignment
2070          */
2071         // java.util.Timer version:
2072         // Timer t = new Timer("ScrollThreadTimer", true);
2073         // TimerTask task = new TimerTask()
2074         // {
2075         // @Override
2076         // public void run()
2077         // {
2078         // if (!scrollThread.scrollOnce())
2079         // {
2080         // cancel();
2081         // }
2082         // }
2083         // };
2084         // t.schedule(task, 20, 20);
2085         Timer t = new Timer(20, new ActionListener()
2086         {
2087           @Override
2088           public void actionPerformed(ActionEvent e)
2089           {
2090             if (scrollThread != null)
2091             {
2092               // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2093               scrollThread.scrollOnce();
2094             }
2095           }
2096         });
2097         t.addActionListener(new ActionListener()
2098         {
2099           @Override
2100           public void actionPerformed(ActionEvent e)
2101           {
2102             if (scrollThread == null)
2103             {
2104               // finished and nulled itself
2105               t.stop();
2106             }
2107           }
2108         });
2109         t.start();
2110       }
2111     }
2112   }
2113
2114   /**
2115    * Performs scrolling of the visible alignment left, right, up or down, until
2116    * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2117    * limit of the alignment is reached
2118    */
2119   class ScrollThread extends Thread
2120   {
2121     private Point mousePos;
2122
2123     private volatile boolean keepRunning = true;
2124
2125     /**
2126      * Constructor
2127      */
2128     public ScrollThread()
2129     {
2130       setName("SeqPanel$ScrollThread");
2131     }
2132
2133     /**
2134      * Sets the position of the mouse that determines the direction of the
2135      * scroll to perform. If this is called as the mouse moves, scrolling should
2136      * respond accordingly. For example, if the mouse is dragged right, scroll
2137      * right should start; if the drag continues down, scroll down should also
2138      * happen.
2139      * 
2140      * @param p
2141      */
2142     public void setMousePosition(Point p)
2143     {
2144       mousePos = p;
2145     }
2146
2147     /**
2148      * Sets a flag that will cause the thread to exit
2149      */
2150     public void stopScrolling()
2151     {
2152       keepRunning = false;
2153     }
2154
2155     /**
2156      * Scrolls the alignment left or right, and/or up or down, depending on the
2157      * last notified mouse position, until the limit of the alignment is
2158      * reached, or a flag is set to stop the scroll
2159      */
2160     @Override
2161     public void run()
2162     {
2163       while (keepRunning)
2164       {
2165         if (mousePos != null)
2166         {
2167           keepRunning = scrollOnce();
2168         }
2169         try
2170         {
2171           Thread.sleep(20);
2172         } catch (Exception ex)
2173         {
2174         }
2175       }
2176       SeqPanel.this.scrollThread = null;
2177     }
2178
2179     /**
2180      * Scrolls
2181      * <ul>
2182      * <li>one row up, if the mouse is above the panel</li>
2183      * <li>one row down, if the mouse is below the panel</li>
2184      * <li>one column left, if the mouse is left of the panel</li>
2185      * <li>one column right, if the mouse is right of the panel</li>
2186      * </ul>
2187      * Answers true if a scroll was performed, false if not - meaning either
2188      * that the mouse position is within the panel, or the edge of the alignment
2189      * has been reached.
2190      */
2191     boolean scrollOnce()
2192     {
2193       /*
2194        * quit after mouseUp ensures interrupt in JalviewJS
2195        */
2196       if (!mouseDragging)
2197       {
2198         return false;
2199       }
2200
2201       boolean scrolled = false;
2202       ViewportRanges ranges = SeqPanel.this.av.getRanges();
2203
2204       /*
2205        * scroll up or down
2206        */
2207       if (mousePos.y < 0)
2208       {
2209         // mouse is above this panel - try scroll up
2210         scrolled = ranges.scrollUp(true);
2211       }
2212       else if (mousePos.y >= getHeight())
2213       {
2214         // mouse is below this panel - try scroll down
2215         scrolled = ranges.scrollUp(false);
2216       }
2217
2218       /*
2219        * scroll left or right
2220        */
2221       if (mousePos.x < 0)
2222       {
2223         scrolled |= ranges.scrollRight(false);
2224       }
2225       else if (mousePos.x >= getWidth())
2226       {
2227         scrolled |= ranges.scrollRight(true);
2228       }
2229       return scrolled;
2230     }
2231   }
2232
2233   /**
2234    * modify current selection according to a received message.
2235    */
2236   @Override
2237   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2238           HiddenColumns hidden, SelectionSource source)
2239   {
2240     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2241     // handles selection messages...
2242     // TODO: extend config options to allow user to control if selections may be
2243     // shared between viewports.
2244     boolean iSentTheSelection = (av == source
2245             || (source instanceof AlignViewport
2246                     && ((AlignmentViewport) source).getSequenceSetId()
2247                             .equals(av.getSequenceSetId())));
2248
2249     if (iSentTheSelection)
2250     {
2251       // respond to our own event by updating dependent dialogs
2252       if (ap.getCalculationDialog() != null)
2253       {
2254         ap.getCalculationDialog().validateCalcTypes();
2255       }
2256
2257       return;
2258     }
2259
2260     // process further ?
2261     if (!av.followSelection)
2262     {
2263       return;
2264     }
2265
2266     /*
2267      * Ignore the selection if there is one of our own pending.
2268      */
2269     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2270     {
2271       return;
2272     }
2273
2274     /*
2275      * Check for selection in a view of which this one is a dna/protein
2276      * complement.
2277      */
2278     if (selectionFromTranslation(seqsel, colsel, hidden, source))
2279     {
2280       return;
2281     }
2282
2283     // do we want to thread this ? (contention with seqsel and colsel locks, I
2284     // suspect)
2285     /*
2286      * only copy colsel if there is a real intersection between
2287      * sequence selection and this panel's alignment
2288      */
2289     boolean repaint = false;
2290     boolean copycolsel = false;
2291
2292     SequenceGroup sgroup = null;
2293     if (seqsel != null && seqsel.getSize() > 0)
2294     {
2295       if (av.getAlignment() == null)
2296       {
2297         Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2298                 + " ViewId=" + av.getViewId()
2299                 + " 's alignment is NULL! returning immediately.");
2300         return;
2301       }
2302       sgroup = seqsel.intersect(av.getAlignment(),
2303               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2304       if ((sgroup != null && sgroup.getSize() > 0))
2305       {
2306         copycolsel = true;
2307       }
2308     }
2309     if (sgroup != null && sgroup.getSize() > 0)
2310     {
2311       av.setSelectionGroup(sgroup);
2312     }
2313     else
2314     {
2315       av.setSelectionGroup(null);
2316     }
2317     av.isSelectionGroupChanged(true);
2318     repaint = true;
2319
2320     if (copycolsel)
2321     {
2322       // the current selection is unset or from a previous message
2323       // so import the new colsel.
2324       if (colsel == null || colsel.isEmpty())
2325       {
2326         if (av.getColumnSelection() != null)
2327         {
2328           av.getColumnSelection().clear();
2329           repaint = true;
2330         }
2331       }
2332       else
2333       {
2334         // TODO: shift colSel according to the intersecting sequences
2335         if (av.getColumnSelection() == null)
2336         {
2337           av.setColumnSelection(new ColumnSelection(colsel));
2338         }
2339         else
2340         {
2341           av.getColumnSelection().setElementsFrom(colsel,
2342                   av.getAlignment().getHiddenColumns());
2343         }
2344       }
2345       av.isColSelChanged(true);
2346       repaint = true;
2347     }
2348
2349     if (copycolsel && av.hasHiddenColumns()
2350             && (av.getAlignment().getHiddenColumns() == null))
2351     {
2352       System.err.println("Bad things");
2353     }
2354     if (repaint) // always true!
2355     {
2356       // probably finessing with multiple redraws here
2357       PaintRefresher.Refresh(this, av.getSequenceSetId());
2358       // ap.paintAlignment(false);
2359     }
2360
2361     // lastly, update dependent dialogs
2362     if (ap.getCalculationDialog() != null)
2363     {
2364       ap.getCalculationDialog().validateCalcTypes();
2365     }
2366
2367   }
2368
2369   /**
2370    * If this panel is a cdna/protein translation view of the selection source,
2371    * tries to map the source selection to a local one, and returns true. Else
2372    * returns false.
2373    * 
2374    * @param seqsel
2375    * @param colsel
2376    * @param source
2377    */
2378   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2379           ColumnSelection colsel, HiddenColumns hidden,
2380           SelectionSource source)
2381   {
2382     if (!(source instanceof AlignViewportI))
2383     {
2384       return false;
2385     }
2386     final AlignViewportI sourceAv = (AlignViewportI) source;
2387     if (sourceAv.getCodingComplement() != av
2388             && av.getCodingComplement() != sourceAv)
2389     {
2390       return false;
2391     }
2392
2393     /*
2394      * Map sequence selection
2395      */
2396     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2397     av.setSelectionGroup(sg);
2398     av.isSelectionGroupChanged(true);
2399
2400     /*
2401      * Map column selection
2402      */
2403     // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2404     // av);
2405     ColumnSelection cs = new ColumnSelection();
2406     HiddenColumns hs = new HiddenColumns();
2407     MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2408     av.setColumnSelection(cs);
2409     av.getAlignment().setHiddenColumns(hs);
2410
2411     // lastly, update any dependent dialogs
2412     if (ap.getCalculationDialog() != null)
2413     {
2414       ap.getCalculationDialog().validateCalcTypes();
2415     }
2416
2417     PaintRefresher.Refresh(this, av.getSequenceSetId());
2418
2419     return true;
2420   }
2421
2422   /**
2423    * 
2424    * @return null or last search results handled by this panel
2425    */
2426   public SearchResultsI getLastSearchResults()
2427   {
2428     return lastSearchResults;
2429   }
2430 }