• category: swing folder: TabAreaPopupMenu title: JTabbedPaneのTabAreaで開くJPopupMenuを設定する tags: [JTabbedPane, JPopupMenu] author: aterai pubdate: 2024-01-29T05:46:34+09:00 description: JTabbedPaneのタブ上とTabArea内では異なるJPopupMenuを開くよう設定します。 image: https://drive.google.com/uc?id=1gYHccpOFLZywYrdW0agN_qhRAitbFgtF

概要

JTabbedPaneのタブ上とTabArea内では異なるJPopupMenuを開くよう設定します。

サンプルコード

JTabbedPane tabbedPane = new JTabbedPane() {
  private final JPopupMenu popup1 = makeTabPopupMenu();
  private final JPopupMenu popup2 = makeTabAreaPopupMenu();

  @Override public void updateUI() {
    super.updateUI();
    EventQueue.invokeLater(() -> {
      SwingUtilities.updateComponentTreeUI(popup1);
      SwingUtilities.updateComponentTreeUI(popup2);
      setComponentPopupMenu(popup1);
    });
  }

  @Override public Point getPopupLocation(MouseEvent e) {
    int idx = indexAtLocation(e.getX(), e.getY());
    if (idx < 0 && getTabAreaBounds().contains(e.getPoint())) {
      setComponentPopupMenu(popup2);
    } else {
      setComponentPopupMenu(popup1);
    }
    return super.getPopupLocation(e);
  }

  private Rectangle getTabAreaBounds() {
    Rectangle r = SwingUtilities.calculateInnerArea(this, null);
    Rectangle cr = Optional.ofNullable(getSelectedComponent())
        .map(Component::getBounds)
        .orElseGet(Rectangle::new);
    int tp = getTabPlacement();
    // Note: don't call BasicTabbedPaneUI#getTabAreaInsets(),
    // because it causes rotation.
    Insets i1 = UIManager.getInsets("TabbedPane.tabAreaInsets");
    Insets i2 = UIManager.getInsets("TabbedPane.contentBorderInsets");
    if (tp == TOP || tp == BOTTOM) {
      r.height -= cr.height + i1.top + i1.bottom + i2.top + i2.bottom;
      // r.x += i1.left;
      r.y += tp == TOP ? i1.top : cr.y + cr.height + i1.bottom + i2.bottom;
    } else {
      r.width -= cr.width + i1.top + i1.bottom + i2.left + i2.right;
      r.x += tp == LEFT ? i1.top : cr.x + cr.width + i1.bottom + i2.right;
      // r.y += i1.left;
    }
    return r;
  }
};
View in GitHub: Java, Kotlin

解説

  • タブ上用popup1TabArea内用popup22種類のJPopupMenuを用意
  • JTabbedPane#getPopupLocation(MouseEvent)をオーバーライドしてクリック位置によって使用するJPopupMenuを切り替える
    • JTabbedPane#getPopupLocation(MouseEvent)JTabbedPane#getComponentPopupMenu()nullで未設定の場合は実行されないので、初期状態で適当なJPopupMenuを設定しておく必要がある
    • TabArea内、かつJTabbedPane#indexAtLocation(...)が負でクリック位置がタブ上でない場合はTabArea内用のpopup2JTabbedPane#setComponentPopupMenu(popup2)で設定
    • それ以外の場合はタブ上用のpopup1JTabbedPane#setComponentPopupMenu(popup1)で設定
      • タブコンテナ内のコンポーネントにマウスリスナーが未設定の場合タブ上用のpopup1JPopupMenuとして使用される
  • ひとつのJPopupMenuJPopupMenu#show(invoker, x, y)をオーバーライドし、このxy座標で使用するJMenuItemを入れ替える方法もある
    • どちらを使用する場合も動作中にLookAndFeelを変更する場合は手動でSwingUtilities.updateComponentTreeUI(...)を実行して現在使用していないJPopupMenuJMenuItemLookAndFeelを更新する必要がある
  • JTabbedPane#setTabComponentAt(...)で設定したタブコンポーネントにJPopupMenuを設定して以下のようにJTabbedPane#getComponentPopupMenu()で切り替える方法もあるが、マウスクリックによるタブ移動が不可になる場合がある
@Override public JPopupMenu getComponentPopupMenu() {
  int idx = getSelectedIndex();
  Component c = getTabComponentAt(idx);
  JPopupMenu popup;
  if (idx>= 0 && c instanceof JComponent) {
    popup = ((JComponent) c).getComponentPopupMenu();
  } else {
    popup = super.getComponentPopupMenu();
  }
  return popup;
}
  • TabbedPane.tabAreaInsetsTabbedPane.contentBorderInsetsの余白を考慮してTabArea領域を計算するよう修正
    • TabbedPane.tabAreaInsetsはタブ配置位置によって上下左右の余白が入れ替わる場合がある
    • TabbedPane.contentBorderInsetsは変化しない
    • 入れ替え方法はBasicTabbedPaneUI#rotateInsets(...)を参照
protected static void rotateInsets(
    Insets topInsets, Insets targetInsets, int targetPlacement) {
  switch(targetPlacement) {
    case LEFT:
      targetInsets.top = topInsets.left;
      targetInsets.left = topInsets.top;
      targetInsets.bottom = topInsets.right;
      targetInsets.right = topInsets.bottom;
      break;
    case BOTTOM:
      targetInsets.top = topInsets.bottom;
      targetInsets.left = topInsets.left;
      targetInsets.bottom = topInsets.top;
      targetInsets.right = topInsets.right;
      break;
    case RIGHT:
      targetInsets.top = topInsets.left;
      targetInsets.left = topInsets.bottom;
      targetInsets.bottom = topInsets.right;
      targetInsets.right = topInsets.top;
      break;
    case TOP:
    default:
      targetInsets.top = topInsets.top;
      targetInsets.left = topInsets.left;
      targetInsets.bottom = topInsets.bottom;
      targetInsets.right = topInsets.right;
  }
}
  • スクロールタブレイアウトの場合のみで十分な場合は、以下のように名前がTabbedPane.scrollableViewportJViewportを検索してエッジボタンを除くTabArea領域を取得する方法もある
private static Rectangle getTabAreaBounds2(JTabbedPane tabbedPane) {
  JComponent c = descendants(tabbedPane)
      .filter(JViewport.class::isInstance)
      .map(JComponent.class::cast)
      .filter(v -> "TabbedPane.scrollableViewport".equals(v.getName()))
      .findFirst().orElse(null);
  Rectangle r = SwingUtilities.calculateInnerArea(c, null);
  // Note: BasicTabbedPaneUI#getTabAreaInsets() causes rotation.
  Insets tabAreaInsets = UIManager.getInsets("TabbedPane.tabAreaInsets");
  Insets targetInsets = new Insets(0, 0, 0, 0);
  rotateInsets(tabAreaInsets, targetInsets, tabbedPane.getTabPlacement());
  if (r != null) {
    r.x += tabAreaInsets.left;
    r.y += tabAreaInsets.top;
    r.width -= tabAreaInsets.left + tabAreaInsets.right;
    r.height -= tabAreaInsets.top + tabAreaInsets.bottom;
  }
  return r;
}

private static Stream<Component> descendants(Container parent) {
  return Stream.of(parent.getComponents())
      .filter(Container.class::isInstance).map(Container.class::cast)
      .flatMap(c -> Stream.concat(Stream.of(c), descendants(c)));
}

参考リンク

コメント