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