584a69a52c46ece5053ac47db499157610eab73b
[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.Hashtable;
63 import java.util.List;
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     if (fr.isTransparencyAvailable())
99     {
100       transparency.addAdjustmentListener(this);
101     }
102     else
103     {
104       transparency.setEnabled(false);
105     }
106
107     java.net.URL url = getClass().getResource("/images/link.gif");
108     if (url != null)
109     {
110       linkImage = java.awt.Toolkit.getDefaultToolkit().getImage(url);
111     }
112
113     if (av.isShowSequenceFeatures() || !fr.hasRenderOrder())
114     {
115       fr.findAllFeatures(true); // was default - now true to make all visible
116     }
117
118     discoverAllFeatureData();
119
120     this.setLayout(new BorderLayout());
121     scrollPane = new ScrollPane();
122     scrollPane.add(featurePanel);
123     if (fr.getAllFeatureColours() != null
124             && fr.getAllFeatureColours().size() > 0)
125     {
126       add(scrollPane, BorderLayout.CENTER);
127     }
128
129     Button invert = new Button("Invert Selection");
130     invert.addActionListener(this);
131
132     Panel lowerPanel = new Panel(new GridLayout(2, 1, 5, 10));
133     lowerPanel.add(invert);
134
135     Panel tPanel = new Panel(new BorderLayout());
136
137     if (fr.isTransparencyAvailable())
138     {
139       tPanel.add(transparency, BorderLayout.CENTER);
140       tPanel.add(new Label("Transparency"), BorderLayout.EAST);
141     }
142     else
143     {
144       tPanel.add(
145               new Label("Transparency not available in this web browser"),
146               BorderLayout.CENTER);
147     }
148
149     lowerPanel.add(tPanel, BorderLayout.SOUTH);
150
151     add(lowerPanel, BorderLayout.SOUTH);
152
153     if (groupPanel != null)
154     {
155       groupPanel.setLayout(new GridLayout(
156               (fr.getFeatureGroupsSize()) / 4 + 1, 4)); // JBPNote - this was
157                                                         // scaled on number of
158                                                         // visible groups. seems
159                                                         // broken
160       groupPanel.validate();
161
162       add(groupPanel, BorderLayout.NORTH);
163     }
164     frame = new Frame();
165     frame.add(this);
166     final FeatureSettings me = this;
167     frame.addWindowListener(new WindowAdapter()
168     {
169       @Override
170       public void windowClosing(WindowEvent e)
171       {
172         if (me.av.featureSettings == me)
173         {
174           me.av.featureSettings = null;
175           me.ap = null;
176           me.av = null;
177         }
178       }
179     });
180     int height = featurePanel.getComponentCount() * 50 + 60;
181
182     height = Math.max(200, height);
183     height = Math.min(400, height);
184     int width = 300;
185     jalview.bin.JalviewLite.addFrame(frame,
186             MessageManager.getString("label.sequence_feature_settings"),
187             width, height);
188   }
189
190   @Override
191   public void paint(Graphics g)
192   {
193     g.setColor(Color.black);
194     g.drawString(MessageManager
195             .getString("label.no_features_added_to_this_alignment"), 10, 20);
196     g.drawString(MessageManager
197             .getString("label.features_can_be_added_from_searches_1"), 10,
198             40);
199     g.drawString(MessageManager
200             .getString("label.features_can_be_added_from_searches_2"), 10,
201             60);
202   }
203
204   protected void popupSort(final MyCheckbox check, final Hashtable minmax,
205           int x, int y)
206   {
207     final String type = check.type;
208     final Object typeCol = fr.getFeatureStyle(type);
209     java.awt.PopupMenu men = new PopupMenu(MessageManager.formatMessage(
210             "label.settings_for_type", new String[] { type }));
211     java.awt.MenuItem scr = new MenuItem(
212             MessageManager.getString("label.sort_by_score"));
213     men.add(scr);
214     final FeatureSettings me = this;
215     scr.addActionListener(new ActionListener()
216     {
217
218       @Override
219       public void actionPerformed(ActionEvent e)
220       {
221         me.ap.alignFrame.avc.sortAlignmentByFeatureScore(Arrays
222                 .asList(new String[] { type }));
223       }
224
225     });
226     MenuItem dens = new MenuItem(
227             MessageManager.getString("label.sort_by_density"));
228     dens.addActionListener(new ActionListener()
229     {
230
231       @Override
232       public void actionPerformed(ActionEvent e)
233       {
234         me.ap.alignFrame.avc.sortAlignmentByFeatureDensity(Arrays
235                 .asList(new String[] { type }));
236       }
237
238     });
239     men.add(dens);
240     if (minmax != null)
241     {
242       final Object typeMinMax = minmax.get(type);
243       /*
244        * final java.awt.CheckboxMenuItem chb = new
245        * java.awt.CheckboxMenuItem("Vary Height"); // this is broken at the
246        * moment chb.setState(minmax.get(type) != null);
247        * chb.addActionListener(new ActionListener() {
248        * 
249        * public void actionPerformed(ActionEvent e) {
250        * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
251        * null); } else { minmax.put(type, typeMinMax); } }
252        * 
253        * }); men.add(chb);
254        */
255       if (typeMinMax != null && ((float[][]) typeMinMax)[0] != null)
256       {
257         // graduated colourschemes for those where minmax exists for the
258         // positional features
259         MenuItem mxcol = new MenuItem(
260                 (typeCol instanceof Color) ? "Graduated Colour"
261                         : "Single Colour");
262         men.add(mxcol);
263         mxcol.addActionListener(new ActionListener()
264         {
265
266           @Override
267           public void actionPerformed(ActionEvent e)
268           {
269             if (typeCol instanceof Color)
270             {
271               new FeatureColourChooser(me, type);
272               // write back the current colour object to update the table
273               check.updateColor(fr.getFeatureStyle(type));
274             }
275             else
276             {
277               new UserDefinedColours(me, check.type,
278                       ((GraduatedColor) typeCol));
279             }
280           }
281
282         });
283       }
284     }
285     this.featurePanel.add(men);
286     men.show(this.featurePanel, x, y);
287   }
288
289   @Override
290   public void discoverAllFeatureData()
291   {
292     if (fr.getAllFeatureColours() != null
293             && fr.getAllFeatureColours().size() > 0)
294     {
295       rebuildGroups();
296
297     }
298     resetTable(false);
299   }
300
301   /**
302    * rebuilds the group panel
303    */
304   public void rebuildGroups()
305   {
306     boolean rdrw = false;
307     if (groupPanel == null)
308     {
309       groupPanel = new Panel();
310     }
311     else
312     {
313       rdrw = true;
314       groupPanel.removeAll();
315     }
316     // TODO: JAL-964 - smoothly incorporate new group entries if panel already
317     // displayed and new groups present
318     for (String group : fr.getFeatureGroups())
319     {
320       boolean vis = fr.checkGroupVisibility(group, false);
321       Checkbox check = new MyCheckbox(group, vis, false);
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 visibleChecks = new Vector();
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 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(type, selected, false,
460               fr.getFeatureStyle(type));
461
462       check.addMouseListener(this);
463       check.addMouseMotionListener(this);
464       check.addItemListener(this);
465       if (groupsChanged)
466       {
467         // add at beginning of stack.
468         featurePanel.add(check, 0);
469       }
470       else
471       {
472         // add at end of stack.
473         featurePanel.add(check);
474       }
475     }
476   }
477
478   @Override
479   public void actionPerformed(ActionEvent evt)
480   {
481     for (int i = 0; i < featurePanel.getComponentCount(); i++)
482     {
483       Checkbox check = (Checkbox) featurePanel.getComponent(i);
484       check.setState(!check.getState());
485     }
486     selectionChanged();
487   }
488
489   private ItemListener groupItemListener = new ItemListener()
490   {
491     @Override
492     public void itemStateChanged(ItemEvent evt)
493     {
494       Checkbox source = (Checkbox) evt.getSource();
495       fr.setGroupVisibility(source.getLabel(), source.getState());
496       ap.seqPanel.seqCanvas.repaint();
497       if (ap.overviewPanel != null)
498       {
499         ap.overviewPanel.updateOverviewImage();
500       }
501       resetTable(true);
502       return;
503     };
504   };
505
506   @Override
507   public void itemStateChanged(ItemEvent evt)
508   {
509     selectionChanged();
510   }
511
512   void selectionChanged()
513   {
514     Component[] comps = featurePanel.getComponents();
515     int cSize = comps.length;
516
517     Object[][] tmp = new Object[cSize][3];
518     int tmpSize = 0;
519     for (int i = 0; i < cSize; i++)
520     {
521       MyCheckbox check = (MyCheckbox) comps[i];
522       tmp[tmpSize][0] = check.type;
523       tmp[tmpSize][1] = fr.getFeatureStyle(check.type);
524       tmp[tmpSize][2] = new Boolean(check.getState());
525       tmpSize++;
526     }
527
528     Object[][] data = new Object[tmpSize][3];
529     System.arraycopy(tmp, 0, data, 0, tmpSize);
530
531     fr.setFeaturePriority(data);
532
533     ap.paintAlignment(true);
534   }
535
536   MyCheckbox selectedCheck;
537
538   boolean dragging = false;
539
540   @Override
541   public void mouseDragged(MouseEvent evt)
542   {
543     if (((Component) evt.getSource()).getParent() != featurePanel)
544     {
545       return;
546     }
547     dragging = true;
548   }
549
550   @Override
551   public void mouseReleased(MouseEvent evt)
552   {
553     if (((Component) evt.getSource()).getParent() != featurePanel)
554     {
555       return;
556     }
557
558     Component comp = null;
559     Checkbox target = null;
560
561     int height = evt.getY() + evt.getComponent().getLocation().y;
562
563     if (height > featurePanel.getSize().height)
564     {
565
566       comp = featurePanel
567               .getComponent(featurePanel.getComponentCount() - 1);
568     }
569     else if (height < 0)
570     {
571       comp = featurePanel.getComponent(0);
572     }
573     else
574     {
575       comp = featurePanel.getComponentAt(evt.getX(), evt.getY()
576               + evt.getComponent().getLocation().y);
577     }
578
579     if (comp != null && comp instanceof Checkbox)
580     {
581       target = (Checkbox) comp;
582     }
583
584     if (selectedCheck != null && target != null && selectedCheck != target)
585     {
586       int targetIndex = -1;
587       for (int i = 0; i < featurePanel.getComponentCount(); i++)
588       {
589         if (target == featurePanel.getComponent(i))
590         {
591           targetIndex = i;
592           break;
593         }
594       }
595
596       featurePanel.remove(selectedCheck);
597       featurePanel.add(selectedCheck, targetIndex);
598       featurePanel.validate();
599       itemStateChanged(null);
600     }
601   }
602
603   public void setUserColour(String feature, Object originalColour)
604   {
605     if (originalColour instanceof Color
606             || originalColour instanceof GraduatedColor)
607     {
608       fr.setColour(feature, originalColour);
609     }
610     else
611     {
612       throw new Error(
613               MessageManager
614                       .getString("error.implementation_error_unsupported_feature_colour_object"));
615     }
616     refreshTable();
617   }
618
619   public void refreshTable()
620   {
621     featurePanel.removeAll();
622     resetTable(false);
623     ap.paintAlignment(true);
624   }
625
626   @Override
627   public void mouseEntered(MouseEvent evt)
628   {
629   }
630
631   @Override
632   public void mouseExited(MouseEvent evt)
633   {
634   }
635
636   @Override
637   public void mouseClicked(MouseEvent evt)
638   {
639     MyCheckbox check = (MyCheckbox) evt.getSource();
640     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) != 0)
641     {
642       this.popupSort(check, fr.getMinMax(), evt.getX(), evt.getY());
643     }
644
645     if (check.getParent() != featurePanel)
646     {
647       return;
648     }
649
650     if (evt.getClickCount() > 1)
651     {
652       Object fcol = fr.getFeatureStyle(check.type);
653       if (fcol instanceof Color)
654       {
655         new UserDefinedColours(this, check.type, (Color) fcol);
656       }
657       else
658       {
659         new FeatureColourChooser(this, check.type);
660         // write back the current colour object to update the table
661         check.updateColor(fr.getFeatureStyle(check.type));
662       }
663     }
664   }
665
666   @Override
667   public void mouseMoved(MouseEvent evt)
668   {
669   }
670
671   @Override
672   public void adjustmentValueChanged(AdjustmentEvent evt)
673   {
674     fr.setTransparency((100 - transparency.getValue()) / 100f);
675     ap.seqPanel.seqCanvas.repaint();
676
677   }
678
679   class MyCheckbox extends Checkbox
680   {
681     public String type;
682
683     public int stringWidth;
684
685     boolean hasLink;
686
687     GraduatedColor gcol;
688
689     Color col;
690
691     public void updateColor(Object newcol)
692     {
693       if (newcol instanceof Color)
694       {
695         col = (Color) newcol;
696         gcol = null;
697       }
698       else if (newcol instanceof GraduatedColor)
699       {
700         gcol = (GraduatedColor) newcol;
701         col = null;
702       }
703       else
704       {
705         throw new Error(
706                 MessageManager
707                         .getString("error.invalid_colour_for_mycheckbox"));
708       }
709       if (col != null)
710       {
711         setBackground(col);
712       }
713       else
714       {
715         String vlabel = type;
716         if (gcol.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
717         {
718           vlabel += " "
719                   + ((gcol.getThreshType() == AnnotationColourGradient.ABOVE_THRESHOLD) ? "(>)"
720                           : "(<)");
721         }
722         if (gcol.isColourByLabel())
723         {
724           setBackground(Color.white);
725           vlabel += " (by Label)";
726         }
727         else
728         {
729           setBackground(gcol.getMinColor());
730         }
731         this.setLabel(vlabel);
732       }
733       repaint();
734     }
735
736     public MyCheckbox(String label, boolean checked, boolean haslink)
737     {
738       super(label, checked);
739       type = label;
740       FontMetrics fm = av.nullFrame.getFontMetrics(av.nullFrame.getFont());
741       stringWidth = fm.stringWidth(label);
742       this.hasLink = haslink;
743     }
744
745     public MyCheckbox(String type, boolean selected, boolean b,
746             Object featureStyle)
747     {
748       this(type, selected, b);
749       updateColor(featureStyle);
750     }
751
752     @Override
753     public void paint(Graphics g)
754     {
755       Dimension d = getSize();
756       if (gcol != null)
757       {
758         if (gcol.isColourByLabel())
759         {
760           g.setColor(Color.white);
761           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
762           /*
763            * g.setColor(Color.black); Font f=g.getFont().deriveFont(9);
764            * g.setFont(f);
765            * 
766            * // g.setFont(g.getFont().deriveFont( //
767            * AffineTransform.getScaleInstance( //
768            * width/g.getFontMetrics().stringWidth("Label"), //
769            * height/g.getFontMetrics().getHeight()))); g.drawString("Label",
770            * width/2, 0);
771            */
772
773         }
774         else
775         {
776           Color maxCol = gcol.getMaxColor();
777           g.setColor(maxCol);
778           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
779
780         }
781       }
782
783       if (hasLink)
784       {
785         g.drawImage(linkImage, stringWidth + 25,
786                 (getSize().height - linkImage.getHeight(this)) / 2, this);
787       }
788     }
789   }
790
791   @Override
792   public void mousePressed(MouseEvent e)
793   {
794   }
795
796 }