JAL-1863 allow RNA secondary structure rows exported by Jalview to be imported again
[jalview.git] / src / jalview / gui / AnnotationChooser.java
1 package jalview.gui;
2
3 import jalview.datamodel.AlignmentAnnotation;
4 import jalview.datamodel.AlignmentI;
5 import jalview.datamodel.SequenceGroup;
6 import jalview.util.MessageManager;
7
8 import java.awt.BorderLayout;
9 import java.awt.Checkbox;
10 import java.awt.CheckboxGroup;
11 import java.awt.FlowLayout;
12 import java.awt.Font;
13 import java.awt.GridLayout;
14 import java.awt.event.ActionEvent;
15 import java.awt.event.ActionListener;
16 import java.awt.event.ItemEvent;
17 import java.awt.event.ItemListener;
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22
23 import javax.swing.JButton;
24 import javax.swing.JInternalFrame;
25 import javax.swing.JLayeredPane;
26 import javax.swing.JPanel;
27
28 /**
29  * A panel that allows the user to select which sequence-associated annotation
30  * rows to show or hide.
31  * 
32  * @author gmcarstairs
33  *
34  */
35 @SuppressWarnings("serial")
36 public class AnnotationChooser extends JPanel
37 {
38
39   private static final Font CHECKBOX_FONT = new Font("Serif", Font.BOLD, 12);
40
41   private static final int MY_FRAME_WIDTH = 600;
42
43   private static final int MY_FRAME_HEIGHT = 250;
44
45   private JInternalFrame frame;
46
47   private AlignmentPanel ap;
48
49   private SequenceGroup sg;
50
51   // all annotation rows' original visible state
52   private boolean[] resetState = null;
53
54   // is 'Show' selected?
55   private boolean showSelected;
56
57   // apply settings to selected (or all) sequences?
58   private boolean applyToSelectedSequences;
59
60   // apply settings to unselected (or all) sequences?
61   private boolean applyToUnselectedSequences;
62
63   // currently selected 'annotation type' checkboxes
64   private Map<String, String> selectedTypes = new HashMap<String, String>();
65   
66   /**
67    * Constructor.
68    * 
69    * @param alignPane
70    */
71   public AnnotationChooser(AlignmentPanel alignPane)
72   {
73     super();
74     this.ap = alignPane;
75     this.sg = alignPane.av.getSelectionGroup();
76     saveResetState(alignPane.getAlignment());
77
78     try
79     {
80       jbInit();
81     } catch (Exception ex)
82     {
83       ex.printStackTrace();
84     }
85     showFrame();
86   }
87
88   /**
89    * Save the initial show/hide state of all annotations to allow a Cancel
90    * operation.
91    * 
92    * @param alignment
93    */
94   protected void saveResetState(AlignmentI alignment)
95   {
96     AlignmentAnnotation[] annotations = alignment.getAlignmentAnnotation();
97     final int count = annotations.length;
98     this.resetState = new boolean[count];
99     for (int i = 0; i < count; i++)
100     {
101       this.resetState[i] = annotations[i].visible;
102     }
103   }
104
105   /**
106    * Populate this frame with:
107    * <p>
108    * checkboxes for the types of annotation to show or hide (i.e. any annotation
109    * type shown for any sequence in the whole alignment)
110    * <p>
111    * option to show or hide selected types
112    * <p>
113    * option to show/hide for the currently selected group, or its inverse
114    * <p>
115    * OK and Cancel (reset) buttons
116    */
117   protected void jbInit()
118   {
119     setLayout(new GridLayout(3, 1));
120     add(buildAnnotationTypesPanel());
121     add(buildShowHideOptionsPanel());
122     add(buildActionButtonsPanel());
123     validate();
124   }
125
126   /**
127    * Construct the panel with checkboxes for annotation types.
128    * 
129    * @return
130    */
131   protected JPanel buildAnnotationTypesPanel()
132   {
133     JPanel jp = new JPanel(new FlowLayout(FlowLayout.LEFT));
134
135     List<String> annotationTypes = getAnnotationTypes(
136             this.ap.getAlignment(), true);
137
138     for (final String type : annotationTypes)
139     {
140       final Checkbox check = new Checkbox(type);
141       check.setFont(CHECKBOX_FONT);
142       check.addItemListener(new ItemListener()
143       {
144         @Override
145         public void itemStateChanged(ItemEvent evt)
146         {
147           if (evt.getStateChange() == ItemEvent.SELECTED)
148           {
149             AnnotationChooser.this.selectedTypes.put(type, type);
150           }
151           else
152           {
153             AnnotationChooser.this.selectedTypes.remove(type);
154           }
155           changeTypeSelected_actionPerformed(type);
156         }
157       });
158       jp.add(check);
159     }
160     return jp;
161   }
162
163   /**
164    * Update display when scope (All/Selected sequences/Unselected) is changed.
165    * <p>
166    * Set annotations (with one of the selected types) to the selected Show/Hide
167    * visibility, if they are in the new application scope. Set to the opposite
168    * if outside the scope.
169    * <p>
170    * Note this only affects sequence-specific annotations, others are left
171    * unchanged.
172    */
173   protected void changeApplyTo_actionPerformed()
174   {
175     setAnnotationVisibility(true);
176
177     // copied from AnnotationLabel.actionPerformed (after show/hide row)...
178     // TODO should drive this functionality into AlignmentPanel
179     ap.updateAnnotation();
180     // this.ap.annotationPanel.adjustPanelHeight();
181     // this.ap.alabels.setSize(this.ap.alabels.getSize().width,
182     // this.ap.annotationPanel.getSize().height);
183     // this.ap.validate();
184     this.ap.paintAlignment(true);
185   }
186
187   /**
188    * Update display when an annotation type is selected or deselected.
189    * <p>
190    * If the type is selected, set visibility of annotations of that type which
191    * are in the application scope (all, selected or unselected sequences).
192    * <p>
193    * If the type is unselected, set visibility to the opposite value. That is,
194    * treat select/deselect as a 'toggle' operation.
195    * 
196    * @param type
197    */
198   protected void changeTypeSelected_actionPerformed(String type)
199   {
200     boolean typeSelected = this.selectedTypes.containsKey(type);
201     for (AlignmentAnnotation aa : this.ap.getAlignment()
202             .getAlignmentAnnotation())
203     {
204       if (aa.sequenceRef != null && type.equals(aa.label)
205               && isInActionScope(aa))
206       {
207         aa.visible = typeSelected ? this.showSelected : !this.showSelected;
208       }
209     }
210     ap.updateAnnotation();
211     // // this.ap.annotationPanel.adjustPanelHeight();
212     // this.ap.alabels.setSize(this.ap.alabels.getSize().width,
213     // this.ap.annotationPanel.getSize().height);
214     // this.ap.validate();
215     this.ap.paintAlignment(true);
216   }
217
218   /**
219    * Update display on change of choice of Show or Hide
220    * <p>
221    * For annotations of any selected type, set visibility of annotations of that
222    * type which are in the application scope (all, selected or unselected
223    * sequences).
224    * 
225    * @param type
226    */
227   protected void changeShowHide_actionPerformed()
228   {
229     setAnnotationVisibility(false);
230
231     this.ap.updateAnnotation();
232     // this.ap.annotationPanel.adjustPanelHeight();
233     this.ap.paintAlignment(true);
234   }
235
236   /**
237    * Update visibility flags on annotation rows as per the current user choices.
238    * 
239    * @param updateAllRows
240    */
241   protected void setAnnotationVisibility(boolean updateAllRows)
242   {
243     for (AlignmentAnnotation aa : this.ap.getAlignment()
244             .getAlignmentAnnotation())
245     {
246       if (aa.sequenceRef != null)
247       {
248         setAnnotationVisibility(aa, updateAllRows);
249       }
250     }
251   }
252
253   /**
254    * Determine and set the visibility of the given annotation from the currently
255    * selected options.
256    * <p>
257    * Only update annotations whose type is one of the selected types.
258    * <p>
259    * If its sequence is in the selected application scope
260    * (all/selected/unselected sequences), then we set its visibility according
261    * to the current choice of Show or Hide.
262    * <p>
263    * If force update of all rows is wanted, then set rows not in the sequence
264    * selection scope to the opposite visibility to those in scope.
265    * 
266    * @param aa
267    * @param updateAllRows
268    */
269   protected void setAnnotationVisibility(AlignmentAnnotation aa,
270           boolean updateAllRows)
271   {
272     if (this.selectedTypes.containsKey(aa.label))
273     {
274       if (isInActionScope(aa))
275       {
276         aa.visible = this.showSelected;
277       }
278       else if (updateAllRows)
279       {
280         aa.visible = !this.showSelected;
281       }
282     }
283     // TODO force not visible if associated sequence is hidden?
284     // currently hiding a sequence does not hide its annotation rows
285   }
286
287   /**
288    * Answers true if the annotation falls in the current selection criteria for
289    * show/hide.
290    * <p>
291    * It must be in the sequence selection group (for 'Apply to selection'), or
292    * not in it (for 'Apply except to selection'). No check needed for 'Apply to
293    * all'.
294    * 
295    * @param aa
296    * @return
297    */
298   protected boolean isInActionScope(AlignmentAnnotation aa)
299   {
300     boolean result = false;
301     if (this.applyToSelectedSequences && this.applyToUnselectedSequences)
302     {
303       // we don't care if the annotation's sequence is selected or not
304       result = true;
305     }
306     else if (this.sg == null)
307     {
308       // shouldn't happen - defensive programming
309       result = true;
310     }
311     else if (this.sg.getSequences().contains(aa.sequenceRef))
312     {
313       // annotation is for a member of the selection group
314       result = this.applyToSelectedSequences ? true : false;
315     }
316     else
317     {
318       // annotation is not associated with the selection group
319       result = this.applyToUnselectedSequences ? true : false;
320     }
321     return result;
322   }
323
324   /**
325    * Get annotation 'types' for an alignment, optionally restricted to
326    * sequence-specific annotations only. The label is currently used for 'type'.
327    * 
328    * TODO refactor to helper class. See
329    * AnnotationColourChooser.getAnnotationItems() for another client
330    * 
331    * @param alignment
332    * @param sequenceSpecific
333    * @return
334    */
335   public static List<String> getAnnotationTypes(AlignmentI alignment,
336           boolean sequenceSpecificOnly)
337   {
338     List<String> result = new ArrayList<String>();
339     for (AlignmentAnnotation aa : alignment.getAlignmentAnnotation())
340     {
341       if (!sequenceSpecificOnly || aa.sequenceRef != null)
342       {
343         String label = aa.label;
344         if (!result.contains(label))
345         {
346           result.add(label);
347         }
348       }
349     }
350     return result;
351   }
352
353   /**
354    * Construct the panel with options to:
355    * <p>
356    * show or hide the selected annotation types
357    * <p>
358    * do this for the current selection group or its inverse
359    * 
360    * @return
361    */
362   protected JPanel buildShowHideOptionsPanel()
363   {
364     JPanel jp = new JPanel();
365     jp.setLayout(new BorderLayout());
366
367     JPanel showHideOptions = buildShowHidePanel();
368     jp.add(showHideOptions, BorderLayout.CENTER);
369
370     JPanel applyToOptions = buildApplyToOptionsPanel();
371     jp.add(applyToOptions, BorderLayout.SOUTH);
372
373     return jp;
374   }
375
376   /**
377    * Build a panel with radio buttons options for sequences to apply show/hide
378    * to. Options are all, current selection, all except current selection.
379    * Initial state has 'current selection' selected.
380    * <p>
381    * If the sequence group is null, then we are acting on the whole alignment,
382    * and only 'all sequences' is enabled (and selected).
383    * 
384    * @return
385    */
386   protected JPanel buildApplyToOptionsPanel()
387   {
388     final boolean wholeAlignment = this.sg == null;
389     JPanel applyToOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
390     CheckboxGroup actingOn = new CheckboxGroup();
391     
392     String forAll = MessageManager.getString("label.all_sequences");
393     final Checkbox allSequences = new Checkbox(forAll, actingOn,
394             wholeAlignment);
395     allSequences.addItemListener(new ItemListener()
396     {
397       @Override
398       public void itemStateChanged(ItemEvent evt)
399       {
400         if (evt.getStateChange() == ItemEvent.SELECTED) {
401           AnnotationChooser.this.setApplyToSelectedSequences(true);
402           AnnotationChooser.this.setApplyToUnselectedSequences(true);
403           AnnotationChooser.this.changeApplyTo_actionPerformed();
404         }
405       }
406     });
407     applyToOptions.add(allSequences);
408     
409     String forSelected = MessageManager
410             .getString("label.selected_sequences");
411     final Checkbox selectedSequences = new Checkbox(forSelected, actingOn,
412             !wholeAlignment);
413     selectedSequences.setEnabled(!wholeAlignment);
414     selectedSequences.addItemListener(new ItemListener()
415     {
416       @Override
417       public void itemStateChanged(ItemEvent evt)
418       {
419         if (evt.getStateChange() == ItemEvent.SELECTED)
420         {
421           AnnotationChooser.this.setApplyToSelectedSequences(true);
422           AnnotationChooser.this.setApplyToUnselectedSequences(false);
423           AnnotationChooser.this.changeApplyTo_actionPerformed();
424         }
425       }
426     });
427     applyToOptions.add(selectedSequences);
428     
429     String exceptSelected = MessageManager
430             .getString("label.except_selected_sequences");
431     final Checkbox unselectedSequences = new Checkbox(exceptSelected, actingOn, false);
432     unselectedSequences.setEnabled(!wholeAlignment);
433     unselectedSequences.addItemListener(new ItemListener()
434     {
435       @Override
436       public void itemStateChanged(ItemEvent evt)
437       {
438         if (evt.getStateChange() == ItemEvent.SELECTED)
439         {
440           AnnotationChooser.this.setApplyToSelectedSequences(false);
441           AnnotationChooser.this.setApplyToUnselectedSequences(true);
442           AnnotationChooser.this.changeApplyTo_actionPerformed();
443         }
444       }
445     });
446     applyToOptions.add(unselectedSequences);
447     
448     // set member variables to match the initial selection state
449     this.applyToSelectedSequences = selectedSequences.getState()
450             || allSequences.getState();
451     this.applyToUnselectedSequences = unselectedSequences.getState()
452             || allSequences.getState();
453
454     return applyToOptions;
455   }
456
457   /**
458    * Build a panel with radio button options to show or hide selected
459    * annotations.
460    * 
461    * @return
462    */
463   protected JPanel buildShowHidePanel()
464   {
465     JPanel showHideOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
466     CheckboxGroup showOrHide = new CheckboxGroup();
467
468     /*
469      * Radio button 'Show selected annotations' - initially unselected
470      */
471     String showLabel = MessageManager
472             .getString("label.show_selected_annotations");
473     final Checkbox showOption = new Checkbox(showLabel, showOrHide, false);
474     showOption.addItemListener(new ItemListener()
475     {
476       @Override
477       public void itemStateChanged(ItemEvent evt)
478       {
479         if (evt.getStateChange() == ItemEvent.SELECTED) {
480           AnnotationChooser.this.setShowSelected(true);
481           AnnotationChooser.this.changeShowHide_actionPerformed();
482         }
483       }
484     });
485     showHideOptions.add(showOption);
486
487     /*
488      * Radio button 'hide selected annotations'- initially selected
489      */
490     String hideLabel = MessageManager
491             .getString("label.hide_selected_annotations");
492     final Checkbox hideOption = new Checkbox(hideLabel, showOrHide, true);
493     hideOption.addItemListener(new ItemListener()
494     {
495       @Override
496       public void itemStateChanged(ItemEvent evt)
497       {
498         if (evt.getStateChange() == ItemEvent.SELECTED)
499         {
500           AnnotationChooser.this.setShowSelected(false);
501           AnnotationChooser.this.changeShowHide_actionPerformed();
502         }
503       }
504     });
505     showHideOptions.add(hideOption);
506
507     /*
508      * Set member variable to match initial selection state
509      */
510     this.showSelected = showOption.getState();
511
512     return showHideOptions;
513   }
514
515   /**
516    * Construct the panel with OK and Cancel buttons.
517    * 
518    * @return
519    */
520   protected JPanel buildActionButtonsPanel()
521   {
522     JPanel jp = new JPanel();
523     final Font labelFont = JvSwingUtils.getLabelFont();
524     
525     JButton ok = new JButton(MessageManager.getString("action.ok"));
526     ok.setFont(labelFont);
527     ok.addActionListener(new ActionListener()
528     {
529       @Override
530       public void actionPerformed(ActionEvent e)
531       {
532         close_actionPerformed();
533       }
534     });
535     jp.add(ok);
536     
537     JButton cancel = new JButton(MessageManager.getString("action.cancel"));
538     cancel.setFont(labelFont);
539     cancel.addActionListener(new ActionListener()
540     {
541       @Override
542       public void actionPerformed(ActionEvent e)
543       {
544         cancel_actionPerformed();
545       }
546     });
547     jp.add(cancel);
548
549     return jp;
550   }
551
552   /**
553    * On 'Cancel' button, undo any changes.
554    */
555   protected void cancel_actionPerformed()
556   {
557     resetOriginalState();
558     this.ap.repaint();
559     close_actionPerformed();
560   }
561
562   /**
563    * Restore annotation visibility to their state on entry here, and repaint
564    * alignment.
565    */
566   protected void resetOriginalState()
567   {
568     int i = 0;
569     for (AlignmentAnnotation aa : this.ap.getAlignment()
570             .getAlignmentAnnotation())
571     {
572       aa.visible = this.resetState[i++];
573     }
574   }
575
576   /**
577    * On 'Close' button, close the dialog.
578    */
579   protected void close_actionPerformed()
580   {
581     try
582     {
583       this.frame.setClosed(true);
584     } catch (Exception exe)
585     {
586     }
587   }
588
589   /**
590    * Render a frame containing this panel.
591    */
592   private void showFrame()
593   {
594     frame = new JInternalFrame();
595     frame.setContentPane(this);
596     frame.setLayer(JLayeredPane.PALETTE_LAYER);
597     Desktop.addInternalFrame(frame,
598             MessageManager.getString("label.choose_annotations"),
599             MY_FRAME_WIDTH, MY_FRAME_HEIGHT, true);
600   }
601
602   protected void setShowSelected(boolean showSelected)
603   {
604     this.showSelected = showSelected;
605   }
606
607   protected void setApplyToSelectedSequences(
608           boolean applyToSelectedSequences)
609   {
610     this.applyToSelectedSequences = applyToSelectedSequences;
611   }
612
613   protected void setApplyToUnselectedSequences(
614           boolean applyToUnselectedSequences)
615   {
616     this.applyToUnselectedSequences = applyToUnselectedSequences;
617   }
618
619   protected boolean isShowSelected()
620   {
621     return showSelected;
622   }
623
624   protected boolean isApplyToSelectedSequences()
625   {
626     return applyToSelectedSequences;
627   }
628
629   protected boolean isApplyToUnselectedSequences()
630   {
631     return applyToUnselectedSequences;
632   }
633
634 }