/* * Copyright (c) 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.apple.laf; import java.awt.*; import java.awt.event.*; import java.beans.*; import javax.swing.*; import javax.swing.event.MouseInputAdapter; import javax.swing.plaf.*; import javax.swing.plaf.basic.BasicTreeUI; import javax.swing.tree.*; import com.apple.laf.AquaUtils.RecyclableSingleton; import apple.laf.*; import apple.laf.JRSUIConstants.*; import apple.laf.JRSUIState.AnimationFrameState; /** * AquaTreeUI supports the client property "value-add" system of customization See MetalTreeUI * This is heavily based on the 1.3.1 AquaTreeUI implementation. */ public class AquaTreeUI extends BasicTreeUI { // Create PLAF public static ComponentUI createUI(final JComponent c) { return new AquaTreeUI(); } // Begin Line Stuff from Metal private static final String LINE_STYLE = "JTree.lineStyle"; private static final String LEG_LINE_STYLE_STRING = "Angled"; private static final String HORIZ_STYLE_STRING = "Horizontal"; private static final String NO_STYLE_STRING = "None"; private static final int LEG_LINE_STYLE = 2; private static final int HORIZ_LINE_STYLE = 1; private static final int NO_LINE_STYLE = 0; private int lineStyle = HORIZ_LINE_STYLE; private final PropertyChangeListener lineStyleListener = new LineListener(); // mouse tracking state protected TreePath fTrackingPath; protected boolean fIsPressed = false; protected boolean fIsInBounds = false; protected int fAnimationFrame = -1; protected TreeArrowMouseInputHandler fMouseHandler; protected final AquaPainter<AnimationFrameState> painter = AquaPainter.create(JRSUIStateFactory.getDisclosureTriangle()); public AquaTreeUI() { } public void installUI(final JComponent c) { super.installUI(c); final Object lineStyleFlag = c.getClientProperty(LINE_STYLE); decodeLineStyle(lineStyleFlag); c.addPropertyChangeListener(lineStyleListener); } public void uninstallUI(final JComponent c) { c.removePropertyChangeListener(lineStyleListener); super.uninstallUI(c); } /** * Creates the focus listener to repaint the focus ring */ protected FocusListener createFocusListener() { return new AquaTreeUI.FocusHandler(); } /** * this function converts between the string passed into the client property and the internal representation * (currently an int) */ protected void decodeLineStyle(final Object lineStyleFlag) { if (lineStyleFlag == null || NO_STYLE_STRING.equals(lineStyleFlag)) { lineStyle = NO_LINE_STYLE; // default case return; } if (LEG_LINE_STYLE_STRING.equals(lineStyleFlag)) { lineStyle = LEG_LINE_STYLE; } else if (HORIZ_STYLE_STRING.equals(lineStyleFlag)) { lineStyle = HORIZ_LINE_STYLE; } } public TreePath getClosestPathForLocation(final JTree treeLocal, final int x, final int y) { if (treeLocal == null || treeState == null) return null; Insets i = treeLocal.getInsets(); if (i == null) i = new Insets(0, 0, 0, 0); return treeState.getPathClosestTo(x - i.left, y - i.top); } public void paint(final Graphics g, final JComponent c) { super.paint(g, c); // Paint the lines if (lineStyle == HORIZ_LINE_STYLE && !largeModel) { paintHorizontalSeparators(g, c); } } protected void paintHorizontalSeparators(final Graphics g, final JComponent c) { g.setColor(UIManager.getColor("Tree.line")); final Rectangle clipBounds = g.getClipBounds(); final int beginRow = getRowForPath(tree, getClosestPathForLocation(tree, 0, clipBounds.y)); final int endRow = getRowForPath(tree, getClosestPathForLocation(tree, 0, clipBounds.y + clipBounds.height - 1)); if (beginRow <= -1 || endRow <= -1) { return; } for (int i = beginRow; i <= endRow; ++i) { final TreePath path = getPathForRow(tree, i); if (path != null && path.getPathCount() == 2) { final Rectangle rowBounds = getPathBounds(tree, getPathForRow(tree, i)); // Draw a line at the top if (rowBounds != null) g.drawLine(clipBounds.x, rowBounds.y, clipBounds.x + clipBounds.width, rowBounds.y); } } } protected void paintVerticalPartOfLeg(final Graphics g, final Rectangle clipBounds, final Insets insets, final TreePath path) { if (lineStyle == LEG_LINE_STYLE) { super.paintVerticalPartOfLeg(g, clipBounds, insets, path); } } protected void paintHorizontalPartOfLeg(final Graphics g, final Rectangle clipBounds, final Insets insets, final Rectangle bounds, final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded, final boolean isLeaf) { if (lineStyle == LEG_LINE_STYLE) { super.paintHorizontalPartOfLeg(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); } } /** This class listens for changes in line style */ class LineListener implements PropertyChangeListener { public void propertyChange(final PropertyChangeEvent e) { final String name = e.getPropertyName(); if (name.equals(LINE_STYLE)) { decodeLineStyle(e.getNewValue()); } } } /** * Paints the expand (toggle) part of a row. The reciever should NOT modify <code>clipBounds</code>, or * <code>insets</code>. */ protected void paintExpandControl(final Graphics g, final Rectangle clipBounds, final Insets insets, final Rectangle bounds, final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded, final boolean isLeaf) { final Object value = path.getLastPathComponent(); // Draw icons if not a leaf and either hasn't been loaded, // or the model child count is > 0. if (isLeaf || (hasBeenExpanded && treeModel.getChildCount(value) <= 0)) return; final boolean isLeftToRight = AquaUtils.isLeftToRight(tree); // Basic knows, but keeps it private final State state = getState(path); // if we are not animating, do the expected thing, and use the icon // also, if there is a custom (non-LaF defined) icon - just use that instead if (fAnimationFrame == -1 && state != State.PRESSED) { super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); return; } // Both icons are the same size final Icon icon = isExpanded ? getExpandedIcon() : getCollapsedIcon(); if (!(icon instanceof UIResource)) { super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); return; } // if painting a right-to-left knob, we ensure that we are only painting when // the clipbounds rect is set to the exact size of the knob, and positioned correctly // (this code is not the same as metal) int middleXOfKnob; if (isLeftToRight) { middleXOfKnob = bounds.x - (getRightChildIndent() - 1); } else { middleXOfKnob = clipBounds.x + clipBounds.width / 2; } // Center vertically final int middleYOfKnob = bounds.y + (bounds.height / 2); final int x = middleXOfKnob - icon.getIconWidth() / 2; final int y = middleYOfKnob - icon.getIconHeight() / 2; final int height = icon.getIconHeight(); // use the icon height so we don't get drift we modify the bounds (by changing row height) final int width = 20; // this is a hardcoded value from our default icon (since we are only at this point for animation) setupPainter(state, isExpanded, isLeftToRight); painter.paint(g, tree, x, y, width, height); } @Override public Icon getCollapsedIcon() { final Icon icon = super.getCollapsedIcon(); if (AquaUtils.isLeftToRight(tree)) return icon; if (!(icon instanceof UIResource)) return icon; return UIManager.getIcon("Tree.rightToLeftCollapsedIcon"); } protected void setupPainter(State state, final boolean isExpanded, final boolean leftToRight) { if (!fIsInBounds && state == State.PRESSED) state = State.ACTIVE; painter.state.set(state); if (JRSUIUtils.Tree.useLegacyTreeKnobs()) { if (fAnimationFrame == -1) { painter.state.set(isExpanded ? Direction.DOWN : Direction.RIGHT); } else { painter.state.set(Direction.NONE); painter.state.setAnimationFrame(fAnimationFrame - 1); } } else { painter.state.set(getDirection(isExpanded, leftToRight)); painter.state.setAnimationFrame(fAnimationFrame); } } protected Direction getDirection(final boolean isExpanded, final boolean isLeftToRight) { if (isExpanded && (fAnimationFrame == -1)) return Direction.DOWN; return isLeftToRight ? Direction.RIGHT : Direction.LEFT; } protected State getState(final TreePath path) { if (!tree.isEnabled()) return State.DISABLED; if (fIsPressed) { if (fTrackingPath.equals(path)) return State.PRESSED; } return State.ACTIVE; } /** * Misnamed - this is called on mousePressed Macs shouldn't react till mouseReleased * We install a motion handler that gets removed after. * See super.MouseInputHandler & super.startEditing for why */ protected void handleExpandControlClick(final TreePath path, final int mouseX, final int mouseY) { fMouseHandler = new TreeArrowMouseInputHandler(path); } /** * Returning true signifies a mouse event on the node should toggle the selection of only the row under mouse. */ protected boolean isToggleSelectionEvent(final MouseEvent event) { return SwingUtilities.isLeftMouseButton(event) && event.isMetaDown(); } class FocusHandler extends BasicTreeUI.FocusHandler { public void focusGained(final FocusEvent e) { super.focusGained(e); AquaBorder.repaintBorder(tree); } public void focusLost(final FocusEvent e) { super.focusLost(e); AquaBorder.repaintBorder(tree); } } protected PropertyChangeListener createPropertyChangeListener() { return new MacPropertyChangeHandler(); } public class MacPropertyChangeHandler extends PropertyChangeHandler { public void propertyChange(final PropertyChangeEvent e) { final String prop = e.getPropertyName(); if (prop.equals(AquaFocusHandler.FRAME_ACTIVE_PROPERTY)) { AquaBorder.repaintBorder(tree); AquaFocusHandler.swapSelectionColors("Tree", tree, e.getNewValue()); } else { super.propertyChange(e); } } } /** * TreeArrowMouseInputHandler handles passing all mouse events the way a Mac should - hilite/dehilite on enter/exit, * only perform the action if released in arrow. * * Just like super.MouseInputHandler, this is removed once it's not needed, so they won't clash with each other */ // The Adapters take care of defining all the empties class TreeArrowMouseInputHandler extends MouseInputAdapter { protected Rectangle fPathBounds = new Rectangle(); // Values needed for paintOneControl protected boolean fIsLeaf, fIsExpanded, fHasBeenExpanded; protected Rectangle fBounds, fVisibleRect; int fTrackingRow; Insets fInsets; Color fBackground; TreeArrowMouseInputHandler(final TreePath path) { fTrackingPath = path; fIsPressed = true; fIsInBounds = true; this.fPathBounds = getPathArrowBounds(path); tree.addMouseListener(this); tree.addMouseMotionListener(this); fBackground = tree.getBackground(); if (!tree.isOpaque()) { final Component p = tree.getParent(); if (p != null) fBackground = p.getBackground(); } // Set up values needed to paint the triangle - see // BasicTreeUI.paint fVisibleRect = tree.getVisibleRect(); fInsets = tree.getInsets(); if (fInsets == null) fInsets = new Insets(0, 0, 0, 0); fIsLeaf = treeModel.isLeaf(path.getLastPathComponent()); if (fIsLeaf) fIsExpanded = fHasBeenExpanded = false; else { fIsExpanded = treeState.getExpandedState(path); fHasBeenExpanded = tree.hasBeenExpanded(path); } final Rectangle boundsBuffer = new Rectangle(); fBounds = treeState.getBounds(fTrackingPath, boundsBuffer); fBounds.x += fInsets.left; fBounds.y += fInsets.top; fTrackingRow = getRowForPath(fTrackingPath); paintOneControl(); } public void mouseDragged(final MouseEvent e) { fIsInBounds = fPathBounds.contains(e.getX(), e.getY()); paintOneControl(); } @Override public void mouseExited(MouseEvent e) { fIsInBounds = fPathBounds.contains(e.getX(), e.getY()); paintOneControl(); } public void mouseReleased(final MouseEvent e) { if (tree == null) return; if (fIsPressed) { final boolean wasInBounds = fIsInBounds; fIsPressed = false; fIsInBounds = false; if (wasInBounds) { fIsExpanded = !fIsExpanded; paintAnimation(fIsExpanded); if (e.isAltDown()) { if (fIsExpanded) { expandNode(fTrackingRow, true); } else { collapseNode(fTrackingRow, true); } } else { toggleExpandState(fTrackingPath); } } } fTrackingPath = null; removeFromSource(); } protected void paintAnimation(final boolean expanding) { if (expanding) { paintAnimationFrame(1); paintAnimationFrame(2); paintAnimationFrame(3); } else { paintAnimationFrame(3); paintAnimationFrame(2); paintAnimationFrame(1); } fAnimationFrame = -1; } protected void paintAnimationFrame(final int frame) { fAnimationFrame = frame; paintOneControl(); try { Thread.sleep(20); } catch (final InterruptedException e) { } } // Utility to paint just one widget while it's being tracked // Just doing "repaint" runs into problems if someone does "translate" on the graphics // (ie, Sun's JTreeTable example, which is used by Moneydance - see Radar 2697837) void paintOneControl() { if (tree == null) return; final Graphics g = tree.getGraphics(); if (g == null) { // i.e. source is not displayable return; } try { g.setClip(fVisibleRect); // If we ever wanted a callback for drawing the arrow between // transition stages // the code between here and paintExpandControl would be it g.setColor(fBackground); g.fillRect(fPathBounds.x, fPathBounds.y, fPathBounds.width, fPathBounds.height); // if there is no tracking path, we don't need to paint anything if (fTrackingPath == null) return; // draw the vertical line to the parent final TreePath parentPath = fTrackingPath.getParentPath(); if (parentPath != null) { paintVerticalPartOfLeg(g, fPathBounds, fInsets, parentPath); paintHorizontalPartOfLeg(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf); } else if (isRootVisible() && fTrackingRow == 0) { paintHorizontalPartOfLeg(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf); } paintExpandControl(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf); } finally { g.dispose(); } } protected void removeFromSource() { tree.removeMouseListener(this); tree.removeMouseMotionListener(this); } } protected int getRowForPath(final TreePath path) { return treeState.getRowForPath(path); } /** * see isLocationInExpandControl for bounds calc */ protected Rectangle getPathArrowBounds(final TreePath path) { final Rectangle bounds = getPathBounds(tree, path); // Gives us the y values, but x is adjusted for the contents final Insets i = tree.getInsets(); if (getExpandedIcon() != null) bounds.width = getExpandedIcon().getIconWidth(); else bounds.width = 8; int boxLeftX = (i != null) ? i.left : 0; if (AquaUtils.isLeftToRight(tree)) { boxLeftX += (((path.getPathCount() + depthOffset - 2) * totalChildIndent) + getLeftChildIndent()) - bounds.width / 2; } else { boxLeftX += tree.getWidth() - 1 - ((path.getPathCount() - 2 + depthOffset) * totalChildIndent) - getLeftChildIndent() - bounds.width / 2; } bounds.x = boxLeftX; return bounds; } protected void installKeyboardActions() { super.installKeyboardActions(); tree.getActionMap().put("aquaExpandNode", new KeyboardExpandCollapseAction(true, false)); tree.getActionMap().put("aquaCollapseNode", new KeyboardExpandCollapseAction(false, false)); tree.getActionMap().put("aquaFullyExpandNode", new KeyboardExpandCollapseAction(true, true)); tree.getActionMap().put("aquaFullyCollapseNode", new KeyboardExpandCollapseAction(false, true)); } class KeyboardExpandCollapseAction extends AbstractAction { /** * Determines direction to traverse, 1 means expand, -1 means collapse. */ final boolean expand; final boolean recursive; /** * True if the selection is reset, false means only the lead path changes. */ public KeyboardExpandCollapseAction(final boolean expand, final boolean recursive) { this.expand = expand; this.recursive = recursive; } public void actionPerformed(final ActionEvent e) { if (tree == null || 0 > getRowCount(tree)) return; final TreePath[] selectionPaths = tree.getSelectionPaths(); if (selectionPaths == null) return; for (int i = selectionPaths.length - 1; i >= 0; i--) { final TreePath path = selectionPaths[i]; /* * Try and expand the node, otherwise go to next node. */ if (expand) { expandNode(tree.getRowForPath(path), recursive); continue; } // else collapse // in the special case where there is only one row selected, // we want to do what the Cocoa does, and select the parent if (selectionPaths.length == 1 && tree.isCollapsed(path)) { final TreePath parentPath = path.getParentPath(); if (parentPath != null && (!(parentPath.getParentPath() == null) || tree.isRootVisible())) { tree.scrollPathToVisible(parentPath); tree.setSelectionPath(parentPath); } continue; } collapseNode(tree.getRowForPath(path), recursive); } } public boolean isEnabled() { return (tree != null && tree.isEnabled()); } } void expandNode(final int row, final boolean recursive) { final TreePath path = getPathForRow(tree, row); if (path == null) return; tree.expandPath(path); if (!recursive) return; expandAllNodes(path, row + 1); } void expandAllNodes(final TreePath parent, final int initialRow) { for (int i = initialRow; true; i++) { final TreePath path = getPathForRow(tree, i); if (!parent.isDescendant(path)) return; tree.expandPath(path); } } void collapseNode(final int row, final boolean recursive) { final TreePath path = getPathForRow(tree, row); if (path == null) return; if (recursive) { collapseAllNodes(path, row + 1); } tree.collapsePath(path); } void collapseAllNodes(final TreePath parent, final int initialRow) { int lastRow = -1; for (int i = initialRow; lastRow == -1; i++) { final TreePath path = getPathForRow(tree, i); if (!parent.isDescendant(path)) { lastRow = i - 1; } } for (int i = lastRow; i >= initialRow; i--) { final TreePath path = getPathForRow(tree, i); tree.collapsePath(path); } } }