bb940e8e5544ba0f7fb0420acaca5ece8ed325f6
[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.JCheckBox;
25 import javax.swing.JInternalFrame;
26 import javax.swing.JLayeredPane;
27 import javax.swing.JPanel;
28
29 /**
30  * A panel that allows the user to select which sequence-associated annotation
31  * rows to show or hide.
32  * 
33  * @author gmcarstairs
34  *
35  */
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 JCheckBox check = new JCheckBox(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           repaintAnnotations(false);
156         }
157       });
158       jp.add(check);
159     }
160     return jp;
161   }
162
163   /**
164    * Set visibility flags on annotation rows then repaint the alignment panel.
165    * <p>
166    * Optionally, update all rows, including those not in the 'apply to' scope.
167    * This makes more sense when switching between selected and unselected
168    * sequences. When selecting annotation types, or show/hide, we only apply the
169    * settings to the selected sequences.
170    * <p>
171    * Note this only affects sequence-specific annotations, others are left
172    * unchanged.
173    */
174   protected void repaintAnnotations(boolean updateAllRows)
175   {
176     for (AlignmentAnnotation aa : this.ap.getAlignment()
177             .getAlignmentAnnotation())
178     {
179       if (aa.sequenceRef != null)
180       {
181         setAnnotationVisibility(aa, updateAllRows);
182       }
183     }
184     // copied from AnnotationLabel.actionPerformed (after show/hide row)...
185     // TODO should drive this functionality into AlignmentPanel
186     ap.updateAnnotation();
187     this.ap.annotationPanel.adjustPanelHeight();
188     this.ap.alabels.setSize(this.ap.alabels.getSize().width,
189             this.ap.annotationPanel.getSize().height);
190     this.ap.validate();
191     this.ap.paintAlignment(true);
192   }
193
194   /**
195    * Determine and set the visibility of the given annotation from the currently
196    * selected options.
197    * <p>
198    * If its sequence is in the selected application scope
199    * (all/selected/unselected sequences), then we set its visibility.
200    * <p>
201    * If the annotation type is one of those currently selected by checkbox, set
202    * its visibility to the selected value. If it is not currently selected, set
203    * it to the opposite value. So, unselecting an annotation type with 'hide'
204    * selected, will cause those annotations to be unhidden.
205    * <p>
206    * If force update of all rows is wanted, then set rows not in the sequence
207    * selection scope to the opposite visibility to those in scope.
208    * 
209    * @param aa
210    * @param updateAllRows
211    */
212   protected void setAnnotationVisibility(AlignmentAnnotation aa,
213           boolean updateAllRows)
214   {
215     boolean setToVisible = false;
216     if (this.selectedTypes.containsKey(aa.label))
217     {
218       setToVisible = this.showSelected;
219     }
220     else
221     {
222       setToVisible = !this.showSelected;
223     }
224     if (isInActionScope(aa))
225     {
226       aa.visible = setToVisible;
227     }
228     else if (updateAllRows)
229     {
230       aa.visible = !setToVisible;
231     }
232     // TODO force not visible if associated sequence is hidden?
233     // currently hiding a sequence does not hide its annotation rows
234   }
235
236   /**
237    * Answers true if the annotation falls in the current selection criteria for
238    * show/hide.
239    * <p>
240    * It must be in the sequence selection group (for 'Apply to selection'), or
241    * not in it (for 'Apply except to selection'). No check needed for 'Apply to
242    * all'.
243    * 
244    * @param aa
245    * @return
246    */
247   protected boolean isInActionScope(AlignmentAnnotation aa)
248   {
249     boolean result = false;
250     if (this.applyToSelectedSequences && this.applyToUnselectedSequences)
251     {
252       // we don't care if the annotation's sequence is selected or not
253       result = true;
254     }
255     else if (this.sg.getSequences().contains(aa.sequenceRef))
256     {
257       // annotation is for a member of the selection group
258       result = this.applyToSelectedSequences ? true : false;
259     }
260     else
261     {
262       // annotation is not associated with the selection group
263       result = this.applyToUnselectedSequences ? true : false;
264     }
265     return result;
266   }
267
268   /**
269    * Get annotation 'types' for an alignment, optionally restricted to
270    * sequence-specific annotations only. The label is currently used for 'type'.
271    * 
272    * TODO refactor to helper class. See
273    * AnnotationColourChooser.getAnnotationItems() for another client
274    * 
275    * @param alignment
276    * @param sequenceSpecific
277    * @return
278    */
279   public static List<String> getAnnotationTypes(AlignmentI alignment,
280           boolean sequenceSpecificOnly)
281   {
282     // stub
283     List<String> result = new ArrayList<String>();
284     for (AlignmentAnnotation aa : alignment.getAlignmentAnnotation())
285     {
286       if (!sequenceSpecificOnly || aa.sequenceRef != null)
287       {
288         String label = aa.label;
289         if (!result.contains(label))
290         {
291           result.add(label);
292         }
293       }
294     }
295     return result;
296   }
297
298   /**
299    * Construct the panel with options to:
300    * <p>
301    * show or hide the selected annotation types
302    * <p>
303    * do this for the current selection group or its inverse
304    * 
305    * @return
306    */
307   protected JPanel buildShowHideOptionsPanel()
308   {
309     JPanel jp = new JPanel();
310     jp.setLayout(new BorderLayout());
311
312     JPanel showHideOptions = buildShowHidePanel();
313     jp.add(showHideOptions, BorderLayout.CENTER);
314
315     JPanel applyToOptions = buildApplyToOptionsPanel();
316     jp.add(applyToOptions, BorderLayout.SOUTH);
317
318     return jp;
319   }
320
321   /**
322    * Build a panel with radio buttons options for sequences to apply show/hide
323    * to. Options are all, current selection, all except current selection.
324    * Initial state has 'current selection' selected.
325    * <p>
326    * If the sequence group is null, then we are acting on the whole alignment,
327    * and only 'all sequences' is enabled (and selected).
328    * 
329    * @return
330    */
331   protected JPanel buildApplyToOptionsPanel()
332   {
333     final boolean wholeAlignment = this.sg == null;
334     JPanel applyToOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
335     CheckboxGroup actingOn = new CheckboxGroup();
336     
337     String forAll = MessageManager.getString("label.all_sequences");
338     final Checkbox allSequences = new Checkbox(forAll, actingOn,
339             wholeAlignment);
340     allSequences.addItemListener(new ItemListener()
341     {
342       @Override
343       public void itemStateChanged(ItemEvent evt)
344       {
345         if (evt.getStateChange() == ItemEvent.SELECTED) {
346           AnnotationChooser.this.setApplyToSelectedSequences(true);
347           AnnotationChooser.this.setApplyToUnselectedSequences(true);
348           AnnotationChooser.this.repaintAnnotations(true);
349         }
350       }
351     });
352     applyToOptions.add(allSequences);
353     
354     String forSelected = MessageManager
355             .getString("label.selected_sequences");
356     final Checkbox selectedSequences = new Checkbox(forSelected, actingOn,
357             !wholeAlignment);
358     selectedSequences.setEnabled(!wholeAlignment);
359     selectedSequences.addItemListener(new ItemListener()
360     {
361       @Override
362       public void itemStateChanged(ItemEvent evt)
363       {
364         if (evt.getStateChange() == ItemEvent.SELECTED)
365         {
366           AnnotationChooser.this.setApplyToSelectedSequences(true);
367           AnnotationChooser.this.setApplyToUnselectedSequences(false);
368           AnnotationChooser.this.repaintAnnotations(true);
369         }
370       }
371     });
372     applyToOptions.add(selectedSequences);
373     
374     String exceptSelected = MessageManager
375             .getString("label.except_selected_sequences");
376     final Checkbox unselectedSequences = new Checkbox(exceptSelected, actingOn, false);
377     unselectedSequences.setEnabled(!wholeAlignment);
378     unselectedSequences.addItemListener(new ItemListener()
379     {
380       @Override
381       public void itemStateChanged(ItemEvent evt)
382       {
383         if (evt.getStateChange() == ItemEvent.SELECTED)
384         {
385           AnnotationChooser.this.setApplyToSelectedSequences(false);
386           AnnotationChooser.this.setApplyToUnselectedSequences(true);
387           AnnotationChooser.this.repaintAnnotations(true);
388         }
389       }
390     });
391     applyToOptions.add(unselectedSequences);
392     
393     // set member variables to match the initial selection state
394     this.applyToSelectedSequences = selectedSequences.getState()
395             || allSequences.getState();
396     this.applyToUnselectedSequences = unselectedSequences.getState()
397             || allSequences.getState();
398
399     return applyToOptions;
400   }
401
402   /**
403    * Build a panel with radio buttons options to show or hide selected
404    * annotations.
405    * 
406    * @return
407    */
408   protected JPanel buildShowHidePanel()
409   {
410     JPanel showHideOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
411     CheckboxGroup showOrHide = new CheckboxGroup();
412
413     /*
414      * Radio button 'Show selected annotations' - initially unselected
415      */
416     String showLabel = MessageManager
417             .getString("label.show_selected_annotations");
418     final Checkbox showOption = new Checkbox(showLabel, showOrHide, false);
419     showOption.addItemListener(new ItemListener()
420     {
421       @Override
422       public void itemStateChanged(ItemEvent evt)
423       {
424         if (evt.getStateChange() == ItemEvent.SELECTED) {
425           AnnotationChooser.this.setShowSelected(true);
426           AnnotationChooser.this.repaintAnnotations(false);
427         }
428       }
429     });
430     showHideOptions.add(showOption);
431
432     /*
433      * Radio button 'hide selected annotations'- initially selected
434      */
435     String hideLabel = MessageManager
436             .getString("label.hide_selected_annotations");
437     final Checkbox hideOption = new Checkbox(hideLabel, showOrHide, true);
438     hideOption.addItemListener(new ItemListener()
439     {
440       @Override
441       public void itemStateChanged(ItemEvent evt)
442       {
443         if (evt.getStateChange() == ItemEvent.SELECTED)
444         {
445           AnnotationChooser.this.setShowSelected(false);
446           AnnotationChooser.this.repaintAnnotations(false);
447         }
448       }
449     });
450     showHideOptions.add(hideOption);
451
452     /*
453      * Set member variable to match initial selection state
454      */
455     this.showSelected = showOption.getState();
456
457     return showHideOptions;
458   }
459
460   /**
461    * Construct the panel with OK and Cancel buttons.
462    * 
463    * @return
464    */
465   protected JPanel buildActionButtonsPanel()
466   {
467     JPanel jp = new JPanel();
468     final Font labelFont = JvSwingUtils.getLabelFont();
469     
470     JButton ok = new JButton(MessageManager.getString("action.ok"));
471     ok.setFont(labelFont);
472     ok.addActionListener(new ActionListener()
473     {
474       @Override
475       public void actionPerformed(ActionEvent e)
476       {
477         close_actionPerformed();
478       }
479     });
480     jp.add(ok);
481     
482     JButton cancel = new JButton(MessageManager.getString("action.cancel"));
483     cancel.setFont(labelFont);
484     cancel.addActionListener(new ActionListener()
485     {
486       @Override
487       public void actionPerformed(ActionEvent e)
488       {
489         cancel_actionPerformed();
490       }
491     });
492     jp.add(cancel);
493
494     return jp;
495   }
496
497   /**
498    * On 'Cancel' button, undo any changes.
499    */
500   protected void cancel_actionPerformed()
501   {
502     resetOriginalState();
503     this.ap.repaint();
504     close_actionPerformed();
505   }
506
507   /**
508    * Restore annotation visibility to their state on entry here, and repaint
509    * alignment.
510    */
511   protected void resetOriginalState()
512   {
513     int i = 0;
514     for (AlignmentAnnotation aa : this.ap.getAlignment()
515             .getAlignmentAnnotation())
516     {
517       aa.visible = this.resetState[i++];
518     }
519   }
520
521   /**
522    * On 'Close' button, close the dialog.
523    */
524   protected void close_actionPerformed()
525   {
526     try
527     {
528       this.frame.setClosed(true);
529     } catch (Exception exe)
530     {
531     }
532   }
533
534   /**
535    * Render a frame containing this panel.
536    */
537   private void showFrame()
538   {
539     frame = new JInternalFrame();
540     frame.setContentPane(this);
541     frame.setLayer(JLayeredPane.PALETTE_LAYER);
542     Desktop.addInternalFrame(frame,
543             MessageManager.getString("label.choose_annotations"),
544             MY_FRAME_WIDTH, MY_FRAME_HEIGHT, true);
545   }
546
547   protected void setShowSelected(boolean showSelected)
548   {
549     this.showSelected = showSelected;
550   }
551
552   protected void setApplyToSelectedSequences(
553           boolean applyToSelectedSequences)
554   {
555     this.applyToSelectedSequences = applyToSelectedSequences;
556   }
557
558   protected void setApplyToUnselectedSequences(
559           boolean applyToUnselectedSequences)
560   {
561     this.applyToUnselectedSequences = applyToUnselectedSequences;
562   }
563
564 }