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