changeset 1285:4b729a28bcad

New component for selecting a time interval Reviewed-by: neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-October/008468.html
author Omair Majid <omajid@redhat.com>
date Fri, 18 Oct 2013 18:11:03 -0400
parents ca5077eea3c2
children f1ce8301fdcf
files client/swing/src/main/java/com/redhat/thermostat/client/swing/components/BasicEventTimelineUI.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalMouseHandler.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelector.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorModel.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorUI.java client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorUIBasic.java client/swing/src/test/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorModelTest.java client/swing/src/test/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorTest.java
diffstat 8 files changed, 772 insertions(+), 70 deletions(-) [+]
line wrap: on
line diff
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/BasicEventTimelineUI.java	Fri Oct 18 16:57:09 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/BasicEventTimelineUI.java	Fri Oct 18 18:11:03 2013 -0400
@@ -55,14 +55,14 @@
 import java.awt.event.HierarchyBoundsListener;
 import java.awt.event.HierarchyEvent;
 import java.awt.event.HierarchyListener;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
 import java.util.concurrent.TimeUnit;
 
 import javax.swing.JButton;
+import javax.swing.JComponent;
 import javax.swing.JPanel;
 
 import com.redhat.thermostat.client.swing.components.EventTimelineModel.Event;
+import com.redhat.thermostat.client.swing.components.TimelineIntervalMouseHandler.TimeIntervalSelectorTarget;
 import com.redhat.thermostat.client.swing.components.timeline.Timeline;
 import com.redhat.thermostat.client.swing.internal.LocaleResources;
 import com.redhat.thermostat.common.model.LongRangeNormalizer;
@@ -306,23 +306,15 @@
         }
     }
 
