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