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