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