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