| /* | |
| * Copyright (c) 2011, 2014, 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 receiver should NOT modify {@code clipBounds}, or | |
| * {@code insets}. | |
| */ | |
| 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)); | |
| } | |
| @SuppressWarnings("serial") // Superclass is not serializable across versions | |
| 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); | |
| } | |
| } | |
| } |