JAL-3048 amendFeatures as dialog runner - repaint after amend not always reliable
[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               updateColourButton(mainPanel, colour, new FeatureColour(c));
209             };
210           };
211           JalviewColourChooser.showColourChooser(Desktop.getDesktop(),
212                   title, fcol.getColour(), listener);
213         }
214         else
215         {
216           /*
217            * variable colour dialog - on OK, refetch the updated
218            * feature colour and update this display
219            */
220           final String ft = features.get(featureIndex).getType();
221           final String type = ft == null ? lastFeatureAdded : ft;
222           FeatureTypeSettings fcc = new FeatureTypeSettings(
223                   FeatureRenderer.this, type);
224           fcc.setRequestFocusEnabled(true);
225           fcc.requestFocus();
226           fcc.addActionListener(new ActionListener()
227           {
228             @Override
229             public void actionPerformed(ActionEvent e)
230             {
231               fcol = FeatureRenderer.this.getFeatureStyle(ft);
232               setColour(type, fcol);
233               updateColourButton(mainPanel, colour, fcol);
234             }
235           });
236         }
237       }
238     });
239     JPanel gridPanel = new JPanel(new GridLayout(3, 1));
240
241     if (!create && features.size() > 1)
242     {
243       /*
244        * more than one feature at selected position - 
245        * add a drop-down to choose the feature to amend
246        * space pad text if necessary to make entries distinct
247        */
248       gridPanel = new JPanel(new GridLayout(4, 1));
249       JPanel choosePanel = new JPanel();
250       choosePanel.add(new JLabel(
251               MessageManager.getString("label.select_feature") + ":"));
252       final JComboBox<String> overlaps = new JComboBox<>();
253       List<String> added = new ArrayList<>();
254       for (SequenceFeature sf : features)
255       {
256         String text = String.format("%s/%d-%d (%s)", sf.getType(),
257                 sf.getBegin(), sf.getEnd(), sf.getFeatureGroup());
258         while (added.contains(text))
259         {
260           text += " ";
261         }
262         overlaps.addItem(text);
263         added.add(text);
264       }
265       choosePanel.add(overlaps);
266
267       overlaps.addItemListener(new ItemListener()
268       {
269         @Override
270         public void itemStateChanged(ItemEvent e)
271         {
272           int index = overlaps.getSelectedIndex();
273           if (index != -1)
274           {
275             featureIndex = index;
276             SequenceFeature sf = features.get(index);
277             name.setText(sf.getType());
278             description.setText(sf.getDescription());
279             group.setText(sf.getFeatureGroup());
280             start.setValue(new Integer(sf.getBegin()));
281             end.setValue(new Integer(sf.getEnd()));
282
283             SearchResultsI highlight = new SearchResults();
284             highlight.addResult(sequences.get(0), sf.getBegin(),
285                     sf.getEnd());
286
287             alignPanel.getSeqPanel().seqCanvas
288                     .highlightSearchResults(highlight, false);
289           }
290           FeatureColourI col = getFeatureStyle(name.getText());
291           if (col == null)
292           {
293             col = new FeatureColour(
294                     ColorUtils.createColourFromName(name.getText()));
295           }
296           oldcol = fcol = col;
297           updateColourButton(mainPanel, colour, col);
298         }
299       });
300
301       gridPanel.add(choosePanel);
302     }
303
304     JPanel namePanel = new JPanel();
305     gridPanel.add(namePanel);
306     namePanel.add(new JLabel(MessageManager.getString("label.name:"),
307             JLabel.RIGHT));
308     namePanel.add(name);
309
310     JPanel groupPanel = new JPanel();
311     gridPanel.add(groupPanel);
312     groupPanel.add(new JLabel(MessageManager.getString("label.group:"),
313             JLabel.RIGHT));
314     groupPanel.add(group);
315
316     JPanel colourPanel = new JPanel();
317     gridPanel.add(colourPanel);
318     colourPanel.add(new JLabel(MessageManager.getString("label.colour"),
319             JLabel.RIGHT));
320     colourPanel.add(colour);
321     colour.setPreferredSize(new Dimension(150, 15));
322     colour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 9));
323     colour.setForeground(Color.black);
324     colour.setHorizontalAlignment(SwingConstants.CENTER);
325     colour.setVerticalAlignment(SwingConstants.CENTER);
326     colour.setHorizontalTextPosition(SwingConstants.CENTER);
327     colour.setVerticalTextPosition(SwingConstants.CENTER);
328     mainPanel.add(gridPanel, BorderLayout.NORTH);
329
330     JPanel descriptionPanel = new JPanel();
331     descriptionPanel.add(new JLabel(
332             MessageManager.getString("label.description:"), JLabel.RIGHT));
333     description.setFont(JvSwingUtils.getTextAreaFont());
334     description.setLineWrap(true);
335     descriptionPanel.add(new JScrollPane(description));
336
337     if (!create)
338     {
339       mainPanel.add(descriptionPanel, BorderLayout.SOUTH);
340
341       JPanel startEndPanel = new JPanel();
342       startEndPanel.add(new JLabel(MessageManager.getString("label.start"),
343               JLabel.RIGHT));
344       startEndPanel.add(start);
345       startEndPanel.add(new JLabel(MessageManager.getString("label.end"),
346               JLabel.RIGHT));
347       startEndPanel.add(end);
348       mainPanel.add(startEndPanel, BorderLayout.CENTER);
349     }
350     else
351     {
352       mainPanel.add(descriptionPanel, BorderLayout.CENTER);
353     }
354
355     /*
356      * default feature type and group to that of the first feature supplied,
357      * or to the last feature created if not supplied (null value) 
358      */
359     SequenceFeature firstFeature = features.get(0);
360     boolean useLastDefaults = firstFeature.getType() == null;
361     final String featureType = useLastDefaults ? lastFeatureAdded
362             : firstFeature.getType();
363     final String featureGroup = useLastDefaults ? lastFeatureGroupAdded
364             : firstFeature.getFeatureGroup();
365     name.setText(featureType);
366     group.setText(featureGroup);
367
368     start.setValue(new Integer(firstFeature.getBegin()));
369     end.setValue(new Integer(firstFeature.getEnd()));
370     description.setText(firstFeature.getDescription());
371     updateColourButton(mainPanel, colour,
372             (oldcol = fcol = getFeatureStyle(featureType)));
373     Object[] options;
374     if (!create)
375     {
376       options = new Object[] { MessageManager.getString("label.amend"),
377           MessageManager.getString("action.delete"),
378           MessageManager.getString("action.cancel") };
379     }
380     else
381     {
382       options = new Object[] { MessageManager.getString("action.ok"),
383           MessageManager.getString("action.cancel") };
384     }
385
386     String title = create
387             ? MessageManager.getString("label.create_new_sequence_features")
388             : MessageManager.formatMessage("label.amend_delete_features",
389                     new String[]
390                     { sequences.get(0).getName() });
391
392     /*
393      * register responses and show the dialog
394      */
395     JvOptionPane.newOptionDialog(Desktop.desktop).response(
396
397             new RunResponse(JvOptionPane.OK_OPTION)
398             {
399               public void run()
400               {
401                 final String enteredType = name.getText().trim();
402                 final String enteredGroup = group.getText().trim();
403                 final String enteredDescription = description.getText()
404                         .replaceAll("\n", " ");
405                 if (enteredType.length() > 0)
406
407                 {
408                   /*
409                    * update default values only if creating using default values
410                    */
411                   if (useLastDefaults)
412                   {
413                     lastFeatureAdded = enteredType;
414                     lastFeatureGroupAdded = enteredGroup;
415                     // TODO: determine if the null feature group is valid
416                     if (lastFeatureGroupAdded.length() < 1)
417                     {
418                       lastFeatureGroupAdded = null;
419                     }
420                   }
421                 }
422
423                 if (create)
424                 {
425                   // NEW FEATURES ADDED
426                   if (enteredType.length() > 0)
427                   {
428                     for (int i = 0; i < sequences.size(); i++)
429                     {
430                       SequenceFeature sf = features.get(i);
431                       SequenceFeature sf2 = new SequenceFeature(enteredType,
432                               enteredDescription, sf.getBegin(),
433                               sf.getEnd(), enteredGroup);
434                       new FeaturesFile().parseDescriptionHTML(sf2, false);
435                       sequences.get(i).addSequenceFeature(sf2);
436                     }
437
438                     setColour(enteredType, fcol);
439
440                     featuresAdded();
441
442                     responseHandler.run();
443                   }
444                 } else {
445                   SequenceFeature sf = features.get(featureIndex);
446                   /*
447                    * Feature amended - YES_OPTION corresponds to the Amend button
448                    * need to refresh Feature Settings if type, group or colour changed;
449                    * note we don't force the feature to be visible - the user has been
450                    * warned if a hidden feature type or group was entered
451                    */
452                   boolean refreshSettings = (!featureType.equals(enteredType)
453                           || !featureGroup.equals(enteredGroup));
454                   refreshSettings |= (fcol != oldcol);
455                   setColour(enteredType, fcol);
456                   int newBegin = sf.begin;
457                   int newEnd = sf.end;
458                   try
459                   {
460                     newBegin = ((Integer) start.getValue()).intValue();
461                     newEnd = ((Integer) end.getValue()).intValue();
462                   } catch (NumberFormatException ex)
463                   {
464                     // JSpinner doesn't accept invalid format data :-)
465                   }
466
467                   /*
468                    * replace the feature by deleting it and adding a new one
469                    * (to ensure integrity of SequenceFeatures data store)
470                    */
471                   sequences.get(0).deleteFeature(sf);
472                   SequenceFeature newSf = new SequenceFeature(sf, enteredType,
473                           newBegin, newEnd, enteredGroup, sf.getScore());
474                   newSf.setDescription(enteredDescription);
475                   new FeaturesFile().parseDescriptionHTML(newSf, false);
476                   // amend features dialog only updates one sequence at a time
477                   sequences.get(0).addSequenceFeature(newSf);
478
479                   if (refreshSettings)
480                   {
481                     featuresAdded();
482                   }
483                 }
484                 alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(null, false);
485                 alignPanel.paintAlignment(true, true);
486               }
487             }).response(new RunResponse(JvOptionPane.NO_OPTION)
488             {
489               public void run()
490               {
491                 SequenceFeature sf = features.get(featureIndex);
492                 /*
493                  * NO_OPTION corresponds to the Delete button
494                  */
495                 sequences.get(0).getDatasetSequence().deleteFeature(sf);
496                 // update Feature Settings for removal of feature / group
497                 featuresAdded();
498                 alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(null, false);
499                 alignPanel.paintAlignment(true, true);
500               }
501             }).defaultResponse(new Runnable()
502             {
503               public void run()
504               {
505                 alignPanel.getSeqPanel().seqCanvas.highlightSearchResults(null, false);
506                 alignPanel.paintAlignment(true, true);
507               }
508             }).showInternalDialog(mainPanel, title,
509                     JvOptionPane.YES_NO_CANCEL_OPTION,
510                     JvOptionPane.QUESTION_MESSAGE, null, options,
511                     MessageManager.getString("action.ok"));
512     return true;
513   }
514
515   /**
516    * Show a warning message if the entered type is one that is currently hidden
517    * 
518    * @param panel
519    * @param type
520    */
521   protected void warnIfTypeHidden(JPanel panel, String type)
522   {
523     if (getRenderOrder().contains(type))
524     {
525       if (!showFeatureOfType(type))
526       {
527         String msg = MessageManager.formatMessage("label.warning_hidden",
528                 MessageManager.getString("label.feature_type"), type);
529         JvOptionPane.showMessageDialog(panel, msg, "",
530                 JvOptionPane.OK_OPTION);
531       }
532     }
533   }
534
535   /**
536    * Show a warning message if the entered group is one that is currently hidden
537    * 
538    * @param panel
539    * @param group
540    */
541   protected void warnIfGroupHidden(JPanel panel, String group)
542   {
543     if (featureGroups.containsKey(group) && !featureGroups.get(group))
544     {
545       String msg = MessageManager.formatMessage("label.warning_hidden",
546               MessageManager.getString("label.group"), group);
547       JvOptionPane.showMessageDialog(panel, msg, "",
548               JvOptionPane.OK_OPTION);
549     }
550   }
551
552   /**
553    * update the amend feature button dependent on the given style
554    * 
555    * @param bigPanel
556    * @param col
557    * @param col
558    */
559   protected void updateColourButton(JPanel bigPanel, JLabel colour,
560           FeatureColourI col)
561   {
562     colour.removeAll();
563     colour.setIcon(null);
564     colour.setToolTipText(null);
565     colour.setText("");
566
567     if (col.isSimpleColour())
568     {
569       colour.setBackground(col.getColour());
570     }
571     else
572     {
573       colour.setBackground(bigPanel.getBackground());
574       colour.setForeground(Color.black);
575       FeatureSettings.renderGraduatedColor(colour, col);
576     }
577   }
578
579   /**
580    * Orders features in render precedence (last in order is last to render, so
581    * displayed on top of other features)
582    * 
583    * @param order
584    */
585   public void orderFeatures(Comparator<String> order)
586   {
587     Arrays.sort(renderOrder, order);
588   }
589 }