diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeExpansionAnimationListener.java b/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeExpansionAnimationListener.java new file mode 100644 index 00000000..df56c571 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeExpansionAnimationListener.java @@ -0,0 +1,113 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.ui.tree; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeExpansionListener; +import javax.swing.tree.TreePath; + +import com.github.weisj.darklaf.graphics.Animator; + +public class DarkTreeExpansionAnimationListener implements TreeExpansionListener { + + private final JTree tree; + private final TreeStateAnimator animator; + + public DarkTreeExpansionAnimationListener(final JTree tree) { + this.tree = tree; + this.animator = new TreeStateAnimator(); + animator.setEnabled(UIManager.getBoolean("Tree.iconAnimations")); + tree.addTreeExpansionListener(this); + } + + @Override + public void treeExpanded(final TreeExpansionEvent event) { + startAnimation(event.getPath()); + } + + @Override + public void treeCollapsed(final TreeExpansionEvent event) { + startAnimation(event.getPath()); + } + + public void startAnimation(final TreePath path) { + if (!animator.isEnabled()) return; + animator.state = 0; + animator.path = path; + animator.animationRow = tree.getRowForPath(path); + boolean running = animator.isRunning(); + animator.suspend(); + if (running) { + // Forces paintCycleEnd to be called. + animator.resume(animator.getTotalFrames()); + } + animator.resume(0, tree); + } + + public TreePath getAnimationPath() { + return animator.path; + } + + public float getAnimationState() { + return animator.state; + } + + protected class TreeStateAnimator extends Animator { + + private static final int DURATION = 60; + private static final int RESOLUTION = 10; + + private TreePath path; + private float state; + private int animationRow; + + public TreeStateAnimator() { + super(DURATION / RESOLUTION, DURATION, 0); + } + + private void repaint() { + if (animationRow >= 0) { + Rectangle bounds = tree.getRowBounds(animationRow); + bounds.x = 0; + bounds.width = tree.getWidth(); + tree.paintImmediately(bounds); + } + } + + @Override + public void paintNow(final float fraction) { + state = fraction; + repaint(); + } + + @Override + protected void paintCycleEnd() { + super.paintCycleEnd(); + state = 1; + repaint(); + animationRow = -1; + } + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java b/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java index 061ec31c..e9a46e06 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java @@ -32,12 +32,10 @@ import javax.swing.*; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicTreeUI; -import javax.swing.tree.DefaultTreeCellRenderer; -import javax.swing.tree.TreeCellEditor; -import javax.swing.tree.TreeCellRenderer; -import javax.swing.tree.TreePath; +import javax.swing.tree.*; import com.github.weisj.darklaf.graphics.PaintUtil; +import com.github.weisj.darklaf.icons.RotatableIcon; import com.github.weisj.darklaf.ui.cell.CellConstants; import com.github.weisj.darklaf.ui.cell.CellUtil; import com.github.weisj.darklaf.ui.cell.DarkCellRendererPane; @@ -66,6 +64,7 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C public static final String KEY_IS_TABLE_TREE = "JComponent.isTableTree"; protected static final Rectangle boundsBuffer = new Rectangle(); + protected static final RotatableIcon paintingIcon = new RotatableIcon(); protected MouseListener selectionListener; protected Color lineColor; @@ -92,6 +91,7 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C private int dashLength; private int dashGapLength; + private DarkTreeExpansionAnimationListener treeExpansionAnimationListener; public static ComponentUI createUI(final JComponent c) { return new DarkTreeUI(); @@ -162,6 +162,12 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C tree.addPropertyChangeListener(this); selectionListener = createMouseSelectionListener(); tree.addMouseListener(selectionListener); + this.treeExpansionAnimationListener = createExpansionAnimationListener(); + tree.addTreeExpansionListener(treeExpansionAnimationListener); + } + + protected DarkTreeExpansionAnimationListener createExpansionAnimationListener() { + return new DarkTreeExpansionAnimationListener(tree); } protected MouseListener createMouseSelectionListener() { @@ -247,6 +253,8 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C tree.removeMouseListener(selectionListener); selectionListener = null; tree.removePropertyChangeListener(this); + tree.removeTreeExpansionListener(treeExpansionAnimationListener); + treeExpansionAnimationListener = null; } @Override @@ -254,6 +262,7 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C if (tree != c) { throw new InternalError("incorrect component"); } + // Should never happen if installed for a UI if (treeState == null) { return; @@ -270,7 +279,8 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C boolean done = false; while (!done && paintingEnumerator.hasMoreElements()) { TreePath path = (TreePath) paintingEnumerator.nextElement(); - if (!paintSingleRow(g, paintBounds, insets, path, row)) { + Rectangle cellBounds = paintSingleRow(g, paintBounds, insets, path, row); + if (cellBounds == null || (cellBounds.y + cellBounds.height) >= paintBounds.y + paintBounds.height) { done = true; } row++; @@ -293,15 +303,15 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C g.translate(0, rowBounds.y); } - protected boolean paintSingleRow(final Graphics g, final Rectangle paintBounds, final Insets insets, + protected Rectangle paintSingleRow(final Graphics g, final Rectangle paintBounds, final Insets insets, final TreePath path, final int row) { - if (path == null) return false; + if (path == null) return null; final int xOffset = tree.getParent() instanceof JViewport ? ((JViewport) tree.getParent()).getViewPosition().x : 0; final int containerWidth = tree.getParent() instanceof JViewport ? tree.getParent().getWidth() : tree.getWidth(); final Rectangle cellBounds = getPathBounds(path, insets, boundsBuffer); - if (cellBounds == null) return false; + if (cellBounds == null) return null; final int boundsX = cellBounds.x; final int boundsWidth = cellBounds.width; @@ -333,7 +343,7 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C PaintUtil.drawRect(g, cellBounds, leadSelectionBorderInsets); } - return (cellBounds.y + cellBounds.height) < paintBounds.y + paintBounds.height; + return cellBounds; } protected void paintRowBackground(final Graphics g, final Rectangle bounds, final TreePath path, final int row, @@ -519,11 +529,27 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C int iconCenterY = bounds.y + (bounds.height / 2); if (isExpanded) { - Icon expandedIcon = getExpandedIcon(); - if (expandedIcon != null) drawCentered(tree, g, expandedIcon, iconCenterX, iconCenterY); + Icon expIcon = getExpandedIcon(); + if (expIcon != null) { + if (Objects.equals(treeExpansionAnimationListener.getAnimationPath(), path)) { + paintingIcon.setIcon(expIcon); + paintingIcon + .setRotation((treeExpansionAnimationListener.getAnimationState() - 1) * Math.PI / 2.0); + expIcon = paintingIcon; + } + drawCentered(tree, g, expIcon, iconCenterX, iconCenterY); + } } else { - Icon collapsedIcon = getCollapsedIcon(); - if (collapsedIcon != null) drawCentered(tree, g, collapsedIcon, iconCenterX, iconCenterY); + Icon collIcon = getCollapsedIcon(); + if (collIcon != null) { + if (Objects.equals(treeExpansionAnimationListener.getAnimationPath(), path)) { + paintingIcon.setIcon(collIcon); + paintingIcon + .setRotation((1 - treeExpansionAnimationListener.getAnimationState()) * Math.PI / 2.0); + collIcon = paintingIcon; + } + drawCentered(tree, g, collIcon, iconCenterX, iconCenterY); + } } } } diff --git a/core/src/main/resources/com/github/weisj/darklaf/properties/overwrites.properties b/core/src/main/resources/com/github/weisj/darklaf/properties/overwrites.properties index 9b9e7725..9c9f3ff3 100644 --- a/core/src/main/resources/com/github/weisj/darklaf/properties/overwrites.properties +++ b/core/src/main/resources/com/github/weisj/darklaf/properties/overwrites.properties @@ -35,3 +35,4 @@ ToolTip.defaultStyle = tooltipStyle TitlePane.unifiedMenuBar = unifiedMenuBar ToggleButton.animated = animations ScrollBar.animated = animations +Tree.iconAnimations = animations diff --git a/core/src/main/resources/com/github/weisj/darklaf/properties/ui/tree.properties b/core/src/main/resources/com/github/weisj/darklaf/properties/ui/tree.properties index 761b986a..e38c6f6b 100644 --- a/core/src/main/resources/com/github/weisj/darklaf/properties/ui/tree.properties +++ b/core/src/main/resources/com/github/weisj/darklaf/properties/ui/tree.properties @@ -34,6 +34,7 @@ Tree.hash = %borderFocus Tree.lineFocusSelected = %borderFocus Tree.lineSelected = %gridLine Tree.lineUnselected = %gridLine +Tree.iconAnimations = true Tree.background = %Cell.background Tree.backgroundAlternative = %Cell.backgroundAlternative @@ -84,12 +85,12 @@ Tree.leafIcon = files/general.svg[themed] Tree.collapsed.selected.focused.icon = navigation/arrow/thin/arrowRightSelected.svg[themed] Tree.collapsed.selected.unfocused.icon = navigation/arrow/thin/arrowRight.svg[themed] -Tree.collapsed.unselected.focused.icon = navigation/arrow/thin/arrowRight.svg[themed] -Tree.collapsed.unselected.unfocused.icon = navigation/arrow/thin/arrowRight.svg[themed] -Tree.collapsed.disabled.icon = navigation/arrow/thin/arrowRightDisabled.svg[themed] +Tree.collapsed.unselected.focused.icon = navigation/arrow/thin/arrowRight.svg[themed] +Tree.collapsed.unselected.unfocused.icon = navigation/arrow/thin/arrowRight.svg[themed] +Tree.collapsed.disabled.icon = navigation/arrow/thin/arrowRightDisabled.svg[themed] -Tree.expanded.selected.focused.icon = navigation/arrow/thin/arrowDownSelected.svg[themed] -Tree.expanded.selected.unfocused.icon = navigation/arrow/thin/arrowDown.svg[themed] -Tree.expanded.unselected.focused.icon = navigation/arrow/thin/arrowDown.svg[themed] -Tree.expanded.unselected.unfocused.icon = navigation/arrow/thin/arrowDown.svg[themed] -Tree.expanded.disabled.icon = navigation/arrow/thin/arrowRightDisabled.svg[themed] +Tree.expanded.selected.focused.icon = navigation/arrow/thin/arrowDownSelected.svg[themed] +Tree.expanded.selected.unfocused.icon = navigation/arrow/thin/arrowDown.svg[themed] +Tree.expanded.unselected.focused.icon = navigation/arrow/thin/arrowDown.svg[themed] +Tree.expanded.unselected.unfocused.icon = navigation/arrow/thin/arrowDown.svg[themed] +Tree.expanded.disabled.icon = navigation/arrow/thin/arrowDownDisabled.svg[themed]