1 /* 2 * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.apple.laf; 27 28 import java.awt.*; 29 import java.awt.event.*; 30 import java.beans.*; 31 32 import javax.swing.*; 33 import javax.swing.event.MouseInputAdapter; 34 import javax.swing.plaf.*; 35 import javax.swing.plaf.basic.BasicTreeUI; 36 import javax.swing.tree.*; 37 38 import com.apple.laf.AquaUtils.RecyclableSingleton; 39 40 import apple.laf.*; 41 import apple.laf.JRSUIConstants.*; 42 import apple.laf.JRSUIState.AnimationFrameState; 43 44 /** 45 * AquaTreeUI supports the client property "value-add" system of customization See MetalTreeUI 46 * This is heavily based on the 1.3.1 AquaTreeUI implementation. 47 */ 48 public class AquaTreeUI extends BasicTreeUI { 49 50 // Create PLAF 51 public static ComponentUI createUI(final JComponent c) { 52 return new AquaTreeUI(); 53 } 54 55 // Begin Line Stuff from Metal 56 57 private static final String LINE_STYLE = "JTree.lineStyle"; 58 59 private static final String LEG_LINE_STYLE_STRING = "Angled"; 60 private static final String HORIZ_STYLE_STRING = "Horizontal"; 61 private static final String NO_STYLE_STRING = "None"; 62 63 private static final int LEG_LINE_STYLE = 2; 64 private static final int HORIZ_LINE_STYLE = 1; 65 private static final int NO_LINE_STYLE = 0; 66 67 private int lineStyle = HORIZ_LINE_STYLE; 68 private final PropertyChangeListener lineStyleListener = new LineListener(); 69 70 // mouse tracking state 71 protected TreePath fTrackingPath; 72 protected boolean fIsPressed = false; 73 protected boolean fIsInBounds = false; 74 protected int fAnimationFrame = -1; 75 protected TreeArrowMouseInputHandler fMouseHandler; 76 77 protected final AquaPainter<AnimationFrameState> painter = AquaPainter.create(JRSUIStateFactory.getDisclosureTriangle()); 78 79 public AquaTreeUI() { 80 81 } 82 83 public void installUI(final JComponent c) { 84 super.installUI(c); 85 86 final Object lineStyleFlag = c.getClientProperty(LINE_STYLE); 87 decodeLineStyle(lineStyleFlag); 88 c.addPropertyChangeListener(lineStyleListener); 89 } 90 91 public void uninstallUI(final JComponent c) { 92 c.removePropertyChangeListener(lineStyleListener); 93 super.uninstallUI(c); 94 } 95 96 /** 97 * Creates the focus listener to repaint the focus ring 98 */ 99 protected FocusListener createFocusListener() { 100 return new AquaTreeUI.FocusHandler(); 101 } 102 103 /** 104 * this function converts between the string passed into the client property and the internal representation 105 * (currently an int) 106 */ 107 protected void decodeLineStyle(final Object lineStyleFlag) { 108 if (lineStyleFlag == null || NO_STYLE_STRING.equals(lineStyleFlag)) { 109 lineStyle = NO_LINE_STYLE; // default case 110 return; 111 } 112 113 if (LEG_LINE_STYLE_STRING.equals(lineStyleFlag)) { 114 lineStyle = LEG_LINE_STYLE; 115 } else if (HORIZ_STYLE_STRING.equals(lineStyleFlag)) { 116 lineStyle = HORIZ_LINE_STYLE; 117 } 118 } 119 120 public TreePath getClosestPathForLocation(final JTree treeLocal, final int x, final int y) { 121 if (treeLocal == null || treeState == null) return null; 122 123 Insets i = treeLocal.getInsets(); 124 if (i == null) i = new Insets(0, 0, 0, 0); 125 return treeState.getPathClosestTo(x - i.left, y - i.top); 126 } 127 128 public void paint(final Graphics g, final JComponent c) { 129 super.paint(g, c); 130 131 // Paint the lines 132 if (lineStyle == HORIZ_LINE_STYLE && !largeModel) { 133 paintHorizontalSeparators(g, c); 134 } 135 } 136 137 protected void paintHorizontalSeparators(final Graphics g, final JComponent c) { 138 g.setColor(UIManager.getColor("Tree.line")); 139 140 final Rectangle clipBounds = g.getClipBounds(); 141 142 final int beginRow = getRowForPath(tree, getClosestPathForLocation(tree, 0, clipBounds.y)); 143 final int endRow = getRowForPath(tree, getClosestPathForLocation(tree, 0, clipBounds.y + clipBounds.height - 1)); 144 145 if (beginRow <= -1 || endRow <= -1) { return; } 146 147 for (int i = beginRow; i <= endRow; ++i) { 148 final TreePath path = getPathForRow(tree, i); 149 150 if (path != null && path.getPathCount() == 2) { 151 final Rectangle rowBounds = getPathBounds(tree, getPathForRow(tree, i)); 152 153 // Draw a line at the top 154 if (rowBounds != null) g.drawLine(clipBounds.x, rowBounds.y, clipBounds.x + clipBounds.width, rowBounds.y); 155 } 156 } 157 } 158 159 protected void paintVerticalPartOfLeg(final Graphics g, final Rectangle clipBounds, final Insets insets, final TreePath path) { 160 if (lineStyle == LEG_LINE_STYLE) { 161 super.paintVerticalPartOfLeg(g, clipBounds, insets, path); 162 } 163 } 164 165 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) { 166 if (lineStyle == LEG_LINE_STYLE) { 167 super.paintHorizontalPartOfLeg(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); 168 } 169 } 170 171 /** This class listens for changes in line style */ 172 class LineListener implements PropertyChangeListener { 173 public void propertyChange(final PropertyChangeEvent e) { 174 final String name = e.getPropertyName(); 175 if (name.equals(LINE_STYLE)) { 176 decodeLineStyle(e.getNewValue()); 177 } 178 } 179 } 180 181 /** 182 * Paints the expand (toggle) part of a row. The receiver should NOT modify <code>clipBounds</code>, or 183 * <code>insets</code>. 184 */ 185 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) { 186 final Object value = path.getLastPathComponent(); 187 188 // Draw icons if not a leaf and either hasn't been loaded, 189 // or the model child count is > 0. 190 if (isLeaf || (hasBeenExpanded && treeModel.getChildCount(value) <= 0)) return; 191 192 final boolean isLeftToRight = AquaUtils.isLeftToRight(tree); // Basic knows, but keeps it private 193 194 final State state = getState(path); 195 196 // if we are not animating, do the expected thing, and use the icon 197 // also, if there is a custom (non-LaF defined) icon - just use that instead 198 if (fAnimationFrame == -1 && state != State.PRESSED) { 199 super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); 200 return; 201 } 202 203 // Both icons are the same size 204 final Icon icon = isExpanded ? getExpandedIcon() : getCollapsedIcon(); 205 if (!(icon instanceof UIResource)) { 206 super.paintExpandControl(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); 207 return; 208 } 209 210 // if painting a right-to-left knob, we ensure that we are only painting when 211 // the clipbounds rect is set to the exact size of the knob, and positioned correctly 212 // (this code is not the same as metal) 213 int middleXOfKnob; 214 if (isLeftToRight) { 215 middleXOfKnob = bounds.x - (getRightChildIndent() - 1); 216 } else { 217 middleXOfKnob = clipBounds.x + clipBounds.width / 2; 218 } 219 220 // Center vertically 221 final int middleYOfKnob = bounds.y + (bounds.height / 2); 222 223 final int x = middleXOfKnob - icon.getIconWidth() / 2; 224 final int y = middleYOfKnob - icon.getIconHeight() / 2; 225 final int height = icon.getIconHeight(); // use the icon height so we don't get drift we modify the bounds (by changing row height) 226 final int width = 20; // this is a hardcoded value from our default icon (since we are only at this point for animation) 227 228 setupPainter(state, isExpanded, isLeftToRight); 229 painter.paint(g, tree, x, y, width, height); 230 } 231 232 @Override 233 public Icon getCollapsedIcon() { 234 final Icon icon = super.getCollapsedIcon(); 235 if (AquaUtils.isLeftToRight(tree)) return icon; 236 if (!(icon instanceof UIResource)) return icon; 237 return UIManager.getIcon("Tree.rightToLeftCollapsedIcon"); 238 } 239 240 protected void setupPainter(State state, final boolean isExpanded, final boolean leftToRight) { 241 if (!fIsInBounds && state == State.PRESSED) state = State.ACTIVE; 242 243 painter.state.set(state); 244 if (JRSUIUtils.Tree.useLegacyTreeKnobs()) { 245 if (fAnimationFrame == -1) { 246 painter.state.set(isExpanded ? Direction.DOWN : Direction.RIGHT); 247 } else { 248 painter.state.set(Direction.NONE); 249 painter.state.setAnimationFrame(fAnimationFrame - 1); 250 } 251 } else { 252 painter.state.set(getDirection(isExpanded, leftToRight)); 253 painter.state.setAnimationFrame(fAnimationFrame); 254 } 255 } 256 257 protected Direction getDirection(final boolean isExpanded, final boolean isLeftToRight) { 258 if (isExpanded && (fAnimationFrame == -1)) return Direction.DOWN; 259 return isLeftToRight ? Direction.RIGHT : Direction.LEFT; 260 } 261 262 protected State getState(final TreePath path) { 263 if (!tree.isEnabled()) return State.DISABLED; 264 if (fIsPressed) { 265 if (fTrackingPath.equals(path)) return State.PRESSED; 266 } 267 return State.ACTIVE; 268 } 269 270 /** 271 * Misnamed - this is called on mousePressed Macs shouldn't react till mouseReleased 272 * We install a motion handler that gets removed after. 273 * See super.MouseInputHandler & super.startEditing for why 274 */ 275 protected void handleExpandControlClick(final TreePath path, final int mouseX, final int mouseY) { 276 fMouseHandler = new TreeArrowMouseInputHandler(path); 277 } 278 279 /** 280 * Returning true signifies a mouse event on the node should toggle the selection of only the row under mouse. 281 */ 282 protected boolean isToggleSelectionEvent(final MouseEvent event) { 283 return SwingUtilities.isLeftMouseButton(event) && event.isMetaDown(); 284 } 285 286 class FocusHandler extends BasicTreeUI.FocusHandler { 287 public void focusGained(final FocusEvent e) { 288 super.focusGained(e); 289 AquaBorder.repaintBorder(tree); 290 } 291 292 public void focusLost(final FocusEvent e) { 293 super.focusLost(e); 294 AquaBorder.repaintBorder(tree); 295 } 296 } 297 298 protected PropertyChangeListener createPropertyChangeListener() { 299 return new MacPropertyChangeHandler(); 300 } 301 302 public class MacPropertyChangeHandler extends PropertyChangeHandler { 303 public void propertyChange(final PropertyChangeEvent e) { 304 final String prop = e.getPropertyName(); 305 if (prop.equals(AquaFocusHandler.FRAME_ACTIVE_PROPERTY)) { 306 AquaBorder.repaintBorder(tree); 307 AquaFocusHandler.swapSelectionColors("Tree", tree, e.getNewValue()); 308 } else { 309 super.propertyChange(e); 310 } 311 } 312 } 313 314 /** 315 * TreeArrowMouseInputHandler handles passing all mouse events the way a Mac should - hilite/dehilite on enter/exit, 316 * only perform the action if released in arrow. 317 * 318 * Just like super.MouseInputHandler, this is removed once it's not needed, so they won't clash with each other 319 */ 320 // The Adapters take care of defining all the empties 321 class TreeArrowMouseInputHandler extends MouseInputAdapter { 322 protected Rectangle fPathBounds = new Rectangle(); 323 324 // Values needed for paintOneControl 325 protected boolean fIsLeaf, fIsExpanded, fHasBeenExpanded; 326 protected Rectangle fBounds, fVisibleRect; 327 int fTrackingRow; 328 Insets fInsets; 329 Color fBackground; 330 331 TreeArrowMouseInputHandler(final TreePath path) { 332 fTrackingPath = path; 333 fIsPressed = true; 334 fIsInBounds = true; 335 this.fPathBounds = getPathArrowBounds(path); 336 tree.addMouseListener(this); 337 tree.addMouseMotionListener(this); 338 fBackground = tree.getBackground(); 339 if (!tree.isOpaque()) { 340 final Component p = tree.getParent(); 341 if (p != null) fBackground = p.getBackground(); 342 } 343 344 // Set up values needed to paint the triangle - see 345 // BasicTreeUI.paint 346 fVisibleRect = tree.getVisibleRect(); 347 fInsets = tree.getInsets(); 348 349 if (fInsets == null) fInsets = new Insets(0, 0, 0, 0); 350 fIsLeaf = treeModel.isLeaf(path.getLastPathComponent()); 351 if (fIsLeaf) fIsExpanded = fHasBeenExpanded = false; 352 else { 353 fIsExpanded = treeState.getExpandedState(path); 354 fHasBeenExpanded = tree.hasBeenExpanded(path); 355 } 356 final Rectangle boundsBuffer = new Rectangle(); 357 fBounds = treeState.getBounds(fTrackingPath, boundsBuffer); 358 fBounds.x += fInsets.left; 359 fBounds.y += fInsets.top; 360 fTrackingRow = getRowForPath(fTrackingPath); 361 362 paintOneControl(); 363 } 364 365 public void mouseDragged(final MouseEvent e) { 366 fIsInBounds = fPathBounds.contains(e.getX(), e.getY()); 367 paintOneControl(); 368 } 369 370 @Override 371 public void mouseExited(MouseEvent e) { 372 fIsInBounds = fPathBounds.contains(e.getX(), e.getY()); 373 paintOneControl(); 374 } 375 376 public void mouseReleased(final MouseEvent e) { 377 if (tree == null) return; 378 379 if (fIsPressed) { 380 final boolean wasInBounds = fIsInBounds; 381 382 fIsPressed = false; 383 fIsInBounds = false; 384 385 if (wasInBounds) { 386 fIsExpanded = !fIsExpanded; 387 paintAnimation(fIsExpanded); 388 if (e.isAltDown()) { 389 if (fIsExpanded) { 390 expandNode(fTrackingRow, true); 391 } else { 392 collapseNode(fTrackingRow, true); 393 } 394 } else { 395 toggleExpandState(fTrackingPath); 396 } 397 } 398 } 399 fTrackingPath = null; 400 removeFromSource(); 401 } 402 403 protected void paintAnimation(final boolean expanding) { 404 if (expanding) { 405 paintAnimationFrame(1); 406 paintAnimationFrame(2); 407 paintAnimationFrame(3); 408 } else { 409 paintAnimationFrame(3); 410 paintAnimationFrame(2); 411 paintAnimationFrame(1); 412 } 413 fAnimationFrame = -1; 414 } 415 416 protected void paintAnimationFrame(final int frame) { 417 fAnimationFrame = frame; 418 paintOneControl(); 419 try { Thread.sleep(20); } catch (final InterruptedException e) { } 420 } 421 422 // Utility to paint just one widget while it's being tracked 423 // Just doing "repaint" runs into problems if someone does "translate" on the graphics 424 // (ie, Sun's JTreeTable example, which is used by Moneydance - see Radar 2697837) 425 void paintOneControl() { 426 if (tree == null) return; 427 final Graphics g = tree.getGraphics(); 428 if (g == null) { 429 // i.e. source is not displayable 430 return; 431 } 432 433 try { 434 g.setClip(fVisibleRect); 435 // If we ever wanted a callback for drawing the arrow between 436 // transition stages 437 // the code between here and paintExpandControl would be it 438 g.setColor(fBackground); 439 g.fillRect(fPathBounds.x, fPathBounds.y, fPathBounds.width, fPathBounds.height); 440 441 // if there is no tracking path, we don't need to paint anything 442 if (fTrackingPath == null) return; 443 444 // draw the vertical line to the parent 445 final TreePath parentPath = fTrackingPath.getParentPath(); 446 if (parentPath != null) { 447 paintVerticalPartOfLeg(g, fPathBounds, fInsets, parentPath); 448 paintHorizontalPartOfLeg(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf); 449 } else if (isRootVisible() && fTrackingRow == 0) { 450 paintHorizontalPartOfLeg(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf); 451 } 452 paintExpandControl(g, fPathBounds, fInsets, fBounds, fTrackingPath, fTrackingRow, fIsExpanded, fHasBeenExpanded, fIsLeaf); 453 } finally { 454 g.dispose(); 455 } 456 } 457 458 protected void removeFromSource() { 459 tree.removeMouseListener(this); 460 tree.removeMouseMotionListener(this); 461 } 462 } 463 464 protected int getRowForPath(final TreePath path) { 465 return treeState.getRowForPath(path); 466 } 467 468 /** 469 * see isLocationInExpandControl for bounds calc 470 */ 471 protected Rectangle getPathArrowBounds(final TreePath path) { 472 final Rectangle bounds = getPathBounds(tree, path); // Gives us the y values, but x is adjusted for the contents 473 final Insets i = tree.getInsets(); 474 475 if (getExpandedIcon() != null) bounds.width = getExpandedIcon().getIconWidth(); 476 else bounds.width = 8; 477 478 int boxLeftX = (i != null) ? i.left : 0; 479 if (AquaUtils.isLeftToRight(tree)) { 480 boxLeftX += (((path.getPathCount() + depthOffset - 2) * totalChildIndent) + getLeftChildIndent()) - bounds.width / 2; 481 } else { 482 boxLeftX += tree.getWidth() - 1 - ((path.getPathCount() - 2 + depthOffset) * totalChildIndent) - getLeftChildIndent() - bounds.width / 2; 483 } 484 bounds.x = boxLeftX; 485 return bounds; 486 } 487 488 protected void installKeyboardActions() { 489 super.installKeyboardActions(); 490 tree.getActionMap().put("aquaExpandNode", new KeyboardExpandCollapseAction(true, false)); 491 tree.getActionMap().put("aquaCollapseNode", new KeyboardExpandCollapseAction(false, false)); 492 tree.getActionMap().put("aquaFullyExpandNode", new KeyboardExpandCollapseAction(true, true)); 493 tree.getActionMap().put("aquaFullyCollapseNode", new KeyboardExpandCollapseAction(false, true)); 494 } 495 496 @SuppressWarnings("serial") // Superclass is not serializable across versions 497 class KeyboardExpandCollapseAction extends AbstractAction { 498 /** 499 * Determines direction to traverse, 1 means expand, -1 means collapse. 500 */ 501 final boolean expand; 502 final boolean recursive; 503 504 /** 505 * True if the selection is reset, false means only the lead path changes. 506 */ 507 public KeyboardExpandCollapseAction(final boolean expand, final boolean recursive) { 508 this.expand = expand; 509 this.recursive = recursive; 510 } 511 512 public void actionPerformed(final ActionEvent e) { 513 if (tree == null || 0 > getRowCount(tree)) return; 514 515 final TreePath[] selectionPaths = tree.getSelectionPaths(); 516 if (selectionPaths == null) return; 517 518 for (int i = selectionPaths.length - 1; i >= 0; i--) { 519 final TreePath path = selectionPaths[i]; 520 521 /* 522 * Try and expand the node, otherwise go to next node. 523 */ 524 if (expand) { 525 expandNode(tree.getRowForPath(path), recursive); 526 continue; 527 } 528 // else collapse 529 530 // in the special case where there is only one row selected, 531 // we want to do what the Cocoa does, and select the parent 532 if (selectionPaths.length == 1 && tree.isCollapsed(path)) { 533 final TreePath parentPath = path.getParentPath(); 534 if (parentPath != null && (!(parentPath.getParentPath() == null) || tree.isRootVisible())) { 535 tree.scrollPathToVisible(parentPath); 536 tree.setSelectionPath(parentPath); 537 } 538 continue; 539 } 540 541 collapseNode(tree.getRowForPath(path), recursive); 542 } 543 } 544 545 public boolean isEnabled() { 546 return (tree != null && tree.isEnabled()); 547 } 548 } 549 550 void expandNode(final int row, final boolean recursive) { 551 final TreePath path = getPathForRow(tree, row); 552 if (path == null) return; 553 554 tree.expandPath(path); 555 if (!recursive) return; 556 557 expandAllNodes(path, row + 1); 558 } 559 560 void expandAllNodes(final TreePath parent, final int initialRow) { 561 for (int i = initialRow; true; i++) { 562 final TreePath path = getPathForRow(tree, i); 563 if (!parent.isDescendant(path)) return; 564 565 tree.expandPath(path); 566 } 567 } 568 569 void collapseNode(final int row, final boolean recursive) { 570 final TreePath path = getPathForRow(tree, row); 571 if (path == null) return; 572 573 if (recursive) { 574 collapseAllNodes(path, row + 1); 575 } 576 577 tree.collapsePath(path); 578 } 579 580 void collapseAllNodes(final TreePath parent, final int initialRow) { 581 int lastRow = -1; 582 for (int i = initialRow; lastRow == -1; i++) { 583 final TreePath path = getPathForRow(tree, i); 584 if (!parent.isDescendant(path)) { 585 lastRow = i - 1; 586 } 587 } 588 589 for (int i = lastRow; i >= initialRow; i--) { 590 final TreePath path = getPathForRow(tree, i); 591 tree.collapsePath(path); 592 } 593 } 594 }