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