changeset 637:2e7690c2c441

Thread timeline improvements review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-September/003355.html reviewed-by: rkennke
author Mario Torre <neugens.limasoftware@gmail.com>
date Fri, 21 Sep 2012 21:11:23 +0200
parents 9e4437930235
children f355627c5cf3
files client/swing-components/src/main/java/com/redhat/thermostat/swing/Palette.java thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadTimelineBean.java thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadTimelineView.java thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/ThreadTimelineController.java thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineChart.java thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineView.java thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/ThreadTimelineChart.java thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/ThreadTimelineLegendPanel.java
diffstat 8 files changed, 467 insertions(+), 151 deletions(-) [+]
line wrap: on
line diff
--- a/client/swing-components/src/main/java/com/redhat/thermostat/swing/Palette.java	Fri Sep 21 18:17:41 2012 +0200
+++ b/client/swing-components/src/main/java/com/redhat/thermostat/swing/Palette.java	Fri Sep 21 21:11:23 2012 +0200
@@ -40,6 +40,9 @@
 
 public enum Palette {
 
+    THERMOSTAT_BLU(new Color(74, 93, 117)),
+    THERMOSTAT_RED(new Color(226, 46, 42)),
+    
     RED(new Color(192, 0, 0)),
     PALE_RED(new Color(192, 80, 77)),
     
--- a/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadTimelineBean.java	Fri Sep 21 18:17:41 2012 +0200
+++ b/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadTimelineBean.java	Fri Sep 21 21:11:23 2012 +0200
@@ -38,13 +38,23 @@
 
 import java.util.Date;
 
-public class ThreadTimelineBean {
+public class ThreadTimelineBean implements Cloneable {
 
     private long startTime;
     private long stopTime;
     private String name;
     private Thread.State state;
     
+    private boolean highlight;
+    
+    public boolean isHighlight() {
+        return highlight;
+    }
+    
+    public void setHighlight(boolean highlight) {
+        this.highlight = highlight;
+    }
+    
     public Thread.State getState() {
         return state;
     }
@@ -83,4 +93,68 @@
                 + ", startTime=" + startTime + " (" + new Date(startTime) + ")"
                 + ", stopTime=" + stopTime + " (" + new Date(stopTime) + ")]";
     }
+    
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + (int) (startTime ^ (startTime >>> 32));
+        result = prime * result + ((state == null) ? 0 : state.hashCode());
+        result = prime * result + (int) (stopTime ^ (stopTime >>> 32));
+        return result;
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        ThreadTimelineBean other = (ThreadTimelineBean) obj;
+        if (name == null) {
+            if (other.name != null)
+                return false;
+        } else if (!name.equals(other.name))
+            return false;
+        if (startTime != other.startTime)
+            return false;
+        if (state != other.state)
+            return false;
+        if (stopTime != other.stopTime)
+            return false;
+        return true;
+    }
+    
+    /**
+     * NOTE: A {@link ThreadTimelineBean} is contains another if they are
+     * either equals, or the the name, state and start time are the same
+     * (in other words, this method does not check the stop time, and is not a
+     * strict Set operation).
+     */
+    public boolean contains(ThreadTimelineBean other) {
+        if (equals(other)) {
+            return true;
+        }
+        if (getName().equals(other.getName())      &&
+            getState().equals(other.getState())    &&
+            getStartTime() == other.getStartTime())
+        {
+            return true;
+        }
+        
+        return false;
+    }
+    
+    @Override
+    public ThreadTimelineBean clone() {
+        ThreadTimelineBean copy = new ThreadTimelineBean();
+        copy.name = this.name;
+        copy.startTime = this.startTime;
+        copy.stopTime = this.stopTime;
+        copy.state = this.state;
+        return copy;
+    }
 }
--- a/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadTimelineView.java	Fri Sep 21 18:17:41 2012 +0200
+++ b/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadTimelineView.java	Fri Sep 21 21:11:23 2012 +0200
@@ -40,10 +40,30 @@
 import java.util.Map;
 
 import com.redhat.thermostat.client.osgi.service.BasicView;
