JAL-2808 spike updated with latest
[jalview.git] / src / jalview / gui / FeatureColourChooser.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.gui;
22
23 import jalview.api.FeatureColourI;
24 import jalview.datamodel.GraphLine;
25 import jalview.schemes.FeatureColour;
26 import jalview.util.MessageManager;
27
28 import java.awt.BorderLayout;
29 import java.awt.Color;
30 import java.awt.Dimension;
31 import java.awt.FlowLayout;
32 import java.awt.GridLayout;
33 import java.awt.event.ActionEvent;
34 import java.awt.event.ActionListener;
35 import java.awt.event.FocusAdapter;
36 import java.awt.event.FocusEvent;
37 import java.awt.event.MouseAdapter;
38 import java.awt.event.MouseEvent;
39
40 import javax.swing.BorderFactory;
41 import javax.swing.JCheckBox;
42 import javax.swing.JColorChooser;
43 import javax.swing.JComboBox;
44 import javax.swing.JLabel;
45 import javax.swing.JPanel;
46 import javax.swing.JSlider;
47 import javax.swing.JTextField;
48 import javax.swing.border.LineBorder;
49 import javax.swing.event.ChangeEvent;
50 import javax.swing.event.ChangeListener;
51
52 public class FeatureColourChooser extends JalviewDialog
53 {
54   // FeatureSettings fs;
55   private FeatureRenderer fr;
56
57   private FeatureColourI cs;
58
59   private FeatureColourI oldcs;
60
61   private AlignmentPanel ap;
62
63   private boolean adjusting = false;
64
65   final private float min;
66
67   final private float max;
68
69   final private float scaleFactor;
70
71   private String type = null;
72
73   private JPanel minColour = new JPanel();
74
75   private JPanel maxColour = new JPanel();
76
77   private JComboBox<String> threshold = new JComboBox<>();
78
79   private JSlider slider = new JSlider();
80
81   private JTextField thresholdValue = new JTextField(20);
82
83   // TODO implement GUI for tolower flag
84   // JCheckBox toLower = new JCheckBox();
85
86   private JCheckBox thresholdIsMin = new JCheckBox();
87
88   private JCheckBox colourByLabel = new JCheckBox();
89
90   private GraphLine threshline;
91
92   private Color oldmaxColour;
93
94   private Color oldminColour;
95
96   private ActionListener colourEditor = null;
97
98   /**
99    * Constructor
100    * 
101    * @param frender
102    * @param theType
103    */
104   public FeatureColourChooser(FeatureRenderer frender, String theType)
105   {
106     this(frender, false, theType);
107   }
108
109   /**
110    * Constructor, with option to make a blocking dialog (has to complete in the
111    * AWT event queue thread). Currently this option is always set to false.
112    * 
113    * @param frender
114    * @param blocking
115    * @param theType
116    */
117   FeatureColourChooser(FeatureRenderer frender, boolean blocking,
118           String theType)
119   {
120     this.fr = frender;
121     this.type = theType;
122     ap = fr.ap;
123     String title = MessageManager
124             .formatMessage("label.graduated_color_for_params", new String[]
125             { theType });
126     initDialogFrame(this, true, blocking, title, 480, 185);
127
128     slider.addChangeListener(new ChangeListener()
129     {
130       @Override
131       public void stateChanged(ChangeEvent evt)
132       {
133         if (!adjusting)
134         {
135           thresholdValue.setText((slider.getValue() / scaleFactor) + "");
136           sliderValueChanged();
137         }
138       }
139     });
140     slider.addMouseListener(new MouseAdapter()
141     {
142       @Override
143       public void mouseReleased(MouseEvent evt)
144       {
145         /*
146          * only update Overview and/or structure colouring
147          * when threshold slider drag ends (mouse up)
148          */
149         if (ap != null)
150         {
151           ap.paintAlignment(true, true);
152         }
153       }
154     });
155
156     float mm[] = fr.getMinMax().get(theType)[0];
157     min = mm[0];
158     max = mm[1];
159
160     /*
161      * ensure scale factor allows a scaled range with
162      * 10 integer divisions ('ticks'); if we have got here,
163      * we should expect that max != min
164      */
165     scaleFactor = (max == min) ? 1f : 100f / (max - min);
166
167     oldcs = fr.getFeatureColours().get(theType);
168     if (!oldcs.isSimpleColour())
169     {
170       if (oldcs.isAutoScaled())
171       {
172         // update the scale
173         cs = new FeatureColour((FeatureColour) oldcs, min, max);
174       }
175       else
176       {
177         cs = new FeatureColour((FeatureColour) oldcs);
178       }
179     }
180     else
181     {
182       // promote original color to a graduated color
183       Color bl = oldcs.getColour();
184       if (bl == null)
185       {
186         bl = Color.BLACK;
187       }
188       // original colour becomes the maximum colour
189       cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
190       cs.setColourByLabel(false);
191     }
192     minColour.setBackground(oldminColour = cs.getMinColour());
193     maxColour.setBackground(oldmaxColour = cs.getMaxColour());
194     adjusting = true;
195
196     try
197     {
198       jbInit();
199     } catch (Exception ex)
200     {
201     }
202     // update the gui from threshold state
203     thresholdIsMin.setSelected(!cs.isAutoScaled());
204     colourByLabel.setSelected(cs.isColourByLabel());
205     if (cs.hasThreshold())
206     {
207       // initialise threshold slider and selector
208       threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2);
209       slider.setEnabled(true);
210       slider.setValue((int) (cs.getThreshold() * scaleFactor));
211       thresholdValue.setEnabled(true);
212       threshline = new GraphLine((max - min) / 2f, "Threshold",
213               Color.black);
214       threshline.value = cs.getThreshold();
215     }
216
217     adjusting = false;
218
219     changeColour(false);
220     waitForInput();
221   }
222
223   private void jbInit() throws Exception
224   {
225     this.setLayout(new GridLayout(4, 1));
226
227     JPanel colourByPanel = initColoursPanel();
228
229     JPanel thresholdPanel = initThresholdPanel();
230
231     JPanel okCancelPanel = initOkCancelPanel();
232
233     this.add(colourByPanel);
234     this.add(thresholdPanel);
235
236     this.add(okCancelPanel);
237   }
238
239   /**
240    * Lay out fields for threshold options
241    * 
242    * @return
243    */
244   protected JPanel initThresholdPanel()
245   {
246     JPanel thresholdPanel = new JPanel();
247     thresholdPanel.setLayout(new FlowLayout());
248     threshold.addActionListener(new ActionListener()
249     {
250       @Override
251       public void actionPerformed(ActionEvent e)
252       {
253         changeColour(true);
254       }
255     });
256     threshold.setToolTipText(MessageManager
257             .getString("label.threshold_feature_display_by_score"));
258     threshold.addItem(MessageManager
259             .getString("label.threshold_feature_no_threshold")); // index 0
260     threshold.addItem(MessageManager
261             .getString("label.threshold_feature_above_threshold")); // index 1
262     threshold.addItem(MessageManager
263             .getString("label.threshold_feature_below_threshold")); // index 2
264
265     thresholdValue.addActionListener(new ActionListener()
266     {
267       @Override
268       public void actionPerformed(ActionEvent e)
269       {
270         thresholdValue_actionPerformed();
271       }
272     });
273     thresholdValue.addFocusListener(new FocusAdapter()
274     {
275       @Override
276       public void focusLost(FocusEvent e)
277       {
278         thresholdValue_actionPerformed();
279       }
280     });
281     slider.setPaintLabels(false);
282     slider.setPaintTicks(true);
283     slider.setBackground(Color.white);
284     slider.setEnabled(false);
285     slider.setOpaque(false);
286     slider.setPreferredSize(new Dimension(100, 32));
287     slider.setToolTipText(
288             MessageManager.getString("label.adjust_threshold"));
289     thresholdValue.setEnabled(false);
290     thresholdValue.setColumns(7);
291     thresholdPanel.setBackground(Color.white);
292     thresholdIsMin.setBackground(Color.white);
293     thresholdIsMin
294             .setText(MessageManager.getString("label.threshold_minmax"));
295     thresholdIsMin.setToolTipText(MessageManager
296             .getString("label.toggle_absolute_relative_display_threshold"));
297     thresholdIsMin.addActionListener(new ActionListener()
298     {
299       @Override
300       public void actionPerformed(ActionEvent actionEvent)
301       {
302         changeColour(true);
303       }
304     });
305     thresholdPanel.add(threshold);
306     thresholdPanel.add(slider);
307     thresholdPanel.add(thresholdValue);
308     thresholdPanel.add(thresholdIsMin);
309     return thresholdPanel;
310   }
311
312   /**
313    * Lay out OK and Cancel buttons
314    * 
315    * @return
316    */
317   protected JPanel initOkCancelPanel()
318   {
319     JPanel okCancelPanel = new JPanel();
320     okCancelPanel.setBackground(Color.white);
321     okCancelPanel.add(ok);
322     okCancelPanel.add(cancel);
323     return okCancelPanel;
324   }
325
326   /**
327    * Lay out Colour by Label and min/max colour widgets
328    * 
329    * @return
330    */
331   protected JPanel initColoursPanel()
332   {
333     JPanel colourByPanel = new JPanel();
334     colourByPanel.setLayout(new FlowLayout());
335     colourByPanel.setBackground(Color.white);
336     minColour.setFont(JvSwingUtils.getLabelFont());
337     minColour.setBorder(BorderFactory.createLineBorder(Color.black));
338     minColour.setPreferredSize(new Dimension(40, 20));
339     minColour.setToolTipText(MessageManager.getString("label.min_colour"));
340     minColour.addMouseListener(new MouseAdapter()
341     {
342       @Override
343       public void mousePressed(MouseEvent e)
344       {
345         if (minColour.isEnabled())
346         {
347           minColour_actionPerformed();
348         }
349       }
350     });
351     maxColour.setFont(JvSwingUtils.getLabelFont());
352     maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
353     maxColour.setPreferredSize(new Dimension(40, 20));
354     maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
355     maxColour.addMouseListener(new MouseAdapter()
356     {
357       @Override
358       public void mousePressed(MouseEvent e)
359       {
360         if (maxColour.isEnabled())
361         {
362           maxColour_actionPerformed();
363         }
364       }
365     });
366     maxColour.setBorder(new LineBorder(Color.black));
367     JLabel minText = new JLabel(MessageManager.getString("label.min"));
368     minText.setFont(JvSwingUtils.getLabelFont());
369     JLabel maxText = new JLabel(MessageManager.getString("label.max"));
370     maxText.setFont(JvSwingUtils.getLabelFont());
371
372     JPanel colourPanel = new JPanel();
373     colourPanel.setBackground(Color.white);
374     colourPanel.add(minText);
375     colourPanel.add(minColour);
376     colourPanel.add(maxText);
377     colourPanel.add(maxColour);
378     colourByPanel.add(colourByLabel, BorderLayout.WEST);
379     colourByPanel.add(colourPanel, BorderLayout.EAST);
380
381     colourByLabel.setBackground(Color.white);
382     colourByLabel
383             .setText(MessageManager.getString("label.colour_by_label"));
384     colourByLabel
385             .setToolTipText(MessageManager
386                     .getString("label.display_features_same_type_different_label_using_different_colour"));
387     colourByLabel.addActionListener(new ActionListener()
388     {
389       @Override
390       public void actionPerformed(ActionEvent actionEvent)
391       {
392         changeColour(true);
393       }
394     });
395
396     return colourByPanel;
397   }
398
399   /**
400    * Action on clicking the 'minimum colour' - open a colour chooser dialog, and
401    * set the selected colour (if the user does not cancel out of the dialog)
402    */
403   protected void minColour_actionPerformed()
404   {
405     Color col = JColorChooser.showDialog(this,
406             MessageManager.getString("label.select_colour_minimum_value"),
407             minColour.getBackground());
408     if (col != null)
409     {
410       minColour.setBackground(col);
411       minColour.setForeground(col);
412     }
413     minColour.repaint();
414     changeColour(true);
415   }
416
417   /**
418    * Action on clicking the 'maximum colour' - open a colour chooser dialog, and
419    * set the selected colour (if the user does not cancel out of the dialog)
420    */
421   protected void maxColour_actionPerformed()
422   {
423     Color col = JColorChooser.showDialog(this,
424             MessageManager.getString("label.select_colour_maximum_value"),
425             maxColour.getBackground());
426     if (col != null)
427     {
428       maxColour.setBackground(col);
429       maxColour.setForeground(col);
430     }
431     maxColour.repaint();
432     changeColour(true);
433   }
434
435   /**
436    * Constructs and sets the selected colour options as the colour for the
437    * feature type, and repaints the alignment, and optionally the Overview
438    * and/or structure viewer if open
439    * 
440    * @param updateStructsAndOverview
441    */
442   void changeColour(boolean updateStructsAndOverview)
443   {
444     // Check if combobox is still adjusting
445     if (adjusting)
446     {
447       return;
448     }
449
450     boolean aboveThreshold = false;
451     boolean belowThreshold = false;
452     if (threshold.getSelectedIndex() == 1)
453     {
454       aboveThreshold = true;
455     }
456     else if (threshold.getSelectedIndex() == 2)
457     {
458       belowThreshold = true;
459     }
460     boolean hasThreshold = aboveThreshold || belowThreshold;
461
462     slider.setEnabled(true);
463     thresholdValue.setEnabled(true);
464
465     FeatureColourI acg;
466     if (cs.isColourByLabel())
467     {
468       acg = new FeatureColour(oldminColour, oldmaxColour, min, max);
469     }
470     else
471     {
472       acg = new FeatureColour(oldminColour = minColour.getBackground(),
473               oldmaxColour = maxColour.getBackground(), min, max);
474     }
475
476     if (!hasThreshold)
477     {
478       slider.setEnabled(false);
479       thresholdValue.setEnabled(false);
480       thresholdValue.setText("");
481       thresholdIsMin.setEnabled(false);
482     }
483     else if (threshline == null)
484     {
485       /*
486        * todo not yet implemented: visual indication of feature threshold
487        */
488       threshline = new GraphLine((max - min) / 2f, "Threshold",
489               Color.black);
490     }
491
492     if (hasThreshold)
493     {
494       adjusting = true;
495       acg.setThreshold(threshline.value);
496
497       float range = (max - min) * scaleFactor;
498
499       slider.setMinimum((int) (min * scaleFactor));
500       slider.setMaximum((int) (max * scaleFactor));
501       // slider.setValue((int) (threshline.value * scaleFactor));
502       slider.setValue(Math.round(threshline.value * scaleFactor));
503       thresholdValue.setText(threshline.value + "");
504       slider.setMajorTickSpacing((int) (range / 10f));
505       slider.setEnabled(true);
506       thresholdValue.setEnabled(true);
507       thresholdIsMin.setEnabled(!colourByLabel.isSelected());
508       adjusting = false;
509     }
510
511     acg.setAboveThreshold(aboveThreshold);
512     acg.setBelowThreshold(belowThreshold);
513     if (thresholdIsMin.isSelected() && hasThreshold)
514     {
515       acg.setAutoScaled(false);
516       if (aboveThreshold)
517       {
518         acg = new FeatureColour((FeatureColour) acg, threshline.value, max);
519       }
520       else
521       {
522         acg = new FeatureColour((FeatureColour) acg, min, threshline.value);
523       }
524     }
525     else
526     {
527       acg.setAutoScaled(true);
528     }
529     acg.setColourByLabel(colourByLabel.isSelected());
530     if (acg.isColourByLabel())
531     {
532       maxColour.setEnabled(false);
533       minColour.setEnabled(false);
534       maxColour.setBackground(this.getBackground());
535       maxColour.setForeground(this.getBackground());
536       minColour.setBackground(this.getBackground());
537       minColour.setForeground(this.getBackground());
538
539     }
540     else
541     {
542       maxColour.setEnabled(true);
543       minColour.setEnabled(true);
544       maxColour.setBackground(oldmaxColour);
545       minColour.setBackground(oldminColour);
546       maxColour.setForeground(oldmaxColour);
547       minColour.setForeground(oldminColour);
548     }
549
550     fr.setColour(type, acg);
551     cs = acg;
552     ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
553   }
554
555   @Override
556   protected void raiseClosed()
557   {
558     if (this.colourEditor != null)
559     {
560       colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
561     }
562   }
563
564   @Override
565   public void okPressed()
566   {
567     changeColour(false);
568   }
569
570   @Override
571   public void cancelPressed()
572   {
573     reset();
574   }
575
576   /**
577    * Action when the user cancels the dialog. All previous settings should be
578    * restored and rendered on the alignment, and any linked Overview window or
579    * structure.
580    */
581   void reset()
582   {
583     fr.setColour(type, oldcs);
584     ap.paintAlignment(true, true);
585     cs = null;
586   }
587
588   /**
589    * Action on text entry of a threshold value
590    */
591   protected void thresholdValue_actionPerformed()
592   {
593     try
594     {
595       float f = Float.parseFloat(thresholdValue.getText());
596       slider.setValue((int) (f * scaleFactor));
597       threshline.value = f;
598
599       /*
600        * force repaint of any Overview window or structure
601        */
602       ap.paintAlignment(true, true);
603     } catch (NumberFormatException ex)
604     {
605     }
606   }
607
608   /**
609    * Action on change of threshold slider value. This may be done interactively
610    * (by moving the slider), or programmatically (to update the slider after
611    * manual input of a threshold value).
612    */
613   protected void sliderValueChanged()
614   {
615     /*
616      * squash rounding errors by forcing min/max of slider to 
617      * actual min/max of feature score range
618      */
619     int value = slider.getValue();
620     threshline.value = value == slider.getMaximum() ? max
621             : (value == slider.getMinimum() ? min : value / scaleFactor);
622     cs.setThreshold(threshline.value);
623
624     /*
625      * repaint alignment, but not Overview or structure,
626      * to avoid overload while dragging the slider
627      */
628     changeColour(false);
629   }
630
631   void addActionListener(ActionListener graduatedColorEditor)
632   {
633     if (colourEditor != null)
634     {
635       System.err.println(
636               "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
637     }
638     colourEditor = graduatedColorEditor;
639   }
640
641   /**
642    * Answers the last colour setting selected by user - either oldcs (which may
643    * be a java.awt.Color) or the new GraduatedColor
644    * 
645    * @return
646    */
647   FeatureColourI getLastColour()
648   {
649     if (cs == null)
650     {
651       return oldcs;
652     }
653     return cs;
654   }
655
656 }