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