+import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.ActionNotifier;
 import com.redhat.thermostat.thread.model.ThreadInfoData;
 
 public abstract class ThreadTimelineView extends BasicView {
 
+    public static enum ThreadTimelineViewAction {
+        THREAD_TIMELINE_SELECTED
+    }
+    
+    protected final ActionNotifier<ThreadTimelineViewAction> threadTimelineNotifier;
+    public ThreadTimelineView() {
+        threadTimelineNotifier = new ActionNotifier<>(this);
+    }
+    
+    public void addThreadSelectionActionListener(ActionListener<ThreadTimelineViewAction> listener) {
+        threadTimelineNotifier.addActionListener(listener);
+    }
+    
+    public void removeThreadSelectionActionListener(ActionListener<ThreadTimelineViewAction> listener) {
+        threadTimelineNotifier.removeActionListener(listener);
+    }
+    
     public abstract void displayStats(Map<ThreadInfoData, List<ThreadTimelineBean>> timelines, long start, long stop);
-
+    public abstract void setMarkersMessage(String left, String right);
+    public abstract void resetMarkerMessage();
 }
--- a/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/ThreadTimelineController.java	Fri Sep 21 18:17:41 2012 +0200
+++ b/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/ThreadTimelineController.java	Fri Sep 21 21:11:23 2012 +0200
@@ -36,14 +36,18 @@
 
 package com.redhat.thermostat.thread.client.controller.impl;
 
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Stack;
 
+import com.redhat.thermostat.common.ActionEvent;
+import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.Timer;
 import com.redhat.thermostat.thread.client.common.ThreadTimelineBean;
 import com.redhat.thermostat.thread.client.common.ThreadTimelineView;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineView.ThreadTimelineViewAction;
 import com.redhat.thermostat.thread.client.common.collector.ThreadCollector;
 import com.redhat.thermostat.thread.model.ThreadInfoData;
 
@@ -52,16 +56,37 @@
     private ThreadTimelineView view;
     private ThreadCollector collector;
     
+    private final String lock = new String("ThreadTimelineController"); 
+    private ThreadTimelineBean latestSelected;
+    
     public ThreadTimelineController(ThreadTimelineView view, ThreadCollector collector, Timer timer) {
         super(timer, view);
         timer.setAction(new ThreadTimelineControllerAction());
         this.view = view;
+        this.view.addThreadSelectionActionListener(new ThreadTimelineSelectedAction());
         this.collector = collector;
     }
 
+    private class ThreadTimelineSelectedAction implements ActionListener<ThreadTimelineViewAction> {
+        
+        @Override
+        public void actionPerformed(ActionEvent<ThreadTimelineViewAction> actionEvent) {
+            synchronized (lock) {
+                latestSelected = (ThreadTimelineBean) actionEvent.getPayload();
+            }
+        }
+    }
+    
     private class ThreadTimelineControllerAction implements Runnable {
         @Override
         public void run() {
+            ThreadTimelineBean _latestSelected = null;
+            synchronized (lock) {
+                if (latestSelected != null) {
+                    _latestSelected = latestSelected.clone();
+                }
+            }
+            
             List<ThreadInfoData> infos = collector.getThreadInfo();
             if(infos.size() > 0) {
                 
@@ -88,6 +113,10 @@
                     
                     Stack<ThreadTimelineBean> threadTimelines = new Stack<ThreadTimelineBean>();
                     timelines.put(lastThreadInfo, threadTimelines);
+                    
+                    if (_latestSelected != null && _latestSelected.contains(timeline)) {
+                        timeline.setHighlight(true);
+                    }                    
                     threadTimelines.push(timeline);
                     
                     for (int i = beanList.size() - 1; i >= 0; i--) {
@@ -100,6 +129,10 @@
                             timeline = threadTimelines.pop();
                             timeline.setStopTime(threadInfo.getTimeStamp());
                             
+                            if (_latestSelected != null && _latestSelected.contains(timeline)) {
+                                timeline.setHighlight(true);
+                            }
+                            
                             threadTimelines.push(timeline);
                             
                             timeline = new ThreadTimelineBean();
@@ -111,6 +144,10 @@
                             lastThreadInfo = threadInfo;
                             lastState = currentState;
                             
+                            if (_latestSelected != null && _latestSelected.contains(timeline)) {
+                                timeline.setHighlight(true);
+                            }
+
                             // add the new thread stat
                             threadTimelines.push(timeline);
                         }
@@ -118,6 +155,13 @@
                 }
                 
                 view.displayStats(timelines, infos.get(infos.size() - 1).getTimeStamp(), infos.get(0).getTimeStamp());
+
+                if (_latestSelected != null) {
+                    view.setMarkersMessage(new Date(_latestSelected.getStartTime()).toString(),
+                                           new Date(_latestSelected.getStopTime()).toString());
+                } else {
+                    view.resetMarkerMessage();
+                }
             }
         }
     }
--- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineChart.java	Fri Sep 21 18:17:41 2012 +0200
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineChart.java	Fri Sep 21 21:11:23 2012 +0200
@@ -38,13 +38,19 @@
 
 import java.awt.BasicStroke;
 import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
 import java.awt.GradientPaint;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.Paint;
+import java.awt.Point;
+import java.awt.Rectangle;
 import java.awt.Shape;
-import java.awt.geom.Area;
+import java.awt.Stroke;
+import java.awt.geom.Rectangle2D;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 
 import javax.swing.JFrame;
@@ -57,14 +63,23 @@
 import com.redhat.thermostat.swing.models.LongRange;
 import com.redhat.thermostat.swing.models.LongRangeNormalizer;
 import com.redhat.thermostat.thread.client.common.ThreadTimelineBean;
+import com.redhat.thermostat.thread.client.common.chart.ChartColors;
 
 @SuppressWarnings("serial")
 public class SwingThreadTimelineChart extends JPanel {
 
+    private String leftMarkerMessage; 
+    private String rightMarkerMessage;
+    
+    public static final String HIGHLIGHT_THREAD_STATE_PROPERTY = "highlightThreadThreadProperty";
+    private ThreadTimelineBean selectedThread;
+    
     private List<ThreadTimelineBean> timeline;
     
     private LongRangeNormalizer normalizer;
 
+    private Point clickArea;
+    
     public SwingThreadTimelineChart(List<ThreadTimelineBean> timeline, long rangeStart, long rangeStop) {
         this.timeline = timeline;
     
@@ -75,10 +90,23 @@
         setBorder(new GradientRoundBorder());
         normalizer = new LongRangeNormalizer(range);
     }
+
+    public void clickAndHighlightArea(Point point) {
+        clickArea = point;
+        repaint();
+    }
+   
+    public void unsetHighlightArea() {
+        clickArea = null;
+        selectedThread = null;
+        repaint();
+    }
     
     @Override
     protected void paintComponent(Graphics g) {
         
+        ThreadTimelineBean oldSelectedThread = selectedThread;
+        
         normalizer.setMinNormalized(0);
         normalizer.setMaxNormalized(getWidth() - 3);
         
@@ -86,72 +114,95 @@
         
         Graphics2D graphics = utils.createAAGraphics(g);
         
+        Font font = graphics.getFont();
+        graphics.setFont(font.deriveFont(font.getSize() - 2));
+        
         int y = getHeight()/3;
         graphics.clearRect(0, 0, getWidth(), getHeight());
         graphics.drawString(timeline.get(0).getName(), 2, y);
         
         y = getHeight()/2;
+
+        graphics.translate(2, y);
+        if (clickArea != null) {
+            clickArea.translate(0, -y);
+        }
         
         int w = getWidth() - 4;
-        int h = 10;
+        int h = 15;
         
         Shape shape = utils.getRoundShape(w, h);
-        
-        graphics.translate(2, y);
 
         Paint paint = new GradientPaint(0, 0, Palette.DARK_GRAY.getColor(), 0, h, Palette.WHITE.getColor());
         graphics.setPaint(paint);        
         graphics.fill(shape);        
 
+                
         for (ThreadTimelineBean thread : timeline) {
+            
+            boolean isSelected = false;
             normalizer.setValue(thread.getStartTime());
             int x0 = (int) normalizer.getValueNormalized();
 
             normalizer.setValue(thread.getStopTime());
             int x1 = (int) normalizer.getValueNormalized();
 
-            graphics.setColor(getColor(thread));
+            Color currentThreadColour = ChartColors.getColor(thread.getState());
+            Rectangle currentArea = new Rectangle(x0, 0, x1 - x0, h);
+            if (clickArea != null && currentArea.contains(clickArea)) {
+                currentThreadColour = Palette.THERMOSTAT_BLU.getColor();
+                selectedThread = thread;
+                isSelected = true;
+                
+            } else if (thread.isHighlight()) {
+                currentThreadColour = Palette.THERMOSTAT_BLU.getColor();
+                isSelected = true;
+            }
+            
+            graphics.setColor(currentThreadColour);
             graphics.fillRect(x0, 1, x1 - x0, h - 1);
+            
+            if (isSelected) {
+                paintMarks(graphics, x0, x1, h, thread.getStartTime(), thread.getStopTime(), ChartColors.getColor(thread.getState()));
+                String tooltipString = "";
+                if (leftMarkerMessage != null) {
+                    tooltipString = leftMarkerMessage + " - ";
+                }
+                if (rightMarkerMessage != null) {
+                    tooltipString += " " + rightMarkerMessage;
+                }
+                setToolTipText(tooltipString);
+            }
         }
         
-        graphics.setColor(getColor(timeline.get(timeline.size() - 1)));
-        graphics.draw(shape);        
-        graphics.dispose();
-    }
-
-    private Color getColor(ThreadTimelineBean thread) {
-        Color result = null;
-        
-        switch (thread.getState()) {
-        case TIMED_WAITING:
-            result = Palette.PALE_RED.getColor();
-            break;
-            
-        case NEW:
-            result = Palette.POMP_AND_POWER_VIOLET.getColor();
-            break;
+        graphics.setColor(ChartColors.getColor(timeline.get(timeline.size() - 1).getState()));
+        graphics.draw(shape);
 
-        case RUNNABLE:
-            result = Palette.PRUSSIAN_BLUE.getColor();
-            break;
-
-        case TERMINATED:
-            result = Palette.GRAY.getColor();
-            break;
-
-        case BLOCKED:
-            result = Palette.RED.getColor();            
-            break;
-
-        case WAITING:
-            result = Palette.GRANITA_ORANGE.getColor();            
-            break;
-
-        default:
-            result = Color.BLACK;            
-            break;
+        graphics.dispose();
+        
+        if (selectedThread != null || oldSelectedThread != null) {
+            firePropertyChange(HIGHLIGHT_THREAD_STATE_PROPERTY, oldSelectedThread, selectedThread);
         }
-        return result;
+    }
+    
+    private void paintMarks(Graphics2D graphics, int x0, int x1, int y, long start, long stop, Color colour) {
+        
+        graphics.setColor(colour);
+        graphics.fillRect(x0, y + 5, x1 - x0, 2);
+        
+        GraphicsUtils utils = GraphicsUtils.getInstance();
+        FontMetrics metrics = utils.getFontMetrics(this, graphics.getFont());
+        
+        graphics.setColor(getForeground());
+        if (leftMarkerMessage != null) {
+            Rectangle2D rect = metrics.getStringBounds(leftMarkerMessage, graphics);
+            graphics.drawString(leftMarkerMessage, x0 - (int) rect.getWidth(), y - (int) rect.getY());
+        }
+        
+        if (rightMarkerMessage != null) {
+            Rectangle2D rect = metrics.getStringBounds(leftMarkerMessage, graphics);
+            graphics.drawString(rightMarkerMessage, x1, y - (int) rect.getY());
+        }
     }
     
     public static void main(String[] args) {
@@ -185,7 +236,10 @@
                 timeline.add(bean2);
                 timeline.add(bean3);
 
+                bean3.setHighlight(true);
+                
                 SwingThreadTimelineChart chart = new SwingThreadTimelineChart(timeline, 0, 2500);
+                chart.setMarkersMessage(new Date(bean3.getStartTime()).toString(), new Date(bean3.getStopTime()).toString());
                 
                 frame.add(chart);
                 frame.setSize(800, 150);
@@ -194,4 +248,9 @@
             }
         });
     }
