1c156dc43f934ad415c6de77981372787894885d
[jalview.git] / src / jalview / appletgui / FeatureSettings.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.api.FeatureSettingsControllerI;
24 import jalview.datamodel.AlignmentI;
25 import jalview.datamodel.SequenceFeature;
26 import jalview.schemes.AnnotationColourGradient;
27 import jalview.schemes.GraduatedColor;
28 import jalview.util.MessageManager;
29
30 import java.awt.BorderLayout;
31 import java.awt.Button;
32 import java.awt.Checkbox;
33 import java.awt.Color;
34 import java.awt.Component;
35 import java.awt.Dimension;
36 import java.awt.Font;
37 import java.awt.FontMetrics;
38 import java.awt.Frame;
39 import java.awt.Graphics;
40 import java.awt.GridLayout;
41 import java.awt.Image;
42 import java.awt.Label;
43 import java.awt.MenuItem;
44 import java.awt.Panel;
45 import java.awt.PopupMenu;
46 import java.awt.ScrollPane;
47 import java.awt.Scrollbar;
48 import java.awt.event.ActionEvent;
49 import java.awt.event.ActionListener;
50 import java.awt.event.AdjustmentEvent;
51 import java.awt.event.AdjustmentListener;
52 import java.awt.event.InputEvent;
53 import java.awt.event.ItemEvent;
54 import java.awt.event.ItemListener;
55 import java.awt.event.MouseEvent;
56 import java.awt.event.MouseListener;
57 import java.awt.event.MouseMotionListener;
58 import java.awt.event.WindowAdapter;
59 import java.awt.event.WindowEvent;
60 import java.util.Enumeration;
61 import java.util.Hashtable;
62 import java.util.List;
63 import java.util.Vector;
64
65 public class FeatureSettings extends Panel implements ItemListener,
66         MouseListener, MouseMotionListener, ActionListener,
67         AdjustmentListener, FeatureSettingsControllerI
68 {
69   FeatureRenderer fr;
70
71   AlignmentPanel ap;
72
73   AlignViewport av;
74
75   Frame frame;
76
77   Panel groupPanel;
78
79   Panel featurePanel = new Panel();
80
81   ScrollPane scrollPane;
82
83   Image linkImage;
84
85   Scrollbar transparency;
86
87   public FeatureSettings(final AlignmentPanel ap)
88   {
89     this.ap = ap;
90     this.av = ap.av;
91     ap.av.featureSettings = this;
92     fr = ap.seqPanel.seqCanvas.getFeatureRenderer();
93
94     transparency = new Scrollbar(Scrollbar.HORIZONTAL,
95             100 - (int) (fr.getTransparency() * 100), 1, 1, 100);
96
97     if (fr.isTransparencyAvailable())
98     {
99       transparency.addAdjustmentListener(this);
100     }
101     else
102     {
103       transparency.setEnabled(false);
104     }
105
106     java.net.URL url = getClass().getResource("/images/link.gif");
107     if (url != null)
108     {
109       linkImage = java.awt.Toolkit.getDefaultToolkit().getImage(url);
110     }
111
112     if (av.isShowSequenceFeatures() || !fr.hasRenderOrder())
113     {
114       fr.findAllFeatures(true); // was default - now true to make all visible
115     }
116
117     discoverAllFeatureData();
118
119     this.setLayout(new BorderLayout());
120     scrollPane = new ScrollPane();
121     scrollPane.add(featurePanel);
122     if (fr.getAllFeatureColours() != null
123             && fr.getAllFeatureColours().size() > 0)
124     {
125       add(scrollPane, BorderLayout.CENTER);
126     }
127
128     Button invert = new Button("Invert Selection");
129     invert.addActionListener(this);
130
131     Panel lowerPanel = new Panel(new GridLayout(2, 1, 5, 10));
132     lowerPanel.add(invert);
133
134     Panel tPanel = new Panel(new BorderLayout());
135
136     if (fr.isTransparencyAvailable())
137     {
138       tPanel.add(transparency, BorderLayout.CENTER);
139       tPanel.add(new Label("Transparency"), BorderLayout.EAST);
140     }
141     else
142     {
143       tPanel.add(
144               new Label("Transparency not available in this web browser"),
145               BorderLayout.CENTER);
146     }
147
148     lowerPanel.add(tPanel, BorderLayout.SOUTH);
149
150     add(lowerPanel, BorderLayout.SOUTH);
151
152     if (groupPanel != null)
153     {
154       groupPanel.setLayout(new GridLayout(
155               (fr.getFeatureGroupsSize()) / 4 + 1, 4)); // JBPNote - this was
156                                                         // scaled on number of
157                                                         // visible groups. seems
158                                                         // broken
159       groupPanel.validate();
160
161       add(groupPanel, BorderLayout.NORTH);
162     }
163     frame = new Frame();
164     frame.add(this);
165     final FeatureSettings me = this;
166     frame.addWindowListener(new WindowAdapter()
167     {
168       public void windowClosing(WindowEvent e)
169       {
170         if (me.av.featureSettings == me)
171         {
172           me.av.featureSettings = null;
173           me.ap = null;
174           me.av = null;
175         }
176       }
177     });
178     int height = featurePanel.getComponentCount() * 50 + 60;
179
180     height = Math.max(200, height);
181     height = Math.min(400, height);
182     int width = 300;
183     jalview.bin.JalviewLite.addFrame(frame,
184             MessageManager.getString("label.sequence_feature_settings"),
185             width, height);
186   }
187
188   public void paint(Graphics g)
189   {
190     g.setColor(Color.black);
191     g.drawString(MessageManager
192             .getString("label.no_features_added_to_this_alignment"), 10, 20);
193     g.drawString(MessageManager
194             .getString("label.features_can_be_added_from_searches_1"), 10,
195             40);
196     g.drawString(MessageManager
197             .getString("label.features_can_be_added_from_searches_2"), 10,
198             60);
199   }
200
201   protected void popupSort(final MyCheckbox check, final Hashtable minmax,
202           int x, int y)
203   {
204     final String type = check.type;
205     final Object typeCol = fr.getFeatureStyle(type);
206     java.awt.PopupMenu men = new PopupMenu(MessageManager.formatMessage(
207             "label.settings_for_type", new String[] { type }));
208     java.awt.MenuItem scr = new MenuItem(
209             MessageManager.getString("label.sort_by_score"));
210     men.add(scr);
211     final FeatureSettings me = this;
212     scr.addActionListener(new ActionListener()
213     {
214
215       public void actionPerformed(ActionEvent e)
216       {
217         me.ap.alignFrame.avc
218                 .sortAlignmentByFeatureScore(new String[] { type });
219       }
220
221     });
222     MenuItem dens = new MenuItem(
223             MessageManager.getString("label.sort_by_density"));
224     dens.addActionListener(new ActionListener()
225     {
226
227       public void actionPerformed(ActionEvent e)
228       {
229         me.ap.alignFrame.avc
230                 .sortAlignmentByFeatureDensity(new String[] { type });
231       }
232
233     });
234     men.add(dens);
235     if (minmax != null)
236     {
237       final Object typeMinMax = minmax.get(type);
238       /*
239        * final java.awt.CheckboxMenuItem chb = new
240        * java.awt.CheckboxMenuItem("Vary Height"); // this is broken at the
241        * moment chb.setState(minmax.get(type) != null);
242        * chb.addActionListener(new ActionListener() {
243        * 
244        * public void actionPerformed(ActionEvent e) {
245        * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
246        * null); } else { minmax.put(type, typeMinMax); } }
247        * 
248        * }); men.add(chb);
249        */
250       if (typeMinMax != null && ((float[][]) typeMinMax)[0] != null)
251       {
252         // graduated colourschemes for those where minmax exists for the
253         // positional features
254         MenuItem mxcol = new MenuItem(
255                 (typeCol instanceof Color) ? "Graduated Colour"
256                         : "Single Colour");
257         men.add(mxcol);
258         mxcol.addActionListener(new ActionListener()
259         {
260
261           public void actionPerformed(ActionEvent e)
262           {
263             if (typeCol instanceof Color)
264             {
265               new FeatureColourChooser(me, type);
266               // write back the current colour object to update the table
267               check.updateColor(fr.getFeatureStyle(type));
268             }
269             else
270             {
271               new UserDefinedColours(me, check.type,
272                       ((GraduatedColor) typeCol));
273             }
274           }
275
276         });
277       }
278     }
279     this.featurePanel.add(men);
280     men.show(this.featurePanel, x, y);
281   }
282
283   @Override
284   public void discoverAllFeatureData()
285   {
286     if (fr.getAllFeatureColours() != null
287             && fr.getAllFeatureColours().size() > 0)
288     {
289       rebuildGroups();
290
291     }
292     resetTable(false);
293   }
294
295   /**
296    * rebuilds the group panel
297    */
298   public void rebuildGroups()
299   {
300     boolean rdrw = false;
301     if (groupPanel == null)
302     {
303       groupPanel = new Panel();
304     }
305     else
306     {
307       rdrw = true;
308       groupPanel.removeAll();
309     }
310     // TODO: JAL-964 - smoothly incorporate new group entries if panel already
311     // displayed and new groups present
312     for (String group : fr.getFeatureGroups())
313     {
314       boolean vis = fr.checkGroupVisibility(group, false);
315       Checkbox check = new MyCheckbox(group, vis,
316               (fr.featureLinks != null && fr.featureLinks
317                       .containsKey(group)));
318       check.addMouseListener(this);
319       check.setFont(new Font("Serif", Font.BOLD, 12));
320       check.addItemListener(groupItemListener);
321       // note - visibility seems to correlate with displayed. ???wtf ??
322       check.setVisible(vis);
323       groupPanel.add(check);
324     }
325     if (rdrw)
326     {
327       groupPanel.validate();
328     }
329   }
330
331   // This routine adds and removes checkboxes depending on
332   // Group selection states
333   void resetTable(boolean groupsChanged)
334   {
335     SequenceFeature[] tmpfeatures;
336     String group = null, type;
337     Vector visibleChecks = new Vector();
338     AlignmentI alignment = av.getAlignment();
339     for (int i = 0; i < alignment.getHeight(); i++)
340     {
341       if (alignment.getSequenceAt(i).getSequenceFeatures() == null)
342       {
343         continue;
344       }
345
346       tmpfeatures = alignment.getSequenceAt(i).getSequenceFeatures();
347       int index = 0;
348       while (index < tmpfeatures.length)
349       {
350         group = tmpfeatures[index].featureGroup;
351
352         if (group == null || fr.checkGroupVisibility(group, true))
353         {
354           type = tmpfeatures[index].getType();
355           if (!visibleChecks.contains(type))
356           {
357             visibleChecks.addElement(type);
358           }
359         }
360         index++;
361       }
362     }
363
364     Component[] comps;
365     int cSize = featurePanel.getComponentCount();
366     MyCheckbox check;
367     // This will remove any checkboxes which shouldn't be
368     // visible
369     for (int i = 0; i < cSize; i++)
370     {
371       comps = featurePanel.getComponents();
372       check = (MyCheckbox) comps[i];
373       if (!visibleChecks.contains(check.type))
374       {
375         featurePanel.remove(i);
376         cSize--;
377         i--;
378       }
379     }
380
381     if (fr.getRenderOrder() != null)
382     {
383       // First add the checks in the previous render order,
384       // in case the window has been closed and reopened
385       List<String> rol = fr.getRenderOrder();
386       for (int ro = rol.size() - 1; ro > -1; ro--)
387       {
388         String item = rol.get(ro);
389
390         if (!visibleChecks.contains(item))
391         {
392           continue;
393         }
394
395         visibleChecks.removeElement(item);
396
397         addCheck(false, item);
398       }
399     }
400
401     // now add checkboxes which should be visible,
402     // if they have not already been added
403     Enumeration en = visibleChecks.elements();
404
405     while (en.hasMoreElements())
406     {
407       addCheck(groupsChanged, en.nextElement().toString());
408     }
409
410     featurePanel.setLayout(new GridLayout(featurePanel.getComponentCount(),
411             1, 10, 5));
412     featurePanel.validate();
413
414     if (scrollPane != null)
415     {
416       scrollPane.validate();
417     }
418
419     itemStateChanged(null);
420   }
421
422   /**
423    * update the checklist of feature types with the given type
424    * 
425    * @param groupsChanged
426    *          true means if the type is not in the display list then it will be
427    *          added and displayed
428    * @param type
429    *          feature type to be checked for in the list.
430    */
431   void addCheck(boolean groupsChanged, String type)
432   {
433     boolean addCheck;
434     Component[] comps = featurePanel.getComponents();
435     MyCheckbox check;
436     addCheck = true;
437     for (int i = 0; i < featurePanel.getComponentCount(); i++)
438     {
439       check = (MyCheckbox) comps[i];
440       if (check.type.equals(type))
441       {
442         addCheck = false;
443         break;
444       }
445     }
446
447     if (addCheck)
448     {
449       boolean selected = false;
450       if (groupsChanged || av.getFeaturesDisplayed().isVisible(type))
451       {
452         selected = true;
453       }
454
455       check = new MyCheckbox(
456               type,
457               selected,
458               (fr.featureLinks != null && fr.featureLinks.containsKey(type)),
459               fr.getFeatureStyle(type));
460
461       check.addMouseListener(this);
462       check.addMouseMotionListener(this);
463       check.addItemListener(this);
464       if (groupsChanged)
465       {
466         // add at beginning of stack.
467         featurePanel.add(check, 0);
468       }
469       else
470       {
471         // add at end of stack.
472         featurePanel.add(check);
473       }
474     }
475   }
476
477   public void actionPerformed(ActionEvent evt)
478   {
479     for (int i = 0; i < featurePanel.getComponentCount(); i++)
480     {
481       Checkbox check = (Checkbox) featurePanel.getComponent(i);
482       check.setState(!check.getState());
483     }
484     selectionChanged();
485   }
486
487   private ItemListener groupItemListener = new ItemListener()
488   {
489     public void itemStateChanged(ItemEvent evt)
490     {
491       Checkbox source = (Checkbox) evt.getSource();
492       fr.setGroupVisibility(source.getLabel(), source.getState());
493       ap.seqPanel.seqCanvas.repaint();
494       if (ap.overviewPanel != null)
495       {
496         ap.overviewPanel.updateOverviewImage();
497       }
498       resetTable(true);
499       return;
500     };
501   };
502
503   public void itemStateChanged(ItemEvent evt)
504   {
505     selectionChanged();
506   }
507
508   void selectionChanged()
509   {
510     Component[] comps = featurePanel.getComponents();
511     int cSize = comps.length;
512
513     Object[][] tmp = new Object[cSize][3];
514     int tmpSize = 0;
515     for (int i = 0; i < cSize; i++)
516     {
517       MyCheckbox check = (MyCheckbox) comps[i];
518       tmp[tmpSize][0] = check.type;
519       tmp[tmpSize][1] = fr.getFeatureStyle(check.type);
520       tmp[tmpSize][2] = new Boolean(check.getState());
521       tmpSize++;
522     }
523
524     Object[][] data = new Object[tmpSize][3];
525     System.arraycopy(tmp, 0, data, 0, tmpSize);
526
527     fr.setFeaturePriority(data);
528
529     ap.paintAlignment(true);
530   }
531
532   MyCheckbox selectedCheck;
533
534   boolean dragging = false;
535
536   public void mousePressed(MouseEvent evt)
537   {
538
539     selectedCheck = (MyCheckbox) evt.getSource();
540
541     if (fr.featureLinks != null
542             && fr.featureLinks.containsKey(selectedCheck.type))
543     {
544       if (evt.getX() > selectedCheck.stringWidth + 20)
545       {
546         evt.consume();
547       }
548     }
549
550   }
551
552   public void mouseDragged(MouseEvent evt)
553   {
554     if (((Component) evt.getSource()).getParent() != featurePanel)
555     {
556       return;
557     }
558     dragging = true;
559   }
560
561   public void mouseReleased(MouseEvent evt)
562   {
563     if (((Component) evt.getSource()).getParent() != featurePanel)
564     {
565       return;
566     }
567
568     Component comp = null;
569     Checkbox target = null;
570
571     int height = evt.getY() + evt.getComponent().getLocation().y;
572
573     if (height > featurePanel.getSize().height)
574     {
575
576       comp = featurePanel
577               .getComponent(featurePanel.getComponentCount() - 1);
578     }
579     else if (height < 0)
580     {
581       comp = featurePanel.getComponent(0);
582     }
583     else
584     {
585       comp = featurePanel.getComponentAt(evt.getX(), evt.getY()
586               + evt.getComponent().getLocation().y);
587     }
588
589     if (comp != null && comp instanceof Checkbox)
590     {
591       target = (Checkbox) comp;
592     }
593
594     if (selectedCheck != null && target != null && selectedCheck != target)
595     {
596       int targetIndex = -1;
597       for (int i = 0; i < featurePanel.getComponentCount(); i++)
598       {
599         if (target == featurePanel.getComponent(i))
600         {
601           targetIndex = i;
602           break;
603         }
604       }
605
606       featurePanel.remove(selectedCheck);
607       featurePanel.add(selectedCheck, targetIndex);
608       featurePanel.validate();
609       itemStateChanged(null);
610     }
611   }
612
613   public void setUserColour(String feature, Object originalColour)
614   {
615     if (originalColour instanceof Color
616             || originalColour instanceof GraduatedColor)
617     {
618       fr.setColour(feature, originalColour);
619     }
620     else
621     {
622       throw new Error(
623               MessageManager
624                       .getString("error.implementation_error_unsupported_feature_colour_object"));
625     }
626     refreshTable();
627   }
628
629   public void refreshTable()
630   {
631     featurePanel.removeAll();
632     resetTable(false);
633     ap.paintAlignment(true);
634   }
635
636   public void mouseEntered(MouseEvent evt)
637   {
638   }
639
640   public void mouseExited(MouseEvent evt)
641   {
642   }
643
644   public void mouseClicked(MouseEvent evt)
645   {
646     MyCheckbox check = (MyCheckbox) evt.getSource();
647     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) != 0)
648     {
649       this.popupSort(check, fr.getMinMax(), evt.getX(), evt.getY());
650     }
651     if (fr.featureLinks != null && fr.featureLinks.containsKey(check.type))
652     {
653       if (evt.getX() > check.stringWidth + 20)
654       {
655         evt.consume();
656         String link = fr.featureLinks.get(check.type).toString();
657         ap.alignFrame.showURL(link.substring(link.indexOf("|") + 1),
658                 link.substring(0, link.indexOf("|")));
659       }
660     }
661
662     if (check.getParent() != featurePanel)
663     {
664       return;
665     }
666
667     if (evt.getClickCount() > 1)
668     {
669       Object fcol = fr.getFeatureStyle(check.type);
670       if (fcol instanceof Color)
671       {
672         new UserDefinedColours(this, check.type, (Color) fcol);
673       }
674       else
675       {
676         new FeatureColourChooser(this, check.type);
677         // write back the current colour object to update the table
678         check.updateColor(fr.getFeatureStyle(check.type));
679       }
680     }
681   }
682
683   public void mouseMoved(MouseEvent evt)
684   {
685   }
686
687   public void adjustmentValueChanged(AdjustmentEvent evt)
688   {
689     fr.setTransparency((100 - transparency.getValue()) / 100f);
690     ap.seqPanel.seqCanvas.repaint();
691
692   }
693
694   class MyCheckbox extends Checkbox
695   {
696     public String type;
697
698     public int stringWidth;
699
700     boolean hasLink;
701
702     GraduatedColor gcol;
703
704     Color col;
705
706     public void updateColor(Object newcol)
707     {
708       if (newcol instanceof Color)
709       {
710         col = (Color) newcol;
711         gcol = null;
712       }
713       else if (newcol instanceof GraduatedColor)
714       {
715         gcol = (GraduatedColor) newcol;
716         col = null;
717       }
718       else
719       {
720         throw new Error(
721                 MessageManager
722                         .getString("error.invalid_colour_for_mycheckbox"));
723       }
724       if (col != null)
725       {
726         setBackground(col);
727       }
728       else
729       {
730         String vlabel = type;
731         if (gcol.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
732         {
733           vlabel += " "
734                   + ((gcol.getThreshType() == AnnotationColourGradient.ABOVE_THRESHOLD) ? "(>)"
735                           : "(<)");
736         }
737         if (gcol.isColourByLabel())
738         {
739           setBackground(Color.white);
740           vlabel += " (by Label)";
741         }
742         else
743         {
744           setBackground(gcol.getMinColor());
745         }
746         this.setLabel(vlabel);
747       }
748       repaint();
749     }
750
751     public MyCheckbox(String label, boolean checked, boolean haslink)
752     {
753       super(label, checked);
754       type = label;
755       FontMetrics fm = av.nullFrame.getFontMetrics(av.nullFrame.getFont());
756       stringWidth = fm.stringWidth(label);
757       this.hasLink = haslink;
758     }
759
760     public MyCheckbox(String type, boolean selected, boolean b,
761             Object featureStyle)
762     {
763       this(type, selected, b);
764       updateColor(featureStyle);
765     }
766
767     public void paint(Graphics g)
768     {
769       Dimension d = getSize();
770       if (gcol != null)
771       {
772         if (gcol.isColourByLabel())
773         {
774           g.setColor(Color.white);
775           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
776           /*
777            * g.setColor(Color.black); Font f=g.getFont().deriveFont(9);
778            * g.setFont(f);
779            * 
780            * // g.setFont(g.getFont().deriveFont( //
781            * AffineTransform.getScaleInstance( //
782            * width/g.getFontMetrics().stringWidth("Label"), //
783            * height/g.getFontMetrics().getHeight()))); g.drawString("Label",
784            * width/2, 0);
785            */
786
787         }
788         else
789         {
790           Color maxCol = gcol.getMaxColor();
791           g.setColor(maxCol);
792           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
793
794         }
795       }
796
797       if (hasLink)
798       {
799         g.drawImage(linkImage, stringWidth + 25,
800                 (getSize().height - linkImage.getHeight(this)) / 2, this);
801       }
802     }
803   }
804
805 }