Mercurial > hg > release > thermostat-1.6
view client/swing/src/main/java/com/redhat/thermostat/client/swing/components/experimental/TreeMapComponent.java @ 1999:8a65bb11a6f9
Add profiler treemap
PR3070
Reviewed-by: jkang
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-June/020062.html
Original-thread: http://icedtea.classpath.org/pipermail/thermostat/2015-December/017148.html
author | James Aziz <jaziz@redhat.com> |
---|---|
date | Thu, 30 Jun 2016 11:36:54 -0400 |
parents | c7d7084f1ed5 |
children | a92d602216ad |
line wrap: on
line source
/* * Copyright 2012-2016 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.client.swing.components.experimental; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.font.FontRenderContext; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Stack; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import javax.swing.border.EtchedBorder; import javax.swing.border.LineBorder; import com.redhat.thermostat.client.swing.ThermostatSwingCursors; /** * This class allows to represent a hierarchical data structure as a TreeMap. * It extends {@link JComponent} so it can be used like usual Swing objects. * */ public class TreeMapComponent extends JComponent { private static final long serialVersionUID = 1L; /** * TreeMap's graphic root. */ Comp mainComp; /** * Label Object to clone for faster initialization. */ private Label cachedLabel; /** * The tree to render as TreeMap. */ private TreeMapNode tree; /** * Horizontal and vertical padding for nested component. */ private final int X_PADDING = TreeProcessor.X_PADDING; private final int Y_PADDING = TreeProcessor.Y_PADDING; /** * Min size for rectangles' sides. rectangles having one or both sides less * than MIN_SIDE pixels will be not drawn. */ private final int MIN_SIDE = 1; /** * Default value for a TreeMap component. */ private static final String TITLE = ""; /* * TreeMap border styles */ public static final int BORDER_SIMPLE = 0; public static final int BORDER_FLAT = 1; public static final int BORDER_ETCHED_LOWERED = 2; public static final int BORDER_ETCHED_RAISED = 3; private int borderStyle = BORDER_ETCHED_LOWERED; /** * Font and size for this component's label. */ private int FONT_SIZE = 8; private Font FONT = (Font) UIManager.get("thermostat-default-font"); /** * Variable in which store last resize dimension. */ private Dimension lastDim; /** * Variable in which store last resize event call time. */ private long lastCall = 0; /** * Wait time in millisec to resize the TreeMap. */ private final int MIN_DRAGGING_TIME = 60; /** * Stack containing the zoom calls on the TreeMap. */ private Stack<TreeMapNode> zoomStack; /** * This object stores the last clicked rectangle in the TreeMap, in order to * repaint it when another rectangle will be selected. */ private Comp lastClicked; /** * List of objects observing this. */ private List<TreeMapObserver> observers; private ToolTipRenderer tooltipRenderer = new SimpleRenderer(); public TreeMapComponent() { this(null); } /** * Constructor that sets up a TreeMapComponent using the specified {@TreeMapNode} tree. */ public TreeMapComponent(TreeMapNode tree) { super(); this.tree = tree; lastDim = getSize(); this.zoomStack = new Stack<>(); this.observers = new ArrayList<>(); if (tree != null) { this.zoomStack.push(this.tree); processAndDrawTreeMap(this.tree); } addResizeListener(this); addKeyBindings(this); } /** * This method returns the root of the tree showed ad TreeMap. * @return the TreeMap's root node. */ public TreeMapNode getTreeMapRoot() { return this.tree; } public void setModel(TreeMapNode tree) { this.tree = Objects.requireNonNull(tree); this.zoomStack.clear(); this.zoomStack.push(this.tree); resetTreeMapNodeWeights(this.tree); processAndDrawTreeMap(this.tree); } private void resetTreeMapNodeWeights(TreeMapNode node) { Objects.requireNonNull(node); node.setWeight(node.getRealWeight()); for(TreeMapNode child : node.getChildren()) { resetTreeMapNodeWeights(child); } } public void setToolTipRenderer(ToolTipRenderer renderer) { this.tooltipRenderer = renderer; } /** * This method is responsible for the TreeMap drawing process. * @param tree the tree to represent as TreeMap. */ private void drawTreeMap(TreeMapNode tree) { // draw root drawMainComp(tree); setBorderStyle(borderStyle); // draw subtrees nested in children for (TreeMapNode child : tree.getChildren()) { drawSubTree(child, mainComp); } // setup this component prepareGUI(); } /** * This method prepares the layout for this component. */ private void prepareGUI() { setLayout(new BorderLayout()); setBounds(mainComp.getBounds()); setBorder(null); add(mainComp, BorderLayout.CENTER); revalidate(); repaint(); } /** * This method prepares the main component which is the parent object where * sub components will be placed. * @param tree the tree's root used to prepare the main component. */ private void drawMainComp(TreeMapNode tree) { mainComp = new Comp(); mainComp.setLayout(null); mainComp.setBounds(tree.getRectangle().getBounds()); mainComp.setNode(tree); cachedLabel = new Label(TITLE + tree.getLabel()); addLabelIfPossible(TITLE + tree.getLabel(), mainComp); } /** * Create a TreeMapComp from the given node. The component is not * instantiated as a new component but is cloned from an existing one, in * order to improve performance. * * @param node the node to represent as a component. * @return the component representing the given node. */ private Comp renderizeNode(TreeMapNode node) { // if the rectangle's node is too small to be viewed, don't draw it. if (node.getRectangle().getWidth() <= MIN_SIDE || node.getRectangle().getHeight() <= MIN_SIDE) { return null; } Comp comp = new Comp(mainComp); comp.setBounds(node.getRectangle().getBounds()); return comp; } /** * This method checks if the given container has enough space to instantiate * a Label in it. If yes, a Label is cloned from an existing one, in order * to improve performance. If not, it exits. * * @param s the label text. * @param cont the parent container which will contain the new label. * @return the cloned label. */ private Label addLabelIfPossible(String s, Container cont) { if (s == null || s.equals("")) { return null; } int componentW = cont.getSize().width; int componentH = cont.getSize().height; // get the rectangle associated to the area needed for the label's text Rectangle fontArea = FONT.getStringBounds(s, new FontRenderContext(FONT.getTransform(), false, false)).getBounds(); // if the container is greater than the label, add it to the container if (componentW > fontArea.width && componentH > fontArea.height) { Label label = new Label(cachedLabel); label.setBounds(5, 1, cont.getWidth(), fontArea.height); label.setText(s); cont.add(label); return label; } return null; } /** * Draw the whole {@param tree}'s subtree inside the given component. * @param tree the tree to draw * @param parent the component in which build the tree. */ private void drawSubTree(TreeMapNode tree, JComponent parent) { Comp comp = addCompIfPossible(tree, parent); // if space was enough to draw a component, try to draw its children if (comp != null) { comp.setNode(tree); for (TreeMapNode child : tree.getChildren()) { drawSubTree(child, comp); } } } /** * Create and add to the {@link Container} given in input a * {@link java.awt.event.ComponentListener} listener. * @param container the container in to assign the listener. */ private void addResizeListener(final Container container) { ComponentAdapter adapter = new ComponentAdapter() { public void componentResized(ComponentEvent e) { // if enough time is passed from the last call, redraw the TreeMap if (canResize(MIN_DRAGGING_TIME)) { Dimension newDim = container.getSize(); if (isChangedSize(newDim)) { processAndDrawTreeMap(Objects.requireNonNull(tree)); } } } }; container.addComponentListener(adapter); } private void addKeyBindings(final JComponent component) { final int NO_MODIFIERS = 0; final String ZOOM_OUT = "zoomOut"; final String ZOOM_FULL = "zoomFull"; final String ZOOM_IN = "zoomIn"; InputMap inputMap = component.getInputMap(WHEN_FOCUSED); ActionMap actionMap = component.getActionMap(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, NO_MODIFIERS), ZOOM_OUT); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, NO_MODIFIERS), ZOOM_FULL); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, NO_MODIFIERS), ZOOM_OUT); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, NO_MODIFIERS), ZOOM_IN); actionMap.put(ZOOM_OUT, new AbstractAction() { @Override public void actionPerformed(ActionEvent actionEvent) { zoomOut(); lastClicked = null; } }); actionMap.put(ZOOM_FULL, new AbstractAction() { @Override public void actionPerformed(ActionEvent actionEvent) { zoomFull(); lastClicked = null; } }); actionMap.put(ZOOM_IN, new AbstractAction() { @Override public void actionPerformed(ActionEvent actionEvent) { if (lastClicked != null) { zoomIn(lastClicked.getNode()); } } }); } /** * This method checks if the given container has enough space to instantiate * a TreeMapComp object in it. If yes, a Label is cloned from an existing * one, in order to improve performance. If not, it exits. * * @param node the node to draw and add to the given container. * @param cont the parent container which will contain the new component. * @return true if the component was created and added, else false. */ private Comp addCompIfPossible(TreeMapNode node, Container cont) { Rectangle2D rect = node.getRectangle(); // if the ndoe's rectangle is smaller than the container, it is added if (cont.getWidth() > rect.getWidth() + X_PADDING && cont.getHeight() > rect.getHeight() + Y_PADDING) { Comp toReturn = renderizeNode(node); if (toReturn == null) { return null; } addLabelIfPossible(TITLE + node.getLabel(), toReturn); // leaves some space from the parent's origin location Point loc = toReturn.getLocation(); loc.x += X_PADDING; loc.y += Y_PADDING; toReturn.setLocation(loc); cont.add(toReturn); return toReturn; } return null; } /** * Package-private for testing only. */ void processAndDrawTreeMap(TreeMapNode root) { tree = Objects.requireNonNull(root); Rectangle2D.Double newArea = tree.getRectangle(); // give to the root node the size of this object so it can be recalculated newArea.width = getSize().width; newArea.height = getSize().height; // recalculate the tree TreeProcessor.processTreeMap(tree, newArea); removeAll(); drawTreeMap(tree); } boolean isZoomInEnabled(TreeMapNode node) { return !(node == null || node.equals(Objects.requireNonNull(this.tree)) || node.isLeaf()); } public void zoomIn(TreeMapNode node) { if (isZoomInEnabled(node)) { fillZoomStack(node.getAncestors()); processAndDrawTreeMap(node); notifyZoomInToObservers(zoomStack.peek()); } } private void fillZoomStack(LinkedList<TreeMapNode> ancestors) { zoomStack.clear(); while (!ancestors.isEmpty()) { zoomStack.push(ancestors.removeLast()); } } public void zoomOut() { // if the actual root element is not the tree's original root if (zoomStack.size() > 1) { zoomStack.pop(); processAndDrawTreeMap(zoomStack.peek()); notifyZoomOutToObservers(); } } /** * Zoom out the view directly to the original root. */ public void zoomFull() { if (zoomStack.size() > 1) { clearZoomCallsStack(); processAndDrawTreeMap(zoomStack.peek()); notifyZoomFullToObservers(); } } /** * Add the object in input to the list of registered objects to this TreeMap. * @param observer the Notifiable object to register to this object. */ public void register(TreeMapObserver observer) { this.observers.add(observer); } /** * Remove the object in input from the list of registered objects to this TreeMap. * @param observer the Notifiable object to unregister from this object. */ public void unregister(TreeMapObserver observer) { this.observers.remove(observer); } /** * Notify observers that an object in the TreeMap has been selected. */ private void notifySelectionToObservers(TreeMapNode node) { for (TreeMapObserver observer : observers) { observer.notifySelection(node); } } /** * Notify observers that TreeMap has been zoomed. */ private void notifyZoomInToObservers(TreeMapNode node) { for (TreeMapObserver observer : observers) { observer.notifyZoomIn(node); } } /** * Notify observers that TreeMap has been zoomed. */ private void notifyZoomOutToObservers() { for (TreeMapObserver observer : observers) { observer.notifyZoomOut(); } } /** * Notify observers that TreeMap has been zoomed. */ private void notifyZoomFullToObservers() { for (TreeMapObserver observer : observers) { observer.notifyZoomFull(); } } /** * Returns the list of zoom operation calls. * @return the stack that holds the zoom calls. */ public Stack<TreeMapNode> getZoomCallsStack() { return zoomStack; } /** * Clear the zoom calls of this object leaving the stack with just the root. */ public void clearZoomCallsStack() { while (zoomStack.size() > 1) { zoomStack.pop(); } } /** * check if last resize operation was called too closer to this * one. If so, ignore it: the container is being dragged. * * @return true if this method is invoked at distance of * MIN_DRAGGING_TIME millisec, else false. */ private boolean canResize(int millisec) { long time = System.currentTimeMillis(); if (time - lastCall >= millisec) { lastCall = time; return true; } return false; } /** * Check if the dimension given in input differs from the last one stored * by 2. * @param newDim the new dimension to check. * @return true if the dimensions are different, else false. */ private boolean isChangedSize(Dimension newDim) { int minResizeDim = 2; int deltaX = Math.abs(newDim.width - lastDim.width); int deltaY = Math.abs(newDim.height - lastDim.height); if (deltaX > minResizeDim || deltaY > minResizeDim) { lastDim = newDim; return true; } return false; } /** * Switch the component's border style to the one given in input. * * @param newBorderStyle the border style to use */ public void setBorderStyle(int newBorderStyle) { Border border; switch (newBorderStyle) { case BORDER_SIMPLE : { border = new EmptyBorder(0, 0, 0, 0); break; } case BORDER_FLAT : { border = new LineBorder(Color.black, 1); break; } case BORDER_ETCHED_LOWERED : { border = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED, Color.white, Color.darkGray); break; } case BORDER_ETCHED_RAISED : { border = BorderFactory.createEtchedBorder(EtchedBorder.RAISED, Color.white, Color.darkGray); break; } default : { throw new IllegalArgumentException("Unknown border style: " + newBorderStyle); } } this.borderStyle = newBorderStyle; applyBorderToSubtree(mainComp, border); } /** * Traverse recursively the tree from the given component applying to it * the default border. * @param comp the subtree's root from which apply the border style. */ private void applyBorderToSubtree(Comp comp, Border border) { comp.setBorder(border); Component[] children = comp.getComponents(); for (int i = 0; i < children.length; i++) { if (children[i] instanceof Comp) { applyBorderToSubtree((Comp) children[i], border); } } } /** * Return the last clicked component inside the TreeMap. * @return the last clicked {@Comp} object. */ public Comp getClickedComponent() { return lastClicked; } class Label extends JLabel { private static final long serialVersionUID = 1L; public Label(String s) { super(s); setFont(FONT); setBounds(0, 0, getPreferredSize().width, FONT_SIZE); } protected Label(Label other) { this(other.getText()); this.setFont(other.getFont()); this.setBackground(other.getBackground()); this.setBounds(other.getBounds()); this.setBorder(other.getBorder()); } } /** * This class provides some action listeners that allow to select it, performing * zoom operations for the treemap. */ class Comp extends JComponent { private static final long serialVersionUID = 1L; /** * The node represented by this component. */ private TreeMapNode node; /** * The background color. It depends by the node's depth. */ private Color color; /** * Reference to this. */ private Comp thisComponent; public Comp() { super(); thisComponent = this; addClickListener(this); addMouseListener(this); } public Comp(Comp other) { this(); this.setBounds(other.getBounds()); this.setBorder(other.getBorder()); this.setLayout(other.getLayout()); this.setOpaque(true); } public void setNode(TreeMapNode node) { this.node = node; this.color = node.getColor(); this.setToolTipText(Objects.requireNonNull(TreeMapComponent.this.tooltipRenderer).render(node)); } public TreeMapNode getNode() { return this.node; } public Color getColor() { return this.color; } public void setColor(Color c) { this.color = c; } @Override public void paintComponent(Graphics g) { super.paintComponent(g); if (this.color != null) { g.setColor(color); g.fillRect(0, 0, getWidth(), getHeight()); } } /** * Add a mouse listener to this component. It allows to select it and * zoom it. * @param component the component which will have the mouse listener. */ private void addClickListener(final JComponent component) { MouseListener click = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { TreeMapComponent.this.requestFocusInWindow(); // one left click select the rectangle if (SwingUtilities.isLeftMouseButton(e)) { selectComp(); } // double left click to zoom in (on non-leaf nodes only) if (e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton(e)) { zoomIn(getNode()); } // one right click to zoom out if (SwingUtilities.isRightMouseButton(e)) { zoomOut(); } // one middle click to reset zoom if (SwingUtilities.isMiddleMouseButton(e)) { zoomFull(); } } }; component.addMouseListener(click); } /** * Add a mouse motion listener to this component. This allows for the mouse cursor to be changed into a * magnifying glass icon when the cursor enters a zoomable component, and back to a default cursor when it * exits a zoomable component. * @param component the component which will have the mouse motion listener. */ private void addMouseListener(final JComponent component) { MouseListener listener = new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { if (getNode().isLeaf()) { setDefaultCursor(); } else { setZoomableCursor(); } } @Override public void mouseExited(MouseEvent e) { if (!getNode().isLeaf()) { setDefaultCursor(); } else { setZoomableCursor(); } } private void setZoomableCursor() { component.setCursor(ThermostatSwingCursors.getZoomIconCursor()); } private void setDefaultCursor() { component.setCursor(Cursor.getDefaultCursor()); } }; component.addMouseListener(listener); } /** * This method gives a darker color to this component and restore the * original color to the last selected component. */ private void selectComp() { if (lastClicked != null) { if (!lastClicked.getNode().isLeaf()) { lastClicked.setColor(lastClicked.getColor().brighter()); } lastClicked.repaint(); } lastClicked = thisComponent; if (!getNode().isLeaf()) { setColor(getColor().darker()); } repaint(); notifySelectionToObservers(node); } } public static interface ToolTipRenderer { public String render(TreeMapNode node); } public static class SimpleRenderer implements ToolTipRenderer { @Override public String render(TreeMapNode node) { return node.getLabel(); } } }