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