-    private class OverviewPanel extends JPanel {
+    private class OverviewPanel extends JPanel implements TimeIntervalSelectorTarget {
 
         private int MOUSE_MARGIN = 10;
 
         private int left;
         private int right;
 
-        private boolean moving = false;
-        private boolean movingLeft = false;
-        private boolean movingRight = false;
-
-        private int oldX = -1;
-        private int oldLeft = -1;
-        private int oldRight = -1;
-
         public OverviewPanel() {
-            OverviewMotionListener chartMotionListener = new OverviewMotionListener();
+            TimelineIntervalMouseHandler chartMotionListener = new TimelineIntervalMouseHandler(this);
             addMouseMotionListener(chartMotionListener);
             addMouseListener(chartMotionListener);
         }
@@ -399,67 +391,29 @@
             g.dispose();
         }
 
-        class OverviewMotionListener extends MouseAdapter {
-            @Override
-            public void mouseMoved(MouseEvent e) {
-                if (Math.abs(e.getX() - left) < MOUSE_MARGIN) {
-                    setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
-                } else if (Math.abs(e.getX() - right) < MOUSE_MARGIN) {
-                    setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
-                } else if ((e.getX() > MOUSE_MARGIN + left) && (e.getX() < right - MOUSE_MARGIN)) {
-                    setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
-                } else {
-                    setCursor(Cursor.getDefaultCursor());
-                }
-
-            }
+        @Override
+        public int getSelectionMargin() {
+            return MOUSE_MARGIN;
+        }
 
-            @Override
-            public void mouseDragged(MouseEvent e) {
-                if (moving || movingLeft || movingRight) {
-
-                    int newLeft = oldLeft;
-                    int newRight = oldRight;
-                    if (movingLeft) {
-                        newLeft = e.getX();
-                    } else if (movingRight) {
-                        newRight = e.getX();
-                    } else if (moving) {
-                        long delta = e.getX() - oldX;
-                        newLeft += delta;
-                        newRight += delta;
-                    }
+        @Override
+        public int getLeftSelectionPosition() {
+            return left;
+        }
 
-                    Range<Long> range = new Range<Long>(positionToTimeStamp(newLeft), positionToTimeStamp(newRight));
-
-                    eventTimeline.getModel().setDetailRange(range);
-
-                    refresh();
-                }
-            }
-
-            @Override
-            public void mouseReleased(MouseEvent e) {
-                moving = movingLeft = movingRight = false;
-                oldLeft = oldRight = oldX = -1;
-            }
+        @Override
+        public int getRightSelectionPosition() {
+            return right;
+        }
 
-            @Override
-            public void mousePressed(MouseEvent e) {
-                if (Math.abs(e.getX() - left) < MOUSE_MARGIN) {
-                    movingLeft = true;
-                } else if (Math.abs(e.getX() - right) < MOUSE_MARGIN) {
-                    movingRight = true;
-                } else if ((e.getX() > left + MOUSE_MARGIN) && (e.getX() < right - MOUSE_MARGIN)) {
-                    moving = true;
-                }
-                Range<Long> range = eventTimeline.getModel().getDetailRange();
-                oldLeft = timeStampToPosition(range.getMin());
-                oldRight = timeStampToPosition(range.getMax());
-                oldX = e.getX();
-            }
-
-            // TODO implement wheel scrolling
+        @Override
+        public void updateSelectionPosition(int newLeft, int newRight) {
+            left = newLeft;
+            right = newRight;
+            Range<Long> range = new Range<Long>(positionToTimeStamp(left), positionToTimeStamp(right));
+            eventTimeline.getModel().setDetailRange(range);
+            refresh();
         }
     }
+
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalMouseHandler.java	Fri Oct 18 18:11:03 2013 -0400
@@ -0,0 +1,118 @@
+/*
+ * 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.Cursor;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+class TimelineIntervalMouseHandler extends MouseAdapter {
+
+    static interface TimeIntervalSelectorTarget {
+        void setCursor(Cursor cursor);
+        int getSelectionMargin();
+        int getLeftSelectionPosition();
+        int getRightSelectionPosition();
+        void updateSelectionPosition(int left, int right);
+    }
+
+    private final TimeIntervalSelectorTarget target;
+
+    private boolean moving = false;
+    private boolean movingLeft = false;
+    private boolean movingRight = false;
+
+    private int oldX = -1;
+    private int oldLeft = -1;
+    private int oldRight = -1;
+
+    public TimelineIntervalMouseHandler(TimeIntervalSelectorTarget target) {
+        this.target = target;
+    }
+
+    @Override
+    public void mouseMoved(MouseEvent e) {
+        if (Math.abs(e.getX() - target.getLeftSelectionPosition()) < target.getSelectionMargin()) {
+            target.setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
+        } else if (Math.abs(e.getX() - target.getRightSelectionPosition()) < target.getSelectionMargin()) {
+            target.setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
+        } else if ((e.getX() > target.getSelectionMargin() + target.getLeftSelectionPosition()) && (e.getX() < target.getRightSelectionPosition() - target.getSelectionMargin())) {
+            target.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+        } else {
+            target.setCursor(Cursor.getDefaultCursor());
+        }
+
+    }
+
+    @Override
+    public void mouseDragged(MouseEvent e) {
+        if (moving || movingLeft || movingRight) {
+
+            int newLeft = oldLeft;
+            int newRight = oldRight;
+            if (movingLeft) {
+                newLeft = e.getX();
+            } else if (movingRight) {
+                newRight = e.getX();
+            } else if (moving) {
+                long delta = e.getX() - oldX;
+                newLeft += delta;
+                newRight += delta;
+            }
+
+            target.updateSelectionPosition(newLeft, newRight);
+        }
+    }
+
+    @Override
+    public void mouseReleased(MouseEvent e) {
+        moving = movingLeft = movingRight = false;
+        oldLeft = oldRight = oldX = -1;
+    }
+
+    @Override
+    public void mousePressed(MouseEvent e) {
+        if (Math.abs(e.getX() - target.getLeftSelectionPosition()) < target.getSelectionMargin()) {
+            movingLeft = true;
+        } else if (Math.abs(e.getX() - target.getRightSelectionPosition()) < target.getSelectionMargin()) {
+            movingRight = true;
+        } else if ((e.getX() > target.getLeftSelectionPosition() + target.getSelectionMargin()) && (e.getX() < target.getRightSelectionPosition() - target.getSelectionMargin())) {
+            moving = true;
+        }
+        oldLeft = target.getLeftSelectionPosition();
+        oldRight = target.getRightSelectionPosition();
+        oldX = e.getX();
+    }
+
+    // TODO implement wheel scrolling
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelector.java	Fri Oct 18 18:11:03 2013 -0400
@@ -0,0 +1,79 @@
+/*
+ * 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.UIManager;
+
+/**
+ * A component that allows specifying a time range to select
+ */
+public class TimelineIntervalSelector extends JComponent {
+
+    private static final String uiClassID = "TimelineIntervalSelectorUI";
+
+    private TimelineIntervalSelectorModel model;
+
+    public TimelineIntervalSelector() {
+        model = new TimelineIntervalSelectorModel();
+
+        updateUI();
+    }
+
+    public TimelineIntervalSelectorModel getModel() {
+        return model;
+    }
+
+    public void setUI(TimelineIntervalSelectorUI ui) {
+        super.setUI(ui);
+    }
+
+    @Override
+    public void updateUI() {
+        if (UIManager.get(getUIClassID()) != null) {
+            setUI((TimelineIntervalSelectorUI) UIManager.getUI(this));
+        } else {
+            setUI(new TimelineIntervalSelectorUIBasic());
+        }
+    }
+
+    public TimelineIntervalSelectorUI getUI() {
+        return (TimelineIntervalSelectorUI) ui;
+    }
+
+    @Override
+    public String getUIClassID() {
+        return uiClassID;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorModel.java	Fri Oct 18 18:11:03 2013 -0400
@@ -0,0 +1,141 @@
+/*
+ * 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.util.EventListener;
+
+import javax.swing.event.EventListenerList;
+
+public class TimelineIntervalSelectorModel {
+
+    public static interface ChangeListener extends EventListener {
+        public void changed();
+    }
+
+    private final EventListenerList listeners = new EventListenerList();
+
+    private long totalMinimum = 0;
+    private long totalMaximum = 100;
+
+    private long selectedMinimum = 0;
+    private long selectedMaximum = 10;
+
+    public long getTotalMinimum() {
+        return totalMinimum;
+    }
+
+    public void setTotalMinimum(long totalMinimum) {
+        if (this.totalMinimum != totalMinimum) {
+            this.totalMinimum = totalMinimum;
+
+            if (this.totalMaximum < this.totalMinimum) {
+                this.totalMaximum = this.totalMinimum;
+            }
+
+            if (this.selectedMaximum < this.totalMinimum){
+                this.selectedMaximum = this.totalMinimum;
+            }
+
+            if (this.selectedMinimum < this.totalMinimum) {
+                this.selectedMinimum = this.totalMinimum;
+            }
+
+            fireModelChanged();
+        }
+    }
+
+    public long getTotalMaximum() {
+        return totalMaximum;
+    }
+
+    public void setTotalMaximum(long totalMaximum) {
+        if (this.totalMaximum != totalMaximum) {
+            this.totalMaximum = totalMaximum;
+
+            if (this.totalMinimum > this.totalMaximum) {
+                this.totalMinimum = this.totalMaximum;
+            }
+
+            if (this.selectedMaximum > this.totalMaximum) {
+                this.selectedMaximum = this.totalMaximum;
+            }
+
+            if (this.selectedMinimum > this.totalMaximum) {
+                this.selectedMinimum = this.totalMaximum;
+            }
+
+            fireModelChanged();
+        }
+    }
+
+    public long getSelectedMinimum() {
+        return selectedMinimum;
+    }
+
+    public void setSelectedMinimum(long selectedMinimum) {
+        if(this.selectedMinimum != selectedMinimum) {
+            this.selectedMinimum = selectedMinimum;
+            fireModelChanged();
+        }
+    }
+
+    public long getSelectedMaximum() {
+        return selectedMaximum;
+    }
+
+    public void setSelectedMaximum(long selectedMaximum) {
+        if (this.selectedMaximum != selectedMaximum) {
+            this.selectedMaximum = selectedMaximum;
+            fireModelChanged();
+        }
+    }
+
+    public void addChangeListener(ChangeListener l) {
+        listeners.add(ChangeListener.class, l);
+    }
+
+    public void removeChangeListener(ChangeListener l) {
+        listeners.remove(ChangeListener.class, l);
+    }
+
+    private void fireModelChanged() {
+        Object[] listeners = this.listeners.getListenerList();
+
+        for (int i = listeners.length - 2; i >= 0; i -= 2) {
+            if (listeners[i] == ChangeListener.class) {
+                ((ChangeListener) listeners[i + 1]).changed();
+            }
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorUI.java	Fri Oct 18 18:11:03 2013 -0400
@@ -0,0 +1,39 @@
+/*
+ * 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.plaf.ComponentUI;
+
+public class TimelineIntervalSelectorUI extends ComponentUI {
+    // a marker class
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorUIBasic.java	Fri Oct 18 18:11:03 2013 -0400
@@ -0,0 +1,213 @@
+/*
+ * 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.Component;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.LineBorder;
+
+import com.redhat.thermostat.client.swing.components.TimelineIntervalMouseHandler.TimeIntervalSelectorTarget;
+import com.redhat.thermostat.client.swing.components.TimelineIntervalSelectorModel.ChangeListener;
+import com.redhat.thermostat.client.swing.components.timeline.Timeline;
+import com.redhat.thermostat.common.model.Range;
+
+public class TimelineIntervalSelectorUIBasic extends TimelineIntervalSelectorUI implements TimeIntervalSelectorTarget {
+
+    private static final int PREF_HEIGHT = 20; /* pixels */
+
+    private TimelineIntervalSelector component;
+
+    private TimelineIntervalMouseHandler mouseListener = new TimelineIntervalMouseHandler(this);
+    private Timeline timeline = new Timeline(new Range<Long>(0l, 100l));
+    private JPanel customPaintingPanel = new CustomPaintPanel();
+
+    private Component topGlue;
+    private Component bottomGlue;
+
+    private ChangeListener timelineSelectionPainter = new ChangeListener() {
+        @Override
+        public void changed() {
+            component.repaint();
+        }
+    };
+    private ChangeListener timelineRangeUpdater = new ChangeListener() {
+        @Override
+        public void changed() {
+            TimelineIntervalSelectorModel model = component.getModel();
+            timeline.setRange(new Range<>(model.getTotalMinimum(), model.getTotalMaximum()));
+        }
+    };
+
+    @Override
+    public void installUI(JComponent c) {
+        super.installUI(c);
+
+        component = (TimelineIntervalSelector) c;
+
+        installDefaults();
+        installComponents();
+        installListeners();
+
+    }
+
+    protected void installDefaults() {
+        component.setLayout(new BoxLayout(component, BoxLayout.PAGE_AXIS));
+        component.setBorder(new EmptyBorder(5,5,5,5));
+    }
+
+    protected void installComponents() {
+        topGlue = Box.createVerticalGlue();
+        bottomGlue = Box.createVerticalGlue();
+
+        component.add(topGlue);
+        component.add(timeline);
+        component.add(customPaintingPanel);
+        component.add(bottomGlue);
+    }
+
+    protected void installListeners() {
+        component.getModel().addChangeListener(timelineSelectionPainter);
+        component.getModel().addChangeListener(timelineRangeUpdater);
+
+        component.addMouseListener(mouseListener);
+        component.addMouseMotionListener(mouseListener);
+        component.addMouseWheelListener(mouseListener);
+    }
+
+    @Override
+    public void uninstallUI(JComponent c) {
+        uninstallListeners();
+        uninstallComponents();
+        uninstallDefaults();
+
+        component = null;
+
+        super.uninstallUI(c);
+    }
+
+    protected void uninstallComponents() {
+        component.remove(bottomGlue);
+        component.remove(customPaintingPanel);
+        component.remove(timeline);
+        component.remove(topGlue);
+    }
+
+    protected void uninstallListeners() {
+        component.removeMouseWheelListener(mouseListener);
+        component.removeMouseMotionListener(mouseListener);
+        component.removeMouseListener(mouseListener);
+
+        component.getModel().removeChangeListener(timelineRangeUpdater);
+        component.getModel().removeChangeListener(timelineSelectionPainter);
+    }
+
+    protected void uninstallDefaults() {
+        component.setLayout(null);
+    }
+
+    @Override
+    public int getLeftSelectionPosition() {
+        return domainToX(component.getModel().getSelectedMinimum());
+    }
+
+    @Override
+    public int getRightSelectionPosition() {
+        return domainToX(component.getModel().getSelectedMaximum());
+    }
+
+    @Override
+    public int getSelectionMargin() {
+        return 20;
+    }
+
+    @Override
+    public void setCursor(Cursor cursor) {
+        component.setCursor(cursor);
+    }
+
+    @Override
+    public void updateSelectionPosition(int left, int right) {
+        long min = xToDomain(left);
+        long max = xToDomain(right);
+        component.getModel().setSelectedMaximum(max);
+        component.getModel().setSelectedMinimum(min);
+
+        component.repaint();
+    }
+
+    private int domainToX(long domainValue) {
+        long domainMin = component.getModel().getTotalMinimum();
+        long domainMax = component.getModel().getTotalMaximum();
+        int width = component.getWidth();
+        return (int) (1.0 * (domainValue - domainMin) / (domainMax - domainMin) * width);
+    }
+
+    private long xToDomain(int x) {
+        long domainMin = component.getModel().getTotalMinimum();
+        long domainMax = component.getModel().getTotalMaximum();
+        int width = component.getWidth();
+        return (long) ((1.0 * x / (width) * (domainMax - domainMin)) + domainMin);
+    }
+
+    private class CustomPaintPanel extends JPanel {
+
+        @Override
+        public Dimension getPreferredSize() {
+            return new Dimension(super.getPreferredSize().height, PREF_HEIGHT);
+        }
+
+        @Override
+        public void paint(Graphics g) {
+            super.paint(g);
+
+            Graphics2D g2 = (Graphics2D) g.create();
+
+            int startX = domainToX(component.getModel().getSelectedMinimum());
+            int endX = domainToX(component.getModel().getSelectedMaximum());
+
+            g2.fillRect(startX, 0, (endX - startX), getHeight());
+
+            g2.dispose();
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/test/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorModelTest.java	Fri Oct 18 18:11:03 2013 -0400
@@ -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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class TimelineIntervalSelectorModelTest {
+
+    @Test
+    public void testChangingTotalMinimumAdjustsOtherValues() {
+        TimelineIntervalSelectorModel model = new TimelineIntervalSelectorModel();
+        model.setTotalMinimum(0);
+        model.setTotalMaximum(100);
+        model.setSelectedMinimum(20);
+        model.setSelectedMaximum(80);
+
+        model.setTotalMinimum(1000);
+
+        assertEquals(1000, model.getTotalMinimum());
+        assertTrue(model.getTotalMaximum() >= 1000);
+        assertTrue(model.getSelectedMinimum() >= 1000);
+        assertTrue(model.getSelectedMaximum() >= 1000);
+    }
+
+    @Test
+    public void testChangingTotalMaximumAdjustsOtherValues() {
+        TimelineIntervalSelectorModel model = new TimelineIntervalSelectorModel();
+        model.setTotalMinimum(0);
+        model.setTotalMaximum(100);
+        model.setSelectedMinimum(20);
+        model.setSelectedMaximum(80);
+
+        model.setTotalMaximum(-100);
+
+        assertEquals(-100, model.getTotalMaximum());
+        assertTrue(model.getTotalMinimum() <= -100);
+        assertTrue(model.getSelectedMinimum() <= -100);
+        assertTrue(model.getSelectedMaximum() <= -100);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing/src/test/java/com/redhat/thermostat/client/swing/components/TimelineIntervalSelectorTest.java	Fri Oct 18 18:11:03 2013 -0400
@@ -0,0 +1,84 @@
+/*
+ * 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.BorderLayout;
+import java.awt.Color;
+import java.util.concurrent.TimeUnit;
+
+import javax.swing.BorderFactory;
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+import javax.swing.WindowConstants;
+
+import com.redhat.thermostat.client.swing.components.TimelineIntervalSelectorModel.ChangeListener;
+
+public class TimelineIntervalSelectorTest {
+
+    public static void main(String[] args) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                JFrame mainWindow = new JFrame();
+
+                final TimelineIntervalSelector intervalSelector = new TimelineIntervalSelector();
+
+                long now = System.currentTimeMillis();
+
+                intervalSelector.getModel().addChangeListener(new ChangeListener() {
+                    @Override
+                    public void changed() {
+                        // System.out.println(intervalSelector.getModel().getTotalMinimum());
+                        // System.out.println(intervalSelector.getModel().getTotalMaximum());
+                        //
+                        // System.out.println(intervalSelector.getModel().getSelectedMinimum());
+                        // System.out.println(intervalSelector.getModel().getSelectedMaximum());
+
+                    }
+                });
+
+                intervalSelector.getModel().setTotalMinimum(now);
+                intervalSelector.getModel().setTotalMaximum(now + TimeUnit.HOURS.toMillis(1));
+
+                intervalSelector.getModel().setSelectedMinimum(now);
+                intervalSelector.getModel().setSelectedMaximum(now + TimeUnit.MINUTES.toMillis(10));
+
+                mainWindow.add(intervalSelector, BorderLayout.CENTER);
+                mainWindow.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+
+                mainWindow.setVisible(true);
+
+            }
+        });
+    }
+}