Summary

JPopupMenu内に配置した3つのJListで「hh:mm aa」形式の時刻を選択可能な時刻ピッカーを作成します。

Source Code Examples

class TimePickerPopup extends JPopupMenu {
  private final TimePickerField owner;
  private final JList<String> hourList;
  private final JList<String> minList;
  private final JList<String> ampmList;
  private final List<String> hourModel;
  private final List<String> minModel;

  protected TimePickerPopup(TimePickerField owner) {
    super();
    this.owner = owner;

    hourModel = IntStream.rangeClosed(1, 12)
        .mapToObj(h -> String.format("%02d", h))
        .collect(Collectors.toList());
    minModel = IntStream.range(0, 60)
        .mapToObj(m -> String.format("%02d", m))
        .collect(Collectors.toList());

    hourList = createList(hourModel.toArray(new String[0]));
    minList = createList(minModel.toArray(new String[0]));
    ampmList = createList(TimePickerField.getAmPmStrings());

    JPanel listsPanel = new JPanel(new GridBagLayout());
    listsPanel.setBorder(BorderFactory.createEmptyBorder(2, 6, 2, 6));

    GridBagConstraints c = new GridBagConstraints();
    c.fill = GridBagConstraints.BOTH;
    c.weightx = 1d;
    c.weighty = 1d;
    c.insets = new Insets(0, 2, 0, 2);

    c.gridx = GridBagConstraints.RELATIVE;
    Locale loc = Locale.getDefault();
    String hourLabel = ChronoField.HOUR_OF_DAY.getDisplayName(loc);
    listsPanel.add(createColumn(hourLabel, hourList, true), c);

    String minLabel = ChronoField.MINUTE_OF_HOUR.getDisplayName(loc);
    listsPanel.add(createColumn(minLabel, minList, true), c);

    String ampmLabel = getAmpmLabel(loc);
    listsPanel.add(createColumn(ampmLabel, ampmList, false), c);

    JPanel root = new JPanel(new BorderLayout(0, 0));
    root.add(listsPanel, BorderLayout.CENTER);
    root.add(createFooter(), BorderLayout.SOUTH);

    setLayout(new BorderLayout());
    add(root);

    addPopupMenuListener(new PopupMenuListener() {
      @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
        synchronizeFromField(owner.getTimeText());
        Point p = popupMenuLocation();
        if (p != null) {
          setInvoker(owner);
          setLocation(p.x, p.y);
        }
      }

      @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
        // No operation needed
      }

      @Override public void popupMenuCanceled(PopupMenuEvent e) {
        // No operation needed
      }
    });
  }

  private Point popupMenuLocation() {
    Component invoker = getInvoker();
    if (invoker == null || !invoker.isShowing()) {
      return null;
    }
    Point p = owner.getLocationOnScreen();
    p.y += owner.getHeight();
    return p;
  }

  @Override public void show(Component invoker, int x, int y) {
    setInvoker(invoker);
    Point p = popupMenuLocation();
    if (p != null) {
      setLocation(p.x, p.y);
      setVisible(true);
    } else {
      super.show(invoker, x, y);
    }
  }

  private static String getAmpmLabel(Locale loc) {
    String ampmRaw = ChronoField.AMPM_OF_DAY.getDisplayName(loc);
    String ampmLabel;
    boolean b1 = !Objects.equals(ampmRaw, "AmPmOfDay");
    boolean b2 = !Objects.equals(ampmRaw, "AMPM_OF_DAY");
    if (ampmRaw != null && !ampmRaw.isEmpty() && b1 && b2) {
      ampmLabel = ampmRaw;
    } else {
      String[] ap2 = DateFormatSymbols.getInstance(loc).getAmPmStrings();
      ampmLabel = ap2[0] + "/" + ap2[1];
    }
    return ampmLabel;
  }

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

  @Override public final void setLayout(LayoutManager mgr) {
    super.setLayout(mgr);
  }

  @Override public final Component add(Component comp) {
    return super.add(comp);
  }

  private JList<String> createList(String... model) {
    JList<String> list = new JList<>(model);
    list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    list.setFixedCellHeight(20);
    list.setFocusable(true);
    list.setCellRenderer(new DefaultListCellRenderer() {
      @Override public Component getListCellRendererComponent(
          JList<?> l, Object val, int idx, boolean sel, boolean focus) {
        super.getListCellRendererComponent(l, val, idx, sel, focus);
        setHorizontalAlignment(CENTER);
        Border border = BorderFactory.createEmptyBorder(1, 4, 1, 4);
        setBorder(border);
        return this;
      }
    });
    return list;
  }

  private JPanel createColumn(String label, JList<String> list, boolean alwaysScroll) {
    JLabel lbl = new JLabel(label, SwingConstants.CENTER);
    lbl.setBorder(BorderFactory.createEmptyBorder(0, 0, 2, 0));
    Component sp;
    if (alwaysScroll) {
      sp = new TranslucentScrollPane(list);
    } else {
      sp = new JScrollPane(list) {
        @Override public void updateUI() {
          super.updateUI();
          setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER);
        }
      };
    }
    JPanel col = new JPanel(new BorderLayout(0, 1));
    col.setOpaque(false);
    col.add(lbl, BorderLayout.NORTH);
    col.add(sp);
    return col;
  }

  private JPanel createFooter() {
    JButton resetBtn = new JButton("Now");
    resetBtn.addActionListener(e ->
        synchronizeFromField(TimePickerField.getNowString()));
    JButton okBtn = new JButton("OK");
    okBtn.addActionListener(e -> applyAndClose());
    JPanel footer = new JPanel(new FlowLayout(FlowLayout.TRAILING, 6, 1));
    footer.add(resetBtn);
    footer.add(okBtn);
    return footer;
  }

  public void synchronizeFromField(String text) {
    int[] t = TimePickerField.parseTime(text);
    int hour = t[0]; // 1..12
    int min = t[1]; // 0..59
    int ampm = t[2]; // 0=AM, 1=PM
    int hourIndex = Math.min(Math.max(hour - 1, 0), 11);
    hourList.setSelectedIndex(hourIndex);
    minList.setSelectedIndex(min);
    ampmList.setSelectedIndex(ampm);
    EventQueue.invokeLater(() -> {
      scrollToSelected(hourList);
      scrollToSelected(minList);
      scrollToSelected(ampmList);
    });
  }

  private void scrollToSelected(JList<?> list) {
    int idx = list.getSelectedIndex();
    if (idx >= 0) {
      Rectangle cell = list.getCellBounds(idx, idx);
      if (cell != null) {
        Rectangle vis = list.getVisibleRect();
        vis.y = Math.max(0, cell.y + cell.height / 2 - vis.height / 2);
        list.scrollRectToVisible(vis);
      }
    }
  }

  private void applyAndClose() {
    int hourIndex = hourList.getSelectedIndex();
    int minuteIndex = minList.getSelectedIndex();
    int ampmIndex = ampmList.getSelectedIndex();
    String hour = hourIndex >= 0 ? hourModel.get(hourIndex) : "12";
    String min = minuteIndex >= 0 ? minModel.get(minuteIndex) : "00";
    String[] ampmStrings = TimePickerField.getAmPmStrings();
    String ampm = ampmIndex == 1 ? ampmStrings[1] : ampmStrings[0];
    owner.applyTime(hour + ":" + min + " " + ampm.toUpperCase(Locale.ENGLISH));
    setVisible(false);
  }
}
View in GitHub: Java, Kotlin

