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