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