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