/*
* Copyright (c) 2009, 2012, 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.
*
* 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.max.ins.gui;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
import javax.swing.table.*;
import com.sun.max.ins.*;
import com.sun.max.ins.InspectionSettings.AbstractSaveSettingsListener;
import com.sun.max.ins.InspectionSettings.SaveSettingsEvent;
import com.sun.max.ins.InspectionSettings.SaveSettingsListener;
import com.sun.max.ins.util.*;
import com.sun.max.ins.view.*;
import com.sun.max.ins.view.InspectionViews.ViewKind;
import com.sun.max.program.*;
import com.sun.max.program.option.*;
import com.sun.max.tele.*;
import com.sun.max.unsafe.*;
import com.sun.max.vm.heap.*;
/**
* The main GUI window for an inspection of a VM, with related GUI services.
* Contains multiple instances of {@link AbstractView} in a {@link JDesktopPane}.
*/
public final class InspectorMainFrame extends JFrame implements InspectorGUI, Prober {
private static final int TRACE_VALUE = 1;
private static final int MOUSE_TRACE_VALUE = 3;
private static final String FRAME_SETTINGS_NAME = "windowGeometry";
private static final String FRAME_X_KEY = "x";
private static final String FRAME_Y_KEY = "y";
private static final String FRAME_HEIGHT_KEY = "height";
private static final String FRAME_WIDTH_KEY = "width";
/**
* A mouse location to cache when no other location is available.
*/
private static final Point DEFAULT_LOCATION = new Point(100, 100);
private static final DataFlavor[] supportedDropDataFlavors =
new DataFlavor[] {InspectorTransferable.ADDRESS_FLAVOR,
InspectorTransferable.MEMORY_REGION_FLAVOR,
InspectorTransferable.TELE_OBJECT_FLAVOR};
/**
* Records the last position of the mouse when it was over a component.
* The position is recorded in the coordinates of the desktop pane,
* which is the coordinate system we use to place Inspector windows.
*/
private final class MouseLocationListener implements AWTEventListener {
public void eventDispatched(AWTEvent awtEvent) {
final Component source = (Component) awtEvent.getSource();
if (source != null) {
// We should only be getting mouse events; others are masked out.
final MouseEvent mouseEvent = (MouseEvent) awtEvent;
final Point mouseLocation = mouseEvent.getLocationOnScreen();
SwingUtilities.convertPointFromScreen(mouseLocation, scrollPane);
recordMostRecentMouseLocation(mouseLocation);
}
}
}
/**
* Support for the desktop pane to act as a Drag and Drop <em>target</em>.
* Only copy operations are supported.
*
*/
private final class MainFrameTransferHandler extends TransferHandler {
/**
* Returns true iff there is at least one element that is contained in both arrays.
*/
private boolean containsAny(DataFlavor[] array1, DataFlavor[] array2) {
for (DataFlavor element1 : array1) {
for (DataFlavor element2 : array2) {
if (element1 == element2) {
return true;
}
}
}
return false;
}
@Override
public boolean canImport(TransferHandler.TransferSupport support) {
// Only support drops
if (!support.isDrop()) {
return false;
}
// Only support copies:
if ((COPY & support.getSourceDropActions()) == 0) {
return false;
}
// Only support the enumerated data flavors.
if (!containsAny(support.getDataFlavors(), supportedDropDataFlavors)) {
return false;
}
// Only support drop over the background
// Location arrives in coordinates of the content pane of the desktop
TransferHandler.DropLocation dropLocation = support.getDropLocation();
Component component = desktopPane.getComponentAt(dropLocation.getDropPoint());
if (component != null && (component instanceof InspectorInternalFrame)) {
return false;
}
support.setShowDropLocation(true);
support.setDropAction(COPY);
return true;
}
@Override
public boolean importData(TransferHandler.TransferSupport support) {
if (!canImport(support)) {
return false;
}
//Remember where the mouse was when dropped; will guide creation of new window if needed.
final Point dropPoint = support.getDropLocation().getDropPoint();
final Point eventLocationOnScreen = support.getComponent().getLocationOnScreen();
eventLocationOnScreen.translate(dropPoint.x, dropPoint.y);
recordMostRecentMouseLocation(eventLocationOnScreen);
final Transferable transferable = support.getTransferable();
try {
if (support.isDataFlavorSupported(InspectorTransferable.ADDRESS_FLAVOR)) {
final Address address = (Address) transferable.getTransferData(InspectorTransferable.ADDRESS_FLAVOR);
Trace.line(TRACE_VALUE, tracePrefix + "address dropped on desktop");
InspectorMainFrame.this.inspection.views().memory().makeView(address).highlight();
return true;
}
if (support.isDataFlavorSupported(InspectorTransferable.MEMORY_REGION_FLAVOR)) {
final MaxMemoryRegion memoryRegion = (MaxMemoryRegion) transferable.getTransferData(InspectorTransferable.MEMORY_REGION_FLAVOR);
Trace.line(TRACE_VALUE, tracePrefix + "memory region dropped on desktop");
InspectorMainFrame.this.inspection.views().memory().makeView(memoryRegion, null).highlight();
return true;
}
if (support.isDataFlavorSupported(InspectorTransferable.TELE_OBJECT_FLAVOR)) {
final MaxObject object = (MaxObject) transferable.getTransferData(InspectorTransferable.TELE_OBJECT_FLAVOR);
Trace.line(TRACE_VALUE, tracePrefix + "teleObject dropped on desktop");
InspectorMainFrame.this.inspection.focus().setHeapObject(object);
return true;
}
} catch (UnsupportedFlavorException e) {
e.printStackTrace();
InspectorError.unexpected("Attempt to drop an unsupported data flavor");
} catch (IOException e) {
e.printStackTrace();
InspectorError.unexpected("Unknown drop failure");
}
return false;
}
}
private interface MouseButtonMapper {
int getButton(MouseEvent mouseEvent);
}
private final Inspection inspection;
private final InspectorGeometry geometry;
private final String tracePrefix;
private final InspectorNameDisplay nameDisplay;
private final SaveSettingsListener saveSettingsListener;
private final Cursor busyCursor = new Cursor(Cursor.WAIT_CURSOR);
private final JDesktopPane desktopPane;
/**
* The scroll pane is the "content" pane for this main frame, and
* components should be placed relative to its coordinates.
*/
private final JScrollPane scrollPane;
private final InspectorMainMenuBar menuBar;
private final InspectorPopupMenu desktopMenu;
private final InspectorLabel emptyDataTableCellRenderer;
private final InspectorLabel unavailableDataTableCellRenderer;
/**
* Location in absolute screen coordinates of the most recent mouse location of interest.
*/
private Point mostRecentMouseLocation = DEFAULT_LOCATION;
/**
* Platform-specific way to handle mouse clicks.
*/
private final MouseButtonMapper mouseButtonMapper;
/**
* Creates a new main window frame for the VM inspection session.
*
* @param inspection the inspection's main state: {@link Inspection#actions()} and
* {@link Inspection#settings()} must already be initialized.
* @param inspectorName the name to display on the window.
* @param nameDisplay provides standard naming service
* @param settings settings manager for the session, already initialized
* @param actions available for the session, already initialized
*/
public InspectorMainFrame(Inspection inspection, String inspectorName, InspectorNameDisplay nameDisplay, InspectionSettings settings, InspectionActions actions) {
super(inspectorName);
this.tracePrefix = "[" + getClass().getSimpleName() + "] ";
this.inspection = inspection;
this.geometry = inspection.preference().geometry();
this.nameDisplay = nameDisplay;
this.desktopMenu = new InspectorPopupMenu(inspection.vm().entityName() + " Inspector");
setDefaultLookAndFeelDecorated(true);
Toolkit.getDefaultToolkit().addAWTEventListener(new MouseLocationListener(), AWTEvent.MOUSE_EVENT_MASK);
if (inspection.vm().platform().getOS() == MaxPlatform.OS.DARWIN) {
// For Darwin, make sure alternate mouse buttons get mapped properly
mouseButtonMapper = new MouseButtonMapper() {
public int getButton(MouseEvent mouseEvent) {
if (mouseEvent.getButton() == MouseEvent.BUTTON1) {
if (mouseEvent.isControlDown()) {
if (!mouseEvent.isAltDown()) {
return MouseEvent.BUTTON3;
}
} else if (mouseEvent.isAltDown()) {
return MouseEvent.BUTTON2;
}
}
return mouseEvent.getButton();
}
};
} else {
// For all other platforms, use the standard mouse event mapping.
mouseButtonMapper = new MouseButtonMapper() {
public int getButton(MouseEvent mouseEvent) {
return mouseEvent.getButton();
}
};
}
saveSettingsListener = new AbstractSaveSettingsListener(FRAME_SETTINGS_NAME) {
public void saveSettings(SaveSettingsEvent saveSettingsEvent) {
final Rectangle geometry = getBounds();
saveSettingsEvent.save(FRAME_X_KEY, geometry.x);
saveSettingsEvent.save(FRAME_Y_KEY, geometry.y);
saveSettingsEvent.save(FRAME_WIDTH_KEY, geometry.width);
saveSettingsEvent.save(FRAME_HEIGHT_KEY, geometry.height);
}
};
settings.addSaveSettingsListener(saveSettingsListener);
setMinimumSize(geometry.inspectorMinFrameSize());
try {
if (settings.containsKey(saveSettingsListener, FRAME_X_KEY)) {
// Adjust any negative (off-screen) locations to be on-screen.
final int x = Math.max(settings.get(saveSettingsListener, FRAME_X_KEY, OptionTypes.INT_TYPE, -1), 0);
final int y = Math.max(settings.get(saveSettingsListener, FRAME_Y_KEY, OptionTypes.INT_TYPE, -1), 0);
// Adjust any excessive window size to fit within the screen
final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
final int width = Math.min(settings.get(saveSettingsListener, FRAME_WIDTH_KEY, OptionTypes.INT_TYPE, -1), screenSize.width);
final int height = Math.min(settings.get(saveSettingsListener, FRAME_HEIGHT_KEY, OptionTypes.INT_TYPE, -1), screenSize.height);
setPreferredSize(new Dimension(width, height));
setLocation(x, y);
} else {
setPreferredSize(geometry.inspectorPrefFrameSize());
setLocation(geometry.inspectorDefaultFrameLocation());
}
} catch (Option.Error optionError) {
InspectorWarning.message(inspection, "Inspector Main Frame settings", optionError);
setPreferredSize(geometry.inspectorPrefFrameSize());
setLocation(geometry.inspectorDefaultFrameLocation());
}
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent windowevent) {
InspectorMainFrame.this.inspection.quit();
}
});
desktopPane = new JDesktopPane();
desktopPane.setOpaque(true);
if (inspection.vm().inspectionMode() == MaxInspectionMode.IMAGE) {
desktopPane.setBackground(inspection.preference().style().vmNoProcessBackgroundColor());
} else {
desktopPane.setBackground(inspection.preference().style().vmStoppedBackgroundColor(true));
}
scrollPane = new InspectorScrollPane(inspection, desktopPane);
setContentPane(scrollPane);
menuBar = new InspectorMainMenuBar(actions);
setJMenuBar(menuBar);
final InspectionViews views = inspection.views();
desktopMenu.add(views.activateSingletonViewAction(ViewKind.ALLOCATIONS));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.BOOT_IMAGE));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.BREAKPOINTS));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.CARD_TABLE));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.DEBUG_INFO));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.MARK_BITMAP));
desktopMenu.add(views.memory().viewMenu());
desktopMenu.add(views.memoryBytes().viewMenu());
desktopMenu.add(views.activateSingletonViewAction(ViewKind.METHODS));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.NOTEPAD));
desktopMenu.add(views.objects().viewMenu());
desktopMenu.add(views.activateSingletonViewAction(ViewKind.REGISTERS));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.STACK));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.STACK_FRAME));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.THREADS));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.THREAD_LOCALS));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.WATCHPOINTS));
desktopMenu.add(views.activateSingletonViewAction(ViewKind.VMLOG));
desktopPane.addMouseListener(new InspectorMouseClickAdapter(inspection) {
@Override
public void procedure(final MouseEvent mouseEvent) {
if (InspectorMainFrame.this.inspection.gui().getButton(mouseEvent) == MouseEvent.BUTTON3) {
desktopMenu.show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
}
}
});
emptyDataTableCellRenderer = new EmptyDataTableCellRenderer(inspection);
unavailableDataTableCellRenderer = new UnavailableDataTableCellRenderer(inspection);
pack();
desktopPane.setTransferHandler(new MainFrameTransferHandler());
}
public void addView(InspectorView view) {
final JComponent component = view.getJComponent();
desktopPane.add(component);
component.setVisible(true);
repaint();
}
public void showInspectorBusy(boolean busy) {
if (busy) {
desktopPane.setCursor(busyCursor);
} else {
desktopPane.setCursor(null);
}
}
public void informationMessage(Object message, String title) {
JOptionPane.showMessageDialog(desktopPane, message, title, JOptionPane.INFORMATION_MESSAGE);
}
public void informationMessage(Object message) {
informationMessage(message, inspection.currentActionTitle());
}
public void warningMessage(Object message, String title) {
JOptionPane.showMessageDialog(desktopPane, message, title, JOptionPane.WARNING_MESSAGE);
}
public void warningMessage(Object message) {
warningMessage(message, inspection.currentActionTitle());
}
public void errorMessage(Object message, String title) {
JOptionPane.showMessageDialog(desktopPane, message, title, JOptionPane.ERROR_MESSAGE);
}
public void errorMessage(Object message) {
errorMessage(message, inspection.currentActionTitle());
}
public String inputDialog(String message, String initialValue) {
return JOptionPane.showInputDialog(desktopPane, message, initialValue);
}
public boolean yesNoDialog(String message) {
return JOptionPane.showConfirmDialog(this, message, inspection.currentActionTitle(), JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION;
}
public String questionMessage(String message) {
return JOptionPane.showInputDialog(desktopPane, message, inspection.currentActionTitle(), JOptionPane.QUESTION_MESSAGE);
}
public void postToClipboard(String text) {
final StringSelection selection = new StringSelection(text);
getToolkit().getSystemClipboard().setContents(selection, selection);
}
public int getButton(MouseEvent mouseEvent) {
return mouseButtonMapper.getButton(mouseEvent);
}
public void setLocationRelativeToMouse(InspectorView view, int diagonalOffset) {
setLocationRelativeToMouse(view, diagonalOffset, diagonalOffset);
}
public void moveToMiddle(Component component) {
final int newX = Math.max(0, (scrollPane.getWidth() - component.getWidth()) / 2);
final int newY = Math.max(0, (scrollPane.getHeight() - component.getHeight()) / 2);
component.setLocation(newX, newY);
}
public void moveToMiddle(InspectorView view) {
moveToMiddle(view.getJComponent());
}
public InspectorMainMenuBar getMainMenuBar() {
return menuBar;
}
public InspectorAction moveToMiddleAction(final InspectorView view) {
return new InspectorAction(inspection, "Move to center of frame") {
@Override
protected void procedure() {
moveToMiddle(view);
}
};
}
public void moveToFullyVisible(InspectorView view) {
final JComponent component = view.getJComponent();
if (!isFullyVisible(component)) {
int x = component.getX();
if (x < 0) {
// Off the pane to the left
x = 0;
} else if (x + component.getWidth() > scrollPane.getWidth()) {
// Off the pane to the right
x = Math.max(0, scrollPane.getWidth() - component.getWidth());
}
int y = component.getY();
if (y < 0) {
// Off the top of the pane
y = 0;
} else if (y + component.getHeight() > scrollPane.getHeight()) {
// Off the bottom of the pane
y = Math.max(0, scrollPane.getHeight() - component.getHeight());
}
component.setLocation(x, y);
}
}
public void moveToExposeDefaultMenu(InspectorView view) {
final JComponent component = view.getJComponent();
final int x = component.getX();
final int y = component.getY();
if (x < 0 || y < 0) {
component.setLocation(Math.max(x, 0), Math.max(y, 0));
}
}
public void resizeToFit(InspectorView view) {
final JComponent component = view.getJComponent();
if (!isFullyVisible(component)) {
final int x = component.getX();
final int y = component.getY();
final int width = component.getWidth();
final int height = component.getHeight();
final int newWidth = (Math.min(scrollPane.getWidth(), x + width)) - x;
final int newHeight = (Math.min(scrollPane.getHeight(), y + height)) - y;
component.setSize(newWidth, newHeight);
}
}
public InspectorAction resizeToFitAction(final InspectorView view) {
return new InspectorAction(inspection, "Resize to fit inside frame") {
@Override
protected void procedure() {
resizeToFit(view);
}
};
}
public void resizeToFill(InspectorView view) {
view.getJComponent().setBounds(0, 0, scrollPane.getWidth(), scrollPane.getHeight());
}
public InspectorAction resizeToFillAction(final InspectorView view) {
return new InspectorAction(inspection, "Resize to fill frame") {
@Override
protected void procedure() {
resizeToFill(view);
}
};
}
public void restoreDefaultGeometry(InspectorView view) {
final Rectangle defaultFrameGeometry = view.defaultGeometry();
if (defaultFrameGeometry != null) {
view.setGeometry(defaultFrameGeometry);
} else {
moveToMiddle(view);
}
}
public InspectorAction restoreDefaultGeometryAction(final InspectorView view) {
return new InspectorAction(inspection, "Restore size/location to default") {
@Override
protected void procedure() {
restoreDefaultGeometry(view);
}
};
}
public void setLocationRelativeToMouse(JDialog dialog, int diagonalOffset) {
final Point mouse = mostRecentMouseLocation;
dialog.setLocation(mouse.x + diagonalOffset, mouse.y + diagonalOffset);
}
public Frame frame() {
return this;
}
public InspectorLabel getEmptyDataTableCellRenderer() {
return emptyDataTableCellRenderer;
}
public InspectorLabel getUnavailableDataTableCellRenderer() {
return unavailableDataTableCellRenderer;
}
/**
* Set frame location to a point displaced by specified amount from the most recently known mouse position.
*/
private void setLocationRelativeToMouse(InspectorView view, int xOffset, int yOffset) {
final JComponent component = view.getJComponent();
final int width = component.getWidth();
final int height = component.getHeight();
final Rectangle paneBounds = scrollPane.getBounds();
final Point mouse = mostRecentMouseLocation;
// Try a location down and to the right of the mouse
Rectangle newBounds = new Rectangle(mouse.x + xOffset, mouse.y + yOffset, width, height);
if (paneBounds.contains(newBounds)) {
component.setBounds(newBounds);
} else {
// Doesn't fit fully; try a location up and to the left
newBounds = new Rectangle((mouse.x - xOffset) - width, (mouse.y - yOffset) - height, width, height);
if (paneBounds.contains(newBounds)) {
component.setBounds(newBounds);
} else {
// Doesn't fit fully there either; give up and go for the middle.
moveToMiddle(component);
}
}
}
private boolean isFullyVisible(Component component) {
return scrollPane.getBounds().contains(component.getBounds());
}
/**
* Records the most recent mouse event of interest.
*
* @param point mouse location in absolute screen coordinates.
*/
private void recordMostRecentMouseLocation(Point point) {
Trace.line(MOUSE_TRACE_VALUE, InspectorMainFrame.this.tracePrefix + "Mouse@(" + point.x + "," + point.y + ")");
mostRecentMouseLocation = point;
}
public void redisplay() {
refresh(true);
}
private MaxVMState lastRefreshedState = null;
public void refresh(boolean force) {
final MaxVMState maxVMState = inspection.vm().state();
final boolean invalidReferenceDetected = !inspection.vm().invalidReferencesLogger().isEmpty();
if (maxVMState.newerThan(lastRefreshedState)) {
lastRefreshedState = maxVMState;
setTitle(inspection.currentInspectionTitle());
final InspectorStyle style = inspection.preference().style();
switch (maxVMState.processState()) {
case STOPPED:
if (maxVMState.heapPhase() != HeapPhase.MUTATING) {
desktopPane.setBackground(style.vmStoppedInGCBackgroundColor(invalidReferenceDetected));
menuBar.setStateColor(style.vmStoppedInGCBackgroundColor(invalidReferenceDetected));
} else if (maxVMState.isInEviction()) {
desktopPane.setBackground(style.vmStoppedInEvictionBackgroundColor());
menuBar.setStateColor(style.vmStoppedInEvictionBackgroundColor());
} else {
desktopPane.setBackground(style.vmStoppedBackgroundColor(invalidReferenceDetected));
menuBar.setStateColor(style.vmStoppedBackgroundColor(invalidReferenceDetected));
}
break;
case RUNNING:
desktopPane.setBackground(style.vmRunningBackgroundColor());
menuBar.setStateColor(style.vmRunningBackgroundColor());
break;
case UNKNOWN:
desktopPane.setBackground(style.vmNoProcessBackgroundColor());
break;
case TERMINATED:
desktopPane.setBackground(style.vmTerminatedBackgroundColor());
menuBar.setStateColor(style.vmTerminatedBackgroundColor());
break;
default:
InspectorError.unknownCase(maxVMState.processState().toString());
}
}
repaint();
}
private final class EmptyDataTableCellRenderer extends InspectorLabel implements TableCellRenderer, TextSearchable, Prober {
EmptyDataTableCellRenderer(Inspection inspection) {
super(inspection);
setText("");
setToolTipText("");
}
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
return this;
}
@Override
public String getSearchableText() {
return null;
}
public void redisplay() {
}
public void refresh(boolean force) {
}
}
private final class UnavailableDataTableCellRenderer extends InspectorLabel implements TableCellRenderer, TextSearchable, Prober {
UnavailableDataTableCellRenderer(Inspection inspection) {
super(inspection);
setText(inspection.nameDisplay().unavailableDataShortText());
setToolTipText(inspection.nameDisplay().unavailableDataLongText());
}
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
return this;
}
@Override
public String getSearchableText() {
return null;
}
public void redisplay() {
}
public void refresh(boolean force) {
}
}
}