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