/*
* ChartPanel.java, a decoration of a Chart2D that adds popup menues for traces and the chart.
* Copyright (C) 2005 - 2011 Achim Westermann.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* If you modify or optimize the code in a useful way please let me know.
* Achim.Westermann@gmx.de
*
*/
package info.monitorenter.gui.chart.views;
import info.monitorenter.gui.chart.Chart2D;
import info.monitorenter.gui.chart.ITrace2D;
import info.monitorenter.gui.chart.annotations.IAnnotationCreator;
import info.monitorenter.gui.chart.annotations.bubble.AnnotationCreatorBubble;
import info.monitorenter.gui.chart.controls.LayoutFactory;
import info.monitorenter.gui.chart.layouts.FlowLayoutCorrectMinimumSize;
import info.monitorenter.gui.chart.traces.Trace2DLtd;
import info.monitorenter.util.StringUtil;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
/**
* A decoration for {@link Chart2D} that adds various controls for a
* {@link Chart2D} and it's {@link ITrace2D} instances in form of popup menues.
* <p>
* <h2>Performance note</h2>
* The context menu items register themselves with the chart to adapt their
* basic UI properties (font, foreground color, background color) via weak
* referenced instances of
* {@link info.monitorenter.gui.chart.controls.LayoutFactory.BasicPropertyAdaptSupport}
* . This ensures that dropping a complete menu tree from the UI makes them
* garbage collectable without introduction of highly unstable and
* unmaintainable active memory management code. A side effect is that these
* listeners remain in the property change listener list of the chart unless
* they are finalized.
* <p>
* Adding and removing many traces to / from charts that are wrapped in
* {@link ChartPanel} without {@link java.lang.System#gc()} followed by
* {@link java.lang.System#runFinalization()} in your code will leave a huge
* amount of listeners for non-visible uncleaned menu items in the chart which
* causes a high cpu throttle for increasing the listener list.
* <p>
* The reason seems to be the implementation of (
* {@link javax.swing.event.EventListenerList} that is used by
* {@link javax.swing.event.SwingPropertyChangeSupport}). It is based upon an
* array an grows only for the space of an additional listener by using
* {@link java.lang.System#arraycopy(java.lang.Object, int, java.lang.Object, int, int)}
* (ouch, this should be changed).
* <p>
*
* Profiling a day with showed that up to 2000 dead listeners remained in the
* list. The cpu load increased after about 200 add / remove trace operations.
* Good news is that no memory leak could be detected.
* <p>
* If those add and remove trace operations on {@link ChartPanel} - connected
* charts are performed with intermediate UI action property change events on
* dead listeners will let them remove themselves from the listener list thus
* avoiding the cpu overhead. So UI / user - controlled applications will
* unlikely suffer from this problem.
* <p>
*
* @author <a href="mailto:Achim.Westermann@gmx.de">Achim Westermann </a>
*
*/
public class ChartPanel extends JLayeredPane implements PropertyChangeListener {
/**
* Generated <code>serialVersionUID</code>.
*/
private static final long serialVersionUID = 3905801963714197560L;
/**
* Main enbtry for demo app.
* <p>
*
* @param args
* ignored.
*/
public static void main(final String[] args) {
// some data:
final double[] data = new double[100];
for (int i = 0; i < 100; i++) {
data[i] = Math.random() * i + 1;
}
final JFrame frame = new JFrame("ChartPanel demo");
final Chart2D chart = new Chart2D();
// trace 1
final ITrace2D trace1 = new Trace2DLtd(100);
trace1.setName("Trace 1");
// AbstractDataCollector collector1 = new
// RandomDataCollectorOffset(trace1,500);
// trace2
final ITrace2D trace2 = new Trace2DLtd(100);
trace2.setName("Trace 2");
// add to chart
chart.addTrace(trace1);
chart.addTrace(trace2);
// AbstractDataCollector collector2 = new
// RandomDataCollectorOffset(trace2,500);
for (int i = 0; i < 100; i++) {
trace1.addPoint(i + 2, data[i]);
trace2.addPoint(i + 2, 100 - data[i]);
}
final ChartPanel cPanel = new ChartPanel(chart);
frame.getContentPane().add(cPanel);
frame.setSize(new Dimension(400, 600));
frame.addWindowListener(new WindowAdapter() {
/**
* @see java.awt.event.WindowAdapter#windowClosing(java.awt.event.WindowEvent)
*/
@Override
public void windowClosing(final WindowEvent w) {
System.exit(0);
}
});
frame.setJMenuBar(LayoutFactory.getInstance().createChartMenuBar(cPanel, false));
frame.setVisible(true);
}
/** The annotation creator factory for this panel. */
private IAnnotationCreator m_annotationCreator = AnnotationCreatorBubble.getInstance();
/** The decorated chart. */
private final Chart2D m_chart;
/**
* <p>
* An internal panel for the labels of the traces that uses a
* {@link FlowLayout}.
* </p>
*
*/
protected JPanel m_labelPanel;
/**
* Creates an instance that decorates the given chart with controls in form of
* popup menus.
* <p>
*
* @param chart
* A configured Chart2D instance that will be displayed and
* controlled by this panel.
*/
public ChartPanel(final Chart2D chart) {
this(chart,true);
}
/**
* Creates an instance that decorates the given chart with controls in form of
* popup menus.
* <p>
*
* @param chart
* A configured Chart2D instance that will be displayed and
* controlled by this panel.
*
* @param adaptUI2Chart
* if true the menu will adapt it's basic UI properties (font,
* foreground and background color) to the given chart.
*/
public ChartPanel(final Chart2D chart, final boolean adaptUI2Chart) {
super();
this.m_chart = chart;
this.setBackground(chart.getBackground());
// we paint our own labels
chart.setPaintLabels(false);
// get the layout factory for popup menus:
final LayoutFactory factory = LayoutFactory.getInstance();
factory.createChartPopupMenu(this, adaptUI2Chart);
// layout
this.setLayout(new BorderLayout());
this.add(chart, BorderLayout.CENTER);
// initial Labels
// put to a flow layout panel
this.m_labelPanel = new JPanel();
this.m_labelPanel.setFont(chart.getFont());
this.m_labelPanel.setLayout(new FlowLayoutCorrectMinimumSize(FlowLayout.LEFT));
this.m_labelPanel.setBackground(chart.getBackground());
JLabel label;
for (final ITrace2D trace : chart) {
label = factory.createTraceContextMenuLabel(chart, trace, true);
if (label != null) {
this.m_labelPanel.add(label);
}
// In case trace.getLabel() becomes empty hide the corresponding
// menu label via listeners!
trace.addPropertyChangeListener(ITrace2D.PROPERTY_PHYSICALUNITS, this);
trace.addPropertyChangeListener(ITrace2D.PROPERTY_NAME, this);
}
this.add(this.m_labelPanel, BorderLayout.SOUTH);
chart.addPropertyChangeListener("background", this);
// listen to new traces and deleted ones:
chart.addPropertyChangeListener(Chart2D.PROPERTY_ADD_REMOVE_TRACE, this);
}
/**
* Internal helper that returns whether a label for the given trace is already
* contained in the internal label panel.
* <p>
*
* This is needed because an addTrace(ITrace2D) call on the Chart2D is
* delegated to two axes thus resulting in two events per added trace: We have
* to avoid adding duplicate labels!
* <p>
*
* @param tracetoAdd
* the trace to check whether a label for it is already contained in
* the internal label panel.
*
* @return true if a label for the given trace is already contained in the
* internal label panel.
*/
private boolean containsTraceLabel(final ITrace2D tracetoAdd) {
boolean result = false;
final Component[] traceLabels = this.m_labelPanel.getComponents();
JLabel label;
final String labelName = tracetoAdd.getLabel();
for (int i = traceLabels.length - 1; i >= 0; i--) {
label = (JLabel) traceLabels[i];
if (labelName.equals(label.getText())) {
result = true;
break;
}
}
return result;
}
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (this.getClass() != obj.getClass()) {
return false;
}
final ChartPanel other = (ChartPanel) obj;
if (this.m_annotationCreator == null) {
if (other.m_annotationCreator != null) {
return false;
}
} else if (!this.m_annotationCreator.equals(other.m_annotationCreator)) {
return false;
}
if (this.m_chart == null) {
if (other.m_chart != null) {
return false;
}
} else if (!this.m_chart.equals(other.m_chart)) {
return false;
}
if (this.m_labelPanel == null) {
if (other.m_labelPanel != null) {
return false;
}
} else if (!this.m_labelPanel.equals(other.m_labelPanel)) {
return false;
}
return true;
}
/**
* Returns the annotationCreator.
* <p>
*
* @return the annotationCreator
*/
public final IAnnotationCreator getAnnotationCreator() {
return this.m_annotationCreator;
}
/**
* Returns the chart.
* <p>
*
* @return the chart
*/
public final Chart2D getChart() {
return this.m_chart;
}
/**
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((this.m_annotationCreator == null) ? 0 : this.m_annotationCreator.hashCode());
result = prime * result + ((this.m_chart == null) ? 0 : this.m_chart.hashCode());
result = prime * result + ((this.m_labelPanel == null) ? 0 : this.m_labelPanel.hashCode());
return result;
}
/**
* Listens for property "background" of the <code>Chart2D</code> instance that
* is contained in this component and sets the background color.
* <p>
*
* @see java.beans.PropertyChangeListener#propertyChange(java.beans.PropertyChangeEvent)
*/
public void propertyChange(final PropertyChangeEvent evt) {
final String prop = evt.getPropertyName();
if (prop.equals("background")) {
final Color color = (Color) evt.getNewValue();
this.setBackground(color);
this.m_labelPanel.setBackground(color);
} else if (prop.equals(Chart2D.PROPERTY_ADD_REMOVE_TRACE)) {
final ITrace2D oldTrace = (ITrace2D) evt.getOldValue();
final ITrace2D newTrace = (ITrace2D) evt.getNewValue();
JLabel label;
if ((oldTrace == null) && (newTrace != null)) {
if (!this.containsTraceLabel(newTrace)) {
label = LayoutFactory.getInstance().createTraceContextMenuLabel(this.m_chart, newTrace,
true);
if (label != null) {
this.m_labelPanel.add(label);
this.invalidate();
this.m_labelPanel.invalidate();
this.validateTree();
this.m_labelPanel.doLayout();
}
}
} else if ((oldTrace != null) && (newTrace == null)) {
// search for label:
final String labelName = oldTrace.getLabel();
if (!StringUtil.isEmpty(labelName)) {
final Component[] labels = (this.m_labelPanel.getComponents());
for (final Component label2 : labels) {
if (((JLabel) label2).getText().equals(labelName)) {
this.disposeTraceLabel((JLabel) label2, oldTrace);
}
}
}
} else {
throw new IllegalArgumentException("Bad property change event for add / remove trace.");
}
} else if (prop.equals(ITrace2D.PROPERTY_LABEL)) {
final ITrace2D trace = (ITrace2D) evt.getSource();
final String oldLabel = (String) evt.getOldValue();
final String newLabel = (String) evt.getNewValue();
JLabel label;
if ((!StringUtil.isEmpty(oldLabel)) && (StringUtil.isEmpty(newLabel))) {
final Component[] labels = (this.m_labelPanel.getComponents());
for (final Component label2 : labels) {
if (((JLabel) label2).getText().equals(oldLabel)) {
((JLabel) label2).setText("<unnamed>");
}
}
} else if ((StringUtil.isEmpty(oldLabel)) && (!StringUtil.isEmpty(newLabel))) {
if (!this.containsTraceLabel(trace)) {
label = LayoutFactory.getInstance()
.createTraceContextMenuLabel(this.m_chart, trace, true);
if (label != null) {
this.m_labelPanel.add(label);
this.invalidate();
this.m_labelPanel.invalidate();
this.validateTree();
this.m_labelPanel.doLayout();
}
}
}
}
}
/**
* Makes the given trace label garbage collectable and removes all menu
* entries of the menu of the popup menu attachted to it as listeners on the
* chart object graph.
* <p>
*
* @see LayoutFactory#createTraceContextMenuLabel(Chart2D, ITrace2D, boolean)
*
* @param owner
* the owner of the label.
*
* @param label
* the label to wipe out.
*/
private void disposeTraceLabel(final JLabel label, final ITrace2D owner) {
/*
* We have to check if we have to remove and dispose the old label. The old
* label is connected with a JLabel (standing for the old trace) that holds
* many references and is referenced to many other instances that have to be
* cleared to make the whole menu for the old label disposable (see
* <code>{@link LayoutFactory#createTraceContextMenuLabel(Chart2D, ITrace2D,
* boolean)}</code>):
*
* Referred as listener on the chart for font. Referred as listener on the
* trace for foreground,name and physicalunits. The JLabel itself has a
* popup menu with many items that are registered on the chart for
* properties like background, foreground.
*/
PropertyChangeListener listenerLabel = (PropertyChangeListener) label;
this.m_labelPanel.remove(label);
// clear the popup menu listeners:
final MouseListener[] mouseListeners = label.getMouseListeners();
for (final MouseListener mouseListener2 : mouseListeners) {
this.removeMouseListener(mouseListener2);
}
/*
* Omitting this should not hinder the label from being garbage-collectable as I assume
* the traces removed will not be stored in the application for later use. But it's clean to do this:
*/
owner.removePropertyChangeListener(ITrace2D.PROPERTY_COLOR, listenerLabel);
owner.removePropertyChangeListener(ITrace2D.PROPERTY_NAME, listenerLabel);
owner.removePropertyChangeListener(ITrace2D.PROPERTY_PHYSICALUNITS, listenerLabel);
this.m_labelPanel.doLayout();
this.doLayout();
}
/**
* Sets the annotationCreator.
* <p>
*
* @param annotationCreator
* the annotationCreator to set
*/
public final void setAnnotationCreator(final IAnnotationCreator annotationCreator) {
this.m_annotationCreator = annotationCreator;
}
}