Description

JFormattedTextFieldの右端にドロップダウン用JButtonを重ねて配置し、ロケール対応の3JListを配置したJPopupMenuから時刻を選択できるピッカーを作成します。

  • JFormattedTextField:
    • OverlayLayoutを親JPanelに設定してJFormattedTextFieldの右端の領域にドロップダウン起動用JButtonを重ねて配置
    • MaskFormatter##:## **##:## UU」形式のマスクを設定して時刻の入力、表示を制限
  • JButton:
    • JButton#setComponentPopupMenu(...)で設定され、WindowsLookAndFeelなどの環境でボタンが右クリックされた場合でも、マウスポインタの位置ではなく左クリックと同様に親JPanelの左下端を基準とした適正位置へ配置されるようJPopupMenu#show()メソッドやPopupMenuListener内で表示座標を補正
    • 可視化直前にJPopup#synchronizeFromField()を呼び出し、JFormattedTextFieldの時刻をJPopupMenu内に配置したJListの表示する時刻と同期
  • JPopupMenu:
    • JPopupMenuのメイン領域には、時(01..12)、分(00..59)、AM/PM(ロケール対応表示名)を個別に選択可能な3つのJListGridBagLayoutで横一列に配置
    • 時・分の領域には垂直スクロールバーを常時表示
    • ポップアップが表示される直前に、現在テキストフィールドに保持されている時刻文字列を解析し、各JListの選択状態を同期
    • ChronoFieldDateFormatSymbolsを使用して実行環境のロケール(日本語環境なら「午前/午後」、英語環境なら「AM/PM」など)に合致した表示ラベルやリスト要素へ切り替え可能に設定
  • JScrollPane(JList):
    • 時・分用の縦スクロールバーは常時表示、AM/PM用は非表示なので3つの列幅の推奨サイズが不均等になるが、これを回避して等幅でレイアウトするため縦JScrollBarJViewport(JList)の上に重ねて半透明描画するカスタムJScrollPaneを使用している
    • 時刻の同期後、選択された時・分要素が可能なかぎりスクロール領域の中央付近へ表示されるようJList#scrollRectToVisible(...)を用いてビューポート位置を調整している

Reference

Comment