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