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