Summary

JButton領域内にマウスカーソルが入ったら自動的にJPopupMenuを開き、JButtonJPopupMenu領域外にマウスカーソルが出たら自動的にそれを閉じるようイベントリスナーを設定します。

Source Code Examples

class AutoClosePopupMenu extends JPopupMenu {
  private transient PopupMenuListener listener;

  @Override public void updateUI() {
    removePopupMenuListener(listener);
    super.updateUI();
    listener = new AwtPopupMenuListener();
    addPopupMenuListener(listener);
  }

  private void checkAutoClose(MouseEvent e) {
    Component c = e.getComponent();
    Rectangle r = getBounds();
    r.grow(0, 5);
    Point pt = SwingUtilities.convertPoint(c, e.getPoint(), this);
    if (!r.contains(pt) && !(c instanceof JButton)) {
      setVisible(false);
    }
  }

  private final class AwtPopupMenuListener implements PopupMenuListener {
    private final AWTEventListener handler = e -> {
      if (e instanceof MouseEvent) {
        int id = e.getID();
        if (id == MouseEvent.MOUSE_MOVED || id == MouseEvent.MOUSE_EXITED) {
          checkAutoClose((MouseEvent) e);
        }
      }
    };

    @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
      long mask = AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK;
      Toolkit.getDefaultToolkit().addAWTEventListener(handler, mask);
    }

    @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
      Toolkit.getDefaultToolkit().removeAWTEventListener(handler);
    }

    @Override public void popupMenuCanceled(PopupMenuEvent e) {
      /* not needed */
    }
  }
}
View in GitHub: Java, Kotlin

Description

  • 左: MouseListener
    • JPopupMenuMouseListenerを追加し、mouseExitedイベントでJPopupMenuを閉じる
    • JPopupMenuから内部のJMenuItemにマウスカーソルが移動した際にもmouseExitedイベントが発生するので、mouseExitedイベントが発生かつエイム状態のJMenuItemが存在しない場合はJPopupMenuを閉じるよう設定
    • マウスカーソルを高速移動するとmouseExitedイベントが発生せず、JPopupMenuが閉じない場合がある
      • JPopupMenuの親Windowを半透明化、サイズ拡張してmouseExitedイベントが発生しやすくすれば回避が可能?
JPopupMenu popup = new JPopupMenu();
initPopupMenu(popup);
popup.addMouseListener(new MouseAdapter() {
  @Override public void mouseExited(MouseEvent e) {
    EventQueue.invokeLater(() -> {
    popup.addMouseListener(new MouseAdapter() {
      @Override public void mouseExited(MouseEvent e) {
        EventQueue.invokeLater(() -> {
          boolean isArmed = Stream.of(popup.getSubElements())
              .filter(AbstractButton.class::isInstance)
              .map(AbstractButton.class::cast)
              .map(AbstractButton::getModel)
              .anyMatch(ButtonModel::isArmed);
          if (!isArmed) {
            popup.setVisible(false);
          }
        });
      }
    });
  }
});
JButton button = new JButton(UIManager.getIcon("FileChooser.listViewIcon"));
button.setFocusPainted(false);
button.addActionListener(e -> {
  popup.show(button, 0, button.getHeight());
  popup.requestFocusInWindow();
});
button.addMouseListener(new MouseAdapter() {
  @Override public void mouseEntered(MouseEvent e) {
    ((AbstractButton) e.getComponent()).doClick();
  }
});
  • 右: AWTEventListener
    • JPopupMenuPopupMenuListenerを追加し、popupMenuWillBecomeVisibleイベントでToolkit.getDefaultToolkit().addAWTEventListener(...)でアプリケーション全体のマウスイベントを取得するAWTEventListenerToolkitに追加
      • このAWTEventListenerpopupMenuWillBecomeInvisibleイベントで削除する
    • JToolTip JMenuから開くサブJPopupMenuと同じ手法?
    • AWTEventListener#eventDispatched(...)イベントでJButtonJPopupMenu領域外にマウスカーソルが移動したらJPopupMenuを閉じる
    • JPopupMenuJButtonの親JFrame外にはみ出してHeavyWeightWindowで表示されている状態でJPopupMenu領域から直接アプリケーション領域外にマウスカーソルを移動するとMouseEvent.MOUSE_MOVEDイベントが発生しないため、代わりにMouseEvent.MOUSE_EXITEDイベントでアプリケーション領域外にマウスカーソルが存在するかを調査している

Reference

Comment