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