/*
* 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) 2007 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.stack;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.MouseInputAdapter;
import bibliothek.gui.DockController;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.DockElement;
import bibliothek.gui.dock.DockElementRepresentative;
import bibliothek.gui.dock.action.ActionPopup;
import bibliothek.gui.dock.action.DockActionSource;
import bibliothek.gui.dock.control.RemoteRelocator;
import bibliothek.gui.dock.control.RemoteRelocator.Reaction;
import bibliothek.gui.dock.disable.TabDisablingStrategyObserver;
import bibliothek.gui.dock.station.stack.tab.layouting.TabPlacement;
import bibliothek.gui.dock.util.SimpleDockElementRepresentative;
/**
* The standard-implementation of {@link StackDockComponent}. This implementation
* uses a {@link JTabbedPane} to display its children.
*
* @author Janni Kovacs
* @author Benjamin Sigg
* @see StackDockComponent
* @see JTabbedPane
*/
public class DefaultStackDockComponent extends JTabbedPane implements StackDockComponent {
/** The Dockables shown on this component and their RemoteRelocators to control drag&drop operations */
private List<Tab> dockables = new ArrayList<Tab>();
/** The controller for which this component is shown */
private DockController controller;
/** the tab to which mouse-events are currently redirected */
private Tab mouseTarget;
/** listeners to be informed if the selection changes */
private List<StackDockComponentListener> listeners = new ArrayList<StackDockComponentListener>();
/** keeps track of all tabs that need to be disabled */
private TabDisablingStrategyObserver tabDisabling = new TabDisablingStrategyObserver(){
@Override
public void setDisabled( Dockable dockable, boolean disabled ){
int index = indexOf( dockable );
if( index >= 0 ){
setEnabledAt( index, !disabled );
}
}
};
/**
* Constructs the component, sets the location of the tabs to bottom.
*/
public DefaultStackDockComponent(){
super(BOTTOM);
Listener listener = new Listener();
addMouseListener( listener );
addMouseMotionListener( listener );
addChangeListener( listener );
setOpaque( false );
}
public void addStackDockComponentListener( StackDockComponentListener listener ){
listeners.add( listener );
}
public void removeStackDockComponentListener( StackDockComponentListener listener ){
listeners.remove( listener );
}
public void setDockTabPlacement( TabPlacement tabSide ){
switch( tabSide ){
case BOTTOM_OF_DOCKABLE:
setTabPlacement( BOTTOM );
break;
case LEFT_OF_DOCKABLE:
setTabPlacement( LEFT );
break;
case TOP_OF_DOCKABLE:
setTabPlacement( TOP );
break;
case RIGHT_OF_DOCKABLE:
setTabPlacement( RIGHT );
break;
}
}
public TabPlacement getDockTabPlacement(){
switch( getTabPlacement() ){
case BOTTOM:
return TabPlacement.BOTTOM_OF_DOCKABLE;
case LEFT:
return TabPlacement.LEFT_OF_DOCKABLE;
case RIGHT:
return TabPlacement.RIGHT_OF_DOCKABLE;
case TOP:
return TabPlacement.TOP_OF_DOCKABLE;
}
throw new IllegalStateException( "unknown position: " + getTabPlacement() );
}
public void insertTab(String title, Icon icon, Component comp, Dockable dockable, int index) {
insertTab(title, icon, comp, (String)null, index);
Tab tab = createTab( dockable );
dockables.add( index, tab );
tab.setController( controller );
tabDisabling.add( dockable );
}
/**
* Creates a new representation of a tab on this component.
* @param dockable the element which is represented by the tab
* @return the new tab
*/
protected Tab createTab( Dockable dockable ){
return new Tab( dockable );
}
public void addTab( String title, Icon icon, Component comp, Dockable dockable ){
addTab( title, icon, comp );
Tab tab = createTab( dockable );
dockables.add( tab );
tab.setController( controller );
tabDisabling.add( dockable );
}
public Dockable getDockableAt( int index ){
return dockables.get( index ).getDockable();
}
/**
* The structure of the {@link JTabbedPane} does not allow its tabs to recognized as {@link DockElementRepresentative},
* hence this method always returns <code>null</code>.
* @param index ignored
* @return <code>null</code>
*/
public DockElementRepresentative getTabAt( int index ){
return null;
}
public void moveTab( int source, int destination ){
if( source == destination ){
return;
}
if( destination < 0 || destination >= getTabCount() ){
throw new ArrayIndexOutOfBoundsException();
}
int selected = getSelectedIndex();
String title = getTitleAt( source );
String tooltip = getToolTipTextAt( source );
Icon icon = getIconAt( source );
Component comp = getComponentAt( source );
Dockable dockable = dockables.get( source ).getDockable();
remove( source );
insertTab( title, icon, comp, dockable, destination );
setTooltipAt( destination, tooltip );
if( selected == source ){
selected = destination;
}
else if( selected > source && selected <= destination ){
selected++;
}
setSelectedIndex( selected );
}
@Override
public void removeAll(){
for( Tab tab : dockables ){
tab.setController( null );
tabDisabling.remove( tab.getDockable() );
}
super.removeAll();
dockables.clear();
}
@Override
public void remove( int index ){
Tab tab = dockables.remove( index );
tab.setController( null );
tabDisabling.remove( tab.getDockable() );
super.remove( index );
}
public Component getComponent() {
return this;
}
@Override
public void setTitleAt( int index, String title ){
super.setTitleAt( index, title == null ? "" : title );
}
public void setTooltipAt( int index, String newTooltip ) {
setToolTipTextAt( index, newTooltip );
}
public void setController( DockController controller ){
if( this.controller != controller ){
if( mouseTarget != null ){
if( mouseTarget.relocator != null ){
mouseTarget.relocator.cancel();
}
mouseTarget = null;
}
this.controller = controller;
tabDisabling.setController( controller );
for( Tab tab : dockables ){
tab.setController( controller );
}
}
}
public boolean hasBorder() {
return true;
}
public boolean isSingleTabComponent(){
return false;
}
public DockElementRepresentative createDefaultRepresentation( DockElement target ){
return new SimpleDockElementRepresentative( target, this );
}
public int getIndexOfTabAt( Point mouseLocation ){
for( int i = 0, n = getTabCount(); i<n; i++ ){
Rectangle tab = getBoundsAt( i );
if( tab != null && tab.contains( mouseLocation )){
return i;
}
}
return -1;
}
public int indexOf( Dockable dockable ){
int index = 0;
for( Tab tab : dockables ){
if( tab.getDockable() == dockable ){
return index;
}
index++;
}
return -1;
}
@Override
public Dimension getMinimumSize(){
Dimension result = new Dimension( 1, 1 );
for( int i = 0, n = getTabCount(); i<n; i++ ){
Dimension size = getComponentAt( i ).getMinimumSize();
result.width = Math.max( result.width, size.width );
result.height = Math.max( result.height, size.height );
}
return result;
}
/**
* Representation of a single tab of this {@link StackDockComponent}.
* @author Benjamin Sigg
*/
public class Tab extends ActionPopup{
/** the element on the tab */
protected Dockable dockable;
/** used to drag and drop the tab */
private RemoteRelocator relocator;
/**
* Creates a new Tab
* @param dockable the element on the tab
*/
public Tab( Dockable dockable ){
super( true );
this.dockable = dockable;
}
public Dockable getDockable() {
return dockable;
}
/**
* Tells this tab which controller is currently used. Set to <code>null</code>
* if this tab is no longer used, or when the connection to a
* {@link DockController} is lost.
* @param controller the new source of information, can be <code>null</code>
*/
public void setController( DockController controller ){
if( controller == null )
relocator = null;
else
relocator = controller.getRelocator().createRemote( dockable );
}
public void popup( MouseEvent event ){
if( !event.isConsumed() && event.isPopupTrigger() ){
super.popup( event );
}
}
protected DockActionSource getActions(){
return dockable.getGlobalActionOffers();
}
@Override
protected Object getSource(){
return this;
}
protected boolean isEnabled(){
return true;
}
}
/**
* A listener to the enclosing component, using some {@link RemoteRelocator}
* to do drag & drop operations.
* @author Benjamin Sigg
*/
private class Listener extends MouseInputAdapter implements ChangeListener{
public void stateChanged( ChangeEvent e ){
for( StackDockComponentListener listener : listeners.toArray( new StackDockComponentListener[ listeners.size() ] )){
listener.selectionChanged( DefaultStackDockComponent.this );
}
}
/**
* Updates the value of {@link DefaultStackDockComponent#mouseTarget relocator}
* @param x the x-coordinate of the mouse
* @param y the y-coordinate of the mouse
* @param searchDockable if <code>true</code>, then a new current relocator can be
* selected, otherwise the relocator may only be canceled by not exchanged
* @param forceSearch if <code>true</code> then a search is always made, even if the user is
* not allowed to move a tab anyways
*/
private void updateRelocator( int x, int y, boolean searchDockable, boolean forceSearch ){
boolean allowed = controller == null || !controller.getRelocator().isDragOnlyTitle();
if( mouseTarget != null ){
if( !allowed ){
mouseTarget.relocator.cancel();
if( !forceSearch ){
mouseTarget = null;
}
}
return;
}
if( !allowed && !forceSearch ){
return;
}
if( searchDockable ){
for( int i = 0, n = getTabCount(); i<n; i++ ){
Rectangle bounds = getBoundsAt( i );
if( bounds != null && bounds.contains( x, y )){
mouseTarget = dockables.get( i );
}
}
}
}
@Override
public void mousePressed( MouseEvent e ){
if( e.isConsumed() )
return;
updateRelocator( e.getX(), e.getY(), true, false );
if( mouseTarget != null && mouseTarget.relocator != null ){
mouseTarget.popup( e );
if( e.isConsumed() ){
return;
}
Point mouse = e.getPoint();
SwingUtilities.convertPointToScreen( mouse, e.getComponent() );
Reaction reaction = mouseTarget.relocator.init( mouse.x, mouse.y, 0, 0, e.getModifiersEx() );
switch( reaction ){
case BREAK_CONSUMED:
e.consume();
// fall through
case BREAK:
mouseTarget = null;
break;
case CONTINUE:
case CONTINUE_CONSUMED:
// consume MouseEvent in all cases: there will never be any component below the tab that should start
// a drag and drop operation
e.consume();
}
}
else{
updateRelocator( e.getX(), e.getY(), true, true );
if( mouseTarget != null ){
mouseTarget.popup( e );
mouseTarget = null;
}
}
}
@Override
public void mouseReleased( MouseEvent e ){
if( e.isConsumed() )
return;
updateRelocator( e.getX(), e.getY(), false, false );
if( mouseTarget != null && mouseTarget.relocator != null ){
Point mouse = e.getPoint();
SwingUtilities.convertPointToScreen( mouse, e.getComponent() );
Reaction reaction = mouseTarget.relocator.drop( mouse.x, mouse.y, e.getModifiersEx() );
switch( reaction ){
case BREAK_CONSUMED:
e.consume();
mouseTarget = null;
break;
case BREAK:
mouseTarget.popup( e );
mouseTarget = null;
break;
case CONTINUE_CONSUMED:
e.consume();
break;
}
}
else{
updateRelocator( e.getX(), e.getY(), true, true );
if( mouseTarget != null ){
mouseTarget.popup( e );
mouseTarget = null;
}
}
}
@Override
public void mouseDragged( MouseEvent e ){
if( e.isConsumed() )
return;
updateRelocator( e.getX(), e.getY(), false, false );
if( mouseTarget != null && mouseTarget.relocator != null ){
Point mouse = e.getPoint();
SwingUtilities.convertPointToScreen( mouse, e.getComponent() );
Reaction reaction = mouseTarget.relocator.drag( mouse.x, mouse.y, e.getModifiersEx() );
switch( reaction ){
case BREAK_CONSUMED:
e.consume();
// fall through
case BREAK:
mouseTarget = null;
break;
case CONTINUE_CONSUMED:
e.consume();
break;
}
}
}
}
}