/*
* Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.tools.visualvm.modules.threadinspect;
import com.sun.tools.visualvm.application.Application;
import com.sun.tools.visualvm.core.datasupport.DataRemovedListener;
import com.sun.tools.visualvm.core.datasupport.Stateful;
import com.sun.tools.visualvm.core.ui.components.ScrollableContainer;
import com.sun.tools.visualvm.uisupport.HTMLTextArea;
import com.sun.tools.visualvm.uisupport.UISupport;
import com.sun.tools.visualvm.uisupport.VerticalLayout;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.management.ThreadInfo;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicSplitPaneDivider;
import javax.swing.plaf.basic.BasicSplitPaneUI;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
/**
*
* @author Jiri Sedlacek
*/
final class ThreadsInspector extends JPanel implements DataRemovedListener<Application>,
PropertyChangeListener {
private static final Color BACKGROUND;
private static final Color ITEM_HIGHLIGHT;
private static final Color SPLITTER_HIGHLIGHT;
private static final int SPACING = getPresenterSpacing();
private static final String EMPTY = "E"; // NOI18N
private static final String DATA = "D"; // NOI18N
static {
BACKGROUND = UISupport.getDefaultBackground();
int darkerR = BACKGROUND.getRed() - 11;
if (darkerR < 0) darkerR += 26;
int darkerG = BACKGROUND.getGreen() - 11;
if (darkerG < 0) darkerG += 26;
int darkerB = BACKGROUND.getBlue() - 11;
if (darkerB < 0) darkerB += 26;
ITEM_HIGHLIGHT = new Color(darkerR, darkerG, darkerB);
darkerR = BACKGROUND.getRed() - 20;
if (darkerR < 0) darkerR += 40;
darkerG = BACKGROUND.getGreen() - 20;
if (darkerG < 0) darkerG += 40;
darkerB = BACKGROUND.getBlue() - 20;
if (darkerB < 0) darkerB += 40;
SPLITTER_HIGHLIGHT = new Color(darkerR, darkerG, darkerB);
}
private final Application application;
private Engine threadEngine;
private Set<Long> selectedThreads;
private JButton refreshButton;
private JPanel threadsContainer;
private JPanel threadsContainerContainer;
private CardLayout detailsLayout;
private JPanel detailsContainer;
private HTMLTextArea threadsDetails;
private Long focusedThreadId = Long.MIN_VALUE;
private boolean internalDetailsChange;
public ThreadsInspector(Application application) {
this.application = application;
initUI();
showProgress();
initThreads();
}
public void dataRemoved(Application application) {
disableUI();
}
public void propertyChange(PropertyChangeEvent evt) {
disableUI();
}
private void initUI() {
setOpaque(false);
setLayout(new BorderLayout(0, 0));
}
private void initThreads() {
RequestProcessor.getDefault().post(new Runnable() {
public void run() {
if (application.getState() != Stateful.STATE_AVAILABLE) {
showError("Application finished");
} else {
threadEngine = Engine.getEngine(application);
if (threadEngine == null) {
showError("Cannot access threads using JMX.");
} else {
application.notifyWhenRemoved(ThreadsInspector.this);
application.addPropertyChangeListener(Stateful.PROPERTY_STATE,
WeakListeners.propertyChange(ThreadsInspector.this,
application));
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createUI();
refreshData();
}
});
}
}
}
});
}
private void refreshData() {
if (!refreshButton.isEnabled()) return;
if (application.getState() != Stateful.STATE_AVAILABLE) {
disableUI();
return;
}
RequestProcessor.getDefault().post(new Runnable() {
public void run() {
final List<ThreadInfo> tinfs = threadEngine.getThreadInfos();
if (tinfs == null) {
disableUI();
return;
}
if (selectedThreads == null) selectedThreads = new HashSet();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
threadsContainer.removeAll();
List<Long> toDisplay = new ArrayList();
Set<Long> selectedZombies = new HashSet(selectedThreads);
for (ThreadInfo tinfo : tinfs) {
String name = tinfo.getThreadName();
final long id = tinfo.getThreadId();
selectedZombies.remove(id);
final JCheckBox cb = new JCheckBox(name, selectedThreads.
contains(id)) {
protected void fireActionPerformed(ActionEvent e) {
focusedThreadId = id;
if (!selectedThreads.remove(id))
selectedThreads.add(id);
refreshData();
}
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
size.height += SPACING;
return size;
}
};
cb.setOpaque(false);
JPanel cbp = new JPanel(null) {
public Dimension getPreferredSize() {
Dimension size = cb.getPreferredSize();
size.width += 8;
return size;
}
public void doLayout() {
cb.setBounds(4, 0, getWidth() - 8, getHeight());
}
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
for (Component c : getComponents())
c.setEnabled(enabled);
}
};
cbp.setOpaque(true);
cbp.setBackground(threadsContainer.getComponentCount() %
2 == 0 ? BACKGROUND : ITEM_HIGHLIGHT);
cbp.add(cb, BorderLayout.CENTER);
threadsContainer.add(cbp);
if (focusedThreadId == id) {
cb.requestFocusInWindow();
focusedThreadId = Long.MIN_VALUE;
}
if (cb.isSelected()) toDisplay.add(id);
}
selectedThreads.removeAll(selectedZombies);
// Workaround for JDK7 bug, JScrollPane doesn't layout
// correctly when in a not-selected JTabPane and updated
// lazily. Overriding isValidateRoot() on JScrollPane
// to return false also works around this problem.
threadsContainerContainer.invalidate();
threadsContainerContainer.validate();
if (!toDisplay.isEmpty()) displayStackTraces(toDisplay);
else showDetails(""); // NOI18N
}
});
}
});
}
private void displayStackTraces(final List<Long> toDisplay) {
RequestProcessor.getDefault().post(new Runnable() {
public void run() {
final String text = threadEngine.getStackTraces(toDisplay);
if (text != null) SwingUtilities.invokeLater(new Runnable() {
public void run() { showDetails(text); }
});
}
});
}
private void disableUI() {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
refreshButton.setEnabled(false);
threadsContainer.setEnabled(false);
Component focused = KeyboardFocusManager.
getCurrentKeyboardFocusManager().getFocusOwner();
if (focused != null && focused.getParent() == threadsContainer)
threadsDetails.requestFocusInWindow();
}
});
}
private void showProgress() {
JLabel waitLabel = new JLabel("Resolving threads...", SwingConstants.CENTER);
waitLabel.setEnabled(false);
add(waitLabel, BorderLayout.CENTER);
}
private void showError(final String error) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
removeAll();
add(new HTMLTextArea("<b>Unable to inspect threads.</b><br>" +
error), BorderLayout.CENTER);
revalidate();
repaint();
}
});
}
private void showDetails(String text) {
internalDetailsChange = true;
threadsDetails.setText(text);
if (text.isEmpty()) detailsLayout.show(detailsContainer, EMPTY);
else detailsLayout.show(detailsContainer, DATA);
}
private void createUI() {
JLabel hintLabel = new JLabel("<Select thread(s) to display stack traces>");
hintLabel.setHorizontalAlignment(JLabel.CENTER);
hintLabel.setOpaque(false);
hintLabel.setEnabled(false);
detailsLayout = new CardLayout();
detailsContainer = new JPanel(detailsLayout);
detailsContainer.setOpaque(false);
detailsContainer.add(hintLabel, EMPTY);
refreshButton = new JButton("Refresh") {
protected void fireActionPerformed(ActionEvent e) { refreshData(); }
};
threadsContainer = new JPanel(new VerticalLayout(false)) {
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
for (Component c : getComponents()) c.setEnabled(enabled);
}
};
threadsContainer.setOpaque(false);
threadsDetails = new HTMLTextArea() {
private Rectangle lastR;
public void scrollRectToVisible(Rectangle r) {
if (internalDetailsChange) internalDetailsChange = false;
else if (!r.equals(lastR)) super.scrollRectToVisible(r);
lastR = r;
}
};
threadsDetails.setForeground(new Color(0xcc, 0x33, 0));
detailsContainer.add(threadsDetails, DATA);
showDetails(""); // NOI18N
threadsContainerContainer = new JPanel(new BorderLayout(0, 5));
threadsContainerContainer.setOpaque(false);
threadsContainerContainer.add(new ScrollableContainer(threadsContainer), BorderLayout.CENTER);
threadsContainerContainer.add(refreshButton, BorderLayout.SOUTH);
final CustomizedSplitPaneUI detailsVerticalSplitterUI = new CustomizedSplitPaneUI();
JExtendedSplitPane splitPane = new JExtendedSplitPane(JExtendedSplitPane.
HORIZONTAL_SPLIT, threadsContainerContainer, new ScrollableContainer(detailsContainer)) {
public void updateUI() {
if (getUI() != detailsVerticalSplitterUI)
setUI(detailsVerticalSplitterUI);
setBorder(null);
setOpaque(false);
setDividerSize(6);
setContinuousLayout(true);
final BasicSplitPaneDivider divider = ((BasicSplitPaneUI)getUI()).
getDivider();
divider.setBackground(BACKGROUND);
divider.setBorder(null);
divider.addMouseListener(new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
divider.setBackground(SPLITTER_HIGHLIGHT);
divider.repaint();
}
public void mouseExited(MouseEvent e) {
divider.setBackground(BACKGROUND);
divider.repaint();
}
});
}
};
splitPane.setDividerLocation(250);
removeAll();
add(splitPane, BorderLayout.CENTER);
revalidate();
repaint();
}
private static int getPresenterSpacing() {
if (UISupport.isNimbusLookAndFeel()) return 6;
else if (UISupport.isGTKLookAndFeel()) return 4;
else return 2;
}
private static class CustomizedSplitPaneUI extends BasicSplitPaneUI {
public BasicSplitPaneDivider createDefaultDivider() {
return new BasicSplitPaneDivider(this) {
public void paint(Graphics g) {
Dimension size = getSize();
g.setColor(getBackground());
g.fillRect(0, 0, size.width, size.height);
}
};
}
}
}