content: /*
   * The MIT License (MIT)
   *
   * Copyright (c) 2011-2021 Broad Institute, Aiden Lab, Rice University, Baylor College of Medicine
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy
   * of this software and associated documentation files (the "Software"), to deal
   * in the Software without restriction, including without limitation the rights
   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   * copies of the Software, and to permit persons to whom the Software is
   * furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *
   *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   *  FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
   *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
   *  THE SOFTWARE.
   */
  
  package juicebox.windowui;
  
  import javax.accessibility.Accessible;
  import javax.accessibility.AccessibleContext;
  import javax.accessibility.AccessibleRole;
  import javax.swing.*;
  import javax.swing.event.ChangeListener;
  import javax.swing.plaf.ButtonUI;
  import java.awt.*;
  import java.awt.event.*;
  import java.awt.image.BufferedImage;
  
  /**
   * A split button. The user can either click the text, which executes an
   * action, or click the icon, which opens a popup menu.
   *
   * @author eoogbe
   */
  class JSplitButton extends AbstractButton implements Accessible {
  
      private static final long serialVersionUID = 900002;
      private static final String uiClassID = "ButtonUI";
      private static final int DEFAULT_POPUP_ICON_LENGTH = 10;
      private static final String ALWAYS_SHOWS_POPUP_CHANGED_PROPERTY =
              "alwaysShowPopup";
      private static final String SPLIT_GAP_CHANGED_PROPERTY = "splitGap";
      private static final String POPUP_ICON_CHANGED_PROPERTY = "popupIcon";
      private static final String DISABLED_POPUP_ICON_CHANGED_PROPERTY =
              "disabledPopupIcon";
      private static final String
              DISABLED_SELECTED_POPUP_ICON_CHANGED_PROPERTY = "disabledSelectedPopupIcon";
      private static final String PRESSED_POPUP_ICON_CHANGED_PROPERTY =
              "pressedPopupIcon";
      private static final String ROLLOVER_POPUP_ICON_CHANGED_PROPERTY =
              "rolloverPopupIcon";
      private static final String
              ROLLOVER_SELECTED_POPUP_ICON_CHANGED_PROPERTY = "rolloverSelectedPopupIcon";
      private static final String SELECTED_POPUP_ICON_CHANGED_PROPERTY =
              "selectedPopupIcon";
      private static final String MAIN_TEXT_CHANGED_PROPERTY = "mainText";
      private final JButton mainButton;
      private final JButton popupButton;
      private PopupAction popupAction;
      private boolean alwaysShowPopup;
      private int splitGap = 5;
  
      /**
       * Creates a new JSplitButton with no set text.
       */
      private JSplitButton() {
          this("");
      }
  
      /**
       * Creates a new JSplitButton with initial text.
       *
       * @param text the text displayed on this JSplitButton
       */
      public JSplitButton(String text) {
          mainButton = new JButton(text);
          popupButton = new JButton(createDefaultPopupIcon());
          //   popupButton.setPreferredSize(new Dimension((int)popupButton.getPreferredSize().getWidth()+10,(int)popupButton.getPreferredSize().getHeight()));
  
          setModel(new DefaultButtonModel());
  
          mainButton.setBorder(BorderFactory.createEmptyBorder());
          // popupButton.setBorder(BorderFactory.createEmptyBorder());
  
          mainButton.setContentAreaFilled(false);
  
          mainButton.addActionListener(new ActionListener() {
  
              @Override
              public void actionPerformed(ActionEvent e) {
                  doClick();
              }
  
          });
  
          mainButton.addMouseListener(new MouseAdapter() {
  
              @Override
              public void mouseEntered(MouseEvent e) {
                  if (isRolloverEnabled()) model.setRollover(true);
              }
  
              @Override
              public void mouseExited(MouseEvent e) {
                  model.setRollover(false);
              }
  
          });
  
          setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
          resetComponents();
  
          init(null, null);
      }
  
      /**
       * Creates a new JSplitButton with properties taken from the Action
       * supplied.
       *
       * @param action used to specify the properties of this JSplitButton
       */
      public JSplitButton(Action action) {
          this();
          setAction(action);
      }
  
      private Icon createDefaultPopupIcon() {
          Image image = new BufferedImage(DEFAULT_POPUP_ICON_LENGTH,
                  DEFAULT_POPUP_ICON_LENGTH, BufferedImage.TYPE_INT_ARGB);
          Graphics g = image.getGraphics();
  
          g.setColor(new Color(255, 255, 255, 0));
          g.fillRect(0, 0, DEFAULT_POPUP_ICON_LENGTH,
                  DEFAULT_POPUP_ICON_LENGTH);
  
          g.setColor(Color.BLACK);
          Polygon traingle = new Polygon(
                  new int[]{0, DEFAULT_POPUP_ICON_LENGTH,
                          DEFAULT_POPUP_ICON_LENGTH / 2, 0},
                  new int[]{0, 0, DEFAULT_POPUP_ICON_LENGTH, 0}, 4
          );
          g.fillPolygon(traingle);
  
          g.dispose();
          return new ImageIcon(image);
      }
  
      private void resetComponents() {
          removeAll();
          add(mainButton);
          add(Box.createRigidArea(new Dimension(splitGap, 0)));
          add(new JSeparator(VERTICAL));
          add(Box.createRigidArea(new Dimension(splitGap, 0)));
          add(popupButton);
          revalidate();
      }
  
      /**
       * Returns true if this JSplitButton shows the popup menu for every
       * click.
       *
       * @return true if this JSplitButton shows the popup menu for every
       * click
       * @see JSplitButton#setAlwaysShowPopup(boolean)
       */
      public boolean isAlwaysShowPopup() {
          return alwaysShowPopup;
      }
  
      /**
       * Sets whether this JSplitButton shows the popup menu for every click.
       *
       * @param alwaysShowPopup true if this JSplitButton shows the popup menu
       *                        for every click
       * @see JSplitButton#isAlwaysShowPopup()
       */
      public void setAlwaysShowPopup(boolean alwaysShowPopup) {
          boolean oldValue = this.alwaysShowPopup;
          this.alwaysShowPopup = alwaysShowPopup;
          firePropertyChange(ALWAYS_SHOWS_POPUP_CHANGED_PROPERTY, oldValue,
                  alwaysShowPopup);
  
          if (popupAction != null && oldValue != alwaysShowPopup) {
              setComponentPopupMenu(popupAction.getPopupMenu());
          }
      }
  
      /**
       * Returns the gap between the separator and the labels on each side
       *
       * @return the gap between the separator and the labels on each side
       * @see JSplitButton#setSplitGap(int)
       */
      public int getSplitGap() {
          return splitGap;
      }
  
      /**
       * Sets the gap between the separator and the labels on each side.
       *
       * @param splitGap the gap set
       * @see JSplitButton#getSplitGap()
       */
      public void setSplitGap(int splitGap) {
          int oldValue = this.splitGap;
          this.splitGap = splitGap;
          firePropertyChange(SPLIT_GAP_CHANGED_PROPERTY, oldValue, splitGap);
  
          if (oldValue != splitGap) {
              resetComponents();
          }
      }
  
      /**
       * Returns the Icon used on the popup side. This Icon is also used as the
       * "pressed" and "disabled" Icon if they are not explicitly set.
       *
       * @return the popupIcon property
       * @see AbstractButton#getIcon()
       * @see JSplitButton#setPopupIcon(Icon)
       */
      private Icon getPopupIcon() {
          return popupButton.getIcon();
      }
  
      /**
       * Sets the Icon used on the popup side. If null, the Icon will be set to
       * a default value. This Icon is also used as the "pressed" and "disabled"
       * Icon if they are not explicitly set.
       *
       * @param icon the Icon set
       * @see AbstractButton#setIcon(Icon)
       * @see JSplitButton#getPopupIcon()
       */
      public void setPopupIcon(Icon icon) {
          Icon oldValue = getPopupIcon();
          if (icon == null) icon = createDefaultPopupIcon();
          firePropertyChange(POPUP_ICON_CHANGED_PROPERTY, oldValue, icon);
  
          if (!oldValue.equals(icon)) {
              popupButton.setIcon(icon);
          }
      }
  
      /**
       * Returns the icon used on the popup side when this JSplitButton is
       * disabled. If no disabled icon has been set this will forward the call
       * to the look and feel to construct an appropriate disabled Icon. Some
       * look and feels might not render the disabled Icon, in which case they
       * will ignore this.
       *
       * @return the disabledPopupIcon property
       * @see AbstractButton#getDisabledIcon()
       * @see JSplitButton#setDisabledPopupIcon(Icon)
       */
      private Icon getDisabledPopupIcon() {
          return popupButton.getDisabledIcon();
      }
  
      /**
       * Sets the icon used on the popup side when this JSplitButton is
       * disabled. Some look and feels might not render the disabled Icon, in
       * which case they will ignore this.
       *
       * @param icon the icon set
       * @see AbstractButton#setDisabledIcon(Icon)
       * @see JSplitButton#getDisabledPopupIcon()
       */
      public void setDisabledPopupIcon(Icon icon) {
          Icon oldValue = getDisabledPopupIcon();
          firePropertyChange(DISABLED_POPUP_ICON_CHANGED_PROPERTY, oldValue,
                  icon);
  
          if (!oldValue.equals(icon)) {
              popupButton.setDisabledIcon(icon);
          }
      }
  
      /**
       * Returns the icon used on the popup side when this JSplitButton is
       * disabled and selected. If no disabled selected Icon has been set, this
       * will forward the call to the LookAndFeel to construct an appropriate
       * disabled Icon from the selection icon if it has been set and to
       * getDisabledPopupIcon() otherwise.
       *
       * @return the disabledSelctedPopupIcon property
       * @see AbstractButton#getDisabledSelectedIcon()
       * @see JSplitButton#setDisabledSelectedPopupIcon(Icon)
       */
      private Icon getDisabledSelectedPopupIcon() {
          return popupButton.getDisabledSelectedIcon();
      }
  
      /**
       * Sets the icon used on the popup side when this JSplitButton is
       * disabled and selected.
       *
       * @param icon the icon set
       * @see AbstractButton#setDisabledSelectedIcon(Icon)
       * @see JSplitButton#getDisabledSelectedPopupIcon()
       */
      public void setDisabledSelectedPopupIcon(Icon icon) {
          Icon oldValue = getDisabledSelectedPopupIcon();
          firePropertyChange(DISABLED_SELECTED_POPUP_ICON_CHANGED_PROPERTY,
                  oldValue, icon);
  
          if (!oldValue.equals(icon)) {
              popupButton.setDisabledSelectedIcon(icon);
          }
      }
  
      /**
       * Returns the icon used on the popup side when this JSpitButton is
       * pressed.
       *
       * @return the pressedPopupIcon property
       * @see AbstractButton#getPressedIcon()
       * @see JSplitButton#setPressedPopupIcon(Icon)
       */
      private Icon getPressedPopupIcon() {
          return popupButton.getPressedIcon();
      }
  
      /**
       * Sets the icon used on the popup side when this JSplitButton is pressed.
       *
       * @param icon the icon set
       * @see AbstractButton#setPressedIcon(Icon)
       * @see JSplitButton#getPressedPopupIcon()
       */
      public void setPressedPopupIcon(Icon icon) {
          Icon oldValue = getPressedPopupIcon();
          firePropertyChange(PRESSED_POPUP_ICON_CHANGED_PROPERTY, oldValue,
                  icon);
  
          if (!oldValue.equals(icon)) {
              popupButton.setPressedIcon(icon);
          }
      }
  
      /**
       * Returns the rollover icon used on the popup side.
       *
       * @return the rolloverPopupIcon property
       * @see AbstractButton#getRolloverIcon()
       * @see JSplitButton#setRolloverPopupIcon(Icon)
       */
      private Icon getRolloverPopupIcon() {
          return popupButton.getRolloverIcon();
      }
  
      /**
       * Sets the rollover icon used on the popup side.
       *
       * @param icon the icon set
       * @see AbstractButton#setRolloverIcon(Icon)
       * @see JSplitButton#getRolloverPopupIcon()
       */
      public void setRolloverPopupIcon(Icon icon) {
          Icon oldValue = getRolloverPopupIcon();
          firePropertyChange(ROLLOVER_POPUP_ICON_CHANGED_PROPERTY, oldValue,
                  icon);
  
          if (!oldValue.equals(icon)) {
              popupButton.setRolloverIcon(icon);
          }
      }
  
      /**
       * Returns the rollover selection icon used on the popup side.
       *
       * @return the rolloverSelectedPopupIcon property
       * @see AbstractButton#getRolloverSelectedIcon()
       * @see JSplitButton#setRolloverSelectedPopupIcon(Icon)
       */
      private Icon getRolloverSelectedPopupIcon() {
          return popupButton.getRolloverSelectedIcon();
      }
  
      /**
       * Sets the rollover selection icon used on the popup side.
       *
       * @param icon the icon set
       * @see AbstractButton#setRolloverSelectedIcon(Icon)
       * @see JSplitButton#getRolloverSelectedPopupIcon()
       */
      public void setRolloverSelectedPopupIcon(Icon icon) {
          Icon oldValue = getRolloverSelectedPopupIcon();
          firePropertyChange(ROLLOVER_SELECTED_POPUP_ICON_CHANGED_PROPERTY,
                  oldValue, icon);
  
          if (!oldValue.equals(icon)) {
              popupButton.setRolloverSelectedIcon(icon);
          }
      }
  
      /**
       * Returns the selected icon used on the popup side.
       *
       * @return the selectedPopupIcon property
       * @see AbstractButton#getSelectedIcon()
       * @see JSplitButton#setSelectedPopupIcon(Icon)
       */
      private Icon getSelectedPopupIcon() {
          return popupButton.getSelectedIcon();
      }
  
      /**
       * Sets the selected icon used on the popup side.
       *
       * @param icon the icon set
       * @see AbstractButton#setSelectedIcon(Icon)
       * @see JSplitButton#getSelectedPopupIcon()
       */
      public void setSelectedPopupIcon(Icon icon) {
          Icon oldValue = getSelectedPopupIcon();
          firePropertyChange(SELECTED_POPUP_ICON_CHANGED_PROPERTY, oldValue,
                  icon);
  
          if (!oldValue.equals(icon)) {
              popupButton.setSelectedIcon(icon);
          }
      }
  
      /**
       * Returns the text of the main part of this JSplitButton. Use this
       * method instead of {@link AbstractButton#getText()}.
       *
       * @return the text of the main part of this JSplitButton
       * @see AbstractButton#getText()
       * @see JSplitButton#setMainText(String)
       */
      private String getMainText() {
          return mainButton.getText();
      }
  
      /**
       * Sets the text of the main part of this JSplitButton. Use this method
       * instead of {@link AbstractButton#setText(String)}.
       *
       * @param text the text set
       */
      public void setMainText(String text) {
          String oldValue = getMainText();
          firePropertyChange(MAIN_TEXT_CHANGED_PROPERTY, oldValue, text);
  
          if (!oldValue.equals(text)) {
              mainButton.setText(text);
          }
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#addActionListener(java.awt.event.ActionListener)
       */
      @Override
      public void addActionListener(ActionListener listener) {
          mainButton.addActionListener(listener);
          super.addActionListener(listener);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#removeActionListener(java.awt.event.ActionListener)
       */
      @Override
      public void removeActionListener(ActionListener listener) {
          mainButton.removeActionListener(listener);
          super.removeActionListener(listener);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#addChangeListener(javax.swing.event.ChangeListener)
       */
      @Override
      public void addChangeListener(ChangeListener listener) {
          mainButton.addChangeListener(listener);
          super.addChangeListener(listener);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#removeChangeListener(javax.swing.event.ChangeListener)
       */
      @Override
      public void removeChangeListener(ChangeListener listener) {
          mainButton.removeChangeListener(listener);
          super.removeChangeListener(listener);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#addItemListener(java.awt.event.ItemListener)
       */
      @Override
      public void addItemListener(ItemListener listener) {
          mainButton.addItemListener(listener);
          super.addItemListener(listener);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#removeItemListener(java.awt.event.ItemListener)
       */
      @Override
      public void removeItemListener(ItemListener listener) {
          mainButton.removeItemListener(listener);
          super.removeItemListener(listener);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#getAction()
       */
      @Override
      public Action getAction() {
          return mainButton.getAction();
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#setAction(javax.swing.Action)
       */
      @Override
      public void setAction(Action action) {
          Action oldValue = getAction();
          firePropertyChange("action", oldValue, action);
          mainButton.setAction(action);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.JComponent#getAccessibleContext()
       */
      @Override
      public AccessibleContext getAccessibleContext() {
          if (accessibleContext == null) {
              accessibleContext = new AccessibleJSplitButton();
          }
  
          return accessibleContext;
      }
  
      /* (non-Javadoc)
       * @see javax.swing.JComponent#getComponentPopupMenu()
       */
      @Override
      public JPopupMenu getComponentPopupMenu() {
          return (popupAction == null) ? null : popupAction.getPopupMenu();
      }
  
      /* (non-Javadoc)
       * @see javax.swing.JComponent#setComponentPopupMenu(javax.swing.JPopupMenu)
       */
      @Override
      public void setComponentPopupMenu(JPopupMenu popupMenu) {
          if (popupAction != null) {
              super.removeActionListener(popupAction);
              mainButton.removeActionListener(popupAction);
              popupButton.removeActionListener(popupAction);
              popupAction = null;
          }
  
          if (popupMenu != null) {
              popupAction = new PopupAction(popupMenu);
              popupButton.addActionListener(popupAction);
  
              if (alwaysShowPopup) {
                  super.addActionListener(popupAction);
                  mainButton.addActionListener(popupAction);
              }
          }
      }
  
      /* (non-Javadoc)
       * @see javax.swing.JComponent#getPopupLocation(java.awt.event.MouseEvent)
       */
      @Override
      public Point getPopupLocation(MouseEvent event) {
          return getPopupLocationRelativeTo(event.getComponent());
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#getSelectedObjects()
       */
      @Override
      public Object[] getSelectedObjects() {
          Object[] result = new Object[1];
  
          if (mainButton.isSelected()) {
              result[0] = mainButton;
          } else if (popupButton.isSelected()) {
              result[0] = popupButton;
          } else {
              return null;
          }
  
          return result;
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#getMnemonic()
       */
      @Override
      public int getMnemonic() {
          return mainButton.getMnemonic();
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#setMnemonic(int)
       */
      @Override
      public void setMnemonic(int mnemonic) {
          int oldValue = getMnemonic();
          firePropertyChange(MNEMONIC_CHANGED_PROPERTY, oldValue, mnemonic);
          mainButton.setMnemonic(mnemonic);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#getDisplayedMnemonicIndex()
       */
      @Override
      public int getDisplayedMnemonicIndex() {
          return mainButton.getDisplayedMnemonicIndex();
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#setDisplayedMnemonicIndex(int)
       */
      @Override
      public void setDisplayedMnemonicIndex(int index)
              throws IllegalArgumentException {
          int oldValue = getDisplayedMnemonicIndex();
          mainButton.setDisplayedMnemonicIndex(index);
          firePropertyChange("displayedMnemonicIndex", oldValue, index);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#setHideActionText(boolean)
       */
      @Override
      public void setHideActionText(boolean hideActionText) {
          mainButton.setHideActionText(hideActionText);
          super.setHideActionText(hideActionText);
      }
  
      /* (non-Javadoc)
       * @see javax.swing.JComponent#getUIClassID()
       */
      @Override
      public String getUIClassID() {
          return uiClassID;
      }
  
      /* (non-Javadoc)
       * @see javax.swing.AbstractButton#updateUI()
       */
      @Override
      public void updateUI() {
          setUI((ButtonUI) UIManager.getUI(this));
      }
  
      private Point getPopupLocationRelativeTo(Component comp) {
          Insets insets = getInsets();
          int height = getHeight();
  
          if (comp == popupButton) {
              Insets mainButtonInsets = mainButton.getInsets();
              int width = mainButton.getWidth();
              return new Point(-splitGap - width - mainButtonInsets.left - mainButtonInsets.right - 30, height);
          } else {
              Insets mainButtonInsets = mainButton.getInsets();
              int width = mainButton.getWidth() + mainButtonInsets.left +
                      mainButtonInsets.right + splitGap;
              if (comp == this) width += insets.left;
              return new Point(width, height);
          }
      }
  
      private class AccessibleJSplitButton extends AccessibleAbstractButton {
  
          private static final long serialVersionUID = 900001;
  
          @Override
          public AccessibleRole getAccessibleRole() {
              return AccessibleRole.PUSH_BUTTON;
          }
  
      }
  
      private class PopupAction implements ActionListener {
  
          private final JPopupMenu popupMenu;
  
          PopupAction(JPopupMenu popupMenu) {
              this.popupMenu = popupMenu;
          }
  
          /* (non-Javadoc)
           * @see java.awt.event.ActionListener#actionPerformed(
           * java.awt.event.ActionEvent)
           */
          @Override
          public void actionPerformed(ActionEvent e) {
              Component comp = (Component) e.getSource();
              Point popupLocation = getPopupLocationRelativeTo(comp);
              popupMenu.show(comp, popupLocation.x, popupLocation.y);
          }
  
          JPopupMenu getPopupMenu() {
              return popupMenu;
          }
  
      }
  }
  
  