changeset 740:2f1cedb72af5

Core Eclipse charting functionality This commit contains common code for each of the Thermostat Eclipse chart views. The commit includes Eclipse ViewParts that handle a HostRef or VmRef being selected in the Hosts/VMs view. These serve as base classes for each individual chart view. The commit also contains a RecentTimeSeriesComposite and RecentTimeSeriesController which are SWT-ported versions of the exisiting similarly-named Swing GUI classes. There is also a ViewFactory for SWT-based Thermostat views (that's MVC view, not Eclipse view). This will go away once each view becomes an OSGi service, like what is done with the Thread view. Also included is a ViewVisibilityWatcher (that's Eclipse view), that maps ActionNotifier callbacks to the associated view visibility events. Reviewed-by: jerboaa, omajid Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-September/003347.html
author Elliott Baron <ebaron@redhat.com>
date Thu, 25 Oct 2012 13:30:44 -0400
parents 588ee42e01d4
children e9531651f79d
files eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/Activator.java eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/ChartUtils.java eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/HostRefViewPart.java eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/RecentTimeSeriesChartComposite.java eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/RecentTimeSeriesChartController.java eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/RefViewPart.java eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/ViewVisibilityWatcher.java eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/VmRefViewPart.java eclipse/com.redhat.thermostat.eclipse.test.ui/src/com/redhat/thermostat/eclipse/test/ui/RecentTimeSeriesChartCompositeTest.java eclipse/com.redhat.thermostat.eclipse.test/src/com/redhat/thermostat/eclipse/test/views/ViewVisibilityWatcherTest.java
diffstat 10 files changed, 1306 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/Activator.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import org.eclipse.ui.plugin.AbstractUIPlugin;
+import org.osgi.framework.BundleContext;
+
+import com.redhat.thermostat.client.core.views.HostCpuViewProvider;
+import com.redhat.thermostat.client.core.views.HostMemoryViewProvider;
+import com.redhat.thermostat.client.core.views.VmCpuViewProvider;
+import com.redhat.thermostat.client.core.views.VmGcViewProvider;
+import com.redhat.thermostat.common.utils.OSGIUtils;
+
+public class Activator extends AbstractUIPlugin {
+
+    // The plug-in ID
+    public static final String PLUGIN_ID = "com.redhat.thermostat.eclipse.chart.common"; //$NON-NLS-1$
+
+    private static Activator plugin;
+  
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext
+     * )
+     */
+    public void start(BundleContext context) throws Exception {
+        super.start(context);
+        plugin = this;
+        
+        // Register our ViewProviders
+        OSGIUtils.getInstance().registerService(HostCpuViewProvider.class, new SWTHostCpuViewProvider());
+        OSGIUtils.getInstance().registerService(HostMemoryViewProvider.class, new SWTHostMemoryViewProvider());
+        OSGIUtils.getInstance().registerService(VmCpuViewProvider.class, new SWTVmCpuViewProvider());
+        OSGIUtils.getInstance().registerService(VmGcViewProvider.class, new SWTVmGcViewProvider());
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext
+     * )
+     */
+    public void stop(BundleContext context) throws Exception {
+        plugin = null;
+        super.stop(context);
+    }
+    
+    public static Activator getDefault() {
+        return plugin;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/ChartUtils.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.eclipse.ui.PlatformUI;
+
+public class ChartUtils {
+    
+    /**
+     * Executes runnable in the SWT UI thread after the necessary
+     * controls are created.
+     * @param latch - blocks runnable until count is zero
+     * @param runnable - task to execute
+     */
+    public static void runAfterCreated(CountDownLatch latch, Runnable runnable) {
+        try {
+            latch.await();
+            PlatformUI.getWorkbench().getDisplay().syncExec(runnable);
+        } catch (InterruptedException e) {
+            // Restore interrupted status
+            Thread.currentThread().interrupt();
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/HostRefViewPart.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import com.redhat.thermostat.common.dao.HostRef;
+import com.redhat.thermostat.common.dao.VmRef;
+
+public abstract class HostRefViewPart extends RefViewPart<HostRef> {
+
+    public HostRefViewPart() {
+        super();
+    }
+
+    @Override
+    protected HostRef getRefFromSelection(Object selection) {
+        HostRef ref = null;
+        if (selection instanceof HostRef) {
+            ref = (HostRef) selection;
+        }
+        else if (selection instanceof VmRef) {
+            ref = ((VmRef) selection).getAgent();
+        }
+        return ref;
+    }
+
+    @Override
+    protected String getNoSelectionMessage() {
+        return "No host selected";
+    }
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/RecentTimeSeriesChartComposite.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowData;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.PlatformUI;
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+
+import com.redhat.thermostat.client.locale.LocaleResources;
+import com.redhat.thermostat.common.locale.Translate;
+import com.redhat.thermostat.eclipse.ThermostatConstants;
+
+public class RecentTimeSeriesChartComposite extends Composite {
+    public static final String TEST_ID_UNIT_COMBO = "RecentTimeSeriesChartComposite.timeUnit";
+    public static final String TEST_ID_DURATION_TEXT = "RecentTimeSeriesChartComposite.timeDuration";
+    
+    private static final int MINIMUM_DRAW_SIZE = 100;
+    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+
+    private final RecentTimeSeriesChartController controller;
+
+    private Composite labelContainer;
+    private Label label;
+    private Combo unitSelector;
+    private Text durationSelector;
+
+    public RecentTimeSeriesChartComposite(Composite parent, int style, JFreeChart chart) {
+        super(parent, style);
+        this.setLayout(new GridLayout());
+        this.controller = new RecentTimeSeriesChartController(this, chart);
+
+        final ChartPanel cp = controller.getChartComposite();
+
+        cp.setDisplayToolTips(false);
+        cp.setMouseZoomable(false);
+        cp.setPopupMenu(null);
+
+        /*
+         * By default, ChartPanel scales itself instead of redrawing things when
+         * it's resized. To have it resize automatically, we need to set minimum
+         * and maximum sizes. Lets constrain the minimum, but not the maximum
+         * size.
+         */
+        cp.setMinimumDrawHeight(MINIMUM_DRAW_SIZE);
+        cp.setMaximumDrawHeight(Integer.MAX_VALUE);
+        cp.setMinimumDrawWidth(MINIMUM_DRAW_SIZE);
+        cp.setMaximumDrawWidth(Integer.MAX_VALUE);
+
+        getControlsAndAdditionalDisplay(this);
+    }
+
+    private Composite getControlsAndAdditionalDisplay(Composite parent) {
+        Composite container = new Composite(parent, SWT.NONE);
+        GridLayout containerLayout = new GridLayout(2, false);
+        containerLayout.marginHeight = 0;
+        container.setLayout(containerLayout);
+
+        getChartControls(container);
+        getAdditionalDataDisplay(container);
+
+        return container;
+    }
+
+    private Composite getChartControls(Composite parent) {
+        Composite container = new Composite(parent, SWT.NONE);
+        RowLayout topLayout = new RowLayout(SWT.HORIZONTAL);
+        topLayout.center = true;
+        topLayout.marginHeight = 0;
+        container.setLayout(topLayout);
+        
+        Label prompt = new Label(container, SWT.NONE);
+        prompt.setText(translator.localize(LocaleResources.CHART_DURATION_SELECTOR_LABEL));
+
+        durationSelector = new Text(container, SWT.BORDER);
+        durationSelector.setData(ThermostatConstants.TEST_TAG, TEST_ID_DURATION_TEXT);
+        // Make 5 chars wide
+        GC gc = new GC(durationSelector);
+        FontMetrics metrics = gc.getFontMetrics();
+        int width = metrics.getAverageCharWidth() * 5;
+        int height = metrics.getHeight();
+        gc.dispose();
+        durationSelector.setLayoutData(new RowData(computeSize(width, height)));
+        
+        unitSelector = new Combo(container, SWT.NONE);
+        unitSelector.setData(ThermostatConstants.TEST_TAG, TEST_ID_UNIT_COMBO);
+        TimeUnit[] units = controller.getTimeUnits();
+        for (TimeUnit unit : units) {
+            unitSelector.add(unit.toString());
+        }
+
+        int defaultValue = controller.getTimeValue();
+        TimeUnit defaultUnit = controller.getTimeUnit();
+
+        TimeUnitChangeListener timeUnitChangeListener = new TimeUnitChangeListener(controller, defaultValue, defaultUnit);
+
+        durationSelector.addModifyListener(timeUnitChangeListener);
+        unitSelector.addSelectionListener(timeUnitChangeListener);
+
+        durationSelector.setText(String.valueOf(defaultValue));
+        int defaultPos = Arrays.asList(units).indexOf(defaultUnit);
+        unitSelector.select(defaultPos);
+
+        return container;
+    }
+
+    private Composite getAdditionalDataDisplay(Composite parent) {
+        Composite top = new Composite(parent, SWT.NONE);
+        GridLayout topLayout = new GridLayout();
+        topLayout.marginHeight = 0;
+        top.setLayout(topLayout);
+        top.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+        labelContainer = new Composite(top, SWT.NONE);
+        return top;
+    }
+
+    public void setDataInformationLabel(final String text) {
+        PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (label == null) {
+                    label = new Label(labelContainer, SWT.NONE);
+                }
+
+                label.setText(text);
+            }
+        });
+    }
+
+    private class TimeUnitChangeListener implements SelectionListener, ModifyListener {
+
+        private final RecentTimeSeriesChartController controller;
+        private int value;
+        private TimeUnit unit;
+
+        public TimeUnitChangeListener(RecentTimeSeriesChartController controller, int defaultValue, TimeUnit defaultUnit) {
+            this.controller = controller;
+            this.value = defaultValue;
+            this.unit = defaultUnit;
+        }
+
+        private void updateChartParameters() {
+            controller.setTime(value, unit);
+        }
+
+        @Override
+        public void modifyText(ModifyEvent e) {
+            if (durationSelector.equals(e.widget)) {
+                try {
+                    this.value = Integer.valueOf(durationSelector.getText());
+                    updateChartParameters();
+                } catch (NumberFormatException nfe) {
+                    // ignore
+                }
+            }
+        }
+
+        @Override
+        public void widgetSelected(SelectionEvent e) {
+            if (unitSelector.equals(e.widget)) {
+                int idx = unitSelector.getSelectionIndex();
+                if (idx >= 0) {
+                    unit = controller.getTimeUnits()[idx];
+                    updateChartParameters();
+                }
+            }
+        }
+
+        @Override
+        public void widgetDefaultSelected(SelectionEvent e) {
+            widgetSelected(e);
+        }
+    }
+    
+    public RecentTimeSeriesChartController getController() {
+        return controller;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/RecentTimeSeriesChartController.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import java.awt.Frame;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.awt.SWT_AWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.plot.XYPlot;
+
+public class RecentTimeSeriesChartController {
+
+    private static final int DEFAULT_VALUE = 10;
+    private static final TimeUnit DEFAULT_UNIT = TimeUnit.MINUTES;
+
+    private JFreeChart chart;
+    private ChartPanel panel;
+    private int timeValue = DEFAULT_VALUE;
+    private TimeUnit timeUnit = DEFAULT_UNIT;
+
+    public RecentTimeSeriesChartController(Composite parent, JFreeChart chart) {
+        this.chart = chart;
+        Composite top = new Composite(parent, SWT.NONE | SWT.EMBEDDED);
+        top.setLayout(new GridLayout());
+        top.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+        Frame frame = SWT_AWT.new_Frame(top);
+        this.panel = new ChartPanel(chart, true);
+        frame.add(panel);
+
+        // instead of just disabling display of tooltips, disable their generation too
+        if (chart.getPlot() instanceof XYPlot) {
+            chart.getXYPlot().getRenderer().setBaseToolTipGenerator(null);
+        }
+
+        chart.getXYPlot().getDomainAxis().setAutoRange(true);
+        chart.getXYPlot().getDomainAxis().setFixedAutoRange(timeUnit.toMillis(timeValue));
+
+        chart.getXYPlot().getRangeAxis().setAutoRange(true);
+
+    }
+
+    public ChartPanel getChartComposite() {
+        return panel;
+    }
+
+    public TimeUnit[] getTimeUnits() {
+        return new TimeUnit[] { TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES };
+    }
+
+    public int getTimeValue() {
+        return timeValue;
+    }
+
+    public TimeUnit getTimeUnit() {
+        return timeUnit;
+    }
+
+    public void setTime(int value, TimeUnit unit) {
+        this.timeValue = value;
+        this.timeUnit = unit;
+
+        updateChart();
+    }
+
+    private void updateChart() {
+        chart.getXYPlot().getDomainAxis().setAutoRange(true);
+        chart.getXYPlot().getDomainAxis().setFixedAutoRange(timeUnit.toMillis(timeValue));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/RefViewPart.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.ISelectionListener;
+import org.eclipse.ui.IViewPart;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.part.ViewPart;
+
+import com.redhat.thermostat.common.dao.Ref;
+import com.redhat.thermostat.eclipse.ThermostatConstants;
+import com.redhat.thermostat.eclipse.views.HostsVmsTreeViewPart;
+
+public abstract class RefViewPart<T extends Ref> extends ViewPart implements ISelectionListener {
+
+    protected Composite top;
+
+    private Composite parent;
+    private Object selectedElement;
+
+    public RefViewPart() {
+        super();
+    }
+
+    @Override
+    public void createPartControl(Composite parent) {
+        this.parent = parent;
+        
+        createComposite();
+        
+        getWorkbenchWindow().getSelectionService().addSelectionListener(this);
+        
+        // Check for an existing selection
+        boolean selected = false;
+        IViewPart part = getWorkbenchWindow().getActivePage().findView(ThermostatConstants.VIEW_ID_HOST_VM);
+        if (part != null && part instanceof HostsVmsTreeViewPart) {
+            ISelection selection = part.getSite().getSelectionProvider().getSelection();
+            if (selection instanceof IStructuredSelection) {
+                handleSelection(selection);
+                selected = true;
+            }
+        }
+        if (!selected) {
+            createNoSelectionLabel();
+        }
+    }
+
+    public void createNoSelectionLabel() {
+        Label noHost = new Label(top, SWT.NONE);
+        noHost.setText(getNoSelectionMessage());
+    }
+
+    public IWorkbenchWindow getWorkbenchWindow() {
+        return PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+    }
+    
+    @Override
+    public void setFocus() {
+        parent.setFocus();
+    }
+
+    protected abstract void createControllerView(T ref);
+    
+    protected abstract T getRefFromSelection(Object selection);
+    
+    protected abstract String getNoSelectionMessage();
+    
+    @Override
+    public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+        // We must have received createPartControl
+        if (parent != null && !parent.isDisposed()) {
+            // Check if a HostRef has been selected
+            if (part instanceof HostsVmsTreeViewPart) {
+                if (selection instanceof IStructuredSelection) {
+                    handleSelection(selection);
+                }
+            }
+        }
+    }
+
+    private void handleSelection(ISelection selection) {
+        Object previous = selectedElement;
+        selectedElement = ((IStructuredSelection) selection).getFirstElement();
+        if (selectedElement != previous) {
+            T ref = getRefFromSelection(selectedElement);
+            if (ref != null) {
+                // Replace the existing view
+                top.dispose();
+                createComposite();
+   
+                createControllerView(ref);
+   
+                parent.layout();
+            }
+        }
+    }
+
+    private void createComposite() {
+        top = new Composite(parent, SWT.NONE);
+        top.setLayout(new GridLayout());
+        top.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/ViewVisibilityWatcher.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IPartListener2;
+import org.eclipse.ui.IViewPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPartReference;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+
+import com.redhat.thermostat.client.core.views.BasicView.Action;
+import com.redhat.thermostat.common.ActionNotifier;
+
+public class ViewVisibilityWatcher {
+    
+    private ActionNotifier<Action> notifier;
+    private boolean visible;
+
+    public ViewVisibilityWatcher(ActionNotifier<Action> notifier) {
+        this.notifier = notifier;
+        this.visible = false;
+    }
+    
+    public void watch(Composite parent, final String viewID) {
+        // Check if the view is currently visible
+        IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
+        IViewPart chartView = activePage.findView(viewID);
+        if (activePage.isPartVisible(chartView)) {
+            visible = true;
+            notifier.fireAction(Action.VISIBLE);
+        }
+        
+        // Register listener for visibility updates on the Eclipse view
+        final IPartListener2 partListener = new IPartListener2() {
+            
+            @Override
+            public void partVisible(IWorkbenchPartReference partRef) {
+                // The workbench fires a visible event when the view first takes
+                // focus, even if it was already on top
+                if (!visible && viewID.equals(partRef.getId())) {
+                    notifier.fireAction(Action.VISIBLE);
+                    visible = true;
+                }
+            }
+            
+            @Override
+            public void partHidden(IWorkbenchPartReference partRef) {
+                if (visible && viewID.equals(partRef.getId())) {
+                    notifier.fireAction(Action.HIDDEN);
+                    visible = false;
+                }
+            }
+
+            @Override
+            public void partOpened(IWorkbenchPartReference partRef) {
+            }
+            
+            @Override
+            public void partInputChanged(IWorkbenchPartReference partRef) {
+            }
+            
+            @Override
+            public void partDeactivated(IWorkbenchPartReference partRef) {
+            }
+            
+            @Override
+            public void partClosed(IWorkbenchPartReference partRef) {
+            }
+            
+            @Override
+            public void partBroughtToTop(IWorkbenchPartReference partRef) {
+            }
+            
+            @Override
+            public void partActivated(IWorkbenchPartReference partRef) {
+            }
+        };
+        
+        PlatformUI.getWorkbench().getActiveWorkbenchWindow().getPartService().addPartListener(partListener);
+        
+        parent.addDisposeListener(new DisposeListener() {
+            
+            @Override
+            public void widgetDisposed(DisposeEvent e) {
+                PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
+                    
+                    @Override
+                    public void run() {
+                        IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+                        if (window != null) {
+                            window.getPartService().removePartListener(partListener);
+                        }
+                    }
+                });
+            }
+        });
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/VmRefViewPart.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import com.redhat.thermostat.common.dao.VmRef;
+
+public abstract class VmRefViewPart extends RefViewPart<VmRef> {
+
+    public VmRefViewPart() {
+        super();
+    }
+
+    @Override
+    protected VmRef getRefFromSelection(Object selection) {
+        VmRef ref = null;
+        if (selection instanceof VmRef) {
+            ref = (VmRef) selection;
+        }
+        return ref;
+    }
+
+    @Override
+    protected String getNoSelectionMessage() {
+        return "No VM selected";
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.test.ui/src/com/redhat/thermostat/eclipse/test/ui/RecentTimeSeriesChartCompositeTest.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2012 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.eclipse.test.ui;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
+import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
+import org.eclipse.swtbot.swt.finder.waits.DefaultCondition;
+import org.eclipse.swtbot.swt.finder.widgets.SWTBotCombo;
+import org.eclipse.swtbot.swt.finder.widgets.SWTBotText;
+import org.eclipse.swtbot.swt.finder.widgets.TimeoutException;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.redhat.thermostat.eclipse.ThermostatConstants;
+import com.redhat.thermostat.eclipse.chart.common.RecentTimeSeriesChartComposite;
+
+@RunWith(SWTBotJunit4ClassRunner.class)
+public class RecentTimeSeriesChartCompositeTest {
+    private SWTWorkbenchBot bot;
+    private Shell shell;
+    private JFreeChart chart;
+    private RecentTimeSeriesChartComposite composite;
+
+    @Before
+    public void beforeTest() throws Exception {
+        bot = new SWTWorkbenchBot();
+
+        Display.getDefault().syncExec(new Runnable() {
+
+            @Override
+            public void run() {
+                shell = new Shell(Display.getCurrent());
+                Composite parent = new Composite(shell, SWT.NONE);
+                parent.setLayout(new GridLayout());
+                parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true,
+                        true));
+
+                chart = createChart();
+                composite = new RecentTimeSeriesChartComposite(parent,
+                        SWT.NONE, chart);
+                shell.open();
+            }
+        });
+    }
+
+    private JFreeChart createChart() {
+        JFreeChart chart = ChartFactory.createTimeSeriesChart(null, null, null,
+                new TimeSeriesCollection(), false, false, false);
+        return chart;
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        Display.getDefault().syncExec(new Runnable() {
+
+            @Override
+            public void run() {
+                if (shell != null) {
+                    shell.close();
+                }
+            }
+        });
+    }
+
+    @Test
+    public void testTimeUnitMinutes() throws Exception {
+        SWTBotCombo timeUnitCombo = bot.comboBoxWithId(
+                ThermostatConstants.TEST_TAG,
+                RecentTimeSeriesChartComposite.TEST_ID_UNIT_COMBO);
+        
+        List<TimeUnit> units = Arrays.asList(composite.getController().getTimeUnits());
+        timeUnitCombo.setSelection(units.indexOf(TimeUnit.MINUTES));
+        
+        checkTimeUnit(TimeUnit.MINUTES);
+    }
+    
+    @Test
+    public void testTimeUnitHours() throws Exception {
+        SWTBotCombo timeUnitCombo = bot.comboBoxWithId(
+                ThermostatConstants.TEST_TAG,
+                RecentTimeSeriesChartComposite.TEST_ID_UNIT_COMBO);
+        
+        List<TimeUnit> units = Arrays.asList(composite.getController().getTimeUnits());
+        timeUnitCombo.setSelection(units.indexOf(TimeUnit.HOURS));
+        
+        checkTimeUnit(TimeUnit.HOURS);
+    }
+    
+    @Test
+    public void testTimeUnitDays() throws Exception {
+        SWTBotCombo timeUnitCombo = bot.comboBoxWithId(
+                ThermostatConstants.TEST_TAG,
+                RecentTimeSeriesChartComposite.TEST_ID_UNIT_COMBO);
+        
+        List<TimeUnit> units = Arrays.asList(composite.getController().getTimeUnits());
+        timeUnitCombo.setSelection(units.indexOf(TimeUnit.DAYS));
+        
+        checkTimeUnit(TimeUnit.DAYS);
+    }
+    
+    @Test
+    public void testTimeDuration() throws Exception {
+        final int duration = 200;
+        SWTBotText durationText = bot.textWithId(ThermostatConstants.TEST_TAG,
+                RecentTimeSeriesChartComposite.TEST_ID_DURATION_TEXT);
+        
+        durationText.setText(String.valueOf(duration));
+        
+        checkTimeDuration(duration);
+    }
+    
+    @Test(expected = TimeoutException.class)
+    public void testTimeDurationBad() throws Exception {
+        final int duration = 200;
+        
+        // Set a proper duration value, then try a bad one
+        SWTBotText durationText = bot.textWithId(ThermostatConstants.TEST_TAG,
+                RecentTimeSeriesChartComposite.TEST_ID_DURATION_TEXT);
+        
+        durationText.setText(String.valueOf(duration));
+        
+        checkTimeDuration(duration);
+        
+        durationText.setText("Not an int");
+        
+        // Ensure duration unchanged
+        bot.waitWhile(new DefaultCondition() {
+            
+            @Override
+            public boolean test() throws Exception {
+                return composite.getController().getTimeValue() == duration;
+            }
+            
+            @Override
+            public String getFailureMessage() {
+                return "Success";
+            }
+        });
+    }
+
+    private void checkTimeDuration(final int duration) {
+        bot.waitUntil(new DefaultCondition() {
+            
+            @Override
+            public boolean test() throws Exception {
+                return composite.getController().getTimeValue() == duration;
+            }
+            
+            @Override
+            public String getFailureMessage() {
+                return "Duration not set";
+            }
+        });
+    }
+
+    private void checkTimeUnit(final TimeUnit unit) {
+        bot.waitUntil(new DefaultCondition() {
+            
+            @Override
+            public boolean test() throws Exception {
+                return unit.equals(composite.getController().getTimeUnit());
+            }
+            
+            @Override
+            public String getFailureMessage() {
+                return "Time unit not set to " + unit.toString();
+            }
+        });
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.test/src/com/redhat/thermostat/eclipse/test/views/ViewVisibilityWatcherTest.java	Thu Oct 25 13:30:44 2012 -0400
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2012 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.eclipse.test.views;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IPageLayout;
+import org.eclipse.ui.IViewPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.client.core.views.BasicView;
+import com.redhat.thermostat.client.core.views.BasicView.Action;
+import com.redhat.thermostat.common.ActionEvent;
+import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.ActionNotifier;
+import com.redhat.thermostat.eclipse.chart.common.ViewVisibilityWatcher;
+
+public class ViewVisibilityWatcherTest {
+    private static final Long TIME_OUT_MILLIS = 5000L;
+    private static final String VIEW_ID = IPageLayout.ID_PROBLEM_VIEW;
+    private IViewPart view;
+    private Shell shell;
+    private CountDownLatch latch;
+
+    @Before
+    public void beforeTest() throws Exception {
+        shell = new Shell(Display.getCurrent());
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        shell = null;
+    }
+
+    @Test
+    public void testVisibleBeforeAttach() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final Action[] action = new Action[1];
+
+        ActionNotifier<Action> notifier = new ActionNotifier<>(this);
+        notifier.addActionListener(new ActionListener<BasicView.Action>() {
+
+            @Override
+            public void actionPerformed(ActionEvent<Action> actionEvent) {
+                action[0] = actionEvent.getActionId();
+                latch.countDown();
+            }
+        });
+
+        ViewVisibilityWatcher watcher = new ViewVisibilityWatcher(notifier);
+
+        showView();
+
+        // Attach
+        watcher.watch(shell, VIEW_ID);
+
+        waitForAction(latch);
+
+        assertEquals(Action.VISIBLE, action[0]);
+    }
+
+    @Test
+    public void testVisibleAfterAttach() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final Action[] action = new Action[1];
+
+        ActionNotifier<Action> notifier = new ActionNotifier<>(this);
+        notifier.addActionListener(new ActionListener<BasicView.Action>() {
+
+            @Override
+            public void actionPerformed(ActionEvent<Action> actionEvent) {
+                action[0] = actionEvent.getActionId();
+                latch.countDown();
+            }
+        });
+
+        ViewVisibilityWatcher watcher = new ViewVisibilityWatcher(notifier);
+
+        // Attach
+        watcher.watch(shell, VIEW_ID);
+
+        showView();
+
+        waitForAction(latch);
+
+        assertEquals(Action.VISIBLE, action[0]);
+    }
+
+    @Test
+    public void testVisibleBeforeAttachHiddenAfter() throws Exception {
+        latch = new CountDownLatch(1);
+
+        final Action[] action = new Action[1];
+
+        ActionNotifier<Action> notifier = new ActionNotifier<>(this);
+        notifier.addActionListener(new ActionListener<BasicView.Action>() {
+
+            @Override
+            public void actionPerformed(ActionEvent<Action> actionEvent) {
+                action[0] = actionEvent.getActionId();
+                latch.countDown();
+            }
+        });
+
+        ViewVisibilityWatcher watcher = new ViewVisibilityWatcher(notifier);
+
+        showView();
+
+        // Attach
+        watcher.watch(shell, VIEW_ID);
+
+        waitForAction(latch);
+
+        assertEquals(Action.VISIBLE, action[0]);
+
+        // Hide view
+        latch = new CountDownLatch(1);
+
+        hideView();
+
+        waitForAction(latch);
+
+        assertEquals(Action.HIDDEN, action[0]);
+    }
+
+    private void waitForAction(final CountDownLatch latch)
+            throws InterruptedException {
+        if (!latch.await(TIME_OUT_MILLIS, TimeUnit.MILLISECONDS)) {
+            fail("Timeout while waiting for action");
+        }
+    }
+
+    private void showView() throws PartInitException {
+        view = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
+                .getActivePage().showView(VIEW_ID);
+    }
+
+    private void hideView() {
+        PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()
+                .hideView(view);
+    }
+
+}