Merge branch 'bug/JAL-3365_extendedDSSP_via_stockholm' 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.equals(lastTooltip))
1188     {
1189       lastTooltip = tooltip;
1190       lastFormattedTooltip = tooltip == null ? null
1191               : JvSwingUtils.wrapTooltip(true, tooltip);
1192       setToolTipText(lastFormattedTooltip);
1193     }
1194
1195     String msg = AnnotationPanel.getStatusMessage(av.getAlignment(), column,
1196             anns[rowIndex]);
1197     ap.alignFrame.setStatus(msg);
1198   }
1199
1200   /*
1201    * if Shift key is held down while moving the mouse, 
1202    * the tooltip location is not changed once shown
1203    */
1204   private Point lastTooltipLocation = null;
1205
1206   /*
1207    * this flag is false for pixel moves within a residue,
1208    * to reduce tooltip flicker
1209    */
1210   private boolean moveTooltip = true;
1211
1212   /*
1213    * a dummy tooltip used to estimate where to position tooltips
1214    */
1215   private JToolTip tempTip = new JLabel().createToolTip();
1216
1217   /*
1218    * (non-Javadoc)
1219    * 
1220    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
1221    */
1222   @Override
1223   public Point getToolTipLocation(MouseEvent event)
1224   {
1225     // BH 2018
1226
1227     if (lastTooltip == null || !moveTooltip)
1228     {
1229       return null;
1230     }
1231
1232     if (lastTooltipLocation != null && event.isShiftDown())
1233     {
1234       return lastTooltipLocation;
1235     }
1236
1237     int x = event.getX();
1238     int y = event.getY();
1239     int w = getWidth();
1240
1241     tempTip.setTipText(lastFormattedTooltip);
1242     int tipWidth = (int) tempTip.getPreferredSize().getWidth();
1243
1244     // was x += (w - x < 200) ? -(w / 2) : 5;
1245     x = (x + tipWidth < w ? x + 10 : w - tipWidth);
1246     Point p = new Point(x, y + av.getCharHeight()); // BH 2018 was - 20?
1247
1248     return lastTooltipLocation = p;
1249   }
1250
1251   /**
1252    * set when the current UI interaction has resulted in a change that requires
1253    * shading in overviews and structures to be recalculated. this could be
1254    * changed to a something more expressive that indicates what actually has
1255    * changed, so selective redraws can be applied (ie. only structures, only
1256    * overview, etc)
1257    */
1258   private boolean updateOverviewAndStructs = false; // TODO: refactor to
1259                                                     // avcontroller
1260
1261   /**
1262    * set if av.getSelectionGroup() refers to a group that is defined on the
1263    * alignment view, rather than a transient selection
1264    */
1265   // private boolean editingDefinedGroup = false; // TODO: refactor to
1266   // avcontroller or viewModel
1267
1268   /**
1269    * Sets the status message in alignment panel, showing the sequence number
1270    * (index) and id, and residue and residue position if not at a gap, for the
1271    * given sequence and column position. Returns the residue position returned
1272    * by Sequence.findPosition. Note this may be for the nearest adjacent residue
1273    * if at a gapped position.
1274    * 
1275    * @param sequence
1276    *          aligned sequence object
1277    * @param column
1278    *          alignment column
1279    * @param seqIndex
1280    *          index of sequence in alignment
1281    * @return sequence position of residue at column, or adjacent residue if at a
1282    *         gap
1283    */
1284   int setStatusMessage(SequenceI sequence, final int column, int seqIndex)
1285   {
1286     char sequenceChar = sequence.getCharAt(column);
1287     int pos = sequence.findPosition(column);
1288     setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
1289
1290     return pos;
1291   }
1292
1293   /**
1294    * Builds the status message for the current cursor location and writes it to
1295    * the status bar, for example
1296    * 
1297    * <pre>
1298    * Sequence 3 ID: FER1_SOLLC
1299    * Sequence 5 ID: FER1_PEA Residue: THR (4)
1300    * Sequence 5 ID: FER1_PEA Residue: B (3)
1301    * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
1302    * </pre>
1303    * 
1304    * @param seqName
1305    * @param seqIndex
1306    *          sequence position in the alignment (1..)
1307    * @param sequenceChar
1308    *          the character under the cursor
1309    * @param residuePos
1310    *          the sequence residue position (if not over a gap)
1311    */
1312   protected void setStatusMessage(String seqName, int seqIndex,
1313           char sequenceChar, int residuePos)
1314   {
1315     StringBuilder text = new StringBuilder(32);
1316
1317     /*
1318      * Sequence number (if known), and sequence name.
1319      */
1320     String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
1321     text.append("Sequence").append(seqno).append(" ID: ").append(seqName);
1322
1323     String residue = null;
1324
1325     /*
1326      * Try to translate the display character to residue name (null for gap).
1327      */
1328     boolean isGapped = Comparison.isGap(sequenceChar);
1329
1330     if (!isGapped)
1331     {
1332       boolean nucleotide = av.getAlignment().isNucleotide();
1333       String displayChar = String.valueOf(sequenceChar);
1334       if (nucleotide)
1335       {
1336         residue = ResidueProperties.nucleotideName.get(displayChar);
1337       }
1338       else
1339       {
1340         residue = "X".equalsIgnoreCase(displayChar) ? "X"
1341                 : ("*".equals(displayChar) ? "STOP"
1342                         : ResidueProperties.aa2Triplet.get(displayChar));
1343       }
1344       text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
1345               .append(": ").append(residue == null ? displayChar : residue);
1346
1347       text.append(" (").append(Integer.toString(residuePos)).append(")");
1348     }
1349     ap.alignFrame.setStatus(text.toString());
1350   }
1351
1352   /**
1353    * Set the status bar message to highlight the first matched position in
1354    * search results.
1355    * 
1356    * @param results
1357    */
1358   private void setStatusMessage(SearchResultsI results)
1359   {
1360     AlignmentI al = this.av.getAlignment();
1361     int sequenceIndex = al.findIndex(results);
1362     if (sequenceIndex == -1)
1363     {
1364       return;
1365     }
1366     SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
1367     SequenceI ds = alignedSeq.getDatasetSequence();
1368     for (SearchResultMatchI m : results.getResults())
1369     {
1370       SequenceI seq = m.getSequence();
1371       if (seq.getDatasetSequence() != null)
1372       {
1373         seq = seq.getDatasetSequence();
1374       }
1375
1376       if (seq == ds)
1377       {
1378         int start = m.getStart();
1379         setStatusMessage(alignedSeq.getName(), sequenceIndex,
1380                 seq.getCharAt(start - 1), start);
1381         return;
1382       }
1383     }
1384   }
1385
1386   /**
1387    * {@inheritDoc}
1388    */
1389   @Override
1390   public void mouseDragged(MouseEvent evt)
1391   {
1392     MousePos pos = findMousePosition(evt);
1393     if (pos.isOverAnnotation() || pos.column == -1)
1394     {
1395       return;
1396     }
1397
1398     if (mouseWheelPressed)
1399     {
1400       boolean inSplitFrame = ap.av.getCodingComplement() != null;
1401       boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
1402
1403       int oldWidth = av.getCharWidth();
1404
1405       // Which is bigger, left-right or up-down?
1406       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math
1407               .abs(evt.getX() - lastMousePress.getX()))
1408       {
1409         /*
1410          * on drag up or down, decrement or increment font size
1411          */
1412         int fontSize = av.font.getSize();
1413         boolean fontChanged = false;
1414
1415         if (evt.getY() < lastMousePress.getY())
1416         {
1417           fontChanged = true;
1418           fontSize--;
1419         }
1420         else if (evt.getY() > lastMousePress.getY())
1421         {
1422           fontChanged = true;
1423           fontSize++;
1424         }
1425
1426         if (fontSize < 1)
1427         {
1428           fontSize = 1;
1429         }
1430
1431         if (fontChanged)
1432         {
1433           Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1434                   fontSize);
1435           av.setFont(newFont, true);
1436           av.setCharWidth(oldWidth);
1437           ap.fontChanged();
1438           if (copyChanges)
1439           {
1440             ap.av.getCodingComplement().setFont(newFont, true);
1441             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1442                     .getSplitViewContainer();
1443             splitFrame.adjustLayout();
1444             splitFrame.repaint();
1445           }
1446         }
1447       }
1448       else
1449       {
1450         /*
1451          * on drag left or right, decrement or increment character width
1452          */
1453         int newWidth = 0;
1454         if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1455         {
1456           newWidth = av.getCharWidth() - 1;
1457           av.setCharWidth(newWidth);
1458         }
1459         else if (evt.getX() > lastMousePress.getX())
1460         {
1461           newWidth = av.getCharWidth() + 1;
1462           av.setCharWidth(newWidth);
1463         }
1464         if (newWidth > 0)
1465         {
1466           ap.paintAlignment(false, false);
1467           if (copyChanges)
1468           {
1469             /*
1470              * need to ensure newWidth is set on cdna, regardless of which
1471              * panel the mouse drag happened in; protein will compute its 
1472              * character width as 1:1 or 3:1
1473              */
1474             av.getCodingComplement().setCharWidth(newWidth);
1475             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1476                     .getSplitViewContainer();
1477             splitFrame.adjustLayout();
1478             splitFrame.repaint();
1479           }
1480         }
1481       }
1482
1483       FontMetrics fm = getFontMetrics(av.getFont());
1484       av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1485
1486       lastMousePress = evt.getPoint();
1487
1488       return;
1489     }
1490
1491     if (!editingSeqs)
1492     {
1493       dragStretchGroup(evt);
1494       return;
1495     }
1496
1497     int res = pos.column;
1498
1499     if (res < 0)
1500     {
1501       res = 0;
1502     }
1503
1504     if ((editLastRes == -1) || (editLastRes == res))
1505     {
1506       return;
1507     }
1508
1509     if ((res < av.getAlignment().getWidth()) && (res < editLastRes))
1510     {
1511       // dragLeft, delete gap
1512       editSequence(false, false, res);
1513     }
1514     else
1515     {
1516       editSequence(true, false, res);
1517     }
1518
1519     mouseDragging = true;
1520     if (scrollThread != null)
1521     {
1522       scrollThread.setMousePosition(evt.getPoint());
1523     }
1524   }
1525
1526   /**
1527    * Edits the sequence to insert or delete one or more gaps, in response to a
1528    * mouse drag or cursor mode command. The number of inserts/deletes may be
1529    * specified with the cursor command, or else depends on the mouse event
1530    * (normally one column, but potentially more for a fast mouse drag).
1531    * <p>
1532    * Delete gaps is limited to the number of gaps left of the cursor position
1533    * (mouse drag), or at or right of the cursor position (cursor mode).
1534    * <p>
1535    * In group editing mode (Ctrl or Cmd down), the edit acts on all sequences in
1536    * the current selection group.
1537    * <p>
1538    * In locked editing mode (with a selection group present), inserts/deletions
1539    * within the selection group are limited to its boundaries (and edits outside
1540    * the group stop at its border).
1541    * 
1542    * @param insertGap
1543    *          true to insert gaps, false to delete gaps
1544    * @param editSeq
1545    *          (unused parameter)
1546    * @param startres
1547    *          the column at which to perform the action; the number of columns
1548    *          affected depends on <code>this.editLastRes</code> (cursor column
1549    *          position)
1550    */
1551   synchronized void editSequence(boolean insertGap, boolean editSeq,
1552           final int startres)
1553   {
1554     int fixedLeft = -1;
1555     int fixedRight = -1;
1556     boolean fixedColumns = false;
1557     SequenceGroup sg = av.getSelectionGroup();
1558
1559     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1560
1561     // No group, but the sequence may represent a group
1562     if (!groupEditing && av.hasHiddenRows())
1563     {
1564       if (av.isHiddenRepSequence(seq))
1565       {
1566         sg = av.getRepresentedSequences(seq);
1567         groupEditing = true;
1568       }
1569     }
1570
1571     StringBuilder message = new StringBuilder(64); // for status bar
1572
1573     /*
1574      * make a name for the edit action, for
1575      * status bar message and Undo/Redo menu
1576      */
1577     String label = null;
1578     if (groupEditing)
1579     {
1580       message.append("Edit group:");
1581       label = MessageManager.getString("action.edit_group");
1582     }
1583     else
1584     {
1585       message.append("Edit sequence: " + seq.getName());
1586       label = seq.getName();
1587       if (label.length() > 10)
1588       {
1589         label = label.substring(0, 10);
1590       }
1591       label = MessageManager.formatMessage("label.edit_params",
1592               new String[]
1593               { label });
1594     }
1595
1596     /*
1597      * initialise the edit command if there is not
1598      * already one being extended
1599      */
1600     if (editCommand == null)
1601     {
1602       editCommand = new EditCommand(label);
1603     }
1604
1605     if (insertGap)
1606     {
1607       message.append(" insert ");
1608     }
1609     else
1610     {
1611       message.append(" delete ");
1612     }
1613
1614     message.append(Math.abs(startres - editLastRes) + " gaps.");
1615     ap.alignFrame.setStatus(message.toString());
1616
1617     /*
1618      * is there a selection group containing the sequence being edited?
1619      * if so the boundary of the group is the limit of the edit
1620      * (but the edit may be inside or outside the selection group)
1621      */
1622     boolean inSelectionGroup = sg != null
1623             && sg.getSequences(av.getHiddenRepSequences()).contains(seq);
1624     if (groupEditing || inSelectionGroup)
1625     {
1626       fixedColumns = true;
1627
1628       // sg might be null as the user may only see 1 sequence,
1629       // but the sequence represents a group
1630       if (sg == null)
1631       {
1632         if (!av.isHiddenRepSequence(seq))
1633         {
1634           endEditing();
1635           return;
1636         }
1637         sg = av.getRepresentedSequences(seq);
1638       }
1639
1640       fixedLeft = sg.getStartRes();
1641       fixedRight = sg.getEndRes();
1642
1643       if ((startres < fixedLeft && editLastRes >= fixedLeft)
1644               || (startres >= fixedLeft && editLastRes < fixedLeft)
1645               || (startres > fixedRight && editLastRes <= fixedRight)
1646               || (startres <= fixedRight && editLastRes > fixedRight))
1647       {
1648         endEditing();
1649         return;
1650       }
1651
1652       if (fixedLeft > startres)
1653       {
1654         fixedRight = fixedLeft - 1;
1655         fixedLeft = 0;
1656       }
1657       else if (fixedRight < startres)
1658       {
1659         fixedLeft = fixedRight;
1660         fixedRight = -1;
1661       }
1662     }
1663
1664     if (av.hasHiddenColumns())
1665     {
1666       fixedColumns = true;
1667       int y1 = av.getAlignment().getHiddenColumns()
1668               .getNextHiddenBoundary(true, startres);
1669       int y2 = av.getAlignment().getHiddenColumns()
1670               .getNextHiddenBoundary(false, startres);
1671
1672       if ((insertGap && startres > y1 && editLastRes < y1)
1673               || (!insertGap && startres < y2 && editLastRes > y2))
1674       {
1675         endEditing();
1676         return;
1677       }
1678
1679       // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1680       // Selection spans a hidden region
1681       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1682       {
1683         if (startres >= y2)
1684         {
1685           fixedLeft = y2;
1686         }
1687         else
1688         {
1689           fixedRight = y2 - 1;
1690         }
1691       }
1692     }
1693
1694     boolean success = doEditSequence(insertGap, editSeq, startres,
1695             fixedRight, fixedColumns, sg);
1696
1697     /*
1698      * report what actually happened (might be less than
1699      * what was requested), by inspecting the edit commands added
1700      */
1701     String msg = getEditStatusMessage(editCommand);
1702     ap.alignFrame.setStatus(msg == null ? " " : msg);
1703     if (!success)
1704     {
1705       endEditing();
1706     }
1707
1708     editLastRes = startres;
1709     seqCanvas.repaint();
1710   }
1711
1712   /**
1713    * A helper method that performs the requested editing to insert or delete
1714    * gaps (if possible). Answers true if the edit was successful, false if could
1715    * only be performed in part or not at all. Failure may occur in 'locked edit'
1716    * mode, when an insertion requires a matching gapped position (or column) to
1717    * delete, and deletion requires an adjacent gapped position (or column) to
1718    * remove.
1719    * 
1720    * @param insertGap
1721    *          true if inserting gap(s), false if deleting
1722    * @param editSeq
1723    *          (unused parameter, currently always false)
1724    * @param startres
1725    *          the column at which to perform the edit
1726    * @param fixedRight
1727    *          fixed right boundary column of a locked edit (within or to the
1728    *          left of a selection group)
1729    * @param fixedColumns
1730    *          true if this is a locked edit
1731    * @param sg
1732    *          the sequence group (if group edit is being performed)
1733    * @return
1734    */
1735   protected boolean doEditSequence(final boolean insertGap,
1736           final boolean editSeq, final int startres, int fixedRight,
1737           final boolean fixedColumns, final SequenceGroup sg)
1738   {
1739     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1740     SequenceI[] seqs = new SequenceI[] { seq };
1741
1742     if (groupEditing)
1743     {
1744       List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1745       int g, groupSize = vseqs.size();
1746       SequenceI[] groupSeqs = new SequenceI[groupSize];
1747       for (g = 0; g < groupSeqs.length; g++)
1748       {
1749         groupSeqs[g] = vseqs.get(g);
1750       }
1751
1752       // drag to right
1753       if (insertGap)
1754       {
1755         // If the user has selected the whole sequence, and is dragging to
1756         // the right, we can still extend the alignment and selectionGroup
1757         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1758                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1759         {
1760           sg.setEndRes(
1761                   av.getAlignment().getWidth() + startres - editLastRes);
1762           fixedRight = sg.getEndRes();
1763         }
1764
1765         // Is it valid with fixed columns??
1766         // Find the next gap before the end
1767         // of the visible region boundary
1768         boolean blank = false;
1769         for (; fixedRight > editLastRes; fixedRight--)
1770         {
1771           blank = true;
1772
1773           for (g = 0; g < groupSize; g++)
1774           {
1775             for (int j = 0; j < startres - editLastRes; j++)
1776             {
1777               if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1778               {
1779                 blank = false;
1780                 break;
1781               }
1782             }
1783           }
1784           if (blank)
1785           {
1786             break;
1787           }
1788         }
1789
1790         if (!blank)
1791         {
1792           if (sg.getSize() == av.getAlignment().getHeight())
1793           {
1794             if ((av.hasHiddenColumns()
1795                     && startres < av.getAlignment().getHiddenColumns()
1796                             .getNextHiddenBoundary(false, startres)))
1797             {
1798               return false;
1799             }
1800
1801             int alWidth = av.getAlignment().getWidth();
1802             if (av.hasHiddenRows())
1803             {
1804               int hwidth = av.getAlignment().getHiddenSequences()
1805                       .getWidth();
1806               if (hwidth > alWidth)
1807               {
1808                 alWidth = hwidth;
1809               }
1810             }
1811             // We can still insert gaps if the selectionGroup
1812             // contains all the sequences
1813             sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1814             fixedRight = alWidth + startres - editLastRes;
1815           }
1816           else
1817           {
1818             return false;
1819           }
1820         }
1821       }
1822
1823       // drag to left
1824       else if (!insertGap)
1825       {
1826         // / Are we able to delete?
1827         // ie are all columns blank?
1828
1829         for (g = 0; g < groupSize; g++)
1830         {
1831           for (int j = startres; j < editLastRes; j++)
1832           {
1833             if (groupSeqs[g].getLength() <= j)
1834             {
1835               continue;
1836             }
1837
1838             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1839             {
1840               // Not a gap, block edit not valid
1841               return false;
1842             }
1843           }
1844         }
1845       }
1846
1847       if (insertGap)
1848       {
1849         // dragging to the right
1850         if (fixedColumns && fixedRight != -1)
1851         {
1852           for (int j = editLastRes; j < startres; j++)
1853           {
1854             insertGap(j, groupSeqs, fixedRight);
1855           }
1856         }
1857         else
1858         {
1859           appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1860                   startres - editLastRes, false);
1861         }
1862       }
1863       else
1864       {
1865         // dragging to the left
1866         if (fixedColumns && fixedRight != -1)
1867         {
1868           for (int j = editLastRes; j > startres; j--)
1869           {
1870             deleteChar(startres, groupSeqs, fixedRight);
1871           }
1872         }
1873         else
1874         {
1875           appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1876                   editLastRes - startres, false);
1877         }
1878       }
1879     }
1880     else
1881     {
1882       /*
1883        * editing a single sequence
1884        */
1885       if (insertGap)
1886       {
1887         // dragging to the right
1888         if (fixedColumns && fixedRight != -1)
1889         {
1890           for (int j = editLastRes; j < startres; j++)
1891           {
1892             if (!insertGap(j, seqs, fixedRight))
1893             {
1894               /*
1895                * e.g. cursor mode command specified 
1896                * more inserts than are possible
1897                */
1898               return false;
1899             }
1900           }
1901         }
1902         else
1903         {
1904           appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1905                   startres - editLastRes, false);
1906         }
1907       }
1908       else
1909       {
1910         if (!editSeq)
1911         {
1912           // dragging to the left
1913           if (fixedColumns && fixedRight != -1)
1914           {
1915             for (int j = editLastRes; j > startres; j--)
1916             {
1917               if (!Comparison.isGap(seq.getCharAt(startres)))
1918               {
1919                 return false;
1920               }
1921               deleteChar(startres, seqs, fixedRight);
1922             }
1923           }
1924           else
1925           {
1926             // could be a keyboard edit trying to delete none gaps
1927             int max = 0;
1928             for (int m = startres; m < editLastRes; m++)
1929             {
1930               if (!Comparison.isGap(seq.getCharAt(m)))
1931               {
1932                 break;
1933               }
1934               max++;
1935             }
1936             if (max > 0)
1937             {
1938               appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1939             }
1940           }
1941         }
1942         else
1943         {// insertGap==false AND editSeq==TRUE;
1944           if (fixedColumns && fixedRight != -1)
1945           {
1946             for (int j = editLastRes; j < startres; j++)
1947             {
1948               insertGap(j, seqs, fixedRight);
1949             }
1950           }
1951           else
1952           {
1953             appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1954                     startres - editLastRes, false);
1955           }
1956         }
1957       }
1958     }
1959
1960     return true;
1961   }
1962
1963   /**
1964    * Constructs an informative status bar message while dragging to insert or
1965    * delete gaps. Answers null if inserts and deletes cancel out.
1966    * 
1967    * @param editCommand
1968    *          a command containing the list of individual edits
1969    * @return
1970    */
1971   protected static String getEditStatusMessage(EditCommand editCommand)
1972   {
1973     if (editCommand == null)
1974     {
1975       return null;
1976     }
1977
1978     /*
1979      * add any inserts, and subtract any deletes,  
1980      * not counting those auto-inserted when doing a 'locked edit'
1981      * (so only counting edits 'under the cursor')
1982      */
1983     int count = 0;
1984     for (Edit cmd : editCommand.getEdits())
1985     {
1986       if (!cmd.isSystemGenerated())
1987       {
1988         count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1989                 : -cmd.getNumber();
1990       }
1991     }
1992
1993     if (count == 0)
1994     {
1995       /*
1996        * inserts and deletes cancel out
1997        */
1998       return null;
1999     }
2000
2001     String msgKey = count > 1 ? "label.insert_gaps"
2002             : (count == 1 ? "label.insert_gap"
2003                     : (count == -1 ? "label.delete_gap"
2004                             : "label.delete_gaps"));
2005     count = Math.abs(count);
2006
2007     return MessageManager.formatMessage(msgKey, String.valueOf(count));
2008   }
2009
2010   /**
2011    * Inserts one gap at column j, deleting the right-most gapped column up to
2012    * (and including) fixedColumn. Returns true if the edit is successful, false
2013    * if no blank column is available to allow the insertion to be balanced by a
2014    * deletion.
2015    * 
2016    * @param j
2017    * @param seq
2018    * @param fixedColumn
2019    * @return
2020    */
2021   boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
2022   {
2023     int blankColumn = fixedColumn;
2024     for (int s = 0; s < seq.length; s++)
2025     {
2026       // Find the next gap before the end of the visible region boundary
2027       // If lastCol > j, theres a boundary after the gap insertion
2028
2029       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
2030       {
2031         if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
2032         {
2033           // Theres a space, so break and insert the gap
2034           break;
2035         }
2036       }
2037
2038       if (blankColumn <= j)
2039       {
2040         blankColumn = fixedColumn;
2041         endEditing();
2042         return false;
2043       }
2044     }
2045
2046     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
2047
2048     appendEdit(Action.INSERT_GAP, seq, j, 1, false);
2049
2050     return true;
2051   }
2052
2053   /**
2054    * Helper method to add and perform one edit action
2055    * 
2056    * @param action
2057    * @param seq
2058    * @param pos
2059    * @param count
2060    * @param systemGenerated
2061    *          true if the edit is a 'balancing' delete (or insert) to match a
2062    *          user's insert (or delete) in a locked editing region
2063    */
2064   protected void appendEdit(Action action, SequenceI[] seq, int pos,
2065           int count, boolean systemGenerated)
2066   {
2067
2068     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2069             av.getAlignment().getGapCharacter());
2070     edit.setSystemGenerated(systemGenerated);
2071
2072     editCommand.appendEdit(edit, av.getAlignment(), true, null);
2073   }
2074
2075   /**
2076    * Deletes the character at column j, and inserts a gap at fixedColumn, in
2077    * each of the given sequences. The caller should ensure that all sequences
2078    * are gapped in column j.
2079    * 
2080    * @param j
2081    * @param seqs
2082    * @param fixedColumn
2083    */
2084   void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2085   {
2086     appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2087
2088     appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2089   }
2090
2091   /**
2092    * On reentering the panel, stops any scrolling that was started on dragging
2093    * out of the panel
2094    * 
2095    * @param e
2096    */
2097   @Override
2098   public void mouseEntered(MouseEvent e)
2099   {
2100     if (oldSeq < 0)
2101     {
2102       oldSeq = 0;
2103     }
2104     stopScrolling();
2105   }
2106
2107   /**
2108    * On leaving the panel, if the mouse is being dragged, starts a thread to
2109    * scroll it until the mouse is released (in unwrapped mode only)
2110    * 
2111    * @param e
2112    */
2113   @Override
2114   public void mouseExited(MouseEvent e)
2115   {
2116     lastMousePosition = null;
2117     ap.alignFrame.setStatus(" ");
2118     if (av.getWrapAlignment())
2119     {
2120       return;
2121     }
2122
2123     if (mouseDragging && scrollThread == null)
2124     {
2125       startScrolling(e.getPoint());
2126     }
2127   }
2128
2129   /**
2130    * Handler for double-click on a position with one or more sequence features.
2131    * Opens the Amend Features dialog to allow feature details to be amended, or
2132    * the feature deleted.
2133    */
2134   @Override
2135   public void mouseClicked(MouseEvent evt)
2136   {
2137     SequenceGroup sg = null;
2138     MousePos pos = findMousePosition(evt);
2139     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2140     {
2141       return;
2142     }
2143
2144     if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2145     {
2146       sg = av.getSelectionGroup();
2147       if (sg != null && sg.getSize() == 1
2148               && sg.getEndRes() - sg.getStartRes() < 2)
2149       {
2150         av.setSelectionGroup(null);
2151       }
2152
2153       int column = pos.column;
2154
2155       /*
2156        * find features at the position (if not gapped), or straddling
2157        * the position (if at a gap)
2158        */
2159       SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2160       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2161               .findFeaturesAtColumn(sequence, column + 1);
2162
2163       if (!features.isEmpty())
2164       {
2165         /*
2166          * highlight the first feature at the position on the alignment
2167          */
2168         SearchResultsI highlight = new SearchResults();
2169         highlight.addResult(sequence, features.get(0).getBegin(),
2170                 features.get(0).getEnd());
2171         seqCanvas.highlightSearchResults(highlight, true);
2172
2173         /*
2174          * open the Amend Features dialog
2175          */
2176         new FeatureEditor(ap, Collections.singletonList(sequence), features,
2177                 false).showDialog();
2178       }
2179     }
2180   }
2181
2182   @Override
2183   public void mouseWheelMoved(MouseWheelEvent e)
2184   {
2185     e.consume();
2186     double wheelRotation = e.getPreciseWheelRotation();
2187     if (wheelRotation > 0)
2188     {
2189       if (e.isShiftDown())
2190       {
2191         av.getRanges().scrollRight(true);
2192
2193       }
2194       else
2195       {
2196         av.getRanges().scrollUp(false);
2197       }
2198     }
2199     else if (wheelRotation < 0)
2200     {
2201       if (e.isShiftDown())
2202       {
2203         av.getRanges().scrollRight(false);
2204       }
2205       else
2206       {
2207         av.getRanges().scrollUp(true);
2208       }
2209     }
2210
2211     /*
2212      * update status bar and tooltip for new position
2213      * (need to synthesize a mouse movement to refresh tooltip)
2214      */
2215     mouseMoved(e);
2216     ToolTipManager.sharedInstance().mouseMoved(e);
2217   }
2218
2219   /**
2220    * DOCUMENT ME!
2221    * 
2222    * @param pos
2223    *          DOCUMENT ME!
2224    */
2225   protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2226   {
2227     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2228     {
2229       return;
2230     }
2231
2232     final int res = pos.column;
2233     final int seq = pos.seqIndex;
2234     oldSeq = seq;
2235     updateOverviewAndStructs = false;
2236
2237     startWrapBlock = wrappedBlock;
2238
2239     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2240
2241     if ((sequence == null) || (res > sequence.getLength()))
2242     {
2243       return;
2244     }
2245
2246     stretchGroup = av.getSelectionGroup();
2247
2248     if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2249     {
2250       stretchGroup = av.getAlignment().findGroup(sequence, res);
2251       if (stretchGroup != null)
2252       {
2253         // only update the current selection if the popup menu has a group to
2254         // focus on
2255         av.setSelectionGroup(stretchGroup);
2256       }
2257     }
2258
2259     /*
2260      * defer right-mouse click handling to mouseReleased on Windows
2261      * (where isPopupTrigger() will answer true)
2262      * NB isRightMouseButton is also true for Cmd-click on Mac
2263      */
2264     if (Platform.isWinRightButton(evt))
2265     {
2266       return;
2267     }
2268
2269     if (evt.isPopupTrigger()) // Mac: mousePressed
2270     {
2271       showPopupMenu(evt, pos);
2272       return;
2273     }
2274
2275     if (av.cursorMode)
2276     {
2277       seqCanvas.cursorX = res;
2278       seqCanvas.cursorY = seq;
2279       seqCanvas.repaint();
2280       return;
2281     }
2282
2283     if (stretchGroup == null)
2284     {
2285       createStretchGroup(res, sequence);
2286     }
2287
2288     if (stretchGroup != null)
2289     {
2290       stretchGroup.addPropertyChangeListener(seqCanvas);
2291     }
2292
2293     seqCanvas.repaint();
2294   }
2295
2296   private void createStretchGroup(int res, SequenceI sequence)
2297   {
2298     // Only if left mouse button do we want to change group sizes
2299     // define a new group here
2300     SequenceGroup sg = new SequenceGroup();
2301     sg.setStartRes(res);
2302     sg.setEndRes(res);
2303     sg.addSequence(sequence, false);
2304     av.setSelectionGroup(sg);
2305     stretchGroup = sg;
2306
2307     if (av.getConservationSelected())
2308     {
2309       SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2310               ap.getViewName());
2311     }
2312
2313     if (av.getAbovePIDThreshold())
2314     {
2315       SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2316               ap.getViewName());
2317     }
2318     // TODO: stretchGroup will always be not null. Is this a merge error ?
2319     // or is there a threading issue here?
2320     if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2321     {
2322       // Edit end res position of selected group
2323       changeEndRes = true;
2324     }
2325     else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2326     {
2327       // Edit end res position of selected group
2328       changeStartRes = true;
2329     }
2330     stretchGroup.getWidth();
2331
2332   }
2333
2334   /**
2335    * Build and show a pop-up menu at the right-click mouse position
2336    *
2337    * @param evt
2338    * @param pos
2339    */
2340   void showPopupMenu(MouseEvent evt, MousePos pos)
2341   {
2342     final int column = pos.column;
2343     final int seq = pos.seqIndex;
2344     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2345     if (sequence != null)
2346     {
2347       PopupMenu pop = new PopupMenu(ap, sequence, column);
2348       pop.show(this, evt.getX(), evt.getY());
2349     }
2350   }
2351
2352   /**
2353    * Update the display after mouse up on a selection or group
2354    * 
2355    * @param evt
2356    *          mouse released event details
2357    * @param afterDrag
2358    *          true if this event is happening after a mouse drag (rather than a
2359    *          mouse down)
2360    */
2361   protected void doMouseReleasedDefineMode(MouseEvent evt,
2362           boolean afterDrag)
2363   {
2364     if (stretchGroup == null)
2365     {
2366       return;
2367     }
2368
2369     stretchGroup.removePropertyChangeListener(seqCanvas);
2370
2371     // always do this - annotation has own state
2372     // but defer colourscheme update until hidden sequences are passed in
2373     boolean vischange = stretchGroup.recalcConservation(true);
2374     updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2375             && afterDrag;
2376     if (stretchGroup.cs != null)
2377     {
2378       if (afterDrag)
2379       {
2380         stretchGroup.cs.alignmentChanged(stretchGroup,
2381                 av.getHiddenRepSequences());
2382       }
2383
2384       ResidueShaderI groupColourScheme = stretchGroup
2385               .getGroupColourScheme();
2386       String name = stretchGroup.getName();
2387       if (stretchGroup.cs.conservationApplied())
2388       {
2389         SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2390       }
2391       if (stretchGroup.cs.getThreshold() > 0)
2392       {
2393         SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2394       }
2395     }
2396     PaintRefresher.Refresh(this, av.getSequenceSetId());
2397     // TODO: structure colours only need updating if stretchGroup used to or now
2398     // does contain sequences with structure views
2399     ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2400     updateOverviewAndStructs = false;
2401     changeEndRes = false;
2402     changeStartRes = false;
2403     stretchGroup = null;
2404     av.sendSelection();
2405   }
2406
2407   /**
2408    * Resizes the borders of a selection group depending on the direction of
2409    * mouse drag
2410    * 
2411    * @param evt
2412    */
2413   protected void dragStretchGroup(MouseEvent evt)
2414   {
2415     if (stretchGroup == null)
2416     {
2417       return;
2418     }
2419
2420     MousePos pos = findMousePosition(evt);
2421     if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2422     {
2423       return;
2424     }
2425
2426     int res = pos.column;
2427     int y = pos.seqIndex;
2428
2429     if (wrappedBlock != startWrapBlock)
2430     {
2431       return;
2432     }
2433
2434     res = Math.min(res, av.getAlignment().getWidth() - 1);
2435
2436     if (stretchGroup.getEndRes() == res)
2437     {
2438       // Edit end res position of selected group
2439       changeEndRes = true;
2440     }
2441     else if (stretchGroup.getStartRes() == res)
2442     {
2443       // Edit start res position of selected group
2444       changeStartRes = true;
2445     }
2446
2447     if (res < av.getRanges().getStartRes())
2448     {
2449       res = av.getRanges().getStartRes();
2450     }
2451
2452     if (changeEndRes)
2453     {
2454       if (res > (stretchGroup.getStartRes() - 1))
2455       {
2456         stretchGroup.setEndRes(res);
2457         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2458       }
2459     }
2460     else if (changeStartRes)
2461     {
2462       if (res < (stretchGroup.getEndRes() + 1))
2463       {
2464         stretchGroup.setStartRes(res);
2465         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2466       }
2467     }
2468
2469     int dragDirection = 0;
2470
2471     if (y > oldSeq)
2472     {
2473       dragDirection = 1;
2474     }
2475     else if (y < oldSeq)
2476     {
2477       dragDirection = -1;
2478     }
2479
2480     while ((y != oldSeq) && (oldSeq > -1)
2481             && (y < av.getAlignment().getHeight()))
2482     {
2483       // This routine ensures we don't skip any sequences, as the
2484       // selection is quite slow.
2485       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2486
2487       oldSeq += dragDirection;
2488
2489       if (oldSeq < 0)
2490       {
2491         break;
2492       }
2493
2494       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2495
2496       if (stretchGroup.getSequences(null).contains(nextSeq))
2497       {
2498         stretchGroup.deleteSequence(seq, false);
2499         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2500       }
2501       else
2502       {
2503         if (seq != null)
2504         {
2505           stretchGroup.addSequence(seq, false);
2506         }
2507
2508         stretchGroup.addSequence(nextSeq, false);
2509         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2510       }
2511     }
2512
2513     if (oldSeq < 0)
2514     {
2515       oldSeq = -1;
2516     }
2517
2518     mouseDragging = true;
2519
2520     if (scrollThread != null)
2521     {
2522       scrollThread.setMousePosition(evt.getPoint());
2523     }
2524
2525     /*
2526      * construct a status message showing the range of the selection
2527      */
2528     StringBuilder status = new StringBuilder(64);
2529     List<SequenceI> seqs = stretchGroup.getSequences();
2530     String name = seqs.get(0).getName();
2531     if (name.length() > 20)
2532     {
2533       name = name.substring(0, 20);
2534     }
2535     status.append(name).append(" - ");
2536     name = seqs.get(seqs.size() - 1).getName();
2537     if (name.length() > 20)
2538     {
2539       name = name.substring(0, 20);
2540     }
2541     status.append(name).append(" ");
2542     int startRes = stretchGroup.getStartRes();
2543     status.append(" cols ").append(String.valueOf(startRes + 1))
2544             .append("-");
2545     int endRes = stretchGroup.getEndRes();
2546     status.append(String.valueOf(endRes + 1));
2547     status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2548             .append(String.valueOf(endRes - startRes + 1)).append(")");
2549     ap.alignFrame.setStatus(status.toString());
2550   }
2551
2552   /**
2553    * Stops the scroll thread if it is running
2554    */
2555   void stopScrolling()
2556   {
2557     if (scrollThread != null)
2558     {
2559       scrollThread.stopScrolling();
2560       scrollThread = null;
2561     }
2562     mouseDragging = false;
2563   }
2564
2565   /**
2566    * Starts a thread to scroll the alignment, towards a given mouse position
2567    * outside the panel bounds, unless the alignment is in wrapped mode
2568    * 
2569    * @param mousePos
2570    */
2571   void startScrolling(Point mousePos)
2572   {
2573     /*
2574      * set this.mouseDragging in case this was called from 
2575      * a drag in ScalePanel or AnnotationPanel
2576      */
2577     mouseDragging = true;
2578     if (!av.getWrapAlignment() && scrollThread == null)
2579     {
2580       scrollThread = new ScrollThread();
2581       scrollThread.setMousePosition(mousePos);
2582       if (Platform.isJS())
2583       {
2584         /*
2585          * Javascript - run every 20ms until scrolling stopped
2586          * or reaches the limit of scrollable alignment
2587          */
2588         Timer t = new Timer(20, new ActionListener()
2589         {
2590           @Override
2591           public void actionPerformed(ActionEvent e)
2592           {
2593             if (scrollThread != null)
2594             {
2595               // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2596               scrollThread.scrollOnce();
2597             }
2598           }
2599         });
2600         t.addActionListener(new ActionListener()
2601         {
2602           @Override
2603           public void actionPerformed(ActionEvent e)
2604           {
2605             if (scrollThread == null)
2606             {
2607               // SeqPanel.stopScrolling called
2608               t.stop();
2609             }
2610           }
2611         });
2612         t.start();
2613       }
2614       else
2615       {
2616         /*
2617          * Java - run in a new thread
2618          */
2619         scrollThread.start();
2620       }
2621     }
2622   }
2623
2624   /**
2625    * Performs scrolling of the visible alignment left, right, up or down, until
2626    * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2627    * limit of the alignment is reached
2628    */
2629   class ScrollThread extends Thread
2630   {
2631     private Point mousePos;
2632
2633     private volatile boolean keepRunning = true;
2634
2635     /**
2636      * Constructor
2637      */
2638     public ScrollThread()
2639     {
2640       setName("SeqPanel$ScrollThread");
2641     }
2642
2643     /**
2644      * Sets the position of the mouse that determines the direction of the
2645      * scroll to perform. If this is called as the mouse moves, scrolling should
2646      * respond accordingly. For example, if the mouse is dragged right, scroll
2647      * right should start; if the drag continues down, scroll down should also
2648      * happen.
2649      * 
2650      * @param p
2651      */
2652     public void setMousePosition(Point p)
2653     {
2654       mousePos = p;
2655     }
2656
2657     /**
2658      * Sets a flag that will cause the thread to exit
2659      */
2660     public void stopScrolling()
2661     {
2662       keepRunning = false;
2663     }
2664
2665     /**
2666      * Scrolls the alignment left or right, and/or up or down, depending on the
2667      * last notified mouse position, until the limit of the alignment is
2668      * reached, or a flag is set to stop the scroll
2669      */
2670     @Override
2671     public void run()
2672     {
2673       while (keepRunning)
2674       {
2675         if (mousePos != null)
2676         {
2677           keepRunning = scrollOnce();
2678         }
2679         try
2680         {
2681           Thread.sleep(20);
2682         } catch (Exception ex)
2683         {
2684         }
2685       }
2686       SeqPanel.this.scrollThread = null;
2687     }
2688
2689     /**
2690      * Scrolls
2691      * <ul>
2692      * <li>one row up, if the mouse is above the panel</li>
2693      * <li>one row down, if the mouse is below the panel</li>
2694      * <li>one column left, if the mouse is left of the panel</li>
2695      * <li>one column right, if the mouse is right of the panel</li>
2696      * </ul>
2697      * Answers true if a scroll was performed, false if not - meaning either
2698      * that the mouse position is within the panel, or the edge of the alignment
2699      * has been reached.
2700      */
2701     boolean scrollOnce()
2702     {
2703       /*
2704        * quit after mouseUp ensures interrupt in JalviewJS
2705        */
2706       if (!mouseDragging)
2707       {
2708         return false;
2709       }
2710
2711       boolean scrolled = false;
2712       ViewportRanges ranges = SeqPanel.this.av.getRanges();
2713
2714       /*
2715        * scroll up or down
2716        */
2717       if (mousePos.y < 0)
2718       {
2719         // mouse is above this panel - try scroll up
2720         scrolled = ranges.scrollUp(true);
2721       }
2722       else if (mousePos.y >= getHeight())
2723       {
2724         // mouse is below this panel - try scroll down
2725         scrolled = ranges.scrollUp(false);
2726       }
2727
2728       /*
2729        * scroll left or right
2730        */
2731       if (mousePos.x < 0)
2732       {
2733         scrolled |= ranges.scrollRight(false);
2734       }
2735       else if (mousePos.x >= getWidth())
2736       {
2737         scrolled |= ranges.scrollRight(true);
2738       }
2739       return scrolled;
2740     }
2741   }
2742
2743   /**
2744    * modify current selection according to a received message.
2745    */
2746   @Override
2747   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2748           HiddenColumns hidden, SelectionSource source)
2749   {
2750     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2751     // handles selection messages...
2752     // TODO: extend config options to allow user to control if selections may be
2753     // shared between viewports.
2754     boolean iSentTheSelection = (av == source
2755             || (source instanceof AlignViewport
2756                     && ((AlignmentViewport) source).getSequenceSetId()
2757                             .equals(av.getSequenceSetId())));
2758
2759     if (iSentTheSelection)
2760     {
2761       // respond to our own event by updating dependent dialogs
2762       if (ap.getCalculationDialog() != null)
2763       {
2764         ap.getCalculationDialog().validateCalcTypes();
2765       }
2766
2767       return;
2768     }
2769
2770     // process further ?
2771     if (!av.followSelection)
2772     {
2773       return;
2774     }
2775
2776     /*
2777      * Ignore the selection if there is one of our own pending.
2778      */
2779     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2780     {
2781       return;
2782     }
2783
2784     /*
2785      * Check for selection in a view of which this one is a dna/protein
2786      * complement.
2787      */
2788     if (selectionFromTranslation(seqsel, colsel, hidden, source))
2789     {
2790       return;
2791     }
2792
2793     // do we want to thread this ? (contention with seqsel and colsel locks, I
2794     // suspect)
2795     /*
2796      * only copy colsel if there is a real intersection between
2797      * sequence selection and this panel's alignment
2798      */
2799     boolean repaint = false;
2800     boolean copycolsel = false;
2801
2802     SequenceGroup sgroup = null;
2803     if (seqsel != null && seqsel.getSize() > 0)
2804     {
2805       if (av.getAlignment() == null)
2806       {
2807         Console.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2808                 + " ViewId=" + av.getViewId()
2809                 + " 's alignment is NULL! returning immediately.");
2810         return;
2811       }
2812       sgroup = seqsel.intersect(av.getAlignment(),
2813               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2814       if ((sgroup != null && sgroup.getSize() > 0))
2815       {
2816         copycolsel = true;
2817       }
2818     }
2819     if (sgroup != null && sgroup.getSize() > 0)
2820     {
2821       av.setSelectionGroup(sgroup);
2822     }
2823     else
2824     {
2825       av.setSelectionGroup(null);
2826     }
2827     av.isSelectionGroupChanged(true);
2828     repaint = true;
2829
2830     if (copycolsel)
2831     {
2832       // the current selection is unset or from a previous message
2833       // so import the new colsel.
2834       if (colsel == null || colsel.isEmpty())
2835       {
2836         if (av.getColumnSelection() != null)
2837         {
2838           av.getColumnSelection().clear();
2839           repaint = true;
2840         }
2841       }
2842       else
2843       {
2844         // TODO: shift colSel according to the intersecting sequences
2845         if (av.getColumnSelection() == null)
2846         {
2847           av.setColumnSelection(new ColumnSelection(colsel));
2848         }
2849         else
2850         {
2851           av.getColumnSelection().setElementsFrom(colsel,
2852                   av.getAlignment().getHiddenColumns());
2853         }
2854       }
2855       av.isColSelChanged(true);
2856       repaint = true;
2857     }
2858
2859     if (copycolsel && av.hasHiddenColumns()
2860             && (av.getAlignment().getHiddenColumns() == null))
2861     {
2862       System.err.println("Bad things");
2863     }
2864     if (repaint) // always true!
2865     {
2866       // probably finessing with multiple redraws here
2867       PaintRefresher.Refresh(this, av.getSequenceSetId());
2868       // ap.paintAlignment(false);
2869     }
2870
2871     // lastly, update dependent dialogs
2872     if (ap.getCalculationDialog() != null)
2873     {
2874       ap.getCalculationDialog().validateCalcTypes();
2875     }
2876
2877   }
2878
2879   /**
2880    * If this panel is a cdna/protein translation view of the selection source,
2881    * tries to map the source selection to a local one, and returns true. Else
2882    * returns false.
2883    * 
2884    * @param seqsel
2885    * @param colsel
2886    * @param source
2887    */
2888   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2889           ColumnSelection colsel, HiddenColumns hidden,
2890           SelectionSource source)
2891   {
2892     if (!(source instanceof AlignViewportI))
2893     {
2894       return false;
2895     }
2896     final AlignViewportI sourceAv = (AlignViewportI) source;
2897     if (sourceAv.getCodingComplement() != av
2898             && av.getCodingComplement() != sourceAv)
2899     {
2900       return false;
2901     }
2902
2903     /*
2904      * Map sequence selection
2905      */
2906     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2907     av.setSelectionGroup(sg != null && sg.getSize() > 0 ? sg : null);
2908     av.isSelectionGroupChanged(true);
2909
2910     /*
2911      * Map column selection
2912      */
2913     // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2914     // av);
2915     ColumnSelection cs = new ColumnSelection();
2916     HiddenColumns hs = new HiddenColumns();
2917     MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2918     av.setColumnSelection(cs);
2919     boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2920
2921     // lastly, update any dependent dialogs
2922     if (ap.getCalculationDialog() != null)
2923     {
2924       ap.getCalculationDialog().validateCalcTypes();
2925     }
2926
2927     /*
2928      * repaint alignment, and also Overview or Structure
2929      * if hidden column selection has changed
2930      */
2931     ap.paintAlignment(hiddenChanged, hiddenChanged);
2932
2933     return true;
2934   }
2935
2936   /**
2937    * 
2938    * @return null or last search results handled by this panel
2939    */
2940   public SearchResultsI getLastSearchResults()
2941   {
2942     return lastSearchResults;
2943   }
2944 }