/* * Copyright (c) 2005-2016 Substance Kirill Grouchnikov. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * o Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * o Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * o Neither the name of Substance Kirill Grouchnikov nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.pushingpixels.substance.internal.ui; import java.awt.Color; import java.awt.Component; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.swing.ButtonModel; import javax.swing.DefaultButtonModel; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicTreeUI; import javax.swing.tree.TreeCellRenderer; import javax.swing.tree.TreePath; import org.pushingpixels.lafwidget.LafWidget; import org.pushingpixels.lafwidget.LafWidgetRepository; import org.pushingpixels.lafwidget.LafWidgetUtilities; import org.pushingpixels.lafwidget.icon.HiDpiAwareIconUiResource; import org.pushingpixels.lafwidget.utils.RenderingUtils; import org.pushingpixels.substance.api.ColorSchemeAssociationKind; import org.pushingpixels.substance.api.ComponentState; import org.pushingpixels.substance.api.ComponentStateFacet; import org.pushingpixels.substance.api.SubstanceColorScheme; import org.pushingpixels.substance.api.SubstanceLookAndFeel; import org.pushingpixels.substance.api.renderers.SubstanceDefaultTreeCellRenderer; import org.pushingpixels.substance.internal.animation.StateTransitionMultiTracker; import org.pushingpixels.substance.internal.animation.StateTransitionTracker; import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils; import org.pushingpixels.substance.internal.painter.HighlightPainterUtils; import org.pushingpixels.substance.internal.utils.SubstanceColorSchemeUtilities; import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities; import org.pushingpixels.substance.internal.utils.SubstanceSizeUtils; import org.pushingpixels.substance.internal.utils.SubstanceStripingUtils; import org.pushingpixels.substance.internal.utils.icon.SubstanceIconFactory; import org.pushingpixels.trident.Timeline.TimelineState; import org.pushingpixels.trident.callback.UIThreadTimelineCallbackAdapter; /** * UI for lists in <b>Substance</b> look and feel. * * @author Kirill Grouchnikov */ public class SubstanceTreeUI extends BasicTreeUI { /** * Holds the list of currently selected paths. */ protected Map<TreePathId, Object> selectedPaths; /** * Holds the currently rolled-over path or <code>null</code> if none such. */ protected TreePathId currRolloverPathId; /** * Listener that listens to changes on tree properties. */ protected PropertyChangeListener substancePropertyChangeListener; /** * Listener for selection animations. */ protected TreeSelectionListener substanceSelectionFadeListener; /** * Listener for transition animations on tree rollovers. */ protected RolloverFadeListener substanceFadeRolloverListener; /** * Listener for selection of an entire row. */ protected MouseListener substanceRowSelectionListener; private StateTransitionMultiTracker<TreePathId> stateTransitionMultiTracker; /** * The current default color scheme. Is computed in * {@link #update(Graphics, JComponent)} and reused in * {@link SubstanceDefaultTreeCellRenderer#getTreeCellRendererComponent(JTree, Object, boolean, boolean, boolean, int, boolean)} * for performance optimizations. */ private SubstanceColorScheme currDefaultColorScheme; /** * Cell renderer insets. Is computed in {@link #installDefaults()} and * reused in * {@link SubstanceDefaultTreeCellRenderer#getTreeCellRendererComponent(JTree, Object, boolean, boolean, boolean, int, boolean)} * for performance optimizations. */ private Insets cellRendererInsets; private Set<LafWidget> lafWidgets; /* * (non-Javadoc) * * @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent) */ public static ComponentUI createUI(JComponent comp) { SubstanceCoreUtilities.testComponentCreationThreadingViolation(comp); return new SubstanceTreeUI(); } /** * Creates a UI delegate for tree. */ public SubstanceTreeUI() { super(); this.selectedPaths = new HashMap<TreePathId, Object>(); this.stateTransitionMultiTracker = new StateTransitionMultiTracker<TreePathId>(); } @Override public void installUI(JComponent c) { this.lafWidgets = LafWidgetRepository.getRepository().getMatchingWidgets(c); super.installUI(c); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installUI(); } } @Override public void uninstallUI(JComponent c) { for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallUI(); } super.uninstallUI(c); } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTreeUI#installDefaults() */ @Override protected void installDefaults() { super.installDefaults(); if (SubstanceCoreUtilities.toDrawWatermark(this.tree)) this.tree.setOpaque(false); if (this.tree.getSelectionPaths() != null) { for (TreePath selectionPath : this.tree.getSelectionPaths()) { TreePathId pathId = new TreePathId(selectionPath); selectedPaths.put(pathId, selectionPath.getLastPathComponent()); } } Icon expandedIcon = SubstanceIconFactory.getTreeIcon(this.tree, false); Icon collapsedIcon = SubstanceIconFactory.getTreeIcon(this.tree, true); setExpandedIcon(new HiDpiAwareIconUiResource(expandedIcon)); setCollapsedIcon(new HiDpiAwareIconUiResource(collapsedIcon)); // instead of computing the cell renderer insets on // every cell rendering, compute it once and expose to the // SubstanceDefaultTreeCellRenderer this.cellRendererInsets = SubstanceSizeUtils .getTreeCellRendererInsets(SubstanceSizeUtils.getComponentFontSize(tree)); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installDefaults(); } } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTreeUI#uninstallDefaults() */ @Override protected void uninstallDefaults() { this.selectedPaths.clear(); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallDefaults(); } super.uninstallDefaults(); } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTreeUI#paintRow(java.awt.Graphics, * java.awt.Rectangle, java.awt.Insets, java.awt.Rectangle, * javax.swing.tree.TreePath, int, boolean, boolean, boolean) */ @Override protected void paintRow(Graphics g, Rectangle clipBounds, Insets insets, Rectangle bounds, TreePath path, int row, boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf) { // Don't paint the renderer if editing this row. if ((this.editingComponent != null) && (this.editingRow == row)) { // fix for issue 446 - paint the expand control // on the editing row if (shouldPaintExpandControl(path, row, isExpanded, hasBeenExpanded, isLeaf)) { paintExpandControlEnforce(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); } } int leadIndex; if (this.tree.hasFocus()) { TreePath leadPath = this.tree.getLeadSelectionPath(); leadIndex = this.getRowForPath(this.tree, leadPath); } else { leadIndex = -1; } Component renderer = this.currentCellRenderer.getTreeCellRendererComponent(this.tree, path.getLastPathComponent(), this.tree.isRowSelected(row), isExpanded, isLeaf, row, (leadIndex == row)); if (!(renderer instanceof SubstanceDefaultTreeCellRenderer)) { // if it's not Substance renderer - ask the Basic delegate to paint // it. super.paintRow(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); if (shouldPaintExpandControl(path, row, isExpanded, hasBeenExpanded, isLeaf)) { paintExpandControlEnforce(g, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); } return; } TreePathId pathId = new TreePathId(path); Graphics2D g2d = (Graphics2D) g.create(); g2d.setComposite(LafWidgetUtilities.getAlphaComposite(tree, g)); // Color background = renderer.getBackground(); // if (background == null) // background = tree.getBackground(); StateTransitionTracker.ModelStateInfo modelStateInfo = getModelStateInfo(pathId); Map<ComponentState, StateTransitionTracker.StateContributionInfo> activeStates = ((modelStateInfo == null) ? null : modelStateInfo.getStateContributionMap()); ComponentState currState = ((modelStateInfo == null) ? getPathState(pathId) : modelStateInfo.getCurrModelState()); // Compute the alpha values for the animation. boolean hasHighlights = false; if (renderer.isEnabled()) { if (activeStates != null) { for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> stateEntry : activeStates .entrySet()) { hasHighlights = (SubstanceColorSchemeUtilities.getHighlightAlpha(this.tree, stateEntry.getKey()) * stateEntry.getValue().getContribution() > 0.0f); if (hasHighlights) break; } } else { hasHighlights = (SubstanceColorSchemeUtilities.getHighlightAlpha(this.tree, currState) > 0.0f); } } // System.out.println(row + ":" + prevTheme.getDisplayName() + "[" // + alphaForPrevBackground + "]:" + currTheme.getDisplayName() // + "[" + alphaForCurrBackground + "]"); // At this point the renderer is an instance of // SubstanceDefaultTreeCellRenderer JTree.DropLocation dropLocation = tree.getDropLocation(); Rectangle rowRectangle = new Rectangle(this.tree.getInsets().left, bounds.y, this.tree.getWidth() - this.tree.getInsets().right - this.tree.getInsets().left, bounds.height); if (dropLocation != null && dropLocation.getChildIndex() == -1 && tree.getRowForPath(dropLocation.getPath()) == row) { // mark drop location SubstanceColorScheme scheme = SubstanceColorSchemeUtilities.getColorScheme(tree, ColorSchemeAssociationKind.HIGHLIGHT_TEXT, currState); SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities.getColorScheme(tree, ColorSchemeAssociationKind.HIGHLIGHT_BORDER, currState); HighlightPainterUtils.paintHighlight(g2d, this.rendererPane, renderer, rowRectangle, 0.8f, null, scheme, borderScheme); } else { if (hasHighlights) { if (activeStates == null) { float alpha = SubstanceColorSchemeUtilities.getHighlightAlpha(this.tree, currState); if (alpha > 0.0f) { SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tree, ColorSchemeAssociationKind.HIGHLIGHT_TEXT, currState); SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tree, ColorSchemeAssociationKind.HIGHLIGHT_BORDER, currState); g2d.setComposite(LafWidgetUtilities.getAlphaComposite(this.tree, alpha, g)); // Fix for defect 180 - painting the // highlight beneath the entire row HighlightPainterUtils.paintHighlight(g2d, this.rendererPane, renderer, rowRectangle, 0.8f, null, fillScheme, borderScheme); g2d.setComposite(LafWidgetUtilities.getAlphaComposite(this.tree, g)); } } else { for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> stateEntry : activeStates .entrySet()) { ComponentState activeState = stateEntry.getKey(); float alpha = SubstanceColorSchemeUtilities.getHighlightAlpha(this.tree, activeState) * stateEntry.getValue().getContribution(); if (alpha == 0.0f) continue; SubstanceColorScheme fillScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tree, ColorSchemeAssociationKind.HIGHLIGHT_TEXT, activeState); SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities .getColorScheme(this.tree, ColorSchemeAssociationKind.HIGHLIGHT_BORDER, activeState); g2d.setComposite(LafWidgetUtilities.getAlphaComposite(this.tree, alpha, g)); // Fix for defect 180 - painting the // highlight beneath the entire row HighlightPainterUtils.paintHighlight(g2d, this.rendererPane, renderer, rowRectangle, 0.8f, null, fillScheme, borderScheme); g2d.setComposite(LafWidgetUtilities.getAlphaComposite(this.tree, g)); } } } } // System.out.println("Painting row " + row); // Play with opacity to make our own gradient background // on selected elements to show - safe to cast and set opacity // since at this point the renderer can only by the // SubstanceDefaultTreeCellRenderer JComponent jRenderer = (JComponent) renderer; boolean newOpaque = !this.tree.isRowSelected(row); if (SubstanceCoreUtilities.toDrawWatermark(this.tree)) newOpaque = false; Map<Component, Boolean> opacity = new HashMap<Component, Boolean>(); if (!newOpaque) SubstanceCoreUtilities.makeNonOpaque(jRenderer, opacity); this.rendererPane.paintComponent(g2d, renderer, this.tree, bounds.x, bounds.y, Math.max(this.tree.getWidth() - this.tree.getInsets().right - this.tree.getInsets().left - bounds.x, bounds.width), bounds.height, true); if (!newOpaque) SubstanceCoreUtilities.restoreOpaque(jRenderer, opacity); // Paint the expand control now after the row background has been // overlayed by the highlight background on selected and rolled over // rows. See comments on paintExpandControl(). if (shouldPaintExpandControl(path, row, isExpanded, hasBeenExpanded, isLeaf)) { paintExpandControlEnforce(g2d, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); } g2d.dispose(); } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTreeUI#paintExpandControl(java.awt.Graphics, * java.awt.Rectangle, java.awt.Insets, java.awt.Rectangle, * javax.swing.tree.TreePath, int, boolean, boolean, boolean) */ @Override protected void paintExpandControl(Graphics g, Rectangle clipBounds, Insets insets, Rectangle bounds, TreePath path, int row, boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf) { // This does nothing. The base implementation of paint() paints // the tree lines and tree expand controls *before* painting the // renderer. In Substance, the highlights are painted in the // paintRow, and thus would overlay the expand controls. This results // in expand controls being much less visible under most of the skins. // So, Substance paints the expand controls *after* painting the // highlights (and the renderer which doesn't overlap with the expand // controls in any case). This is done in paintRow() by calling // the paintExpandControlEnforce() instead (that eventually calls the // super implementation of paintExpandControl(). } /** * Paints the expand control of the specified row. * * @param g * Graphics context. * @param clipBounds * Clip bounds. * @param insets * Insets. * @param bounds * Row bounds. * @param path * Tree path. * @param row * Tree row. * @param isExpanded * Expand indication. * @param hasBeenExpanded * Indication whether this row has ever been expanded. * @param isLeaf * Indication whether this row is a leaf. */ protected void paintExpandControlEnforce(Graphics g, Rectangle clipBounds, Insets insets, Rectangle bounds, TreePath path, int row, boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf) { float alpha = SubstanceColorSchemeUtilities.getAlpha(this.tree, this.tree.isEnabled() ? ComponentState.ENABLED : ComponentState.DISABLED_UNSELECTED); Graphics2D graphics = (Graphics2D) g.create(); graphics.setComposite(LafWidgetUtilities.getAlphaComposite(this.tree, alpha, g)); super.paintExpandControl(graphics, clipBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); graphics.dispose(); } /* * (non-Javadoc) * * @see * javax.swing.plaf.basic.BasicTreeUI#paintHorizontalPartOfLeg(java.awt. * Graphics, java.awt.Rectangle, java.awt.Insets, java.awt.Rectangle, * javax.swing.tree.TreePath, int, boolean, boolean, boolean) */ @Override protected void paintHorizontalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets, Rectangle bounds, TreePath path, int row, boolean isExpanded, boolean hasBeenExpanded, boolean isLeaf) { } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTreeUI#paintVerticalPartOfLeg(java.awt. * Graphics , java.awt.Rectangle, java.awt.Insets, * javax.swing.tree.TreePath) */ @Override protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets, TreePath path) { } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTreeUI#createDefaultCellRenderer() */ @Override protected TreeCellRenderer createDefaultCellRenderer() { return new SubstanceDefaultTreeCellRenderer(); } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTreeUI#installListeners() */ @Override protected void installListeners() { super.installListeners(); this.substancePropertyChangeListener = (PropertyChangeEvent evt) -> { if (SubstanceLookAndFeel.WATERMARK_VISIBLE.equals(evt.getPropertyName())) { tree.setOpaque(!SubstanceCoreUtilities.toDrawWatermark(tree)); } if ("font".equals(evt.getPropertyName())) { SwingUtilities.invokeLater(() -> tree.updateUI()); } if ("dropLocation".equals(evt.getPropertyName())) { JTree.DropLocation oldValue = (JTree.DropLocation) evt.getOldValue(); if (oldValue != null) { TreePath oldDrop = oldValue.getPath(); Rectangle oldBounds = getPathBounds(tree, oldDrop); tree.repaint(0, oldBounds.y, tree.getWidth(), oldBounds.height); } JTree.DropLocation currLocation = tree.getDropLocation(); if (currLocation != null) { TreePath newDrop = currLocation.getPath(); if (newDrop != null) { Rectangle newBounds = getPathBounds(tree, newDrop); tree.repaint(0, newBounds.y, tree.getWidth(), newBounds.height); } } } }; this.tree.addPropertyChangeListener(this.substancePropertyChangeListener); this.substanceSelectionFadeListener = new MyTreeSelectionListener(); this.tree.getSelectionModel().addTreeSelectionListener(this.substanceSelectionFadeListener); this.substanceRowSelectionListener = new RowSelectionListener(); this.tree.addMouseListener(this.substanceRowSelectionListener); // Add listener for the fade animation this.substanceFadeRolloverListener = new RolloverFadeListener(); this.tree.addMouseMotionListener(this.substanceFadeRolloverListener); this.tree.addMouseListener(this.substanceFadeRolloverListener); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installListeners(); } } /* * (non-Javadoc) * * @see javax.swing.plaf.basic.BasicTreeUI#uninstallListeners() */ @Override protected void uninstallListeners() { this.tree.removeMouseListener(this.substanceRowSelectionListener); this.substanceRowSelectionListener = null; this.tree.getSelectionModel() .removeTreeSelectionListener(this.substanceSelectionFadeListener); this.substanceSelectionFadeListener = null; this.tree.removePropertyChangeListener(this.substancePropertyChangeListener); this.substancePropertyChangeListener = null; // Remove listener for the fade animation this.tree.removeMouseMotionListener(this.substanceFadeRolloverListener); this.tree.removeMouseListener(this.substanceFadeRolloverListener); this.substanceFadeRolloverListener = null; for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallListeners(); } super.uninstallListeners(); } @Override protected void installComponents() { super.installComponents(); for (LafWidget lafWidget : this.lafWidgets) { lafWidget.installComponents(); } } @Override protected void uninstallComponents() { for (LafWidget lafWidget : this.lafWidgets) { lafWidget.uninstallComponents(); } super.uninstallComponents(); } /** * ID of a single tree path. * * @author Kirill Grouchnikov */ public static class TreePathId implements Comparable { /** * Tree path. */ protected TreePath path; /** * Creates a tree path ID. * * @param path * Tree path. */ public TreePathId(TreePath path) { this.path = path; } /* * (non-Javadoc) * * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(Object o) { if (o instanceof TreePathId) { TreePathId otherId = (TreePathId) o; if ((this.path == null) && (otherId.path != null)) return 1; if ((otherId.path == null) && (this.path != null)) return -1; Object[] path1Objs = this.path.getPath(); Object[] path2Objs = otherId.path.getPath(); if (path1Objs.length != path2Objs.length) return 1; for (int i = 0; i < path1Objs.length; i++) if (!path1Objs[i].equals(path2Objs[i])) return 1; return 0; } return -1; } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { return this.compareTo(obj) == 0; } /* * (non-Javadoc) * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { if (this.path == null) return 0; Object[] pathObjs = this.path.getPath(); int result = pathObjs[0].hashCode(); for (int i = 1; i < pathObjs.length; i++) result = result ^ pathObjs[i].hashCode(); return result; } } /** * Selection listener for selection animation effects. * * @author Kirill Grouchnikov */ protected class MyTreeSelectionListener implements TreeSelectionListener { /* * (non-Javadoc) * * @see * javax.swing.event.TreeSelectionListener#valueChanged(javax.swing. * event.TreeSelectionEvent) */ public void valueChanged(TreeSelectionEvent e) { // Map<TreePathId, Object> currSelected = (Map<TreePathId, Object>) // tree // .getClientProperty(SELECTED_INDICES); if (tree.getSelectionPaths() != null) { for (TreePath selectionPath : tree.getSelectionPaths()) { TreePathId pathId = new TreePathId(selectionPath); // check if was selected before if (!selectedPaths.containsKey(pathId)) { // start fading in StateTransitionTracker tracker = getTracker(pathId, (currRolloverPathId != null) && pathId.equals(currRolloverPathId), false); tracker.getModel().setSelected(true); selectedPaths.put(pathId, selectionPath.getLastPathComponent()); } } } for (Iterator<Map.Entry<TreePathId, Object>> it = selectedPaths.entrySet() .iterator(); it.hasNext();) { Map.Entry<TreePathId, Object> entry = it.next(); if (tree.getSelectionModel().isPathSelected(entry.getKey().path)) continue; // fade out for deselected path TreePathId pathId = entry.getKey(); StateTransitionTracker tracker = getTracker(pathId, (currRolloverPathId != null) && pathId.equals(currRolloverPathId), true); tracker.getModel().setSelected(false); it.remove(); } } } /** * Repaints a single path during the fade animation cycle. * * @author Kirill Grouchnikov */ protected class PathRepaintCallback extends UIThreadTimelineCallbackAdapter { /** * Associated tree. */ protected JTree tree; /** * Associated (animated) path. */ protected TreePath treePath; /** * Creates a new animation repaint callback. * * @param tree * Associated tree. * @param treePath * Associated (animated) path. */ public PathRepaintCallback(JTree tree, TreePath treePath) { super(); this.tree = tree; this.treePath = treePath; } @Override public void onTimelinePulse(float durationFraction, float timelinePosition) { this.repaintPath(); } @Override public void onTimelineStateChanged(TimelineState oldState, TimelineState newState, float durationFraction, float timelinePosition) { this.repaintPath(); } /** * Repaints the associated path. */ private void repaintPath() { SwingUtilities.invokeLater(() -> { if (SubstanceTreeUI.this.tree == null) { // may happen if the LAF was switched in the meantime return; } Rectangle boundsBuffer = new Rectangle(); Rectangle bounds = treeState.getBounds(treePath, boundsBuffer); if (bounds != null) { // still visible // fix for defect 180 - refresh the entire row bounds.x = 0; bounds.width = tree.getWidth(); // fix for defect 188 - rollover effects for trees // with insets Insets insets = tree.getInsets(); bounds.x += insets.left; bounds.y += insets.top; tree.repaint(bounds); } }); } } /** * Listener for rollover animation effects. * * @author Kirill Grouchnikov */ private class RolloverFadeListener implements MouseListener, MouseMotionListener { public void mouseClicked(MouseEvent e) { } public void mouseEntered(MouseEvent e) { if (!tree.isEnabled()) return; // isInside = true; } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseExited(MouseEvent e) { if (!tree.isEnabled()) return; // isInside = false; this.fadeOut(); // System.out.println("Nulling RO index"); currRolloverPathId = null; } public void mouseMoved(MouseEvent e) { if (!tree.isEnabled()) return; // isInside = true; handleMove(e); } public void mouseDragged(MouseEvent e) { if (!tree.isEnabled()) return; handleMove(e); } /** * Handles various mouse move events and initiates the fade animation if * necessary. * * @param e * Mouse event. */ private void handleMove(MouseEvent e) { TreePath closestPath = tree.getClosestPathForLocation(e.getX(), e.getY()); Rectangle bounds = tree.getPathBounds(closestPath); if (bounds == null) { this.fadeOut(); currRolloverPathId = null; return; } if ((e.getY() < bounds.y) || (e.getY() > (bounds.y + bounds.height))) { this.fadeOut(); currRolloverPathId = null; return; } // check if this is the same index TreePathId newPathId = new TreePathId(closestPath); if ((currRolloverPathId != null) && newPathId.equals(currRolloverPathId)) { // System.out.println("Same location " + // System.currentTimeMillis()); // System.out.print("Current : "); // for (Object o1 : currPathId.path.getPath()) { // System.out.print(o1); // } // System.out.println(""); // System.out.print("Closest : "); // for (Object o2 : newPathId.path.getPath()) { // System.out.print(o2); // } // System.out.println(""); return; } this.fadeOut(); StateTransitionTracker tracker = getTracker(newPathId, false, selectedPaths.containsKey(newPathId)); tracker.getModel().setRollover(true); currRolloverPathId = newPathId; } /** * Initiates the fade out effect. */ private void fadeOut() { if (currRolloverPathId == null) return; StateTransitionTracker tracker = getTracker(currRolloverPathId, true, selectedPaths.containsKey(currRolloverPathId)); tracker.getModel().setRollover(false); } } /** * Listener for selecting the entire rows. * * @author Kirill Grouchnikov */ private class RowSelectionListener extends MouseAdapter { /* * (non-Javadoc) * * @see * java.awt.event.MouseAdapter#mousePressed(java.awt.event.MouseEvent) */ @Override public void mousePressed(MouseEvent e) { if (!tree.isEnabled()) return; TreePath closestPath = tree.getClosestPathForLocation(e.getX(), e.getY()); if (closestPath == null) return; Rectangle bounds = tree.getPathBounds(closestPath); // Process events outside the immediate bounds - fix for defect // 19 on substance-netbeans. This properly handles Ctrl and Shift // selections on trees. if ((e.getY() >= bounds.y) && (e.getY() < (bounds.y + bounds.height)) && ((e.getX() < bounds.x) || (e.getX() > (bounds.x + bounds.width)))) { // tree.setSelectionPath(closestPath); // fix - don't select a node if the click was on the // expand control if (isLocationInExpandControl(closestPath, e.getX(), e.getY())) return; selectPathForEvent(closestPath, e); } } } /** * Returns the pivot X for the cells rendered in the specified area. Used * for the smart tree scroll ( * {@link SubstanceLookAndFeel#TREE_SMART_SCROLL_ANIMATION_KIND}). * * @param paintBounds * Area bounds. * @return Pivot X for the cells rendered in the specified area */ public int getPivotRendererX(Rectangle paintBounds) { TreePath initialPath = getClosestPathForLocation(tree, 0, paintBounds.y); Enumeration<?> paintingEnumerator = treeState.getVisiblePathsFrom(initialPath); int endY = paintBounds.y + paintBounds.height; int totalY = 0; int count = 0; if (initialPath != null && paintingEnumerator != null) { boolean done = false; Rectangle boundsBuffer = new Rectangle(); Rectangle bounds; TreePath path; Insets insets = tree.getInsets(); while (!done && paintingEnumerator.hasMoreElements()) { path = (TreePath) paintingEnumerator.nextElement(); if (path != null) { bounds = treeState.getBounds(path, boundsBuffer); bounds.x += insets.left; bounds.y += insets.top; int currMedianX = bounds.x;// + bounds.width / 2; totalY += currMedianX; count++; if ((bounds.y + bounds.height) >= endY) done = true; } else { done = true; } } } if (count == 0) return -1; return totalY / count - 2 * SubstanceSizeUtils.getTreeIconSize(SubstanceSizeUtils.getComponentFontSize(tree)); } /** * Returns the current state for the specified path. * * @param pathId * Path index. * @return The current state for the specified path. */ public ComponentState getPathState(TreePathId pathId) { boolean isEnabled = this.tree.isEnabled(); StateTransitionTracker tracker = this.stateTransitionMultiTracker.getTracker(pathId); if (tracker == null) { int rowIndex = this.tree.getRowForPath(pathId.path); boolean isRollover = (this.currRolloverPathId != null) && pathId.equals(this.currRolloverPathId); boolean isSelected = this.tree.isRowSelected(rowIndex); return ComponentState.getState(isEnabled, isRollover, isSelected); } else { ComponentState fromTracker = tracker.getModelStateInfo().getCurrModelState(); return ComponentState.getState(isEnabled, fromTracker.isFacetActive(ComponentStateFacet.ROLLOVER), fromTracker.isFacetActive(ComponentStateFacet.SELECTION)); } } public StateTransitionTracker.ModelStateInfo getModelStateInfo(TreePathId pathId) { if (this.stateTransitionMultiTracker.size() == 0) return null; StateTransitionTracker tracker = this.stateTransitionMultiTracker.getTracker(pathId); if (tracker == null) { return null; } else { return tracker.getModelStateInfo(); } } /* * (non-Javadoc) * * @see javax.swing.plaf.ComponentUI#update(java.awt.Graphics, * javax.swing.JComponent) */ @Override public void update(Graphics g, JComponent c) { BackgroundPaintingUtils.updateIfOpaque(g, c); // Should never happen if installed for a UI if (treeState == null) { return; } // compute the default color scheme - to optimize the performance // SubstanceColorScheme scheme = SubstanceColorSchemeUtilities // .getColorScheme(this.tree, // this.tree.isEnabled() ? ComponentState.DEFAULT // : ComponentState.DISABLED_UNSELECTED); // this.currHashColor = scheme.getLineColor(); this.currDefaultColorScheme = SubstanceColorSchemeUtilities.getColorScheme(tree, ComponentState.ENABLED); Rectangle paintBounds = g.getClipBounds(); Insets insets = tree.getInsets(); TreePath initialPath = getClosestPathForLocation(tree, 0, paintBounds.y); Enumeration<?> paintingEnumerator = treeState.getVisiblePathsFrom(initialPath); int row = treeState.getRowForPath(initialPath); int endY = paintBounds.y + paintBounds.height; // second part - fix for defect 214 (rollover effects on non-opaque // trees resulted in inconsistent behaviour) boolean isWatermarkBleed = SubstanceCoreUtilities.toDrawWatermark(tree) || !tree.isOpaque(); Graphics2D g2d = (Graphics2D) g.create(); RenderingUtils.installDesktopHints(g2d, c); SubstanceStripingUtils.setup(c); if (initialPath != null && paintingEnumerator != null) { boolean done = false; Rectangle boundsBuffer = new Rectangle(); Rectangle bounds; TreePath path; while (!done && paintingEnumerator.hasMoreElements()) { path = (TreePath) paintingEnumerator.nextElement(); if (path != null) { // respect the background color of the renderer. boolean isLeaf = treeModel.isLeaf(path.getLastPathComponent()); boolean isExpanded = isLeaf ? false : treeState.getExpandedState(path); Component renderer = this.currentCellRenderer.getTreeCellRendererComponent( this.tree, path.getLastPathComponent(), this.tree.isRowSelected(row), isExpanded, isLeaf, row, tree.hasFocus() ? (tree.getLeadSelectionRow() == row) : false); Color background = renderer.getBackground(); if (background == null) background = tree.getBackground(); bounds = treeState.getBounds(path, boundsBuffer); bounds.x += insets.left; bounds.y += insets.top; if (!isWatermarkBleed) { g2d.setColor(background); g2d.fillRect(paintBounds.x, bounds.y, paintBounds.width, bounds.height); } else { if (this.tree.getComponentOrientation().isLeftToRight()) { BackgroundPaintingUtils.fillAndWatermark(g2d, this.tree, background, new Rectangle(paintBounds.x, bounds.y, paintBounds.width, bounds.height)); } else { BackgroundPaintingUtils.fillAndWatermark(g2d, this.tree, background, new Rectangle(paintBounds.x, bounds.y, paintBounds.width, bounds.height)); } } if ((bounds.y + bounds.height) >= endY) done = true; } else { done = true; } row++; } } this.paint(g2d, c); SubstanceStripingUtils.tearDown(c); g2d.dispose(); } // /* // * (non-Javadoc) // * // * @see javax.swing.plaf.basic.BasicTreeUI#getHashColor() // */ // @Override // protected Color getHashColor() { // return this.currHashColor; // } /** * Returns the default color scheme of this tree. Is for internal use only. * * @return The default color scheme of this tree. */ public SubstanceColorScheme getDefaultColorScheme() { return this.currDefaultColorScheme; } /** * Returns the cell renderer insets of this tree. Is for internal use only. * * @return The cell renderer insets of this tree. */ public Insets getCellRendererInsets() { return cellRendererInsets; } @Override public Rectangle getPathBounds(JTree tree, TreePath path) { Rectangle result = super.getPathBounds(tree, path); if (result != null) { if (tree.getComponentOrientation().isLeftToRight()) { result.width = tree.getWidth() - tree.getInsets().right - result.x; } else { int delta = result.x - tree.getInsets().left; result.x -= delta; result.width += delta; } } return result; } private StateTransitionTracker getTracker(final TreePathId pathId, boolean initialRollover, boolean initialSelected) { StateTransitionTracker tracker = stateTransitionMultiTracker.getTracker(pathId); if (tracker == null) { ButtonModel model = new DefaultButtonModel(); model.setSelected(initialSelected); model.setRollover(initialRollover); tracker = new StateTransitionTracker(this.tree, model); tracker.registerModelListeners(); tracker.setRepaintCallback(() -> new PathRepaintCallback(tree, pathId.path)); stateTransitionMultiTracker.addTracker(pathId, tracker); } return tracker; } public StateTransitionTracker getStateTransitionTracker(TreePathId pathId) { return this.stateTransitionMultiTracker.getTracker(pathId); } }