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