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