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