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