JAL-1989 more unit tests and encapsulation for ColumnSelection
[jalview.git] / src / jalview / appletgui / 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.appletgui;
22
23 import jalview.datamodel.AlignmentAnnotation;
24 import jalview.datamodel.Annotation;
25 import jalview.renderer.AnnotationRenderer;
26 import jalview.renderer.AwtRenderPanelI;
27 import jalview.util.MessageManager;
28
29 import java.awt.Color;
30 import java.awt.Dimension;
31 import java.awt.Font;
32 import java.awt.FontMetrics;
33 import java.awt.Graphics;
34 import java.awt.Image;
35 import java.awt.MenuItem;
36 import java.awt.Panel;
37 import java.awt.PopupMenu;
38 import java.awt.event.ActionEvent;
39 import java.awt.event.ActionListener;
40 import java.awt.event.AdjustmentEvent;
41 import java.awt.event.AdjustmentListener;
42 import java.awt.event.InputEvent;
43 import java.awt.event.MouseEvent;
44 import java.awt.event.MouseListener;
45 import java.awt.event.MouseMotionListener;
46
47 public class AnnotationPanel extends Panel implements AwtRenderPanelI,
48         AdjustmentListener, ActionListener, MouseListener,
49         MouseMotionListener
50 {
51   AlignViewport av;
52
53   AlignmentPanel ap;
54
55   int activeRow = -1;
56
57   final String HELIX = "Helix";
58
59   final String SHEET = "Sheet";
60
61   /**
62    * For RNA secondary structure "stems" aka helices
63    */
64   final String STEM = "RNA Helix";
65
66   final String LABEL = "Label";
67
68   final String REMOVE = "Remove Annotation";
69
70   final String COLOUR = "Colour";
71
72   final Color HELIX_COLOUR = Color.red.darker();
73
74   final Color SHEET_COLOUR = Color.green.darker().darker();
75
76   Image image;
77
78   Graphics gg;
79
80   FontMetrics fm;
81
82   int imgWidth = 0;
83
84   boolean fastPaint = false;
85
86   // Used For mouse Dragging and resizing graphs
87   int graphStretch = -1;
88
89   int graphStretchY = -1;
90
91   boolean mouseDragging = false;
92
93   public static int GRAPH_HEIGHT = 40;
94
95   boolean MAC = false;
96
97   public final AnnotationRenderer renderer;
98
99   public AnnotationPanel(AlignmentPanel ap)
100   {
101     MAC = new jalview.util.Platform().isAMac();
102     this.ap = ap;
103     av = ap.av;
104     setLayout(null);
105     int height = adjustPanelHeight();
106     ap.apvscroll.setValues(0, getSize().height, 0, height);
107
108     addMouseMotionListener(this);
109
110     addMouseListener(this);
111
112     // ap.annotationScroller.getVAdjustable().addAdjustmentListener( this );
113     renderer = new AnnotationRenderer();
114   }
115
116   public AnnotationPanel(AlignViewport av)
117   {
118     this.av = av;
119     renderer = new AnnotationRenderer();
120   }
121
122   @Override
123   public void adjustmentValueChanged(AdjustmentEvent evt)
124   {
125   }
126
127   /**
128    * DOCUMENT ME!
129    * 
130    * @param evt
131    *          DOCUMENT ME!
132    */
133   @Override
134   public void actionPerformed(ActionEvent evt)
135   {
136     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
137     if (aa == null)
138     {
139       return;
140     }
141     Annotation[] anot = aa[activeRow].annotations;
142
143     if (anot.length < av.getColumnSelection().getMax())
144     {
145       Annotation[] temp = new Annotation[av.getColumnSelection().getMax() + 2];
146       System.arraycopy(anot, 0, temp, 0, anot.length);
147       anot = temp;
148       aa[activeRow].annotations = anot;
149     }
150
151     String label = "";
152     if (av.getColumnSelection() != null
153             && !av.getColumnSelection().isEmpty()
154             && anot[av.getColumnSelection().getMin()] != null)
155     {
156       label = anot[av.getColumnSelection().getMin()].displayCharacter;
157     }
158
159     if (evt.getActionCommand().equals(REMOVE))
160     {
161       for (int sel : av.getColumnSelection().getSelected())
162       {
163         anot[sel] = null;
164       }
165     }
166     else if (evt.getActionCommand().equals(LABEL))
167     {
168       label = enterLabel(label, "Enter Label");
169
170       if (label == null)
171       {
172         return;
173       }
174
175       if ((label.length() > 0) && !aa[activeRow].hasText)
176       {
177         aa[activeRow].hasText = true;
178       }
179
180       for (int index : av.getColumnSelection().getSelected())
181       {
182         if (!av.getColumnSelection().isVisible(index))
183         {
184           continue;
185         }
186
187         if (anot[index] == null)
188         {
189           anot[index] = new Annotation(label, "", ' ', 0);
190         }
191
192         anot[index].displayCharacter = label;
193       }
194     }
195     else if (evt.getActionCommand().equals(COLOUR))
196     {
197       UserDefinedColours udc = new UserDefinedColours(this, Color.black,
198               ap.alignFrame);
199
200       Color col = udc.getColor();
201
202       for (int index : av.getColumnSelection().getSelected())
203       {
204         if (!av.getColumnSelection().isVisible(index))
205         {
206           continue;
207         }
208
209         if (anot[index] == null)
210         {
211           anot[index] = new Annotation("", "", ' ', 0);
212         }
213
214         anot[index].colour = col;
215       }
216     }
217     else
218     // HELIX OR SHEET
219     {
220       char type = 0;
221       String symbol = "\u03B1";
222
223       if (evt.getActionCommand().equals(HELIX))
224       {
225         type = 'H';
226       }
227       else if (evt.getActionCommand().equals(SHEET))
228       {
229         type = 'E';
230         symbol = "\u03B2";
231       }
232
233       // Added by LML to color stems
234       else if (evt.getActionCommand().equals(STEM))
235       {
236         type = 'S';
237         symbol = "\u03C3";
238       }
239
240       if (!aa[activeRow].hasIcons)
241       {
242         aa[activeRow].hasIcons = true;
243       }
244
245       label = enterLabel(symbol, "Enter Label");
246
247       if (label == null)
248       {
249         return;
250       }
251
252       if ((label.length() > 0) && !aa[activeRow].hasText)
253       {
254         aa[activeRow].hasText = true;
255         if (evt.getActionCommand().equals(STEM))
256         {
257           aa[activeRow].showAllColLabels = true;
258         }
259       }
260
261       for (int index : av.getColumnSelection().getSelected())
262       {
263         if (!av.getColumnSelection().isVisible(index))
264         {
265           continue;
266         }
267
268         if (anot[index] == null)
269         {
270           anot[index] = new Annotation(label, "", type, 0);
271         }
272
273         anot[index].secondaryStructure = type != 'S' ? type : label
274                 .length() == 0 ? ' ' : label.charAt(0);
275         anot[index].displayCharacter = label;
276       }
277     }
278
279     av.getAlignment().validateAnnotation(aa[activeRow]);
280
281     ap.alignmentChanged();
282     adjustPanelHeight();
283     repaint();
284
285     return;
286   }
287
288   String enterLabel(String text, String label)
289   {
290     EditNameDialog dialog = new EditNameDialog(text, null, label, null,
291             ap.alignFrame, "Enter Label", 400, 200, true);
292
293     if (dialog.accept)
294     {
295       return dialog.getName();
296     }
297     else
298     {
299       return null;
300     }
301   }
302
303   @Override
304   public void mousePressed(MouseEvent evt)
305   {
306     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
307     if (aa == null)
308     {
309       return;
310     }
311
312     int height = -scrollOffset;
313     activeRow = -1;
314
315     for (int i = 0; i < aa.length; i++)
316     {
317       if (aa[i].visible)
318       {
319         height += aa[i].height;
320       }
321
322       if (evt.getY() < height)
323       {
324         if (aa[i].editable)
325         {
326           activeRow = i;
327         }
328         else if (aa[i].graph > 0)
329         {
330           // Stretch Graph
331           graphStretch = i;
332           graphStretchY = evt.getY();
333         }
334
335         break;
336       }
337     }
338
339     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) == InputEvent.BUTTON3_MASK
340             && activeRow != -1)
341     {
342       if (av.getColumnSelection() == null)
343       {
344         return;
345       }
346
347       PopupMenu pop = new PopupMenu(
348               MessageManager.getString("label.structure_type"));
349       MenuItem item;
350       /*
351        * Just display the needed structure options
352        */
353       if (av.getAlignment().isNucleotide() == true)
354       {
355         item = new MenuItem(STEM);
356         item.addActionListener(this);
357         pop.add(item);
358       }
359       else
360       {
361         item = new MenuItem(HELIX);
362         item.addActionListener(this);
363         pop.add(item);
364         item = new MenuItem(SHEET);
365         item.addActionListener(this);
366         pop.add(item);
367       }
368       item = new MenuItem(LABEL);
369       item.addActionListener(this);
370       pop.add(item);
371       item = new MenuItem(COLOUR);
372       item.addActionListener(this);
373       pop.add(item);
374       item = new MenuItem(REMOVE);
375       item.addActionListener(this);
376       pop.add(item);
377       ap.alignFrame.add(pop);
378       pop.show(this, evt.getX(), evt.getY());
379
380       return;
381     }
382
383     ap.scalePanel.mousePressed(evt);
384   }
385
386   @Override
387   public void mouseReleased(MouseEvent evt)
388   {
389     graphStretch = -1;
390     graphStretchY = -1;
391     mouseDragging = false;
392     if (needValidating)
393     {
394       ap.validate();
395       needValidating = false;
396     }
397     ap.scalePanel.mouseReleased(evt);
398   }
399
400   @Override
401   public void mouseClicked(MouseEvent evt)
402   {
403   }
404
405   boolean needValidating = false;
406
407   @Override
408   public void mouseDragged(MouseEvent evt)
409   {
410     if (graphStretch > -1)
411     {
412       av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight += graphStretchY
413               - evt.getY();
414       if (av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight < 0)
415       {
416         av.getAlignment().getAlignmentAnnotation()[graphStretch].graphHeight = 0;
417       }
418       graphStretchY = evt.getY();
419       av.calcPanelHeight();
420       needValidating = true;
421       ap.paintAlignment(true);
422     }
423     else
424     {
425       ap.scalePanel.mouseDragged(evt);
426     }
427   }
428
429   @Override
430   public void mouseMoved(MouseEvent evt)
431   {
432     AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
433     if (aa == null)
434     {
435       return;
436     }
437
438     int row = -1;
439     int height = -scrollOffset;
440     for (int i = 0; i < aa.length; i++)
441     {
442
443       if (aa[i].visible)
444       {
445         height += aa[i].height;
446       }
447
448       if (evt.getY() < height)
449       {
450         row = i;
451         break;
452       }
453     }
454
455     int res = evt.getX() / av.getCharWidth() + av.getStartRes();
456
457     if (av.hasHiddenColumns())
458     {
459       res = av.getColumnSelection().adjustForHiddenColumns(res);
460     }
461
462     if (row > -1 && res < aa[row].annotations.length
463             && aa[row].annotations[res] != null)
464     {
465       StringBuffer text = new StringBuffer("Sequence position " + (res + 1));
466       if (aa[row].annotations[res].description != null)
467       {
468         text.append("  " + aa[row].annotations[res].description);
469       }
470       ap.alignFrame.statusBar.setText(text.toString());
471     }
472   }
473
474   @Override
475   public void mouseEntered(MouseEvent evt)
476   {
477     ap.scalePanel.mouseEntered(evt);
478   }
479
480   @Override
481   public void mouseExited(MouseEvent evt)
482   {
483     ap.scalePanel.mouseExited(evt);
484   }
485
486   public int adjustPanelHeight()
487   {
488     return adjustPanelHeight(true);
489   }
490
491   public int adjustPanelHeight(boolean repaint)
492   {
493     int height = av.calcPanelHeight();
494     this.setSize(new Dimension(getSize().width, height));
495     if (repaint)
496     {
497       repaint();
498     }
499     return height;
500   }
501
502   /**
503    * calculate the height for visible annotation, revalidating bounds where
504    * necessary ABSTRACT GUI METHOD
505    * 
506    * @return total height of annotation
507    */
508
509   public void addEditableColumn(int i)
510   {
511     if (activeRow == -1)
512     {
513       AlignmentAnnotation[] aa = av.getAlignment().getAlignmentAnnotation();
514       if (aa == null)
515       {
516         return;
517       }
518
519       for (int j = 0; j < aa.length; j++)
520       {
521         if (aa[j].editable)
522         {
523           activeRow = j;
524           break;
525         }
526       }
527     }
528   }
529
530   @Override
531   public void update(Graphics g)
532   {
533     paint(g);
534   }
535
536   @Override
537   public void paint(Graphics g)
538   {
539     Dimension d = getSize();
540     imgWidth = d.width;
541     // (av.endRes - av.startRes + 1) * av.charWidth;
542     if (imgWidth < 1 || d.height < 1)
543     {
544       return;
545     }
546     if (image == null || imgWidth != image.getWidth(this)
547             || d.height != image.getHeight(this))
548     {
549       image = createImage(imgWidth, d.height);
550       gg = image.getGraphics();
551       gg.setFont(av.getFont());
552       fm = gg.getFontMetrics();
553       fastPaint = false;
554     }
555
556     if (fastPaint)
557     {
558       g.drawImage(image, 0, 0, this);
559       fastPaint = false;
560       return;
561     }
562
563     gg.setColor(Color.white);
564     gg.fillRect(0, 0, getSize().width, getSize().height);
565     drawComponent(gg, av.startRes, av.endRes + 1);
566
567     g.drawImage(image, 0, 0, this);
568   }
569
570   public void fastPaint(int horizontal)
571   {
572     if (horizontal == 0
573             || av.getAlignment().getAlignmentAnnotation() == null
574             || av.getAlignment().getAlignmentAnnotation().length < 1)
575     {
576       repaint();
577       return;
578     }
579
580     gg.copyArea(0, 0, imgWidth, getSize().height,
581             -horizontal * av.getCharWidth(), 0);
582     int sr = av.startRes, er = av.endRes + 1, transX = 0;
583
584     if (horizontal > 0) // scrollbar pulled right, image to the left
585     {
586       transX = (er - sr - horizontal) * av.getCharWidth();
587       sr = er - horizontal;
588     }
589     else if (horizontal < 0)
590     {
591       er = sr - horizontal;
592     }
593
594     gg.translate(transX, 0);
595
596     drawComponent(gg, sr, er);
597
598     gg.translate(-transX, 0);
599
600     fastPaint = true;
601     repaint();
602   }
603
604   /**
605    * DOCUMENT ME!
606    * 
607    * @param g
608    *          DOCUMENT ME!
609    * @param startRes
610    *          DOCUMENT ME!
611    * @param endRes
612    *          DOCUMENT ME!
613    */
614   public void drawComponent(Graphics g, int startRes, int endRes)
615   {
616     Font ofont = av.getFont();
617     g.setFont(ofont);
618
619     g.setColor(Color.white);
620     g.fillRect(0, 0, (endRes - startRes) * av.getCharWidth(),
621             getSize().height);
622
623     if (fm == null)
624     {
625       fm = g.getFontMetrics();
626     }
627
628     if ((av.getAlignment().getAlignmentAnnotation() == null)
629             || (av.getAlignment().getAlignmentAnnotation().length < 1))
630     {
631       g.setColor(Color.white);
632       g.fillRect(0, 0, getSize().width, getSize().height);
633       g.setColor(Color.black);
634       if (av.validCharWidth)
635       {
636         g.drawString(MessageManager
637                 .getString("label.alignment_has_no_annotations"), 20, 15);
638       }
639
640       return;
641     }
642     g.translate(0, -scrollOffset);
643     renderer.drawComponent(this, av, g, activeRow, startRes, endRes);
644     g.translate(0, +scrollOffset);
645   }
646
647   int scrollOffset = 0;
648
649   public void setScrollOffset(int value, boolean repaint)
650   {
651     scrollOffset = value;
652     if (repaint)
653     {
654       repaint();
655     }
656   }
657
658   @Override
659   public FontMetrics getFontMetrics()
660   {
661     return fm;
662   }
663
664   @Override
665   public Image getFadedImage()
666   {
667     return image;
668   }
669
670   @Override
671   public int getFadedImageWidth()
672   {
673     return imgWidth;
674   }
675
676   private int[] bounds = new int[2];
677
678   @Override
679   public int[] getVisibleVRange()
680   {
681     if (ap != null && ap.alabels != null)
682     {
683       int sOffset = -ap.alabels.scrollOffset;
684       int visHeight = sOffset + ap.annotationPanelHolder.getHeight();
685       bounds[0] = sOffset;
686       bounds[1] = visHeight;
687       return bounds;
688     }
689     else
690     {
691       return null;
692     }
693   }
694 }