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