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