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