/*
* Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.components;
import com.codename1.ui.Button;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.FontImage;
import com.codename1.ui.Image;
import com.codename1.ui.Label;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.plaf.UIManager;
/**
* <p>The {@code Accordion} ui pattern is a vertically stacked list of items.
* Each item can be opened/closed to reveal more content similarly to a {@link com.codename1.ui.tree.Tree}
* however unlike the {@link com.codename1.ui.tree.Tree} the {@code Accordion} is designed to include
* containers or arbitrary components rather than model based data.</p>
* <p>
* This makes the {@code Accordion} more convenient as a tool for folding/collapsing UI elements known in advance
* whereas a {@link com.codename1.ui.tree.Tree} makes more sense as a tool to map data e.g. filesystem
* structure, XML hierarchy etc.
* </p>
* <p>
* Note that the {@code Accordion} like many composite components in Codename One is scrollable by default
* which means you should use it within a non-scrollable hierarchy. If you wish to add it into a scrollable
* {@link com.codename1.ui.Container} you should disable it's default scrollability using {@code setScrollable(false)}.
* </p>
*
* <h3>Example Usage</h3>
*
* <script src="https://gist.github.com/codenameone/2b48d1650d8c5032d094066c79922cf1.js"></script>
*
* <h3>Screenshots</h3>
* <p><img src="https://www.codenameone.com/img/developer-guide/components-accordion.png" alt="Accordion Component"/></p>
*
*
* @author Chen
*/
public class Accordion extends Container {
private Image closeIcon;
private Image openIcon;
private boolean autoClose = true;
/**
* Empty Constructor
*/
public Accordion() {
super.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
closeIcon = FontImage.createMaterial(FontImage.MATERIAL_KEYBOARD_ARROW_RIGHT, UIManager.getInstance().getComponentStyle("Label"));
openIcon = FontImage.createMaterial(FontImage.MATERIAL_KEYBOARD_ARROW_DOWN, UIManager.getInstance().getComponentStyle("Label"));
setScrollableY(true);
}
/**
* Add an item to the Accordion Container
*
* @param header the item title
* @param body the item Component to hide/show
*/
public void addContent(String header, Component body) {
addContent(new Label(header), body);
}
/**
* Replaces the title for content that was already added. Notice that this will fail if the content isn't
* in yet.
* @param header the new title for the content
* @param body the content that was already added with a different header using addContent
*/
public void setHeader(String header, Component body) {
AccordionContent ac = (AccordionContent) body.getParent();
((Label)ac.header).setText(header);
}
/**
* Replaces the title for content that was already added. Notice that this will fail if the content isn't
* in yet.
* @param header the new title for the content
* @param body the content that was already added with a different header using addContent
*/
public void setHeader(Component header, Component body) {
AccordionContent ac = (AccordionContent) body.getParent();
ac.header.getParent().replace(ac.header, header, null);
}
/**
* Removes the content from the accordion
* @param body the body previously added with {@link #addContent(com.codename1.ui.Component, com.codename1.ui.Component)} or
*/
public void removeContent(Component body) {
body.getParent().remove();
body.remove();
}
/**
* Add an item to the Accordion Container
*
* @param header the item title Component
* @param body the item Component to hide/show
*/
public void addContent(Component header, Component body) {
add(new AccordionContent(header, body));
}
/**
* Returns the body component of the currently expanded accordion element or null if none is expanded
* @return a component
*/
public Component getCurrentlyExpanded() {
for (Component cc : this) {
AccordionContent c = (AccordionContent)cc;
if(!c.isClosed()){
return c.body;
}
}
return null;
}
/**
* Expands the accordion with the given "body"
* @param body the body component of the accordion to expand
*/
public void expand(Component body) {
if(autoClose) {
for (Component cc : this) {
AccordionContent c = (AccordionContent)cc;
c.openClose(!(body == c.body));
}
} else {
for (Component cc : this) {
AccordionContent c = (AccordionContent)cc;
if(body == c.body) {
c.openClose(false);
}
}
}
}
/**
* Closes the accordion with the given "body"
* @param body the body component of the accordion to close
*/
public void collapse(Component body) {
for (Component cc : this) {
AccordionContent c = (AccordionContent)cc;
if(body == c.body) {
c.openClose(true);
}
}
}
/**
* Sets the closed icon
* @param closeIcon the close icon
*/
public void setCloseIcon(Image closeIcon) {
this.closeIcon = closeIcon;
}
/**
* Sets the open icon
* @param openIcon the open icon
*/
public void setOpenIcon(Image openIcon) {
this.openIcon = openIcon;
}
/**
* Sets the auto close flag, if this flag is true clicking on an item to open
* an item will automatically close the previous opened item.
*
* @param autoClose determines if more then 1 item can be opened on screen
*/
public void setAutoClose(boolean autoClose) {
this.autoClose = autoClose;
}
class AccordionContent extends Container {
private boolean closed = true;
private Button arrow = new Button();
private Component body;
Component header;
public AccordionContent(Component header, final Component body) {
setUIID("AccordionItem");
setLayout(new BorderLayout());
this.body = body;
this.header = header;
header.setSelectedStyle(header.getUnselectedStyle());
header.setPressedStyle(header.getUnselectedStyle());
Container top = new Container(new BorderLayout());
top.add(BorderLayout.CENTER, header);
top.setUIID("AccordionHeader");
arrow.setUIID("AccordionArrow");
arrow.setIcon(closeIcon);
arrow.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
//toggle the current
openClose(!isClosed());
if(autoClose){
for (int i = 0; i < Accordion.this.getComponentCount(); i++) {
AccordionContent c = (AccordionContent)Accordion.this.getComponentAt(i);
if(c != AccordionContent.this && !c.isClosed()){
c.openClose(true);
}
}
}
Accordion.this.animateLayout(250);
}
});
top.add(BorderLayout.EAST, arrow);
top.setLeadComponent(arrow);
add(BorderLayout.NORTH, top);
body.setHidden(true);
add(BorderLayout.CENTER, body);
}
public boolean isClosed() {
return closed;
}
public void openClose(boolean close) {
closed = close;
if (closed) {
arrow.setIcon(closeIcon);
} else {
arrow.setIcon(openIcon);
}
body.setHidden(closed);
}
}
}