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