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