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