JAL-3048 response() renamed addResponse(); checkstyle warning fixes
[jalview.git] / src / jalview / gui / FeatureEditor.java
1 package jalview.gui;
2
3 import jalview.api.FeatureColourI;
4 import jalview.datamodel.SearchResults;
5 import jalview.datamodel.SearchResultsI;
6 import jalview.datamodel.SequenceFeature;
7 import jalview.datamodel.SequenceI;
8 import jalview.gui.JalviewColourChooser.ColourChooserListener;
9 import jalview.io.FeaturesFile;
10 import jalview.schemes.FeatureColour;
11 import jalview.util.ColorUtils;
12 import jalview.util.MessageManager;
13 import jalview.util.dialogrunner.RunResponse;
14
15 import java.awt.BorderLayout;
16 import java.awt.Color;
17 import java.awt.Dimension;
18 import java.awt.Font;
19 import java.awt.GridLayout;
20 import java.awt.event.ActionEvent;
21 import java.awt.event.ActionListener;
22 import java.awt.event.ItemEvent;
23 import java.awt.event.ItemListener;
24 import java.awt.event.MouseAdapter;
25 import java.awt.event.MouseEvent;
26 import java.util.ArrayList;
27 import java.util.List;
28
29 import javax.swing.JComboBox;
30 import javax.swing.JLabel;
31 import javax.swing.JPanel;
32 import javax.swing.JScrollPane;
33 import javax.swing.JSpinner;
34 import javax.swing.JTextArea;
35 import javax.swing.JTextField;
36 import javax.swing.SpinnerNumberModel;
37 import javax.swing.SwingConstants;
38 import javax.swing.event.ChangeEvent;
39 import javax.swing.event.ChangeListener;
40 import javax.swing.event.DocumentEvent;
41 import javax.swing.event.DocumentListener;
42
43 /**
44  * Provides a dialog allowing the user to add new features, or amend or delete
45  * existing features
46  */
47 public class FeatureEditor
48 {
49   /*
50    * defaults for creating a new feature are the last created
51    * feature type and group
52    */
53   static String lastFeatureAdded = "feature_1";
54
55   static String lastFeatureGroupAdded = "Jalview";
56
57   /*
58    * the sequence(s) with features to be created / amended
59    */
60   final List<SequenceI> sequences;
61
62   /*
63    * the features (or template features) to be created / amended
64    */
65   final List<SequenceFeature> features;
66
67   /*
68    * true if the dialog is to create a new feature, false if
69    * for amend or delete of existing feature(s)
70    */
71   final boolean forCreate;
72
73   /*
74    * index into the list of features
75    */
76   int featureIndex;
77
78   FeatureColourI oldColour;
79
80   FeatureColourI featureColour;
81
82   FeatureRenderer fr;
83
84   AlignmentPanel ap;
85
86   JTextField name;
87
88   JTextField group;
89
90   JTextArea description;
91
92   JSpinner start;
93
94   JSpinner end;
95
96   JPanel mainPanel;
97
98   /**
99    * Constructor
100    * 
101    * @param alignPanel
102    * @param seqs
103    * @param feats
104    * @param create
105    *          if true create a new feature, else amend or delete an existing
106    *          feature
107    */
108   public FeatureEditor(AlignmentPanel alignPanel, List<SequenceI> seqs,
109           List<SequenceFeature> feats, boolean create)
110   {
111     ap = alignPanel;
112     fr = alignPanel.getSeqPanel().seqCanvas.fr;
113     sequences = seqs;
114     features = feats;
115     this.forCreate = create;
116
117     init();
118   }
119
120   /**
121    * Initialise the layout and controls
122    */
123   protected void init()
124   {
125     featureIndex = 0;
126
127     mainPanel = new JPanel(new BorderLayout());
128
129     name = new JTextField(25);
130     name.getDocument().addDocumentListener(new DocumentListener()
131     {
132       @Override
133       public void insertUpdate(DocumentEvent e)
134       {
135         warnIfTypeHidden(mainPanel, name.getText());
136       }
137
138       @Override
139       public void removeUpdate(DocumentEvent e)
140       {
141         warnIfTypeHidden(mainPanel, name.getText());
142       }
143
144       @Override
145       public void changedUpdate(DocumentEvent e)
146       {
147         warnIfTypeHidden(mainPanel, name.getText());
148       }
149     });
150
151     group = new JTextField(25);
152     group.getDocument().addDocumentListener(new DocumentListener()
153     {
154       @Override
155       public void insertUpdate(DocumentEvent e)
156       {
157         warnIfGroupHidden(mainPanel, group.getText());
158       }
159
160       @Override
161       public void removeUpdate(DocumentEvent e)
162       {
163         warnIfGroupHidden(mainPanel, group.getText());
164       }
165
166       @Override
167       public void changedUpdate(DocumentEvent e)
168       {
169         warnIfGroupHidden(mainPanel, group.getText());
170       }
171     });
172
173     description = new JTextArea(3, 25);
174     
175     start = new JSpinner();
176     end = new JSpinner();
177     start.setPreferredSize(new Dimension(80, 20));
178     end.setPreferredSize(new Dimension(80, 20));
179     
180     /*
181      * ensure that start can never be more than end
182      */
183     start.addChangeListener(new ChangeListener() 
184     {
185       @Override
186       public void stateChanged(ChangeEvent e)
187       {
188         Integer startVal = (Integer) start.getValue(); 
189         ((SpinnerNumberModel) end.getModel()).setMinimum(startVal);
190       }
191     });
192     end.addChangeListener(new ChangeListener() 
193     {
194       @Override
195       public void stateChanged(ChangeEvent e)
196       {
197         Integer endVal = (Integer) end.getValue(); 
198         ((SpinnerNumberModel) start.getModel()).setMaximum(endVal);
199       }
200     });
201
202     final JLabel colour = new JLabel();
203     colour.setOpaque(true);
204     colour.setMaximumSize(new Dimension(30, 16));
205     colour.addMouseListener(new MouseAdapter()
206     {
207       @Override
208       public void mousePressed(MouseEvent evt)
209       {
210         if (featureColour.isSimpleColour())
211         {
212           /*
213            * open colour chooser on click in colour panel
214            */
215           String title = MessageManager
216                   .getString("label.select_feature_colour");
217           ColourChooserListener listener = new ColourChooserListener()
218           {
219             @Override
220             public void colourSelected(Color c)
221             {
222               featureColour = new FeatureColour(c);
223               updateColourButton(mainPanel, colour, featureColour);
224             };
225           };
226           JalviewColourChooser.showColourChooser(Desktop.getDesktop(),
227                   title, featureColour.getColour(), listener);
228         }
229         else
230         {
231           /*
232            * variable colour dialog - on OK, refetch the updated
233            * feature colour and update this display
234            */
235           final String ft = features.get(featureIndex).getType();
236           final String type = ft == null ? lastFeatureAdded : ft;
237           FeatureTypeSettings fcc = new FeatureTypeSettings(fr, type);
238           fcc.setRequestFocusEnabled(true);
239           fcc.requestFocus();
240           fcc.addActionListener(new ActionListener()
241           {
242             @Override
243             public void actionPerformed(ActionEvent e)
244             {
245               featureColour = fr.getFeatureStyle(ft);
246               fr.setColour(type, featureColour);
247               updateColourButton(mainPanel, colour, featureColour);
248             }
249           });
250         }
251       }
252     });
253     JPanel gridPanel = new JPanel(new GridLayout(3, 1));
254
255     if (!forCreate && features.size() > 1)
256     {
257       /*
258        * more than one feature at selected position - 
259        * add a drop-down to choose the feature to amend
260        * space pad text if necessary to make entries distinct
261        */
262       gridPanel = new JPanel(new GridLayout(4, 1));
263       JPanel choosePanel = new JPanel();
264       choosePanel.add(new JLabel(
265               MessageManager.getString("label.select_feature") + ":"));
266       final JComboBox<String> overlaps = new JComboBox<>();
267       List<String> added = new ArrayList<>();
268       for (SequenceFeature sf : features)
269       {
270         String text = String.format("%s/%d-%d (%s)", sf.getType(),
271                 sf.getBegin(), sf.getEnd(), sf.getFeatureGroup());
272         while (added.contains(text))
273         {
274           text += " ";
275         }
276         overlaps.addItem(text);
277         added.add(text);
278       }
279       choosePanel.add(overlaps);
280
281       overlaps.addItemListener(new ItemListener()
282       {
283         @Override
284         public void itemStateChanged(ItemEvent e)
285         {
286           int index = overlaps.getSelectedIndex();
287           if (index != -1)
288           {
289             featureIndex = index;
290             SequenceFeature sf = features.get(index);
291             name.setText(sf.getType());
292             description.setText(sf.getDescription());
293             group.setText(sf.getFeatureGroup());
294             start.setValue(new Integer(sf.getBegin()));
295             end.setValue(new Integer(sf.getEnd()));
296             ((SpinnerNumberModel) start.getModel()).setMaximum(sf.getEnd());
297             ((SpinnerNumberModel) end.getModel()).setMinimum(sf.getBegin());
298
299             SearchResultsI highlight = new SearchResults();
300             highlight.addResult(sequences.get(0), sf.getBegin(),
301                     sf.getEnd());
302
303             ap.getSeqPanel().seqCanvas.highlightSearchResults(highlight);
304           }
305           FeatureColourI col = fr.getFeatureStyle(name.getText());
306           if (col == null)
307           {
308             col = new FeatureColour(
309                     ColorUtils.createColourFromName(name.getText()));
310           }
311           oldColour = featureColour = col;
312           updateColourButton(mainPanel, colour, col);
313         }
314       });
315
316       gridPanel.add(choosePanel);
317     }
318
319     JPanel namePanel = new JPanel();
320     gridPanel.add(namePanel);
321     namePanel.add(new JLabel(MessageManager.getString("label.name:"),
322             JLabel.RIGHT));
323     namePanel.add(name);
324
325     JPanel groupPanel = new JPanel();
326     gridPanel.add(groupPanel);
327     groupPanel.add(new JLabel(MessageManager.getString("label.group:"),
328             JLabel.RIGHT));
329     groupPanel.add(group);
330
331     JPanel colourPanel = new JPanel();
332     gridPanel.add(colourPanel);
333     colourPanel.add(new JLabel(MessageManager.getString("label.colour"),
334             JLabel.RIGHT));
335     colourPanel.add(colour);
336     colour.setPreferredSize(new Dimension(150, 15));
337     colour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 9));
338     colour.setForeground(Color.black);
339     colour.setHorizontalAlignment(SwingConstants.CENTER);
340     colour.setVerticalAlignment(SwingConstants.CENTER);
341     colour.setHorizontalTextPosition(SwingConstants.CENTER);
342     colour.setVerticalTextPosition(SwingConstants.CENTER);
343     mainPanel.add(gridPanel, BorderLayout.NORTH);
344
345     JPanel descriptionPanel = new JPanel();
346     descriptionPanel.add(new JLabel(
347             MessageManager.getString("label.description:"), JLabel.RIGHT));
348     description.setFont(JvSwingUtils.getTextAreaFont());
349     description.setLineWrap(true);
350     descriptionPanel.add(new JScrollPane(description));
351
352     if (!forCreate)
353     {
354       mainPanel.add(descriptionPanel, BorderLayout.SOUTH);
355
356       JPanel startEndPanel = new JPanel();
357       startEndPanel.add(new JLabel(MessageManager.getString("label.start"),
358               JLabel.RIGHT));
359       startEndPanel.add(start);
360       startEndPanel.add(new JLabel(MessageManager.getString("label.end"),
361               JLabel.RIGHT));
362       startEndPanel.add(end);
363       mainPanel.add(startEndPanel, BorderLayout.CENTER);
364     }
365     else
366     {
367       mainPanel.add(descriptionPanel, BorderLayout.CENTER);
368     }
369
370     /*
371      * default feature type and group to that of the first feature supplied,
372      * or to the last feature created if not supplied (null value) 
373      */
374     SequenceFeature firstFeature = features.get(0);
375     boolean useLastDefaults = firstFeature.getType() == null;
376     final String featureType = useLastDefaults ? lastFeatureAdded
377             : firstFeature.getType();
378     final String featureGroup = useLastDefaults ? lastFeatureGroupAdded
379             : firstFeature.getFeatureGroup();
380     name.setText(featureType);
381     group.setText(featureGroup);
382
383     start.setValue(new Integer(firstFeature.getBegin()));
384     end.setValue(new Integer(firstFeature.getEnd()));
385     ((SpinnerNumberModel) start.getModel()).setMaximum(firstFeature.getEnd());
386     ((SpinnerNumberModel) end.getModel()).setMinimum(firstFeature.getBegin());
387
388     description.setText(firstFeature.getDescription());
389     featureColour = fr.getFeatureStyle(featureType);
390     oldColour = featureColour;
391     updateColourButton(mainPanel, colour, oldColour);
392   }
393
394   /**
395    * Presents a dialog allowing the user to add new features, or amend or delete
396    * an existing feature. Currently this can be on
397    * <ul>
398    * <li>double-click on a sequence - Amend/Delete a selected feature at the
399    * position</li>
400    * <li>Create sequence feature(s) from pop-up menu on selected region</li>
401    * <li>Create features for pattern matches from Find</li>
402    * </ul>
403    * If the supplied feature type is null, show (and update on confirm) the type
404    * and group of the last new feature created (with initial defaults of
405    * "feature_1" and "Jalview").
406    */
407   public void showDialog()
408   {
409     RunResponse okAction = forCreate ? getCreateAction() : getAmendAction();
410     RunResponse cancelAction = getCancelAction();
411
412     /*
413      * set dialog action handlers for OK (create/Amend) and Cancel options
414      * also for Delete if applicable (when amending features)
415      */
416     JvOptionPane dialog = JvOptionPane.newOptionDialog(Desktop.desktop)
417             .addResponse(okAction).addResponse(cancelAction);
418     if (!forCreate)
419     {
420       dialog.addResponse(getDeleteAction());
421     }
422
423     String title = null;
424     Object[] options = null;
425     if (forCreate)
426     {
427       title = MessageManager
428               .getString("label.create_new_sequence_features");
429       options = new Object[] { MessageManager.getString("action.ok"),
430           MessageManager.getString("action.cancel") };
431     }
432     else
433     {
434       title = MessageManager.formatMessage("label.amend_delete_features",
435               new String[]
436               { sequences.get(0).getName() });
437       options = new Object[] { MessageManager.getString("label.amend"),
438           MessageManager.getString("action.delete"),
439           MessageManager.getString("action.cancel") };
440     }
441
442     dialog.showInternalDialog(mainPanel, title,
443             JvOptionPane.YES_NO_CANCEL_OPTION,
444             JvOptionPane.PLAIN_MESSAGE, null, options,
445             MessageManager.getString("action.ok"));
446   }
447
448   /**
449    * Answers an action to run on Cancel in the dialog. This is just to remove
450    * any feature highlighting from the display. Changes in the dialog are not
451    * applied until it is dismissed with OK, Amend or Delete, so there are no
452    * updates to reset on Cancel.
453    * 
454    * @return
455    */
456   protected RunResponse getCancelAction()
457   {
458     RunResponse okAction = new RunResponse(JvOptionPane.CANCEL_OPTION)
459     {
460       @Override
461       public void run()
462       {
463         ap.highlightSearchResults(null);
464         ap.paintAlignment(false, false);
465       }
466     };
467     return okAction;
468   }
469
470   /**
471    * Returns the action to be run on OK in the dialog when creating one or more
472    * sequence features. Note these may have a pre-supplied feature type (such as
473    * a Find pattern), or none, in which case the feature type and group default
474    * to those last added through this dialog. The action includes refreshing the
475    * Feature Settings panel (if it is open), to show any new feature type, or
476    * amended colour for an existing type.
477    * 
478    * @return
479    */
480   protected RunResponse getCreateAction()
481   {
482     RunResponse okAction = new RunResponse(JvOptionPane.OK_OPTION)
483     {
484       boolean useLastDefaults = features.get(0).getType() == null;
485
486       public void run()
487       {
488         final String enteredType = name.getText().trim();
489         final String enteredGroup = group.getText().trim();
490         final String enteredDescription = description.getText()
491                 .replaceAll("\n", " ");
492         if (enteredType.length() > 0)
493         {
494           /*
495            * update default values only if creating using default values
496            */
497           if (useLastDefaults)
498           {
499             lastFeatureAdded = enteredType;
500             lastFeatureGroupAdded = enteredGroup;
501             // TODO: determine if the null feature group is valid
502             if (lastFeatureGroupAdded.length() < 1)
503             {
504               lastFeatureGroupAdded = null;
505             }
506           }
507         }
508
509         if (enteredType.length() > 0)
510         {
511           for (int i = 0; i < sequences.size(); i++)
512           {
513             SequenceFeature sf = features.get(i);
514             SequenceFeature sf2 = new SequenceFeature(enteredType,
515                     enteredDescription, sf.getBegin(), sf.getEnd(),
516                     enteredGroup);
517             new FeaturesFile().parseDescriptionHTML(sf2, false);
518             sequences.get(i).addSequenceFeature(sf2);
519           }
520
521           fr.setColour(enteredType, featureColour);
522           fr.featuresAdded();
523
524           repaintPanel();
525         }
526       }
527     };
528     return okAction;
529   }
530
531   /**
532    * Answers the action to run on Delete in the dialog. Note this includes
533    * refreshing the Feature Settings (if open) in case the only instance of a
534    * feature type or group has been deleted.
535    * 
536    * @return
537    */
538   protected RunResponse getDeleteAction()
539   {
540     RunResponse deleteAction = new RunResponse(JvOptionPane.NO_OPTION)
541     {
542       public void run()
543       {
544         SequenceFeature sf = features.get(featureIndex);
545         sequences.get(0).getDatasetSequence().deleteFeature(sf);
546         fr.featuresAdded();
547         ap.getSeqPanel().seqCanvas.highlightSearchResults(null);
548         ap.paintAlignment(true, true);
549       }
550     };
551     return deleteAction;
552   }
553
554   /**
555    * update the amend feature button dependent on the given style
556    * 
557    * @param bigPanel
558    * @param col
559    * @param col
560    */
561   protected void updateColourButton(JPanel bigPanel, JLabel colour,
562           FeatureColourI col)
563   {
564     colour.removeAll();
565     colour.setIcon(null);
566     colour.setText("");
567
568     if (col.isSimpleColour())
569     {
570       colour.setToolTipText(null);
571       colour.setBackground(col.getColour());
572     }
573     else
574     {
575       colour.setBackground(bigPanel.getBackground());
576       colour.setForeground(Color.black);
577       colour.setToolTipText(FeatureSettings.getColorTooltip(col, false));
578       FeatureSettings.renderGraduatedColor(colour, col);
579     }
580   }
581
582   /**
583    * Show a warning message if the entered group is one that is currently hidden
584    * 
585    * @param panel
586    * @param group
587    */
588   protected void warnIfGroupHidden(JPanel panel, String group)
589   {
590     if (!fr.isGroupVisible(group))
591     {
592       String msg = MessageManager.formatMessage("label.warning_hidden",
593               MessageManager.getString("label.group"), group);
594       JvOptionPane.showMessageDialog(panel, msg, "",
595               JvOptionPane.OK_OPTION);
596     }
597   }
598
599   /**
600    * Show a warning message if the entered type is one that is currently hidden
601    * 
602    * @param panel
603    * @param type
604    */
605   protected void warnIfTypeHidden(JPanel panel, String type)
606   {
607     if (fr.getRenderOrder().contains(type))
608     {
609       if (!fr.showFeatureOfType(type))
610       {
611         String msg = MessageManager.formatMessage("label.warning_hidden",
612                 MessageManager.getString("label.feature_type"), type);
613         JvOptionPane.showMessageDialog(panel, msg, "",
614                 JvOptionPane.OK_OPTION);
615       }
616     }
617   }
618
619   /**
620    * On closing the dialog - ensure feature display is turned on, to show any
621    * new features - remove highlighting of the last selected feature - repaint
622    * the panel to show any changes
623    */
624   protected void repaintPanel()
625   {
626     ap.alignFrame.showSeqFeatures.setSelected(true);
627     ap.av.setShowSequenceFeatures(true);
628     ap.av.setSearchResults(null);
629     ap.paintAlignment(true, true);
630   }
631
632   /**
633    * Returns the action to be run on OK in the dialog when amending a feature.
634    * Note this may include refreshing the Feature Settings panel (if it is
635    * open), if feature type, group or colour has changed (but not for
636    * description or extent).
637    * 
638    * @return
639    */
640   protected RunResponse getAmendAction()
641   {
642     RunResponse okAction = new RunResponse(JvOptionPane.OK_OPTION)
643     {
644       boolean useLastDefaults = features.get(0).getType() == null;
645   
646       String featureType = name.getText();
647   
648       String featureGroup = group.getText();
649   
650       public void run()
651       {
652         final String enteredType = name.getText().trim();
653         final String enteredGroup = group.getText().trim();
654         final String enteredDescription = description.getText()
655                 .replaceAll("\n", " ");
656         if (enteredType.length() > 0)
657
658         {
659           /*
660            * update default values only if creating using default values
661            */
662           if (useLastDefaults)
663           {
664             lastFeatureAdded = enteredType;
665             lastFeatureGroupAdded = enteredGroup;
666             // TODO: determine if the null feature group is valid
667             if (lastFeatureGroupAdded.length() < 1)
668             {
669               lastFeatureGroupAdded = null;
670             }
671           }
672         }
673
674         SequenceFeature sf = features.get(featureIndex);
675
676         /*
677          * Need to refresh Feature Settings if type, group or colour changed;
678          * note we don't force the feature to be visible - the user has been
679          * warned if a hidden feature type or group was entered
680          */
681         boolean refreshSettings = (!featureType.equals(enteredType)
682                 || !featureGroup.equals(enteredGroup));
683         refreshSettings |= (featureColour != oldColour);
684         fr.setColour(enteredType, featureColour);
685         int newBegin = sf.begin;
686         int newEnd = sf.end;
687         try
688         {
689           newBegin = ((Integer) start.getValue()).intValue();
690           newEnd = ((Integer) end.getValue()).intValue();
691         } catch (NumberFormatException ex)
692         {
693           // JSpinner doesn't accept invalid format data :-)
694         }
695
696         /*
697          * 'amend' the feature by deleting it and adding a new one
698          * (to ensure integrity of SequenceFeatures data store)
699          * note this dialog only updates one sequence at a time
700          */
701         sequences.get(0).deleteFeature(sf);
702         SequenceFeature newSf = new SequenceFeature(sf, enteredType,
703                 newBegin, newEnd, enteredGroup, sf.getScore());
704         newSf.setDescription(enteredDescription);
705         new FeaturesFile().parseDescriptionHTML(newSf, false);
706         sequences.get(0).addSequenceFeature(newSf);
707
708         if (refreshSettings)
709         {
710           fr.featuresAdded();
711         }
712         repaintPanel();
713       }
714     };
715     return okAction;
716   }
717
718 }