/**************************************************************** * Copyright (c) 2006, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Red Hat Inc. - ongoing maintenance **************************************************************** */ package org.eclipse.linuxtools.systemtap.graphing.ui.charts; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.linuxtools.internal.systemtap.graphing.ui.GraphingUIPlugin; import org.eclipse.linuxtools.internal.systemtap.graphing.ui.preferences.GraphingPreferenceConstants; import org.eclipse.linuxtools.systemtap.graphing.core.adapters.IAdapter; import org.eclipse.linuxtools.systemtap.graphing.ui.charts.listeners.AbstractChartMouseMoveListener; import org.eclipse.linuxtools.systemtap.structures.listeners.IUpdateListener; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.swtchart.Chart; import org.swtchart.ITitle; /** * A {@link Composite} that provides the common members and the framework to build one chart. * @author Qi Liang */ public abstract class AbstractChartBuilder extends Composite implements IUpdateListener { /** * Font name for all titles, labels, and values. */ protected final static String FONT_NAME = "MS Sans Serif"; //$NON-NLS-1$ protected int maxItems; protected double scale = 1.0; /** * @since 3.0 */ protected double scaleY = 1.0; /** * @since 3.0 */ protected double scroll = 1.0; /** * @since 3.0 */ protected double scrollY = 1.0; /** * Provides data for chart. */ protected IAdapter adapter = null; protected static final Color WHITE = Display.getDefault().getSystemColor(SWT.COLOR_WHITE); protected static final Color BLACK = Display.getDefault().getSystemColor(SWT.COLOR_BLACK); protected static final Color RED = Display.getDefault().getSystemColor(SWT.COLOR_RED); protected static final Color[] COLORS = { RED, Display.getDefault().getSystemColor(SWT.COLOR_GREEN), Display.getDefault().getSystemColor(SWT.COLOR_BLUE), Display.getDefault().getSystemColor(SWT.COLOR_YELLOW), Display.getDefault().getSystemColor(SWT.COLOR_MAGENTA), Display.getDefault().getSystemColor(SWT.COLOR_CYAN), BLACK, new Color(Display.getDefault(), 64, 128, 128), new Color(Display.getDefault(), 255, 165, 0), new Color(Display.getDefault(), 128, 128, 128), }; /** * Chart instance. */ protected Chart chart = null; /** * Chart title. */ protected String title = null; private List<IUpdateListener> listeners = new ArrayList<>(); /** * The mouse listener that watches for MouseMove events over a specified region. * It is null by default. * @since 3.0 */ protected AbstractChartMouseMoveListener chartMouseMoveListener = null; /** * If a mouse listener is registered, returns a message with details on the mouse's * current position on the chart. This method is primarily used for testing purposes. * @return A String message if a mouse listener is registered; null otherwise. * @since 3.2 */ public String getMouseMessage() { return chartMouseMoveListener != null ? chartMouseMoveListener.getMouseMessage() : null; } /** * A reference to the SystemTap Graphing preference store. * @since 3.0 */ protected IPreferenceStore store; /** * Updates the chart with properties read from user-set preferences. It is called automatically * whenever a change is made to SystemTap Graphing preferences. * @param event The update event containing details on the preference that was changed. * @since 3.0 */ protected void updateProperties(PropertyChangeEvent event) { if (event.getProperty().equals(GraphingPreferenceConstants.P_VIEWABLE_DATA_ITEMS) || event.getProperty().equals(GraphingPreferenceConstants.P_MAX_DATA_ITEMS)) { maxItems = Math.min(store.getInt(GraphingPreferenceConstants.P_VIEWABLE_DATA_ITEMS), store.getInt(GraphingPreferenceConstants.P_MAX_DATA_ITEMS)); updateDataSet(); } } private IPropertyChangeListener propertyChangeListener; /** * Constructs a chart builder and associates it to one data set. * @param adapter An {@link IAdapter} for reading from the chart's data set. * @param parent The parent {@link Composite} that will contain this chart builder. * @param style The style of the chart to construct. * @param title The title of the chart to construct. */ public AbstractChartBuilder(IAdapter adapter, Composite parent, int style, String title) { super(parent, style); this.adapter = adapter; this.title = title; this.setLayout(new FillLayout()); store = GraphingUIPlugin.getDefault().getPreferenceStore(); maxItems = Math.min(store.getInt(GraphingPreferenceConstants.P_VIEWABLE_DATA_ITEMS), store.getInt(GraphingPreferenceConstants.P_MAX_DATA_ITEMS)); propertyChangeListener = event -> updateProperties(event); store.addPropertyChangeListener(propertyChangeListener); } @Override public void dispose() { store.removePropertyChangeListener(propertyChangeListener); propertyChangeListener = null; super.dispose(); } /** * Builds one chart. */ public void build() { createChart(); buildPlot(); buildLegend(); buildTitle(); buildXAxis(); buildYAxis(); buildXSeries(); buildYSeries(); updateDataSet(); } /** * Creates chart instance. */ protected void createChart() { this.chart = new Chart(this, getStyle()); } /** * Builds plot. */ private void buildPlot() { this.chart.setBackground(WHITE); this.chart.setBackgroundInPlotArea(WHITE); } /** * Builds X axis. */ protected abstract void buildXAxis(); /** * Builds Y axis. */ protected abstract void buildYAxis(); /** * Builds X series. */ protected abstract void buildXSeries(); /** * Builds Y series. */ protected abstract void buildYSeries(); public abstract void updateDataSet(); /** * Builds legend. * */ private void buildLegend() { chart.getLegend().setPosition(SWT.RIGHT); } /** * Builds the chart title. */ private void buildTitle() { ITitle ctitle = chart.getTitle(); ctitle.setForeground(BLACK); ctitle.setText(this.title); } /** * Returns the chart instance. * * @return the chart instance */ public Chart getChart() { return chart; } public void setScale(double scale) { if (scale < 0) { this.scale = 0; } else if (scale > 1) { this.scale = 1; } else { this.scale = scale; } handleUpdateEvent(); } /** * @since 3.0 * @return The current horizontal scale of the chart. */ public double getScale() { return this.scale; } /** * @param scale The desired vertical scale of the chart. * @since 3.0 */ public void setScaleY(double scale) { if (scale < 0) { this.scaleY = 0; } else if (scale > 1) { this.scaleY = 1; } else { this.scaleY = scale; } handleUpdateEvent(); } /** * @since 3.0 * @return The current vertical scale of the chart. */ public double getScaleY() { return this.scaleY; } /** * @param scroll The desired horizontal scroll of the chart. * @since 3.0 */ public void setScroll(double scroll) { if (scroll < 0) { this.scroll = 0; } else if (scroll > 1) { this.scroll = 1; } else { this.scroll = scroll; } handleUpdateEvent(); } /** * @since 3.0 * @return The current horizontal scroll of the chart. */ public double getScroll() { return this.scroll; } /** * @param scroll The desired vertical scroll of the chart. * @since 3.0 */ public void setScrollY(double scroll) { if (scroll < 0) { this.scrollY = 0; } else if (scroll > 1) { this.scrollY = 1; } else { this.scrollY = scroll; } handleUpdateEvent(); } /** * @since 3.0 * @return The current vertical scroll of the chart. */ public double getScrollY() { return this.scrollY; } /** * Converts a value into its {@link Double} equivalent. * @param o The object to convert to a {@link Double}. * @return The object in the form of a {@link Double}. May be <code>null</code> * if conversion is not possible, or if the object was null in the first place. * @since 3.0 */ protected Double getDoubleOrNullValue(Object o) { if (o == null) { return null; } if (o instanceof Integer) { return ((Integer)o).doubleValue(); } if (o instanceof Double) { return (Double) o; } try { return new Double(o.toString()); } catch (NumberFormatException e) { return null; } } @Override public void handleUpdateEvent() { if (chart != null && !chart.isDisposed()) { repaint(); } } /** * @param l A {@link IUpdateListener} to register with this chart. * @since 3.0 */ public void addUpdateListener(IUpdateListener l) { listeners.add(l); } /** * @param l A previously-registered {@link IUpdateListener} to remove. * @return <code>true</code> if the listener was removed, * <code>false</code> otherwise (such as when the provided * listener was not already registered). * @since 3.0 */ public boolean removeUpdateListener(IUpdateListener l) { return listeners.remove(l); } private void repaint() { getDisplay().asyncExec(() -> { if (!chart.isDisposed()) { updateDataSet(); for (IUpdateListener l : listeners) { l.handleUpdateEvent(); } } }); } /** * Given an array of label name strings, returns a new array in which all duplicate labels * have been given unique names. * @param labels An array of label names. * @return A new array containing unique label names. * @since 3.0 */ protected String[] getUniqueNames(String[] labels) { Set<String> labelsUnique = new LinkedHashSet<>(); for (String label : labels) { int count = 1; while (!labelsUnique.add(makeCountedLabel(label, count))) { count++; } } return labelsUnique.toArray(new String[labels.length]); } private String makeCountedLabel(String original, int count) { return count <= 1 ? original : original.concat(String.format(" (%d)", count)); //$NON-NLS-1$ } }