+
+    public void setMarkersMessage(String leftMarkerMessage, String rightMarkerMessage) {
+        this.leftMarkerMessage = leftMarkerMessage;
+        this.rightMarkerMessage = rightMarkerMessage;   
+    }
 }
--- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineView.java	Fri Sep 21 18:17:41 2012 +0200
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineView.java	Fri Sep 21 21:11:23 2012 +0200
@@ -39,6 +39,12 @@
 import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Dimension;
+import java.awt.GridLayout;
+import java.awt.Point;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
 import java.util.List;
 import java.util.Map;
 
@@ -47,8 +53,8 @@
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.ListCellRenderer;
-import javax.swing.ListSelectionModel;
 import javax.swing.SwingUtilities;
+import javax.swing.SwingWorker;
 
 import com.redhat.thermostat.client.ui.ComponentVisibleListener;
 import com.redhat.thermostat.client.ui.SwingComponent;
@@ -58,8 +64,14 @@
 
 public class SwingThreadTimelineView extends ThreadTimelineView  implements SwingComponent  {
 
+    private final String lock = new String("SwingThreadTimelineViewLock");
+        
     private JPanel timeLinePanel;
-    private DefaultListModel<SwingThreadTimelineChart> model;
+    private JList<SwingThreadTimelineChart> chartList;
+    private DefaultListModel<SwingThreadTimelineChart> chartModel;
+    
+    private String leftMarkerMessage; 
+    private String rightMarkerMessage;
     
     public SwingThreadTimelineView() {
         timeLinePanel = new JPanel();
@@ -76,22 +88,21 @@
         });
         
         timeLinePanel.setLayout(new BorderLayout(0, 0));
