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