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}