-        model = new DefaultListModel<>();
-        JList<SwingThreadTimelineChart> chartList = new JList<>(model);
+        
         
-        chartList.setLayoutOrientation(JList.VERTICAL);
-        chartList.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
+        chartModel = new DefaultListModel<>();
+        chartList = new JList<>(chartModel);
+        
+        chartList.setCellRenderer(new ChartRenderer());
+        chartList.addMouseListener(new ChartListListener());
+        
         JScrollPane scrollPane = new JScrollPane(chartList);
-        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
-        timeLinePanel.add(scrollPane);
-        chartList.setCellRenderer(new ListCellRenderer<SwingThreadTimelineChart>() {
-            @Override
-            public Component getListCellRendererComponent(
-                    JList<? extends SwingThreadTimelineChart> list, SwingThreadTimelineChart value,
-                    int index, boolean isSelected, boolean cellHasFocus) {
-                return value;
-            }
-        });
+        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+        
+        timeLinePanel.add(scrollPane, BorderLayout.CENTER);
+        ThreadTimelineLegendPanel timelineLegend = new ThreadTimelineLegendPanel();
+        timeLinePanel.add(timelineLegend, BorderLayout.SOUTH);
     }
     
     @Override
@@ -101,18 +112,94 @@
             
             @Override
             public void run() {
-                model.clear();
+                String _leftMarkerMessage = null; 
+                String _rightMarkerMessage = null;
+                synchronized (lock) {
+                    _leftMarkerMessage = leftMarkerMessage;
+                    _rightMarkerMessage = rightMarkerMessage;                        
+                }
+                
+                chartModel.clear();
                 for (List<ThreadTimelineBean> timeline : timelines.values()) {
                     SwingThreadTimelineChart panel = new SwingThreadTimelineChart(timeline, start, stop);
-                    panel.setPreferredSize(new Dimension(timeLinePanel.getWidth(), 50));
-                    model.addElement(panel);
+                    panel.setPreferredSize(new Dimension(chartList.getWidth(), 75));
+                    panel.setMarkersMessage(_leftMarkerMessage, _rightMarkerMessage);
+                    panel.addPropertyChangeListener(SwingThreadTimelineChart.HIGHLIGHT_THREAD_STATE_PROPERTY,
+                                                    new SelectedThreadListener());
+                    panel.setMarkersMessage(_leftMarkerMessage, _rightMarkerMessage);
+                    chartModel.addElement(panel);
                 }
             }
         });
     }
     
+    private class SelectedThreadListener implements PropertyChangeListener {
+        @Override
+        public void propertyChange(final PropertyChangeEvent evt) {
+            SwingWorker<Void, Void> notifier = new SwingWorker<Void, Void>() {
+                @Override
+                protected Void doInBackground() throws Exception {
+                    SwingThreadTimelineView.this.
+                    threadTimelineNotifier.fireAction(ThreadTimelineView.ThreadTimelineViewAction.THREAD_TIMELINE_SELECTED,
+                                                      evt.getNewValue());
+                    return null;
+                }
+            };
+            notifier.execute();
+        }
+    }
+    
+    private class ChartRenderer implements ListCellRenderer<SwingThreadTimelineChart> {
+        @Override
+        public Component getListCellRendererComponent(JList<? extends SwingThreadTimelineChart> list,
+                                                      SwingThreadTimelineChart chart,
+                                                      int index, boolean isSelected,
+                                                      boolean cellHasFocus)
+        {
+            if (!isSelected) {
+                chart.unsetHighlightArea();
+            }
+            return chart;
+        }
+    }
+    
+    private class ChartListListener extends MouseAdapter {
+        
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            int index = chartList.getSelectedIndex();
+            if (index > 0) {
+                Point listIndex = chartList.indexToLocation(index);
+                Point absoluteLocation = e.getPoint();
+                listIndex.x = absoluteLocation.x;
+                if (index != 0) {
+                    listIndex.y = absoluteLocation.y - listIndex.y;
+                }
+                
+                SwingThreadTimelineChart chart = chartModel.get(index);
+                chart.clickAndHighlightArea(listIndex);                
+            }
+        }
+    }
+    
     @Override
     public Component getUiComponent() {
         return timeLinePanel;
     }
