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