/* * Bibliothek - DockingFrames * Library built on Java/Swing, allows the user to "drag and drop" * panels containing any Swing-Component the developer likes to add. * * Copyright (C) 2011 Benjamin Sigg * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Benjamin Sigg * benjamin_sigg@gmx.ch * CH - Switzerland */ package bibliothek.gui.dock.station.screen.magnet; import java.awt.Rectangle; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import bibliothek.gui.DockController; import bibliothek.gui.Dockable; import bibliothek.gui.dock.ScreenDockStation; import bibliothek.gui.dock.station.screen.ScreenDockWindow; import bibliothek.gui.dock.station.screen.magnet.AttractorStrategy.Attraction; import bibliothek.gui.dock.station.screen.magnet.MagnetRequest.Side; import bibliothek.gui.dock.util.PropertyValue; import bibliothek.gui.dock.util.SilentPropertyValue; import bibliothek.util.FrameworkOnly; /** * Each {@link ScreenDockStation} uses one {@link MagnetController} to calculate attractions * between its children. The {@link MagnetController} makes use of a {@link MagnetStrategy} * and of several {@link AttractorStrategy}s to modify the location and size of the currently * moved {@link ScreenDockWindow}.<br> * {@link ScreenDockWindow}s have to call {@link #start(ScreenDockWindow)} when they start * moving or resizing. * @author Benjamin Sigg */ public class MagnetController { /** the owner of this controller */ private ScreenDockStation station; /** the currently executed operation */ private Operation current; /** the current strategy to calculate the new boundaries */ private PropertyValue<MagnetStrategy> strategy = new PropertyValue<MagnetStrategy>( ScreenDockStation.MAGNET_STRATEGY ){ @Override protected void valueChanged( MagnetStrategy oldValue, MagnetStrategy newValue ){ if( oldValue != null ){ oldValue.uninstall( MagnetController.this ); } if( newValue != null ){ newValue.install( MagnetController.this ); } } }; /** the currently used {@link AttractorStrategy} */ private PropertyValue<AttractorStrategy> attraction = new SilentPropertyValue<AttractorStrategy>( ScreenDockStation.ATTRACTOR_STRATEGY ); /** the currently used {@link DockController} */ private DockController controller; /** * Creates a new {@link MagnetController}. * @param station the station using this controller, not <code>null</code> */ public MagnetController( ScreenDockStation station ){ if( station == null ){ throw new IllegalArgumentException( "station must not be null" ); } this.station = station; } /** * Sets the {@link DockController} which is to be used by this {@link MagnetController}. * @param controller the controller to use or <code>null</code> */ @FrameworkOnly public void setController( DockController controller ){ if( this.controller != controller ){ this.controller = controller; strategy.setProperties( controller ); attraction.setProperties( controller ); } } /** * Gets the {@link DockController} that is currently used by this {@link MagnetController}. * @return the controller, can be <code>null</code> */ public DockController getController(){ return controller; } /** * Gets the {@link ScreenDockStation} which is using this {@link MagnetController}. * @return the owner of this controller, never <code>null</code> */ public ScreenDockStation getStation(){ return station; } /** * Starts a move or resize operation that involves <code>window</code>. Only * one operation can be running at the same time. * @param window the window which is moved or resized * @return a callback that is to be informed whenever <code>window</code> further * changes position or size */ public MagnetizedOperation start( ScreenDockWindow window ){ if( current != null ){ current.stop(); } current = new Operation( window ); return current; } /** * Tells whether <code>fixed</code> and <code>moved</code> attract each other. * @param moved the dockable that has moved * @param fixed the dockable that has not moved * @return the attraction, the strongest result from all currently registered {@link AttractorStrategy}s */ public Attraction getAttraction( Dockable moved, Dockable fixed ){ AttractorStrategy strategy = attraction.getValue(); if( strategy == null ){ return Attraction.NEUTRAL; } else{ return strategy.attract( station, moved, fixed ); } } /** * Tells whether <code>fixed</code> and <code>moved</code> stick to each other. * @param moved the dockable that has moved * @param fixed the dockable that has not moved * @return the attraction, the strongest result from all currently registered {@link AttractorStrategy}s */ public Attraction getStickiness( Dockable moved, Dockable fixed ){ AttractorStrategy strategy = attraction.getValue(); if( strategy == null ){ return Attraction.NEUTRAL; } else{ return strategy.stick( station, moved, fixed ); } } /** * Gets the window that is currently moved. * @return the currently reshaped window, may be <code>null</code> */ public ScreenDockWindow getCurrent(){ if( current == null ){ return null; } return current.getWindow(); } /** * Gets an array containing all the {@link ScreenDockWindow}s that are currently shown by the {@link #getStation() station}. * @return all the windows */ public ScreenDockWindow[] getWindows(){ int count = station.getDockableCount(); ScreenDockWindow[] windows = new ScreenDockWindow[ count ]; for( int i = 0; i < count; i++ ){ windows[i] = station.getWindow( i ); } return windows; } /** * Gets all the {@link ScreenDockWindow}s of the {@link #getStation() station} that are attracted to <code>window</code>. * @param window the window that has moved and whose partners are searched * @return all the partner windows, may be empty, is never <code>null</code>, does not contain <code>window</code> */ public ScreenDockWindow[] getAttracted( ScreenDockWindow window ){ List<ScreenDockWindow> result = new ArrayList<ScreenDockWindow>(); int count = station.getDockableCount(); for( int i = 0; i < count; i++ ){ ScreenDockWindow next = station.getWindow( i ); if( next != window ){ Attraction attraction = getAttraction( window.getDockable(), next.getDockable() ); switch( attraction ){ case STRONGLY_ATTRACTED: case ATTRACTED: result.add( next ); break; } } } return result.toArray( new ScreenDockWindow[ result.size() ] ); } /** * Calculates the distance between <code>sideA</code> of <code>windowA</code> to <code>sideB</code> of <code>windowB</code>. * If either window is the {@link #getCurrent() current} window, then its {@link MagnetRequest#getBounds() base boundaries} * are used instead of its current boundaries. * @param windowA the first window * @param sideA the side of the window to check * @param windowB the second window * @param sideB the side of the second window to check * @param initialBoundaries if <code>true</code>, then the initial boundaries of <code>window</code> is used * @return the horizontal or vertical distance between the two sides, always a number greater or equal to 0 * @throws IllegalArgumentException if <code>sideA</code> and <code>sideB</code> are neither equal nor opposite */ public int distance( ScreenDockWindow windowA, MagnetRequest.Side sideA, ScreenDockWindow windowB, MagnetRequest.Side sideB, boolean initialBoundaries ){ if( sideA != sideB ){ if( (sideA == Side.NORTH || sideA == Side.SOUTH) != (sideB == Side.NORTH || sideB == Side.SOUTH) ){ throw new IllegalArgumentException( "sideA and sideB are neither equal nor opposite: " + sideA + ", " + sideB ); } } int valueA = getValue( windowA, sideA, initialBoundaries ); int valueB = getValue( windowB, sideB, initialBoundaries ); return Math.abs( valueA - valueB ); } /** * Tells whether the <code>y</code> coordinate and the <code>height</code> of <code>windowA</code> and <code>windowB</code> * are such that they have at least one pixel at the same height. * @param windowA the first window * @param windowB the second window * @param initialBoundaries if <code>true</code>, then the initial boundaries of <code>window</code> is used * @return <code>true</code> if both windows have at least one pixel on the same height */ public boolean intersectHorizontally( ScreenDockWindow windowA, ScreenDockWindow windowB, boolean initialBoundaries ){ int yA1 = getValue( windowA, Side.NORTH, initialBoundaries ); int yA2 = getValue( windowA, Side.SOUTH, initialBoundaries ); int yB1 = getValue( windowB, Side.NORTH, initialBoundaries ); int yB2 = getValue( windowB, Side.SOUTH, initialBoundaries ); return between( yA1, yA2, yB1 ) || between( yA1, yA2, yB2 ) || between( yB1, yB2, yA1 ) || between( yB1, yB2, yA2 ); } /** * Tells whether the <code>x</code> coordinate and the <code>width</code> of <code>windowA</code> and <code>windowB</code> * are such that they have at least one pixel at the same width. * @param windowA the first window * @param windowB the second window * @param initialBoundaries if <code>true</code>, then the initial boundaries of <code>window</code> is used * @return <code>true</code> if both windows have at least one pixel on the same width */ public boolean intersectVertically( ScreenDockWindow windowA, ScreenDockWindow windowB, boolean initialBoundaries ){ int xA1 = getValue( windowA, Side.WEST, initialBoundaries ); int xA2 = getValue( windowA, Side.EAST, initialBoundaries ); int xB1 = getValue( windowB, Side.WEST, initialBoundaries ); int xB2 = getValue( windowB, Side.EAST, initialBoundaries ); return between( xA1, xA2, xB1 ) || between( xA1, xA2, xB2 ) || between( xB1, xB2, xA1 ) || between( xB1, xB2, xA2 ); } private boolean between( int x1, int x2, int point ){ return x1 <= point && point <= x2; } /** * Gets the location of the side <code>side</code> of <code>window</code>. If <code>window</code> is the * {@link #getCurrent() current window}, then its {@link MagnetRequest#getBounds() base boundaries} are used * to calculate the coordinates, otherwise {@link ScreenDockWindow#getWindowBounds()} is used. * @param window some window * @param side the side to read * @param initialBoundaries if <code>true</code>, then the initial boundaries of <code>window</code> is used * @return the x or y coordinate of <code>side</code> */ public int getValue( ScreenDockWindow window, Side side, boolean initialBoundaries ){ if( initialBoundaries ){ return getValue( current.getInitialBounds( window ), side ); } else if( getCurrent() == window ){ return getValue( current.getBounds(), side ); } else{ return getValue( window.getWindowBounds(), side ); } } /** * Gets the location of the side <code>side</code> of <code>rectangle</code>. * @param rectangle some rectangle * @param side the side to read * @return the x or y coordinate of <code>side</code> */ public int getValue( Rectangle rectangle, Side side ){ switch( side ){ case NORTH: return rectangle.y; case SOUTH: return rectangle.y + rectangle.height - 1; case WEST: return rectangle.x; case EAST: return rectangle.x + rectangle.width - 1; default: throw new IllegalStateException( "unknown side: " + side ); } } /** * Gets the {@link MagnetStrategy} that is currently used by this controller. * @return the current strategy, <code>null</code> if no {@link DockController} is set */ public MagnetStrategy getStrategy(){ return strategy.getValue(); } /** * Sets the {@link MagnetStrategy} that is to be used by this controller. * @param strategy the strategy, a value of <code>null</code> reinstalls the default strategy */ public void setStrategy( MagnetStrategy strategy ){ this.strategy.setValue( strategy ); } /** * Gets the currently used {@link AttractorStrategy}. * @return the strategy defining which two {@link Dockable}s attract each other */ public AttractorStrategy getAttractorStrategy(){ return attraction.getValue(); } /** * Sets the {@link AttractorStrategy} to use. * @param strategy the strategy, a value of <code>null</code> reinstalls the default strategy */ public void setAttractorStrategy( AttractorStrategy strategy ){ this.attraction.setValue( strategy ); } /** * Describes the reshaping of a window both for the {@link ScreenDockWindow} interface and for * the {@link MagnetStrategy}. * @author Benjamin Sigg */ private class Operation implements MagnetizedOperation, MagnetRequest{ /** the window that is reshaped */ private ScreenDockWindow window; /** the boundaries any {@link ScreenDockWindow} had before the operation started */ private Map<ScreenDockWindow, Rectangle> initialBoundaries = new HashMap<ScreenDockWindow, Rectangle>(); /** the unmodified boundaries */ private Rectangle baseBoundaries; /** the boundaries {@link #window} will have after this {@link MagnetRequest} has been executed */ private Rectangle resultBoundaries; /** the currently executer operation */ private MagnetOperation operation; /** * Creates a new operation. * @param window the window that is reshaped */ public Operation( ScreenDockWindow window ){ this.window = window; for( ScreenDockWindow check : getWindows() ){ initialBoundaries.put( check, check.getWindowBounds() ); } } public ScreenDockWindow getWindow(){ return window; } public Rectangle getBounds(){ return new Rectangle( baseBoundaries ); } public Rectangle getResultBounds(){ return new Rectangle( resultBoundaries ); } public Rectangle getInitialBounds( ScreenDockWindow window ){ Rectangle bounds = initialBoundaries.get( window ); if( bounds == null ){ throw new IllegalArgumentException( "window is unknown: " + window ); } return new Rectangle( bounds ); } public boolean isMoved(){ return (isNorth() && isSouth()) || (isEast() && isWest()); } public boolean isResized(){ Rectangle initialBoundaries = getInitialBounds( getWindow() ); return !isMoved() && !initialBoundaries.equals( baseBoundaries ); } public boolean isNorth(){ Rectangle initialBoundaries = getInitialBounds( getWindow() ); return initialBoundaries.y != baseBoundaries.y; } public boolean isSouth(){ Rectangle initialBoundaries = getInitialBounds( getWindow() ); return initialBoundaries.y + initialBoundaries.height != baseBoundaries.y + baseBoundaries.height; } public boolean isWest(){ Rectangle initialBoundaries = getInitialBounds( getWindow() ); return initialBoundaries.x != baseBoundaries.x; } public boolean isEast(){ Rectangle initialBoundaries = getInitialBounds( getWindow() ); return initialBoundaries.x + initialBoundaries.width != baseBoundaries.x + baseBoundaries.width; } public boolean is( Side side ){ switch( side ){ case EAST: return isEast(); case WEST: return isWest(); case NORTH: return isNorth(); case SOUTH: return isSouth(); default: throw new IllegalStateException( "side unknown: " + side ); } } public void resizingAttraction( ScreenDockWindow neighbor, Side windowSide, Side neighborSide ){ checkArguments( neighbor, windowSide, neighborSide ); int neighborValue = getValue( neighbor.getWindowBounds(), neighborSide ); int windowValue = getValue( resultBoundaries, windowSide ); int delta = neighborValue - windowValue; if( windowSide != neighborSide ){ switch( windowSide ){ case NORTH: case WEST: delta += 1; break; case EAST: case SOUTH: delta -= 1; break; } } switch( windowSide ){ case NORTH: resultBoundaries.y += delta; resultBoundaries.height -= delta; break; case SOUTH: resultBoundaries.height += delta; break; case WEST: resultBoundaries.x += delta; resultBoundaries.width -= delta; break; case EAST: resultBoundaries.width += delta; } } public void movingAttraction( ScreenDockWindow neighbor, Side windowSide, Side neighborSide ){ checkArguments( neighbor, windowSide, neighborSide ); int neighborValue = getValue( neighbor.getWindowBounds(), neighborSide ); int windowValue = getValue( resultBoundaries, windowSide ); int delta = neighborValue - windowValue; if( windowSide != neighborSide ){ switch( windowSide ){ case NORTH: case WEST: delta += 1; break; case EAST: case SOUTH: delta -= 1; break; } } switch( windowSide ){ case NORTH: case SOUTH: resultBoundaries.y += delta; break; case EAST: case WEST: resultBoundaries.x += delta; } } private void checkArguments( ScreenDockWindow neighbor, Side windowSide, Side neighborSide ){ if( neighbor == null ){ throw new IllegalArgumentException( "neighbor is null" ); } if( neighbor == getWindow() ){ throw new IllegalArgumentException( "neighbor is identical to window" ); } if( windowSide == null ){ throw new IllegalArgumentException( "windowSide is null" ); } if( neighborSide == null ){ throw new IllegalArgumentException( "neighborSide is null" ); } if( windowSide != neighborSide ){ switch( windowSide ){ case EAST: if( neighborSide != Side.WEST ){ throw new IllegalArgumentException( "windowSide and neighborSide not the same and not opposing: " + windowSide + " vs. " + neighborSide ); } break; case WEST: if( neighborSide != Side.EAST ){ throw new IllegalArgumentException( "windowSide and neighborSide not the same and not opposing: " + windowSide + " vs. " + neighborSide ); } break; case SOUTH: if( neighborSide != Side.NORTH ){ throw new IllegalArgumentException( "windowSide and neighborSide not the same and not opposing: " + windowSide + " vs. " + neighborSide ); } break; case NORTH: if( neighborSide != Side.SOUTH ){ throw new IllegalArgumentException( "windowSide and neighborSide not the same and not opposing: " + windowSide + " vs. " + neighborSide ); } break; } } } public void directAttraction( Rectangle bounds ){ resultBoundaries = new Rectangle( bounds ); } public Rectangle attract( Rectangle bounds ){ baseBoundaries = new Rectangle( bounds ); resultBoundaries = new Rectangle( bounds ); if( operation == null ){ MagnetStrategy strategy = getStrategy(); if( strategy != null ){ operation = strategy.start( MagnetController.this, this ); } } if( operation != null ){ operation.attract( MagnetController.this, this ); } return resultBoundaries; } public void stop(){ if( current == this ){ current = null; } if( operation != null ){ operation.destroy(); operation = null; } } } }