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