changeset 1260:aebcf2e03dc5

New VMTree (part I) review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-September/008321.html reviewed-by: omajid
author Mario Torre <neugens.limasoftware@gmail.com>
date Mon, 30 Sep 2013 16:38:49 +0200
parents 7d6cd64ffd0c
children ecf0cfd9dafe
files client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EmptyIcon.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/FontAwesomeIcon.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/ThermostatScrollBar.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/ThermostatScrollBarUI.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/ThermostatScrollPane.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/VerticalLayout.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/Accordion.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionComponent.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionComponentController.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionComponentEvent.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionComponentFactory.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionContentPane.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionHeaderEvent.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionItemSelectedChangeListener.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionModel.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionModelChangeListener.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/ItemSelectedEvent.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/Selectable.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/TitledPane.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/TitledPanePainter.java client/swing/src/test/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionModelTest.java client/swing/src/test/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionTest.java client/swing/src/test/java/com/redhat/thermostat/client/swing/internal/accordion/TitledPaneTest.java
diffstat 23 files changed, 2295 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EmptyIcon.java	Mon Sep 30 16:38:48 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EmptyIcon.java	Mon Sep 30 16:38:49 2013 +0200
@@ -39,14 +39,11 @@
 import java.awt.Component;
 import java.awt.Graphics;
 
-import javax.swing.Icon;
-import javax.swing.ImageIcon;
-
 /**
  * A blank {@link Icon}.
  */
 @SuppressWarnings("serial")
