Summary

JButtonに展開・折り畳みアニメーションの開始・終了やクリック領域毎のアクション振り分けを行うJLayerを設定して分割ボタンを作成します。

Source Code Examples

class SplitButtonLayerUI extends LayerUI<ExpandableSplitButton> {
  @Override public void installUI(JComponent c) {
    super.installUI(c);
    ((JLayer<?>) c).setLayerEventMask(
        AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
  }

  @Override public void uninstallUI(JComponent c) {
    ((JLayer<?>) c).setLayerEventMask(0);
    super.uninstallUI(c);
  }

  @Override protected void processMouseEvent(
      MouseEvent e, JLayer<? extends ExpandableSplitButton> layer) {
    ExpandableSplitButton button = layer.getView();
    switch (e.getID()) {
      case MouseEvent.MOUSE_ENTERED:
        button.getAnim().expand();
        button.setForeground(new Color(
            UIManager.getColor("List.selectionForeground").getRGB()));
        break;

      case MouseEvent.MOUSE_EXITED:
        button.setMouseOnArrow(false);
        if (!button.isPopupOpen()) {
          button.getAnim().collapse();
        }
        button.setForeground(UIManager.getColor("List.foreground"));
        break;

      case MouseEvent.MOUSE_PRESSED:
      case MouseEvent.MOUSE_CLICKED:
      case MouseEvent.MOUSE_RELEASED:
        if (SwingUtilities.isRightMouseButton(e)) {
          // Right click is consumed to suppress automatic display of JPopupMenu
          e.consume();
        } else {
          // Left click: Popup for arrow area, Action for title area
          if (button.isOnArrowArea(e.getPoint())) {
            button.showPopup();
            e.consume();
          }
        }
        // Clicking on the title area does not consume
        //   → JButton executes the Action
        break;

      default:
        break;
    }
  }

  @Override protected void processMouseMotionEvent(
      MouseEvent e, JLayer<? extends ExpandableSplitButton> layer) {
    if (e.getID() == MouseEvent.MOUSE_MOVED) {
      ExpandableSplitButton btn = layer.getView();
      boolean onArrow = btn.isOnArrowArea(e.getPoint());
      if (onArrow != btn.isMouseOnArrow()) {
        btn.setMouseOnArrow(onArrow);
        btn.repaint();
      }
    }
  }
}
View in GitHub: Java, Kotlin

Description

JLayer(LayerUI)を使用することで、JButton本体に直接リスナーを追加することなく、マウスイベントを傍受してクリック領域に応じたアクションの振り分けやアニメーションのトリガーを制御しています。これにより、ExpandableSplitButton側は自身のレイアウト(幅の展開・折り畳み)とコンポーネント自体の描画処理のみ担当しています。

  • ExpandableSplitButton:
    • マウスカーソルがボタン領域上に入ったとき(JButton#getModel()#isRollover()==trueになる)、JButton#getPreferredSize()の幅を徐々に変更(拡張)するTimerアニメーションを起動
    • 拡張された領域にポップアップ表示用の矢印アイコンを描画し、マウスがその矢印領域内にあるかどうかでアイコンのハイライト色を変更
    • ポップアップメニューが閉じられたとき、またはマウスがコンポーネント外に出たときに折り畳みアニメーション(幅を元に戻す)を実行
  • SplitButtonLayerUI:
    • ExpandableSplitButtonをラップするJLayerに設定し、ボタンに対するすべてのマウスイベント(クリック、移動、出入り)を取得して処理を振り分ける
      • マウス移動時(MOUSE_MOVED)に座標をチェックし、カーソルが矢印領域内にあるかを判定
      • 通常のラベル・アイコン領域(タイトル領域)の左クリック時は、イベントを消費(consume())せずに透過させることで、JButton#setAction(...)で設定された本来のアクションを実行する
      • 拡張された矢印アイコン領域の左クリック時は、イベントを消費(consume())した上で、JButton#setComponentPopupMenu(...)で設定されたJPopupMenuを表示
      • 右クリック時は自動でポップアップメニューが表示されるのを抑制するため、イベントをすべて消費する

Reference

Comment