Merge branch 'develop' into feature/JAL-3416_update_to_flatlaf_3.1.1_with_unpacked_na...
[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 java.awt.BorderLayout;
24 import java.awt.Checkbox;
25 import java.awt.CheckboxGroup;
26 import java.awt.FlowLayout;
27 import java.awt.Font;
28 import java.awt.GridLayout;
29 import java.awt.event.ActionEvent;
30 import java.awt.event.ActionListener;
31 import java.awt.event.ItemEvent;
32 import java.awt.event.ItemListener;
33 import java.util.ArrayList;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Map;
37
38 import javax.swing.JButton;
39 import javax.swing.JInternalFrame;
40 import javax.swing.JLayeredPane;
41 import javax.swing.JPanel;
42
43 import jalview.datamodel.AlignmentAnnotation;
44 import jalview.datamodel.AlignmentI;
45 import jalview.datamodel.SequenceGroup;
46 import jalview.util.MessageManager;
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     ap.updateAnnotation();
199   }
200
201   /**
202    * Update display when an annotation type is selected or deselected.
203    * <p>
204    * If the type is selected, set visibility of annotations of that type which
205    * are in the application scope (all, selected or unselected sequences).
206    * <p>
207    * If the type is unselected, set visibility to the opposite value. That is,
208    * treat select/deselect as a 'toggle' operation.
209    * 
210    * @param type
211    */
212   protected void changeTypeSelected_actionPerformed(String type)
213   {
214     boolean typeSelected = this.selectedTypes.containsKey(type);
215     for (AlignmentAnnotation aa : this.ap.getAlignment()
216             .getAlignmentAnnotation())
217     {
218       if (aa.sequenceRef != null && type.equals(aa.label)
219               && isInActionScope(aa))
220       {
221         aa.visible = typeSelected ? this.showSelected : !this.showSelected;
222       }
223     }
224     ap.updateAnnotation();
225   }
226
227   /**
228    * Update display on change of choice of Show or Hide
229    * <p>
230    * For annotations of any selected type, set visibility of annotations of that
231    * type which are in the application scope (all, selected or unselected
232    * sequences).
233    * 
234    * @param dataSourceType
235    */
236   protected void changeShowHide_actionPerformed()
237   {
238     setAnnotationVisibility(false);
239
240     ap.updateAnnotation();
241   }
242
243   /**
244    * Update visibility flags on annotation rows as per the current user choices.
245    * 
246    * @param updateAllRows
247    */
248   protected void setAnnotationVisibility(boolean updateAllRows)
249   {
250     for (AlignmentAnnotation aa : this.ap.getAlignment()
251             .getAlignmentAnnotation())
252     {
253       if (aa.sequenceRef != null)
254       {
255         setAnnotationVisibility(aa, updateAllRows);
256       }
257     }
258   }
259
260   /**
261    * Determine and set the visibility of the given annotation from the currently
262    * selected options.
263    * <p>
264    * Only update annotations whose type is one of the selected types.
265    * <p>
266    * If its sequence is in the selected application scope
267    * (all/selected/unselected sequences), then we set its visibility according
268    * to the current choice of Show or Hide.
269    * <p>
270    * If force update of all rows is wanted, then set rows not in the sequence
271    * selection scope to the opposite visibility to those in scope.
272    * 
273    * @param aa
274    * @param updateAllRows
275    */
276   protected void setAnnotationVisibility(AlignmentAnnotation aa,
277           boolean updateAllRows)
278   {
279     if (this.selectedTypes.containsKey(aa.label))
280     {
281       if (isInActionScope(aa))
282       {
283         aa.visible = this.showSelected;
284       }
285       else if (updateAllRows)
286       {
287         aa.visible = !this.showSelected;
288       }
289     }
290     // TODO force not visible if associated sequence is hidden?
291     // currently hiding a sequence does not hide its annotation rows
292   }
293
294   /**
295    * Answers true if the annotation falls in the current selection criteria for
296    * show/hide.
297    * <p>
298    * It must be in the sequence selection group (for 'Apply to selection'), or
299    * not in it (for 'Apply except to selection'). No check needed for 'Apply to
300    * all'.
301    * 
302    * @param aa
303    * @return
304    */
305   protected boolean isInActionScope(AlignmentAnnotation aa)
306   {
307     boolean result = false;
308     if (this.applyToSelectedSequences && this.applyToUnselectedSequences)
309     {
310       // we don't care if the annotation's sequence is selected or not
311       result = true;
312     }
313     else if (this.sg == null)
314     {
315       // shouldn't happen - defensive programming
316       result = true;
317     }
318     else if (this.sg.getSequences().contains(aa.sequenceRef))
319     {
320       // annotation is for a member of the selection group
321       result = this.applyToSelectedSequences ? true : false;
322     }
323     else
324     {
325       // annotation is not associated with the selection group
326       result = this.applyToUnselectedSequences ? true : false;
327     }
328     return result;
329   }
330
331   /**
332    * Get annotation 'types' for an alignment, optionally restricted to
333    * sequence-specific annotations only. The label is currently used for 'type'.
334    * 
335    * TODO refactor to helper class. See
336    * AnnotationColourChooser.getAnnotationItems() for another client
337    * 
338    * @param alignment
339    * @param sequenceSpecific
340    * @return
341    */
342   public static List<String> getAnnotationTypes(AlignmentI alignment,
343           boolean sequenceSpecificOnly)
344   {
345     List<String> result = new ArrayList<>();
346     for (AlignmentAnnotation aa : alignment.getAlignmentAnnotation())
347     {
348       if (!sequenceSpecificOnly || aa.sequenceRef != null)
349       {
350         String label = aa.label;
351         if (!result.contains(label))
352         {
353           result.add(label);
354         }
355       }
356     }
357     return result;
358   }
359
360   /**
361    * Construct the panel with options to:
362    * <p>
363    * show or hide the selected annotation types
364    * <p>
365    * do this for the current selection group or its inverse
366    * 
367    * @return
368    */
369   protected JPanel buildShowHideOptionsPanel()
370   {
371     JPanel jp = new JPanel();
372     jp.setLayout(new BorderLayout());
373
374     JPanel showHideOptions = buildShowHidePanel();
375     jp.add(showHideOptions, BorderLayout.CENTER);
376
377     JPanel applyToOptions = buildApplyToOptionsPanel();
378     jp.add(applyToOptions, BorderLayout.SOUTH);
379
380     return jp;
381   }
382
383   /**
384    * Build a panel with radio buttons options for sequences to apply show/hide
385    * to. Options are all, current selection, all except current selection.
386    * Initial state has 'current selection' selected.
387    * <p>
388    * If the sequence group is null, then we are acting on the whole alignment,
389    * and only 'all sequences' is enabled (and selected).
390    * 
391    * @return
392    */
393   protected JPanel buildApplyToOptionsPanel()
394   {
395     final boolean wholeAlignment = this.sg == null;
396     JPanel applyToOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
397     CheckboxGroup actingOn = new CheckboxGroup();
398
399     String forAll = MessageManager.getString("label.all_sequences");
400     final Checkbox allSequences = new Checkbox(forAll, actingOn,
401             wholeAlignment);
402     allSequences.addItemListener(new ItemListener()
403     {
404       @Override
405       public void itemStateChanged(ItemEvent evt)
406       {
407         if (evt.getStateChange() == ItemEvent.SELECTED)
408         {
409           AnnotationChooser.this.setApplyToSelectedSequences(true);
410           AnnotationChooser.this.setApplyToUnselectedSequences(true);
411           AnnotationChooser.this.changeApplyTo_actionPerformed();
412         }
413       }
414     });
415     applyToOptions.add(allSequences);
416
417     String forSelected = MessageManager
418             .getString("label.selected_sequences");
419     final Checkbox selectedSequences = new Checkbox(forSelected, actingOn,
420             !wholeAlignment);
421     selectedSequences.setEnabled(!wholeAlignment);
422     selectedSequences.addItemListener(new ItemListener()
423     {
424       @Override
425       public void itemStateChanged(ItemEvent evt)
426       {
427         if (evt.getStateChange() == ItemEvent.SELECTED)
428         {
429           AnnotationChooser.this.setApplyToSelectedSequences(true);
430           AnnotationChooser.this.setApplyToUnselectedSequences(false);
431           AnnotationChooser.this.changeApplyTo_actionPerformed();
432         }
433       }
434     });
435     applyToOptions.add(selectedSequences);
436
437     String exceptSelected = MessageManager
438             .getString("label.except_selected_sequences");
439     final Checkbox unselectedSequences = new Checkbox(exceptSelected,
440             actingOn, false);
441     unselectedSequences.setEnabled(!wholeAlignment);
442     unselectedSequences.addItemListener(new ItemListener()
443     {
444       @Override
445       public void itemStateChanged(ItemEvent evt)
446       {
447         if (evt.getStateChange() == ItemEvent.SELECTED)
448         {
449           AnnotationChooser.this.setApplyToSelectedSequences(false);
450           AnnotationChooser.this.setApplyToUnselectedSequences(true);
451           AnnotationChooser.this.changeApplyTo_actionPerformed();
452         }
453       }
454     });
455     applyToOptions.add(unselectedSequences);
456
457     // set member variables to match the initial selection state
458     this.applyToSelectedSequences = selectedSequences.getState()
459             || allSequences.getState();
460     this.applyToUnselectedSequences = unselectedSequences.getState()
461             || allSequences.getState();
462
463     return applyToOptions;
464   }
465
466   /**
467    * Build a panel with radio button options to show or hide selected
468    * annotations.
469    * 
470    * @return
471    */
472   protected JPanel buildShowHidePanel()
473   {
474     JPanel showHideOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
475     CheckboxGroup showOrHide = new CheckboxGroup();
476
477     /*
478      * Radio button 'Show selected annotations' - initially unselected
479      */
480     String showLabel = MessageManager
481             .getString("label.show_selected_annotations");
482     final Checkbox showOption = new Checkbox(showLabel, showOrHide, false);
483     showOption.addItemListener(new ItemListener()
484     {
485       @Override
486       public void itemStateChanged(ItemEvent evt)
487       {
488         if (evt.getStateChange() == ItemEvent.SELECTED)
489         {
490           AnnotationChooser.this.setShowSelected(true);
491           AnnotationChooser.this.changeShowHide_actionPerformed();
492         }
493       }
494     });
495     showHideOptions.add(showOption);
496
497     /*
498      * Radio button 'hide selected annotations'- initially selected
499      */
500     String hideLabel = MessageManager
501             .getString("label.hide_selected_annotations");
502     final Checkbox hideOption = new Checkbox(hideLabel, showOrHide, true);
503     hideOption.addItemListener(new ItemListener()
504     {
505       @Override
506       public void itemStateChanged(ItemEvent evt)
507       {
508         if (evt.getStateChange() == ItemEvent.SELECTED)
509         {
510           AnnotationChooser.this.setShowSelected(false);
511           AnnotationChooser.this.changeShowHide_actionPerformed();
512         }
513       }
514     });
515     showHideOptions.add(hideOption);
516
517     /*
518      * Set member variable to match initial selection state
519      */
520     this.showSelected = showOption.getState();
521
522     return showHideOptions;
523   }
524
525   /**
526    * Construct the panel with OK and Cancel buttons.
527    * 
528    * @return
529    */
530   protected JPanel buildActionButtonsPanel()
531   {
532     JPanel jp = new JPanel();
533     final Font labelFont = JvSwingUtils.getLabelFont();
534
535     JButton ok = new JButton(MessageManager.getString("action.ok"));
536     ok.setFont(labelFont);
537     ok.addActionListener(new ActionListener()
538     {
539       @Override
540       public void actionPerformed(ActionEvent e)
541       {
542         close_actionPerformed();
543       }
544     });
545     jp.add(ok);
546
547     JButton cancel = new JButton(MessageManager.getString("action.cancel"));
548     cancel.setFont(labelFont);
549     cancel.addActionListener(new ActionListener()
550     {
551       @Override
552       public void actionPerformed(ActionEvent e)
553       {
554         cancel_actionPerformed();
555       }
556     });
557     jp.add(cancel);
558
559     return jp;
560   }
561
562   /**
563    * On 'Cancel' button, undo any changes.
564    */
565   protected void cancel_actionPerformed()
566   {
567     resetOriginalState();
568     this.ap.repaint();
569     close_actionPerformed();
570   }
571
572   /**
573    * Restore annotation visibility to their state on entry here, and repaint
574    * alignment.
575    */
576   protected void resetOriginalState()
577   {
578     int i = 0;
579     for (AlignmentAnnotation aa : this.ap.getAlignment()
580             .getAlignmentAnnotation())
581     {
582       aa.visible = this.resetState[i++];
583     }
584   }
585
586   /**
587    * On 'Close' button, close the dialog.
588    */
589   protected void close_actionPerformed()
590   {
591     try
592     {
593       this.frame.setClosed(true);
594     } catch (Exception exe)
595     {
596     }
597   }
598
599   /**
600    * Render a frame containing this panel.
601    */
602   private void showFrame()
603   {
604     frame = new JInternalFrame();
605     frame.setFrameIcon(null);
606     frame.setContentPane(this);
607     frame.setLayer(JLayeredPane.PALETTE_LAYER);
608     Desktop.addInternalFrame(frame,
609             MessageManager.getString("label.choose_annotations"),
610             MY_FRAME_WIDTH, MY_FRAME_HEIGHT, true);
611   }
612
613   protected void setShowSelected(boolean showSelected)
614   {
615     this.showSelected = showSelected;
616   }
617
618   protected void setApplyToSelectedSequences(
619           boolean applyToSelectedSequences)
620   {
621     this.applyToSelectedSequences = applyToSelectedSequences;
622   }
623
624   protected void setApplyToUnselectedSequences(
625           boolean applyToUnselectedSequences)
626   {
627     this.applyToUnselectedSequences = applyToUnselectedSequences;
628   }
629
630   protected boolean isShowSelected()
631   {
632     return showSelected;
633   }
634
635   protected boolean isApplyToSelectedSequences()
636   {
637     return applyToSelectedSequences;
638   }
639
640   protected boolean isApplyToUnselectedSequences()
641   {
642     return applyToUnselectedSequences;
643   }
644
645 }