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