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