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