/* * CaroselLayout.java * * Created on November 21, 2006, 11:48 AM * * Copyright 2006-2007 Nigel Hughes * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at http://www.apache.org/ * licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package org.pushingpixels.lafwidget.contrib.blogofbug.swing.layout; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.*; import javax.swing.Timer; import org.pushingpixels.lafwidget.contrib.blogofbug.swing.components.RichComponent; /** * Layout engine for JCarousel components (although would work for any container). * It does have a closer than usual relationship with the container, sometimes causing the container to repaint. * @author bug */ public class CaroselLayout implements LayoutManager,ActionListener{ /** * Number of items in the carousel (that are visible) */ protected int numberOfItems = 0; /** * List of components being laid out */ protected LinkedList<Component> components = new LinkedList<Component>(); /** * List of additional information held on components in the carousel */ protected Hashtable additionalData = new Hashtable(); /** * The current degree of rotation of the carousel */ protected double rotationalOffset = 0.0; /** * The desired rotational offset, which will be moved to by a timer animating the carousel */ protected double targetOffset = 0.0; /** * Time for driving animations */ private Timer animationTimer = null; /** * The container the layout is... laying out */ private Container container = null; /** * Should items furtehr away from the observer be faded out? */ private boolean depthBasedAlpha = true; /** * The width of a component when the scale is 1.0 */ private int neutralContentWidth = 64; /** * Creates a new instance of the layout engine, tied to the specified container. * @param forContainer The container the layout will layout */ public CaroselLayout(Container forContainer){ animationTimer = new Timer(0,this); container = forContainer; } /** * Specify the neutral content width of any laid out component. * @param neutralContentWidth The neutral width of components */ public void setNeutralContentWidth(int neutralContentWidth) { this.neutralContentWidth = neutralContentWidth; } /** * Moves a layout component at a particular location in the * carousel * * @param i The location at which to insert * @param comp The component to insert * */ public void moveComponentTo(int i, Component comp){ components.remove(comp); components.add(i,comp); recalculateCarosel(); } /** * Name is ignored * @param name The name of the component, ignored. * @param comp The component being added */ public void addLayoutComponent(String name, Component comp) { components.addLast(comp); recalculateCarosel(); } /** * Remove the component * @param comp The component being removed */ public void removeLayoutComponent(Component comp) { components.remove(comp); recalculateCarosel(); } /** * Gets the additional data stored by the layout manager for a given component * * @param comp The component you wish retreive the data for * @return A position, which is added if it does not already exist. Never null unless * you run out of memory! */ protected CaroselPosition getPosition(Component comp){ CaroselPosition cpos = (CaroselPosition) additionalData.get(comp); if (cpos==null){ cpos = new CaroselPosition(comp); additionalData.put(comp,cpos); } return cpos; } /** * Determines how many of the items being laid out are currently visible. * @return How many of the items in the carousel are currently visible. */ protected int recalculateVisibleItems(){ int visibleItems=0; try{ for (Component comp : components){ if (comp.isVisible()){ visibleItems++; } } } catch (ConcurrentModificationException ex){ return recalculateVisibleItems(); } return visibleItems; } /** * Updates all of the positions of the carousel. Does not do a repaint, just does the math ready for the next one. */ protected void recalculateCarosel(){ //Need to count visible, not just how many in the list //Again dealing with out-of-EDT modification numberOfItems = recalculateVisibleItems(); //Trap and re-calc on concurrent modification (might as well be up-to-date) try{ boolean animate=false; double itemCount = 0; for (Component comp : components){ CaroselPosition position = getPosition(comp); if (comp.isVisible()){ double localAngle = itemCount * (Math.PI * 2.0 / (double) numberOfItems); position.setAngle(localAngle); } if (position.isAnimating()){ animate=true; } itemCount+=1.0; } //If we do need to animate, get it started if (animate){ animationTimer.start(); } } catch (ConcurrentModificationException ex){ recalculateCarosel(); return; } } /** * Cheats and bases it's size on the prefered sizes of each component * @param parent The container interested in the layout size * @return The minimum size of the layout. See above. */ public Dimension minimumLayoutSize(Container parent) { return preferredLayoutSize(parent); } /** * Determine the widest and tallest dimensions, then return the height as 1.5 * the highest, and 3 * the widest * @param parent The container for the layout * @return The prefered size of the layout */ public Dimension preferredLayoutSize(Container parent) { Dimension dim = new Dimension(0, 0); // get widest preferred width for left && right // get highest preferred height for left && right // add preferred width of middle int widestWidth = 0; int highestHeight = 0; Iterator i = components.iterator(); while (i.hasNext()){ Component comp = (Component) i.next(); if (comp.isVisible()){ widestWidth = Math.max(widestWidth, comp.getPreferredSize().width); highestHeight = Math.max(highestHeight, comp.getPreferredSize().height); } } dim.width = widestWidth * 3; dim.height = highestHeight * 2; Insets insets = parent.getInsets(); dim.width += insets.left + insets.right; dim.height += insets.top + insets.bottom; return dim; } /** * Determines the center of the carousel * @param insets The insets of the container * @param width The width of the container * * @param height The height of the container * @param widest The widest component * @return A point at the center of the carousel */ protected Point calculateCenter(Insets insets, int width, int height, int widest){ return new Point((insets.left+widest/2) + width/2, insets.top + height/2); } /** * Controls if items should fade as they move to the back of the carousel * @param depthBasedAlpha True if they should fade, false if they shouldn't */ public void setDepthBasedAlpha(boolean depthBasedAlpha) { this.depthBasedAlpha = depthBasedAlpha; } /** * Can be over-ridden to restrict the range of angles where the child component * is shown * @return false if the component should not be shown * @param comp Controls if components are hidden or not, in the case of this layout it always returns false * @param angle The angle of the component under consideration * @param s The scale of the component under consideration */ protected boolean shouldHide(Component comp, double angle, double s){ if (depthBasedAlpha){ if (comp instanceof RichComponent){ s = Math.min(1.0f,Math.max(0.0f,s/1.2f)); ((RichComponent) comp).setAlpha((float)s); } } return false; } /** * Determines the correct size of the carousel for the container * @param target The target container * @param insets Insets into the target container * @param width Width of the target container * @param height Height of the target container * @param widestComponent The widest component in the container * @return The */ protected Dimension getCarouselRadius(Container target, Insets insets, int width, int height, int widestComponent){ return null; } /** * Determines the scale to be applied to the component. The default calculation * divides the y co-ordinate by the y-cordinate of the centre. Other implimentations * may use some of the other parameters * * @param angle The angle of the component * @param x The x-position of the component * @param y The y-position of the component * @param carouselX The x centre of the carousel * @param carouselY The y centre of the carousel * @return A double which will be used to scale x and y co-ordinates */ protected double getScale(double angle, double x, double y, double carouselX, double carouselY){ return (y / carouselY); } /** * Lays out all of the components on the carosel. Using the preferred width and height to base * scaling on * @param target The container currently being laid out */ public void layoutContainer(Container target) { //Let's make a local copy of components to avoid concurrent modification //which could happen if someone adds something to the layout outside //of the EDT. This is faster than do any synchronization or brute force //exception catching LinkedList<Component> components = (LinkedList) this.components.clone(); int numberOfItems = this.numberOfItems; recalculateCarosel(); // these variables hold the position where we can draw components // taking into account insets Insets insets = target.getInsets(); int width = target.getSize().width - (insets.left + insets.right); int height = target.getSize().height - (insets.top + insets.bottom); //No longer calculate the width based on prefered sizes, we're going to control //it by the component not the content' int widestWidth = neutralContentWidth; width -= widestWidth; int radiusX = width /2; int radiusY = radiusX/3; Dimension radius = getCarouselRadius(target,insets,width,height,widestWidth); if (radius!=null){ radiusX = radius.width; radiusY = radius.height; } Point center = calculateCenter(insets,width,height,widestWidth); int centerX = center.x; int centerY = center.y; //Go through each visible component and set the scale and z-order, and eventually the bounds //Need to protected against other things adding components at the same time Iterator i = components.iterator(); int p = 0; CaroselPosition z_order[] = new CaroselPosition[numberOfItems]; while (i.hasNext()){ Component comp = (Component) i.next(); CaroselPosition position = getPosition(comp); double finalAngle = position.getAngle()+this.rotationalOffset; double x = (Math.sin(finalAngle) * (double) radiusX)+(double) centerX; double y = (Math.cos(finalAngle) * (double) radiusY)+(double) centerY; double initialWidth = (double) comp.getPreferredSize().width; double initialHeight = ((double) comp.getPreferredSize().height) * (initialWidth / (double) comp.getPreferredSize().width); double s = getScale(finalAngle, x,y,(double) centerX, (double) centerY);//(y / (double) centerY); double boundsWidth = initialWidth * s; double boundsHeight = initialHeight * s; if (!shouldHide(comp, finalAngle,s)){ //Even scaling only to avoid windows jitter... int finalWidth = (int)boundsWidth / 1; int finalHeight = (int)boundsHeight / 1; finalWidth = (int) finalWidth & 0xFFFFFFFE; finalHeight = (int) finalHeight & 0xFFFFFFFE; comp.setBounds((int)x - ((int)boundsWidth/2),(int) y - ((int)boundsHeight /2),finalWidth, finalHeight); } else { comp.setBounds(-100,-100,32,32); } position.setZ(s); z_order[p++] = position; } //Now sort out the z, we may need to cache the dimensions, do the z and then reset the bounds, see what happens on redraw first //bubble sort is actually very fast for a small number of items, and this layout shouldn't be used for loads. boolean swaps = true; int limit = numberOfItems-1; while (swaps){ swaps = false; for (int j=0;j<limit;j++){ if (z_order[j].getZ()<z_order[j+1].getZ()){ CaroselPosition temp = z_order[j+1]; z_order[j+1]=z_order[j]; z_order[j]=temp; swaps=true; } } limit--; //We must be done if we hit the bottom if (limit==0){ swaps=false; } } //Re-order everything (yet as little as possible :-) for (int j=0;j<numberOfItems;j++){ if (target.getComponentZOrder(z_order[j].getComponent())!=j){ target.setComponentZOrder(z_order[j].getComponent(),j); } } } /** * Returns the current rotational angle * * @return The current rotated angle in radians */ public double getAngle() { return this.rotationalOffset; } /** * Sets the current rotational angle. Will not cause an animation to start * * @param d The desired angle in radians */ public void setAngle(double d) { this.rotationalOffset = d; } /** * Determines if an animation is currently playing * * @return true if it is animating, false if it isn't */ protected boolean isAnimating(){ if (!animationTimer.isRunning()){ return false; } try{ for (Component comp : components) { CaroselPosition cpos = getPosition(comp); if (cpos.isAnimating()){ return true; } } } catch (ConcurrentModificationException ex){ return isAnimating(); } if (Math.abs(rotationalOffset - targetOffset) < 0.001){ return false; } else { return true; } } /** * Manages timer actions, terminating the timer if any event is fully achieved * * @param actionEvent the action event, although this will always be the timer */ public void actionPerformed(ActionEvent actionEvent) { if (animationTimer==null){ return; } if (!animationTimer.isRunning()){ return; } if (!isAnimating()){ animationTimer.stop(); return; } //Update any animating icons, could be subject to modification //outside the EDT try { for (Component comp : components) { CaroselPosition cpos = getPosition(comp); if (cpos.isAnimating()){ cpos.updateAngle(); } } } catch (ConcurrentModificationException cMe){ actionPerformed(actionEvent); } rotationalOffset += (targetOffset - rotationalOffset) / 6.0; if (container!=null){ this.layoutContainer(container); if (container instanceof Component){ ((Component) container).repaint(); } } } /** * Moves everything to their "target" positions, without animating anything * */ public void finalizeLayoutImmediately(){ for (Component comp : components){ CaroselPosition cpos = getPosition(comp); cpos.angle=cpos.targetAngle; } rotationalOffset = targetOffset; recalculateCarosel(); container.validate(); } /** * Sets a target angle to rotate to, always choses a direction that is less than * or equal to 180 degrees * * @param target The target angle in radians */ protected final void setTarget(double target){ //We should never have to rotate more than PI radians while (Math.abs(target-rotationalOffset) > Math.PI){ if (target<rotationalOffset){ target += Math.PI * 2; } else { target -= Math.PI * 2; } } targetOffset = target; if (!animationTimer.isRunning()){ animationTimer.setCoalesce(true); animationTimer.setRepeats(true); animationTimer.setDelay(20); animationTimer.start(); } } /** * Moves the specified component to the front * * @param component The component move to the front */ public void setFrontMostComponent(Component component){ setTarget(-getPosition(component).getTargetAngle()); } /** * Retrieve the component before the specified one. It does not filter out invisible ones. * @param component The component you are looking for the one before for. * @return The component before it */ public Component getPreviousComponent(Component component) { int i = this.components.indexOf(component)-1; if (i<0){ return components.get(components.size()-1); } else { return components.get(i); } } /** * Retrieve the component after the specified one. It does not filter out invisible ones. * @param component The component * @return The one after it */ public Component getNextComponent(Component component) { int i = this.components.indexOf(component)+1; if (i>=components.size()){ return components.get(0); } else { return components.get(i); } } /** * The number of components being laid out. Does not included hidden ones * @return The number of components */ public int getComponentCount(){ return components.size(); } /** * Gets the index of the supplied component * @param comp The component * @return The index */ public int getComponentIndex(Component comp) { return components.indexOf(comp); } /** * The size of comopnents a neutral width * @return The size of components at neutral width (scale 1.0) */ public int getNeutralContentWidth() { return neutralContentWidth; } class CaroselPosition{ protected double angle; protected double scale; protected double z; protected Component component; protected boolean firstSet = false; protected double targetAngle = 0.0; public CaroselPosition(Component component){ angle = 0.0; scale = 0.0; z = 0.0; this.component = component; } public Component getComponent(){ return component; } public double getZ(){ return z; } public void setZ(double z){ this.z = z; } public double getTargetAngle(){ return targetAngle; } public double getAngle(){ return angle; } public double getScale(){ return scale; } public boolean isAnimating(){ if ((Math.abs(angle - targetAngle) < 0.001)){ return false; } return true; } public void moveToTarget(){ angle=targetAngle; } public void updateAngle(){ if ((Math.abs(angle - targetAngle) < 0.001)){ angle = targetAngle; } else { angle += Math.min((targetAngle - angle) / 6.0,0.10); } } public void setAngle(double angle){ if (firstSet){ this.angle = angle; this.targetAngle = angle; firstSet = false; } else { this.targetAngle = angle; } } public void setScale(double scale){ this.scale = scale; } } }