package bjc.utils.gui.panels; import javax.swing.*; import javax.swing.border.EmptyBorder; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; /** * A JPanel with a clickable header that can collapse/expand its content. * * Features: * - Title + arrow (▶ / ▼) * - Optional custom header component on the right side (e.g., buttons, filters) * - Add children like a normal JPanel (they go into the content area) */ public class CollapsiblePanel extends JPanel { private static final long serialVersionUID = -5941733171067755926L; private final JPanel headerPanel; private final JPanel headerClickableArea; private final JPanel headerExtrasPanel; private final JButton toggleButton; private final JLabel titleLabel; private final JPanel contentPanel; private boolean collapsed = false; private boolean internalAdd = false; // guard for addImpl /** * Create a new collapsible panel. * * @param title The title for the panel */ public CollapsiblePanel(String title) { this(title, new FlowLayout(FlowLayout.LEFT, 4, 4)); } /** * Create a new collapsible panel, using the following layout manager for the content. * * @param title The title for the panel * @param contentLayout The layout manager for the panel */ public CollapsiblePanel(String title, LayoutManager contentLayout) { super(new BorderLayout()); // === Header === headerPanel = new JPanel(new BorderLayout(4, 0)); headerPanel.setBorder(new EmptyBorder(2, 4, 2, 4)); headerPanel.setOpaque(false); // Left side: clickable region (arrow + title) headerClickableArea = new JPanel(new BorderLayout(4, 0)); headerClickableArea.setOpaque(false); toggleButton = new JButton("\u25BC"); // ▼ expanded toggleButton.setMargin(new Insets(0, 4, 0, 4)); toggleButton.setFocusable(false); toggleButton.setBorderPainted(false); toggleButton.setContentAreaFilled(false); titleLabel = new JLabel(title); titleLabel.setBorder(new EmptyBorder(0, 2, 0, 0)); titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD)); headerClickableArea.add(toggleButton, BorderLayout.WEST); headerClickableArea.add(titleLabel, BorderLayout.CENTER); // Right side: custom header component container headerExtrasPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0)); headerExtrasPanel.setOpaque(false); headerPanel.add(headerClickableArea, BorderLayout.CENTER); headerPanel.add(headerExtrasPanel, BorderLayout.EAST); // === Content === contentPanel = new JPanel(contentLayout); // === Wiring: toggle behavior === toggleButton.addActionListener(e -> setCollapsed(!isCollapsed())); MouseAdapter headerClick = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) { setCollapsed(!isCollapsed()); } } }; // Only the left side (arrow + title) toggles; extras panel is not clickable for // toggle headerClickableArea.addMouseListener(headerClick); titleLabel.addMouseListener(headerClick); toggleButton.addMouseListener(headerClick); // Add header + content to this panel internalAdd = true; add(headerPanel, BorderLayout.NORTH); add(contentPanel, BorderLayout.CENTER); internalAdd = false; } /** * Returns the content panel; you can use this if you want direct access. * * @return The content panel */ public JPanel getContentPanel() { return contentPanel; } /** * Returns the panel that holds extra header components (right side). * * @return The header extras panel */ public JPanel getHeaderExtrasPanel() { return headerExtrasPanel; } /** * Replace any existing header extras with a single component. Pass null to * clear. * * @param comp The header component to use */ public void setHeaderComponent(Component comp) { headerExtrasPanel.removeAll(); if (comp != null) { headerExtrasPanel.add(comp); } headerExtrasPanel.revalidate(); headerExtrasPanel.repaint(); } /** * Add an extra component to the header (right side) without clearing existing * ones. * * * @param comp The component to add */ public void addHeaderComponent(Component comp) { headerExtrasPanel.add(comp); headerExtrasPanel.revalidate(); headerExtrasPanel.repaint(); } /** * Set the header title text. * * @param title The header title */ public void setTitle(String title) { titleLabel.setText(title); } /** * Get the header title * * @return The header title */ public String getTitle() { return titleLabel.getText(); } /** * Collapse or expand the content. * * @param collapsed True to collapse the panel, false to expand it */ public void setCollapsed(boolean collapsed) { if (this.collapsed == collapsed) return; this.collapsed = collapsed; contentPanel.setVisible(!collapsed); toggleButton.setText(collapsed ? "\u25B6" : "\u25BC"); // ▶ / ▼ revalidate(); repaint(); } /** * Check if the panel is collapsed or not. * * @return Is the panel collapsed or not? */ public boolean isCollapsed() { return collapsed; } /** * Override addImpl so that user-added components go into the content area * instead of directly into the outer BorderLayout. */ @Override protected void addImpl(Component comp, Object constraints, int index) { if (internalAdd) { super.addImpl(comp, constraints, index); } else { contentPanel.add(comp, constraints, index); } } /** * Convenience factory for a titled, initially-collapsed panel. * * @param title The title for the panel * @param layout The layout manager for the content * @return The panel created */ public static CollapsiblePanel collapsed(String title, LayoutManager layout) { CollapsiblePanel p = new CollapsiblePanel(title, layout); p.setCollapsed(true); return p; } }