Merge branch 'develop' into features/JAL-2446NCList
[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         if (group == null || checkGroupState(group))
391         {
392           visibleGroups.add(group);
393         }
394       }
395       foundGroups.addAll(groups);
396
397       /*
398        * get distinct feature types for visible groups
399        * record distinct visible types
400        */
401       Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
402               visibleGroups.toArray(new String[visibleGroups.size()]));
403       displayableTypes.addAll(types);
404     }
405
406     /*
407      * remove any checkboxes for groups not present
408      */
409     pruneGroups(foundGroups);
410
411     Component[] comps;
412     int cSize = featurePanel.getComponentCount();
413     MyCheckbox check;
414     // This will remove any checkboxes which shouldn't be
415     // visible
416     for (int i = 0; i < cSize; i++)
417     {
418       comps = featurePanel.getComponents();
419       check = (MyCheckbox) comps[i];
420       if (!displayableTypes.contains(check.type))
421       {
422         featurePanel.remove(i);
423         cSize--;
424         i--;
425       }
426     }
427
428     if (fr.getRenderOrder() != null)
429     {
430       // First add the checks in the previous render order,
431       // in case the window has been closed and reopened
432       List<String> rol = fr.getRenderOrder();
433       for (int ro = rol.size() - 1; ro > -1; ro--)
434       {
435         String item = rol.get(ro);
436
437         if (!displayableTypes.contains(item))
438         {
439           continue;
440         }
441
442         displayableTypes.remove(item);
443
444         addCheck(false, item);
445       }
446     }
447
448     /*
449      * now add checkboxes which should be visible,
450      * if they have not already been added
451      */
452     for (String type : displayableTypes)
453     {
454       addCheck(groupsChanged, type);
455     }
456
457     featurePanel.setLayout(new GridLayout(featurePanel.getComponentCount(),
458             1, 10, 5));
459     featurePanel.validate();
460
461     if (scrollPane != null)
462     {
463       scrollPane.validate();
464     }
465
466     itemStateChanged(null);
467   }
468
469   /**
470    * Remove from the groups panel any checkboxes for groups that are not in the
471    * foundGroups set. This enables removing a group from the display when the
472    * last feature in that group is deleted.
473    * 
474    * @param foundGroups
475    */
476   protected void pruneGroups(Set<String> foundGroups)
477   {
478     for (int g = 0; g < groupPanel.getComponentCount(); g++)
479     {
480       Checkbox checkbox = (Checkbox) groupPanel.getComponent(g);
481       if (!foundGroups.contains(checkbox.getLabel()))
482       {
483         groupPanel.remove(checkbox);
484       }
485     }
486   }
487
488   /**
489    * update the checklist of feature types with the given type
490    * 
491    * @param groupsChanged
492    *          true means if the type is not in the display list then it will be
493    *          added and displayed
494    * @param type
495    *          feature type to be checked for in the list.
496    */
497   void addCheck(boolean groupsChanged, String type)
498   {
499     boolean addCheck;
500     Component[] comps = featurePanel.getComponents();
501     MyCheckbox check;
502     addCheck = true;
503     for (int i = 0; i < featurePanel.getComponentCount(); i++)
504     {
505       check = (MyCheckbox) comps[i];
506       if (check.type.equals(type))
507       {
508         addCheck = false;
509         break;
510       }
511     }
512
513     if (addCheck)
514     {
515       boolean selected = false;
516       if (groupsChanged || av.getFeaturesDisplayed().isVisible(type))
517       {
518         selected = true;
519       }
520
521       check = new MyCheckbox(type, selected, false,
522               fr.getFeatureStyle(type));
523
524       check.addMouseListener(this);
525       check.addMouseMotionListener(this);
526       check.addItemListener(this);
527       if (groupsChanged)
528       {
529         // add at beginning of stack.
530         featurePanel.add(check, 0);
531       }
532       else
533       {
534         // add at end of stack.
535         featurePanel.add(check);
536       }
537     }
538   }
539
540   @Override
541   public void actionPerformed(ActionEvent evt)
542   {
543     for (int i = 0; i < featurePanel.getComponentCount(); i++)
544     {
545       Checkbox check = (Checkbox) featurePanel.getComponent(i);
546       check.setState(!check.getState());
547     }
548     selectionChanged(true);
549   }
550
551   private ItemListener groupItemListener = new ItemListener()
552   {
553     @Override
554     public void itemStateChanged(ItemEvent evt)
555     {
556       Checkbox source = (Checkbox) evt.getSource();
557       fr.setGroupVisibility(source.getLabel(), source.getState());
558       ap.seqPanel.seqCanvas.repaint();
559       if (ap.overviewPanel != null)
560       {
561         ap.overviewPanel.updateOverviewImage();
562       }
563       resetTable(true);
564       return;
565     };
566   };
567
568   @Override
569   public void itemStateChanged(ItemEvent evt)
570   {
571     selectionChanged(true);
572   }
573
574   void selectionChanged(boolean updateOverview)
575   {
576     Component[] comps = featurePanel.getComponents();
577     int cSize = comps.length;
578
579     Object[][] tmp = new Object[cSize][3];
580     int tmpSize = 0;
581     for (int i = 0; i < cSize; i++)
582     {
583       MyCheckbox check = (MyCheckbox) comps[i];
584       tmp[tmpSize][0] = check.type;
585       tmp[tmpSize][1] = fr.getFeatureStyle(check.type);
586       tmp[tmpSize][2] = new Boolean(check.getState());
587       tmpSize++;
588     }
589
590     Object[][] data = new Object[tmpSize][3];
591     System.arraycopy(tmp, 0, data, 0, tmpSize);
592
593     fr.setFeaturePriority(data);
594
595     ap.paintAlignment(updateOverview);
596   }
597
598   MyCheckbox selectedCheck;
599
600   boolean dragging = false;
601
602   @Override
603   public void mouseDragged(MouseEvent evt)
604   {
605     if (((Component) evt.getSource()).getParent() != featurePanel)
606     {
607       return;
608     }
609     dragging = true;
610   }
611
612   @Override
613   public void mouseReleased(MouseEvent evt)
614   {
615     if (((Component) evt.getSource()).getParent() != featurePanel)
616     {
617       return;
618     }
619
620     Component comp = null;
621     Checkbox target = null;
622
623     int height = evt.getY() + evt.getComponent().getLocation().y;
624
625     if (height > featurePanel.getSize().height)
626     {
627
628       comp = featurePanel
629               .getComponent(featurePanel.getComponentCount() - 1);
630     }
631     else if (height < 0)
632     {
633       comp = featurePanel.getComponent(0);
634     }
635     else
636     {
637       comp = featurePanel.getComponentAt(evt.getX(), evt.getY()
638               + evt.getComponent().getLocation().y);
639     }
640
641     if (comp != null && comp instanceof Checkbox)
642     {
643       target = (Checkbox) comp;
644     }
645
646     if (selectedCheck != null && target != null && selectedCheck != target)
647     {
648       int targetIndex = -1;
649       for (int i = 0; i < featurePanel.getComponentCount(); i++)
650       {
651         if (target == featurePanel.getComponent(i))
652         {
653           targetIndex = i;
654           break;
655         }
656       }
657
658       featurePanel.remove(selectedCheck);
659       featurePanel.add(selectedCheck, targetIndex);
660       featurePanel.validate();
661       itemStateChanged(null);
662     }
663   }
664
665   public void setUserColour(String feature, FeatureColourI originalColour)
666   {
667     fr.setColour(feature, originalColour);
668     refreshTable();
669   }
670
671   public void refreshTable()
672   {
673     featurePanel.removeAll();
674     resetTable(false);
675     ap.paintAlignment(true);
676   }
677
678   @Override
679   public void mouseEntered(MouseEvent evt)
680   {
681   }
682
683   @Override
684   public void mouseExited(MouseEvent evt)
685   {
686   }
687
688   @Override
689   public void mouseClicked(MouseEvent evt)
690   {
691     MyCheckbox check = (MyCheckbox) evt.getSource();
692     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) != 0)
693     {
694       this.popupSort(check, fr.getMinMax(), evt.getX(), evt.getY());
695     }
696
697     if (check.getParent() != featurePanel)
698     {
699       return;
700     }
701
702     if (evt.getClickCount() > 1)
703     {
704       FeatureColourI fcol = fr.getFeatureStyle(check.type);
705       if (fcol.isSimpleColour())
706       {
707         new UserDefinedColours(this, check.type, fcol.getColour());
708       }
709       else
710       {
711         new FeatureColourChooser(this, check.type);
712         // write back the current colour object to update the table
713         check.updateColor(fr.getFeatureStyle(check.type));
714       }
715     }
716   }
717
718   @Override
719   public void mouseMoved(MouseEvent evt)
720   {
721   }
722
723   @Override
724   public void adjustmentValueChanged(AdjustmentEvent evt)
725   {
726     fr.setTransparency((100 - transparency.getValue()) / 100f);
727     ap.paintAlignment(true);
728   }
729
730   class MyCheckbox extends Checkbox
731   {
732     public String type;
733
734     public int stringWidth;
735
736     boolean hasLink;
737
738     FeatureColourI col;
739
740     public void updateColor(FeatureColourI newcol)
741     {
742       col = newcol;
743       if (col.isSimpleColour())
744       {
745         setBackground(col.getColour());
746       }
747       else
748       {
749         String vlabel = type;
750         if (col.isAboveThreshold())
751         {
752           vlabel += " (>)";
753         }
754         else if (col.isBelowThreshold())
755         {
756           vlabel += " (<)";
757         }
758         if (col.isColourByLabel())
759         {
760           setBackground(Color.white);
761           vlabel += " (by Label)";
762         }
763         else
764         {
765           setBackground(col.getMinColour());
766         }
767         this.setLabel(vlabel);
768       }
769       repaint();
770     }
771
772     public MyCheckbox(String label, boolean checked, boolean haslink)
773     {
774       super(label, checked);
775       type = label;
776       FontMetrics fm = av.nullFrame.getFontMetrics(av.nullFrame.getFont());
777       stringWidth = fm.stringWidth(label);
778       this.hasLink = haslink;
779     }
780
781     public MyCheckbox(String type, boolean selected, boolean b,
782             FeatureColourI featureStyle)
783     {
784       this(type, selected, b);
785       updateColor(featureStyle);
786     }
787
788     @Override
789     public void paint(Graphics g)
790     {
791       Dimension d = getSize();
792       if (col != null)
793       {
794         if (col.isColourByLabel())
795         {
796           g.setColor(Color.white);
797           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
798           /*
799            * g.setColor(Color.black); Font f=g.getFont().deriveFont(9);
800            * g.setFont(f);
801            * 
802            * // g.setFont(g.getFont().deriveFont( //
803            * AffineTransform.getScaleInstance( //
804            * width/g.getFontMetrics().stringWidth("Label"), //
805            * height/g.getFontMetrics().getHeight()))); g.drawString("Label",
806            * width/2, 0);
807            */
808
809         }
810         else if (col.isGraduatedColour())
811         {
812           Color maxCol = col.getMaxColour();
813           g.setColor(maxCol);
814           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
815
816         }
817       }
818
819       if (hasLink)
820       {
821         g.drawImage(linkImage, stringWidth + 25,
822                 (getSize().height - linkImage.getHeight(this)) / 2, this);
823       }
824     }
825   }
826
827   /**
828    * Hide columns containing (or not containing) a given feature type
829    * 
830    * @param type
831    * @param columnsContaining
832    */
833   void hideFeatureColumns(final String type, boolean columnsContaining)
834   {
835     if (ap.alignFrame.avc.markColumnsContainingFeatures(columnsContaining,
836             false, false, type))
837     {
838       if (ap.alignFrame.avc.markColumnsContainingFeatures(
839               !columnsContaining, false, false, type))
840       {
841         ap.alignFrame.viewport.hideSelectedColumns();
842       }
843     }
844   }
845
846   @Override
847   public void mousePressed(MouseEvent e)
848   {
849     // TODO Auto-generated method stub
850
851   }
852
853 }