JAL-3058 save modified colour from dialog
[jalview.git] / src / jalview / gui / FeatureRenderer.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.SearchResults;
25 import jalview.datamodel.SearchResultsI;
26 import jalview.datamodel.SequenceFeature;
27 import jalview.datamodel.SequenceI;
28 import jalview.gui.JalviewColourChooser.ColourChooserListener;
29 import jalview.io.FeaturesFile;
30 import jalview.schemes.FeatureColour;
31 import jalview.util.ColorUtils;
32 import jalview.util.MessageManager;
33 import jalview.util.dialogrunner.RunResponse;
34
35 import java.awt.BorderLayout;
36 import java.awt.Color;
37 import java.awt.Dimension;
38 import java.awt.Font;
39 import java.awt.GridLayout;
40 import java.awt.event.ActionEvent;
41 import java.awt.event.ActionListener;
42 import java.awt.event.ItemEvent;
43 import java.awt.event.ItemListener;
44 import java.awt.event.MouseAdapter;
45 import java.awt.event.MouseEvent;
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.Comparator;
49 import java.util.List;
50
51 import javax.swing.JColorChooser;
52 import javax.swing.JComboBox;
53 import javax.swing.JDialog;
54 import javax.swing.JLabel;
55 import javax.swing.JPanel;
56 import javax.swing.JScrollPane;
57 import javax.swing.JSpinner;
58 import javax.swing.JTextArea;
59 import javax.swing.JTextField;
60 import javax.swing.SwingConstants;
61 import javax.swing.event.DocumentEvent;
62 import javax.swing.event.DocumentListener;
63
64 /**
65  * DOCUMENT ME!
66  * 
67  * @author $author$
68  * @version $Revision$
69  */
70 public class FeatureRenderer
71         extends jalview.renderer.seqfeatures.FeatureRenderer
72 {
73   /*
74    * defaults for creating a new feature are the last created
75    * feature type and group
76    */
77   static String lastFeatureAdded = "feature_1";
78
79   static String lastFeatureGroupAdded = "Jalview";
80
81   Color resBoxColour;
82
83   AlignmentPanel ap;
84
85   /**
86    * Creates a new FeatureRenderer object
87    * 
88    * @param alignPanel
89    */
90   public FeatureRenderer(AlignmentPanel alignPanel)
91   {
92     super(alignPanel.av);
93     this.ap = alignPanel;
94     if (alignPanel.getSeqPanel() != null
95             && alignPanel.getSeqPanel().seqCanvas != null
96             && alignPanel.getSeqPanel().seqCanvas.fr != null)
97     {
98       transferSettings(alignPanel.getSeqPanel().seqCanvas.fr);
99     }
100   }
101
102   FeatureColourI oldcol, fcol;
103
104   int featureIndex = 0;
105
106   /**
107    * Presents a dialog allowing the user to add new features, or amend or delete
108    * existing features. Currently this can be on
109    * <ul>
110    * <li>double-click on a sequence - Amend/Delete features at position</li>
111    * <li>Create sequence feature from pop-up menu on selected region</li>
112    * <li>Create features for pattern matches from Find</li>
113    * </ul>
114    * If the supplied feature type is null, show (and update on confirm) the type
115    * and group of the last new feature created (with initial defaults of
116    * "feature_1" and "Jalview").
117    * 
118    * @param sequences
119    *          the sequences features are to be created on (if creating
120    *          features), or a single sequence (if amending features)
121    * @param features
122    *          the current features at the position (if amending), or template
123    *          new feature(s) with start/end position set (if creating)
124    * @param create
125    *          true to create features, false to amend or delete
126    * @param alignPanel
127    * @param responseHandler
128    *          boolean true RunResponse is run if features are created
129    */
130   protected boolean amendFeatures(final List<SequenceI> sequences,
131           final List<SequenceFeature> features, boolean create,
132           final AlignmentPanel alignPanel, final Runnable responseHandler)
133   {
134     featureIndex = 0;
135
136     final JPanel mainPanel = new JPanel(new BorderLayout());
137
138     final JTextField name = new JTextField(25);
139     name.getDocument().addDocumentListener(new DocumentListener()
140     {
141       @Override
142       public void insertUpdate(DocumentEvent e)
143       {
144         warnIfTypeHidden(mainPanel, name.getText());
145       }
146
147       @Override
148       public void removeUpdate(DocumentEvent e)
149       {
150         warnIfTypeHidden(mainPanel, name.getText());
151       }
152
153       @Override
154       public void changedUpdate(DocumentEvent e)
155       {
156         warnIfTypeHidden(mainPanel, name.getText());
157       }
158     });
159
160     final JTextField group = new JTextField(25);
161     group.getDocument().addDocumentListener(new DocumentListener()
162     {
163       @Override
164       public void insertUpdate(DocumentEvent e)
165       {
166         warnIfGroupHidden(mainPanel, group.getText());
167       }
168
169       @Override
170       public void removeUpdate(DocumentEvent e)
171       {
172         warnIfGroupHidden(mainPanel, group.getText());
173       }
174
175       @Override
176       public void changedUpdate(DocumentEvent e)
177       {
178         warnIfGroupHidden(mainPanel, group.getText());
179       }
180     });
181
182     final JTextArea description = new JTextArea(3, 25);
183     final JSpinner start = new JSpinner();
184     final JSpinner end = new JSpinner();
185     start.setPreferredSize(new Dimension(80, 20));
186     end.setPreferredSize(new Dimension(80, 20));
187     final JLabel colour = new JLabel();
188     colour.setOpaque(true);
189     // colour.setBorder(BorderFactory.createEtchedBorder());
190     colour.setMaximumSize(new Dimension(30, 16));
191     colour.addMouseListener(new MouseAdapter()
192     {
193       /*
194        * open colour chooser on click in colour panel
195        */
196       @Override
197       public void mousePressed(MouseEvent evt)
198       {
199         if (fcol.isSimpleColour())
200         {
201           String title = MessageManager
202                   .getString("label.select_feature_colour");
203           ColourChooserListener listener = new ColourChooserListener()
204           {
205             @Override
206             public void colourSelected(Color c)
207             {
208               fcol = new FeatureColour(c);
209               updateColourButton(mainPanel, colour, fcol);
210             };
211           };
212           JalviewColourChooser.showColourChooser(Desktop.getDesktop(),
213                   title, fcol.getColour(), listener);
214         }
215         else
216         {
217           /*
218            * variable colour dialog - on OK, refetch the updated
219            * feature colour and update this display
220            */
221           final String ft = features.get(featureIndex).getType();
222           final String type = ft == null ? lastFeatureAdded : ft;
223           FeatureTypeSettings fcc = new FeatureTypeSettings(
224                   FeatureRenderer.this, type);
225           fcc.setRequestFocusEnabled(true);
226           fcc.requestFocus();
227           fcc.addActionListener(new ActionListener()
228           {
229             @Override
230             public void actionPerformed(ActionEvent e)
231             {
232               fcol = FeatureRenderer.this.getFeatureStyle(ft);
233               setColour(type, fcol);
234               updateColourButton(mainPanel, colour, fcol);
235             }
236           });
237         }
238       }
239     });
240     JPanel gridPanel = new JPanel(new GridLayout(3, 1));
241
242     if (!create && features.size() > 1)
243     {
244       /*
245        * more than one feature at selected position - 
246        * add a drop-down to choose the feature to amend
247        * space pad text if necessary to make entries distinct
248        */
249       gridPanel = new JPanel(new GridLayout(4, 1));
250       JPanel choosePanel = new JPanel();
251       choosePanel.add(new JLabel(
252               MessageManager.getString("label.select_feature") + ":"));
253       final JComboBox<String> overlaps = new JComboBox<>();
254       List<String> added = new ArrayList<>();
255       for (SequenceFeature sf : features)
256       {
257         String text = String.format("%s/%d-%d (%s)", sf.getType(),
258                 sf.getBegin(), sf.getEnd(), sf.getFeatureGroup());
259         while (added.contains(text))
260         {
261           text += " ";
262         }
263         overlaps.addItem(text);
264         added.add(text);
265       }
266       choosePanel.add(overlaps);
267
268       overlaps.addItemListener(new ItemListener()
269       {
270         @Override
271         public void itemStateChanged(ItemEvent e)
272         {
273           int index = overlaps.getSelectedIndex();
274           if (index != -1)
275           {
276             featureIndex = index;
277             SequenceFeature sf = features.get(index);
278             name.setText(sf.getType());
279             description.setText(sf.getDescription());
280             group.setText(sf.getFeatureGroup());
281             start.setValue(new Integer(sf.getBegin()));
282             end.setValue(new Integer(sf.getEnd()));
283
284             SearchResultsI highlight = new SearchResults();
285             highlight.addResult(sequences.get(0), sf.getBegin(),
286                     sf.getEnd());
287
288             alignPanel.getSeqPanel().seqCanvas
289                     .highlightSearchResults(highlight, false);
290           }
291           FeatureColourI col = getFeatureStyle(name.getText());
292           if (col == null)
293           {
294             col = new FeatureColour(
295                     ColorUtils.createColourFromName(name.getText()));
296           }
297           oldcol = fcol = col;
298           updateColourButton(mainPanel, colour, col);
299         }
300       });
301
302       gridPanel.add(choosePanel);
303     }
304
305     JPanel namePanel = new JPanel();
306     gridPanel.add(namePanel);
307     namePanel.add(new JLabel(MessageManager.getString("label.name:"),
308             JLabel.RIGHT));
309     namePanel.add(name);
310
311     JPanel groupPanel = new JPanel();
312     gridPanel.add(groupPanel);
313     groupPanel.add(new JLabel(MessageManager.getString("label.group:"),
314             JLabel.RIGHT));
315     groupPanel.add(group);
316
317     JPanel colourPanel = new JPanel();
318     gridPanel.add(colourPanel);
319     colourPanel.add(new JLabel(MessageManager.getString("label.colour"),
320             JLabel.RIGHT));
321     colourPanel.add(colour);
322     colour.setPreferredSize(new Dimension(150, 15));
323     colour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 9));
324     colour.setForeground(Color.black);
325     colour.setHorizontalAlignment(SwingConstants.CENTER);
326     colour.setVerticalAlignment(SwingConstants.CENTER);
327     colour.setHorizontalTextPosition(SwingConstants.CENTER);
328     colour.setVerticalTextPosition(SwingConstants.CENTER);
329     mainPanel.add(gridPanel, BorderLayout.NORTH);
330
331     JPanel descriptionPanel = new JPanel();
332     descriptionPanel.add(new JLabel(
333             MessageManager.getString("label.description:"), JLabel.RIGHT));
334     description.setFont(JvSwingUtils.getTextAreaFont());
335     description.setLineWrap(true);
336     descriptionPanel.add(new JScrollPane(description));
337
338     if (!create)
339     {
340       mainPanel.add(descriptionPanel, BorderLayout.SOUTH);
341
342       JPanel startEndPanel = new JPanel();
343       startEndPanel.add(new JLabel(MessageManager.getString("label.start"),
344               JLabel.RIGHT));
345       startEndPanel.add(start);
346       startEndPanel.add(new JLabel(MessageManager.getString("label.end"),
347               JLabel.RIGHT));
348       startEndPanel.add(end);
349       mainPanel.add(startEndPanel, BorderLayout.CENTER);
350     }
351     else
352     {
353       mainPanel.add(descriptionPanel, BorderLayout.CENTER);
354     }
355
356     /*
357      * default feature type and group to that of the first feature supplied,
358      * or to the last feature created if not supplied (null value) 
359      */
360     SequenceFeature firstFeature = features.get(0);
361     boolean useLastDefaults = firstFeature.getType() == null;
362     final String featureType = useLastDefaults ? lastFeatureAdded
363             : firstFeature.getType();
364     final String featureGroup = useLastDefaults ? lastFeatureGroupAdded
365             : firstFeature.getFeatureGroup();
366     name.setText(featureType);
367     group.setText(featureGroup);
368
369     start.setValue(new Integer(firstFeature.getBegin()));
370     end.setValue(new Integer(firstFeature.getEnd()));
371     description.setText(firstFeature.getDescription());
372     updateColourButton(mainPanel, colour,
373             (oldcol = fcol = getFeatureStyle(featureType)));
374     Object[] options;
375     if (!create)
376     {
377       options = new Object[] { MessageManager.getString("label.amend"),
378           MessageManager.getString("action.delete"),
379           MessageManager.getString("action.cancel") };
380     }
381     else
382     {
383       options = new Object[] { MessageManager.getString("action.ok"),
384           MessageManager.getString("action.cancel") };
385     }
386
387     String title = create
388             ? MessageManager.getString("label.create_new_sequence_features")
389             : MessageManager.formatMessage("label.amend_delete_features",
390                     new String[]
391                     { sequences.get(0).getName() });
392
393     /*
394      * register responses and show the dialog
395      */
396     JvOptionPane.newOptionDialog(Desktop.desktop).response(
397
398             new RunResponse(JvOptionPane.OK_OPTION)
399             {
400               public void run()
401               {
402                 final String enteredType = name.getText().trim();
403                 final String enteredGroup = group.getText().trim();
404                 final String enteredDescription = description.getText()
405                         .replaceAll("\n", " ");
406                 if (enteredType.length() > 0)
407
408                 {
409                   /*
410                    * update default values only if creating using default values
411                    */
412                   if (useLastDefaults)
413                   {
414                     lastFeatureAdded = enteredType;
415                     lastFeatureGroupAdded = enteredGroup;
416                     // TODO: determine if the null feature group is valid
417                     if (lastFeatureGroupAdded.length() < 1)
418                     {
419                       lastFeatureGroupAdded = null;
420                     }
421                   }
422                 }
423
424                 if (create)
425                 {
426                   // NEW FEATURES ADDED
427                   if (enteredType.length() > 0)
428                   {
429                     for (int i = 0; i < sequences.size(); i++)
430                     {
431                       SequenceFeature sf = features.get(i);
432                       SequenceFeature sf2 = new SequenceFeature(enteredType,
433                               enteredDescription, sf.getBegin(),
434                               sf.getEnd(), enteredGroup);
435                       new FeaturesFile().parseDescriptionHTML(sf2, false);
436                       sequences.get(i).addSequenceFeature(sf2);
437                     }
438
439                     setColour(enteredType, fcol);
440
441                     featuresAdded();
442
443                     responseHandler.run();
444                   }
445                 } else {
446                   SequenceFeature sf = features.get(featureIndex);
447                   /*
448                    * Feature amended - YES_OPTION corresponds to the Amend button
449                    * need to refresh Feature Settings if type, group or colour changed;
450                    * note we don't force the feature to be visible - the user has been
451                    * warned if a hidden feature type or group was entered
452                    */
453                   boolean refreshSettings = (!featureType.equals(enteredType)
454                           || !featureGroup.equals(enteredGroup));
455                   refreshSettings |= (fcol != oldcol);
456                   setColour(enteredType, fcol);
457                   int newBegin = sf.begin;
458                   int newEnd = sf.end;
459                   try
460                   {
461                     newBegin = ((Integer) start.getValue()).intValue();
462                     newEnd = ((Integer) end.getValue()).intValue();
463                   } catch (NumberFormatException ex)
464                   {
465                     // JSpinner doesn't accept invalid format data :-)
466                   }
467
468                   /*
469                    * replace the feature by deleting it and adding a new one
470                    * (to ensure integrity of SequenceFeatures data store)
471                    */
472                   sequences.get(0).deleteFeature(sf);
473                   SequenceFeature newSf = new SequenceFeature(sf, enteredType,
474                           newBegin, newEnd, enteredGroup, sf.getScore());
475                   newSf.setDescription(enteredDescription);
476                   new FeaturesFile().parseDescriptionHTML(newSf, false);
477                   // amend features dialog only updates one sequence at a time
478                   sequences.get(0).addSequenceFeature(newSf);
479
480                   if (refreshSettings)
481                   {
482                     featuresAdded();
483                   }
484                 }
485                 alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(null, false);
486                 alignPanel.paintAlignment(true, true);
487               }
488             }).response(new RunResponse(JvOptionPane.NO_OPTION)
489             {
490               public void run()
491               {
492                 SequenceFeature sf = features.get(featureIndex);
493                 /*
494                  * NO_OPTION corresponds to the Delete button
495                  */
496                 sequences.get(0).getDatasetSequence().deleteFeature(sf);
497                 // update Feature Settings for removal of feature / group
498                 featuresAdded();
499                 alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(null, false);
500                 alignPanel.paintAlignment(true, true);
501               }
502             }).defaultResponse(new Runnable()
503             {
504               public void run()
505               {
506                 alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(null, false);
507                 alignPanel.paintAlignment(true, true);
508               }
509             }).showInternalDialog(mainPanel, title,
510                     JvOptionPane.YES_NO_CANCEL_OPTION,
511                     JvOptionPane.QUESTION_MESSAGE, null, options,
512                     MessageManager.getString("action.ok"));
513     return true;
514   }
515
516   /**
517    * Show a warning message if the entered type is one that is currently hidden
518    * 
519    * @param panel
520    * @param type
521    */
522   protected void warnIfTypeHidden(JPanel panel, String type)
523   {
524     if (getRenderOrder().contains(type))
525     {
526       if (!showFeatureOfType(type))
527       {
528         String msg = MessageManager.formatMessage("label.warning_hidden",
529                 MessageManager.getString("label.feature_type"), type);
530         JvOptionPane.showMessageDialog(panel, msg, "",
531                 JvOptionPane.OK_OPTION);
532       }
533     }
534   }
535
536   /**
537    * Show a warning message if the entered group is one that is currently hidden
538    * 
539    * @param panel
540    * @param group
541    */
542   protected void warnIfGroupHidden(JPanel panel, String group)
543   {
544     if (featureGroups.containsKey(group) && !featureGroups.get(group))
545     {
546       String msg = MessageManager.formatMessage("label.warning_hidden",
547               MessageManager.getString("label.group"), group);
548       JvOptionPane.showMessageDialog(panel, msg, "",
549               JvOptionPane.OK_OPTION);
550     }
551   }
552
553   /**
554    * update the amend feature button dependent on the given style
555    * 
556    * @param bigPanel
557    * @param col
558    * @param col
559    */
560   protected void updateColourButton(JPanel bigPanel, JLabel colour,
561           FeatureColourI col)
562   {
563     colour.removeAll();
564     colour.setIcon(null);
565     colour.setToolTipText(null);
566     colour.setText("");
567
568     if (col.isSimpleColour())
569     {
570       colour.setBackground(col.getColour());
571     }
572     else
573     {
574       colour.setBackground(bigPanel.getBackground());
575       colour.setForeground(Color.black);
576       FeatureSettings.renderGraduatedColor(colour, col);
577     }
578   }
579
580   /**
581    * Orders features in render precedence (last in order is last to render, so
582    * displayed on top of other features)
583    * 
584    * @param order
585    */
586   public void orderFeatures(Comparator<String> order)
587   {
588     Arrays.sort(renderOrder, order);
589   }
590 }