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