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