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