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