From 9955011dad278183c77dcebb74864ace8ad52e3c Mon Sep 17 00:00:00 2001 From: Benjamin Culkin Date: Sun, 7 Dec 2025 17:19:17 -0500 Subject: Add a collapsible JPanel Adds a collapsible version of a JPanel, useful in various places --- .../examples/gui/panels/CollapsiblePanelDemo.java | 71 +++++++ .../bjc/utils/gui/panels/CollapsiblePanel.java | 217 +++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 base/src/examples/java/bjc/utils/examples/gui/panels/CollapsiblePanelDemo.java create mode 100644 base/src/main/java/bjc/utils/gui/panels/CollapsiblePanel.java diff --git a/base/src/examples/java/bjc/utils/examples/gui/panels/CollapsiblePanelDemo.java b/base/src/examples/java/bjc/utils/examples/gui/panels/CollapsiblePanelDemo.java new file mode 100644 index 0000000..fed128f --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/gui/panels/CollapsiblePanelDemo.java @@ -0,0 +1,71 @@ +package bjc.utils.examples.gui.panels; + +import java.awt.BorderLayout; +import java.awt.GridLayout; +import java.awt.Insets; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import bjc.utils.gui.panels.CollapsiblePanel; + +public class CollapsiblePanelDemo { + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + try { UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); } catch (Exception ignored) {} + + JFrame frame = new JFrame("CollapsiblePanel Demo"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setLayout(new BorderLayout()); + + JPanel content = new JPanel(); + content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS)); + + CollapsiblePanel section1 = new CollapsiblePanel("Search Options"); + // Add some content + section1.add(new JLabel("Keyword:")); + section1.add(new JTextField(20)); + section1.add(new JCheckBox("Case sensitive")); + + // Add custom header bits (e.g. a small gear button) + JButton gear = new JButton("\u2699"); // ⚙ + gear.setMargin(new Insets(0, 4, 0, 4)); + gear.setFocusable(false); + section1.addHeaderComponent(gear); + + CollapsiblePanel section2 = new CollapsiblePanel("Advanced Filters", new GridLayout(0, 2, 4, 4)); + section2.add(new JLabel("From date:")); + section2.add(new JTextField(10)); + section2.add(new JLabel("To date:")); + section2.add(new JTextField(10)); + section2.add(new JLabel("Status:")); + section2.add(new JComboBox<>(new String[]{"Any", "Open", "Closed"})); + section2.setCollapsed(true); // start collapsed + + // Example: a right-aligned "Reset" button in the header + JButton resetBtn = new JButton("Reset"); + resetBtn.setMargin(new Insets(0, 6, 0, 6)); + resetBtn.setFocusable(false); + section2.setHeaderComponent(resetBtn); + + content.add(section1); + content.add(Box.createVerticalStrut(8)); + content.add(section2); + + frame.add(new JScrollPane(content), BorderLayout.CENTER); + frame.setSize(450, 300); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } +} diff --git a/base/src/main/java/bjc/utils/gui/panels/CollapsiblePanel.java b/base/src/main/java/bjc/utils/gui/panels/CollapsiblePanel.java new file mode 100644 index 0000000..6b4453b --- /dev/null +++ b/base/src/main/java/bjc/utils/gui/panels/CollapsiblePanel.java @@ -0,0 +1,217 @@ +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; + } +} -- cgit v1.2.3