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