/*
* 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.themes.basic.action;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;
import bibliothek.gui.DockController;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.DockElement;
import bibliothek.gui.dock.DockElementRepresentative;
import bibliothek.gui.dock.action.ActionContentModifier;
import bibliothek.gui.dock.action.DockAction;
import bibliothek.gui.dock.event.DockHierarchyEvent;
import bibliothek.gui.dock.event.DockHierarchyListener;
import bibliothek.gui.dock.themes.border.BorderModifier;
import bibliothek.gui.dock.title.DockTitle;
import bibliothek.gui.dock.title.DockTitle.Orientation;
import bibliothek.gui.dock.util.BackgroundComponent;
import bibliothek.gui.dock.util.BackgroundPaint;
import bibliothek.gui.dock.util.DockUtilities;
import bibliothek.util.container.Triple;
/**
* A class containing all properties and methods needed to handle a button-component
* that shows the contents of a {@link DockAction}.<br>
* A model is normally instantiated by a {@link JComponent} which uses <code>this</code>
* as argument for the constructor of the model. The component can use a subclass
* of the model to override {@link #changed()}, which is invoked every time when
* a property of this model changes. The model will add some listeners to
* the button and update its properties when necessary.
* @author Benjamin Sigg
*/
public class BasicButtonModel {
/** whether this model is selected or not */
private boolean selected = false;
/** the icons shown for this model */
private Map<ActionContentModifier, Icon> icons = new HashMap<ActionContentModifier, Icon>();
/** automatically created icons used when this model is not enabled */
private Map<ActionContentModifier, Icon> disabledIcons = new HashMap<ActionContentModifier, Icon>();
/** the element which is represented by the action */
private DockActionRepresentative representative;
/** whether the mouse is inside the button or not */
private boolean mouseInside = false;
/** whether the first button of the mouse is currently pressed or not */
private boolean mousePressed = false;
/** the text of the action */
private String text;
/** the graphical representation of this model */
private JComponent owner;
/** the orientation of the view */
private Orientation orientation = Orientation.FREE_HORIZONTAL;
/** a callback used when the user clicked on the view */
private BasicTrigger trigger;
/** to initialize resources, can be <code>null</code> */
private BasicResourceInitializer initializer;
/** the algorithm that should be used to paint the background of a component */
private BackgroundPaint background;
/** the source of {@link #background} */
private BackgroundComponent backgroundComponent;
/** listeners that were added to this model */
private List<BasicButtonModelListener> listeners = new ArrayList<BasicButtonModelListener>();
/** a list of borders to use by the associated button */
private Map<String, BorderModifier> borders = new HashMap<String, BorderModifier>();
/** the controller in whose realm this model is used */
private DockController controller;
/**
* Creates a new model.
* @param owner the view of this model
* @param trigger the callback used when the user clicks on the view
* @param initializer a strategy to lazily initialize resources, can be <code>null</code>
*/
public BasicButtonModel( JComponent owner, BasicTrigger trigger, BasicResourceInitializer initializer ){
this( owner, trigger, initializer, true );
}
/**
* Creates a new model.
* @param owner the view of this model
* @param trigger the callback used when the user clicks on the view
* @param initializer a strategy to lazily initialize resources, can be <code>null</code>
* @param createListener whether to create and add a {@link MouseListener} and
* a {@link MouseMotionListener} to <code>owner</code>. If this argument
* is <code>false</code>, then the client is responsible to update all
* properties of this model.
*/
public BasicButtonModel( JComponent owner, BasicTrigger trigger, BasicResourceInitializer initializer, boolean createListener ){
this.owner = owner;
this.trigger = trigger;
this.initializer = initializer;
if( createListener ){
Listener listener = new Listener();
owner.addMouseListener( listener );
owner.addMouseMotionListener( listener );
}
List<Triple<KeyStroke, String, Action>> actions = listActions();
if( actions != null ){
InputMap inputMap = owner.getInputMap();
ActionMap actionMap = owner.getActionMap();
for( Triple<KeyStroke, String, Action> action : actions ){
inputMap.put( action.getA(), action.getB() );
actionMap.put( action.getB(), action.getC() );
}
}
}
/**
* Gets a list of {@link KeyStroke}s, String keys and {@link Action}s which
* are to be applied to the {@link #getOwner() owner} of this model.
* @return the list of actions
*/
protected List<Triple<KeyStroke, String, Action>> listActions(){
List<Triple<KeyStroke, String, Action>> actions = new ArrayList<Triple<KeyStroke,String,Action>>();
Triple<KeyStroke, String, Action> select = new Triple<KeyStroke, String, Action>();
select.setA( KeyStroke.getKeyStroke( KeyEvent.VK_SPACE, 0, false ) );
select.setB( "button_model_select" );
select.setC( new AbstractAction(){
public void actionPerformed(java.awt.event.ActionEvent e){
setMousePressed( true );
}
});
actions.add( select );
Triple<KeyStroke, String, Action> trigger = new Triple<KeyStroke, String, Action>();
trigger.setA( KeyStroke.getKeyStroke( KeyEvent.VK_SPACE, 0, true ) );
trigger.setB( "button_model_trigger" );
trigger.setC( new AbstractAction(){
public void actionPerformed(java.awt.event.ActionEvent e){
if( mousePressed ){
setMousePressed( false );
if( isEnabled() ){
trigger();
}
}
}
});
actions.add( trigger );
return actions;
}
/**
* Adds a listener to this model.
* @param listener the new listener
*/
public void addListener( BasicButtonModelListener listener ){
if( listener == null )
throw new NullPointerException( "listener must not be null" );
listeners.add( listener );
}
/**
* Informs this model about the {@link DockController} in whose realm it is used.
* @param controller the realm in which this model works
*/
public void setController( DockController controller ){
if( this.controller != null ){
DockController old = this.controller;
this.controller = null;
for( BasicButtonModelListener listener : listeners() ){
listener.unbound( this, old );
}
}
if( controller != null ){
this.controller = controller;
for( BasicButtonModelListener listener : listeners() ){
listener.bound( this, this.controller );
}
}
}
/**
* Removes a listener from this model.
* @param listener the listener to remove
*/
public void removeListener( BasicButtonModelListener listener ){
listeners.remove( listener );
}
/**
* Gets all the listeners that are known to this model.
* @return the listeners
*/
protected BasicButtonModelListener[] listeners(){
return listeners.toArray( new BasicButtonModelListener[ listeners.size() ] );
}
/**
* Gets the view which paints the properties of this model.
* @return the view
*/
public JComponent getOwner() {
return owner;
}
/**
* Sets the algorithm which should be used to paint the background of the owner.
* @param background the background algorithm, can be <code>null</code>
* @param backgroundComponent the source of <code>background</code>. Must not be <code>null</code> if
* <code>background</code> is not <code>null</code>, must represents {@link #getOwner()} as {@link Component}.
*/
public void setBackground( BackgroundPaint background, BackgroundComponent backgroundComponent ){
if( this.background != background ){
if( background != null ){
if( backgroundComponent == null ){
throw new IllegalArgumentException( "backgroundComponent must not be null" );
}
if( backgroundComponent.getComponent() != getOwner() ){
throw new IllegalArgumentException( "backgroundComponent must exactly represent 'getOwner()'" );
}
}
BackgroundPaint old = this.background;
this.background = background;
this.backgroundComponent = backgroundComponent;
for( BasicButtonModelListener listener : listeners() ){
listener.backgroundChanged( this, old, background );
}
}
}
/**
* Gets the algorithm which should be used to paint the background of components.
* @return the background, can be <code>null</code>
*/
public BackgroundPaint getBackground(){
return background;
}
/**
* Gets the source of {@link #getBackground()}.
* @return the source, can be <code>null</code> if {@link #getBackground()} returns <code>null</code>
*/
public BackgroundComponent getBackgroundComponent(){
return backgroundComponent;
}
/**
* Gets the {@link DockAction} which is handled by this model. This method may return <code>null</code>
* because not every button actually is connected to a {@link DockAction}.
* @return the action or <code>null</code>
*/
public DockAction getAction(){
if( trigger == null ){
return null;
}
return trigger.getAction();
}
/**
* Gets the {@link Dockable} for which the button is shown. This method may return <code>null</code>
* because not every button is connected to a {@link Dockable}.
* @return the dockable or <code>null</code>
*/
public Dockable getDockable(){
if( trigger == null ){
return null;
}
return trigger.getDockable();
}
/**
* Sets the border for some state of the component that displays this model. Which identifiers
* for <code>key</code> are actually used depends on that component.
* @param key the key of the border
* @param border the new border or <code>null</code>
*/
public void setBorder( String key, BorderModifier border ){
BorderModifier oldBorder = borders.get( key );
if( oldBorder != border ){
if( border == null ){
borders.remove( key );
}
else{
borders.put( key, border );
}
for( BasicButtonModelListener listener : listeners() ){
listener.borderChanged( this, key, oldBorder, border );
}
}
}
/**
* Gets the border which is used for the state <code>key</code>. The exact value of
* key depends on the component which shows this model.
* @param key the key for some border
* @return the border or <code>null</code> if not found
*/
public BorderModifier getBorder( String key ){
if( initializer != null ){
initializer.ensureBorder( this, key );
}
return borders.get( key );
}
/**
* Removes any icon that was ever set by {@link #setIcon(ActionContentModifier, Icon)}.
*/
public void clearIcons(){
for( ActionContentModifier key : getIconContexts() ){
setIcon( key, null );
}
}
/**
* Gets all the {@link ActionContentModifier}s for which an icon is set.
* @return all the contexts in which an icon is available
*/
public ActionContentModifier[] getIconContexts(){
return icons.keySet().toArray( new ActionContentModifier[ icons.size() ] );
}
/**
* Sets the text of this button, some button implementations may ignore the text.
* @param text the new text, can be <code>null</code>
*/
public void setText( String text ){
String oldText = this.text;
this.text = text;
for( BasicButtonModelListener listener : listeners() ){
listener.textChanged( this, oldText, text );
}
changed();
}
/**
* Gets the text of this button.
* @return the text, which may be <code>null</code>
*/
public String getText(){
return text;
}
/**
* Sets the icon which is normally shown on the view.
* @param modifier the context in which to use the icon, not <code>null</code>
* @param icon the new icon, can be <code>null</code>
*/
public void setIcon( ActionContentModifier modifier, Icon icon ){
Icon oldIcon = icons.remove( modifier );
if( icon == null ){
icons.remove( modifier );
}
else{
icons.put( modifier, icon );
}
disabledIcons.remove( modifier );
for( BasicButtonModelListener listener : listeners() ){
listener.iconChanged( this, modifier, oldIcon, icon );
}
changed();
}
/**
* Sets the <code>selected</code> property. The view may be painted in
* a different way dependent on this value.
* @param selected the new value
*/
public void setSelected( boolean selected ) {
if( this.selected != selected ){
this.selected = selected;
for( BasicButtonModelListener listener : listeners() ){
listener.selectedStateChanged( this, selected );
}
changed();
}
}
/**
* Tells whether this model is selected or not.
* @return the property
*/
public boolean isSelected() {
return selected;
}
/**
* Sets the <code>enabled</code> property of this model. A model will not
* react on a mouse-click if it is not enabled.
* @param enabled the value
*/
public void setEnabled( boolean enabled ) {
owner.setEnabled( enabled );
if( !enabled ){
setMousePressed( false );
}
for( BasicButtonModelListener listener : listeners() ){
listener.enabledStateChanged( this, enabled );
}
changed();
}
/**
* Tells whether this model reacts on mouse-clicks or not.
* @return the property
*/
public boolean isEnabled() {
return owner.isEnabled();
}
/**
* Sets the text which should be used as tooltip. The text is directly
* forwarded to the {@link #getOwner() owner} of this model using
* {@link JComponent#setToolTipText(String) setToolTipText}.
* @param tooltip the text, can be <code>null</code>
*/
public void setToolTipText( String tooltip ){
String old = owner.getToolTipText();
for( BasicButtonModelListener listener : listeners() ){
listener.tooltipChanged( this, old, tooltip );
}
owner.setToolTipText( tooltip );
}
/**
* Tells this model which orientation the {@link DockTitle} has, on which
* the view of this model is displayed.
* @param orientation the orientation, not <code>null</code>
*/
public void setOrientation( Orientation orientation ) {
if( orientation == null )
throw new IllegalArgumentException( "Orientation must not be null" );
Orientation old = this.orientation;
this.orientation = orientation;
for( BasicButtonModelListener listener : listeners() ){
listener.orientationChanged( this, old, orientation );
}
changed();
}
/**
* Sets the {@link Dockable} for which a {@link DockElementRepresentative} has to be installed.
* @param dockable the dockable to monitor, can be <code>null</code>
*/
public void setDockableRepresentative( Dockable dockable ){
if( representative != null ){
representative.unbind();
representative = null;
}
if( dockable != null ){
representative = new DockActionRepresentative( dockable );
representative.bind();
}
}
/**
* Gets the orientation of the {@link DockTitle} on which the view of
* this model is displayed.
* @return the orientation
* @see #setOrientation(DockTitle.Orientation)
*/
public Orientation getOrientation() {
return orientation;
}
/**
* Called whenever a property of the model has been changed. The
* default behavior is just to call {@link Component#repaint() repaint}
* of the {@link #getOwner() owner}. Clients are encouraged to override
* this method.
*/
public void changed(){
owner.repaint();
}
/**
* Gets the maximum size the icons need.
* @return the maximum size of all icons
*/
public Dimension getMaxIconSize(){
int w = 0;
int h = 0;
for( Icon icon : icons.values() ){
w = Math.max( w, icon.getIconWidth() );
h = Math.max( h, icon.getIconHeight() );
}
return new Dimension( w, h );
}
/**
* Gets the icon which should be painted on the view.
* @return the icon to paint, can be <code>null</code>
*/
public Icon getPaintIcon(){
return getPaintIcon( isEnabled() );
}
/**
* Gets the icon which should be painted on the view.
* @param enabled whether the enabled or the disabled version of the
* icon is requested.
* @return the icon or <code>null</code>
*/
public Icon getPaintIcon( boolean enabled ){
ActionContentModifier modifier;
if( enabled ){
if( mousePressed ){
if( orientation == null || orientation.isHorizontal() ){
modifier = ActionContentModifier.NONE_PRESSED_HORIZONTAL;
}
else{
modifier = ActionContentModifier.NONE_PRESSED_VERTICAL;
}
}
else if( mouseInside ){
if( orientation == null || orientation.isHorizontal() ){
modifier = ActionContentModifier.NONE_HOVER_HORIZONTAL;
}
else{
modifier = ActionContentModifier.NONE_HOVER_VERTICAL;
}
}
else{
if( orientation == null || orientation.isHorizontal() ){
modifier = ActionContentModifier.NONE_HORIZONTAL;
}
else{
modifier = ActionContentModifier.NONE_VERTICAL;
}
}
}
else{
if( mousePressed ){
if( orientation == null || orientation.isHorizontal() ){
modifier = ActionContentModifier.DISABLED_PRESSED_HORIZONTAL;
}
else{
modifier = ActionContentModifier.DISABLED_PRESSED_VERTICAL;
}
}
else if( mouseInside ){
if( orientation == null || orientation.isHorizontal() ){
modifier = ActionContentModifier.DISABLED_HOVER_HORIZONTAL;
}
else{
modifier = ActionContentModifier.DISABLED_HOVER_VERTICAL;
}
}
else{
if( orientation == null || orientation.isHorizontal() ){
modifier = ActionContentModifier.DISABLED_HORIZONTAL;
}
else{
modifier = ActionContentModifier.DISABLED_VERTICAL;
}
}
}
List<ActionContentModifier> modifiers = new LinkedList<ActionContentModifier>();
modifiers.add( modifier );
while( !modifiers.isEmpty() ){
modifier = modifiers.remove( 0 );
Icon icon = icons.get( modifier );
if( icon != null ){
if( !enabled && modifier.isEnabled() ){
Icon disabled = disabledIcons.get( modifier );
if( disabled == null && !disabledIcons.containsKey( modifier )){
disabled = DockUtilities.disabledIcon( owner, icon );
disabledIcons.put( modifier, disabled );
}
if( disabled != null ){
icon = disabled;
}
}
return icon;
}
for( ActionContentModifier backup : modifier.getBackup() ){
modifiers.add( backup );
}
}
// no icon to show
return null;
}
/**
* Changes the <code>mouseInside</code> property. The property tells whether
* the mouse is currently inside the border of the {@link #getOwner() owner}
* or not. Clients should not call this method unless they handle all
* mouse events.
* @param mouseInside whether the mouse is inside
*/
protected void setMouseInside( boolean mouseInside ) {
if( this.mouseInside != mouseInside ){
this.mouseInside = mouseInside;
for( BasicButtonModelListener listener : listeners() ){
listener.mouseInside( this, mouseInside );
}
changed();
}
}
/**
* Tells whether the mouse currently is inside the {@link #getOwner() owner}
* or not.
* @return <code>true</code> if the mouse is inside
*/
public boolean isMouseInside() {
return mouseInside;
}
/**
* Changes the <code>mousePressed</code> property. The property tells
* whether the left mouse button is currently pressed or not. Clients
* should not invoke this method unless they handle all mouse events.
* @param mousePressed whether button 1 is pressed
*/
protected void setMousePressed( boolean mousePressed ) {
if( this.mousePressed != mousePressed ){
this.mousePressed = mousePressed;
for( BasicButtonModelListener listener : listeners() ){
listener.mousePressed( this, mousePressed );
}
changed();
}
}
/**
* Tells whether the left mouse button is currently pressed or not.
* @return <code>true</code> if the button is pressed
*/
public boolean isMousePressed() {
return mousePressed;
}
/**
* Called when the left mouse button has been pressed and released within
* the {@link #getOwner() owner} and when this model is {@link #isEnabled() enabled}.
*/
protected void trigger(){
if( trigger != null ){
trigger.triggered();
}
for( BasicButtonModelListener listener : listeners() ){
listener.triggered();
}
}
/**
* A mouse listener observing the view of the enclosing model.
* @author Benjamin Sigg
*/
private class Listener extends MouseInputAdapter{
@Override
public void mouseEntered( MouseEvent e ) {
setMouseInside( true );
}
@Override
public void mouseExited( MouseEvent e ) {
setMouseInside( false );
}
@Override
public void mouseDragged( MouseEvent e ) {
boolean inside = owner.contains( e.getX(), e.getY() );
if( inside != mouseInside )
setMouseInside( inside );
}
@Override
public void mousePressed( MouseEvent e ) {
if( !mousePressed && e.getButton() == MouseEvent.BUTTON1 ){
setMousePressed( true );
}
}
@Override
public void mouseReleased( MouseEvent e ) {
if( mousePressed && e.getButton() == MouseEvent.BUTTON1 ){
boolean inside = owner.contains( e.getX(), e.getY() );
if( inside && isEnabled() ){
trigger();
}
setMousePressed( false );
if( mouseInside != inside )
setMouseInside( inside );
}
}
}
/**
* A wrapper around the represented {@link Dockable}.
* @author Benjamin Sigg
*/
private class DockActionRepresentative implements DockElementRepresentative, DockHierarchyListener{
private Dockable dockable;
private DockController controller;
/**
* Creates a new representative
* @param dockable the represented {@link Dockable}
*/
public DockActionRepresentative( Dockable dockable ){
this.dockable = dockable;
}
public void bind(){
dockable.addDockHierarchyListener( this );
controller = dockable.getController();
if( controller != null ){
controller.addRepresentative( this );
}
}
public void unbind(){
dockable.removeDockHierarchyListener( this );
if( controller != null ){
controller.removeRepresentative( this );
controller = null;
}
}
public void hierarchyChanged( DockHierarchyEvent event ){
// ignore
}
public void controllerChanged( DockHierarchyEvent event ){
if( controller != null ){
controller.removeRepresentative( this );
controller = null;
}
controller = dockable.getController();
if( controller != null ){
controller.addRepresentative( this );
}
}
public void addMouseInputListener( MouseInputListener listener ){
getOwner().addMouseListener( listener );
getOwner().addMouseMotionListener( listener );
}
public Component getComponent(){
return getOwner();
}
public DockElement getElement(){
return dockable;
}
public Point getPopupLocation( Point click, boolean popupTrigger ){
if( popupTrigger ){
return click;
}
return null;
}
public boolean isUsedAsTitle(){
return false;
}
public void removeMouseInputListener( MouseInputListener listener ){
getOwner().removeMouseListener( listener );
getOwner().removeMouseMotionListener( listener );
}
public boolean shouldFocus(){
return false;
}
public boolean shouldTransfersFocus(){
return true;
}
}
}