Merge branch 'feature/JAL-3093wrappedModeTooltips' into
[jalview.git] / src / jalview / gui / AnnotationPanel.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.datamodel.AlignmentAnnotation;
24 import jalview.datamodel.AlignmentI;
25 import jalview.datamodel.Annotation;
26 import jalview.datamodel.ColumnSelection;
27 import jalview.datamodel.HiddenColumns;
28 import jalview.datamodel.SequenceI;
29 import jalview.renderer.AnnotationRenderer;
30 import jalview.renderer.AwtRenderPanelI;
31 import jalview.schemes.ResidueProperties;
32 import jalview.util.Comparison;
33 import jalview.util.MessageManager;
34 import jalview.util.Platform;
35 import jalview.viewmodel.ViewportListenerI;
36 import jalview.viewmodel.ViewportRanges;
37
38 import java.awt.AlphaComposite;
39 import java.awt.Color;
40 import java.awt.Dimension;
41 import java.awt.FontMetrics;
42 import java.awt.Graphics;
43 import java.awt.Graphics2D;
44 import java.awt.Image;
45 import java.awt.Rectangle;
46 import java.awt.RenderingHints;
47 import java.awt.event.ActionEvent;
48 import java.awt.event.ActionListener;
49 import java.awt.event.AdjustmentEvent;
50 import java.awt.event.AdjustmentListener;
51 import java.awt.event.MouseEvent;
52 import java.awt.event.MouseListener;
53 import java.awt.event.MouseMotionListener;
54 import java.awt.event.MouseWheelEvent;
55 import java.awt.event.MouseWheelListener;
56 import java.awt.image.BufferedImage;
57 import java.beans.PropertyChangeEvent;
58 import java.util.ArrayList;
59 import java.util.Collections;
60 import java.util.List;
61
62 import javax.swing.JColorChooser;
63 import javax.swing.JMenuItem;
64 import javax.swing.JPanel;
65 import javax.swing.JPopupMenu;
66 import javax.swing.Scrollable;
67 import javax.swing.ToolTipManager;
68
69 /**
70  * AnnotationPanel displays visible portion of annotation rows below unwrapped
71  * alignment
72  * 
73  * @author $author$
74  * @version $Revision$
75  */
76 public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
77         MouseListener, MouseWheelListener, MouseMotionListener,
78         ActionListener, AdjustmentListener, Scrollable, ViewportListenerI
79 {
80   String HELIX = MessageManager.getString("label.helix");
81
82   String SHEET = MessageManager.getString("label.sheet");
83
84   /**
85    * For RNA secondary structure "stems" aka helices
86    */
87   String STEM = MessageManager.getString("label.rna_helix");
88
89   String LABEL = MessageManager.getString("label.label");
90
91   String REMOVE = MessageManager.getString("label.remove_annotation");
92
93   String COLOUR = MessageManager.getString("action.colour");
94
95   public final Color HELIX_COLOUR = Color.red.darker();
96
97   public final Color SHEET_COLOUR = Color.green.darker().darker();
98
99   public final Color STEM_COLOUR = Color.blue.darker();
100
101   /** DOCUMENT ME!! */
102   public AlignViewport av;
103
104   AlignmentPanel ap;
105
106   public int activeRow = -1;
107
108   public BufferedImage image;
109
110   public volatile BufferedImage fadedImage;
111
112   Graphics2D gg;
113
114   public FontMetrics fm;
115
116   public int imgWidth = 0;
117
118   boolean fastPaint = false;
119
120   // Used For mouse Dragging and resizing graphs
121   int graphStretch = -1;
122
123   int graphStretchY = -1;
124
125   int min; // used by mouseDragged to see if user
126
127   int max; // used by mouseDragged to see if user
128
129   boolean mouseDragging = false;
130
131   // for editing cursor
132   int cursorX = 0;
133
134   int cursorY = 0;
135
136   public final AnnotationRenderer renderer;
137
138   private MouseWheelListener[] _mwl;
139
140   /**
141    * Creates a new AnnotationPanel object.
142    * 
143    * @param ap
144    *          DOCUMENT ME!
145    */
146   public AnnotationPanel(AlignmentPanel ap)
147   {
148     ToolTipManager.sharedInstance().registerComponent(this);
149     ToolTipManager.sharedInstance().setInitialDelay(0);
150     ToolTipManager.sharedInstance().setDismissDelay(10000);
151     this.ap = ap;
152     av = ap.av;
153     this.setLayout(null);
154     addMouseListener(this);
155     addMouseMotionListener(this);
156     ap.annotationScroller.getVerticalScrollBar()
157             .addAdjustmentListener(this);
158     // save any wheel listeners on the scroller, so we can propagate scroll
159     // events to them.
160     _mwl = ap.annotationScroller.getMouseWheelListeners();
161     // and then set our own listener to consume all mousewheel events
162     ap.annotationScroller.addMouseWheelListener(this);
163     renderer = new AnnotationRenderer();
164
165     av.getRanges().addPropertyChangeListener(this);
166   }
167
168   public AnnotationPanel(AlignViewport av)
169   {
170     this.av = av;
171     renderer = new AnnotationRenderer();
172   }
173
174   @Override
175   public void mouseWheelMoved(MouseWheelEvent e)
176   {
177     if (e.isShiftDown())
178     {
179       e.consume();
180       double wheelRotation = e.getPreciseWheelRotation();
181       if (wheelRotation > 0)
182       {
183         av.getRanges().scrollRight(true);
184       }
185       else if (wheelRotation < 0)
186       {
187         av.getRanges().scrollRight(false);
188       }
189     }
190     else
191     {
192       // TODO: find the correct way to let the event bubble up to
193       // ap.annotationScroller
194       for (MouseWheelListener mwl : _mwl)
195       {
196         if (mwl != null)
197         {
198           mwl.mouseWheelMoved(e);
199         }
200         if (e.isConsumed())
201         {
202           break;
203         }
204       }
205     }
206   }
207
208   @Override
209   public Dimension getPreferredScrollableViewportSize()
210   {
211     Dimension ps = getPreferredSize();
212     return new Dimension(ps.width, adjustForAlignFrame(false, ps.height));
213   }
214
215   @Override
216   public int getScrollableBlockIncrement(Rectangle visibleRect,
217           int orientation, int direction)
218   {
219     return 30;
220   }
221
222   @Override
223   public boolean getScrollableTracksViewportHeight()
224   {
225     return false;
226   }
227
228   @Override
229   public boolean getScrollableTracksViewportWidth()
230   {
231     return true;
232   }
233
234   @Override
235   public int getScrollableUnitIncrement(Rectangle visibleRect,
236           int orientation, int direction)
237   {
238     return 30;
239   }
240
241   /*
242    * (non-Javadoc)
243    * 
244    * @see
245    * java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event
246    * .AdjustmentEvent)
247    */
248   @Override
249   public void adjustmentValueChanged(AdjustmentEvent evt)
250   {
251     // update annotation label display
252     ap.getAlabels().setScrollOffset(-evt.getValue());
253   }
254
255   /**
256    * Calculates the height of the annotation displayed in the annotation panel.
257    * Callers should normally call the ap.adjustAnnotationHeight method to ensure
258    * all annotation associated components are updated correctly.
259    * 
260    */
261   public int adjustPanelHeight()
262   {
263     int height = av.calcPanelHeight();
264     this.setPreferredSize(new Dimension(1, height));
265     if (ap != null)
266     {
267       // revalidate only when the alignment panel is fully constructed
268       ap.validate();
269     }
270
271     return height;
272   }
273
274   /**
275    * DOCUMENT ME!
276    * 
277    * @param evt
278    *          DOCUMENT ME!
279    */
280   @Override
281   public void actionPerformed(ActionEvent evt)
282   {
283     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
284     if (aa == null)
285     {
286       return;
287     }
288     Annotation[] anot = aa[activeRow].annotations;
289
290     if (anot.length < av.getColumnSelection().getMax())
291     {
292       Annotation[] temp = new Annotation[av.getColumnSelection().getMax()
293               + 2];
294       System.arraycopy(anot, 0, temp, 0, anot.length);
295       anot = temp;
296       aa[activeRow].annotations = anot;
297     }
298
299     String action = evt.getActionCommand();
300     if (action.equals(REMOVE))
301     {
302       for (int index : av.getColumnSelection().getSelected())
303       {
304         if (av.getAlignment().getHiddenColumns().isVisible(index))
305         {
306           anot[index] = null;
307         }
308       }
309     }
310     else if (action.equals(LABEL))
311     {
312       String exMesg = collectAnnotVals(anot, LABEL);
313       String label = JvOptionPane.showInputDialog(this,
314               MessageManager.getString("label.enter_label"), exMesg);
315
316       if (label == null)
317       {
318         return;
319       }
320
321       if ((label.length() > 0) && !aa[activeRow].hasText)
322       {
323         aa[activeRow].hasText = true;
324       }
325
326       for (int index : av.getColumnSelection().getSelected())
327       {
328         if (!av.getAlignment().getHiddenColumns().isVisible(index))
329         {
330           continue;
331         }
332
333         if (anot[index] == null)
334         {
335           anot[index] = new Annotation(label, "", ' ', 0);
336         }
337         else
338         {
339           anot[index].displayCharacter = label;
340         }
341       }
342     }
343     else if (action.equals(COLOUR))
344     {
345       Color col = JColorChooser.showDialog(this,
346               MessageManager.getString("label.select_foreground_colour"),
347               Color.black);
348
349       for (int index : av.getColumnSelection().getSelected())
350       {
351         if (!av.getAlignment().getHiddenColumns().isVisible(index))
352         {
353           continue;
354         }
355
356         if (anot[index] == null)
357         {
358           anot[index] = new Annotation("", "", ' ', 0);
359         }
360
361         anot[index].colour = col;
362       }
363     }
364     else
365     // HELIX, SHEET or STEM
366     {
367       char type = 0;
368       String symbol = "\u03B1"; // alpha
369
370       if (action.equals(HELIX))
371       {
372         type = 'H';
373       }
374       else if (action.equals(SHEET))
375       {
376         type = 'E';
377         symbol = "\u03B2"; // beta
378       }
379
380       // Added by LML to color stems
381       else if (action.equals(STEM))
382       {
383         type = 'S';
384         int column = av.getColumnSelection().getSelectedRanges().get(0)[0];
385         symbol = aa[activeRow].getDefaultRnaHelixSymbol(column);
386       }
387
388       if (!aa[activeRow].hasIcons)
389       {
390         aa[activeRow].hasIcons = true;
391       }
392
393       String label = JvOptionPane.showInputDialog(MessageManager
394               .getString("label.enter_label_for_the_structure"), symbol);
395
396       if (label == null)
397       {
398         return;
399       }
400
401       if ((label.length() > 0) && !aa[activeRow].hasText)
402       {
403         aa[activeRow].hasText = true;
404         if (action.equals(STEM))
405         {
406           aa[activeRow].showAllColLabels = true;
407         }
408       }
409       for (int index : av.getColumnSelection().getSelected())
410       {
411         if (!av.getAlignment().getHiddenColumns().isVisible(index))
412         {
413           continue;
414         }
415
416         if (anot[index] == null)
417         {
418           anot[index] = new Annotation(label, "", type, 0);
419         }
420
421         anot[index].secondaryStructure = type != 'S' ? type
422                 : label.length() == 0 ? ' ' : label.charAt(0);
423         anot[index].displayCharacter = label;
424
425       }
426     }
427
428     av.getAlignment().validateAnnotation(aa[activeRow]);
429     ap.alignmentChanged();
430     ap.alignFrame.setMenusForViewport();
431     adjustPanelHeight();
432     repaint();
433
434     return;
435   }
436
437   /**
438    * Returns any existing annotation concatenated as a string. For each
439    * annotation, takes the description, if any, else the secondary structure
440    * character (if type is HELIX, SHEET or STEM), else the display character (if
441    * type is LABEL).
442    * 
443    * @param anots
444    * @param type
445    * @return
446    */
447   private String collectAnnotVals(Annotation[] anots, String type)
448   {
449     // TODO is this method wanted? why? 'last' is not used
450
451     StringBuilder collatedInput = new StringBuilder(64);
452     String last = "";
453     ColumnSelection viscols = av.getColumnSelection();
454     HiddenColumns hidden = av.getAlignment().getHiddenColumns();
455
456     /*
457      * the selection list (read-only view) is in selection order, not
458      * column order; make a copy so we can sort it
459      */
460     List<Integer> selected = new ArrayList<>(viscols.getSelected());
461     Collections.sort(selected);
462     for (int index : selected)
463     {
464       // always check for current display state - just in case
465       if (!hidden.isVisible(index))
466       {
467         continue;
468       }
469       String tlabel = null;
470       if (anots[index] != null)
471       { // LML added stem code
472         if (type.equals(HELIX) || type.equals(SHEET) || type.equals(STEM)
473                 || type.equals(LABEL))
474         {
475           tlabel = anots[index].description;
476           if (tlabel == null || tlabel.length() < 1)
477           {
478             if (type.equals(HELIX) || type.equals(SHEET)
479                     || type.equals(STEM))
480             {
481               tlabel = "" + anots[index].secondaryStructure;
482             }
483             else
484             {
485               tlabel = "" + anots[index].displayCharacter;
486             }
487           }
488         }
489         if (tlabel != null && !tlabel.equals(last))
490         {
491           if (last.length() > 0)
492           {
493             collatedInput.append(" ");
494           }
495           collatedInput.append(tlabel);
496         }
497       }
498     }
499     return collatedInput.toString();
500   }
501
502   /**
503    * DOCUMENT ME!
504    * 
505    * @param evt
506    *          DOCUMENT ME!
507    */
508   @Override
509   public void mousePressed(MouseEvent evt)
510   {
511
512     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
513     if (aa == null)
514     {
515       return;
516     }
517
518     int height = 0;
519     activeRow = -1;
520
521     final int y = evt.getY();
522     for (int i = 0; i < aa.length; i++)
523     {
524       if (aa[i].visible)
525       {
526         height += aa[i].height;
527       }
528
529       if (y < height)
530       {
531         if (aa[i].editable)
532         {
533           activeRow = i;
534         }
535         else if (aa[i].graph > 0)
536         {
537           // Stretch Graph
538           graphStretch = i;
539           graphStretchY = y;
540         }
541
542         break;
543       }
544     }
545
546     /*
547      * isPopupTrigger fires in mousePressed on Mac,
548      * not until mouseRelease on Windows
549      */
550     if (evt.isPopupTrigger() && activeRow != -1)
551     {
552       showPopupMenu(y, evt.getX());
553       return;
554     }
555
556     ap.getScalePanel().mousePressed(evt);
557   }
558
559   /**
560    * Construct and display a context menu at the right-click position
561    * 
562    * @param y
563    * @param x
564    */
565   void showPopupMenu(final int y, int x)
566   {
567     if (av.getColumnSelection() == null
568             || av.getColumnSelection().isEmpty())
569     {
570       return;
571     }
572
573     JPopupMenu pop = new JPopupMenu(
574             MessageManager.getString("label.structure_type"));
575     JMenuItem item;
576     /*
577      * Just display the needed structure options
578      */
579     if (av.getAlignment().isNucleotide())
580     {
581       item = new JMenuItem(STEM);
582       item.addActionListener(this);
583       pop.add(item);
584     }
585     else
586     {
587       item = new JMenuItem(HELIX);
588       item.addActionListener(this);
589       pop.add(item);
590       item = new JMenuItem(SHEET);
591       item.addActionListener(this);
592       pop.add(item);
593     }
594     item = new JMenuItem(LABEL);
595     item.addActionListener(this);
596     pop.add(item);
597     item = new JMenuItem(COLOUR);
598     item.addActionListener(this);
599     pop.add(item);
600     item = new JMenuItem(REMOVE);
601     item.addActionListener(this);
602     pop.add(item);
603     pop.show(this, x, y);
604   }
605
606   /**
607    * DOCUMENT ME!
608    * 
609    * @param evt
610    *          DOCUMENT ME!
611    */
612   @Override
613   public void mouseReleased(MouseEvent evt)
614   {
615     graphStretch = -1;
616     graphStretchY = -1;
617     mouseDragging = false;
618     ap.getScalePanel().mouseReleased(evt);
619
620     /*
621      * isPopupTrigger is set in mouseReleased on Windows
622      * (in mousePressed on Mac)
623      */
624     if (evt.isPopupTrigger() && activeRow != -1)
625     {
626       showPopupMenu(evt.getY(), evt.getX());
627     }
628
629   }
630
631   /**
632    * DOCUMENT ME!
633    * 
634    * @param evt
635    *          DOCUMENT ME!
636    */
637   @Override
638   public void mouseEntered(MouseEvent evt)
639   {
640     ap.getScalePanel().mouseEntered(evt);
641   }
642
643   /**
644    * DOCUMENT ME!
645    * 
646    * @param evt
647    *          DOCUMENT ME!
648    */
649   @Override
650   public void mouseExited(MouseEvent evt)
651   {
652     ap.getScalePanel().mouseExited(evt);
653   }
654
655   /**
656    * DOCUMENT ME!
657    * 
658    * @param evt
659    *          DOCUMENT ME!
660    */
661   @Override
662   public void mouseDragged(MouseEvent evt)
663   {
664     if (graphStretch > -1)
665     {
666       av.getAlignment()
667               .getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
668                       - evt.getY();
669       if (av.getAlignment()
670               .getAlignmentAnnotation()[graphStretch].graphHeight < 0)
671       {
672         av.getAlignment()
673                 .getAlignmentAnnotation()[graphStretch].graphHeight = 0;
674       }
675       graphStretchY = evt.getY();
676       adjustPanelHeight();
677       ap.paintAlignment(false, false);
678     }
679     else
680     {
681       ap.getScalePanel().mouseDragged(evt);
682     }
683   }
684
685   /**
686    * Constructs the tooltip, and constructs and displays a status message, for
687    * the current mouse position
688    * 
689    * @param evt
690    */
691   @Override
692   public void mouseMoved(MouseEvent evt)
693   {
694     int yPos = evt.getY();
695     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
696
697     int row = getRowIndex(yPos, aa);
698
699     if (row == -1)
700     {
701       this.setToolTipText(null);
702       return;
703     }
704
705     int column = (evt.getX() / av.getCharWidth())
706             + av.getRanges().getStartRes();
707     column = Math.min(column, av.getRanges().getEndRes());
708
709     if (av.hasHiddenColumns())
710     {
711       column = av.getAlignment().getHiddenColumns()
712               .visibleToAbsoluteColumn(column);
713     }
714
715     AlignmentAnnotation ann = aa[row];
716     if (row > -1 && ann.annotations != null
717             && column < ann.annotations.length)
718     {
719       setToolTipText(buildToolTip(ann, column, aa));
720       String msg = getStatusMessage(av.getAlignment(), column, ann);
721       ap.alignFrame.setStatus(msg);
722     }
723     else
724     {
725       this.setToolTipText(null);
726       ap.alignFrame.setStatus(" ");
727     }
728   }
729
730   /**
731    * Answers the index in the annotations array of the visible annotation at the
732    * given y position. This is done by adding the heights of visible annotations
733    * until the y position has been exceeded. Answers -1 if no annotations are
734    * visible, or the y position is below all annotations.
735    * 
736    * @param yPos
737    * @param aa
738    * @return
739    */
740   static int getRowIndex(int yPos, AlignmentAnnotation[] aa)
741   {
742     if (aa == null)
743     {
744       return -1;
745     }
746     int row = -1;
747     int height = 0;
748
749     for (int i = 0; i < aa.length; i++)
750     {
751       if (aa[i].visible)
752       {
753         height += aa[i].height;
754       }
755
756       if (height > yPos)
757       {
758         row = i;
759         break;
760       }
761     }
762     return row;
763   }
764
765   /**
766    * Answers a tooltip for the annotation at the current mouse position
767    * 
768    * @param ann
769    * @param column
770    * @param anns
771    */
772   static String buildToolTip(AlignmentAnnotation ann, int column,
773           AlignmentAnnotation[] anns)
774   {
775     String tooltip = null;
776     if (ann.graphGroup > -1)
777     {
778       StringBuilder tip = new StringBuilder(32);
779       tip.append("<html>");
780       for (int i = 0; i < anns.length; i++)
781       {
782         if (anns[i].graphGroup == ann.graphGroup
783                 && anns[i].annotations[column] != null)
784         {
785           tip.append(anns[i].label);
786           String description = anns[i].annotations[column].description;
787           if (description != null && description.length() > 0)
788           {
789             tip.append(" ").append(description);
790           }
791           tip.append("<br>");
792         }
793       }
794       if (tip.length() != 6)
795       {
796         tip.setLength(tip.length() - 4);
797         tooltip = tip.toString() + "</html>";
798       }
799     }
800     else if (column < ann.annotations.length
801             && ann.annotations[column] != null)
802     {
803       String description = ann.annotations[column].description;
804       if (description != null && description.length() > 0)
805       {
806         tooltip = JvSwingUtils.wrapTooltip(true, description);
807       }
808       else
809       {
810         tooltip = null; // no tooltip if null or empty description
811       }
812     }
813     else
814     {
815       // clear the tooltip.
816       tooltip = null;
817     }
818     return tooltip;
819   }
820
821   /**
822    * Constructs and returns the status bar message
823    * 
824    * @param al
825    * @param column
826    * @param ann
827    */
828   static String getStatusMessage(AlignmentI al, int column,
829           AlignmentAnnotation ann)
830   {
831     /*
832      * show alignment column and annotation description if any
833      */
834     StringBuilder text = new StringBuilder(32);
835     text.append(MessageManager.getString("label.column")).append(" ")
836             .append(column + 1);
837
838     if (column < ann.annotations.length && ann.annotations[column] != null)
839     {
840       String description = ann.annotations[column].description;
841       if (description != null && description.trim().length() > 0)
842       {
843         text.append("  ").append(description);
844       }
845     }
846
847     /*
848      * if the annotation is sequence-specific, show the sequence number
849      * in the alignment, and (if not a gap) the residue and position
850      */
851     SequenceI seqref = ann.sequenceRef;
852     if (seqref != null)
853     {
854       int seqIndex = al.findIndex(seqref);
855       if (seqIndex != -1)
856       {
857         text.append(", ").append(MessageManager.getString("label.sequence"))
858                 .append(" ").append(seqIndex + 1);
859         char residue = seqref.getCharAt(column);
860         if (!Comparison.isGap(residue))
861         {
862           text.append(" ");
863           String name;
864           if (al.isNucleotide())
865           {
866             name = ResidueProperties.nucleotideName
867                     .get(String.valueOf(residue));
868             text.append(" Nucleotide: ")
869                     .append(name != null ? name : residue);
870           }
871           else
872           {
873             name = 'X' == residue ? "X"
874                     : ('*' == residue ? "STOP"
875                             : ResidueProperties.aa2Triplet
876                                     .get(String.valueOf(residue)));
877             text.append(" Residue: ").append(name != null ? name : residue);
878           }
879           int residuePos = seqref.findPosition(column);
880           text.append(" (").append(residuePos).append(")");
881         }
882       }
883     }
884
885     return text.toString();
886   }
887
888   /**
889    * DOCUMENT ME!
890    * 
891    * @param evt
892    *          DOCUMENT ME!
893    */
894   @Override
895   public void mouseClicked(MouseEvent evt)
896   {
897     // if (activeRow != -1)
898     // {
899     // AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
900     // AlignmentAnnotation anot = aa[activeRow];
901     // }
902   }
903
904   // TODO mouseClicked-content and drawCursor are quite experimental!
905   public void drawCursor(Graphics graphics, SequenceI seq, int res, int x1,
906           int y1)
907   {
908     int pady = av.getCharHeight() / 5;
909     int charOffset = 0;
910     graphics.setColor(Color.black);
911     graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
912
913     if (av.validCharWidth)
914     {
915       graphics.setColor(Color.white);
916
917       char s = seq.getCharAt(res);
918
919       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
920       graphics.drawString(String.valueOf(s), charOffset + x1,
921               (y1 + av.getCharHeight()) - pady);
922     }
923
924   }
925
926   private volatile boolean imageFresh = false;
927
928   /**
929    * DOCUMENT ME!
930    * 
931    * @param g
932    *          DOCUMENT ME!
933    */
934   @Override
935   public void paintComponent(Graphics g)
936   {
937     super.paintComponent(g);
938
939     g.setColor(Color.white);
940     g.fillRect(0, 0, getWidth(), getHeight());
941
942     if (image != null)
943     {
944       if (fastPaint || (getVisibleRect().width != g.getClipBounds().width)
945               || (getVisibleRect().height != g.getClipBounds().height))
946       {
947         g.drawImage(image, 0, 0, this);
948         fastPaint = false;
949         return;
950       }
951     }
952     imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
953             + 1) * av.getCharWidth();
954     if (imgWidth < 1)
955     {
956       return;
957     }
958     if (image == null || imgWidth != image.getWidth(this)
959             || image.getHeight(this) != getHeight())
960     {
961       try
962       {
963         image = new BufferedImage(imgWidth,
964                 ap.getAnnotationPanel().getHeight(),
965                 BufferedImage.TYPE_INT_RGB);
966       } catch (OutOfMemoryError oom)
967       {
968         try
969         {
970           System.gc();
971         } catch (Exception x)
972         {
973         }
974         ;
975         new OOMWarning(
976                 "Couldn't allocate memory to redraw screen. Please restart Jalview",
977                 oom);
978         return;
979       }
980       gg = (Graphics2D) image.getGraphics();
981
982       if (av.antiAlias)
983       {
984         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
985                 RenderingHints.VALUE_ANTIALIAS_ON);
986       }
987
988       gg.setFont(av.getFont());
989       fm = gg.getFontMetrics();
990       gg.setColor(Color.white);
991       gg.fillRect(0, 0, imgWidth, image.getHeight());
992       imageFresh = true;
993     }
994     
995     drawComponent(gg, av.getRanges().getStartRes(),
996             av.getRanges().getEndRes() + 1);
997     imageFresh = false;
998     g.drawImage(image, 0, 0, this);
999   }
1000
1001   /**
1002    * set true to enable redraw timing debug output on stderr
1003    */
1004   private final boolean debugRedraw = false;
1005
1006   /**
1007    * non-Thread safe repaint
1008    * 
1009    * @param horizontal
1010    *          repaint with horizontal shift in alignment
1011    */
1012   public void fastPaint(int horizontal)
1013   {
1014     if ((horizontal == 0) || gg == null
1015             || av.getAlignment().getAlignmentAnnotation() == null
1016             || av.getAlignment().getAlignmentAnnotation().length < 1
1017             || av.isCalcInProgress())
1018     {
1019       repaint();
1020       return;
1021     }
1022
1023     int sr = av.getRanges().getStartRes();
1024     int er = av.getRanges().getEndRes() + 1;
1025     int transX = 0;
1026
1027     gg.copyArea(0, 0, imgWidth, getHeight(),
1028             -horizontal * av.getCharWidth(), 0);
1029
1030     if (horizontal > 0) // scrollbar pulled right, image to the left
1031     {
1032       transX = (er - sr - horizontal) * av.getCharWidth();
1033       sr = er - horizontal;
1034     }
1035     else if (horizontal < 0)
1036     {
1037       er = sr - horizontal;
1038     }
1039
1040     gg.translate(transX, 0);
1041
1042     drawComponent(gg, sr, er);
1043
1044     gg.translate(-transX, 0);
1045
1046     fastPaint = true;
1047
1048     // Call repaint on alignment panel so that repaints from other alignment
1049     // panel components can be aggregated. Otherwise performance of the overview
1050     // window and others may be adversely affected.
1051     av.getAlignPanel().repaint();
1052   }
1053
1054   private volatile boolean lastImageGood = false;
1055
1056   /**
1057    * DOCUMENT ME!
1058    * 
1059    * @param g
1060    *          DOCUMENT ME!
1061    * @param startRes
1062    *          DOCUMENT ME!
1063    * @param endRes
1064    *          DOCUMENT ME!
1065    */
1066   public void drawComponent(Graphics g, int startRes, int endRes)
1067   {
1068     BufferedImage oldFaded = fadedImage;
1069     if (av.isCalcInProgress())
1070     {
1071       if (image == null)
1072       {
1073         lastImageGood = false;
1074         return;
1075       }
1076       // We'll keep a record of the old image,
1077       // and draw a faded image until the calculation
1078       // has completed
1079       if (lastImageGood
1080               && (fadedImage == null || fadedImage.getWidth() != imgWidth
1081                       || fadedImage.getHeight() != image.getHeight()))
1082       {
1083         // System.err.println("redraw faded image ("+(fadedImage==null ?
1084         // "null image" : "") + " lastGood="+lastImageGood+")");
1085         fadedImage = new BufferedImage(imgWidth, image.getHeight(),
1086                 BufferedImage.TYPE_INT_RGB);
1087
1088         Graphics2D fadedG = (Graphics2D) fadedImage.getGraphics();
1089
1090         fadedG.setColor(Color.white);
1091         fadedG.fillRect(0, 0, imgWidth, image.getHeight());
1092
1093         fadedG.setComposite(
1094                 AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .3f));
1095         fadedG.drawImage(image, 0, 0, this);
1096
1097       }
1098       // make sure we don't overwrite the last good faded image until all
1099       // calculations have finished
1100       lastImageGood = false;
1101
1102     }
1103     else
1104     {
1105       if (fadedImage != null)
1106       {
1107         oldFaded = fadedImage;
1108       }
1109       fadedImage = null;
1110     }
1111
1112     g.setColor(Color.white);
1113     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(), getHeight());
1114
1115     g.setFont(av.getFont());
1116     if (fm == null)
1117     {
1118       fm = g.getFontMetrics();
1119     }
1120
1121     if ((av.getAlignment().getAlignmentAnnotation() == null)
1122             || (av.getAlignment().getAlignmentAnnotation().length < 1))
1123     {
1124       g.setColor(Color.white);
1125       g.fillRect(0, 0, getWidth(), getHeight());
1126       g.setColor(Color.black);
1127       if (av.validCharWidth)
1128       {
1129         g.drawString(MessageManager
1130                 .getString("label.alignment_has_no_annotations"), 20, 15);
1131       }
1132
1133       return;
1134     }
1135     lastImageGood = renderer.drawComponent(this, av, g, activeRow, startRes,
1136             endRes);
1137     if (!lastImageGood && fadedImage == null)
1138     {
1139       fadedImage = oldFaded;
1140     }
1141   }
1142
1143   @Override
1144   public FontMetrics getFontMetrics()
1145   {
1146     return fm;
1147   }
1148
1149   @Override
1150   public Image getFadedImage()
1151   {
1152     return fadedImage;
1153   }
1154
1155   @Override
1156   public int getFadedImageWidth()
1157   {
1158     return imgWidth;
1159   }
1160
1161   private int[] bounds = new int[2];
1162
1163   @Override
1164   public int[] getVisibleVRange()
1165   {
1166     if (ap != null && ap.getAlabels() != null)
1167     {
1168       int sOffset = -ap.getAlabels().getScrollOffset();
1169       int visHeight = sOffset + ap.annotationSpaceFillerHolder.getHeight();
1170       bounds[0] = sOffset;
1171       bounds[1] = visHeight;
1172       return bounds;
1173     }
1174     else
1175     {
1176       return null;
1177     }
1178   }
1179
1180   /**
1181    * Try to ensure any references held are nulled
1182    */
1183   public void dispose()
1184   {
1185     av = null;
1186     ap = null;
1187     image = null;
1188     fadedImage = null;
1189     gg = null;
1190     _mwl = null;
1191
1192     /*
1193      * I created the renderer so I will dispose of it
1194      */
1195     if (renderer != null)
1196     {
1197       renderer.dispose();
1198     }
1199   }
1200
1201   @Override
1202   public void propertyChange(PropertyChangeEvent evt)
1203   {
1204     // Respond to viewport range changes (e.g. alignment panel was scrolled)
1205     // Both scrolling and resizing change viewport ranges: scrolling changes
1206     // both start and end points, but resize only changes end values.
1207     // Here we only want to fastpaint on a scroll, with resize using a normal
1208     // paint, so scroll events are identified as changes to the horizontal or
1209     // vertical start value.
1210     if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
1211     {
1212       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
1213     }
1214     else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
1215     {
1216       fastPaint(((int[]) evt.getNewValue())[0]
1217               - ((int[]) evt.getOldValue())[0]);
1218     }
1219     else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
1220     {
1221       repaint();
1222     }
1223   }
1224
1225   /**
1226    * computes the visible height of the annotation panel
1227    * 
1228    * @param adjustPanelHeight
1229    *          - when false, just adjust existing height according to other
1230    *          windows
1231    * @param annotationHeight
1232    * @return height to use for the ScrollerPreferredVisibleSize
1233    */
1234   public int adjustForAlignFrame(boolean adjustPanelHeight,
1235           int annotationHeight)
1236   {
1237     /*
1238      * Estimate available height in the AlignFrame for alignment +
1239      * annotations. Deduct an estimate for title bar, menu bar, scale panel,
1240      * hscroll, status bar, insets. 
1241      */
1242     int stuff = (ap.getViewName() != null ? 30 : 0)
1243             + (Platform.isAMac() ? 120 : 140);
1244     int availableHeight = ap.alignFrame.getHeight() - stuff;
1245     int rowHeight = av.getCharHeight();
1246
1247     if (adjustPanelHeight)
1248     {
1249       int alignmentHeight = rowHeight * av.getAlignment().getHeight();
1250
1251       /*
1252        * If not enough vertical space, maximize annotation height while keeping
1253        * at least two rows of alignment visible
1254        */
1255       if (annotationHeight + alignmentHeight > availableHeight)
1256       {
1257         annotationHeight = Math.min(annotationHeight,
1258                 availableHeight - 2 * rowHeight);
1259       }
1260     }
1261     else
1262     {
1263       // maintain same window layout whilst updating sliders
1264       annotationHeight = Math.min(ap.annotationScroller.getSize().height,
1265               availableHeight - 2 * rowHeight);
1266     }
1267     return annotationHeight;
1268   }
1269 }