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