001/**
002 * jshred - Shred's Toolbox
003 *
004 * Copyright (C) 2009 Richard "Shred" Körber
005 *   http://jshred.shredzone.org
006 *
007 * This program is free software: you can redistribute it and/or modify
008 * it under the terms of the GNU General Public License / GNU Lesser
009 * General Public License as published by the Free Software Foundation,
010 * either version 3 of the License, or (at your option) any later version.
011 *
012 * Licensed under the Apache License, Version 2.0 (the "License");
013 * you may not use this file except in compliance with the License.
014 *
015 * This program is distributed in the hope that it will be useful,
016 * but WITHOUT ANY WARRANTY; without even the implied warranty of
017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
018 *
019 */
020package net.shredzone.jshred.swing;
021
022import java.awt.BorderLayout;
023import java.awt.Component;
024import java.awt.Cursor;
025import java.awt.Insets;
026import java.awt.event.ActionEvent;
027import java.awt.event.ActionListener;
028import java.util.prefs.Preferences;
029
030import javax.swing.Icon;
031import javax.swing.JPanel;
032import javax.swing.JToggleButton;
033import javax.swing.SwingConstants;
034import javax.swing.event.ChangeEvent;
035import javax.swing.event.ChangeListener;
036
037/**
038 * This {@link JPanel} shows a Component with a headline above it. The user can click on
039 * the headline in order to collapse or exand the component. If the component is
040 * collapsed, it will be hidden and only the headline is shown. If the component is
041 * expanded, everything is shown.
042 * <p>
043 * {@link JCollapsiblePanel} can be used to allow the user to hide unimportant parts of
044 * the GUI if there is only little space available.
045 * <p>
046 * Due to a bug this component was not really functional until R12.
047 *
048 * @author Richard "Shred" Körber
049 * @since R9
050 */
051public class JCollapsiblePanel extends JPanel {
052    private static final long serialVersionUID = 3546645386727994681L;
053    private final static Preferences prefs = Preferences.userNodeForPackage(JCollapsiblePanel.class);
054
055    protected Component content;
056    private JToggleButton jbToggle;
057    private Icon iconCollapsed;
058    private Icon iconExpanded;
059    private String id; // unique id for remembering the collapse state
060    private final ListenerManager<ChangeListener> listener = new ListenerManager<>();
061
062    /**
063     * Creates an empty {@link JCollapsiblePanel} with no title.
064     */
065    public JCollapsiblePanel() {
066        this("");
067    }
068
069    /**
070     * Creates an empty {@link JCollapsiblePanel} with the given title.
071     */
072    public JCollapsiblePanel(String title) {
073        this(title, null);
074    }
075
076    /**
077     * Creates a {@link JCollapsiblePanel} with the given title and {@link Component}. The
078     * panel is initially expanded.
079     *
080     * @param title
081     *            Title
082     * @param comp
083     *            {@link Component} to be used as content
084     */
085    public JCollapsiblePanel(String title, Component comp) {
086        this(title, comp, true);
087    }
088
089    /**
090     * Creates a {@link JCollapsiblePanel} with the given title and {@link Component},
091     * using the given expanded state initially.
092     *
093     * @param title
094     *            Title
095     * @param comp
096     *            {@link Component} to be used as content
097     * @param expanded
098     *            Initial state, {@code true}: expanded, {@code false}: collapsed
099     */
100    public JCollapsiblePanel(String title, Component comp, boolean expanded) {
101        this(title, comp, expanded, null);
102    }
103
104    /**
105     * Creates a {@link JCollapsiblePanel} with the given title and {@link Component},
106     * using the given expanded state initially.
107     * <p>
108     * With the given id, the collapse state is remembered for the next time the
109     * application is started. If there is no state remembered, the given default
110     * "expanded" state will be used instead.
111     *
112     * @param title
113     *            Title
114     * @param comp
115     *            {@link Component} to be used as content
116     * @param expanded
117     *            Initial state, {@code true}: expanded, {@code false}: collapsed
118     * @param id
119     *            Unique identifier to remember the collapse state. Pass {@code null} if
120     *            the state shall not be remembered.
121     * @since R12
122     */
123    public JCollapsiblePanel(String title, Component comp, boolean expanded, String id) {
124        content = comp;
125        this.id = id;
126
127        // --- Check the preferences ---
128        if (id != null) {
129            expanded = prefs.getBoolean("state." + id, expanded);
130        }
131
132        // --- Create the toggle button ---
133        jbToggle = new JToggleButton(title);
134        jbToggle.setSelected(true);
135        jbToggle.setBorderPainted(false);
136        jbToggle.setBackground(getBackground().darker());
137        jbToggle.setRequestFocusEnabled(false);
138        jbToggle.setMargin(new Insets(0, 0, 0, 0));
139        jbToggle.setHorizontalTextPosition(SwingConstants.RIGHT);
140        jbToggle.setHorizontalAlignment(SwingConstants.LEFT);
141        jbToggle.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
142        jbToggle.addActionListener(new Listener());
143
144        // --- Initialize the button ---
145        setCollapsedIcon(new ArrowIcon(7, 7, SwingConstants.EAST));
146        setExpandedIcon(new ArrowIcon(7, 7, SwingConstants.SOUTH));
147
148        // --- Assemble the GUI ---
149        setLayout(new BorderLayout());
150        if (content != null) {
151            add(content, BorderLayout.CENTER);
152        }
153        add(jbToggle, BorderLayout.NORTH);
154
155        // --- Set the expanded state ---
156        setExpanded(expanded);
157    }
158
159    /**
160     * Sets the icon to be used in the title if the component is collapsed. This is an
161     * arrow pointing to the right by default.
162     *
163     * @param icon
164     *            New icon to be used
165     */
166    public void setCollapsedIcon(Icon icon) {
167        jbToggle.setIcon(icon);
168        firePropertyChange("collapsedicon", iconCollapsed, icon);
169        iconCollapsed = icon;
170    }
171
172    /**
173     * Gets the current icon to be used if the component is collapsed.
174     *
175     * @return Collapsed icon
176     */
177    public Icon getCollapsedIcon() {
178        return iconCollapsed;
179    }
180
181    /**
182     * Sets the icon to be used in the title if the component is expanded. This is an
183     * arrow pointing down by default.
184     *
185     * @param icon
186     *            New icon to be used
187     */
188    public void setExpandedIcon(Icon icon) {
189        jbToggle.setSelectedIcon(icon);
190        firePropertyChange("expandedicon", iconExpanded, icon);
191        iconExpanded = icon;
192    }
193
194    /**
195     * Gets the current icon to be used if the component is expanded.
196     *
197     * @return Expanded icon
198     */
199    public Icon getExpandedIcon() {
200        return iconExpanded;
201    }
202
203    /**
204     * Sets the enabled state. Disabling the {@link JCollapsiblePanel} will only disable
205     * the title button, but not the content. I.e. the user cannot collapse the component,
206     * but can still use it.
207     * <p>
208     * <em>NOTE:</em> if {@link #setEnabled(boolean)} is set to {@code false} while the
209     * panel is collapsed, then the user will be unable to expand and use the component.
210     */
211    @Override
212    public void setEnabled(boolean b) {
213        super.setEnabled(b);
214        jbToggle.setEnabled(b);
215    }
216
217    /**
218     * Sets the expanded state. If {@code true}, the {@link Component} will be shown. If
219     * {@code false}, the Component will be hidden.
220     */
221    public void setExpanded(boolean b) {
222        jbToggle.setSelected(b);
223        doExpand(b);
224    }
225
226    /**
227     * Internal method that does the actual collapsing and expanding of the content.
228     *
229     * @param b
230     *            {@code true}: expand, {@code false}: collapse
231     */
232    private void doExpand(boolean b) {
233        if (content != null) {
234            content.setVisible(b);
235            revalidate();
236        }
237        if (id != null) {
238            prefs.putBoolean("state." + id, b);
239        }
240    }
241
242    /**
243     * Gets the current expanded state.
244     */
245    public boolean isExpanded() {
246        return jbToggle.isSelected();
247    }
248
249    /**
250     * Sets the title above the component.
251     */
252    public void setTitle(String title) {
253        firePropertyChange("title", jbToggle.getText(), title);
254        jbToggle.setText(title);
255    }
256
257    /**
258     * Gets the current title above the component.
259     */
260    public String getTitle() {
261        return jbToggle.getText();
262    }
263
264    /**
265     * Sets a new content {@link Component}. It will replace the current content.
266     */
267    public void setContent(Component comp) {
268        if (content != null) {
269            remove(content);
270        }
271        firePropertyChange("content", content, comp);
272
273        if (comp != null) {
274            add(comp, BorderLayout.CENTER);
275            doExpand(isExpanded());
276        }
277        content = comp;
278    }
279
280    /**
281     * Gets the current content {@link Component}.
282     *
283     * @return Content {@link Component}, or {@code null} if none was set.
284     */
285    public Component getContent() {
286        return content;
287    }
288
289    /**
290     * Adds a {@link ChangeListener}. It will be invoked when the collapsed/expanded state
291     * was changed.
292     *
293     * @param l
294     *            {@link ChangeListener} to be added
295     */
296    public void addChangeListener(ChangeListener l) {
297        listener.addListener(l);
298    }
299
300    /**
301     * Removes a {@link ChangeListener}.
302     *
303     * @param l
304     *            {@link ChangeListener} to be removed
305     */
306    public void removeChangeListener(ChangeListener l) {
307        listener.removeListener(l);
308    }
309
310    /**
311     * Private {@link ActionListener} implementation. It will be invoked when the title
312     * {@link JToggleButton} was pressed.
313     */
314    private class Listener implements ActionListener {
315        @Override
316        public void actionPerformed(ActionEvent e) {
317            if (content != null) {
318                // --- Change visibility ---
319                doExpand(jbToggle.isSelected());
320
321                // --- Notify everyone ---
322                ChangeEvent event = new ChangeEvent(JCollapsiblePanel.this);
323                for (ChangeListener l : listener.getListeners()) {
324                    l.stateChanged(event);
325                }
326            }
327        }
328    }
329
330}