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