/******************************************************************************* * Breakout Cave Survey Visualizer * * Copyright (C) 2014 James Edwards * * jedwards8 at fastmail dot fm * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program 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 for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *******************************************************************************/ package org.andork.awt.layout; import java.awt.Component; import java.awt.Container; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.SwingUtilities; import javax.swing.Timer; import org.andork.awt.layout.DelegatingLayoutManager.LayoutDelegate; import org.andork.event.BasicPropertyChangeSupport; public class DrawerLayoutDelegate implements LayoutDelegate { public static final String OPEN = "open"; public static final String MAXIMIZED = "maximized"; boolean open = false; boolean maximized = false; boolean animating = false; Corner dockingCorner; Side dockingSide; float animFactor = .2f; int animSpeed = 10; private long lastAnimTime; private Timer animTimer; Component drawer; boolean fill = false; private Map<Side, SideConstraint> extraConstraints = new HashMap<>(); private BasicPropertyChangeSupport changeSupport = new BasicPropertyChangeSupport(); boolean bypass = false; public DrawerLayoutDelegate(Component drawer, Corner dockingCorner, Side dockingSide) { this(drawer, dockingCorner, dockingSide, false); } private DrawerLayoutDelegate(Component drawer, Corner dockingCorner, Side dockingSide, boolean fill) { super(); if (dockingCorner != null && dockingSide != dockingCorner.xSide() && dockingSide != dockingCorner.ySide()) { throw new IllegalArgumentException("dockingCorner must be on the same side as dockingSide"); } this.drawer = drawer; this.dockingCorner = dockingCorner; this.dockingSide = dockingSide; this.fill = fill; } public DrawerLayoutDelegate(Component drawer, Side dockingSide) { this(drawer, null, dockingSide, true); } public DrawerLayoutDelegate(Component drawer, Side dockingSide, boolean fill) { this(drawer, null, dockingSide, fill); } private void applyConstraints(final Component target, Rectangle targetBounds) { for (Map.Entry<Side, SideConstraint> entry : extraConstraints.entrySet()) { Side side = entry.getKey(); SideConstraint constraint = entry.getValue(); if (dockingSide != null && side.axis() == dockingSide.axis()) { continue; } if (dockingCorner != null && dockingCorner.side(side.axis()) == side) { continue; } Point p = new Point(); side.axis().set(p, constraint.location()); p = SwingUtilities.convertPoint(constraint.targetComponent.getParent(), p, target.getParent()); side.stretch(targetBounds, side.axis().get(p)); } } public BasicPropertyChangeSupport.External changeSupport() { return changeSupport.external(); } public void close() { setOpen(false, true); } public void close(boolean animate) { setOpen(false, animate); } @Override public Rectangle desiredBounds(Container parent, Component target, LayoutSize layoutSize) { return getBounds(parent, target, layoutSize, true, maximized); } public Corner dockingCorner() { return dockingCorner; } public DrawerLayoutDelegate dockingCorner(Corner dockingCorner) { this.dockingCorner = dockingCorner; return this; } public Side dockingSide() { return dockingSide; } public DrawerLayoutDelegate dockingSide(Side dockingSide) { this.dockingSide = dockingSide; return this; } public SideConstraint extraConstraint(Side side) { return extraConstraints.get(side); } public boolean fill() { return fill; } public DrawerLayoutDelegate fill(boolean fill) { this.fill = fill; return this; } private Rectangle getBounds(Container parent, Component target, LayoutSize layoutSize, boolean open, boolean maximized) { Rectangle bounds = new Rectangle(); bounds.setSize(layoutSize.get(target)); if (dockingCorner != null) { Insets insets = parent.getInsets(); Side otherSide = dockingCorner.xSide() == dockingSide ? dockingCorner.ySide() : dockingCorner.xSide(); otherSide.setLocation(bounds, otherSide.insetLocalLocation(parent)); if (maximized) { bounds.width = parent.getWidth() - insets.left - insets.right; bounds.height = parent.getHeight() - insets.top - insets.bottom; } else { bounds.width = Math.min(bounds.width, parent.getWidth() - insets.left - insets.right); bounds.height = Math.min(bounds.height, parent.getHeight() - insets.top - insets.bottom); } if (open) { dockingSide.setLocation(bounds, dockingSide.insetLocalLocation(parent)); } else { dockingSide.opposite().setLocation(bounds, dockingSide.localLocation(parent)); } } else { Side invSide = dockingSide.inverse(); Axis axis = dockingSide.axis(); Axis invAxis = invSide.axis(); if (fill || maximized) { invAxis.setSize(bounds, invAxis.insetSize(parent)); invAxis.setLower(bounds, invAxis.lowerInset(parent)); } else { invAxis.setLower(bounds, invAxis.insetLocalCenter(parent) - invAxis.size(bounds) / 2); } if (maximized) { axis.setSize(bounds, axis.insetSize(parent)); } else { axis.setSize(bounds, Math.min(axis.size(bounds), axis.insetSize(parent))); } if (open) { dockingSide.setLocation(bounds, dockingSide.insetLocalLocation(parent)); } else { dockingSide.opposite().setLocation(bounds, dockingSide.localLocation(parent)); } } return bounds; } @Override public List<Component> getDependencies() { if (extraConstraints.isEmpty()) { return Collections.emptyList(); } List<Component> result = new ArrayList<Component>(extraConstraints.size()); for (SideConstraint constraint : extraConstraints.values()) { result.add(constraint.targetComponent); } return result; } public Component getDrawer() { return drawer; } public boolean isMaximized() { return maximized; } public boolean isOpen() { return open; } @Override public void layoutComponent(final Container parent, final Component target) { if (bypass) { return; } Rectangle targetBounds = getBounds(parent, target, LayoutSize.PREFERRED, open, maximized); applyConstraints(target, targetBounds); Rectangle bounds = target.getBounds(); applyConstraints(target, bounds); if (animating) { if (targetBounds.equals(bounds)) { animating = false; if (animTimer != null) { animTimer.stop(); animTimer = null; } } else { long time = System.currentTimeMillis(); long elapsed = time - lastAnimTime; lastAnimTime = time; if (animTimer == null) { elapsed = animSpeed; } RectangleUtils.animate(bounds, targetBounds, elapsed, animFactor, 10, animSpeed, bounds); applyConstraints(target, bounds); target.setBounds(bounds); bypass = true; try { onLayoutAnimated(parent, target); } finally { bypass = false; } if (animTimer == null) { animTimer = new Timer(animSpeed, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { parent.invalidate(); parent.validate(); } }); animTimer.start(); } } } else { target.setBounds(targetBounds); } } public void maximize() { setMaximized(true, true); } public void maximize(boolean animate) { setMaximized(true, animate); } protected void onLayoutAnimated(Container parent, Component target) { if (parent != null && parent.getLayout() instanceof DelegatingLayoutManager) { ((DelegatingLayoutManager) parent.getLayout()).onLayoutChanged(parent); } else { parent.invalidate(); parent.validate(); } } public void open() { setOpen(true, true); } public void open(boolean animate) { setOpen(true, animate); } public DrawerLayoutDelegate putExtraConstraint(Side side, SideConstraint constraint) { extraConstraints.put(side, constraint); return this; } public void restore() { setMaximized(false, true); } public void restore(boolean animate) { setMaximized(false, animate); } public void setMaximized(boolean maximized) { setMaximized(maximized, true); } public void setMaximized(boolean maximized, boolean animate) { if (this.maximized != maximized) { toggleMaximized(animate); } } public void setOpen(boolean open) { setOpen(open, true); } public void setOpen(boolean open, boolean animate) { if (this.open != open) { toggleOpen(animate); } } public void toggleMaximized() { toggleMaximized(true); } public void toggleMaximized(boolean animate) { maximized = !maximized; animating = animate; if (drawer.getParent() != null) { drawer.getParent().invalidate(); drawer.getParent().validate(); } changeSupport.firePropertyChange(this, MAXIMIZED, !maximized, maximized); } public void toggleOpen() { toggleOpen(true); } public void toggleOpen(boolean animate) { open = !open; animating = animate; if (drawer.getParent() != null) { drawer.getParent().invalidate(); drawer.getParent().validate(); } changeSupport.firePropertyChange(this, OPEN, !open, open); } }