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