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