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