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