JAL-3468 i18n and additional unit tests
[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     return results.isEmpty() ? null : getHighlightInfo(results);
869   }
870
871   /**
872    * temporary hack: answers a message suitable to show on structure hover
873    * label. This is normally null. It is a peptide variation description if
874    * <ul>
875    * <li>results are a single residue in a protein alignment</li>
876    * <li>there is a mapping to a coding sequence (codon)</li>
877    * <li>there are one or more SNP variant features on the codon</li>
878    * </ul>
879    * in which case the answer is of the format (e.g.) "p.Glu388Asp"
880    * 
881    * @param results
882    * @return
883    */
884   private String getHighlightInfo(SearchResultsI results)
885   {
886     /*
887      * ideally, just find mapped CDS (as we don't care about render style here);
888      * for now, go via split frame complement's FeatureRenderer
889      */
890     AlignViewportI complement = ap.getAlignViewport().getCodingComplement();
891     if (complement == null)
892     {
893       return null;
894     }
895     AlignFrame af = Desktop.getAlignFrameFor(complement);
896     FeatureRendererModel fr2 = af.getFeatureRenderer();
897
898     int j = results.getSize();
899     List<String> infos = new ArrayList<>();
900     for (int i = 0; i < j; i++)
901     {
902       SearchResultMatchI match = results.getResults().get(i);
903       int pos = match.getStart();
904       if (pos == match.getEnd())
905       {
906         SequenceI seq = match.getSequence();
907         SequenceI ds = seq.getDatasetSequence() == null ? seq
908                 : seq.getDatasetSequence();
909         MappedFeatures mf = fr2
910                 .findComplementFeaturesAtResidue(ds, pos);
911         if (mf != null)
912         {
913           for (SequenceFeature sf : mf.features)
914           {
915             String pv = mf.findProteinVariants(sf);
916             if (pv.length() > 0 && !infos.contains(pv))
917             {
918               infos.add(pv);
919             }
920           }
921         }
922       }
923     }
924
925     if (infos.isEmpty())
926     {
927       return null;
928     }
929     StringBuilder sb = new StringBuilder();
930     for (String info : infos)
931     {
932       if (sb.length() > 0)
933       {
934         sb.append("|");
935       }
936       sb.append(info);
937     }
938     return sb.toString();
939   }
940
941   @Override
942   public VamsasSource getVamsasSource()
943   {
944     return this.ap == null ? null : this.ap.av;
945   }
946
947   @Override
948   public void updateColours(SequenceI seq, int index)
949   {
950     System.out.println("update the seqPanel colours");
951     // repaint();
952   }
953
954   /**
955    * Action on mouse movement is to update the status bar to show the current
956    * sequence position, and (if features are shown) to show any features at the
957    * position in a tooltip. Does nothing if the mouse move does not change
958    * residue position.
959    * 
960    * @param evt
961    */
962   @Override
963   public void mouseMoved(MouseEvent evt)
964   {
965     if (editingSeqs)
966     {
967       // This is because MacOSX creates a mouseMoved
968       // If control is down, other platforms will not.
969       mouseDragged(evt);
970     }
971
972     final MousePos mousePos = findMousePosition(evt);
973     if (mousePos.equals(lastMousePosition))
974     {
975       /*
976        * just a pixel move without change of 'cell'
977        */
978       return;
979     }
980     lastMousePosition = mousePos;
981
982     if (mousePos.isOverAnnotation())
983     {
984       mouseMovedOverAnnotation(mousePos);
985       return;
986     }
987     final int seq = mousePos.seqIndex;
988
989     final int column = mousePos.column;
990     if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
991     {
992       lastMousePosition = null;
993       setToolTipText(null);
994       lastTooltip = null;
995       ap.alignFrame.setStatus("");
996       return;
997     }
998
999     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1000
1001     if (column >= sequence.getLength())
1002     {
1003       return;
1004     }
1005
1006     /*
1007      * set status bar message, returning residue position in sequence
1008      */
1009     boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1010     final int pos = setStatusMessage(sequence, column, seq);
1011     if (ssm != null && !isGapped)
1012     {
1013       mouseOverSequence(sequence, column, pos);
1014     }
1015
1016     tooltipText.setLength(6); // Cuts the buffer back to <html>
1017
1018     SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
1019     if (groups != null)
1020     {
1021       for (int g = 0; g < groups.length; g++)
1022       {
1023         if (groups[g].getStartRes() <= column
1024                 && groups[g].getEndRes() >= column)
1025         {
1026           if (!groups[g].getName().startsWith("JTreeGroup")
1027                   && !groups[g].getName().startsWith("JGroup"))
1028           {
1029             tooltipText.append(groups[g].getName());
1030           }
1031
1032           if (groups[g].getDescription() != null)
1033           {
1034             tooltipText.append(": " + groups[g].getDescription());
1035           }
1036         }
1037       }
1038     }
1039
1040     /*
1041      * add any features at the position to the tooltip; if over a gap, only
1042      * add features that straddle the gap (pos may be the residue before or
1043      * after the gap)
1044      */
1045     int unshownFeatures = 0;
1046     if (av.isShowSequenceFeatures())
1047     {
1048       List<SequenceFeature> features = ap.getFeatureRenderer()
1049               .findFeaturesAtColumn(sequence, column + 1);
1050       unshownFeatures = seqARep.appendFeaturesLengthLimit(tooltipText, pos,
1051               features,
1052               this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
1053
1054       /*
1055        * add features in CDS/protein complement at the corresponding
1056        * position if configured to do so
1057        */
1058       if (av.isShowComplementFeatures())
1059       {
1060         if (!Comparison.isGap(sequence.getCharAt(column)))
1061         {
1062           AlignViewportI complement = ap.getAlignViewport()
1063                   .getCodingComplement();
1064           AlignFrame af = Desktop.getAlignFrameFor(complement);
1065           FeatureRendererModel fr2 = af.getFeatureRenderer();
1066           MappedFeatures mf = fr2.findComplementFeaturesAtResidue(sequence,
1067                   pos);
1068           if (mf != null)
1069           {
1070             unshownFeatures = seqARep.appendFeaturesLengthLimit(
1071                     tooltipText, pos, mf, fr2,
1072                     MAX_TOOLTIP_LENGTH);
1073           }
1074         }
1075       }
1076     }
1077     if (tooltipText.length() == 6) // "<html>"
1078     {
1079       setToolTipText(null);
1080       lastTooltip = null;
1081     }
1082     else
1083     {
1084       if (tooltipText.length() > MAX_TOOLTIP_LENGTH)
1085       {
1086         tooltipText.setLength(MAX_TOOLTIP_LENGTH);
1087         tooltipText.append("...");
1088       }
1089       if (unshownFeatures > 0)
1090       {
1091         tooltipText.append("<br/>").append("... ").append("<i>")
1092                 .append(MessageManager.formatMessage(
1093                         "label.features_not_shown", unshownFeatures))
1094                 .append("</i>");
1095       }
1096       String textString = tooltipText.toString();
1097       if (lastTooltip == null || !lastTooltip.equals(textString))
1098       {
1099         String formattedTooltipText = JvSwingUtils.wrapTooltip(true,
1100                 textString);
1101         setToolTipText(formattedTooltipText);
1102         lastTooltip = textString;
1103       }
1104     }
1105   }
1106
1107   /**
1108    * When the view is in wrapped mode, and the mouse is over an annotation row,
1109    * shows the corresponding tooltip and status message (if any)
1110    * 
1111    * @param pos
1112    * @param column
1113    */
1114   protected void mouseMovedOverAnnotation(MousePos pos)
1115   {
1116     final int column = pos.column;
1117     final int rowIndex = pos.annotationIndex;
1118
1119     if (column < 0 || !av.getWrapAlignment() || !av.isShowAnnotation()
1120             || rowIndex < 0)
1121     {
1122       return;
1123     }
1124     AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
1125
1126     String tooltip = AnnotationPanel.buildToolTip(anns[rowIndex], column,
1127             anns);
1128     setToolTipText(tooltip);
1129     lastTooltip = tooltip;
1130
1131     String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1132             anns[rowIndex]);
1133     ap.alignFrame.setStatus(msg);
1134   }
1135
1136   private Point lastp = null;
1137
1138   /*
1139    * (non-Javadoc)
1140    * 
1141    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1142    */
1143   @Override
1144   public Point getToolTipLocation(MouseEvent event)
1145   {
1146     if (tooltipText == null || tooltipText.length() <= 6)
1147     {
1148       lastp = null;
1149       return null;
1150     }
1151
1152     int x = event.getX();
1153     int w = getWidth();
1154     // switch sides when tooltip is too close to edge
1155     int wdth = (w - x < 200) ? -(w / 2) : 5;
1156     Point p = lastp;
1157     if (!event.isShiftDown() || p == null)
1158     {
1159       p = new Point(event.getX() + wdth, event.getY() - 20);
1160       lastp = p;
1161     }
1162     /*
1163      * TODO: try to set position so region is not obscured by tooltip
1164      */
1165     return p;
1166   }
1167
1168   String lastTooltip;
1169
1170   /**
1171    * set when the current UI interaction has resulted in a change that requires
1172    * shading in overviews and structures to be recalculated. this could be
1173    * changed to a something more expressive that indicates what actually has
1174    * changed, so selective redraws can be applied (ie. only structures, only
1175    * overview, etc)
1176    */
1177   private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
1178
1179   /**
1180    * set if av.getSelectionGroup() refers to a group that is defined on the
1181    * alignment view, rather than a transient selection
1182    */
1183   // private boolean editingDefinedGroup = false; // TODO: refactor to
1184   // avcontroller or viewModel
1185
1186   /**
1187    * Sets the status message in alignment panel, showing the sequence number
1188    * (index) and id, and residue and residue position if not at a gap, for the
1189    * given sequence and column position. Returns the residue position returned
1190    * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1191    * if at a gapped position.
1192    * 
1193    * @param sequence
1194    *          aligned sequence object
1195    * @param column
1196    *          alignment column
1197    * @param seqIndex
1198    *          index of sequence in alignment
1199    * @return sequence position of residue at column, or adjacent residue if at a
1200    *         gap
1201    */
1202   int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1203   {
1204     char sequenceChar = sequence.getCharAt(column);
1205     int pos = sequence.findPosition(column);
1206     setStatusMessage(sequence, seqIndex, sequenceChar, pos);
1207
1208     return pos;
1209   }
1210
1211   /**
1212    * Builds the status message for the current cursor location and writes it to
1213    * the status bar, for example
1214    * 
1215    * <pre>
1216    * Sequence 3 ID: FER1_SOLLC
1217    * Sequence 5 ID: FER1_PEA Residue: THR (4)
1218    * Sequence 5 ID: FER1_PEA Residue: B (3)
1219    * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1220    * </pre>
1221    * 
1222    * @param sequence
1223    * @param seqIndex
1224    *          sequence position in the alignment (1..)
1225    * @param sequenceChar
1226    *          the character under the cursor
1227    * @param residuePos
1228    *          the sequence residue position (if not over a gap)
1229    */
1230   protected void setStatusMessage(SequenceI sequence, int seqIndex,
1231           char sequenceChar, int residuePos)
1232   {
1233     StringBuilder text = new StringBuilder(32);
1234
1235     /*
1236      * Sequence number (if known), and sequence name.
1237      */
1238     String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1239     text.append("Sequence").append(seqno).append(" ID: ")
1240             .append(sequence.getName());
1241
1242     String residue = null;
1243
1244     /*
1245      * Try to translate the display character to residue name (null for gap).
1246      */
1247     boolean isGapped = Comparison.isGap(sequenceChar);
1248
1249     if (!isGapped)
1250     {
1251       boolean nucleotide = av.getAlignment().isNucleotide();
1252       String displayChar = String.valueOf(sequenceChar);
1253       if (nucleotide)
1254       {
1255         residue = ResidueProperties.nucleotideName.get(displayChar);
1256       }
1257       else
1258       {
1259         residue = "X".equalsIgnoreCase(displayChar) ? "X"
1260                 : ("*".equals(displayChar) ? "STOP"
1261                         : ResidueProperties.aa2Triplet.get(displayChar));
1262       }
1263       text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1264               .append(": ").append(residue == null ? displayChar : residue);
1265
1266       text.append(" (").append(Integer.toString(residuePos)).append(")");
1267     }
1268     ap.alignFrame.setStatus(text.toString());
1269   }
1270
1271   /**
1272    * Set the status bar message to highlight the first matched position in
1273    * search results.
1274    * 
1275    * @param results
1276    */
1277   private void setStatusMessage(SearchResultsI results)
1278   {
1279     AlignmentI al = this.av.getAlignment();
1280     int sequenceIndex = al.findIndex(results);
1281     if (sequenceIndex == -1)
1282     {
1283       return;
1284     }
1285     SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
1286     for (SearchResultMatchI m : results.getResults())
1287     {
1288       SequenceI seq = m.getSequence();
1289       if (seq.getDatasetSequence() != null)
1290       {
1291         seq = seq.getDatasetSequence();
1292       }
1293
1294       if (seq == ds)
1295       {
1296         int start = m.getStart();
1297         setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
1298                 start);
1299         return;
1300       }
1301     }
1302   }
1303
1304   /**
1305    * {@inheritDoc}
1306    */
1307   @Override
1308   public void mouseDragged(MouseEvent evt)
1309   {
1310     MousePos pos = findMousePosition(evt);
1311     if (pos.isOverAnnotation() || pos.column == -1)
1312     {
1313       return;
1314     }
1315
1316     if (mouseWheelPressed)
1317     {
1318       boolean inSplitFrame = ap.av.getCodingComplement() != null;
1319       boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1320
1321       int oldWidth = av.getCharWidth();
1322
1323       // Which is bigger, left-right or up-down?
1324       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1325               .abs(evt.getX() - lastMousePress.getX()))
1326       {
1327         /*
1328          * on drag up or down, decrement or increment font size
1329          */
1330         int fontSize = av.font.getSize();
1331         boolean fontChanged = false;
1332
1333         if (evt.getY() < lastMousePress.getY())
1334         {
1335           fontChanged = true;
1336           fontSize--;
1337         }
1338         else if (evt.getY() > lastMousePress.getY())
1339         {
1340           fontChanged = true;
1341           fontSize++;
1342         }
1343
1344         if (fontSize < 1)
1345         {
1346           fontSize = 1;
1347         }
1348
1349         if (fontChanged)
1350         {
1351           Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1352                   fontSize);
1353           av.setFont(newFont, true);
1354           av.setCharWidth(oldWidth);
1355           ap.fontChanged();
1356           if (copyChanges)
1357           {
1358             ap.av.getCodingComplement().setFont(newFont, true);
1359             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1360                     .getSplitViewContainer();
1361             splitFrame.adjustLayout();
1362             splitFrame.repaint();
1363           }
1364         }
1365       }
1366       else
1367       {
1368         /*
1369          * on drag left or right, decrement or increment character width
1370          */
1371         int newWidth = 0;
1372         if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1373         {
1374           newWidth = av.getCharWidth() - 1;
1375           av.setCharWidth(newWidth);
1376         }
1377         else if (evt.getX() > lastMousePress.getX())
1378         {
1379           newWidth = av.getCharWidth() + 1;
1380           av.setCharWidth(newWidth);
1381         }
1382         if (newWidth > 0)
1383         {
1384           ap.paintAlignment(false, false);
1385           if (copyChanges)
1386           {
1387             /*
1388              * need to ensure newWidth is set on cdna, regardless of which
1389              * panel the mouse drag happened in; protein will compute its 
1390              * character width as 1:1 or 3:1
1391              */
1392             av.getCodingComplement().setCharWidth(newWidth);
1393             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1394                     .getSplitViewContainer();
1395             splitFrame.adjustLayout();
1396             splitFrame.repaint();
1397           }
1398         }
1399       }
1400
1401       FontMetrics fm = getFontMetrics(av.getFont());
1402       av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1403
1404       lastMousePress = evt.getPoint();
1405
1406       return;
1407     }
1408
1409     if (!editingSeqs)
1410     {
1411       dragStretchGroup(evt);
1412       return;
1413     }
1414
1415     int res = pos.column;
1416
1417     if (res < 0)
1418     {
1419       res = 0;
1420     }
1421
1422     if ((editLastRes == -1) || (editLastRes == res))
1423     {
1424       return;
1425     }
1426
1427     if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1428     {
1429       // dragLeft, delete gap
1430       editSequence(false, false, res);
1431     }
1432     else
1433     {
1434       editSequence(true, false, res);
1435     }
1436
1437     mouseDragging = true;
1438     if (scrollThread != null)
1439     {
1440       scrollThread.setMousePosition(evt.getPoint());
1441     }
1442   }
1443
1444   /**
1445    * Edits the sequence to insert or delete one or more gaps, in response to a
1446    * mouse drag or cursor mode command. The number of inserts/deletes may be
1447    * specified with the cursor command, or else depends on the mouse event
1448    * (normally one column, but potentially more for a fast mouse drag).
1449    * <p>
1450    * Delete gaps is limited to the number of gaps left of the cursor position
1451    * (mouse drag), or at or right of the cursor position (cursor mode).
1452    * <p>
1453    * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1454    * the current selection group.
1455    * <p>
1456    * In locked editing mode (with a selection group present), inserts/deletions
1457    * within the selection group are limited to its boundaries (and edits outside
1458    * the group stop at its border).
1459    * 
1460    * @param insertGap
1461    *          true to insert gaps, false to delete gaps
1462    * @param editSeq
1463    *          (unused parameter)
1464    * @param startres
1465    *          the column at which to perform the action; the number of columns
1466    *          affected depends on <code>this.editLastRes</code> (cursor column
1467    *          position)
1468    */
1469   synchronized void editSequence(boolean insertGap, boolean editSeq,
1470           final int startres)
1471   {
1472     int fixedLeft = -1;
1473     int fixedRight = -1;
1474     boolean fixedColumns = false;
1475     SequenceGroup sg = av.getSelectionGroup();
1476
1477     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1478
1479     // No group, but the sequence may represent a group
1480     if (!groupEditing && av.hasHiddenRows())
1481     {
1482       if (av.isHiddenRepSequence(seq))
1483       {
1484         sg = av.getRepresentedSequences(seq);
1485         groupEditing = true;
1486       }
1487     }
1488
1489     StringBuilder message = new StringBuilder(64); // for status bar
1490
1491     /*
1492      * make a name for the edit action, for
1493      * status bar message and Undo/Redo menu
1494      */
1495     String label = null;
1496     if (groupEditing)
1497     {
1498         message.append("Edit group:");
1499       label = MessageManager.getString("action.edit_group");
1500     }
1501     else
1502     {
1503         message.append("Edit sequence: " + seq.getName());
1504       label = seq.getName();
1505       if (label.length() > 10)
1506       {
1507         label = label.substring(0, 10);
1508       }
1509       label = MessageManager.formatMessage("label.edit_params",
1510               new String[]
1511               { label });
1512     }
1513
1514     /*
1515      * initialise the edit command if there is not
1516      * already one being extended
1517      */
1518     if (editCommand == null)
1519     {
1520       editCommand = new EditCommand(label);
1521     }
1522
1523     if (insertGap)
1524     {
1525       message.append(" insert ");
1526     }
1527     else
1528     {
1529       message.append(" delete ");
1530     }
1531
1532     message.append(Math.abs(startres - editLastRes) + " gaps.");
1533     ap.alignFrame.setStatus(message.toString());
1534
1535     /*
1536      * is there a selection group containing the sequence being edited?
1537      * if so the boundary of the group is the limit of the edit
1538      * (but the edit may be inside or outside the selection group)
1539      */
1540     boolean inSelectionGroup = sg != null
1541             && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1542     if (groupEditing || inSelectionGroup)
1543     {
1544       fixedColumns = true;
1545
1546       // sg might be null as the user may only see 1 sequence,
1547       // but the sequence represents a group
1548       if (sg == null)
1549       {
1550         if (!av.isHiddenRepSequence(seq))
1551         {
1552           endEditing();
1553           return;
1554         }
1555         sg = av.getRepresentedSequences(seq);
1556       }
1557
1558       fixedLeft = sg.getStartRes();
1559       fixedRight = sg.getEndRes();
1560
1561       if ((startres < fixedLeft && editLastRes >= fixedLeft)
1562               || (startres >= fixedLeft && editLastRes < fixedLeft)
1563               || (startres > fixedRight && editLastRes <= fixedRight)
1564               || (startres <= fixedRight && editLastRes > fixedRight))
1565       {
1566         endEditing();
1567         return;
1568       }
1569
1570       if (fixedLeft > startres)
1571       {
1572         fixedRight = fixedLeft - 1;
1573         fixedLeft = 0;
1574       }
1575       else if (fixedRight < startres)
1576       {
1577         fixedLeft = fixedRight;
1578         fixedRight = -1;
1579       }
1580     }
1581
1582     if (av.hasHiddenColumns())
1583     {
1584       fixedColumns = true;
1585       int y1 = av.getAlignment().getHiddenColumns()
1586               .getNextHiddenBoundary(true, startres);
1587       int y2 = av.getAlignment().getHiddenColumns()
1588               .getNextHiddenBoundary(false, startres);
1589
1590       if ((insertGap && startres > y1 && editLastRes < y1)
1591               || (!insertGap && startres < y2 && editLastRes > y2))
1592       {
1593         endEditing();
1594         return;
1595       }
1596
1597       // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1598       // Selection spans a hidden region
1599       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1600       {
1601         if (startres >= y2)
1602         {
1603           fixedLeft = y2;
1604         }
1605         else
1606         {
1607           fixedRight = y2 - 1;
1608         }
1609       }
1610     }
1611
1612     boolean success = doEditSequence(insertGap, editSeq, startres,
1613             fixedRight, fixedColumns, sg);
1614
1615     /*
1616      * report what actually happened (might be less than
1617      * what was requested), by inspecting the edit commands added
1618      */
1619     String msg = getEditStatusMessage(editCommand);
1620     ap.alignFrame.setStatus(msg == null ? " " : msg);
1621     if (!success)
1622     {
1623       endEditing();
1624     }
1625
1626     editLastRes = startres;
1627     seqCanvas.repaint();
1628   }
1629
1630   /**
1631    * A helper method that performs the requested editing to insert or delete
1632    * gaps (if possible). Answers true if the edit was successful, false if could
1633    * only be performed in part or not at all. Failure may occur in 'locked edit'
1634    * mode, when an insertion requires a matching gapped position (or column) to
1635    * delete, and deletion requires an adjacent gapped position (or column) to
1636    * remove.
1637    * 
1638    * @param insertGap
1639    *          true if inserting gap(s), false if deleting
1640    * @param editSeq
1641    *          (unused parameter, currently always false)
1642    * @param startres
1643    *          the column at which to perform the edit
1644    * @param fixedRight
1645    *          fixed right boundary column of a locked edit (within or to the
1646    *          left of a selection group)
1647    * @param fixedColumns
1648    *          true if this is a locked edit
1649    * @param sg
1650    *          the sequence group (if group edit is being performed)
1651    * @return
1652    */
1653   protected boolean doEditSequence(final boolean insertGap,
1654           final boolean editSeq, final int startres, int fixedRight,
1655           final boolean fixedColumns, final SequenceGroup sg)
1656   {
1657     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1658     SequenceI[] seqs = new SequenceI[] { seq };
1659
1660     if (groupEditing)
1661     {
1662       List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1663       int g, groupSize = vseqs.size();
1664       SequenceI[] groupSeqs = new SequenceI[groupSize];
1665       for (g = 0; g < groupSeqs.length; g++)
1666       {
1667         groupSeqs[g] = vseqs.get(g);
1668       }
1669
1670       // drag to right
1671       if (insertGap)
1672       {
1673         // If the user has selected the whole sequence, and is dragging to
1674         // the right, we can still extend the alignment and selectionGroup
1675         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1676                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1677         {
1678           sg.setEndRes(
1679                   av.getAlignment().getWidth() + startres - editLastRes);
1680           fixedRight = sg.getEndRes();
1681         }
1682
1683         // Is it valid with fixed columns??
1684         // Find the next gap before the end
1685         // of the visible region boundary
1686         boolean blank = false;
1687         for (; fixedRight > editLastRes; fixedRight--)
1688         {
1689           blank = true;
1690
1691           for (g = 0; g < groupSize; g++)
1692           {
1693             for (int j = 0; j < startres - editLastRes; j++)
1694             {
1695               if (!Comparison
1696                       .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1697               {
1698                 blank = false;
1699                 break;
1700               }
1701             }
1702           }
1703           if (blank)
1704           {
1705             break;
1706           }
1707         }
1708
1709         if (!blank)
1710         {
1711           if (sg.getSize() == av.getAlignment().getHeight())
1712           {
1713             if ((av.hasHiddenColumns()
1714                     && startres < av.getAlignment().getHiddenColumns()
1715                             .getNextHiddenBoundary(false, startres)))
1716             {
1717               return false;
1718             }
1719
1720             int alWidth = av.getAlignment().getWidth();
1721             if (av.hasHiddenRows())
1722             {
1723               int hwidth = av.getAlignment().getHiddenSequences()
1724                       .getWidth();
1725               if (hwidth > alWidth)
1726               {
1727                 alWidth = hwidth;
1728               }
1729             }
1730             // We can still insert gaps if the selectionGroup
1731             // contains all the sequences
1732             sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1733             fixedRight = alWidth + startres - editLastRes;
1734           }
1735           else
1736           {
1737             return false;
1738           }
1739         }
1740       }
1741
1742       // drag to left
1743       else if (!insertGap)
1744       {
1745         // / Are we able to delete?
1746         // ie are all columns blank?
1747
1748         for (g = 0; g < groupSize; g++)
1749         {
1750           for (int j = startres; j < editLastRes; j++)
1751           {
1752             if (groupSeqs[g].getLength() <= j)
1753             {
1754               continue;
1755             }
1756
1757             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1758             {
1759               // Not a gap, block edit not valid
1760               return false;
1761             }
1762           }
1763         }
1764       }
1765
1766       if (insertGap)
1767       {
1768         // dragging to the right
1769         if (fixedColumns && fixedRight != -1)
1770         {
1771           for (int j = editLastRes; j < startres; j++)
1772           {
1773             insertGap(j, groupSeqs, fixedRight);
1774           }
1775         }
1776         else
1777         {
1778           appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1779                   startres - editLastRes, false);
1780         }
1781       }
1782       else
1783       {
1784         // dragging to the left
1785         if (fixedColumns && fixedRight != -1)
1786         {
1787           for (int j = editLastRes; j > startres; j--)
1788           {
1789             deleteChar(startres, groupSeqs, fixedRight);
1790           }
1791         }
1792         else
1793         {
1794           appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1795                   editLastRes - startres, false);
1796         }
1797       }
1798     }
1799     else
1800     {
1801       /*
1802        * editing a single sequence
1803        */
1804       if (insertGap)
1805       {
1806         // dragging to the right
1807         if (fixedColumns && fixedRight != -1)
1808         {
1809           for (int j = editLastRes; j < startres; j++)
1810           {
1811             if (!insertGap(j, seqs, fixedRight))
1812             {
1813               /*
1814                * e.g. cursor mode command specified 
1815                * more inserts than are possible
1816                */
1817               return false;
1818             }
1819           }
1820         }
1821         else
1822         {
1823           appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1824                   startres - editLastRes, false);
1825         }
1826       }
1827       else
1828       {
1829         if (!editSeq)
1830         {
1831           // dragging to the left
1832           if (fixedColumns && fixedRight != -1)
1833           {
1834             for (int j = editLastRes; j > startres; j--)
1835             {
1836               if (!Comparison.isGap(seq.getCharAt(startres)))
1837               {
1838                 return false;
1839               }
1840               deleteChar(startres, seqs, fixedRight);
1841             }
1842           }
1843           else
1844           {
1845             // could be a keyboard edit trying to delete none gaps
1846             int max = 0;
1847             for (int m = startres; m < editLastRes; m++)
1848             {
1849               if (!Comparison.isGap(seq.getCharAt(m)))
1850               {
1851                 break;
1852               }
1853               max++;
1854             }
1855             if (max > 0)
1856             {
1857               appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1858             }
1859           }
1860         }
1861         else
1862         {// insertGap==false AND editSeq==TRUE;
1863           if (fixedColumns && fixedRight != -1)
1864           {
1865             for (int j = editLastRes; j < startres; j++)
1866             {
1867               insertGap(j, seqs, fixedRight);
1868             }
1869           }
1870           else
1871           {
1872             appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1873                     startres - editLastRes, false);
1874           }
1875         }
1876       }
1877     }
1878
1879     return true;
1880   }
1881
1882   /**
1883    * Constructs an informative status bar message while dragging to insert or
1884    * delete gaps. Answers null if inserts and deletes cancel out.
1885    * 
1886    * @param editCommand
1887    *          a command containing the list of individual edits
1888    * @return
1889    */
1890   protected static String getEditStatusMessage(EditCommand editCommand)
1891   {
1892     if (editCommand == null)
1893     {
1894       return null;
1895     }
1896
1897     /*
1898      * add any inserts, and subtract any deletes,  
1899      * not counting those auto-inserted when doing a 'locked edit'
1900      * (so only counting edits 'under the cursor')
1901      */
1902     int count = 0;
1903     for (Edit cmd : editCommand.getEdits())
1904     {
1905       if (!cmd.isSystemGenerated())
1906       {
1907         count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1908                 : -cmd.getNumber();
1909       }
1910     }
1911
1912     if (count == 0)
1913     {
1914       /*
1915        * inserts and deletes cancel out
1916        */
1917       return null;
1918     }
1919
1920     String msgKey = count > 1 ? "label.insert_gaps"
1921             : (count == 1 ? "label.insert_gap"
1922                     : (count == -1 ? "label.delete_gap"
1923                             : "label.delete_gaps"));
1924     count = Math.abs(count);
1925
1926     return MessageManager.formatMessage(msgKey, String.valueOf(count));
1927   }
1928
1929   /**
1930    * Inserts one gap at column j, deleting the right-most gapped column up to
1931    * (and including) fixedColumn. Returns true if the edit is successful, false
1932    * if no blank column is available to allow the insertion to be balanced by a
1933    * deletion.
1934    * 
1935    * @param j
1936    * @param seq
1937    * @param fixedColumn
1938    * @return
1939    */
1940   boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1941   {
1942     int blankColumn = fixedColumn;
1943     for (int s = 0; s < seq.length; s++)
1944     {
1945       // Find the next gap before the end of the visible region boundary
1946       // If lastCol > j, theres a boundary after the gap insertion
1947
1948       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1949       {
1950         if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1951         {
1952           // Theres a space, so break and insert the gap
1953           break;
1954         }
1955       }
1956
1957       if (blankColumn <= j)
1958       {
1959         blankColumn = fixedColumn;
1960         endEditing();
1961         return false;
1962       }
1963     }
1964
1965     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1966
1967     appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1968
1969     return true;
1970   }
1971
1972   /**
1973    * Helper method to add and perform one edit action
1974    * 
1975    * @param action
1976    * @param seq
1977    * @param pos
1978    * @param count
1979    * @param systemGenerated
1980    *          true if the edit is a 'balancing' delete (or insert) to match a
1981    *          user's insert (or delete) in a locked editing region
1982    */
1983   protected void appendEdit(Action action, SequenceI[] seq, int pos,
1984           int count, boolean systemGenerated)
1985   {
1986
1987     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1988             av.getAlignment().getGapCharacter());
1989     edit.setSystemGenerated(systemGenerated);
1990
1991     editCommand.appendEdit(edit, av.getAlignment(), true, null);
1992   }
1993
1994   /**
1995    * Deletes the character at column j, and inserts a gap at fixedColumn, in
1996    * each of the given sequences. The caller should ensure that all sequences
1997    * are gapped in column j.
1998    * 
1999    * @param j
2000    * @param seqs
2001    * @param fixedColumn
2002    */
2003   void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2004   {
2005     appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2006
2007     appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2008   }
2009
2010   /**
2011    * On reentering the panel, stops any scrolling that was started on dragging
2012    * out of the panel
2013    * 
2014    * @param e
2015    */
2016   @Override
2017   public void mouseEntered(MouseEvent e)
2018   {
2019     if (oldSeq < 0)
2020     {
2021       oldSeq = 0;
2022     }
2023     stopScrolling();
2024   }
2025
2026   /**
2027    * On leaving the panel, if the mouse is being dragged, starts a thread to
2028    * scroll it until the mouse is released (in unwrapped mode only)
2029    * 
2030    * @param e
2031    */
2032   @Override
2033   public void mouseExited(MouseEvent e)
2034   {
2035     lastMousePosition = null;
2036     ap.alignFrame.setStatus(" ");
2037     if (av.getWrapAlignment())
2038     {
2039       return;
2040     }
2041
2042     if (mouseDragging && scrollThread == null)
2043     {
2044       scrollThread = new ScrollThread();
2045     }
2046   }
2047
2048   /**
2049    * Handler for double-click on a position with one or more sequence features.
2050    * Opens the Amend Features dialog to allow feature details to be amended, or
2051    * the feature deleted.
2052    */
2053   @Override
2054   public void mouseClicked(MouseEvent evt)
2055   {
2056     SequenceGroup sg = null;
2057     MousePos pos = findMousePosition(evt);
2058     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2059     {
2060       return;
2061     }
2062
2063     if (evt.getClickCount() > 1)
2064     {
2065       sg = av.getSelectionGroup();
2066       if (sg != null && sg.getSize() == 1
2067               && sg.getEndRes() - sg.getStartRes() < 2)
2068       {
2069         av.setSelectionGroup(null);
2070       }
2071
2072       int column = pos.column;
2073
2074       /*
2075        * find features at the position (if not gapped), or straddling
2076        * the position (if at a gap)
2077        */
2078       SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2079       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2080               .findFeaturesAtColumn(sequence, column + 1);
2081
2082       if (!features.isEmpty())
2083       {
2084         /*
2085          * highlight the first feature at the position on the alignment
2086          */
2087         SearchResultsI highlight = new SearchResults();
2088         highlight.addResult(sequence, features.get(0).getBegin(), features
2089                 .get(0).getEnd());
2090         seqCanvas.highlightSearchResults(highlight, false);
2091
2092         /*
2093          * open the Amend Features dialog; clear highlighting afterwards,
2094          * whether changes were made or not
2095          */
2096         List<SequenceI> seqs = Collections.singletonList(sequence);
2097         seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
2098                 ap);
2099         av.setSearchResults(null); // clear highlighting
2100         seqCanvas.repaint(); // draw new/amended features
2101       }
2102     }
2103   }
2104
2105   @Override
2106   public void mouseWheelMoved(MouseWheelEvent e)
2107   {
2108     e.consume();
2109     double wheelRotation = e.getPreciseWheelRotation();
2110     if (wheelRotation > 0)
2111     {
2112       if (e.isShiftDown())
2113       {
2114         av.getRanges().scrollRight(true);
2115
2116       }
2117       else
2118       {
2119         av.getRanges().scrollUp(false);
2120       }
2121     }
2122     else if (wheelRotation < 0)
2123     {
2124       if (e.isShiftDown())
2125       {
2126         av.getRanges().scrollRight(false);
2127       }
2128       else
2129       {
2130         av.getRanges().scrollUp(true);
2131       }
2132     }
2133
2134     /*
2135      * update status bar and tooltip for new position
2136      * (need to synthesize a mouse movement to refresh tooltip)
2137      */
2138     mouseMoved(e);
2139     ToolTipManager.sharedInstance().mouseMoved(e);
2140   }
2141
2142   /**
2143    * DOCUMENT ME!
2144    * 
2145    * @param pos
2146    *          DOCUMENT ME!
2147    */
2148   protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2149   {
2150     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2151     {
2152       return;
2153     }
2154
2155     final int res = pos.column;
2156     final int seq = pos.seqIndex;
2157     oldSeq = seq;
2158     updateOverviewAndStructs = false;
2159
2160     startWrapBlock = wrappedBlock;
2161
2162     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2163
2164     if ((sequence == null) || (res > sequence.getLength()))
2165     {
2166       return;
2167     }
2168
2169     stretchGroup = av.getSelectionGroup();
2170
2171     if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2172     {
2173       stretchGroup = av.getAlignment().findGroup(sequence, res);
2174       if (stretchGroup != null)
2175       {
2176         // only update the current selection if the popup menu has a group to
2177         // focus on
2178         av.setSelectionGroup(stretchGroup);
2179       }
2180     }
2181
2182     if (evt.isPopupTrigger()) // Mac: mousePressed
2183     {
2184       showPopupMenu(evt, pos);
2185       return;
2186     }
2187
2188     /*
2189      * defer right-mouse click handling to mouseReleased on Windows
2190      * (where isPopupTrigger() will answer true)
2191      * NB isRightMouseButton is also true for Cmd-click on Mac
2192      */
2193     if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
2194     {
2195       return;
2196     }
2197
2198     if (av.cursorMode)
2199     {
2200       seqCanvas.cursorX = res;
2201       seqCanvas.cursorY = seq;
2202       seqCanvas.repaint();
2203       return;
2204     }
2205
2206     if (stretchGroup == null)
2207     {
2208       createStretchGroup(res, sequence);
2209     }
2210
2211     if (stretchGroup != null)
2212     {
2213       stretchGroup.addPropertyChangeListener(seqCanvas);
2214     }
2215
2216     seqCanvas.repaint();
2217   }
2218
2219   private void createStretchGroup(int res, SequenceI sequence)
2220   {
2221     // Only if left mouse button do we want to change group sizes
2222     // define a new group here
2223     SequenceGroup sg = new SequenceGroup();
2224     sg.setStartRes(res);
2225     sg.setEndRes(res);
2226     sg.addSequence(sequence, false);
2227     av.setSelectionGroup(sg);
2228     stretchGroup = sg;
2229
2230     if (av.getConservationSelected())
2231     {
2232       SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2233               ap.getViewName());
2234     }
2235
2236     if (av.getAbovePIDThreshold())
2237     {
2238       SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2239               ap.getViewName());
2240     }
2241     // TODO: stretchGroup will always be not null. Is this a merge error ?
2242     // or is there a threading issue here?
2243     if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2244     {
2245       // Edit end res position of selected group
2246       changeEndRes = true;
2247     }
2248     else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2249     {
2250       // Edit end res position of selected group
2251       changeStartRes = true;
2252     }
2253     stretchGroup.getWidth();
2254
2255   }
2256
2257   /**
2258    * Build and show a pop-up menu at the right-click mouse position
2259    *
2260    * @param evt
2261    * @param pos
2262    */
2263   void showPopupMenu(MouseEvent evt, MousePos pos)
2264   {
2265     final int column = pos.column;
2266     final int seq = pos.seqIndex;
2267     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2268     if (sequence != null)
2269     {
2270       PopupMenu pop = new PopupMenu(ap, sequence, column);
2271       pop.show(this, evt.getX(), evt.getY());
2272     }
2273   }
2274
2275   /**
2276    * Update the display after mouse up on a selection or group
2277    * 
2278    * @param evt
2279    *          mouse released event details
2280    * @param afterDrag
2281    *          true if this event is happening after a mouse drag (rather than a
2282    *          mouse down)
2283    */
2284   protected void doMouseReleasedDefineMode(MouseEvent evt,
2285           boolean afterDrag)
2286   {
2287     if (stretchGroup == null)
2288     {
2289       return;
2290     }
2291
2292     stretchGroup.removePropertyChangeListener(seqCanvas);
2293
2294     // always do this - annotation has own state
2295     // but defer colourscheme update until hidden sequences are passed in
2296     boolean vischange = stretchGroup.recalcConservation(true);
2297     updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2298             && afterDrag;
2299     if (stretchGroup.cs != null)
2300     {
2301       if (afterDrag)
2302       {
2303         stretchGroup.cs.alignmentChanged(stretchGroup,
2304                 av.getHiddenRepSequences());
2305       }
2306
2307       ResidueShaderI groupColourScheme = stretchGroup
2308               .getGroupColourScheme();
2309       String name = stretchGroup.getName();
2310       if (stretchGroup.cs.conservationApplied())
2311       {
2312         SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2313       }
2314       if (stretchGroup.cs.getThreshold() > 0)
2315       {
2316         SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2317       }
2318     }
2319     PaintRefresher.Refresh(this, av.getSequenceSetId());
2320     // TODO: structure colours only need updating if stretchGroup used to or now
2321     // does contain sequences with structure views
2322     ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2323     updateOverviewAndStructs = false;
2324     changeEndRes = false;
2325     changeStartRes = false;
2326     stretchGroup = null;
2327     av.sendSelection();
2328   }
2329
2330   /**
2331    * Resizes the borders of a selection group depending on the direction of
2332    * mouse drag
2333    * 
2334    * @param evt
2335    */
2336   protected void dragStretchGroup(MouseEvent evt)
2337   {
2338     if (stretchGroup == null)
2339     {
2340       return;
2341     }
2342
2343     MousePos pos = findMousePosition(evt);
2344     if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2345     {
2346       return;
2347     }
2348
2349     int res = pos.column;
2350     int y = pos.seqIndex;
2351
2352     if (wrappedBlock != startWrapBlock)
2353     {
2354       return;
2355     }
2356
2357     res = Math.min(res, av.getAlignment().getWidth()-1);
2358
2359     if (stretchGroup.getEndRes() == res)
2360     {
2361       // Edit end res position of selected group
2362       changeEndRes = true;
2363     }
2364     else if (stretchGroup.getStartRes() == res)
2365     {
2366       // Edit start res position of selected group
2367       changeStartRes = true;
2368     }
2369
2370     if (res < av.getRanges().getStartRes())
2371     {
2372       res = av.getRanges().getStartRes();
2373     }
2374
2375     if (changeEndRes)
2376     {
2377       if (res > (stretchGroup.getStartRes() - 1))
2378       {
2379         stretchGroup.setEndRes(res);
2380         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2381       }
2382     }
2383     else if (changeStartRes)
2384     {
2385       if (res < (stretchGroup.getEndRes() + 1))
2386       {
2387         stretchGroup.setStartRes(res);
2388         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2389       }
2390     }
2391
2392     int dragDirection = 0;
2393
2394     if (y > oldSeq)
2395     {
2396       dragDirection = 1;
2397     }
2398     else if (y < oldSeq)
2399     {
2400       dragDirection = -1;
2401     }
2402
2403     while ((y != oldSeq) && (oldSeq > -1)
2404             && (y < av.getAlignment().getHeight()))
2405     {
2406       // This routine ensures we don't skip any sequences, as the
2407       // selection is quite slow.
2408       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2409
2410       oldSeq += dragDirection;
2411
2412       if (oldSeq < 0)
2413       {
2414         break;
2415       }
2416
2417       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2418
2419       if (stretchGroup.getSequences(null).contains(nextSeq))
2420       {
2421         stretchGroup.deleteSequence(seq, false);
2422         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2423       }
2424       else
2425       {
2426         if (seq != null)
2427         {
2428           stretchGroup.addSequence(seq, false);
2429         }
2430
2431         stretchGroup.addSequence(nextSeq, false);
2432         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2433       }
2434     }
2435
2436     if (oldSeq < 0)
2437     {
2438       oldSeq = -1;
2439     }
2440
2441     mouseDragging = true;
2442
2443     if (scrollThread != null)
2444     {
2445       scrollThread.setMousePosition(evt.getPoint());
2446     }
2447
2448     /*
2449      * construct a status message showing the range of the selection
2450      */
2451     StringBuilder status = new StringBuilder(64);
2452     List<SequenceI> seqs = stretchGroup.getSequences();
2453     String name = seqs.get(0).getName();
2454     if (name.length() > 20)
2455     {
2456       name = name.substring(0, 20);
2457     }
2458     status.append(name).append(" - ");
2459     name = seqs.get(seqs.size() - 1).getName();
2460     if (name.length() > 20)
2461     {
2462       name = name.substring(0, 20);
2463     }
2464     status.append(name).append(" ");
2465     int startRes = stretchGroup.getStartRes();
2466     status.append(" cols ").append(String.valueOf(startRes + 1))
2467             .append("-");
2468     int endRes = stretchGroup.getEndRes();
2469     status.append(String.valueOf(endRes + 1));
2470     status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2471             .append(String.valueOf(endRes - startRes + 1)).append(")");
2472     ap.alignFrame.setStatus(status.toString());
2473   }
2474
2475   /**
2476    * Stops the scroll thread if it is running
2477    */
2478   void stopScrolling()
2479   {
2480     if (scrollThread != null)
2481     {
2482       scrollThread.stopScrolling();
2483       scrollThread = null;
2484     }
2485     mouseDragging = false;
2486   }
2487
2488   /**
2489    * Starts a thread to scroll the alignment, towards a given mouse position
2490    * outside the panel bounds
2491    * 
2492    * @param mousePos
2493    */
2494   void startScrolling(Point mousePos)
2495   {
2496     if (scrollThread == null)
2497     {
2498       scrollThread = new ScrollThread();
2499     }
2500
2501     mouseDragging = true;
2502     scrollThread.setMousePosition(mousePos);
2503   }
2504
2505   /**
2506    * Performs scrolling of the visible alignment left, right, up or down
2507    */
2508   class ScrollThread extends Thread
2509   {
2510     private Point mousePos;
2511
2512     private volatile boolean threadRunning = true;
2513
2514     /**
2515      * Constructor
2516      */
2517     public ScrollThread()
2518     {
2519       setName("SeqPanel$ScrollThread");
2520       start();
2521     }
2522
2523     /**
2524      * Sets the position of the mouse that determines the direction of the
2525      * scroll to perform
2526      * 
2527      * @param p
2528      */
2529     public void setMousePosition(Point p)
2530     {
2531       mousePos = p;
2532     }
2533
2534     /**
2535      * Sets a flag that will cause the thread to exit
2536      */
2537     public void stopScrolling()
2538     {
2539       threadRunning = false;
2540     }
2541
2542     /**
2543      * Scrolls the alignment left or right, and/or up or down, depending on the
2544      * last notified mouse position, until the limit of the alignment is
2545      * reached, or a flag is set to stop the scroll
2546      */
2547     @Override
2548     public void run()
2549     {
2550       while (threadRunning && mouseDragging)
2551       {
2552         if (mousePos != null)
2553         {
2554           boolean scrolled = false;
2555           ViewportRanges ranges = SeqPanel.this.av.getRanges();
2556
2557           /*
2558            * scroll up or down
2559            */
2560           if (mousePos.y < 0)
2561           {
2562             // mouse is above this panel - try scroll up
2563             scrolled = ranges.scrollUp(true);
2564           }
2565           else if (mousePos.y >= getHeight())
2566           {
2567             // mouse is below this panel - try scroll down
2568             scrolled = ranges.scrollUp(false);
2569           }
2570
2571           /*
2572            * scroll left or right
2573            */
2574           if (mousePos.x < 0)
2575           {
2576             scrolled |= ranges.scrollRight(false);
2577           }
2578           else if (mousePos.x >= getWidth())
2579           {
2580             scrolled |= ranges.scrollRight(true);
2581           }
2582           if (!scrolled)
2583           {
2584             /*
2585              * we have reached the limit of the visible alignment - quit
2586              */
2587             threadRunning = false;
2588             SeqPanel.this.ap.repaint();
2589           }
2590         }
2591
2592         try
2593         {
2594           Thread.sleep(20);
2595         } catch (Exception ex)
2596         {
2597         }
2598       }
2599     }
2600   }
2601
2602   /**
2603    * modify current selection according to a received message.
2604    */
2605   @Override
2606   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2607           HiddenColumns hidden, SelectionSource source)
2608   {
2609     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2610     // handles selection messages...
2611     // TODO: extend config options to allow user to control if selections may be
2612     // shared between viewports.
2613     boolean iSentTheSelection = (av == source
2614             || (source instanceof AlignViewport
2615                     && ((AlignmentViewport) source).getSequenceSetId()
2616                             .equals(av.getSequenceSetId())));
2617
2618     if (iSentTheSelection)
2619     {
2620       // respond to our own event by updating dependent dialogs
2621       if (ap.getCalculationDialog() != null)
2622       {
2623         ap.getCalculationDialog().validateCalcTypes();
2624       }
2625
2626       return;
2627     }
2628
2629     // process further ?
2630     if (!av.followSelection)
2631     {
2632       return;
2633     }
2634
2635     /*
2636      * Ignore the selection if there is one of our own pending.
2637      */
2638     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2639     {
2640       return;
2641     }
2642
2643     /*
2644      * Check for selection in a view of which this one is a dna/protein
2645      * complement.
2646      */
2647     if (selectionFromTranslation(seqsel, colsel, hidden, source))
2648     {
2649       return;
2650     }
2651
2652     // do we want to thread this ? (contention with seqsel and colsel locks, I
2653     // suspect)
2654     /*
2655      * only copy colsel if there is a real intersection between
2656      * sequence selection and this panel's alignment
2657      */
2658     boolean repaint = false;
2659     boolean copycolsel = false;
2660
2661     SequenceGroup sgroup = null;
2662     if (seqsel != null && seqsel.getSize() > 0)
2663     {
2664       if (av.getAlignment() == null)
2665       {
2666         Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2667                 + " ViewId=" + av.getViewId()
2668                 + " 's alignment is NULL! returning immediately.");
2669         return;
2670       }
2671       sgroup = seqsel.intersect(av.getAlignment(),
2672               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2673       if ((sgroup != null && sgroup.getSize() > 0))
2674       {
2675         copycolsel = true;
2676       }
2677     }
2678     if (sgroup != null && sgroup.getSize() > 0)
2679     {
2680       av.setSelectionGroup(sgroup);
2681     }
2682     else
2683     {
2684       av.setSelectionGroup(null);
2685     }
2686     av.isSelectionGroupChanged(true);
2687     repaint = true;
2688
2689     if (copycolsel)
2690     {
2691       // the current selection is unset or from a previous message
2692       // so import the new colsel.
2693       if (colsel == null || colsel.isEmpty())
2694       {
2695         if (av.getColumnSelection() != null)
2696         {
2697           av.getColumnSelection().clear();
2698           repaint = true;
2699         }
2700       }
2701       else
2702       {
2703         // TODO: shift colSel according to the intersecting sequences
2704         if (av.getColumnSelection() == null)
2705         {
2706           av.setColumnSelection(new ColumnSelection(colsel));
2707         }
2708         else
2709         {
2710           av.getColumnSelection().setElementsFrom(colsel,
2711                   av.getAlignment().getHiddenColumns());
2712         }
2713       }
2714       av.isColSelChanged(true);
2715       repaint = true;
2716     }
2717
2718     if (copycolsel && av.hasHiddenColumns()
2719             && (av.getAlignment().getHiddenColumns() == null))
2720     {
2721       System.err.println("Bad things");
2722     }
2723     if (repaint) // always true!
2724     {
2725       // probably finessing with multiple redraws here
2726       PaintRefresher.Refresh(this, av.getSequenceSetId());
2727       // ap.paintAlignment(false);
2728     }
2729
2730     // lastly, update dependent dialogs
2731     if (ap.getCalculationDialog() != null)
2732     {
2733       ap.getCalculationDialog().validateCalcTypes();
2734     }
2735
2736   }
2737
2738   /**
2739    * If this panel is a cdna/protein translation view of the selection source,
2740    * tries to map the source selection to a local one, and returns true. Else
2741    * returns false.
2742    * 
2743    * @param seqsel
2744    * @param colsel
2745    * @param source
2746    */
2747   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2748           ColumnSelection colsel, HiddenColumns hidden,
2749           SelectionSource source)
2750   {
2751     if (!(source instanceof AlignViewportI))
2752     {
2753       return false;
2754     }
2755     final AlignViewportI sourceAv = (AlignViewportI) source;
2756     if (sourceAv.getCodingComplement() != av
2757             && av.getCodingComplement() != sourceAv)
2758     {
2759       return false;
2760     }
2761
2762     /*
2763      * Map sequence selection
2764      */
2765     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2766     av.setSelectionGroup(sg);
2767     av.isSelectionGroupChanged(true);
2768
2769     /*
2770      * Map column selection
2771      */
2772     // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2773     // av);
2774     ColumnSelection cs = new ColumnSelection();
2775     HiddenColumns hs = new HiddenColumns();
2776     MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2777     av.setColumnSelection(cs);
2778     boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2779
2780     // lastly, update any dependent dialogs
2781     if (ap.getCalculationDialog() != null)
2782     {
2783       ap.getCalculationDialog().validateCalcTypes();
2784     }
2785
2786     /*
2787      * repaint alignment, and also Overview or Structure
2788      * if hidden column selection has changed
2789      */
2790     ap.paintAlignment(hiddenChanged, hiddenChanged);
2791
2792     return true;
2793   }
2794
2795   /**
2796    * 
2797    * @return null or last search results handled by this panel
2798    */
2799   public SearchResultsI getLastSearchResults()
2800   {
2801     return lastSearchResults;
2802   }
2803 }