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