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