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