-public class EmptyIcon extends ImageIcon  {
+public class EmptyIcon extends Icon {
 
     private int width;
     private int height;
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/FontAwesomeIcon.java	Mon Sep 30 16:38:48 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/FontAwesomeIcon.java	Mon Sep 30 16:38:49 2013 +0200
@@ -65,6 +65,8 @@
     
     private Font font;
     
+    private Color color;
+    
     static {
         try {
             InputStream stream =
@@ -77,6 +79,10 @@
     }
     
     /**
+     * Creates a new {@link FontAwesomeIcon} painted in {@link Color#BLACK}.
+     * 
+     * <br /><br />
+     * 
      * Please, refer to:
      * <a href='http://fortawesome.github.io/Font-Awesome/cheatsheet/'>
      * Font-Awesome Website</a> for the actual
@@ -85,11 +91,28 @@
      * @see {@link http://fortawesome.github.io/Font-Awesome/cheatsheet/}
      */
     public FontAwesomeIcon(char iconID, int size) {
+        this(iconID, size, Color.BLACK);
+    }
+
+    /**
+     * Creates a new {@link FontAwesomeIcon} painted in the given {@link Color}.
+     * 
+     * <br /><br />
+     * 
+     * Please, refer to:
+     * <a href='http://fortawesome.github.io/Font-Awesome/cheatsheet/'>
+     * Font-Awesome Website</a> for the actual
+     * values accepted as {@code iconID}.
+     * 
+     * @see {@link http://fortawesome.github.io/Font-Awesome/cheatsheet/}
+     */
+    public FontAwesomeIcon(char iconID, int size, Color color) {
         this.iconID = iconID;
         this.size = size;
         font = awesome.deriveFont(Font.PLAIN, size);
+        this.color = color;
     }
-
+    
     @Override
     public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
         
@@ -100,9 +123,11 @@
             Graphics2D graphics = (Graphics2D) buffer.getGraphics();
             graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                       RenderingHints.VALUE_ANTIALIAS_ON);
-            
+            graphics.setRenderingHint(RenderingHints.KEY_RENDERING,
+                                      RenderingHints.VALUE_RENDER_QUALITY);
+
             graphics.setFont(font);
-            graphics.setColor(Color.BLACK);
+            graphics.setColor(color);
             
             int stringY = getIconHeight() - (getIconHeight()/4) + 1;
             graphics.drawString(String.valueOf(iconID), 0, stringY);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/ThermostatScrollBar.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import com.redhat.thermostat.client.swing.components.ThermostatThinScrollBar;
+
+/**
+ * This widget implements standard sized scrollbars.
+ */
+@SuppressWarnings("serial")
+class ThermostatScrollBar extends ThermostatThinScrollBar {
+    public ThermostatScrollBar() {
+        this(ThermostatScrollBar.VERTICAL);
+    }
+    
+    public ThermostatScrollBar(int orientation) {
+        super(orientation);
+        setOpaque(false);
+        setUI(new ThermostatScrollBarUI());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/ThermostatScrollBarUI.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.Shape;
+import java.awt.geom.RoundRectangle2D;
+
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JScrollBar;
+import javax.swing.plaf.basic.BasicScrollBarUI;
+
+import com.redhat.thermostat.client.swing.GraphicsUtils;
+import com.redhat.thermostat.client.ui.Palette;
+
+class ThermostatScrollBarUI extends BasicScrollBarUI {
+
+    // TODO: move to common UIResources class
+    private static final Color FG_COLOR = Palette.EARL_GRAY.getColor();
+    
+    @SuppressWarnings("serial")
+    private class EmptyButton extends JButton {
+
+        public EmptyButton() {
+            setOpaque(false);
+            setMaximumSize(new Dimension(0, 0));
+        }
+        
+        @Override
+        public void paint(Graphics g) {}
+    }
+
+    @Override
+    protected void installDefaults() {
+        super.installDefaults();
+        
+        scrollBarWidth = 16;
+        incrGap = 0;
+        decrGap = 0;
+    }
+    
+    @Override
+    protected JButton createDecreaseButton(int orientation) {
+        return new EmptyButton();
+    }
+
+    @Override
+    protected JButton createIncreaseButton(int orientation) {
+        return new EmptyButton();
+    }
+
+    @Override
+    protected void paintTrack(Graphics g, JComponent c, Rectangle trackBounds) {}
+
+    @Override
+    protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) {
+        if(thumbBounds.isEmpty() || !scrollbar.isEnabled())     {
+            return;
+        }
+
+        int x = 0;
+        int y = 0;
+        
+        int w = 0;
+        int h = 0;
+        
+        GraphicsUtils utils = GraphicsUtils.getInstance();
+        Graphics2D graphics = utils.createAAGraphics(g);
+        if (scrollbar.getOrientation() == JScrollBar.VERTICAL) {
+            x = thumbBounds.x + 4;
+            y = thumbBounds.y;
+
+            w = 8;
+            h = thumbBounds.height - 1;
+            
+        } else {
+            x = thumbBounds.x;
+            y = thumbBounds.y + 4;
+            
+            w = thumbBounds.width - 1;
+            h = 8;            
+        }
+        
+        graphics.translate(x, y);
+
+        Shape shape =  new RoundRectangle2D.Double(0, 0, w, h, 4, 4);
+
+        graphics.setColor(FG_COLOR);
+        graphics.fill(shape);
+        
+        graphics.dispose();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/ThermostatScrollPane.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import javax.swing.JComponent;
+import javax.swing.JScrollPane;
+
+@SuppressWarnings("serial")
+public class ThermostatScrollPane extends JScrollPane {
+
+    public ThermostatScrollPane(JComponent view) {
+        super(view);
+        
+        setVerticalScrollBar(new ThermostatScrollBar(ThermostatThinScrollBar.VERTICAL));
+        setHorizontalScrollBar(new ThermostatScrollBar(ThermostatThinScrollBar.HORIZONTAL));
+
+        setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+        setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+        setBorder(null);
+        
+        setViewportBorder(null);
+        getViewport().setOpaque(false);
+        setOpaque(false);        
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/VerticalLayout.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.components;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Rectangle;
+
+/**
+ * A layout that lays components in a vertical stack according to
+ * the component hierarchy preferred height.
+ */
+public class VerticalLayout extends AbstractLayout {
+    
+    @Override
+    protected void doLayout(Container parent) {
+        Rectangle rect = new Rectangle();
+        for (Component component : parent.getComponents()) {
+            Dimension dim = component.getPreferredSize();
+            rect.width = parent.getWidth();
+            rect.height = dim.height;
+            component.setBounds(rect);
+            rect.y += dim.height;
+        }
+    }
+
+    @Override
+    public Dimension preferredLayoutSize(Container parent) {
+        
+        Dimension size = new Dimension();
+        for (Component component : parent.getComponents()) {
+            Dimension dim = component.getPreferredSize();
+            size.width = Math.max(dim.width, size.width);
+            size.height += dim.height;
+        }
+        
+        return size;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/Accordion.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import com.redhat.thermostat.client.swing.components.ThermostatScrollPane;
+import com.redhat.thermostat.client.swing.components.VerticalLayout;
+
+/**
+ * An Accordion widget. H and C types represent Header and Component and are
+ * added and managed for the accordion via the {@link AccordionModel} class.
+ * 
+ * <br /><br />
+ * 
+ * H and C do not have to be widgets themselves, since the
+ * {@link AccordionComponentFactory} is responsible to create the specific
+ * widget to be used inside the Accordion.
+ * 
+ */
+@SuppressWarnings("serial")
+public class Accordion<H, C> extends JPanel {
+    
+    public static final int ICON_SIZE = 24;
+    
+    static final int MIN_HEIGHT = ICON_SIZE;
+    static final int MIN_WIDTH = 200;
+    
+    private JScrollPane scrollPane;
+    
+    private AccordionModel<H, C> model;
+    private HashMap<H, TitledPane> panes;
+    private HashMap<H, Map<C, Component>> components;
+    
+    private AccordionComponentFactory<H, C> componentFactory;
+    
+    private AccordionContentPane contentPane;
+    
+    private AccordionComponentController componentController;
+    
+    public Accordion(AccordionComponentFactory<H, C> componentFactory) {
+        
+        this.componentFactory = componentFactory;
+        componentController = new AccordionComponentController();
+        
+        setLayout(new BorderLayout());
+
+        contentPane = new AccordionContentPane();
+        contentPane.setLayout(new VerticalLayout());
+        
+        model = new AccordionModel<>();
+        model.addAccordionModelChangeListener(new AccordionModelChangeListenerImpl());
+        panes = new HashMap<>();
+        components = new HashMap<>();
+        
+        scrollPane = new ThermostatScrollPane(contentPane);
+        
+        add(scrollPane, BorderLayout.CENTER);
+    }
+
+    public AccordionModel<H, C> getModel() {
+        return model;
+    }
+    
+    @Override
+    public Dimension getPreferredSize() {
+        Dimension dim = new Dimension();
+        dim.width = Accordion.MIN_WIDTH;
+        for (Component comp : getComponents()) {
+            Dimension pref = comp.getPreferredSize();
+            if (dim.width < pref.width) {
+                dim.width =  pref.width;
+            }
+            dim.height += pref.height;
+        }
+        return dim;
+    }
+    
+    private class AccordionComponentClickEventModel extends MouseAdapter {
+        private AccordionComponent referenceComponent;
+        public AccordionComponentClickEventModel(AccordionComponent referenceComponent) {
+            this.referenceComponent = referenceComponent;
+        }
+
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            componentController.setSelectedItem(referenceComponent);
+        }
+    }
+    
+    private class AccordionHeaderClickEventModel extends MouseAdapter {
+        private TitledPane referenceComponent;
+        public AccordionHeaderClickEventModel(TitledPane referenceComponent) {
+            this.referenceComponent = referenceComponent;
+        }
+
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            if (e.getClickCount() >= 2) {
+                referenceComponent.setExpanded(!referenceComponent.isExpanded());
+            }
+        }
+    }
+    
+    private class AccordionModelChangeListenerImpl implements AccordionModelChangeListener<H, C> {
+
+        @Override
+        public void headerAdded(AccordionHeaderEvent<H> e) {
+            H header = e.getHeader();
+            TitledPane pane = componentFactory.createHeader(header);
+            
+            pane.addMouseListener(new AccordionComponentClickEventModel(pane));
+            pane.addMouseListener(new AccordionHeaderClickEventModel(pane));
+            panes.put(header, pane);
+            
+            Accordion.this.contentPane.add(pane);
+            Accordion.this.contentPane.revalidate();
+        }
+
+        @Override
+        public void componentAdded(AccordionComponentEvent<H, C> e) {
+            C component = e.getComponent();
+            H header = e.getHeader();
+            AccordionComponent comp = componentFactory.createComponent(header, component);
+            
+            TitledPane pane = panes.get(header);
+            JComponent content = pane.getContent();
+            if (content == null) {
+                content = new AccordionContentPane();
+                content.setLayout(new VerticalLayout());
+                pane.setContent(content);
+            }
+            
+            Component contentUnit = comp.getUiComponent();
+            contentUnit.addMouseListener(new AccordionComponentClickEventModel(comp));
+            
+            content.add(contentUnit);
+            content.revalidate();
+            
+            Map<C, Component> componentsMap = components.get(header);
+            if (componentsMap == null) {
+                componentsMap = new HashMap<>();
+                components.put(header, componentsMap);
+            }
+            componentsMap.put(component, contentUnit);
+        }
+
+        @Override
+        public void componentRemoved(AccordionComponentEvent<H, C> e) {
+            C component = e.getComponent();
+            H header = e.getHeader();
+            
+            Map<C, Component> componentsMap = components.get(header);
+            Component contentUnit = componentsMap.remove(component);
+            if (componentsMap.isEmpty()) {
+                components.remove(header);
+            }
+            
+            TitledPane pane = panes.get(header);
+            JComponent content = pane.getContent();
+            content.remove(contentUnit);
+            content.revalidate();
+        }
+
+        @Override
+        public void headerRemoved(AccordionHeaderEvent<H> e) {
+            H header = e.getHeader();
+            
+            components.remove(header);
+            TitledPane pane = panes.remove(header);
+            Accordion.this.contentPane.remove(pane);
+            Accordion.this.contentPane.revalidate();
+        }
+    }
+    
+    public void addAccordionItemSelectedChangeListener(AccordionItemSelectedChangeListener l) {
+        componentController.addAccordionItemSelectedChangeListener(l);
+    }
+    
+    public void removeAccordionItemSelectedChangeListener(AccordionItemSelectedChangeListener l) {
+        componentController.removeAccordionItemSelectedChangeListener(l);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionComponent.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import com.redhat.thermostat.client.swing.SwingComponent;
+
+/**
+ * Represents a {@link Selectable} {@link SwingComponent} inside the
+ * {@link Accordion}.
+ */
+public interface AccordionComponent extends SwingComponent, Selectable {
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionComponentController.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import javax.swing.event.EventListenerList;
+
+class AccordionComponentController {
+
+    protected EventListenerList listenerList;
+
+    private AccordionComponent selectedComponent;
+    
+    public AccordionComponentController() {
+        listenerList = new EventListenerList();
+    }
+    
+    void setSelectedItem(AccordionComponent selectedComponent) {
+        AccordionComponent oldSelected = this.selectedComponent;
+        this.selectedComponent = selectedComponent;
+
+        if (oldSelected != null) {
+            oldSelected.setSelected(false);
+        }
+
+        if (selectedComponent != null) {
+            selectedComponent.setSelected(true);
+        }
+
+        if (oldSelected != this.selectedComponent) {
+            fireItemSelectedChanged(oldSelected, selectedComponent);
+        }
+    }
+
+    void addAccordionItemSelectedChangeListener(AccordionItemSelectedChangeListener l) {
+        listenerList.add(AccordionItemSelectedChangeListener.class, l);
+    }
+    
+    void removeAccordionItemSelectedChangeListener(AccordionItemSelectedChangeListener l) {
+        listenerList.remove(AccordionItemSelectedChangeListener.class, l);
+    }
+    
+    private void fireItemSelectedChanged(AccordionComponent oldComp, AccordionComponent newComp) {
+        Object[] listeners = listenerList.getListenerList();
+
+        ItemSelectedEvent event = new ItemSelectedEvent(oldComp, newComp);
+        for (int i = listeners.length - 2; i >= 0; i -= 2) {
+            if (listeners[i] == AccordionItemSelectedChangeListener.class) {
+                ((AccordionItemSelectedChangeListener) listeners[i + 1]).itemSelected(event);
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionComponentEvent.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.util.EventObject;
+
+@SuppressWarnings("serial")
+public class AccordionComponentEvent<H, C> extends EventObject {
+
+    private H header;
+    public AccordionComponentEvent(H header, C component) {
+        super(component);
+        this.header = header;
+    }
+    
+    public H getHeader() {
+        return header;
+    }
+    
+    @SuppressWarnings("unchecked")
+    public C getComponent() {
+        return (C) getSource();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionComponentFactory.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+/**
+ * Factory class for the creation of {@link Accordion} components (header and
+ * main components).
+ */
+public interface AccordionComponentFactory<H, C> {
+
+    TitledPane createHeader(H header);
+    AccordionComponent createComponent(H header, C component);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionContentPane.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.awt.Component;
+import java.awt.Dimension;
+
+import javax.swing.JPanel;
+
+@SuppressWarnings("serial")
+class AccordionContentPane extends JPanel {
+    
+    @Override
+    public Dimension getPreferredSize() {
+        Dimension dim = new Dimension();
+        dim.width = Accordion.MIN_WIDTH;
+        for (Component comp : getComponents()) {
+            Dimension pref = comp.getPreferredSize();
+            dim.width = Math.max(dim.width, pref.width);
+            dim.height += pref.height;
+        }
+
+        return dim;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionHeaderEvent.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.util.EventObject;
+
+@SuppressWarnings("serial")
+public class AccordionHeaderEvent<H> extends EventObject {
+
+    public AccordionHeaderEvent(H source) {
+        super(source);
+    }
+    
+    @SuppressWarnings("unchecked")
+    public H getHeader() {
+        return (H) getSource();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionItemSelectedChangeListener.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.util.EventListener;
+
+public interface AccordionItemSelectedChangeListener extends EventListener {
+    public void itemSelected(ItemSelectedEvent event);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionModel.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import javax.swing.event.EventListenerList;
+
+/**
+ * A Model class for the {@link Accordion} widget. The model represents the
+ * collection of H (header) and C (components under header) instances that
+ * are added to the {@link Accordion}. As noted in the {@link Accordion}
+ * javadoc, H and C do not have to be widgets themselves, since the
+ * {@link AccordionComponentFactory} is responsible to create the specific
+ * widget to be used inside the Accordion. 
+ * 
+ * <strong>Note</strong>: H and C must be usable as hash keys.
+ */
+public class AccordionModel<H, C> {
+
+    protected EventListenerList listenerList;
+    
+    private HashMap<H, List<C>> components;
+    
+    public AccordionModel() {
+        components = new HashMap<>();
+        listenerList = new EventListenerList();
+    }
+   
+    private List<C> addOrGetHeader(H header) {
+        List<C> _components = components.get(header);
+        if (_components == null) {
+            _components = new ArrayList<>();
+            components.put(header, _components);
+
+            fireHeaderAddedEvent(header);
+        }
+        return _components;
+    }
+    
+    public boolean addHeader(H header) {
+        boolean result = components.containsKey(header);
+        addOrGetHeader(header);
+        return result;
+    }
+    
+    public boolean removeHeader(H header) {
+        List<C> _components = components.remove(header);
+        boolean result = (_components != null);
+        if (result) {
+            for (C component :_components) {
+                fireComponentRemovedEvent(header, component);
+            }
+            fireHeaderRemovedEvent(header);
+        }
+        return result;
+    }
+    
+    public boolean addComponent(H header, C component) {
+        List<C> _components = addOrGetHeader(header);
+        boolean result = _components.add(component);
+        fireComponentAddedEvent(header, component);
+        return result;
+    }
+    
+    public boolean removeComponent(H header, C component) {
+        List<C> _components = components.get(header);
+        boolean result = false;
+        if (_components != null) {
+            result = _components.remove(component);
+            fireComponentRemovedEvent(header, component);
+        }
+        return result;
+    }
+    
+    public int headerSize() {
+        return components.size();
+    }
+    
+    public int size() {
+        int size = components.size();
+        for (List<C> comp : components.values()) {
+            size += comp.size();
+        }
+        return size;
+    }
+    
+    public void addAccordionModelChangeListener(AccordionModelChangeListener l) {
+        listenerList.add(AccordionModelChangeListener.class, l);
+    }
+    
+    private void fireHeaderRemovedEvent(H header) {
+        Object[] listeners = listenerList.getListenerList();
+
+        AccordionHeaderEvent<H> event = new AccordionHeaderEvent<>(header);
+        
+        for (int i = listeners.length - 2; i >= 0; i -= 2) {
+            if (listeners[i] == AccordionModelChangeListener.class) {
+                ((AccordionModelChangeListener) listeners[i + 1]).headerRemoved(event);
+            }
+        }
+    }
+    
+    private void fireHeaderAddedEvent(H header) {
+        Object[] listeners = listenerList.getListenerList();
+
+        AccordionHeaderEvent<H> event = new AccordionHeaderEvent<>(header);
+        
+        for (int i = listeners.length - 2; i >= 0; i -= 2) {
+            if (listeners[i] == AccordionModelChangeListener.class) {
+                ((AccordionModelChangeListener) listeners[i + 1]).headerAdded(event);
+            }
+        }
+    }
+    
+    private void fireComponentAddedEvent(H header, C component) {
+        Object[] listeners = listenerList.getListenerList();
+
+        AccordionComponentEvent<H, C> event = new AccordionComponentEvent<>(header, component);
+        
+        for (int i = listeners.length - 2; i >= 0; i -= 2) {
+            if (listeners[i] == AccordionModelChangeListener.class) {
+                ((AccordionModelChangeListener) listeners[i + 1]).componentAdded(event);
+            }
+        }
+    }
+    
+    private void fireComponentRemovedEvent(H header, C component) {
+        Object[] listeners = listenerList.getListenerList();
+
+        AccordionComponentEvent<H, C> event = new AccordionComponentEvent<>(header, component);
+        
+        for (int i = listeners.length - 2; i >= 0; i -= 2) {
+            if (listeners[i] == AccordionModelChangeListener.class) {
+                ((AccordionModelChangeListener) listeners[i + 1]).componentRemoved(event);
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionModelChangeListener.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.util.EventListener;
+
+public interface AccordionModelChangeListener<H, C> extends EventListener {
+
+    public void headerAdded(AccordionHeaderEvent<H> e);
+    public void headerRemoved(AccordionHeaderEvent<H> e);
+
+    public void componentAdded(AccordionComponentEvent<H, C> e);
+    public void componentRemoved(AccordionComponentEvent<H, C> e);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/ItemSelectedEvent.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.util.EventObject;
+
+/**
+ *
+ */
+@SuppressWarnings("serial")
+public class ItemSelectedEvent extends EventObject {
+
+    private AccordionComponent oldComp;
+
+    public ItemSelectedEvent(AccordionComponent oldComp, AccordionComponent newComp) {
+        super(newComp);
+        this.oldComp = oldComp;
+    }
+
+    public AccordionComponent getPreviousSelected() {
+        return oldComp;
+    }
+    
+    @Override
+    public AccordionComponent getSource() {
+        return (AccordionComponent) super.getSource();
+    }
+    
+    public AccordionComponent getSelected() {
+        return getSource();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/Selectable.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+/**
+ * Represents an object that can be selected.
+ */
+public interface Selectable {
+    void setSelected(boolean selected);
+    boolean isSelected();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/TitledPane.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.event.ContainerEvent;
+import java.awt.event.ContainerListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SwingConstants;
+
+import com.redhat.thermostat.client.swing.GraphicsUtils;
+import com.redhat.thermostat.client.swing.components.EmptyIcon;
+import com.redhat.thermostat.client.swing.components.FontAwesomeIcon;
+import com.redhat.thermostat.client.swing.components.Icon;
+import com.redhat.thermostat.client.swing.components.ShadowLabel;
+import com.redhat.thermostat.client.swing.components.VerticalLayout;
+import com.redhat.thermostat.client.ui.Palette;
+
+/**
+ * The TitledPane is responsible to render the header component of a new
+ * entry in an {@link Accordion}. By default the header is rendered as a
+ * {@link ShadowLabel} widget, but can be fine tuned passing appropriate
+ * instances of {@link TitledPanePainter} and {@link AccordionComponent}
+ * to the constructor.
+ */
+@SuppressWarnings("serial")
+public class TitledPane extends JPanel implements AccordionComponent {
+
+    public static final Color SELECTED_FG = Palette.EARL_GRAY.getColor();
+    public static final Color UNSELECTED_FG = Palette.DARK_GRAY.getColor();
+    
+    private Color selectedColor;
+    private Color unselectedColor;
+
+    public static final String EXPANDED_PROPERTY = "EXPANDED_PROPERTY";
+
+    private boolean expanded;
+    
+    private boolean selected;
+    
+    private Icon expandedIcon;
+    private Icon expandedSelectedIcon;
+    
+    private Icon collapsedIcon;
+    private Icon collapsedIconSelectedIcon;
+    
+    private Icon emptyIcon;
+    
+    private JLabel iconLabel;    
+
+    private JPanel titlePane;
+    
+    private JComponent content;
+
+    private TitledPanePainter backgroundPainter;
+    
+    private class DefaultTitleComponent implements AccordionComponent {
+        private ShadowLabel titleLabel;
+        public DefaultTitleComponent(String title) {
+            titleLabel = new ShadowLabel();
+            titleLabel.setText(title);
+            titleLabel.setName(title + "_label");
+            titleLabel.setForeground(unselectedColor);
+        }
+        
+        @Override
+        public Component getUiComponent() {
+            return titleLabel;
+        }
+
+        @Override
+        public void setSelected(boolean selected) {
+            if (selected) {
+                titleLabel.setForeground(selectedColor);
+            } else {
+                titleLabel.setForeground(unselectedColor);
+            }
+        }
+
+        @Override
+        public boolean isSelected() {
+            return TitledPane.this.isSelected();
+        }
+    }
+    
+    private AccordionComponent titleComponent;
+    
+    public TitledPane(String title, TitledPanePainter backgroundPainter) {
+        this(title, backgroundPainter, null);
+    }
+    
+    public TitledPane(String title, TitledPanePainter backgroundPainter,
+                      AccordionComponent titleComponent)
+    {
+        setLayout(new VerticalLayout());
+        setName(title);
+        
+        titlePane = new JPanel();
+        titlePane.setLayout(new BorderLayout());
+        
+        if (backgroundPainter != null) {
+           
+            this.backgroundPainter = backgroundPainter;
+            
+            selectedColor = backgroundPainter.getSelectedForeground();
+            unselectedColor = backgroundPainter.getUnselectedForeground();
+            titlePane.setOpaque(false);
+            
+        } else {
+            selectedColor = SELECTED_FG;
+            unselectedColor = UNSELECTED_FG;
+        }
+
+        if (titleComponent == null) {
+            titleComponent = new DefaultTitleComponent(title);
+        }
+        this.titleComponent = titleComponent;
+
+        expanded = false;
+        
+        // TODO: move in UIDefault or something class
+        int iconSize = 24;
+        
+        emptyIcon = new EmptyIcon(iconSize, iconSize);
+
+        // TODO: move in UIDefault too, especially the constants
+        expandedIcon = new FontAwesomeIcon('\uf107', iconSize, unselectedColor);
+        expandedSelectedIcon = new FontAwesomeIcon('\uf107', iconSize, selectedColor);
+        
+        collapsedIcon = new FontAwesomeIcon('\uf105', iconSize, unselectedColor);
+        collapsedIconSelectedIcon = new FontAwesomeIcon('\uf105', iconSize, selectedColor);
+
+        iconLabel = new JLabel(emptyIcon);
+        iconLabel.setText(" ");
+        iconLabel.setHorizontalTextPosition(SwingConstants.LEFT);
+        
+        iconLabel.setName(title + "_ExpanderIcon");
+        iconLabel.addMouseListener(new MouseAdapter() {
+            @Override
+            public void mouseClicked(MouseEvent e) {
+                setExpanded(!isExpanded());
+            }
+        });
+
+        titlePane.add(iconLabel, BorderLayout.WEST);
+        titlePane.add(this.titleComponent.getUiComponent(), BorderLayout.CENTER);
+        
+        add(titlePane);
+    }
+        
+    public TitledPane(String title) {
+        this(title, null);
+    }
+
+    private void updateState() {
+        
+        titleComponent.setSelected(isSelected());
+        if (isExpanded()) {            
+            if (isSelected()) {
+                iconLabel.setIcon(expandedSelectedIcon);                
+            } else {
+                iconLabel.setIcon(expandedIcon);
+            }
+        } else {
+            if (isSelected()) {
+                iconLabel.setIcon(collapsedIconSelectedIcon);
+            } else {
+                iconLabel.setIcon(collapsedIcon);
+            }
+        }
+        
+        if (!hasContent()) {
+            iconLabel.setIcon(emptyIcon);
+        }
+        
+        repaint();
+    }
+    
+    protected AccordionComponent getTitleComponent() {
+        return titleComponent;
+    }
+    
+    public boolean isExpanded() {
+        return expanded;
+    }
+    
+    public void setExpanded(boolean expanded) {
+        if (hasContent()) {
+            boolean oldExpaned = this.expanded;
+            this.expanded = expanded;
+            updateState();
+            if (oldExpaned != expanded) {
+                if (expanded) {
+                    add(content);
+                } else {
+                    remove(content);
+                }
+                revalidate();
+                firePropertyChange(EXPANDED_PROPERTY, oldExpaned, expanded);
+            }
+        }
+    }
+
+    @Override
+    public void setSelected(boolean selected) {
+        this.selected = selected;
+        updateState();
+    }
+
+    private boolean hasContent() {
+        return content != null && content.getComponentCount() != 0;
+    }
+    
+    @Override
+    public boolean isSelected() {
+        return selected;
+    }
+
+    public JComponent getContent() {
+        return content;
+    }
+    
+    private void collapse() {
+        remove(content);
+        expanded = false;
+        revalidate();
+    }
+    
+    public void setContent(JComponent content) {
+        JComponent oldContent = this.content;
+        this.content = content;
+        if (content != null) {
+            content.addContainerListener(new ContainerListener() {
+                @Override
+                public void componentAdded(ContainerEvent e) {
+                    updateState();
+                }
+                
+                @Override
+                public void componentRemoved(ContainerEvent e) {
+                    if (!hasContent()) {
+                        collapse();
+                    }
+                    updateState();
+                }
+            });
+        }
+        
+        if (isExpanded()) {
+            if (oldContent != null) {
+                remove(this.content);
+            }
+            setExpanded(true);
+        }
+        
+        updateState();
+    }
+    
+    @Override
+    public Component getUiComponent() {
+        return this;
+    }
+    
+    @Override
+    protected void paintComponent(Graphics g) {
+        super.paintComponent(g);
+        if (backgroundPainter != null) {
+            Graphics2D graphics = GraphicsUtils.getInstance().createAAGraphics(g);
+            backgroundPainter.paint(graphics, this, getWidth(), getHeight());
+            graphics.dispose();
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/accordion/TitledPanePainter.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import java.awt.Color;
+
+import javax.swing.Painter;
+
+/**
+ * {@link Painter} interface to render the background of the {@link TitledPane}.
+ * 
+ * <br /><br />
+ * 
+ * Instances of this class are used to render the background panel of the
+ * {@link TitledPane} either when customising the selected/unselected look
+ * and feel of the default widget or to provide a matching painter when
+ * using a dedicated widget for the header component. 
+ */
+public interface TitledPanePainter extends Painter<AccordionComponent> {
+
+    Color getSelectedForeground();
+    Color getUnselectedForeground();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/test/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionModelTest.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class AccordionModelTest {
+    
+    @Test
+    public void testAddRemove() {
+        
+        final int [] results = new int[4];
+        final Object [] resultsComponents = new Object[results.length];
+
+        AccordionModelChangeListener<String, String> l =
+                new AccordionModelChangeListener<String, String>() {
+
+            @Override
+            public void headerAdded(AccordionHeaderEvent<String> e) {
+                results[2]++;
+                resultsComponents[2] = e.getSource();
+            }
+
+            @Override
+            public void componentAdded(AccordionComponentEvent<String, String> e) {
+                results[1]++;
+                resultsComponents[1] = e.getSource();
+            }
+
+            @Override
+            public void componentRemoved(AccordionComponentEvent<String, String> e) {
+                results[0]++;
+                resultsComponents[0] = e.getSource();
+            }
+            
+            @Override
+            public void headerRemoved(AccordionHeaderEvent<String> e) {
+                results[3]++;
+                resultsComponents[3] = e.getSource();
+            }
+        };
+        
+        AccordionModel<String, String> testModel = new AccordionModel<>();
+        testModel.addAccordionModelChangeListener(l);
+        
+        boolean result = testModel.addComponent("test", "testComponent");
+        assertTrue(result);
+        
+        assertEquals(2, testModel.size());
+        assertEquals(1, testModel.headerSize());
+        
+        result = testModel.removeComponent("test", "testComponent");
+        assertTrue(result);
+        
+        assertEquals(1, testModel.size());
+        assertEquals(1, testModel.headerSize());
+        
+        assertEquals(1, results[0]);
+        assertEquals(1, results[1]);
+        assertEquals(1, results[2]);
+        
+        assertEquals("testComponent", resultsComponents[0]);
+        assertEquals("testComponent", resultsComponents[1]);
+        assertEquals("test", resultsComponents[2]);
+        
+        result = testModel.removeHeader("test");
+        assertTrue(result);
+        assertEquals(1, results[0]);
+        assertEquals(1, results[1]);
+        assertEquals(1, results[2]);
+        assertEquals(1, results[3]);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/test/java/com/redhat/thermostat/client/swing/internal/accordion/AccordionTest.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+
+import net.java.openjdk.cacio.ctc.junit.CacioFESTRunner;
+
+import org.fest.swing.annotation.GUITest;
+import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
+import org.fest.swing.edt.GuiActionRunner;
+import org.fest.swing.edt.GuiQuery;
+import org.fest.swing.edt.GuiTask;
+import org.fest.swing.fixture.FrameFixture;
+import org.fest.swing.fixture.JLabelFixture;
+import org.fest.swing.fixture.JPanelFixture;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import com.redhat.thermostat.client.swing.components.EmptyIcon;
+
+@RunWith(CacioFESTRunner.class)
+public class AccordionTest {
+
+    private FrameFixture fixture;
+    private Accordion<String, String> accordion;
+    
+    private static class AccordionComponentImpl implements AccordionComponent {
+
+        private boolean selected;
+        private JLabel component;
+        
+        public AccordionComponentImpl(final String text, final String header, final AccordionModel<String, String> model) {
+            component = new JLabel(text);
+            component.setName(text);
+        }
+
+        @Override
+        public Component getUiComponent() {
+            return component;
+        }
+        
+        @Override
+        public boolean isSelected() {
+            return selected;
+        }
+        
+        @Override
+        public void setSelected(boolean selected) {
+            this.selected = selected;
+        }
+    }
+    
+    private static class AccordionComponentFactoryImpl implements AccordionComponentFactory<String, String> {
+
+        AccordionModel<String, String> model;
+        
+        @Override
+        public TitledPane createHeader(final String header) {
+            TitledPane pane = new TitledPane(header);
+            pane.setName(header);
+            return pane;
+        }
+
+        @Override
+        public AccordionComponent createComponent(String header, final String component) {
+            return new AccordionComponentImpl(component, header, model);
+        }
+
+        public void setModel(AccordionModel<String, String> model) {
+            this.model = model;
+        }
+    }
+    
+    @BeforeClass
+    public static void setUpOnce() {
+        FailOnThreadViolationRepaintManager.install();
+    }
+    
+    @Before
+    public void setUp() {
+        JFrame frame = GuiActionRunner.execute(new GuiQuery<JFrame>() {
+            @Override
+            protected JFrame executeInEDT() throws Throwable {
+                JFrame frame = new JFrame();
+
+                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+                frame.setMinimumSize(new Dimension(500, 500));
+                AccordionComponentFactoryImpl impl = new AccordionComponentFactoryImpl();
+                accordion = new Accordion<>(impl);
+                AccordionModel<String, String> model = accordion.getModel();
+                impl.setModel(model);
+                frame.add(accordion);
+                
+                return frame;
+            }
+        });
+        fixture = new FrameFixture(frame);
+    }
+    
+    @After
+    public void tearDown() {
+        fixture.cleanUp();
+        fixture = null;
+    }
+    
+    @Category(GUITest.class)
+    @GUITest
+    @Test
+    public void testSelectionListeners() {
+        fixture.show();
+        GuiActionRunner.execute(new GuiTask() {
+            @Override
+            protected void executeInEDT() throws Throwable {
+                AccordionModel<String, String> model = accordion.getModel();
+                model.addHeader("Test0");
+                model.addHeader("Test1");
+                model.addHeader("Test2");
+                
+                model.addComponent("Test0", "TestComponent0");
+                model.addComponent("Test0", "TestComponent1");
+                
+                model.addComponent("Test1", "TestComponent2");
+                model.addComponent("Test1", "TestComponent3");
+            }
+        });
+        
+        JPanelFixture titlePane0 = fixture.panel("Test0");
+        titlePane0.doubleClick();
+        
+        JPanelFixture titlePane1 = fixture.panel("Test1");
+        titlePane1.doubleClick();
+        
+        JPanelFixture titlePane2 = fixture.panel("Test2");
+        titlePane2.click();
+        
+        final List<ItemSelectedEvent> events = new ArrayList<>();
+        
+        GuiActionRunner.execute(new GuiTask() {
+            @Override
+            protected void executeInEDT() throws Throwable {
+                accordion.addAccordionItemSelectedChangeListener(new AccordionItemSelectedChangeListener() {
+                    @Override
+                    public void itemSelected(ItemSelectedEvent event) {
+                        events.add(event);
+                    }
+                });                
+            }
+        });
+        
+        fixture.robot.waitForIdle();
+        
+        assertEquals(0, events.size());
+        
+        titlePane0.label("Test0_label").click();
+        fixture.robot.waitForIdle();
+        
+        assertEquals(1, events.size());
+        
+        ItemSelectedEvent event = events.get(0);
+        assertSame(titlePane2.target, event.getPreviousSelected());
+        assertSame(titlePane0.target, event.getSelected());
+
+        assertTrue(event.getSelected().isSelected());
+        assertFalse(event.getPreviousSelected().isSelected());
+
+        titlePane1.label("Test1_label").click();
+        fixture.robot.waitForIdle();
+        
+        assertEquals(2, events.size());
+        
+        event = events.get(1);
+        assertSame(titlePane0.target, event.getPreviousSelected());
+        assertSame(titlePane1.target, event.getSelected());
+        
+        assertTrue(event.getSelected().isSelected());
+        assertFalse(event.getPreviousSelected().isSelected());
+        
+        titlePane2.label("Test2_label").click();
+        fixture.robot.waitForIdle();
+        
+        assertEquals(3, events.size());
+        
+        event = events.get(2);
+        assertSame(titlePane1.target, event.getPreviousSelected());
+        assertSame(titlePane2.target, event.getSelected());
+        
+        assertTrue(event.getSelected().isSelected());
+        assertFalse(event.getPreviousSelected().isSelected());
+        
+        JLabelFixture component1 = titlePane0.label("TestComponent1");
+        component1.click();
+        fixture.robot.waitForIdle();
+        
+        assertEquals(4, events.size());
+        event = events.get(3);
+        assertSame(titlePane2.target, event.getPreviousSelected());
+        assertSame(component1.target, event.getSelected().getUiComponent());
+        
+        // now those should both be unselected, last event clicked on
+        // TestComponent1, which is a different component than 
+        // its parent header Test0
+        event = events.get(0);
+        assertFalse(event.getSelected().isSelected());
+        assertFalse(event.getPreviousSelected().isSelected());
+    }
+    
+    @Category(GUITest.class)
+    @GUITest
+    @Test
+    public void testComponets() {
+        
+        fixture.show();
+        GuiActionRunner.execute(new GuiTask() {
+            @Override
+            protected void executeInEDT() throws Throwable {
+                AccordionModel<String, String> model = accordion.getModel();
+                model.addHeader("Test0");
+                model.addHeader("Test1");
+                model.addHeader("Test2");
+                
+                model.addComponent("Test0", "TestComponent0");
+                model.addComponent("Test0", "TestComponent1");
+                
+                model.addComponent("Test1", "TestComponent2");
+                model.addComponent("Test1", "TestComponent3");
+            }
+        });
+        
+        // check if things are added as expected
+        JPanelFixture titlePane = fixture.panel("Test0");
+        titlePane.requireVisible();
+        
+        JLabelFixture iconFixture = titlePane.label("Test0_ExpanderIcon");
+        iconFixture.requireVisible();
+        assertNotNull(iconFixture.target.getIcon());
+        assertFalse(iconFixture.target.getIcon() instanceof EmptyIcon);
+
+        // open it up
+        titlePane.doubleClick();
+        
+        JLabelFixture component = titlePane.label("TestComponent0");
+        component.requireVisible();
+        component = titlePane.label("TestComponent1");
+        component.requireVisible();
+        
+        titlePane = fixture.panel("Test1");
+        titlePane.requireVisible();
+        titlePane.doubleClick();
+        
+        iconFixture = titlePane.label("Test1_ExpanderIcon");
+        iconFixture.requireVisible();
+        assertNotNull(iconFixture.target.getIcon());
+        assertFalse(iconFixture.target.getIcon() instanceof EmptyIcon);
+
+        component = titlePane.label("TestComponent2");
+        component.requireVisible();
+        
+        component = titlePane.label("TestComponent3");
+        component.requireVisible();
+        
+        titlePane = fixture.panel("Test2");
+        titlePane.requireVisible();
+        titlePane.doubleClick();
+        
+        iconFixture = titlePane.label("Test2_ExpanderIcon");
+        iconFixture.requireVisible();
+        assertNotNull(iconFixture.target.getIcon());
+        assertTrue(iconFixture.target.getIcon() instanceof EmptyIcon);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/test/java/com/redhat/thermostat/client/swing/internal/accordion/TitledPaneTest.java	Mon Sep 30 16:38:49 2013 +0200
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat 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 for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.swing.internal.accordion;
+
+import static org.junit.Assert.assertSame;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+
+import net.java.openjdk.cacio.ctc.junit.CacioFESTRunner;
+
+import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
+import org.fest.swing.edt.GuiActionRunner;
+import org.fest.swing.edt.GuiQuery;
+import org.fest.swing.edt.GuiTask;
+import org.fest.swing.fixture.FrameFixture;
+import org.fest.swing.fixture.JLabelFixture;
+import org.fest.swing.fixture.JPanelFixture;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.redhat.thermostat.client.swing.components.VerticalLayout;
+
+@RunWith(CacioFESTRunner.class)
+public class TitledPaneTest {
+
+    private FrameFixture fixture;
+
+    private static class TitleBGPainter1 implements TitledPanePainter {
+                
+        @Override
+        public void paint(Graphics2D g, AccordionComponent pane, int width, int height) {
+            if (pane.isSelected()) {
+                g.setColor(Color.BLACK);
+            } else {
+                g.setColor(Color.BLUE);
+            }
+            g.fillRect(0, 0, width, height);
+        }
+        
+        @Override
+        public Color getSelectedForeground() {
+            return Color.BLACK;
+        }
+        
+        @Override
+        public Color getUnselectedForeground() {
+            return Color.RED;
+        }
+    }
+    
+    private static class TitleBGPainter2 implements TitledPanePainter {
+        
+        @Override
+        public void paint(Graphics2D g, AccordionComponent pane, int width, int height) {
+            if (pane.isSelected()) {
+                g.setColor(Color.BLACK);
+            } else {
+                g.setColor(Color.BLUE);
+            }
+            g.fillRect(0, 0, width, height);
+        }
+        
+        @Override
+        public Color getSelectedForeground() {
+            return Color.BLACK;
+        }
+        
+        @Override
+        public Color getUnselectedForeground() {
+            return Color.RED;
+        }
+    }
+    
+    @BeforeClass
+    public static void setUpOnce() {
+        FailOnThreadViolationRepaintManager.install();
+    }
+    
+    @Before
+    public void setUp() {
+        JFrame frame = GuiActionRunner.execute(new GuiQuery<JFrame>() {
+            @Override
+            protected JFrame executeInEDT() throws Throwable {
+                JFrame frame = new JFrame();
+
+                JPanel container = new JPanel();
+                container.setLayout(new VerticalLayout());
+                
+                TitledPane pane1 = new TitledPane("test1", new TitleBGPainter1());
+                TitledPane pane2 = new TitledPane("test2", new TitleBGPainter2());
+                
+                container.add(pane1);
+                container.add(pane2);
+                
+                frame.add(container);
+                
+                return frame;
+            }
+        });
+        fixture = new FrameFixture(frame);
+    }
+    
+    @After
+    public void tearDown() {
+        fixture.cleanUp();
+        fixture = null;
+    }
+    
+    @Test
+    public void testColorDifferences() {
+        
+        fixture.show();
+        final JPanelFixture titlePane1 = fixture.panel("test1");
+        final JPanelFixture titlePane2 = fixture.panel("test2");
+
+        GuiActionRunner.execute(new GuiTask() {
+            @Override
+            protected void executeInEDT() throws Throwable {
+                // TitledPane do not (yet) have a model to keep track of
+                // selection, this is done currently in the Accordion
+                // component controller
+                TitledPane pane1 = (TitledPane) titlePane1.target;                
+                pane1.setSelected(true);
+                
+                TitledPane pane2 = (TitledPane) titlePane2.target;                
+                pane2.setSelected(false);                
+            }
+        });
+        
+        JLabelFixture label1 = titlePane1.label("test1_label");
+        JLabelFixture label2 = titlePane2.label("test2_label");
+
+        assertSame(Color.BLACK, label1.foreground().target());
+        assertSame(Color.RED, label2.foreground().target());
+
+        GuiActionRunner.execute(new GuiTask() {
+            @Override
+            protected void executeInEDT() throws Throwable {
+                TitledPane pane1 = (TitledPane) titlePane1.target;                
+                pane1.setSelected(false);
+                
+                TitledPane pane2 = (TitledPane) titlePane2.target;                
+                pane2.setSelected(true);                 
+            }
+        });
+
+        assertSame(Color.RED, label1.foreground().target());
+        assertSame(Color.BLACK, label2.foreground().target());
+    }
+}