JAL-1264 first cut of panel for annotation show/hide options
[jalview.git] / src / jalview / gui / AnnotationChooser.java
diff --git a/src/jalview/gui/AnnotationChooser.java b/src/jalview/gui/AnnotationChooser.java
new file mode 100644 (file)
index 0000000..bb940e8
--- /dev/null
@@ -0,0 +1,564 @@
+package jalview.gui;
+
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceGroup;
+import jalview.util.MessageManager;
+
+import java.awt.BorderLayout;
+import java.awt.Checkbox;
+import java.awt.CheckboxGroup;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JInternalFrame;
+import javax.swing.JLayeredPane;
+import javax.swing.JPanel;
+
+/**
+ * A panel that allows the user to select which sequence-associated annotation
+ * rows to show or hide.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class AnnotationChooser extends JPanel
+{
+
+  private static final Font CHECKBOX_FONT = new Font("Serif", Font.BOLD, 12);
+
+  private static final int MY_FRAME_WIDTH = 600;
+
+  private static final int MY_FRAME_HEIGHT = 250;
+
+  private JInternalFrame frame;
+
+  private AlignmentPanel ap;
+
+  private SequenceGroup sg;
+
+  // all annotation rows' original visible state
+  private boolean[] resetState = null;
+
+  // is 'Show' selected?
+  private boolean showSelected;
+
+  // apply settings to selected (or all) sequences?
+  private boolean applyToSelectedSequences;
+
+  // apply settings to unselected (or all) sequences?
+  private boolean applyToUnselectedSequences;
+
+  // currently selected 'annotation type' checkboxes
+  private Map<String, String> selectedTypes = new HashMap<String, String>();
+  
+  /**
+   * Constructor.
+   * 
+   * @param alignPane
+   */
+  public AnnotationChooser(AlignmentPanel alignPane)
+  {
+    super();
+    this.ap = alignPane;
+    this.sg = alignPane.av.getSelectionGroup();
+    saveResetState(alignPane.getAlignment());
+
+    try
+    {
+      jbInit();
+    } catch (Exception ex)
+    {
+      ex.printStackTrace();
+    }
+    showFrame();
+  }
+
+  /**
+   * Save the initial show/hide state of all annotations to allow a Cancel
+   * operation.
+   * 
+   * @param alignment
+   */
+  protected void saveResetState(AlignmentI alignment)
+  {
+    AlignmentAnnotation[] annotations = alignment.getAlignmentAnnotation();
+    final int count = annotations.length;
+    this.resetState = new boolean[count];
+    for (int i = 0; i < count; i++)
+    {
+      this.resetState[i] = annotations[i].visible;
+    }
+  }
+
+  /**
+   * Populate this frame with:
+   * <p>
+   * checkboxes for the types of annotation to show or hide (i.e. any annotation
+   * type shown for any sequence in the whole alignment)
+   * <p>
+   * option to show or hide selected types
+   * <p>
+   * option to show/hide for the currently selected group, or its inverse
+   * <p>
+   * OK and Cancel (reset) buttons
+   */
+  protected void jbInit()
+  {
+    setLayout(new GridLayout(3, 1));
+    add(buildAnnotationTypesPanel());
+    add(buildShowHideOptionsPanel());
+    add(buildActionButtonsPanel());
+    validate();
+  }
+
+  /**
+   * Construct the panel with checkboxes for annotation types.
+   * 
+   * @return
+   */
+  protected JPanel buildAnnotationTypesPanel()
+  {
+    JPanel jp = new JPanel(new FlowLayout(FlowLayout.LEFT));
+
+    List<String> annotationTypes = getAnnotationTypes(
+            this.ap.getAlignment(), true);
+
+    for (final String type : annotationTypes)
+    {
+      final JCheckBox check = new JCheckBox(type);
+      check.setFont(CHECKBOX_FONT);
+      check.addItemListener(new ItemListener()
+      {
+        @Override
+        public void itemStateChanged(ItemEvent evt)
+        {
+          if (evt.getStateChange() == ItemEvent.SELECTED)
+          {
+            AnnotationChooser.this.selectedTypes.put(type, type);
+          }
+          else
+          {
+            AnnotationChooser.this.selectedTypes.remove(type);
+          }
+          repaintAnnotations(false);
+        }
+      });
+      jp.add(check);
+    }
+    return jp;
+  }
+
+  /**
+   * Set visibility flags on annotation rows then repaint the alignment panel.
+   * <p>
+   * Optionally, update all rows, including those not in the 'apply to' scope.
+   * This makes more sense when switching between selected and unselected
+   * sequences. When selecting annotation types, or show/hide, we only apply the
+   * settings to the selected sequences.
+   * <p>
+   * Note this only affects sequence-specific annotations, others are left
+   * unchanged.
+   */
+  protected void repaintAnnotations(boolean updateAllRows)
+  {
+    for (AlignmentAnnotation aa : this.ap.getAlignment()
+            .getAlignmentAnnotation())
+    {
+      if (aa.sequenceRef != null)
+      {
+        setAnnotationVisibility(aa, updateAllRows);
+      }
+    }
+    // copied from AnnotationLabel.actionPerformed (after show/hide row)...
+    // TODO should drive this functionality into AlignmentPanel
+    ap.updateAnnotation();
+    this.ap.annotationPanel.adjustPanelHeight();
+    this.ap.alabels.setSize(this.ap.alabels.getSize().width,
+            this.ap.annotationPanel.getSize().height);
+    this.ap.validate();
+    this.ap.paintAlignment(true);
+  }
+
+  /**
+   * Determine and set the visibility of the given annotation from the currently
+   * selected options.
+   * <p>
+   * If its sequence is in the selected application scope
+   * (all/selected/unselected sequences), then we set its visibility.
+   * <p>
+   * If the annotation type is one of those currently selected by checkbox, set
+   * its visibility to the selected value. If it is not currently selected, set
+   * it to the opposite value. So, unselecting an annotation type with 'hide'
+   * selected, will cause those annotations to be unhidden.
+   * <p>
+   * If force update of all rows is wanted, then set rows not in the sequence
+   * selection scope to the opposite visibility to those in scope.
+   * 
+   * @param aa
+   * @param updateAllRows
+   */
+  protected void setAnnotationVisibility(AlignmentAnnotation aa,
+          boolean updateAllRows)
+  {
+    boolean setToVisible = false;
+    if (this.selectedTypes.containsKey(aa.label))
+    {
+      setToVisible = this.showSelected;
+    }
+    else
+    {
+      setToVisible = !this.showSelected;
+    }
+    if (isInActionScope(aa))
+    {
+      aa.visible = setToVisible;
+    }
+    else if (updateAllRows)
+    {
+      aa.visible = !setToVisible;
+    }
+    // TODO force not visible if associated sequence is hidden?
+    // currently hiding a sequence does not hide its annotation rows
+  }
+
+  /**
+   * Answers true if the annotation falls in the current selection criteria for
+   * show/hide.
+   * <p>
+   * It must be in the sequence selection group (for 'Apply to selection'), or
+   * not in it (for 'Apply except to selection'). No check needed for 'Apply to
+   * all'.
+   * 
+   * @param aa
+   * @return
+   */
+  protected boolean isInActionScope(AlignmentAnnotation aa)
+  {
+    boolean result = false;
+    if (this.applyToSelectedSequences && this.applyToUnselectedSequences)
+    {
+      // we don't care if the annotation's sequence is selected or not
+      result = true;
+    }
+    else if (this.sg.getSequences().contains(aa.sequenceRef))
+    {
+      // annotation is for a member of the selection group
+      result = this.applyToSelectedSequences ? true : false;
+    }
+    else
+    {
+      // annotation is not associated with the selection group
+      result = this.applyToUnselectedSequences ? true : false;
+    }
+    return result;
+  }
+
+  /**
+   * Get annotation 'types' for an alignment, optionally restricted to
+   * sequence-specific annotations only. The label is currently used for 'type'.
+   * 
+   * TODO refactor to helper class. See
+   * AnnotationColourChooser.getAnnotationItems() for another client
+   * 
+   * @param alignment
+   * @param sequenceSpecific
+   * @return
+   */
+  public static List<String> getAnnotationTypes(AlignmentI alignment,
+          boolean sequenceSpecificOnly)
+  {
+    // stub
+    List<String> result = new ArrayList<String>();
+    for (AlignmentAnnotation aa : alignment.getAlignmentAnnotation())
+    {
+      if (!sequenceSpecificOnly || aa.sequenceRef != null)
+      {
+        String label = aa.label;
+        if (!result.contains(label))
+        {
+          result.add(label);
+        }
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Construct the panel with options to:
+   * <p>
+   * show or hide the selected annotation types
+   * <p>
+   * do this for the current selection group or its inverse
+   * 
+   * @return
+   */
+  protected JPanel buildShowHideOptionsPanel()
+  {
+    JPanel jp = new JPanel();
+    jp.setLayout(new BorderLayout());
+
+    JPanel showHideOptions = buildShowHidePanel();
+    jp.add(showHideOptions, BorderLayout.CENTER);
+
+    JPanel applyToOptions = buildApplyToOptionsPanel();
+    jp.add(applyToOptions, BorderLayout.SOUTH);
+
+    return jp;
+  }
+
+  /**
+   * Build a panel with radio buttons options for sequences to apply show/hide
+   * to. Options are all, current selection, all except current selection.
+   * Initial state has 'current selection' selected.
+   * <p>
+   * If the sequence group is null, then we are acting on the whole alignment,
+   * and only 'all sequences' is enabled (and selected).
+   * 
+   * @return
+   */
+  protected JPanel buildApplyToOptionsPanel()
+  {
+    final boolean wholeAlignment = this.sg == null;
+    JPanel applyToOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    CheckboxGroup actingOn = new CheckboxGroup();
+    
+    String forAll = MessageManager.getString("label.all_sequences");
+    final Checkbox allSequences = new Checkbox(forAll, actingOn,
+            wholeAlignment);
+    allSequences.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent evt)
+      {
+        if (evt.getStateChange() == ItemEvent.SELECTED) {
+          AnnotationChooser.this.setApplyToSelectedSequences(true);
+          AnnotationChooser.this.setApplyToUnselectedSequences(true);
+          AnnotationChooser.this.repaintAnnotations(true);
+        }
+      }
+    });
+    applyToOptions.add(allSequences);
+    
+    String forSelected = MessageManager
+            .getString("label.selected_sequences");
+    final Checkbox selectedSequences = new Checkbox(forSelected, actingOn,
+            !wholeAlignment);
+    selectedSequences.setEnabled(!wholeAlignment);
+    selectedSequences.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent evt)
+      {
+        if (evt.getStateChange() == ItemEvent.SELECTED)
+        {
+          AnnotationChooser.this.setApplyToSelectedSequences(true);
+          AnnotationChooser.this.setApplyToUnselectedSequences(false);
+          AnnotationChooser.this.repaintAnnotations(true);
+        }
+      }
+    });
+    applyToOptions.add(selectedSequences);
+    
+    String exceptSelected = MessageManager
+            .getString("label.except_selected_sequences");
+    final Checkbox unselectedSequences = new Checkbox(exceptSelected, actingOn, false);
+    unselectedSequences.setEnabled(!wholeAlignment);
+    unselectedSequences.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent evt)
+      {
+        if (evt.getStateChange() == ItemEvent.SELECTED)
+        {
+          AnnotationChooser.this.setApplyToSelectedSequences(false);
+          AnnotationChooser.this.setApplyToUnselectedSequences(true);
+          AnnotationChooser.this.repaintAnnotations(true);
+        }
+      }
+    });
+    applyToOptions.add(unselectedSequences);
+    
+    // set member variables to match the initial selection state
+    this.applyToSelectedSequences = selectedSequences.getState()
+            || allSequences.getState();
+    this.applyToUnselectedSequences = unselectedSequences.getState()
+            || allSequences.getState();
+
+    return applyToOptions;
+  }
+
+  /**
+   * Build a panel with radio buttons options to show or hide selected
+   * annotations.
+   * 
+   * @return
+   */
+  protected JPanel buildShowHidePanel()
+  {
+    JPanel showHideOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    CheckboxGroup showOrHide = new CheckboxGroup();
+
+    /*
+     * Radio button 'Show selected annotations' - initially unselected
+     */
+    String showLabel = MessageManager
+            .getString("label.show_selected_annotations");
+    final Checkbox showOption = new Checkbox(showLabel, showOrHide, false);
+    showOption.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent evt)
+      {
+        if (evt.getStateChange() == ItemEvent.SELECTED) {
+          AnnotationChooser.this.setShowSelected(true);
+          AnnotationChooser.this.repaintAnnotations(false);
+        }
+      }
+    });
+    showHideOptions.add(showOption);
+
+    /*
+     * Radio button 'hide selected annotations'- initially selected
+     */
+    String hideLabel = MessageManager
+            .getString("label.hide_selected_annotations");
+    final Checkbox hideOption = new Checkbox(hideLabel, showOrHide, true);
+    hideOption.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent evt)
+      {
+        if (evt.getStateChange() == ItemEvent.SELECTED)
+        {
+          AnnotationChooser.this.setShowSelected(false);
+          AnnotationChooser.this.repaintAnnotations(false);
+        }
+      }
+    });
+    showHideOptions.add(hideOption);
+
+    /*
+     * Set member variable to match initial selection state
+     */
+    this.showSelected = showOption.getState();
+
+    return showHideOptions;
+  }
+
+  /**
+   * Construct the panel with OK and Cancel buttons.
+   * 
+   * @return
+   */
+  protected JPanel buildActionButtonsPanel()
+  {
+    JPanel jp = new JPanel();
+    final Font labelFont = JvSwingUtils.getLabelFont();
+    
+    JButton ok = new JButton(MessageManager.getString("action.ok"));
+    ok.setFont(labelFont);
+    ok.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        close_actionPerformed();
+      }
+    });
+    jp.add(ok);
+    
+    JButton cancel = new JButton(MessageManager.getString("action.cancel"));
+    cancel.setFont(labelFont);
+    cancel.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        cancel_actionPerformed();
+      }
+    });
+    jp.add(cancel);
+
+    return jp;
+  }
+
+  /**
+   * On 'Cancel' button, undo any changes.
+   */
+  protected void cancel_actionPerformed()
+  {
+    resetOriginalState();
+    this.ap.repaint();
+    close_actionPerformed();
+  }
+
+  /**
+   * Restore annotation visibility to their state on entry here, and repaint
+   * alignment.
+   */
+  protected void resetOriginalState()
+  {
+    int i = 0;
+    for (AlignmentAnnotation aa : this.ap.getAlignment()
+            .getAlignmentAnnotation())
+    {
+      aa.visible = this.resetState[i++];
+    }
+  }
+
+  /**
+   * On 'Close' button, close the dialog.
+   */
+  protected void close_actionPerformed()
+  {
+    try
+    {
+      this.frame.setClosed(true);
+    } catch (Exception exe)
+    {
+    }
+  }
+
+  /**
+   * Render a frame containing this panel.
+   */
+  private void showFrame()
+  {
+    frame = new JInternalFrame();
+    frame.setContentPane(this);
+    frame.setLayer(JLayeredPane.PALETTE_LAYER);
+    Desktop.addInternalFrame(frame,
+            MessageManager.getString("label.choose_annotations"),
+            MY_FRAME_WIDTH, MY_FRAME_HEIGHT, true);
+  }
+
+  protected void setShowSelected(boolean showSelected)
+  {
+    this.showSelected = showSelected;
+  }
+
+  protected void setApplyToSelectedSequences(
+          boolean applyToSelectedSequences)
+  {
+    this.applyToSelectedSequences = applyToSelectedSequences;
+  }
+
+  protected void setApplyToUnselectedSequences(
+          boolean applyToUnselectedSequences)
+  {
+    this.applyToUnselectedSequences = applyToUnselectedSequences;
+  }
+
+}