+    
+    @Override
+    public void resetMarkerMessage() {
+        synchronized (lock) {
+            this.leftMarkerMessage = null;
+            this.rightMarkerMessage = null;            
+        }
+    }
+    
+    @Override
+    public void setMarkersMessage(String left, String right) {
+        synchronized (lock) {
+            this.leftMarkerMessage = left;
+            this.rightMarkerMessage = right;            
+        }
+    }
 }
--- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/ThreadTimelineChart.java	Fri Sep 21 18:17:41 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/*
- * 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.thread.client.swing.impl;
-
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-
-import javax.swing.JFrame;
-import javax.swing.JPanel;
-import javax.swing.SwingUtilities;
-import javax.swing.plaf.ColorUIResource;
-
-import com.redhat.thermostat.swing.GraphicsUtils;
-
-@SuppressWarnings("serial")
-public class ThreadTimelineChart extends JPanel {
-
-    private static final ColorUIResource TICK_COLOR = new ColorUIResource(0xa8aca8);
-
-    public ThreadTimelineChart() {
-    }
-    
-    @Override
-    protected void paintComponent(Graphics g) {
-        Graphics2D graphics = GraphicsUtils.getInstance().createAAGraphics(g);
-        
-        graphics.setColor(TICK_COLOR);
-        
-        int x = 1;
-        int y = 2;
-        int w = getWidth() - 2;
-        int h = getHeight();
-        
-        graphics.drawLine(x, y, w, y);
-        
-        graphics.dispose();
-    }
-    
-    public static void main(String[] args) {
-        SwingUtilities.invokeLater(new Runnable() {
-            
-            @Override
-            public void run() {
-                JFrame frame = new JFrame();
-                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-                
-                ThreadTimelineChart chart = new ThreadTimelineChart();
-                
-                frame.add(chart);
-                frame.setSize(500, 500);
-                frame.setVisible(true);
-            }
-        });
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/ThreadTimelineLegendPanel.java	Fri Sep 21 21:11:23 2012 +0200
@@ -0,0 +1,118 @@
+/*
+ * 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.thread.client.swing.impl;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+
+import javax.swing.Icon;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+
+import com.redhat.thermostat.swing.GraphicsUtils;
+import com.redhat.thermostat.thread.client.common.chart.ChartColors;
+
+@SuppressWarnings("serial")
+public class ThreadTimelineLegendPanel extends JPanel {
+    
+    public ThreadTimelineLegendPanel() {
+        setLayout(new FlowLayout(FlowLayout.RIGHT, 10, 10));
+        setPreferredSize(new Dimension(getWidth(), 40));
+        
+        for (Thread.State state : Thread.State.values()) {
+            
+            Color color = ChartColors.getColor(state);
+            // no chart is black, it's just the default colour
+            if (!color.equals(Color.BLACK)) {
+                JLabel label =  new JLabel(new ColorIcon(color), SwingConstants.LEFT);
+                label.setText(state.toString());
+                add(label);
+            }
+        }
+    }
+    
+    private class ColorIcon implements Icon {
+        
+        private Color color;
+        private ColorIcon(Color color) {
+            this.color = color;
+        }
+        
+        @Override
+        public int getIconHeight() {
+            return 16;
+        }
+        
+        @Override
+        public int getIconWidth() {
+            return 16;
+        }
+        
+        @Override
+        public void paintIcon(Component c, Graphics g, int x, int y) {
+            Graphics2D graphics = GraphicsUtils.getInstance().createAAGraphics(g);
+            graphics.setColor(color);
+            graphics.fillRect(x, y, getIconWidth(), getIconHeight());
+            graphics.dispose();
+        }
+    }
+
+    public static void main(String[] args) {
+        SwingUtilities.invokeLater(new Runnable() {
+            
+            @Override
+            public void run() {
+                JFrame frame = new JFrame();
+                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+               
+                ThreadTimelineLegendPanel panel = new ThreadTimelineLegendPanel();
+                
+                frame.add(panel);
+                frame.setSize(800, 150);
+                
+                frame.setVisible(true);
+            }
+        });
+    }
+}