Merge branch 'releases/Release_2_11_4_Branch'
[jalview.git] / jalview / gui / SeqPanel.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.gui;
22
23 import jalview.api.AlignViewportI;
24 import jalview.bin.Cache;
25 import jalview.commands.EditCommand;
26 import jalview.commands.EditCommand.Action;
27 import jalview.commands.EditCommand.Edit;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.ColumnSelection;
30 import jalview.datamodel.HiddenColumns;
31 import jalview.datamodel.SearchResultMatchI;
32 import jalview.datamodel.SearchResults;
33 import jalview.datamodel.SearchResultsI;
34 import jalview.datamodel.Sequence;
35 import jalview.datamodel.SequenceFeature;
36 import jalview.datamodel.SequenceGroup;
37 import jalview.datamodel.SequenceI;
38 import jalview.io.SequenceAnnotationReport;
39 import jalview.renderer.ResidueShaderI;
40 import jalview.schemes.ResidueProperties;
41 import jalview.structure.SelectionListener;
42 import jalview.structure.SelectionSource;
43 import jalview.structure.SequenceListener;
44 import jalview.structure.StructureSelectionManager;
45 import jalview.structure.VamsasSource;
46 import jalview.util.Comparison;
47 import jalview.util.MappingUtils;
48 import jalview.util.MessageManager;
49 import jalview.util.Platform;
50 import jalview.viewmodel.AlignmentViewport;
51
52 import java.awt.BorderLayout;
53 import java.awt.Color;
54 import java.awt.Font;
55 import java.awt.FontMetrics;
56 import java.awt.Point;
57 import java.awt.event.MouseEvent;
58 import java.awt.event.MouseListener;
59 import java.awt.event.MouseMotionListener;
60 import java.awt.event.MouseWheelEvent;
61 import java.awt.event.MouseWheelListener;
62 import java.util.ArrayList;
63 import java.util.Collections;
64 import java.util.List;
65 import java.util.ListIterator;
66
67 import javax.swing.JPanel;
68 import javax.swing.SwingUtilities;
69 import javax.swing.ToolTipManager;
70
71 /**
72  * DOCUMENT ME!
73  * 
74  * @author $author$
75  * @version $Revision: 1.130 $
76  */
77 public class SeqPanel extends JPanel implements MouseListener,
78         MouseMotionListener, MouseWheelListener, SequenceListener,
79         SelectionListener
80
81 {
82   /** DOCUMENT ME!! */
83   public SeqCanvas seqCanvas;
84
85   /** DOCUMENT ME!! */
86   public AlignmentPanel ap;
87
88   protected int lastres;
89
90   protected int startseq;
91
92   protected AlignViewport av;
93
94   ScrollThread scrollThread = null;
95
96   boolean mouseDragging = false;
97
98   boolean editingSeqs = false;
99
100   boolean groupEditing = false;
101
102   // ////////////////////////////////////////
103   // ///Everything below this is for defining the boundary of the rubberband
104   // ////////////////////////////////////////
105   int oldSeq = -1;
106
107   boolean changeEndSeq = false;
108
109   boolean changeStartSeq = false;
110
111   boolean changeEndRes = false;
112
113   boolean changeStartRes = false;
114
115   SequenceGroup stretchGroup = null;
116
117   boolean remove = false;
118
119   Point lastMousePress;
120
121   boolean mouseWheelPressed = false;
122
123   StringBuffer keyboardNo1;
124
125   StringBuffer keyboardNo2;
126
127   java.net.URL linkImageURL;
128
129   private final SequenceAnnotationReport seqARep;
130
131   StringBuilder tooltipText = new StringBuilder();
132
133   String tmpString;
134
135   EditCommand editCommand;
136
137   StructureSelectionManager ssm;
138
139   SearchResultsI lastSearchResults;
140
141   /**
142    * Creates a new SeqPanel object.
143    * 
144    * @param avp
145    *          DOCUMENT ME!
146    * @param p
147    *          DOCUMENT ME!
148    */
149   public SeqPanel(AlignViewport av, AlignmentPanel ap)
150   {
151     linkImageURL = getClass().getResource("/images/link.gif");
152     seqARep = new SequenceAnnotationReport(linkImageURL.toString());
153     ToolTipManager.sharedInstance().registerComponent(this);
154     ToolTipManager.sharedInstance().setInitialDelay(0);
155     ToolTipManager.sharedInstance().setDismissDelay(10000);
156     this.av = av;
157     setBackground(Color.white);
158
159     seqCanvas = new SeqCanvas(ap);
160     setLayout(new BorderLayout());
161     add(seqCanvas, BorderLayout.CENTER);
162
163     this.ap = ap;
164
165     if (!av.isDataset())
166     {
167       addMouseMotionListener(this);
168       addMouseListener(this);
169       addMouseWheelListener(this);
170       ssm = av.getStructureSelectionManager();
171       ssm.addStructureViewerListener(this);
172       ssm.addSelectionListener(this);
173     }
174   }
175
176   int startWrapBlock = -1;
177
178   int wrappedBlock = -1;
179
180   /**
181    * Returns the aligned sequence position (base 0) at the mouse position, or
182    * the closest visible one
183    * 
184    * @param evt
185    * @return
186    */
187   int findColumn(MouseEvent evt)
188   {
189     int res = 0;
190     int x = evt.getX();
191
192     int startRes = av.getRanges().getStartRes();
193     if (av.getWrapAlignment())
194     {
195
196       int hgap = av.getCharHeight();
197       if (av.getScaleAboveWrapped())
198       {
199         hgap += av.getCharHeight();
200       }
201
202       int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
203               + hgap + seqCanvas.getAnnotationHeight();
204
205       int y = evt.getY();
206       y -= hgap;
207       x = Math.max(0, x - seqCanvas.LABEL_WEST);
208
209       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
210       if (cwidth < 1)
211       {
212         return 0;
213       }
214
215       wrappedBlock = y / cHeight;
216       wrappedBlock += startRes / cwidth;
217       // allow for wrapped view scrolled right (possible from Overview)
218       int startOffset = startRes % cwidth;
219       res = wrappedBlock * cwidth
220               + Math.min(cwidth - 1, startOffset + x / av.getCharWidth());
221     }
222     else
223     {
224       if (x > seqCanvas.getX() + seqCanvas.getWidth())
225       {
226         // make sure we calculate relative to visible alignment, rather than
227         // right-hand gutter
228         x = seqCanvas.getX() + seqCanvas.getWidth();
229       }
230       res = (x / av.getCharWidth()) + startRes;
231       if (res > av.getRanges().getEndRes())
232       {
233         // moused off right
234         res = av.getRanges().getEndRes();
235       }
236     }
237
238     if (av.hasHiddenColumns())
239     {
240       res = av.getAlignment().getHiddenColumns()
241               .adjustForHiddenColumns(res);
242     }
243
244     return res;
245
246   }
247
248   int findSeq(MouseEvent evt)
249   {
250     int seq = 0;
251     int y = evt.getY();
252
253     if (av.getWrapAlignment())
254     {
255       int hgap = av.getCharHeight();
256       if (av.getScaleAboveWrapped())
257       {
258         hgap += av.getCharHeight();
259       }
260
261       int cHeight = av.getAlignment().getHeight() * av.getCharHeight()
262               + hgap + seqCanvas.getAnnotationHeight();
263
264       y -= hgap;
265
266       seq = Math.min((y % cHeight) / av.getCharHeight(), av.getAlignment()
267               .getHeight() - 1);
268     }
269     else
270     {
271       seq = Math.min((y / av.getCharHeight())
272               + av.getRanges().getStartSeq(),
273               av
274               .getAlignment().getHeight() - 1);
275     }
276
277     return seq;
278   }
279
280   /**
281    * When all of a sequence of edits are complete, put the resulting edit list
282    * on the history stack (undo list), and reset flags for editing in progress.
283    */
284   void endEditing()
285   {
286     try
287     {
288       if (editCommand != null && editCommand.getSize() > 0)
289       {
290         ap.alignFrame.addHistoryItem(editCommand);
291         av.firePropertyChange("alignment", null, av.getAlignment()
292                 .getSequences());
293       }
294     } finally
295     {
296       /*
297        * Tidy up come what may...
298        */
299       startseq = -1;
300       lastres = -1;
301       editingSeqs = false;
302       groupEditing = false;
303       keyboardNo1 = null;
304       keyboardNo2 = null;
305       editCommand = null;
306     }
307   }
308
309   void setCursorRow()
310   {
311     seqCanvas.cursorY = getKeyboardNo1() - 1;
312     scrollToVisible();
313   }
314
315   void setCursorColumn()
316   {
317     seqCanvas.cursorX = getKeyboardNo1() - 1;
318     scrollToVisible();
319   }
320
321   void setCursorRowAndColumn()
322   {
323     if (keyboardNo2 == null)
324     {
325       keyboardNo2 = new StringBuffer();
326     }
327     else
328     {
329       seqCanvas.cursorX = getKeyboardNo1() - 1;
330       seqCanvas.cursorY = getKeyboardNo2() - 1;
331       scrollToVisible();
332     }
333   }
334
335   void setCursorPosition()
336   {
337     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
338
339     seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
340     scrollToVisible();
341   }
342
343   void moveCursor(int dx, int dy)
344   {
345     seqCanvas.cursorX += dx;
346     seqCanvas.cursorY += dy;
347
348     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
349
350     if (av.hasHiddenColumns()
351  && !hidden.isVisible(seqCanvas.cursorX))
352     {
353       int original = seqCanvas.cursorX - dx;
354       int maxWidth = av.getAlignment().getWidth();
355
356       while (!hidden.isVisible(seqCanvas.cursorX)
357               && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0)
358       {
359         seqCanvas.cursorX += dx;
360       }
361
362       if (seqCanvas.cursorX >= maxWidth
363               || !hidden.isVisible(seqCanvas.cursorX))
364       {
365         seqCanvas.cursorX = original;
366       }
367     }
368
369     scrollToVisible();
370   }
371
372   void scrollToVisible()
373   {
374     if (seqCanvas.cursorX < 0)
375     {
376       seqCanvas.cursorX = 0;
377     }
378     else if (seqCanvas.cursorX > av.getAlignment().getWidth() - 1)
379     {
380       seqCanvas.cursorX = av.getAlignment().getWidth() - 1;
381     }
382
383     if (seqCanvas.cursorY < 0)
384     {
385       seqCanvas.cursorY = 0;
386     }
387     else if (seqCanvas.cursorY > av.getAlignment().getHeight() - 1)
388     {
389       seqCanvas.cursorY = av.getAlignment().getHeight() - 1;
390     }
391
392     endEditing();
393     if (av.getWrapAlignment())
394     {
395       av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
396     }
397     else
398     {
399       av.getRanges().scrollToVisible(seqCanvas.cursorX, seqCanvas.cursorY);
400     }
401     setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
402             seqCanvas.cursorX, seqCanvas.cursorY);
403
404     seqCanvas.repaint();
405   }
406
407   void setSelectionAreaAtCursor(boolean topLeft)
408   {
409     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
410
411     if (av.getSelectionGroup() != null)
412     {
413       SequenceGroup sg = av.getSelectionGroup();
414       // Find the top and bottom of this group
415       int min = av.getAlignment().getHeight(), max = 0;
416       for (int i = 0; i < sg.getSize(); i++)
417       {
418         int index = av.getAlignment().findIndex(sg.getSequenceAt(i));
419         if (index > max)
420         {
421           max = index;
422         }
423         if (index < min)
424         {
425           min = index;
426         }
427       }
428
429       max++;
430
431       if (topLeft)
432       {
433         sg.setStartRes(seqCanvas.cursorX);
434         if (sg.getEndRes() < seqCanvas.cursorX)
435         {
436           sg.setEndRes(seqCanvas.cursorX);
437         }
438
439         min = seqCanvas.cursorY;
440       }
441       else
442       {
443         sg.setEndRes(seqCanvas.cursorX);
444         if (sg.getStartRes() > seqCanvas.cursorX)
445         {
446           sg.setStartRes(seqCanvas.cursorX);
447         }
448
449         max = seqCanvas.cursorY + 1;
450       }
451
452       if (min > max)
453       {
454         // Only the user can do this
455         av.setSelectionGroup(null);
456       }
457       else
458       {
459         // Now add any sequences between min and max
460         sg.getSequences(null).clear();
461         for (int i = min; i < max; i++)
462         {
463           sg.addSequence(av.getAlignment().getSequenceAt(i), false);
464         }
465       }
466     }
467
468     if (av.getSelectionGroup() == null)
469     {
470       SequenceGroup sg = new SequenceGroup();
471       sg.setStartRes(seqCanvas.cursorX);
472       sg.setEndRes(seqCanvas.cursorX);
473       sg.addSequence(sequence, false);
474       av.setSelectionGroup(sg);
475     }
476
477     ap.paintAlignment(false);
478     av.sendSelection();
479   }
480
481   void insertGapAtCursor(boolean group)
482   {
483     groupEditing = group;
484     startseq = seqCanvas.cursorY;
485     lastres = seqCanvas.cursorX;
486     editSequence(true, false, seqCanvas.cursorX + getKeyboardNo1());
487     endEditing();
488   }
489
490   void deleteGapAtCursor(boolean group)
491   {
492     groupEditing = group;
493     startseq = seqCanvas.cursorY;
494     lastres = seqCanvas.cursorX + getKeyboardNo1();
495     editSequence(false, false, seqCanvas.cursorX);
496     endEditing();
497   }
498
499   void insertNucAtCursor(boolean group, String nuc)
500   {
501     // TODO not called - delete?
502     groupEditing = group;
503     startseq = seqCanvas.cursorY;
504     lastres = seqCanvas.cursorX;
505     editSequence(false, true, seqCanvas.cursorX + getKeyboardNo1());
506     endEditing();
507   }
508
509   void numberPressed(char value)
510   {
511     if (keyboardNo1 == null)
512     {
513       keyboardNo1 = new StringBuffer();
514     }
515
516     if (keyboardNo2 != null)
517     {
518       keyboardNo2.append(value);
519     }
520     else
521     {
522       keyboardNo1.append(value);
523     }
524   }
525
526   int getKeyboardNo1()
527   {
528     try
529     {
530       if (keyboardNo1 != null)
531       {
532         int value = Integer.parseInt(keyboardNo1.toString());
533         keyboardNo1 = null;
534         return value;
535       }
536     } catch (Exception x)
537     {
538     }
539     keyboardNo1 = null;
540     return 1;
541   }
542
543   int getKeyboardNo2()
544   {
545     try
546     {
547       if (keyboardNo2 != null)
548       {
549         int value = Integer.parseInt(keyboardNo2.toString());
550         keyboardNo2 = null;
551         return value;
552       }
553     } catch (Exception x)
554     {
555     }
556     keyboardNo2 = null;
557     return 1;
558   }
559
560   /**
561    * DOCUMENT ME!
562    * 
563    * @param evt
564    *          DOCUMENT ME!
565    */
566   @Override
567   public void mouseReleased(MouseEvent evt)
568   {
569     boolean didDrag = mouseDragging; // did we come here after a drag
570     mouseDragging = false;
571     mouseWheelPressed = false;
572
573     if (evt.isPopupTrigger()) // Windows: mouseReleased
574     {
575       showPopupMenu(evt);
576       evt.consume();
577       return;
578     }
579
580     if (!editingSeqs)
581     {
582       doMouseReleasedDefineMode(evt, didDrag);
583       return;
584     }
585
586     endEditing();
587   }
588
589   /**
590    * DOCUMENT ME!
591    * 
592    * @param evt
593    *          DOCUMENT ME!
594    */
595   @Override
596   public void mousePressed(MouseEvent evt)
597   {
598     lastMousePress = evt.getPoint();
599
600     if (SwingUtilities.isMiddleMouseButton(evt))
601     {
602       mouseWheelPressed = true;
603       return;
604     }
605
606     boolean isControlDown = Platform.isControlDown(evt);
607     if (evt.isShiftDown() || isControlDown)
608     {
609       editingSeqs = true;
610       if (isControlDown)
611       {
612         groupEditing = true;
613       }
614     }
615     else
616     {
617       doMousePressedDefineMode(evt);
618       return;
619     }
620
621     int seq = findSeq(evt);
622     int res = findColumn(evt);
623
624     if (seq < 0 || res < 0)
625     {
626       return;
627     }
628
629     if ((seq < av.getAlignment().getHeight())
630             && (res < av.getAlignment().getSequenceAt(seq).getLength()))
631     {
632       startseq = seq;
633       lastres = res;
634     }
635     else
636     {
637       startseq = -1;
638       lastres = -1;
639     }
640
641     return;
642   }
643
644   String lastMessage;
645
646   @Override
647   public void mouseOverSequence(SequenceI sequence, int index, int pos)
648   {
649     String tmp = sequence.hashCode() + " " + index + " " + pos;
650
651     if (lastMessage == null || !lastMessage.equals(tmp))
652     {
653       // System.err.println("mouseOver Sequence: "+tmp);
654       ssm.mouseOverSequence(sequence, index, pos, av);
655     }
656     lastMessage = tmp;
657   }
658
659   /**
660    * Highlight the mapped region described by the search results object (unless
661    * unchanged). This supports highlight of protein while mousing over linked
662    * cDNA and vice versa. The status bar is also updated to show the location of
663    * the start of the highlighted region.
664    */
665   @Override
666   public void highlightSequence(SearchResultsI results)
667   {
668     if (results == null || results.equals(lastSearchResults))
669     {
670       return;
671     }
672     lastSearchResults = results;
673
674     if (av.isFollowHighlight())
675     {
676       // don't allow highlight of protein/cDNA to also scroll a complementary
677       // panel,as this sets up a feedback loop (scrolling panel 1 causes moused
678       // over residue to change abruptly, causing highlighted residue in panel 2
679       // to change, causing a scroll in panel 1 etc)
680       ap.setToScrollComplementPanel(false);
681       if (ap.scrollToPosition(results, false))
682       {
683         seqCanvas.revalidate();
684       }
685       ap.setToScrollComplementPanel(true);
686     }
687     setStatusMessage(results);
688     seqCanvas.highlightSearchResults(results);
689   }
690
691   @Override
692   public VamsasSource getVamsasSource()
693   {
694     return this.ap == null ? null : this.ap.av;
695   }
696
697   @Override
698   public void updateColours(SequenceI seq, int index)
699   {
700     System.out.println("update the seqPanel colours");
701     // repaint();
702   }
703
704   /**
705    * DOCUMENT ME!
706    * 
707    * @param evt
708    *          DOCUMENT ME!
709    */
710   @Override
711   public void mouseMoved(MouseEvent evt)
712   {
713     if (editingSeqs)
714     {
715       // This is because MacOSX creates a mouseMoved
716       // If control is down, other platforms will not.
717       mouseDragged(evt);
718     }
719
720     final int column = findColumn(evt);
721     int seq = findSeq(evt);
722     if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
723     {
724       return;
725     }
726
727     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
728
729     if (column >= sequence.getLength())
730     {
731       return;
732     }
733
734     /*
735      * set status bar message, returning residue position in sequence
736      */
737     boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
738     final int pos = setStatusMessage(sequence, column, seq);
739     if (ssm != null && !isGapped)
740     {
741       mouseOverSequence(sequence, column, pos);
742     }
743
744     tooltipText.setLength(6); // Cuts the buffer back to <html>
745
746     SequenceGroup[] groups = av.getAlignment().findAllGroups(sequence);
747     if (groups != null)
748     {
749       for (int g = 0; g < groups.length; g++)
750       {
751         if (groups[g].getStartRes() <= column
752                 && groups[g].getEndRes() >= column)
753         {
754           if (!groups[g].getName().startsWith("JTreeGroup")
755                   && !groups[g].getName().startsWith("JGroup"))
756           {
757             tooltipText.append(groups[g].getName());
758           }
759
760           if (groups[g].getDescription() != null)
761           {
762             tooltipText.append(": " + groups[g].getDescription());
763           }
764         }
765       }
766     }
767
768     /*
769      * add any features at the position to the tooltip; if over a gap, only
770      * add features that straddle the gap (pos may be the residue before or
771      * after the gap)
772      */
773     if (av.isShowSequenceFeatures())
774     {
775       List<SequenceFeature> features = ap.getFeatureRenderer()
776               .findFeaturesAtRes(sequence.getDatasetSequence(), pos);
777       if (isGapped)
778       {
779         removeAdjacentFeatures(features, column + 1, sequence);
780       }
781       seqARep.appendFeatures(tooltipText, pos, features,
782               this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
783     }
784     if (tooltipText.length() == 6) // <html>
785     {
786       setToolTipText(null);
787       lastTooltip = null;
788     }
789     else
790     {
791       if (lastTooltip == null
792               || !lastTooltip.equals(tooltipText.toString()))
793       {
794         String formatedTooltipText = JvSwingUtils.wrapTooltip(true,
795                 tooltipText.toString());
796         // String formatedTooltipText = tooltipText.toString();
797         setToolTipText(formatedTooltipText);
798         lastTooltip = tooltipText.toString();
799       }
800
801     }
802
803   }
804
805   /**
806    * Removes from the list of features any that start after, or end before, the
807    * given column position. This allows us to retain only those features
808    * adjacent to a gapped position that straddle the position. Contact features
809    * that 'straddle' the position are also removed, since they are not 'at' the
810    * position.
811    * 
812    * @param features
813    * @param column
814    *          alignment column (1..)
815    * @param sequence
816    */
817   protected void removeAdjacentFeatures(List<SequenceFeature> features,
818           final int column, SequenceI sequence)
819   {
820     // TODO should this be an AlignViewController method (and reused by applet)?
821     ListIterator<SequenceFeature> it = features.listIterator();
822     while (it.hasNext())
823     {
824       SequenceFeature sf = it.next();
825       if (sf.isContactFeature()
826               || sequence.findIndex(sf.getBegin()) > column
827               || sequence.findIndex(sf.getEnd()) < column)
828       {
829         it.remove();
830       }
831     }
832   }
833
834   private Point lastp = null;
835
836   /*
837    * (non-Javadoc)
838    * 
839    * @see javax.swing.JComponent#getToolTipLocation(java.awt.event.MouseEvent)
840    */
841   @Override
842   public Point getToolTipLocation(MouseEvent event)
843   {
844     int x = event.getX(), w = getWidth();
845     int wdth = (w - x < 200) ? -(w / 2) : 5; // switch sides when tooltip is too
846     // close to edge
847     Point p = lastp;
848     if (!event.isShiftDown() || p == null)
849     {
850       p = (tooltipText != null && tooltipText.length() > 6) ? new Point(
851               event.getX() + wdth, event.getY() - 20) : null;
852     }
853     /*
854      * TODO: try to modify position region is not obcured by tooltip
855      */
856     return lastp = p;
857   }
858
859   String lastTooltip;
860
861   /**
862    * set when the current UI interaction has resulted in a change that requires
863    * overview shading to be recalculated. this could be changed to something
864    * more expressive that indicates what actually has changed, so selective
865    * redraws can be applied
866    */
867   private boolean needOverviewUpdate = false; // TODO: refactor to avcontroller
868
869   /**
870    * set if av.getSelectionGroup() refers to a group that is defined on the
871    * alignment view, rather than a transient selection
872    */
873   // private boolean editingDefinedGroup = false; // TODO: refactor to
874   // avcontroller or viewModel
875
876   /**
877    * Sets the status message in alignment panel, showing the sequence number
878    * (index) and id, and residue and residue position if not at a gap, for the
879    * given sequence and column position. Returns the residue position returned
880    * by Sequence.findPosition. Note this may be for the nearest adjacent residue
881    * if at a gapped position.
882    * 
883    * @param sequence
884    *          aligned sequence object
885    * @param column
886    *          alignment column
887    * @param seq
888    *          index of sequence in alignment
889    * @return sequence position of residue at column, or adjacent residue if at a
890    *         gap
891    */
892   int setStatusMessage(SequenceI sequence, final int column, int seq)
893   {
894     StringBuilder text = new StringBuilder(32);
895
896     /*
897      * Sequence number (if known), and sequence name.
898      */
899     String seqno = seq == -1 ? "" : " " + (seq + 1);
900     text.append("Sequence").append(seqno).append(" ID: ")
901             .append(sequence.getName());
902
903     String residue = null;
904
905     /*
906      * Try to translate the display character to residue name (null for gap).
907      */
908     final String displayChar = String.valueOf(sequence.getCharAt(column));
909     boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
910     int pos = sequence.findPosition(column);
911
912     if (!isGapped)
913     {
914       boolean nucleotide = av.getAlignment().isNucleotide();
915       if (nucleotide)
916       {
917         residue = ResidueProperties.nucleotideName.get(displayChar);
918       }
919       else
920       {
921         residue = "X".equalsIgnoreCase(displayChar) ? "X" : ("*"
922                 .equals(displayChar) ? "STOP"
923                 : ResidueProperties.aa2Triplet.get(displayChar));
924       }
925       text.append(" ").append(nucleotide ? "Nucleotide" : "Residue")
926               .append(": ").append(residue == null ? displayChar : residue);
927
928       text.append(" (").append(Integer.toString(pos)).append(")");
929     }
930     ap.alignFrame.statusBar.setText(text.toString());
931
932     return pos;
933   }
934
935   /**
936    * Set the status bar message to highlight the first matched position in
937    * search results.
938    * 
939    * @param results
940    */
941   private void setStatusMessage(SearchResultsI results)
942   {
943     AlignmentI al = this.av.getAlignment();
944     int sequenceIndex = al.findIndex(results);
945     if (sequenceIndex == -1)
946     {
947       return;
948     }
949     SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
950     for (SearchResultMatchI m : results.getResults())
951     {
952       SequenceI seq = m.getSequence();
953       if (seq.getDatasetSequence() != null)
954       {
955         seq = seq.getDatasetSequence();
956       }
957
958       if (seq == ds)
959       {
960         /*
961          * Convert position in sequence (base 1) to sequence character array
962          * index (base 0)
963          */
964         int start = m.getStart() - m.getSequence().getStart();
965         setStatusMessage(seq, start, sequenceIndex);
966         return;
967       }
968     }
969   }
970
971   /**
972    * {@inheritDoc}
973    */
974   @Override
975   public void mouseDragged(MouseEvent evt)
976   {
977     if (mouseWheelPressed)
978     {
979       boolean inSplitFrame = ap.av.getCodingComplement() != null;
980       boolean copyChanges = inSplitFrame && av.isProteinFontAsCdna();
981
982       int oldWidth = av.getCharWidth();
983
984       // Which is bigger, left-right or up-down?
985       if (Math.abs(evt.getY() - lastMousePress.getY()) > Math.abs(evt
986               .getX() - lastMousePress.getX()))
987       {
988         /*
989          * on drag up or down, decrement or increment font size
990          */
991         int fontSize = av.font.getSize();
992         boolean fontChanged = false;
993
994         if (evt.getY() < lastMousePress.getY())
995         {
996           fontChanged = true;
997           fontSize--;
998         }
999         else if (evt.getY() > lastMousePress.getY())
1000         {
1001           fontChanged = true;
1002           fontSize++;
1003         }
1004
1005         if (fontSize < 1)
1006         {
1007           fontSize = 1;
1008         }
1009
1010         if (fontChanged)
1011         {
1012           Font newFont = new Font(av.font.getName(), av.font.getStyle(),
1013                   fontSize);
1014           av.setFont(newFont, true);
1015           av.setCharWidth(oldWidth);
1016           ap.fontChanged();
1017           if (copyChanges)
1018           {
1019             ap.av.getCodingComplement().setFont(newFont, true);
1020             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1021                     .getSplitViewContainer();
1022             splitFrame.adjustLayout();
1023             splitFrame.repaint();
1024           }
1025         }
1026       }
1027       else
1028       {
1029         /*
1030          * on drag left or right, decrement or increment character width
1031          */
1032         int newWidth = 0;
1033         if (evt.getX() < lastMousePress.getX() && av.getCharWidth() > 1)
1034         {
1035           newWidth = av.getCharWidth() - 1;
1036           av.setCharWidth(newWidth);
1037         }
1038         else if (evt.getX() > lastMousePress.getX())
1039         {
1040           newWidth = av.getCharWidth() + 1;
1041           av.setCharWidth(newWidth);
1042         }
1043         if (newWidth > 0)
1044         {
1045           ap.paintAlignment(false);
1046           if (copyChanges)
1047           {
1048             /*
1049              * need to ensure newWidth is set on cdna, regardless of which
1050              * panel the mouse drag happened in; protein will compute its 
1051              * character width as 1:1 or 3:1
1052              */
1053             av.getCodingComplement().setCharWidth(newWidth);
1054             SplitFrame splitFrame = (SplitFrame) ap.alignFrame
1055                     .getSplitViewContainer();
1056             splitFrame.adjustLayout();
1057             splitFrame.repaint();
1058           }
1059         }
1060       }
1061
1062       FontMetrics fm = getFontMetrics(av.getFont());
1063       av.validCharWidth = fm.charWidth('M') <= av.getCharWidth();
1064
1065       lastMousePress = evt.getPoint();
1066
1067       return;
1068     }
1069
1070     if (!editingSeqs)
1071     {
1072       doMouseDraggedDefineMode(evt);
1073       return;
1074     }
1075
1076     int res = findColumn(evt);
1077
1078     if (res < 0)
1079     {
1080       res = 0;
1081     }
1082
1083     if ((lastres == -1) || (lastres == res))
1084     {
1085       return;
1086     }
1087
1088     if ((res < av.getAlignment().getWidth()) && (res < lastres))
1089     {
1090       // dragLeft, delete gap
1091       editSequence(false, false, res);
1092     }
1093     else
1094     {
1095       editSequence(true, false, res);
1096     }
1097
1098     mouseDragging = true;
1099     if (scrollThread != null)
1100     {
1101       scrollThread.setEvent(evt);
1102     }
1103   }
1104
1105   // TODO: Make it more clever than many booleans
1106   synchronized void editSequence(boolean insertGap, boolean editSeq,
1107           int startres)
1108   {
1109     int fixedLeft = -1;
1110     int fixedRight = -1;
1111     boolean fixedColumns = false;
1112     SequenceGroup sg = av.getSelectionGroup();
1113
1114     SequenceI seq = av.getAlignment().getSequenceAt(startseq);
1115
1116     // No group, but the sequence may represent a group
1117     if (!groupEditing && av.hasHiddenRows())
1118     {
1119       if (av.isHiddenRepSequence(seq))
1120       {
1121         sg = av.getRepresentedSequences(seq);
1122         groupEditing = true;
1123       }
1124     }
1125
1126     StringBuilder message = new StringBuilder(64);
1127     if (groupEditing)
1128     {
1129       message.append("Edit group:");
1130       if (editCommand == null)
1131       {
1132         editCommand = new EditCommand(
1133                 MessageManager.getString("action.edit_group"));
1134       }
1135     }
1136     else
1137     {
1138       message.append("Edit sequence: " + seq.getName());
1139       String label = seq.getName();
1140       if (label.length() > 10)
1141       {
1142         label = label.substring(0, 10);
1143       }
1144       if (editCommand == null)
1145       {
1146         editCommand = new EditCommand(MessageManager.formatMessage(
1147                 "label.edit_params", new String[] { label }));
1148       }
1149     }
1150
1151     if (insertGap)
1152     {
1153       message.append(" insert ");
1154     }
1155     else
1156     {
1157       message.append(" delete ");
1158     }
1159
1160     message.append(Math.abs(startres - lastres) + " gaps.");
1161     ap.alignFrame.statusBar.setText(message.toString());
1162
1163     // Are we editing within a selection group?
1164     if (groupEditing
1165             || (sg != null && sg.getSequences(av.getHiddenRepSequences())
1166                     .contains(seq)))
1167     {
1168       fixedColumns = true;
1169
1170       // sg might be null as the user may only see 1 sequence,
1171       // but the sequence represents a group
1172       if (sg == null)
1173       {
1174         if (!av.isHiddenRepSequence(seq))
1175         {
1176           endEditing();
1177           return;
1178         }
1179         sg = av.getRepresentedSequences(seq);
1180       }
1181
1182       fixedLeft = sg.getStartRes();
1183       fixedRight = sg.getEndRes();
1184
1185       if ((startres < fixedLeft && lastres >= fixedLeft)
1186               || (startres >= fixedLeft && lastres < fixedLeft)
1187               || (startres > fixedRight && lastres <= fixedRight)
1188               || (startres <= fixedRight && lastres > fixedRight))
1189       {
1190         endEditing();
1191         return;
1192       }
1193
1194       if (fixedLeft > startres)
1195       {
1196         fixedRight = fixedLeft - 1;
1197         fixedLeft = 0;
1198       }
1199       else if (fixedRight < startres)
1200       {
1201         fixedLeft = fixedRight;
1202         fixedRight = -1;
1203       }
1204     }
1205
1206     if (av.hasHiddenColumns())
1207     {
1208       fixedColumns = true;
1209       int y1 = av.getAlignment().getHiddenColumns()
1210               .getHiddenBoundaryLeft(startres);
1211       int y2 = av.getAlignment().getHiddenColumns()
1212               .getHiddenBoundaryRight(startres);
1213
1214       if ((insertGap && startres > y1 && lastres < y1)
1215               || (!insertGap && startres < y2 && lastres > y2))
1216       {
1217         endEditing();
1218         return;
1219       }
1220
1221       // System.out.print(y1+" "+y2+" "+fixedLeft+" "+fixedRight+"~~");
1222       // Selection spans a hidden region
1223       if (fixedLeft < y1 && (fixedRight > y2 || fixedRight == -1))
1224       {
1225         if (startres >= y2)
1226         {
1227           fixedLeft = y2;
1228         }
1229         else
1230         {
1231           fixedRight = y2 - 1;
1232         }
1233       }
1234     }
1235
1236     if (groupEditing)
1237     {
1238       List<SequenceI> vseqs = sg.getSequences(av.getHiddenRepSequences());
1239       int g, groupSize = vseqs.size();
1240       SequenceI[] groupSeqs = new SequenceI[groupSize];
1241       for (g = 0; g < groupSeqs.length; g++)
1242       {
1243         groupSeqs[g] = vseqs.get(g);
1244       }
1245
1246       // drag to right
1247       if (insertGap)
1248       {
1249         // If the user has selected the whole sequence, and is dragging to
1250         // the right, we can still extend the alignment and selectionGroup
1251         if (sg.getStartRes() == 0 && sg.getEndRes() == fixedRight
1252                 && sg.getEndRes() == av.getAlignment().getWidth() - 1)
1253         {
1254           sg.setEndRes(av.getAlignment().getWidth() + startres - lastres);
1255           fixedRight = sg.getEndRes();
1256         }
1257
1258         // Is it valid with fixed columns??
1259         // Find the next gap before the end
1260         // of the visible region boundary
1261         boolean blank = false;
1262         for (fixedRight = fixedRight; fixedRight > lastres; fixedRight--)
1263         {
1264           blank = true;
1265
1266           for (g = 0; g < groupSize; g++)
1267           {
1268             for (int j = 0; j < startres - lastres; j++)
1269             {
1270               if (!Comparison.isGap(groupSeqs[g].getCharAt(fixedRight - j)))
1271               {
1272                 blank = false;
1273                 break;
1274               }
1275             }
1276           }
1277           if (blank)
1278           {
1279             break;
1280           }
1281         }
1282
1283         if (!blank)
1284         {
1285           if (sg.getSize() == av.getAlignment().getHeight())
1286           {
1287             if ((av.hasHiddenColumns() && startres < av.getAlignment()
1288                     .getHiddenColumns().getHiddenBoundaryRight(startres)))
1289             {
1290               endEditing();
1291               return;
1292             }
1293
1294             int alWidth = av.getAlignment().getWidth();
1295             if (av.hasHiddenRows())
1296             {
1297               int hwidth = av.getAlignment().getHiddenSequences()
1298                       .getWidth();
1299               if (hwidth > alWidth)
1300               {
1301                 alWidth = hwidth;
1302               }
1303             }
1304             // We can still insert gaps if the selectionGroup
1305             // contains all the sequences
1306             sg.setEndRes(sg.getEndRes() + startres - lastres);
1307             fixedRight = alWidth + startres - lastres;
1308           }
1309           else
1310           {
1311             endEditing();
1312             return;
1313           }
1314         }
1315       }
1316
1317       // drag to left
1318       else if (!insertGap)
1319       {
1320         // / Are we able to delete?
1321         // ie are all columns blank?
1322
1323         for (g = 0; g < groupSize; g++)
1324         {
1325           for (int j = startres; j < lastres; j++)
1326           {
1327             if (groupSeqs[g].getLength() <= j)
1328             {
1329               continue;
1330             }
1331
1332             if (!Comparison.isGap(groupSeqs[g].getCharAt(j)))
1333             {
1334               // Not a gap, block edit not valid
1335               endEditing();
1336               return;
1337             }
1338           }
1339         }
1340       }
1341
1342       if (insertGap)
1343       {
1344         // dragging to the right
1345         if (fixedColumns && fixedRight != -1)
1346         {
1347           for (int j = lastres; j < startres; j++)
1348           {
1349             insertChar(j, groupSeqs, fixedRight);
1350           }
1351         }
1352         else
1353         {
1354           appendEdit(Action.INSERT_GAP, groupSeqs, startres, startres
1355                   - lastres);
1356         }
1357       }
1358       else
1359       {
1360         // dragging to the left
1361         if (fixedColumns && fixedRight != -1)
1362         {
1363           for (int j = lastres; j > startres; j--)
1364           {
1365             deleteChar(startres, groupSeqs, fixedRight);
1366           }
1367         }
1368         else
1369         {
1370           appendEdit(Action.DELETE_GAP, groupSeqs, startres, lastres
1371                   - startres);
1372         }
1373
1374       }
1375     }
1376     else
1377     // ///Editing a single sequence///////////
1378     {
1379       if (insertGap)
1380       {
1381         // dragging to the right
1382         if (fixedColumns && fixedRight != -1)
1383         {
1384           for (int j = lastres; j < startres; j++)
1385           {
1386             insertChar(j, new SequenceI[] { seq }, fixedRight);
1387           }
1388         }
1389         else
1390         {
1391           appendEdit(Action.INSERT_GAP, new SequenceI[] { seq }, lastres,
1392                   startres - lastres);
1393         }
1394       }
1395       else
1396       {
1397         if (!editSeq)
1398         {
1399           // dragging to the left
1400           if (fixedColumns && fixedRight != -1)
1401           {
1402             for (int j = lastres; j > startres; j--)
1403             {
1404               if (!Comparison.isGap(seq.getCharAt(startres)))
1405               {
1406                 endEditing();
1407                 break;
1408               }
1409               deleteChar(startres, new SequenceI[] { seq }, fixedRight);
1410             }
1411           }
1412           else
1413           {
1414             // could be a keyboard edit trying to delete none gaps
1415             int max = 0;
1416             for (int m = startres; m < lastres; m++)
1417             {
1418               if (!Comparison.isGap(seq.getCharAt(m)))
1419               {
1420                 break;
1421               }
1422               max++;
1423             }
1424
1425             if (max > 0)
1426             {
1427               appendEdit(Action.DELETE_GAP, new SequenceI[] { seq },
1428                       startres, max);
1429             }
1430           }
1431         }
1432         else
1433         {// insertGap==false AND editSeq==TRUE;
1434           if (fixedColumns && fixedRight != -1)
1435           {
1436             for (int j = lastres; j < startres; j++)
1437             {
1438               insertChar(j, new SequenceI[] { seq }, fixedRight);
1439             }
1440           }
1441           else
1442           {
1443             appendEdit(Action.INSERT_NUC, new SequenceI[] { seq }, lastres,
1444                     startres - lastres);
1445           }
1446         }
1447       }
1448     }
1449
1450     lastres = startres;
1451     seqCanvas.repaint();
1452   }
1453
1454   void insertChar(int j, SequenceI[] seq, int fixedColumn)
1455   {
1456     int blankColumn = fixedColumn;
1457     for (int s = 0; s < seq.length; s++)
1458     {
1459       // Find the next gap before the end of the visible region boundary
1460       // If lastCol > j, theres a boundary after the gap insertion
1461
1462       for (blankColumn = fixedColumn; blankColumn > j; blankColumn--)
1463       {
1464         if (Comparison.isGap(seq[s].getCharAt(blankColumn)))
1465         {
1466           // Theres a space, so break and insert the gap
1467           break;
1468         }
1469       }
1470
1471       if (blankColumn <= j)
1472       {
1473         blankColumn = fixedColumn;
1474         endEditing();
1475         return;
1476       }
1477     }
1478
1479     appendEdit(Action.DELETE_GAP, seq, blankColumn, 1);
1480
1481     appendEdit(Action.INSERT_GAP, seq, j, 1);
1482
1483   }
1484
1485   /**
1486    * Helper method to add and perform one edit action.
1487    * 
1488    * @param action
1489    * @param seq
1490    * @param pos
1491    * @param count
1492    */
1493   protected void appendEdit(Action action, SequenceI[] seq, int pos,
1494           int count)
1495   {
1496
1497     final Edit edit = new EditCommand().new Edit(action, seq, pos, count,
1498             av.getAlignment().getGapCharacter());
1499
1500     editCommand.appendEdit(edit, av.getAlignment(), true, null);
1501   }
1502
1503   void deleteChar(int j, SequenceI[] seq, int fixedColumn)
1504   {
1505
1506     appendEdit(Action.DELETE_GAP, seq, j, 1);
1507
1508     appendEdit(Action.INSERT_GAP, seq, fixedColumn, 1);
1509   }
1510
1511   /**
1512    * DOCUMENT ME!
1513    * 
1514    * @param e
1515    *          DOCUMENT ME!
1516    */
1517   @Override
1518   public void mouseEntered(MouseEvent e)
1519   {
1520     if (oldSeq < 0)
1521     {
1522       oldSeq = 0;
1523     }
1524
1525     if (scrollThread != null)
1526     {
1527       scrollThread.running = false;
1528       scrollThread = null;
1529     }
1530   }
1531
1532   /**
1533    * DOCUMENT ME!
1534    * 
1535    * @param e
1536    *          DOCUMENT ME!
1537    */
1538   @Override
1539   public void mouseExited(MouseEvent e)
1540   {
1541     if (av.getWrapAlignment())
1542     {
1543       return;
1544     }
1545
1546     if (mouseDragging)
1547     {
1548       scrollThread = new ScrollThread();
1549     }
1550   }
1551
1552   /**
1553    * Handler for double-click on a position with one or more sequence features.
1554    * Opens the Amend Features dialog to allow feature details to be amended, or
1555    * the feature deleted.
1556    */
1557   @Override
1558   public void mouseClicked(MouseEvent evt)
1559   {
1560     SequenceGroup sg = null;
1561     SequenceI sequence = av.getAlignment().getSequenceAt(findSeq(evt));
1562     if (evt.getClickCount() > 1)
1563     {
1564       sg = av.getSelectionGroup();
1565       if (sg != null && sg.getSize() == 1
1566               && sg.getEndRes() - sg.getStartRes() < 2)
1567       {
1568         av.setSelectionGroup(null);
1569       }
1570
1571       int column = findColumn(evt);
1572       boolean isGapped = Comparison.isGap(sequence.getCharAt(column));
1573
1574       /*
1575        * find features at the position (if not gapped), or straddling
1576        * the position (if at a gap)
1577        */
1578       List<SequenceFeature> features = seqCanvas.getFeatureRenderer()
1579               .findFeaturesAtRes(sequence.getDatasetSequence(),
1580                       sequence.findPosition(column));
1581       if (isGapped)
1582       {
1583         removeAdjacentFeatures(features, column, sequence);
1584       }
1585
1586       if (!features.isEmpty())
1587       {
1588         /*
1589          * highlight the first feature at the position on the alignment
1590          */
1591         SearchResultsI highlight = new SearchResults();
1592         highlight.addResult(sequence, features.get(0).getBegin(), features
1593                 .get(0).getEnd());
1594         seqCanvas.highlightSearchResults(highlight);
1595
1596         /*
1597          * open the Amend Features dialog; clear highlighting afterwards,
1598          * whether changes were made or not
1599          */
1600         List<SequenceI> seqs = Collections.singletonList(sequence);
1601         seqCanvas.getFeatureRenderer().amendFeatures(seqs, features, false,
1602                 ap);
1603         seqCanvas.highlightSearchResults(null);
1604       }
1605     }
1606   }
1607
1608   @Override
1609   public void mouseWheelMoved(MouseWheelEvent e)
1610   {
1611     e.consume();
1612     if (e.getWheelRotation() > 0)
1613     {
1614       if (e.isShiftDown())
1615       {
1616         av.getRanges().scrollRight(true);
1617
1618       }
1619       else
1620       {
1621         av.getRanges().scrollUp(false);
1622       }
1623     }
1624     else
1625     {
1626       if (e.isShiftDown())
1627       {
1628         av.getRanges().scrollRight(false);
1629       }
1630       else
1631       {
1632         av.getRanges().scrollUp(true);
1633       }
1634     }
1635     // TODO Update tooltip for new position.
1636   }
1637
1638   /**
1639    * DOCUMENT ME!
1640    * 
1641    * @param evt
1642    *          DOCUMENT ME!
1643    */
1644   public void doMousePressedDefineMode(MouseEvent evt)
1645   {
1646     final int res = findColumn(evt);
1647     final int seq = findSeq(evt);
1648     oldSeq = seq;
1649     needOverviewUpdate = false;
1650
1651     startWrapBlock = wrappedBlock;
1652
1653     if (av.getWrapAlignment() && seq > av.getAlignment().getHeight())
1654     {
1655       JvOptionPane.showInternalMessageDialog(Desktop.desktop, MessageManager
1656               .getString("label.cannot_edit_annotations_in_wrapped_view"),
1657               MessageManager.getString("label.wrapped_view_no_edit"),
1658               JvOptionPane.WARNING_MESSAGE);
1659       return;
1660     }
1661
1662     if (seq < 0 || res < 0)
1663     {
1664       return;
1665     }
1666
1667     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1668
1669     if ((sequence == null) || (res > sequence.getLength()))
1670     {
1671       return;
1672     }
1673
1674     stretchGroup = av.getSelectionGroup();
1675
1676     if (stretchGroup == null || !stretchGroup.contains(sequence, res))
1677     {
1678       stretchGroup = av.getAlignment().findGroup(sequence, res);
1679       if (stretchGroup != null)
1680       {
1681         // only update the current selection if the popup menu has a group to
1682         // focus on
1683         av.setSelectionGroup(stretchGroup);
1684       }
1685     }
1686
1687     if (evt.isPopupTrigger()) // Mac: mousePressed
1688     {
1689       showPopupMenu(evt);
1690       return;
1691     }
1692
1693     /*
1694      * defer right-mouse click handling to mouseReleased on Windows
1695      * (where isPopupTrigger() will answer true)
1696      * NB isRightMouseButton is also true for Cmd-click on Mac
1697      */
1698     if (SwingUtilities.isRightMouseButton(evt) && !Platform.isAMac())
1699     {
1700       return;
1701     }
1702
1703     if (av.cursorMode)
1704     {
1705       seqCanvas.cursorX = findColumn(evt);
1706       seqCanvas.cursorY = findSeq(evt);
1707       seqCanvas.repaint();
1708       return;
1709     }
1710
1711     if (stretchGroup == null)
1712     {
1713       // Only if left mouse button do we want to change group sizes
1714
1715       // define a new group here
1716       SequenceGroup sg = new SequenceGroup();
1717       sg.setStartRes(res);
1718       sg.setEndRes(res);
1719       sg.addSequence(sequence, false);
1720       av.setSelectionGroup(sg);
1721       stretchGroup = sg;
1722
1723       if (av.getConservationSelected())
1724       {
1725         SliderPanel.setConservationSlider(ap, av.getResidueShading(),
1726                 ap.getViewName());
1727       }
1728
1729       if (av.getAbovePIDThreshold())
1730       {
1731         SliderPanel.setPIDSliderSource(ap, av.getResidueShading(),
1732                 ap.getViewName());
1733       }
1734       // TODO: stretchGroup will always be not null. Is this a merge error ?
1735       if ((stretchGroup != null) && (stretchGroup.getEndRes() == res))
1736       {
1737         // Edit end res position of selected group
1738         changeEndRes = true;
1739       }
1740       else if ((stretchGroup != null)
1741               && (stretchGroup.getStartRes() == res))
1742       {
1743         // Edit end res position of selected group
1744         changeStartRes = true;
1745       }
1746       stretchGroup.getWidth();
1747     }
1748
1749     seqCanvas.repaint();
1750   }
1751
1752   /**
1753    * Build and show a pop-up menu at the right-click mouse position
1754    * 
1755    * @param evt
1756    * @param res
1757    * @param sequences
1758    */
1759   void showPopupMenu(MouseEvent evt)
1760   {
1761     final int res = findColumn(evt);
1762     final int seq = findSeq(evt);
1763     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
1764     List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
1765             .findFeaturesAtRes(sequence.getDatasetSequence(),
1766                     sequence.findPosition(res));
1767     List<String> links = new ArrayList<>();
1768     for (SequenceFeature sf : allFeatures)
1769     {
1770       if (sf.links != null)
1771       {
1772         for (String link : sf.links)
1773         {
1774           links.add(link);
1775         }
1776       }
1777     }
1778
1779     PopupMenu pop = new PopupMenu(ap, null, links);
1780     pop.show(this, evt.getX(), evt.getY());
1781   }
1782
1783   /**
1784    * Update the display after mouse up on a selection or group
1785    * 
1786    * @param evt
1787    *          mouse released event details
1788    * @param afterDrag
1789    *          true if this event is happening after a mouse drag (rather than a
1790    *          mouse down)
1791    */
1792   public void doMouseReleasedDefineMode(MouseEvent evt, boolean afterDrag)
1793   {
1794     if (stretchGroup == null)
1795     {
1796       return;
1797     }
1798     // always do this - annotation has own state
1799     // but defer colourscheme update until hidden sequences are passed in
1800     boolean vischange = stretchGroup.recalcConservation(true);
1801     needOverviewUpdate |= vischange && av.isSelectionDefinedGroup()
1802             && afterDrag;
1803     if (stretchGroup.cs != null)
1804     {
1805       stretchGroup.cs.alignmentChanged(stretchGroup,
1806               av.getHiddenRepSequences());
1807
1808       ResidueShaderI groupColourScheme = stretchGroup.getGroupColourScheme();
1809       String name = stretchGroup.getName();
1810       if (stretchGroup.cs.conservationApplied())
1811       {
1812         SliderPanel.setConservationSlider(ap, groupColourScheme, name);
1813       }
1814       if (stretchGroup.cs.getThreshold() > 0)
1815       {
1816         SliderPanel.setPIDSliderSource(ap, groupColourScheme, name);
1817       }
1818     }
1819     PaintRefresher.Refresh(this, av.getSequenceSetId());
1820     ap.paintAlignment(needOverviewUpdate);
1821     needOverviewUpdate = false;
1822     changeEndRes = false;
1823     changeStartRes = false;
1824     stretchGroup = null;
1825     av.sendSelection();
1826   }
1827
1828   /**
1829    * DOCUMENT ME!
1830    * 
1831    * @param evt
1832    *          DOCUMENT ME!
1833    */
1834   public void doMouseDraggedDefineMode(MouseEvent evt)
1835   {
1836     int res = findColumn(evt);
1837     int y = findSeq(evt);
1838
1839     if (wrappedBlock != startWrapBlock)
1840     {
1841       return;
1842     }
1843
1844     if (stretchGroup == null)
1845     {
1846       return;
1847     }
1848
1849     if (res >= av.getAlignment().getWidth())
1850     {
1851       res = av.getAlignment().getWidth() - 1;
1852     }
1853
1854     if (stretchGroup.getEndRes() == res)
1855     {
1856       // Edit end res position of selected group
1857       changeEndRes = true;
1858     }
1859     else if (stretchGroup.getStartRes() == res)
1860     {
1861       // Edit start res position of selected group
1862       changeStartRes = true;
1863     }
1864
1865     if (res < av.getRanges().getStartRes())
1866     {
1867       res = av.getRanges().getStartRes();
1868     }
1869
1870     if (changeEndRes)
1871     {
1872       if (res > (stretchGroup.getStartRes() - 1))
1873       {
1874         stretchGroup.setEndRes(res);
1875         needOverviewUpdate |= av.isSelectionDefinedGroup();
1876       }
1877     }
1878     else if (changeStartRes)
1879     {
1880       if (res < (stretchGroup.getEndRes() + 1))
1881       {
1882         stretchGroup.setStartRes(res);
1883         needOverviewUpdate |= av.isSelectionDefinedGroup();
1884       }
1885     }
1886
1887     int dragDirection = 0;
1888
1889     if (y > oldSeq)
1890     {
1891       dragDirection = 1;
1892     }
1893     else if (y < oldSeq)
1894     {
1895       dragDirection = -1;
1896     }
1897
1898     while ((y != oldSeq) && (oldSeq > -1)
1899             && (y < av.getAlignment().getHeight()))
1900     {
1901       // This routine ensures we don't skip any sequences, as the
1902       // selection is quite slow.
1903       Sequence seq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1904
1905       oldSeq += dragDirection;
1906
1907       if (oldSeq < 0)
1908       {
1909         break;
1910       }
1911
1912       Sequence nextSeq = (Sequence) av.getAlignment().getSequenceAt(oldSeq);
1913
1914       if (stretchGroup.getSequences(null).contains(nextSeq))
1915       {
1916         stretchGroup.deleteSequence(seq, false);
1917         needOverviewUpdate |= av.isSelectionDefinedGroup();
1918       }
1919       else
1920       {
1921         if (seq != null)
1922         {
1923           stretchGroup.addSequence(seq, false);
1924         }
1925
1926         stretchGroup.addSequence(nextSeq, false);
1927         needOverviewUpdate |= av.isSelectionDefinedGroup();
1928       }
1929     }
1930
1931     if (oldSeq < 0)
1932     {
1933       oldSeq = -1;
1934     }
1935
1936     mouseDragging = true;
1937
1938     if (scrollThread != null)
1939     {
1940       scrollThread.setEvent(evt);
1941     }
1942
1943     seqCanvas.repaint();
1944   }
1945
1946   void scrollCanvas(MouseEvent evt)
1947   {
1948     if (evt == null)
1949     {
1950       if (scrollThread != null)
1951       {
1952         scrollThread.running = false;
1953         scrollThread = null;
1954       }
1955       mouseDragging = false;
1956     }
1957     else
1958     {
1959       if (scrollThread == null)
1960       {
1961         scrollThread = new ScrollThread();
1962       }
1963
1964       mouseDragging = true;
1965       scrollThread.setEvent(evt);
1966     }
1967
1968   }
1969
1970   // this class allows scrolling off the bottom of the visible alignment
1971   class ScrollThread extends Thread
1972   {
1973     MouseEvent evt;
1974
1975     boolean running = false;
1976
1977     public ScrollThread()
1978     {
1979       start();
1980     }
1981
1982     public void setEvent(MouseEvent e)
1983     {
1984       evt = e;
1985     }
1986
1987     public void stopScrolling()
1988     {
1989       running = false;
1990     }
1991
1992     @Override
1993     public void run()
1994     {
1995       running = true;
1996
1997       while (running)
1998       {
1999         if (evt != null)
2000         {
2001           if (mouseDragging && (evt.getY() < 0)
2002                   && (av.getRanges().getStartSeq() > 0))
2003           {
2004             running = av.getRanges().scrollUp(true);
2005           }
2006
2007           if (mouseDragging && (evt.getY() >= getHeight())
2008                   && (av.getAlignment().getHeight() > av.getRanges()
2009                           .getEndSeq()))
2010           {
2011             running = av.getRanges().scrollUp(false);
2012           }
2013
2014           if (mouseDragging && (evt.getX() < 0))
2015           {
2016             running = av.getRanges().scrollRight(false);
2017           }
2018           else if (mouseDragging && (evt.getX() >= getWidth()))
2019           {
2020             running = av.getRanges().scrollRight(true);
2021           }
2022         }
2023
2024         try
2025         {
2026           Thread.sleep(20);
2027         } catch (Exception ex)
2028         {
2029         }
2030       }
2031     }
2032   }
2033
2034   /**
2035    * modify current selection according to a received message.
2036    */
2037   @Override
2038   public void selection(SequenceGroup seqsel, ColumnSelection colsel,
2039           HiddenColumns hidden, SelectionSource source)
2040   {
2041     // TODO: fix this hack - source of messages is align viewport, but SeqPanel
2042     // handles selection messages...
2043     // TODO: extend config options to allow user to control if selections may be
2044     // shared between viewports.
2045     boolean iSentTheSelection = (av == source || (source instanceof AlignViewport && ((AlignmentViewport) source)
2046             .getSequenceSetId().equals(av.getSequenceSetId())));
2047
2048     if (iSentTheSelection)
2049     {
2050       // respond to our own event by updating dependent dialogs
2051       if (ap.getCalculationDialog() != null)
2052       {
2053         ap.getCalculationDialog().validateCalcTypes();
2054       }
2055
2056       return;
2057     }
2058
2059     // process further ?
2060     if (!av.followSelection)
2061     {
2062       return;
2063     }
2064
2065     /*
2066      * Ignore the selection if there is one of our own pending.
2067      */
2068     if (av.isSelectionGroupChanged(false) || av.isColSelChanged(false))
2069     {
2070       return;
2071     }
2072
2073     /*
2074      * Check for selection in a view of which this one is a dna/protein
2075      * complement.
2076      */
2077     if (selectionFromTranslation(seqsel, colsel, hidden, source))
2078     {
2079       return;
2080     }
2081
2082     // do we want to thread this ? (contention with seqsel and colsel locks, I
2083     // suspect)
2084     /*
2085      * only copy colsel if there is a real intersection between
2086      * sequence selection and this panel's alignment
2087      */
2088     boolean repaint = false;
2089     boolean copycolsel = false;
2090
2091     SequenceGroup sgroup = null;
2092     if (seqsel != null && seqsel.getSize() > 0)
2093     {
2094       if (av.getAlignment() == null)
2095       {
2096         Cache.log.warn("alignviewport av SeqSetId=" + av.getSequenceSetId()
2097                 + " ViewId=" + av.getViewId()
2098                 + " 's alignment is NULL! returning immediately.");
2099         return;
2100       }
2101       sgroup = seqsel.intersect(av.getAlignment(),
2102               (av.hasHiddenRows()) ? av.getHiddenRepSequences() : null);
2103       if ((sgroup != null && sgroup.getSize() > 0))
2104       {
2105         copycolsel = true;
2106       }
2107     }
2108     if (sgroup != null && sgroup.getSize() > 0)
2109     {
2110       av.setSelectionGroup(sgroup);
2111     }
2112     else
2113     {
2114       av.setSelectionGroup(null);
2115     }
2116     av.isSelectionGroupChanged(true);
2117     repaint = true;
2118
2119     if (copycolsel)
2120     {
2121       // the current selection is unset or from a previous message
2122       // so import the new colsel.
2123       if (colsel == null || colsel.isEmpty())
2124       {
2125         if (av.getColumnSelection() != null)
2126         {
2127           av.getColumnSelection().clear();
2128           repaint = true;
2129         }
2130       }
2131       else
2132       {
2133         // TODO: shift colSel according to the intersecting sequences
2134         if (av.getColumnSelection() == null)
2135         {
2136           av.setColumnSelection(new ColumnSelection(colsel));
2137         }
2138         else
2139         {
2140           av.getColumnSelection().setElementsFrom(colsel,
2141                   av.getAlignment().getHiddenColumns());
2142         }
2143       }
2144       av.isColSelChanged(true);
2145       repaint = true;
2146     }
2147
2148     if (copycolsel
2149             && av.hasHiddenColumns()
2150             && (av.getAlignment().getHiddenColumns() == null))
2151     {
2152       System.err.println("Bad things");
2153     }
2154     if (repaint) // always true!
2155     {
2156       // probably finessing with multiple redraws here
2157       PaintRefresher.Refresh(this, av.getSequenceSetId());
2158       // ap.paintAlignment(false);
2159     }
2160
2161     // lastly, update dependent dialogs
2162     if (ap.getCalculationDialog() != null)
2163     {
2164       ap.getCalculationDialog().validateCalcTypes();
2165     }
2166
2167   }
2168
2169   /**
2170    * If this panel is a cdna/protein translation view of the selection source,
2171    * tries to map the source selection to a local one, and returns true. Else
2172    * returns false.
2173    * 
2174    * @param seqsel
2175    * @param colsel
2176    * @param source
2177    */
2178   protected boolean selectionFromTranslation(SequenceGroup seqsel,
2179           ColumnSelection colsel, HiddenColumns hidden,
2180           SelectionSource source)
2181   {
2182     if (!(source instanceof AlignViewportI))
2183     {
2184       return false;
2185     }
2186     final AlignViewportI sourceAv = (AlignViewportI) source;
2187     if (sourceAv.getCodingComplement() != av
2188             && av.getCodingComplement() != sourceAv)
2189     {
2190       return false;
2191     }
2192
2193     /*
2194      * Map sequence selection
2195      */
2196     SequenceGroup sg = MappingUtils.mapSequenceGroup(seqsel, sourceAv, av);
2197     av.setSelectionGroup(sg);
2198     av.isSelectionGroupChanged(true);
2199
2200     /*
2201      * Map column selection
2202      */
2203     // ColumnSelection cs = MappingUtils.mapColumnSelection(colsel, sourceAv,
2204     // av);
2205     ColumnSelection cs = new ColumnSelection();
2206     HiddenColumns hs = new HiddenColumns();
2207     MappingUtils.mapColumnSelection(colsel, hidden, sourceAv, av, cs, hs);
2208     av.setColumnSelection(cs);
2209     av.getAlignment().setHiddenColumns(hs);
2210
2211     // lastly, update any dependent dialogs
2212     if (ap.getCalculationDialog() != null)
2213     {
2214       ap.getCalculationDialog().validateCalcTypes();
2215     }
2216
2217     PaintRefresher.Refresh(this, av.getSequenceSetId());
2218
2219     return true;
2220   }
2221 }