JAL-1421 typed map / lists, explicit imports, method refactoring
[jalview.git] / src / jalview / appletgui / FeatureSettings.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.appletgui;
22
23 import jalview.api.FeatureSettingsControllerI;
24 import jalview.datamodel.AlignmentI;
25 import jalview.datamodel.SequenceFeature;
26 import jalview.schemes.AnnotationColourGradient;
27 import jalview.schemes.GraduatedColor;
28 import jalview.util.MessageManager;
29
30 import java.awt.BorderLayout;
31 import java.awt.Button;
32 import java.awt.Checkbox;
33 import java.awt.Color;
34 import java.awt.Component;
35 import java.awt.Dimension;
36 import java.awt.Font;
37 import java.awt.FontMetrics;
38 import java.awt.Frame;
39 import java.awt.Graphics;
40 import java.awt.GridLayout;
41 import java.awt.Image;
42 import java.awt.Label;
43 import java.awt.MenuItem;
44 import java.awt.Panel;
45 import java.awt.PopupMenu;
46 import java.awt.ScrollPane;
47 import java.awt.Scrollbar;
48 import java.awt.event.ActionEvent;
49 import java.awt.event.ActionListener;
50 import java.awt.event.AdjustmentEvent;
51 import java.awt.event.AdjustmentListener;
52 import java.awt.event.InputEvent;
53 import java.awt.event.ItemEvent;
54 import java.awt.event.ItemListener;
55 import java.awt.event.MouseEvent;
56 import java.awt.event.MouseListener;
57 import java.awt.event.MouseMotionListener;
58 import java.awt.event.WindowAdapter;
59 import java.awt.event.WindowEvent;
60 import java.util.Arrays;
61 import java.util.Enumeration;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Vector;
65
66 public class FeatureSettings extends Panel implements ItemListener,
67         MouseListener, MouseMotionListener, ActionListener,
68         AdjustmentListener, FeatureSettingsControllerI
69 {
70   FeatureRenderer fr;
71
72   AlignmentPanel ap;
73
74   AlignViewport av;
75
76   Frame frame;
77
78   Panel groupPanel;
79
80   Panel featurePanel = new Panel();
81
82   ScrollPane scrollPane;
83
84   Image linkImage;
85
86   Scrollbar transparency;
87
88   public FeatureSettings(final AlignmentPanel ap)
89   {
90     this.ap = ap;
91     this.av = ap.av;
92     ap.av.featureSettings = this;
93     fr = ap.seqPanel.seqCanvas.getFeatureRenderer();
94
95     transparency = new Scrollbar(Scrollbar.HORIZONTAL,
96             100 - (int) (fr.getTransparency() * 100), 1, 1, 100);
97
98     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,
205           final Map<String, float[][]> minmax,
206           int x, int y)
207   {
208     final String type = check.type;
209     final Object typeCol = fr.getFeatureStyle(type);
210     java.awt.PopupMenu men = new PopupMenu(MessageManager.formatMessage(
211             "label.settings_for_type", new String[] { type }));
212     java.awt.MenuItem scr = new MenuItem(
213             MessageManager.getString("label.sort_by_score"));
214     men.add(scr);
215     final FeatureSettings me = this;
216     scr.addActionListener(new ActionListener()
217     {
218
219       @Override
220       public void actionPerformed(ActionEvent e)
221       {
222         me.ap.alignFrame.avc.sortAlignmentByFeatureScore(Arrays
223                 .asList(new String[] { type }));
224       }
225
226     });
227     MenuItem dens = new MenuItem(
228             MessageManager.getString("label.sort_by_density"));
229     dens.addActionListener(new ActionListener()
230     {
231
232       @Override
233       public void actionPerformed(ActionEvent e)
234       {
235         me.ap.alignFrame.avc.sortAlignmentByFeatureDensity(Arrays
236                 .asList(new String[] { type }));
237       }
238
239     });
240     men.add(dens);
241     if (minmax != null)
242     {
243       final float[][] typeMinMax = minmax.get(type);
244       /*
245        * final java.awt.CheckboxMenuItem chb = new
246        * java.awt.CheckboxMenuItem("Vary Height"); // this is broken at the
247        * moment chb.setState(minmax.get(type) != null);
248        * chb.addActionListener(new ActionListener() {
249        * 
250        * public void actionPerformed(ActionEvent e) {
251        * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
252        * null); } else { minmax.put(type, typeMinMax); } }
253        * 
254        * }); men.add(chb);
255        */
256       if (typeMinMax != null && typeMinMax[0] != null)
257       {
258         // graduated colourschemes for those where minmax exists for the
259         // positional features
260         MenuItem mxcol = new MenuItem(
261                 (typeCol instanceof Color) ? "Graduated Colour"
262                         : "Single Colour");
263         men.add(mxcol);
264         mxcol.addActionListener(new ActionListener()
265         {
266
267           @Override
268           public void actionPerformed(ActionEvent e)
269           {
270             if (typeCol instanceof Color)
271             {
272               new FeatureColourChooser(me, type);
273               // write back the current colour object to update the table
274               check.updateColor(fr.getFeatureStyle(type));
275             }
276             else
277             {
278               new UserDefinedColours(me, check.type,
279                       ((GraduatedColor) typeCol));
280             }
281           }
282
283         });
284       }
285     }
286     this.featurePanel.add(men);
287     men.show(this.featurePanel, x, y);
288   }
289
290   @Override
291   public void discoverAllFeatureData()
292   {
293     if (fr.getAllFeatureColours() != null
294             && fr.getAllFeatureColours().size() > 0)
295     {
296       rebuildGroups();
297
298     }
299     resetTable(false);
300   }
301
302   /**
303    * rebuilds the group panel
304    */
305   public void rebuildGroups()
306   {
307     boolean rdrw = false;
308     if (groupPanel == null)
309     {
310       groupPanel = new Panel();
311     }
312     else
313     {
314       rdrw = true;
315       groupPanel.removeAll();
316     }
317     // TODO: JAL-964 - smoothly incorporate new group entries if panel already
318     // displayed and new groups present
319     for (String group : fr.getFeatureGroups())
320     {
321       boolean vis = fr.checkGroupVisibility(group, false);
322       Checkbox check = new MyCheckbox(group, vis, false);
323       check.addMouseListener(this);
324       check.setFont(new Font("Serif", Font.BOLD, 12));
325       check.addItemListener(groupItemListener);
326       // note - visibility seems to correlate with displayed. ???wtf ??
327       check.setVisible(vis);
328       groupPanel.add(check);
329     }
330     if (rdrw)
331     {
332       groupPanel.validate();
333     }
334   }
335
336   // This routine adds and removes checkboxes depending on
337   // Group selection states
338   void resetTable(boolean groupsChanged)
339   {
340     SequenceFeature[] tmpfeatures;
341     String group = null, type;
342     Vector<String> visibleChecks = new Vector<String>();
343     AlignmentI alignment = av.getAlignment();
344     for (int i = 0; i < alignment.getHeight(); i++)
345     {
346       if (alignment.getSequenceAt(i).getSequenceFeatures() == null)
347       {
348         continue;
349       }
350
351       tmpfeatures = alignment.getSequenceAt(i).getSequenceFeatures();
352       int index = 0;
353       while (index < tmpfeatures.length)
354       {
355         group = tmpfeatures[index].featureGroup;
356
357         if (group == null || fr.checkGroupVisibility(group, true))
358         {
359           type = tmpfeatures[index].getType();
360           if (!visibleChecks.contains(type))
361           {
362             visibleChecks.addElement(type);
363           }
364         }
365         index++;
366       }
367     }
368
369     Component[] comps;
370     int cSize = featurePanel.getComponentCount();
371     MyCheckbox check;
372     // This will remove any checkboxes which shouldn't be
373     // visible
374     for (int i = 0; i < cSize; i++)
375     {
376       comps = featurePanel.getComponents();
377       check = (MyCheckbox) comps[i];
378       if (!visibleChecks.contains(check.type))
379       {
380         featurePanel.remove(i);
381         cSize--;
382         i--;
383       }
384     }
385
386     if (fr.getRenderOrder() != null)
387     {
388       // First add the checks in the previous render order,
389       // in case the window has been closed and reopened
390       List<String> rol = fr.getRenderOrder();
391       for (int ro = rol.size() - 1; ro > -1; ro--)
392       {
393         String item = rol.get(ro);
394
395         if (!visibleChecks.contains(item))
396         {
397           continue;
398         }
399
400         visibleChecks.removeElement(item);
401
402         addCheck(false, item);
403       }
404     }
405
406     // now add checkboxes which should be visible,
407     // if they have not already been added
408     Enumeration<String> en = visibleChecks.elements();
409
410     while (en.hasMoreElements())
411     {
412       addCheck(groupsChanged, en.nextElement().toString());
413     }
414
415     featurePanel.setLayout(new GridLayout(featurePanel.getComponentCount(),
416             1, 10, 5));
417     featurePanel.validate();
418
419     if (scrollPane != null)
420     {
421       scrollPane.validate();
422     }
423
424     itemStateChanged(null);
425   }
426
427   /**
428    * update the checklist of feature types with the given type
429    * 
430    * @param groupsChanged
431    *          true means if the type is not in the display list then it will be
432    *          added and displayed
433    * @param type
434    *          feature type to be checked for in the list.
435    */
436   void addCheck(boolean groupsChanged, String type)
437   {
438     boolean addCheck;
439     Component[] comps = featurePanel.getComponents();
440     MyCheckbox check;
441     addCheck = true;
442     for (int i = 0; i < featurePanel.getComponentCount(); i++)
443     {
444       check = (MyCheckbox) comps[i];
445       if (check.type.equals(type))
446       {
447         addCheck = false;
448         break;
449       }
450     }
451
452     if (addCheck)
453     {
454       boolean selected = false;
455       if (groupsChanged || av.getFeaturesDisplayed().isVisible(type))
456       {
457         selected = true;
458       }
459
460       check = new MyCheckbox(type, selected, false,
461               fr.getFeatureStyle(type));
462
463       check.addMouseListener(this);
464       check.addMouseMotionListener(this);
465       check.addItemListener(this);
466       if (groupsChanged)
467       {
468         // add at beginning of stack.
469         featurePanel.add(check, 0);
470       }
471       else
472       {
473         // add at end of stack.
474         featurePanel.add(check);
475       }
476     }
477   }
478
479   @Override
480   public void actionPerformed(ActionEvent evt)
481   {
482     for (int i = 0; i < featurePanel.getComponentCount(); i++)
483     {
484       Checkbox check = (Checkbox) featurePanel.getComponent(i);
485       check.setState(!check.getState());
486     }
487     selectionChanged();
488   }
489
490   private ItemListener groupItemListener = new ItemListener()
491   {
492     @Override
493     public void itemStateChanged(ItemEvent evt)
494     {
495       Checkbox source = (Checkbox) evt.getSource();
496       fr.setGroupVisibility(source.getLabel(), source.getState());
497       ap.seqPanel.seqCanvas.repaint();
498       if (ap.overviewPanel != null)
499       {
500         ap.overviewPanel.updateOverviewImage();
501       }
502       resetTable(true);
503       return;
504     };
505   };
506
507   @Override
508   public void itemStateChanged(ItemEvent evt)
509   {
510     selectionChanged();
511   }
512
513   void selectionChanged()
514   {
515     Component[] comps = featurePanel.getComponents();
516     int cSize = comps.length;
517
518     Object[][] tmp = new Object[cSize][3];
519     int tmpSize = 0;
520     for (int i = 0; i < cSize; i++)
521     {
522       MyCheckbox check = (MyCheckbox) comps[i];
523       tmp[tmpSize][0] = check.type;
524       tmp[tmpSize][1] = fr.getFeatureStyle(check.type);
525       tmp[tmpSize][2] = new Boolean(check.getState());
526       tmpSize++;
527     }
528
529     Object[][] data = new Object[tmpSize][3];
530     System.arraycopy(tmp, 0, data, 0, tmpSize);
531
532     fr.setFeaturePriority(data);
533
534     ap.paintAlignment(true);
535   }
536
537   MyCheckbox selectedCheck;
538
539   boolean dragging = false;
540
541   @Override
542   public void mouseDragged(MouseEvent evt)
543   {
544     if (((Component) evt.getSource()).getParent() != featurePanel)
545     {
546       return;
547     }
548     dragging = true;
549   }
550
551   @Override
552   public void mouseReleased(MouseEvent evt)
553   {
554     if (((Component) evt.getSource()).getParent() != featurePanel)
555     {
556       return;
557     }
558
559     Component comp = null;
560     Checkbox target = null;
561
562     int height = evt.getY() + evt.getComponent().getLocation().y;
563
564     if (height > featurePanel.getSize().height)
565     {
566
567       comp = featurePanel
568               .getComponent(featurePanel.getComponentCount() - 1);
569     }
570     else if (height < 0)
571     {
572       comp = featurePanel.getComponent(0);
573     }
574     else
575     {
576       comp = featurePanel.getComponentAt(evt.getX(), evt.getY()
577               + evt.getComponent().getLocation().y);
578     }
579
580     if (comp != null && comp instanceof Checkbox)
581     {
582       target = (Checkbox) comp;
583     }
584
585     if (selectedCheck != null && target != null && selectedCheck != target)
586     {
587       int targetIndex = -1;
588       for (int i = 0; i < featurePanel.getComponentCount(); i++)
589       {
590         if (target == featurePanel.getComponent(i))
591         {
592           targetIndex = i;
593           break;
594         }
595       }
596
597       featurePanel.remove(selectedCheck);
598       featurePanel.add(selectedCheck, targetIndex);
599       featurePanel.validate();
600       itemStateChanged(null);
601     }
602   }
603
604   public void setUserColour(String feature, Object originalColour)
605   {
606     if (originalColour instanceof Color
607             || originalColour instanceof GraduatedColor)
608     {
609       fr.setColour(feature, originalColour);
610     }
611     else
612     {
613       throw new Error(
614               MessageManager
615                       .getString("error.implementation_error_unsupported_feature_colour_object"));
616     }
617     refreshTable();
618   }
619
620   public void refreshTable()
621   {
622     featurePanel.removeAll();
623     resetTable(false);
624     ap.paintAlignment(true);
625   }
626
627   @Override
628   public void mouseEntered(MouseEvent evt)
629   {
630   }
631
632   @Override
633   public void mouseExited(MouseEvent evt)
634   {
635   }
636
637   @Override
638   public void mouseClicked(MouseEvent evt)
639   {
640     MyCheckbox check = (MyCheckbox) evt.getSource();
641     if ((evt.getModifiers() & InputEvent.BUTTON3_MASK) != 0)
642     {
643       this.popupSort(check, fr.getMinMax(), evt.getX(), evt.getY());
644     }
645
646     if (check.getParent() != featurePanel)
647     {
648       return;
649     }
650
651     if (evt.getClickCount() > 1)
652     {
653       Object fcol = fr.getFeatureStyle(check.type);
654       if (fcol instanceof Color)
655       {
656         new UserDefinedColours(this, check.type, (Color) fcol);
657       }
658       else
659       {
660         new FeatureColourChooser(this, check.type);
661         // write back the current colour object to update the table
662         check.updateColor(fr.getFeatureStyle(check.type));
663       }
664     }
665   }
666
667   @Override
668   public void mouseMoved(MouseEvent evt)
669   {
670   }
671
672   @Override
673   public void adjustmentValueChanged(AdjustmentEvent evt)
674   {
675     fr.setTransparency((100 - transparency.getValue()) / 100f);
676     ap.seqPanel.seqCanvas.repaint();
677
678   }
679
680   class MyCheckbox extends Checkbox
681   {
682     public String type;
683
684     public int stringWidth;
685
686     boolean hasLink;
687
688     GraduatedColor gcol;
689
690     Color col;
691
692     public void updateColor(Object newcol)
693     {
694       if (newcol instanceof Color)
695       {
696         col = (Color) newcol;
697         gcol = null;
698       }
699       else if (newcol instanceof GraduatedColor)
700       {
701         gcol = (GraduatedColor) newcol;
702         col = null;
703       }
704       else
705       {
706         throw new Error(
707                 MessageManager
708                         .getString("error.invalid_colour_for_mycheckbox"));
709       }
710       if (col != null)
711       {
712         setBackground(col);
713       }
714       else
715       {
716         String vlabel = type;
717         if (gcol.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
718         {
719           vlabel += " "
720                   + ((gcol.getThreshType() == AnnotationColourGradient.ABOVE_THRESHOLD) ? "(>)"
721                           : "(<)");
722         }
723         if (gcol.isColourByLabel())
724         {
725           setBackground(Color.white);
726           vlabel += " (by Label)";
727         }
728         else
729         {
730           setBackground(gcol.getMinColor());
731         }
732         this.setLabel(vlabel);
733       }
734       repaint();
735     }
736
737     public MyCheckbox(String label, boolean checked, boolean haslink)
738     {
739       super(label, checked);
740       type = label;
741       FontMetrics fm = av.nullFrame.getFontMetrics(av.nullFrame.getFont());
742       stringWidth = fm.stringWidth(label);
743       this.hasLink = haslink;
744     }
745
746     public MyCheckbox(String type, boolean selected, boolean b,
747             Object featureStyle)
748     {
749       this(type, selected, b);
750       updateColor(featureStyle);
751     }
752
753     @Override
754     public void paint(Graphics g)
755     {
756       Dimension d = getSize();
757       if (gcol != null)
758       {
759         if (gcol.isColourByLabel())
760         {
761           g.setColor(Color.white);
762           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
763           /*
764            * g.setColor(Color.black); Font f=g.getFont().deriveFont(9);
765            * g.setFont(f);
766            * 
767            * // g.setFont(g.getFont().deriveFont( //
768            * AffineTransform.getScaleInstance( //
769            * width/g.getFontMetrics().stringWidth("Label"), //
770            * height/g.getFontMetrics().getHeight()))); g.drawString("Label",
771            * width/2, 0);
772            */
773
774         }
775         else
776         {
777           Color maxCol = gcol.getMaxColor();
778           g.setColor(maxCol);
779           g.fillRect(d.width / 2, 0, d.width / 2, d.height);
780
781         }
782       }
783
784       if (hasLink)
785       {
786         g.drawImage(linkImage, stringWidth + 25,
787                 (getSize().height - linkImage.getHeight(this)) / 2, this);
788       }
789     }
790   }
791
792   @Override
793   public void mousePressed(MouseEvent e)
794   {
795   }
796
797 }