• category: swing folder: TabbedPaneSelectionFollowsFocus title: JTabbedPaneの選択タブとフォーカスタブを分離する tags: [JTabbedPane, UIManager, Focus, InputMap, LookAndFeel] author: aterai pubdate: 2023-04-03T03:44:40+09:00 description: JTabbedPaneのキー入力によるタブ移動で選択タブとフォーカスタブを一致させるか、または別々に扱うかを設定で切り替えます。 image: https://drive.google.com/uc?id=1mS3Bmh6B7dAqMMBMM3TiCTRLVoHZzxiy

概要

JTabbedPaneのキー入力によるタブ移動で選択タブとフォーカスタブを一致させるか、または別々に扱うかを設定で切り替えます。

サンプルコード

JTabbedPane tabs = new JTabbedPane() {
  @Override public void updateUI() {
    super.updateUI();
    if (getUI() instanceof MetalTabbedPaneUI) {
      setUI(new MetalTabbedPaneUI() {
        @Override protected void navigateSelectedTab(int direction) {
          super.navigateSelectedTab(direction);
          focusIndex = getFocusIndex();
        }
      });
    }
  }
};
tabs.addChangeListener(e -> {
  // System.out.println("Tab selected");
  focusIndex = tabs.getSelectedIndex();
  // focusIndex = -1;
  tabs.repaint();
});
tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
String help1 = "SPACE: selectTabWithFocus";
String help2 = "LEFT: navigateLeft";
String help3 = "RIGHT: navigateRight";
JTextArea textArea = new JTextArea(String.join("\n", help1, help2, help3));
textArea.setEditable(false);
tabs.addTab("help", new JScrollPane(textArea));
IntStream.range(0, 10).forEach(i -> tabs.addTab("title" + i, new JLabel("JLabel" + i)));

InputMap im = tabs.getInputMap(WHEN_FOCUSED);
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "selectTabWithFocus");

String key = "TabbedPane.selectionFollowsFocus";
JCheckBox check = new JCheckBox(key, UIManager.getBoolean(key)) {
  @Override public void updateUI() {
    super.updateUI();
    boolean b = UIManager.getLookAndFeelDefaults().getBoolean(key);
    setSelected(b);
    UIManager.put(key, b);
  }
};
check.setFocusable(false);
check.addActionListener(e -> {
  boolean b = ((JCheckBox) e.getSource()).isSelected();
  UIManager.put(key, b);
  SwingUtilities.updateComponentTreeUI(tabs);
});

add(new JLayer<>(tabs, new LayerUI<JTabbedPane>() {
  @Override public void paint(Graphics g, JComponent c) {
    super.paint(g, c);
    if (c instanceof JLayer) {
      JLayer<?> layer = (JLayer<?>) c;
      JTabbedPane tabbedPane = (JTabbedPane) layer.getView();
      if (focusIndex >= 0 && focusIndex != tabbedPane.getSelectedIndex()) {
        Rectangle r = tabbedPane.getBoundsAt(focusIndex);
        Graphics2D g2 = (Graphics2D) g.create();
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f));
        g2.setPaint(Color.RED);
        g2.fill(r);
        g2.dispose();
      }
    }
  }
}));
View in GitHub: Java, Kotlin

解説

  • TabbedPane.selectionFollowsFocus: true
    • MetalLookAndFeel, WindowsLookAndFeel, GTKLookAndFeelなどのデフォルトでカーソルキーなどによるタブ選択移動にフォーカスタブも同期して移動
    • マウスクリックでのタブ選択ではこの設定に関係なく選択タブとフォーカスタブは一致する
      • WindowsLookAndFeelなどのマウスカーソルによるロールオーバータブとフォーカスタブは別物
  • TabbedPane.selectionFollowsFocus: false
    • NimbusLookAndFeelのデフォルトだが、NimbusLookAndFeelはこの設定を無視してカーソルキーなどによるタブ選択移動にフォーカスタブも同期して移動する
    • MetalLookAndFeel, WindowsLookAndFeelではフォーカスタブは選択タブと別々になるがフォーカスタブの描画は選択されていないタブと同じになって見分けがつかない
      • このサンプルではMetalLookAndFeelを適用した場合のみ、テスト用として選択タブとフォーカスタブを半透明の赤色で描画するJLayerJTabbedPaneに設定している
      • BasicTabbedPaneUI#getFocusIndex()protectedBasicTabbedPaneUI#setFocusIndex(...)はパッケージプライベートメソッドなので代わりにBasicTabbedPaneUI#navigateSelectedTab(...)をオーバーライドしてfocusIndexJLayerに伝えている
    • GTKLookAndFeelではフォーカスタブにラウンド矩形のBorderが表示され、デフォルトでSPACEキーでフォーカスタブが選択タブになるActionが実行される
      • このサンプルではGTKLookAndFeel以外の場合でもこのselectTabWithFocusアクションをSPACEキーで実行するよう以下のようなInputMapを設定している
        InputMap im = tabs.getInputMap(WHEN_FOCUSED);
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "selectTabWithFocus");
        

参考リンク

コメント