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