temp push
[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  * The main scrollable region containing the alignment and just to the right of
81  * the IDPanel.
82  * 
83  * @author $author$
84  * @version $Revision: 1.130 $
85  */
86 public class SeqPanel extends JPanel
87         implements MouseListener, MouseMotionListener, MouseWheelListener,
88         SequenceListener, SelectionListener
89 {
90   /*
91    * a class that holds computed mouse position
92    * - column of the alignment (0...)
93    * - sequence offset (0...)
94    * - annotation row offset (0...)
95    * where annotation offset is -1 unless the alignment is shown
96    * in wrapped mode, annotations are shown, and the mouse is
97    * over an annnotation row
98    */
99   static class MousePos
100   {
101     /*
102      * alignment column position of cursor (0...)
103      */
104     final int column;
105
106     /*
107      * index in alignment of sequence under cursor,
108      * or nearest above if cursor is not over a sequence
109      */
110     final int seqIndex;
111
112     /*
113      * index in annotations array of annotation under the cursor
114      * (only possible in wrapped mode with annotations shown),
115      * or -1 if cursor is not over an annotation row
116      */
117     final int annotationIndex;
118
119     MousePos(int col, int seq, int ann)
120     {
121       column = col;
122       seqIndex = seq;
123       annotationIndex = ann;
124     }
125
126     boolean isOverAnnotation()
127     {
128       return annotationIndex != -1;
129     }
130
131     @Override
132     public boolean equals(Object obj)
133     {
134       if (obj == null || !(obj instanceof MousePos))
135       {
136         return false;
137       }
138       MousePos o = (MousePos) obj;
139       boolean b = (column == o.column && seqIndex == o.seqIndex
140               && annotationIndex == o.annotationIndex);
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       // Selection spans a hidden region
1614       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1615       {
1616         if (startres >= y2)
1617         {
1618           fixedLeft = y2;
1619         }
1620         else
1621         {
1622           fixedRight = y2 - 1;
1623         }
1624       }
1625     }
1626
1627     boolean success = doEditSequence(insertGap, editSeq, startres,
1628             fixedRight, fixedColumns, sg);
1629
1630     /*
1631      * report what actually happened (might be less than
1632      * what was requested), by inspecting the edit commands added
1633      */
1634     String msg = getEditStatusMessage(editCommand);
1635     ap.alignFrame.setStatus(msg == null ? " " : msg);
1636     if (!success)
1637     {
1638       endEditing();
1639     }
1640
1641     editLastRes = startres;
1642     seqCanvas.repaint();
1643   }
1644
1645   /**
1646    * A helper method that performs the requested editing to insert or delete
1647    * gaps (if possible). Answers true if the edit was successful, false if could
1648    * only be performed in part or not at all. Failure may occur in 'locked edit'
1649    * mode, when an insertion requires a matching gapped position (or column) to
1650    * delete, and deletion requires an adjacent gapped position (or column) to
1651    * remove.
1652    * 
1653    * @param insertGap
1654    *          true if inserting gap(s), false if deleting
1655    * @param editSeq
1656    *          (unused parameter, currently always false)
1657    * @param startres
1658    *          the column at which to perform the edit
1659    * @param fixedRight
1660    *          fixed right boundary column of a locked edit (within or to the
1661    *          left of a selection group)
1662    * @param fixedColumns
1663    *          true if this is a locked edit
1664    * @param sg
1665    *          the sequence group (if group edit is being performed)
1666    * @return
1667    */
1668   protected boolean doEditSequence(final boolean insertGap,
1669           final boolean editSeq, final int startres, int fixedRight,
1670           final boolean fixedColumns, final SequenceGroup sg)
1671   {
1672     final SequenceI seq = av.getAlignment().getSequenceAt(editStartSeq);
1673     SequenceI[] seqs = new SequenceI[] { seq };
1674
1675     if (groupEditing)
1676     {
1677       List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1678       int g, groupSize = vseqs.size();
1679       SequenceI[] groupSeqs = new SequenceI[groupSize];
1680       for (g = 0; g < groupSeqs.length; g++)
1681       {
1682         groupSeqs[g] = vseqs.get(g);
1683       }
1684
1685       // drag to right
1686       if (insertGap)
1687       {
1688         // If the user has selected the whole sequence, and is dragging to
1689         // the right, we can still extend the alignment and selectionGroup
1690         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1691                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1692         {
1693           sg.setEndRes(
1694                   av.getAlignment().getWidth() + startres - editLastRes);
1695           fixedRight = sg.getEndRes();
1696         }
1697
1698         // Is it valid with fixed columns??
1699         // Find the next gap before the end
1700         // of the visible region boundary
1701         boolean blank = false;
1702         for (; fixedRight > editLastRes; fixedRight--)
1703         {
1704           blank = true;
1705
1706           for (g = 0; g < groupSize; g++)
1707           {
1708             for (int j = 0; j < startres - editLastRes; j++)
1709             {
1710               if (!Comparison
1711                       .isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1712               {
1713                 blank = false;
1714                 break;
1715               }
1716             }
1717           }
1718           if (blank)
1719           {
1720             break;
1721           }
1722         }
1723
1724         if (!blank)
1725         {
1726           if (sg.getSize() == av.getAlignment().getHeight())
1727           {
1728             if ((av.hasHiddenColumns()
1729                     && startres < av.getAlignment().getHiddenColumns()
1730                             .getNextHiddenBoundary(false, startres)))
1731             {
1732               return false;
1733             }
1734
1735             int alWidth = av.getAlignment().getWidth();
1736             if (av.hasHiddenRows())
1737             {
1738               int hwidth = av.getAlignment().getHiddenSequences()
1739                       .getWidth();
1740               if (hwidth > alWidth)
1741               {
1742                 alWidth = hwidth;
1743               }
1744             }
1745             // We can still insert gaps if the selectionGroup
1746             // contains all the sequences
1747             sg.setEndRes(sg.getEndRes() + startres - editLastRes);
1748             fixedRight = alWidth + startres - editLastRes;
1749           }
1750           else
1751           {
1752             return false;
1753           }
1754         }
1755       }
1756
1757       // drag to left
1758       else if (!insertGap)
1759       {
1760         // / Are we able to delete?
1761         // ie are all columns blank?
1762
1763         for (g = 0; g < groupSize; g++)
1764         {
1765           for (int j = startres; j < editLastRes; j++)
1766           {
1767             if (groupSeqs[g].getLength() <= j)
1768             {
1769               continue;
1770             }
1771
1772             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1773             {
1774               // Not a gap, block edit not valid
1775               return false;
1776             }
1777           }
1778         }
1779       }
1780
1781       if (insertGap)
1782       {
1783         // dragging to the right
1784         if (fixedColumns && fixedRight != -1)
1785         {
1786           for (int j = editLastRes; j < startres; j++)
1787           {
1788             insertGap(j, groupSeqs, fixedRight);
1789           }
1790         }
1791         else
1792         {
1793           appendEdit(Action.INSERT_GAP, groupSeqs, startres,
1794                   startres - editLastRes, false);
1795         }
1796       }
1797       else
1798       {
1799         // dragging to the left
1800         if (fixedColumns && fixedRight != -1)
1801         {
1802           for (int j = editLastRes; j > startres; j--)
1803           {
1804             deleteChar(startres, groupSeqs, fixedRight);
1805           }
1806         }
1807         else
1808         {
1809           appendEdit(Action.DELETE_GAP, groupSeqs, startres,
1810                   editLastRes - startres, false);
1811         }
1812       }
1813     }
1814     else
1815     {
1816       /*
1817        * editing a single sequence
1818        */
1819       if (insertGap)
1820       {
1821         // dragging to the right
1822         if (fixedColumns && fixedRight != -1)
1823         {
1824           for (int j = editLastRes; j < startres; j++)
1825           {
1826             if (!insertGap(j, seqs, fixedRight))
1827             {
1828               /*
1829                * e.g. cursor mode command specified 
1830                * more inserts than are possible
1831                */
1832               return false;
1833             }
1834           }
1835         }
1836         else
1837         {
1838           appendEdit(Action.INSERT_GAP, seqs, editLastRes,
1839                   startres - editLastRes, false);
1840         }
1841       }
1842       else
1843       {
1844         if (!editSeq)
1845         {
1846           // dragging to the left
1847           if (fixedColumns && fixedRight != -1)
1848           {
1849             for (int j = editLastRes; j > startres; j--)
1850             {
1851               if (!Comparison.isGap(seq.getCharAt(startres)))
1852               {
1853                 return false;
1854               }
1855               deleteChar(startres, seqs, fixedRight);
1856             }
1857           }
1858           else
1859           {
1860             // could be a keyboard edit trying to delete none gaps
1861             int max = 0;
1862             for (int m = startres; m < editLastRes; m++)
1863             {
1864               if (!Comparison.isGap(seq.getCharAt(m)))
1865               {
1866                 break;
1867               }
1868               max++;
1869             }
1870             if (max > 0)
1871             {
1872               appendEdit(Action.DELETE_GAP, seqs, startres, max, false);
1873             }
1874           }
1875         }
1876         else
1877         {// insertGap==false AND editSeq==TRUE;
1878           if (fixedColumns && fixedRight != -1)
1879           {
1880             for (int j = editLastRes; j < startres; j++)
1881             {
1882               insertGap(j, seqs, fixedRight);
1883             }
1884           }
1885           else
1886           {
1887             appendEdit(Action.INSERT_NUC, seqs, editLastRes,
1888                     startres - editLastRes, false);
1889           }
1890         }
1891       }
1892     }
1893
1894     return true;
1895   }
1896
1897   /**
1898    * Constructs an informative status bar message while dragging to insert or
1899    * delete gaps. Answers null if inserts and deletes cancel out.
1900    * 
1901    * @param editCommand
1902    *          a command containing the list of individual edits
1903    * @return
1904    */
1905   protected static String getEditStatusMessage(EditCommand editCommand)
1906   {
1907     if (editCommand == null)
1908     {
1909       return null;
1910     }
1911
1912     /*
1913      * add any inserts, and subtract any deletes,  
1914      * not counting those auto-inserted when doing a 'locked edit'
1915      * (so only counting edits 'under the cursor')
1916      */
1917     int count = 0;
1918     for (Edit cmd : editCommand.getEdits())
1919     {
1920       if (!cmd.isSystemGenerated())
1921       {
1922         count += cmd.getAction() == Action.INSERT_GAP ? cmd.getNumber()
1923                 : -cmd.getNumber();
1924       }
1925     }
1926
1927     if (count == 0)
1928     {
1929       /*
1930        * inserts and deletes cancel out
1931        */
1932       return null;
1933     }
1934
1935     String msgKey = count > 1 ? "label.insert_gaps"
1936             : (count == 1 ? "label.insert_gap"
1937                     : (count == -1 ? "label.delete_gap"
1938                             : "label.delete_gaps"));
1939     count = Math.abs(count);
1940
1941     return MessageManager.formatMessage(msgKey, String.valueOf(count));
1942   }
1943
1944   /**
1945    * Inserts one gap at column j, deleting the right-most gapped column up to
1946    * (and including) fixedColumn. Returns true if the edit is successful, false
1947    * if no blank column is available to allow the insertion to be balanced by a
1948    * deletion.
1949    * 
1950    * @param j
1951    * @param seq
1952    * @param fixedColumn
1953    * @return
1954    */
1955   boolean insertGap(int j, SequenceI[] seq, int fixedColumn)
1956   {
1957     int blankColumn = fixedColumn;
1958     for (int s = 0; s < seq.length; s++)
1959     {
1960       // Find the next gap before the end of the visible region boundary
1961       // If lastCol > j, theres a boundary after the gap insertion
1962
1963       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1964       {
1965         if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1966         {
1967           // Theres a space, so break and insert the gap
1968           break;
1969         }
1970       }
1971
1972       if (blankColumn <= j)
1973       {
1974         blankColumn = fixedColumn;
1975         endEditing();
1976         return false;
1977       }
1978     }
1979
1980     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1, true);
1981
1982     appendEdit(Action.INSERT_GAP, seq, j, 1, false);
1983
1984     return true;
1985   }
1986
1987   /**
1988    * Helper method to add and perform one edit action
1989    * 
1990    * @param action
1991    * @param seq
1992    * @param pos
1993    * @param count
1994    * @param systemGenerated
1995    *          true if the edit is a 'balancing' delete (or insert) to match a
1996    *          user's insert (or delete) in a locked editing region
1997    */
1998   protected void appendEdit(Action action, SequenceI[] seq, int pos,
1999           int count, boolean systemGenerated)
2000   {
2001
2002     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
2003             av.getAlignment().getGapCharacter());
2004     edit.setSystemGenerated(systemGenerated);
2005
2006     editCommand.appendEdit(edit, av.getAlignment(), true, null);
2007   }
2008
2009   /**
2010    * Deletes the character at column j, and inserts a gap at fixedColumn, in
2011    * each of the given sequences. The caller should ensure that all sequences
2012    * are gapped in column j.
2013    * 
2014    * @param j
2015    * @param seqs
2016    * @param fixedColumn
2017    */
2018   void deleteChar(int j, SequenceI[] seqs, int fixedColumn)
2019   {
2020     appendEdit(Action.DELETE_GAP, seqs, j, 1, false);
2021
2022     appendEdit(Action.INSERT_GAP, seqs, fixedColumn, 1, true);
2023   }
2024
2025   /**
2026    * On reentering the panel, stops any scrolling that was started on dragging
2027    * out of the panel
2028    * 
2029    * @param e
2030    */
2031   @Override
2032   public void mouseEntered(MouseEvent e)
2033   {
2034     if (oldSeq < 0)
2035     {
2036       oldSeq = 0;
2037     }
2038     stopScrolling();
2039   }
2040
2041   /**
2042    * On leaving the panel, if the mouse is being dragged, starts a thread to
2043    * scroll it until the mouse is released (in unwrapped mode only)
2044    * 
2045    * @param e
2046    */
2047   @Override
2048   public void mouseExited(MouseEvent e)
2049   {
2050     lastMousePosition = null;
2051     ap.alignFrame.setStatus(" ");
2052
2053     if (av.getWrapAlignment())
2054     {
2055       return;
2056     }
2057
2058     // BH check was:
2059 //    /*
2060 //     * start scrolling if mouse dragging, whether the drag started
2061 //     * in the scale panel or this panel
2062 //     */
2063 //    if (mouseDragging || ap.getScalePanel().isMouseDragging())
2064 //    {
2065 //      startScrolling(new Point(e.getX(), 0));
2066 //    }
2067
2068     if (mouseDragging && scrollThread == null)
2069     {
2070       startScrolling(e.getPoint());
2071     }
2072   }
2073
2074   /**
2075    * Handler for double-click on a position with one or more sequence features.
2076    * Opens the Amend Features dialog to allow feature details to be amended, or
2077    * the feature deleted.
2078    */
2079   @Override
2080   public void mouseClicked(MouseEvent evt)
2081   {
2082     SequenceGroup sg = null;
2083     MousePos pos = findMousePosition(evt);
2084     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2085     {
2086       return;
2087     }
2088
2089     if (evt.getClickCount() > 1 && av.isShowSequenceFeatures())
2090     {
2091       sg = av.getSelectionGroup();
2092       if (sg != null && sg.getSize() == 1
2093               && sg.getEndRes() - sg.getStartRes() < 2)
2094       {
2095         av.setSelectionGroup(null);
2096       }
2097
2098       int column = pos.column;
2099
2100       /*
2101        * find features at the position (if not gapped), or straddling
2102        * the position (if at a gap)
2103        */
2104       SequenceI sequence = av.getAlignment().getSequenceAt(pos.seqIndex);
2105       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
2106               .findFeaturesAtColumn(sequence, column + 1);
2107
2108       if (!features.isEmpty())
2109       {
2110         /*
2111          * highlight the first feature at the position on the alignment
2112          */
2113         SearchResultsI highlight = new SearchResults();
2114         highlight.addResult(sequence, features.get(0).getBegin(), features
2115                 .get(0).getEnd());
2116         seqCanvas.highlightSearchResults(highlight, true);
2117
2118         /*
2119          * open the Amend Features dialog
2120          */
2121         new FeatureEditor(ap, Collections.singletonList(sequence), features,
2122                 false).showDialog();
2123       }
2124     }
2125   }
2126
2127   @Override
2128   public void mouseWheelMoved(MouseWheelEvent e)
2129   {
2130     e.consume();
2131     double wheelRotation = e.getPreciseWheelRotation();
2132     if (wheelRotation > 0)
2133     {
2134       if (e.isShiftDown())
2135       {
2136         av.getRanges().scrollRight(true);
2137
2138       }
2139       else
2140       {
2141         av.getRanges().scrollUp(false);
2142       }
2143     }
2144     else if (wheelRotation < 0)
2145     {
2146       if (e.isShiftDown())
2147       {
2148         av.getRanges().scrollRight(false);
2149       }
2150       else
2151       {
2152         av.getRanges().scrollUp(true);
2153       }
2154     }
2155
2156     /*
2157      * update status bar and tooltip for new position
2158      * (need to synthesize a mouse movement to refresh tooltip)
2159      */
2160     mouseMoved(e);
2161     ToolTipManager.sharedInstance().mouseMoved(e);
2162   }
2163
2164   /**
2165    * DOCUMENT ME!
2166    * 
2167    * @param pos
2168    *          DOCUMENT ME!
2169    */
2170   protected void doMousePressedDefineMode(MouseEvent evt, MousePos pos)
2171   {
2172     if (pos.isOverAnnotation() || pos.seqIndex == -1 || pos.column == -1)
2173     {
2174       return;
2175     }
2176
2177     final int res = pos.column;
2178     final int seq = pos.seqIndex;
2179     oldSeq = seq;
2180     updateOverviewAndStructs = false;
2181
2182     startWrapBlock = wrappedBlock;
2183
2184     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2185
2186     if ((sequence == null) || (res > sequence.getLength()))
2187     {
2188       return;
2189     }
2190
2191     stretchGroup = av.getSelectionGroup();
2192
2193     if (stretchGroup == null || !stretchGroup.contains(sequence, res))
2194     {
2195       stretchGroup = av.getAlignment().findGroup(sequence, res);
2196       if (stretchGroup != null)
2197       {
2198         // only update the current selection if the popup menu has a group to
2199         // focus on
2200         av.setSelectionGroup(stretchGroup);
2201       }
2202     }
2203
2204     /*
2205      * defer right-mouse click handling to mouseReleased on Windows
2206      * (where isPopupTrigger() will answer true)
2207      * NB isRightMouseButton is also true for Cmd-click on Mac
2208      */
2209     if (Platform.isWinRightButton(evt))
2210     {
2211       return;
2212     }
2213
2214     if (evt.isPopupTrigger()) // Mac: mousePressed
2215     {
2216       showPopupMenu(evt, pos);
2217       return;
2218     }
2219
2220     if (av.cursorMode)
2221     {
2222       seqCanvas.cursorX = res;
2223       seqCanvas.cursorY = seq;
2224       seqCanvas.repaint();
2225       return;
2226     }
2227
2228     if (stretchGroup == null)
2229     {
2230       createStretchGroup(res, sequence);
2231     }
2232
2233     if (stretchGroup != null)
2234     {
2235       stretchGroup.addPropertyChangeListener(seqCanvas);
2236     }
2237
2238     seqCanvas.repaint();
2239   }
2240
2241   private void createStretchGroup(int res, SequenceI sequence)
2242   {
2243     // Only if left mouse button do we want to change group sizes
2244     // define a new group here
2245     SequenceGroup sg = new SequenceGroup();
2246     sg.setStartRes(res);
2247     sg.setEndRes(res);
2248     sg.addSequence(sequence, false);
2249     av.setSelectionGroup(sg);
2250     stretchGroup = sg;
2251
2252     if (av.getConservationSelected())
2253     {
2254       SliderPanel.setConservationSlider(ap, av.getResidueShading(),
2255               ap.getViewName());
2256     }
2257
2258     if (av.getAbovePIDThreshold())
2259     {
2260       SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
2261               ap.getViewName());
2262     }
2263     // TODO: stretchGroup will always be not null. Is this a merge error ?
2264     // or is there a threading issue here?
2265     if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
2266     {
2267       // Edit end res position of selected group
2268       changeEndRes = true;
2269     }
2270     else if ((stretchGroup != null) && (stretchGroup.getStartRes() == res))
2271     {
2272       // Edit end res position of selected group
2273       changeStartRes = true;
2274     }
2275     stretchGroup.getWidth();
2276
2277   }
2278
2279   /**
2280    * Build and show a pop-up menu at the right-click mouse position
2281    *
2282    * @param evt
2283    * @param pos
2284    */
2285   void showPopupMenu(MouseEvent evt, MousePos pos)
2286   {
2287     final int column = pos.column;
2288     final int seq = pos.seqIndex;
2289     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
2290     if (sequence != null)
2291     {
2292       PopupMenu pop = new PopupMenu(ap, sequence, column);
2293     pop.show(this, evt.getX(), evt.getY());
2294   }
2295   }
2296
2297   /**
2298    * Update the display after mouse up on a selection or group
2299    * 
2300    * @param evt
2301    *          mouse released event details
2302    * @param afterDrag
2303    *          true if this event is happening after a mouse drag (rather than a
2304    *          mouse down)
2305    */
2306   protected void doMouseReleasedDefineMode(MouseEvent evt,
2307           boolean afterDrag)
2308   {
2309     if (stretchGroup == null)
2310     {
2311       return;
2312     }
2313
2314     stretchGroup.removePropertyChangeListener(seqCanvas);
2315
2316     // always do this - annotation has own state
2317     // but defer colourscheme update until hidden sequences are passed in
2318     boolean vischange = stretchGroup.recalcConservation(true);
2319     updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
2320             && afterDrag;
2321     if (stretchGroup.cs != null)
2322     {
2323       if (afterDrag)
2324       {
2325         stretchGroup.cs.alignmentChanged(stretchGroup,
2326                 av.getHiddenRepSequences());
2327       }
2328
2329       ResidueShaderI groupColourScheme = stretchGroup
2330               .getGroupColourScheme();
2331       String name = stretchGroup.getName();
2332       if (stretchGroup.cs.conservationApplied())
2333       {
2334         SliderPanel.setConservationSlider(ap, groupColourScheme, name);
2335       }
2336       if (stretchGroup.cs.getThreshold() > 0)
2337       {
2338         SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
2339       }
2340     }
2341     PaintRefresher.Refresh(this, av.getSequenceSetId());
2342     // TODO: structure colours only need updating if stretchGroup used to or now
2343     // does contain sequences with structure views
2344     ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
2345     updateOverviewAndStructs = false;
2346     changeEndRes = false;
2347     changeStartRes = false;
2348     stretchGroup = null;
2349     av.sendSelection();
2350   }
2351
2352   /**
2353    * Resizes the borders of a selection group depending on the direction of
2354    * mouse drag
2355    * 
2356    * @param evt
2357    */
2358   protected void dragStretchGroup(MouseEvent evt)
2359   {
2360     if (stretchGroup == null)
2361     {
2362       return;
2363     }
2364
2365     MousePos pos = findMousePosition(evt);
2366     if (pos.isOverAnnotation() || pos.column == -1 || pos.seqIndex == -1)
2367     {
2368       return;
2369     }
2370
2371     int res = pos.column;
2372     int y = pos.seqIndex;
2373
2374     if (wrappedBlock != startWrapBlock)
2375     {
2376       return;
2377     }
2378
2379     res = Math.min(res, av.getAlignment().getWidth()-1);
2380
2381     if (stretchGroup.getEndRes() == res)
2382     {
2383       // Edit end res position of selected group
2384       changeEndRes = true;
2385     }
2386     else if (stretchGroup.getStartRes() == res)
2387     {
2388       // Edit start res position of selected group
2389       changeStartRes = true;
2390     }
2391
2392     if (res < av.getRanges().getStartRes())
2393     {
2394       res = av.getRanges().getStartRes();
2395     }
2396
2397     if (changeEndRes)
2398     {
2399       if (res > (stretchGroup.getStartRes() - 1))
2400       {
2401         stretchGroup.setEndRes(res);
2402         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2403       }
2404     }
2405     else if (changeStartRes)
2406     {
2407       if (res < (stretchGroup.getEndRes() + 1))
2408       {
2409         stretchGroup.setStartRes(res);
2410         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2411       }
2412     }
2413
2414     int dragDirection = 0;
2415
2416     if (y > oldSeq)
2417     {
2418       dragDirection = 1;
2419     }
2420     else if (y < oldSeq)
2421     {
2422       dragDirection = -1;
2423     }
2424
2425     while ((y != oldSeq) && (oldSeq > -1)
2426             && (y < av.getAlignment().getHeight()))
2427     {
2428       // This routine ensures we don't skip any sequences, as the
2429       // selection is quite slow.
2430       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2431
2432       oldSeq += dragDirection;
2433
2434       if (oldSeq < 0)
2435       {
2436         break;
2437       }
2438
2439       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
2440
2441       if (stretchGroup.getSequences(null).contains(nextSeq))
2442       {
2443         stretchGroup.deleteSequence(seq, false);
2444         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2445       }
2446       else
2447       {
2448         if (seq != null)
2449         {
2450           stretchGroup.addSequence(seq, false);
2451         }
2452
2453         stretchGroup.addSequence(nextSeq, false);
2454         updateOverviewAndStructs |= av.isSelectionDefinedGroup();
2455       }
2456     }
2457
2458     if (oldSeq < 0)
2459     {
2460       oldSeq = -1;
2461     }
2462
2463     mouseDragging = true;
2464
2465     if (scrollThread != null)
2466     {
2467       scrollThread.setMousePosition(evt.getPoint());
2468     }
2469
2470     /*
2471      * construct a status message showing the range of the selection
2472      */
2473     StringBuilder status = new StringBuilder(64);
2474     List<SequenceI> seqs = stretchGroup.getSequences();
2475     String name = seqs.get(0).getName();
2476     if (name.length() > 20)
2477     {
2478       name = name.substring(0, 20);
2479     }
2480     status.append(name).append(" - ");
2481     name = seqs.get(seqs.size() - 1).getName();
2482     if (name.length() > 20)
2483     {
2484       name = name.substring(0, 20);
2485     }
2486     status.append(name).append(" ");
2487     int startRes = stretchGroup.getStartRes();
2488     status.append(" cols ").append(String.valueOf(startRes + 1))
2489             .append("-");
2490     int endRes = stretchGroup.getEndRes();
2491     status.append(String.valueOf(endRes + 1));
2492     status.append(" (").append(String.valueOf(seqs.size())).append(" x ")
2493             .append(String.valueOf(endRes - startRes + 1)).append(")");
2494     ap.alignFrame.setStatus(status.toString());
2495   }
2496
2497   /**
2498    * Stops the scroll thread if it is running
2499    */
2500   void stopScrolling()
2501   {
2502     if (scrollThread != null)
2503     {
2504       scrollThread.stopScrolling();
2505       scrollThread = null;
2506     }
2507     mouseDragging = false;
2508   }
2509
2510   /**
2511    * Starts a thread to scroll the alignment, towards a given mouse position
2512    * outside the panel bounds, unless the alignment is in wrapped mode
2513    * 
2514    * @param mousePos
2515    */
2516   void startScrolling(Point mousePos)
2517   {
2518     /*
2519      * set this.mouseDragging in case this was called from 
2520      * a drag in ScalePanel or AnnotationPanel
2521      */
2522     mouseDragging = true;
2523     if (!av.getWrapAlignment() && scrollThread == null)
2524     {
2525       scrollThread = new ScrollThread();
2526       scrollThread.setMousePosition(mousePos);
2527       if (Platform.isJS())
2528       {
2529         /*
2530          * Javascript - run every 20ms until scrolling stopped
2531          * or reaches the limit of scrollable alignment
2532          */
2533         Timer t = new Timer(20, new ActionListener()
2534         {
2535           @Override
2536           public void actionPerformed(ActionEvent e)
2537           {
2538             if (scrollThread != null)
2539             {
2540               // if (!scrollOnce() {t.stop();}) gives compiler error :-(
2541               scrollThread.scrollOnce();
2542             }
2543           }
2544         });
2545         t.addActionListener(new ActionListener()
2546         {
2547           @Override
2548           public void actionPerformed(ActionEvent e)
2549           {
2550             if (scrollThread == null)
2551             {
2552               // SeqPanel.stopScrolling called
2553               t.stop();
2554             }
2555           }
2556         });
2557         t.start();
2558       }
2559       else
2560       {
2561         /*
2562          * Java - run in a new thread
2563          */
2564         scrollThread.start();
2565       }
2566     }
2567   }
2568
2569   /**
2570    * Performs scrolling of the visible alignment left, right, up or down, until
2571    * scrolling is stopped by calling stopScrolling, mouse drag is ended, or the
2572    * limit of the alignment is reached
2573    */
2574   class ScrollThread extends Thread
2575   {
2576     private Point mousePos;
2577
2578     private volatile boolean keepRunning = true;
2579
2580     /**
2581      * Constructor
2582      */
2583     public ScrollThread()
2584     {
2585       setName("SeqPanel$ScrollThread");
2586     }
2587
2588     /**
2589      * Sets the position of the mouse that determines the direction of the
2590      * scroll to perform. If this is called as the mouse moves, scrolling should
2591      * respond accordingly. For example, if the mouse is dragged right, scroll
2592      * right should start; if the drag continues down, scroll down should also
2593      * happen.
2594      * 
2595      * @param p
2596      */
2597     public void setMousePosition(Point p)
2598     {
2599       mousePos = p;
2600     }
2601
2602     /**
2603      * Sets a flag that will cause the thread to exit
2604      */
2605     public void stopScrolling()
2606     {
2607       keepRunning = false;
2608     }
2609
2610     /**
2611      * Scrolls the alignment left or right, and/or up or down, depending on the
2612      * last notified mouse position, until the limit of the alignment is
2613      * reached, or a flag is set to stop the scroll
2614      */
2615     @Override
2616     public void run()
2617     {
2618       while (keepRunning)
2619       {
2620         if (mousePos != null)
2621         {
2622           keepRunning = scrollOnce();
2623         }
2624         try
2625         {
2626           Thread.sleep(20);
2627         } catch (Exception ex)
2628         {
2629         }
2630       }
2631       SeqPanel.this.scrollThread = null;
2632     }
2633
2634     /**
2635      * Scrolls
2636      * <ul>
2637      * <li>one row up, if the mouse is above the panel</li>
2638      * <li>one row down, if the mouse is below the panel</li>
2639      * <li>one column left, if the mouse is left of the panel</li>
2640      * <li>one column right, if the mouse is right of the panel</li>
2641      * </ul>
2642      * Answers true if a scroll was performed, false if not - meaning either
2643      * that the mouse position is within the panel, or the edge of the alignment
2644      * has been reached.
2645      */
2646     boolean scrollOnce()
2647     {
2648       /*
2649        * quit after mouseUp ensures interrupt in JalviewJS
2650        */
2651       if (!mouseDragging)
2652       {
2653         return false;
2654       }
2655
2656       boolean scrolled = false;
2657       ViewportRanges ranges = SeqPanel.this.av.getRanges();
2658
2659       /*
2660        * scroll up or down
2661        */
2662       if (mousePos.y < 0)
2663       {
2664         // mouse is above this panel - try scroll up
2665         scrolled = ranges.scrollUp(true);
2666       }
2667       else if (mousePos.y >= getHeight())
2668       {
2669         // mouse is below this panel - try scroll down
2670         scrolled = ranges.scrollUp(false);
2671       }
2672
2673       /*
2674        * scroll left or right
2675        */
2676       if (mousePos.x < 0)
2677       {
2678         scrolled |= ranges.scrollRight(false);
2679       }
2680       else if (mousePos.x >= getWidth())
2681       {
2682         scrolled |= ranges.scrollRight(true);
2683       }
2684       return scrolled;
2685     }
2686   }
2687
2688   /**
2689    * modify current selection according to a received message.
2690    */
2691   @Override
2692   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2693           HiddenColumns hidden, SelectionSource source)
2694   {
2695     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2696     // handles selection messages...
2697     // TODO: extend config options to allow user to control if selections may be
2698     // shared between viewports.
2699     boolean iSentTheSelection = (av == source
2700             || (source instanceof AlignViewport
2701                     && ((AlignmentViewport) source).getSequenceSetId()
2702                             .equals(av.getSequenceSetId())));
2703
2704     if (iSentTheSelection)
2705     {
2706       // respond to our own event by updating dependent dialogs
2707       if (ap.getCalculationDialog() != null)
2708       {
2709         ap.getCalculationDialog().validateCalcTypes();
2710       }
2711
2712       return;
2713     }
2714
2715     // process further ?
2716     if (!av.followSelection)
2717     {
2718       return;
2719     }
2720
2721     /*
2722      * Ignore the selection if there is one of our own pending.
2723      */
2724     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2725     {
2726       return;
2727     }
2728
2729     /*
2730      * Check for selection in a view of which this one is a dna/protein
2731      * complement.
2732      */
2733     if (selectionFromTranslation(seqsel, colsel, hidden, source))
2734     {
2735       return;
2736     }
2737
2738     // do we want to thread this ? (contention with seqsel and colsel locks, I
2739     // suspect)
2740     /*
2741      * only copy colsel if there is a real intersection between
2742      * sequence selection and this panel's alignment
2743      */
2744     boolean repaint = false;
2745     boolean copycolsel = false;
2746
2747     SequenceGroup sgroup = null;
2748     if (seqsel != null && seqsel.getSize() > 0)
2749     {
2750       if (av.getAlignment() == null)
2751       {
2752         Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2753                 + " ViewId=" + av.getViewId()
2754                 + " 's alignment is NULL! returning immediately.");
2755         return;
2756       }
2757       sgroup = seqsel.intersect(av.getAlignment(),
2758               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2759       if ((sgroup != null && sgroup.getSize() > 0))
2760       {
2761         copycolsel = true;
2762       }
2763     }
2764     if (sgroup != null && sgroup.getSize() > 0)
2765     {
2766       av.setSelectionGroup(sgroup);
2767     }
2768     else
2769     {
2770       av.setSelectionGroup(null);
2771     }
2772     av.isSelectionGroupChanged(true);
2773     repaint = true;
2774
2775     if (copycolsel)
2776     {
2777       // the current selection is unset or from a previous message
2778       // so import the new colsel.
2779       if (colsel == null || colsel.isEmpty())
2780       {
2781         if (av.getColumnSelection() != null)
2782         {
2783           av.getColumnSelection().clear();
2784           repaint = true;
2785         }
2786       }
2787       else
2788       {
2789         // TODO: shift colSel according to the intersecting sequences
2790         if (av.getColumnSelection() == null)
2791         {
2792           av.setColumnSelection(new ColumnSelection(colsel));
2793         }
2794         else
2795         {
2796           av.getColumnSelection().setElementsFrom(colsel,
2797                   av.getAlignment().getHiddenColumns());
2798         }
2799       }
2800       av.isColSelChanged(true);
2801       repaint = true;
2802     }
2803
2804     if (copycolsel && av.hasHiddenColumns()
2805             && (av.getAlignment().getHiddenColumns() == null))
2806     {
2807       System.err.println("Bad things");
2808     }
2809     if (repaint) // always true!
2810     {
2811       // probably finessing with multiple redraws here
2812       PaintRefresher.Refresh(this, av.getSequenceSetId());
2813       // ap.paintAlignment(false);
2814     }
2815
2816     // lastly, update dependent dialogs
2817     if (ap.getCalculationDialog() != null)
2818     {
2819       ap.getCalculationDialog().validateCalcTypes();
2820     }
2821
2822   }
2823
2824   /**
2825    * If this panel is a cdna/protein translation view of the selection source,
2826    * tries to map the source selection to a local one, and returns true. Else
2827    * returns false.
2828    * 
2829    * @param seqsel
2830    * @param colsel
2831    * @param source
2832    */
2833   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2834           ColumnSelection colsel, HiddenColumns hidden,
2835           SelectionSource source)
2836   {
2837     if (!(source instanceof AlignViewportI))
2838     {
2839       return false;
2840     }
2841     final AlignViewportI sourceAv = (AlignViewportI) source;
2842     if (sourceAv.getCodingComplement() != av
2843             && av.getCodingComplement() != sourceAv)
2844     {
2845       return false;
2846     }
2847
2848     /*
2849      * Map sequence selection
2850      */
2851     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2852     av.setSelectionGroup(sg);
2853     av.isSelectionGroupChanged(true);
2854
2855     /*
2856      * Map column selection
2857      */
2858     // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2859     // av);
2860     ColumnSelection cs = new ColumnSelection();
2861     HiddenColumns hs = new HiddenColumns();
2862     MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2863     av.setColumnSelection(cs);
2864     boolean hiddenChanged = av.getAlignment().setHiddenColumns(hs);
2865
2866     // lastly, update any dependent dialogs
2867     if (ap.getCalculationDialog() != null)
2868     {
2869       ap.getCalculationDialog().validateCalcTypes();
2870     }
2871
2872     /*
2873      * repaint alignment, and also Overview or Structure
2874      * if hidden column selection has changed
2875      */
2876     ap.paintAlignment(hiddenChanged, hiddenChanged);
2877
2878     return true;
2879   }
2880
2881   /**
2882    * 
2883    * @return null or last search results handled by this panel
2884    */
2885   public SearchResultsI getLastSearchResults()
2886   {
2887     return lastSearchResults;
2888   }
2889
2890   /**
2891    * scroll to the given row/column - or nearest visible location
2892    * 
2893    * @param row
2894    * @param column
2895    */
2896   public void scrollTo(int row, int column)
2897   {
2898
2899     row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2900     column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2901     ap.scrollTo(column, column, row, true, true);
2902   }
2903
2904   /**
2905    * scroll to the given row - or nearest visible location
2906    * 
2907    * @param row
2908    */
2909   public void scrollToRow(int row)
2910   {
2911
2912     row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
2913     ap.scrollTo(ap.av.getRanges().getStartRes(),
2914             ap.av.getRanges().getStartRes(), row, true, true);
2915   }
2916
2917   /**
2918    * scroll to the given column - or nearest visible location
2919    * 
2920    * @param column
2921    */
2922   public void scrollToColumn(int column)
2923   {
2924
2925     column = column < 0 ? ap.av.getRanges().getStartRes() : column;
2926     ap.scrollTo(column, column, ap.av.getRanges().getStartSeq(), true,
2927             true);
2928   }
2929
2930 }