概要

JTabbedPaneのタブ用ツールヒントの形状をふきだしにし、そのしっぽの方向をタブ位置に応じて変更します。

サンプルコード

class BalloonToolTip extends JToolTip {
  private static final int SIZE = 4;
  private static final double ARC = 4d;
  private transient HierarchyListener listener;
  private transient Shape shape;

  @Override public void updateUI() {
    removeHierarchyListener(listener);
    super.updateUI();
    setLayout(new BorderLayout());
    listener = e -> {
      Component c = e.getComponent();
      if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0
            && c.isShowing()) {
        Optional.ofNullable(SwingUtilities.getWindowAncestor(c))
            .filter(w -> w.getType() == Window.Type.POPUP)
            .ifPresent(w -> w.setBackground(new Color(0x0, true)));
      }
    };
    addHierarchyListener(listener);
    setOpaque(false);
    setBorder(BorderFactory.createEmptyBorder(SIZE, SIZE, SIZE, SIZE));
  }

  @Override public Dimension getPreferredSize() {
    Dimension d = super.getPreferredSize();
    d.width += SIZE;
    d.height += SIZE;
    return d;
  }

  @Override protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setColor(getBackground());
    g2.fill(shape);
    g2.setPaint(getForeground());
    g2.draw(shape);
    g2.dispose();
    // super.paintComponent(g);
  }

  public void updateBalloonShape(int placement) {
    Insets i = getInsets();
    Dimension d = getPreferredSize();
    Path2D tail = new Path2D.Double();
    double w = d.getWidth() - i.left - i.right - 1d;
    double h = d.getHeight() - i.top - i.bottom - 1d;
    double cx = w / 2d;
    double cy = h / 2d;
    switch (placement) {
      case SwingConstants.LEFT:
        tail.moveTo(0, cy - SIZE);
        tail.lineTo(-SIZE, cy);
        tail.lineTo(0, cy + SIZE);
        break;
      case SwingConstants.RIGHT:
        tail.moveTo(w, cy - SIZE);
        tail.lineTo(w + SIZE, cy);
        tail.lineTo(w, cy + SIZE);
        break;
      case SwingConstants.BOTTOM:
        tail.moveTo(cx - SIZE, h);
        tail.lineTo(cx, h + SIZE);
        tail.lineTo(cx + SIZE, h);
        break;
      default: // case SwingConstants.TOP:
        tail.moveTo(cx - SIZE, 0);
        tail.lineTo(cx, -SIZE);
        tail.lineTo(cx + SIZE, 0);
    }
    Area area = new Area(new RoundRectangle2D.Double(0, 0, w, h, ARC, ARC));
    area.add(new Area(tail));
    AffineTransform at = AffineTransform.getTranslateInstance(i.left, i.top);
    shape = at.createTransformedShape(area);
  }
}
View in GitHub: Java, Kotlin

解説

  • JToolTipの形状変更はJToolTipの形状を吹き出し風に変更すると同様の方法を使用
    • JToolTipテキストが長くなるとJToolTipを常に親JFrame内に表示される方向に設定してもHeavyWeightWindowが使用される可能性があるのでJWindowを透明する設定をそのまま使用している
  • JToolTipが初回表示される前にJToolTip#getVisibleRect()を実行してもその正しいサイズが取得できないので代わりにJToolTip#getPreferredSize()を使用する必要がある
  • JTabbedPane.TOP
    • ふきだしのしっぽの三角形を上辺中央に描画し、対象タブ領域の下辺中央に三角形の頂点が接するようにJToolTipを配置
  • JTabbedPane.BOTTOM
    • ふきだしのしっぽの三角形を下辺中央に描画し、対象タブ領域の上辺中央に三角形の頂点が接するようにJToolTipを配置
  • JTabbedPane.LEFT
    • ふきだしのしっぽの三角形を左辺中央に描画し、対象タブ領域の右辺中央に三角形の頂点が接するようにJToolTipを配置
  • JTabbedPane.RIGHT
    • ふきだしのしっぽの三角形を右辺中央に描画し、対象タブ領域の左辺中央に三角形の頂点が接するようにJToolTipを配置
  • JToolTipの表示位置はJTabbedPane#getToolTipLocation(...)を以下のようにオーバーライドして変更
    • NimbusLookAndFeelなどでJToolTipの形が変更できない
      • UIDefault#put("ToolTip[Enabled].backgroundPainter", Painter<JToolTip>...)などで背景を描画しないPainter<JToolTip>を設定する、またはJToolTipに直接テキストを描画するのではなくJLabelを追加してテキストを描画すれば回避可能
JTabbedPane tabs = new JTabbedPane(
    SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT) {
  private transient BalloonToolTip tip;
  private final JLabel label = new JLabel(" ", CENTER);

  @Override public Point getToolTipLocation(MouseEvent e) {
    int idx = indexAtLocation(e.getX(), e.getY());
    String txt = idx >= 0 ? getToolTipTextAt(idx) : null;
    return Optional.ofNullable(txt).map(toolTipText -> {
      JToolTip tips = createToolTip();
      tips.setTipText(toolTipText);
      label.setText(toolTipText);
      if (tips instanceof BalloonToolTip) {
        ((BalloonToolTip) tips).updateBalloonShape(getTabPlacement());
      }
      return getToolTipPoint(
          getBoundsAt(idx), tips.getPreferredSize());
    }).orElse(null);
  }

  private Point getToolTipPoint(Rectangle r, Dimension d) {
    double dx;
    double dy;
    switch (getTabPlacement()) {
      case LEFT:
        dx = r.getMaxX();
        dy = r.getCenterY() - d.getHeight() / 2d;
        break;
      case RIGHT:
        dx = r.getMinX() - d.width;
        dy = r.getCenterY() - d.getHeight() / 2d;
        break;
      case BOTTOM:
        dx = r.getCenterX() - d.getWidth() / 2d;
        dy = r.getMinY() - d.height;
        break;
      default: // case TOP:
        dx = r.getCenterX() - d.getWidth() / 2d;
        dy = r.getMaxY();
    }
    return new Point((int) (dx + .5), (int) (dy + .5));
  }

  @Override public JToolTip createToolTip() {
    if (tip == null) {
      tip = new BalloonToolTip();
      LookAndFeel.installColorsAndFont(
          label,
          "ToolTip.background",
          "ToolTip.foreground",
          "ToolTip.font");
      tip.add(label);
      tip.updateBalloonShape(getTabPlacement());
      tip.setComponent(this);
    }
    return tip;
  }

  @Override public void updateUI() {
    tip = null;
    super.updateUI();
  }
};

参考リンク

コメント