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