/*
* 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) 2008 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.extension.gui.dock;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EtchedBorder;
import bibliothek.extension.gui.dock.preference.PreferenceEditor;
import bibliothek.extension.gui.dock.preference.PreferenceEditorCallback;
import bibliothek.extension.gui.dock.preference.PreferenceEditorFactory;
import bibliothek.extension.gui.dock.preference.PreferenceModel;
import bibliothek.extension.gui.dock.preference.PreferenceModelListener;
import bibliothek.extension.gui.dock.preference.PreferenceOperation;
import bibliothek.extension.gui.dock.preference.PreferenceOperationView;
import bibliothek.extension.gui.dock.preference.PreferenceOperationViewListener;
import bibliothek.extension.gui.dock.preference.editor.BooleanEditor;
import bibliothek.extension.gui.dock.preference.editor.ChoiceEditor;
import bibliothek.extension.gui.dock.preference.editor.KeyStrokeEditor;
import bibliothek.extension.gui.dock.preference.editor.LabelEditor;
import bibliothek.extension.gui.dock.preference.editor.ModifierMaskEditor;
import bibliothek.extension.gui.dock.preference.editor.StringEditor;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.action.ActionContentModifier;
import bibliothek.gui.dock.action.DockAction;
import bibliothek.gui.dock.themes.basic.action.BasicTrigger;
import bibliothek.gui.dock.themes.basic.action.buttons.BasicMiniButton;
import bibliothek.util.Path;
/**
* A {@link Component} that shows the entries of a {@link PreferenceModel}, the user
* can edit those entries. Each preference is shown in a {@link PreferenceEditor}, this
* table uses a map of {@link PreferenceEditorFactory}s to create them.
* @author Benjamin Sigg
*/
public class PreferenceTable extends JPanel{
/** The factories that are available. */
private Map<Path, PreferenceEditorFactory<?>> factories = new HashMap<Path, PreferenceEditorFactory<?>>();
/** the preferences that are shown in this table */
private PreferenceModel model;
/** the visible rows */
private List<Row<?>> rows = new ArrayList<Row<?>>();
/** the panel showing the contents of this table */
private JPanel panel;
/** the layout used on this panel */
private GridBagLayout layout;
/** a listener observing {@link #model} */
private Listener listener = new Listener();
/** the operations visible on this table */
private List<PreferenceOperation> operations = new ArrayList<PreferenceOperation>();
/** all the views that are currently in use */
private Map<PreferenceOperation, Operation> operationViews = new HashMap<PreferenceOperation, Operation>();
/** whether the order of the operations should be reversed or not */
private boolean reverseOrder = true;
/**
* Creates a new table
*/
public PreferenceTable(){
super( new GridBagLayout() );
layout = new GridBagLayout();
panel = new JPanel( layout );
add( panel, new GridBagConstraints( 0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.FIRST_LINE_START,
GridBagConstraints.HORIZONTAL, new Insets( 0, 0, 0, 0 ), 0, 0 ));
setEditorFactory( Path.TYPE_BOOLEAN_PATH, BooleanEditor.FACTORY );
setEditorFactory( Path.TYPE_MODIFIER_MASK_PATH, ModifierMaskEditor.FACTORY );
setEditorFactory( Path.TYPE_KEYSTROKE_PATH, KeyStrokeEditor.FACTORY );
setEditorFactory( Path.TYPE_STRING_CHOICE_PATH, ChoiceEditor.FACTORY );
setEditorFactory( Path.TYPE_LABEL, LabelEditor.FACTORY );
setEditorFactory( Path.TYPE_STRING_PATH, StringEditor.FACTORY );
addOperation( PreferenceOperation.DEFAULT );
addOperation( PreferenceOperation.DELETE );
}
/**
* Creates a new table
* @param model the model shown on this table
*/
public PreferenceTable( PreferenceModel model ){
this();
setModel( model );
}
/**
* Sets the order in which the operations should be shown. Either
* left to right or right to left. The default value is <code>true</code>
* which means right to left.
* @param reverseOrder how to display the operations
*/
public void setReverseOrder( boolean reverseOrder ) {
this.reverseOrder = reverseOrder;
int index = 0;
for( Row<?> row : rows ){
row.setIndex( index++ );
}
}
/**
* Tells in which order the operations are shown.
* @return the order
* @see #setReverseOrder(boolean)
*/
public boolean isReverseOrder() {
return reverseOrder;
}
/**
* Adds a new operation at the end of the set of operations. The set of
* operations tells the order in which the operations appear on the table.
* @param operation a new operation
*/
public void addOperation( PreferenceOperation operation ){
if( !operations.contains( operation )){
operations.add( operation );
operationViews.put( operation, new Operation( operation ) );
}
}
/**
* Insert an operation at the given index. The set of operations tells
* the order in which the operations appear on the table.
* @param index the index of the new operation
* @param operation the new operation
*/
public void insertOperation( int index, PreferenceOperation operation ){
if( !operations.contains( operation )){
operations.add( index, operation );
operationViews.put( operation, new Operation( operation ) );
}
}
private int getOperationIndex( PreferenceOperation operation ){
int index = operations.indexOf( operation );
if( index < 0 ){
operations.add( operation );
index = operations.size()-1;
}
if( reverseOrder )
index = operations.size() - 1 - index;
return index;
}
private void addTable( Component component ){
panel.add( component );
}
private void removeTable( Component component ){
panel.remove( component );
}
/**
* Gets the model that is used on this table.
* @return the model
*/
public PreferenceModel getModel() {
return model;
}
/**
* Changes the model of this table.
* @param model the new model, can be <code>null</code>
*/
public void setModel( PreferenceModel model ) {
if( this.model != model ){
if( this.model != null ){
this.model.removePreferenceModelListener( listener );
listener.preferenceRemoved( this.model, 0, this.model.getSize()-1 );
}
this.model = model;
if( this.model != null ){
this.model.addPreferenceModelListener( listener );
listener.preferenceAdded( this.model, 0, this.model.getSize()-1 );
}
}
}
/**
* Sets the factory that should be used to create an editor for some
* type of object.
* @param type a path describing the type that will be edited, most
* times the path is just the name of some class. There is a set of
* standard paths defined in {@link Path}
* @param factory the new factory or <code>null</code> to delete a factory
*/
public void setEditorFactory( Path type, PreferenceEditorFactory<?> factory ){
if( factory == null )
factories.remove( type );
else
factories.put( type, factory );
}
/**
* Gets the factory which is responsible to create editors for
* <code>type</code>-objects.
* @param <V> the kind of objects that get edited
* @param type the kind of objects that get edited
* @return the factory for <code>type</code> or <code>null</code>
*/
@SuppressWarnings("unchecked")
public <V> PreferenceEditorFactory<V> getEditorFactory( Path type ){
return (PreferenceEditorFactory<V>)factories.get( type );
}
/**
* Creates a new editor for <code>type</code>.
* @param <V> the kind of value that gets edited
* @param type the kind of value that gets edited
* @return a new editor
*/
@SuppressWarnings("unchecked")
protected <V> PreferenceEditor<V> createEditor( Path type ){
PreferenceEditorFactory<?> factory = factories.get( type );
if( factory == null )
throw new IllegalArgumentException( "No editor defined for type '" + type.toString() + "'" );
return (PreferenceEditor<V>)factory.create();
}
/**
* A wrapper around a {@link PreferenceOperation} adding support for {@link PreferenceOperationView}s.
* @author Benjamin Sigg
*/
private class Operation{
private PreferenceOperation operation;
private PreferenceOperationView view;
private int usages;
/**
* Creates a new wrapper around <code>operation</code>
* @param operation the operation represented by this wrapper
*/
public Operation( PreferenceOperation operation ){
this.operation = operation;
}
/**
* Gets the operation which is hidden behind this wrapper.
* @return the operation
*/
public PreferenceOperation getOperation(){
return operation;
}
/**
* Gets a view of this operation
* @return the view, not <code>null</code>
*/
public synchronized PreferenceOperationView createView(){
if( view == null ){
view = operation.create( model );
}
usages++;
return view;
}
/**
* Informs this operation that its view is no longer required
*/
public synchronized void freeView(){
usages--;
if( usages == 0 ){
view.destroy();
view = null;
}
}
}
/**
* Represents a single row in a {@link PreferenceTable}.
* @author Benjamin Sigg
*
* @param <V> the kind of value in this row
*/
private class Row<V> implements PreferenceEditorCallback<V>{
private int index;
private JLabel label;
private PreferenceEditor<V> editor;
private Map<PreferenceOperation, Button> editorOperations;
private Map<PreferenceOperation, Button> modelOperations;
private boolean initialized = false;
public Row( PreferenceEditor<V> editor, String label, String description ){
this.editor = editor;
this.label = new JLabel( label );
this.label.setToolTipText( description );
addTable( this.label );
if( editor != null ){
editor.setCallback( this );
}
addTable( editor.getComponent() );
}
public void setIndex( int index ) {
this.index = index;
layout.setConstraints( label,
new GridBagConstraints( 0, index, 1, 1, 1.0, 1.0,
GridBagConstraints.LINE_START, GridBagConstraints.NONE,
new Insets( 1, 1, 1, 1 ), 0, 0 ) );
if( editor != null ){
layout.setConstraints( editor.getComponent(),
new GridBagConstraints( 1, index, 1, 1, 100.0, 1.0,
GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL,
new Insets( 1, 1, 1, 1 ), 0, 0 ) );
}
if( !initialized ){
initialized = true;
initialize();
}
if( modelOperations != null ){
for( Map.Entry<PreferenceOperation, Button> entry : modelOperations.entrySet() ){
int location = 2 + getOperationIndex( entry.getKey() );
layout.setConstraints( entry.getValue(),
new GridBagConstraints( location, index, 1, 1, 1.0, 1.0,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets( 1, 0, 1, 0 ), 0, 0 ) );
}
}
if( editorOperations != null ){
for( Map.Entry<PreferenceOperation, Button> entry : editorOperations.entrySet() ){
int location = 2 + getOperationIndex( entry.getKey() );
layout.setConstraints( entry.getValue(),
new GridBagConstraints( location, index, 1, 1, 1.0, 1.0,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets( 1, 0, 1, 0 ), 0, 0 ) );
}
}
revalidate();
}
public void setOperation( PreferenceOperation operation, boolean enabled ) {
setOperation( operation, enabled, true );
}
@SuppressWarnings("unchecked")
private void initialize(){
PreferenceOperation[] operations = model.getOperations( index );
if( operations != null ){
for( PreferenceOperation operation : operations ){
if( editorOperations == null || !editorOperations.containsKey( operation )){
setOperation( operation, model.isEnabled( index, operation ), false );
}
}
}
if( editor != null ){
editor.setValueInfo( model.getValueInfo( index ));
editor.setValue( (V)model.getValue( index ) );
}
}
private void setOperation( final PreferenceOperation operation, boolean enabled, final boolean editor ) {
Map<PreferenceOperation, Button> operations;
if( editor ){
if( editorOperations == null ){
editorOperations = new HashMap<PreferenceOperation, Button>();
}
operations = editorOperations;
}
else{
if( modelOperations == null ){
modelOperations = new HashMap<PreferenceOperation, Button>();
}
operations = modelOperations;
}
Button button = operations.get( operation );
if( button == null ){
Operation view = operationViews.get( operation );
if( view == null ){
view = new Operation( operation );
}
button = new Button( view, editor );
operations.put( operation, button );
addTable( button );
int location = 2 + getOperationIndex( operation );
layout.setConstraints( button,
new GridBagConstraints( location, index, 1, 1, 1.0, 1.0,
GridBagConstraints.LINE_END, GridBagConstraints.NONE,
new Insets( 1, 0, 1, 0 ), 0, 0 ) );
revalidate();
}
button.getModel().setEnabled( enabled );
}
@SuppressWarnings("unchecked")
public void changed(){
if( editor != null ){
editor.setValue( (V)model.getValue( index ) );
}
label.setText( model.getLabel( index ) );
label.setToolTipText( model.getDescription( index ) );
if( modelOperations != null ){
for( Map.Entry<PreferenceOperation, Button> entry : modelOperations.entrySet() ){
boolean enabled = model.isEnabled( index, entry.getKey() );
entry.getValue().getModel().setEnabled( enabled );
}
}
}
/**
* Destroys this row.
*/
public void destroy(){
removeTable( label );
if( editor != null ){
editor.setCallback( null );
editor.setValue( null );
removeTable( editor.getComponent() );
editor.setValueInfo( null );
}
if( editorOperations != null ){
for( Button button : editorOperations.values() ){
button.destroy();
removeTable( button );
}
}
if( modelOperations != null ){
for( Button button : modelOperations.values() ){
button.destroy();
removeTable( button );
}
}
}
@SuppressWarnings("unchecked")
public V get() {
return (V)model.getValue( index );
}
public void set( V value ) {
model.setValue( index, value );
}
private void doOperation( PreferenceOperation operation ){
model.doOperation( index, operation );
}
public PreferenceModel getModel(){
return model;
}
/**
* A small button that can trigger an operation
* @author Benjamin Sigg
*/
private class Button extends BasicMiniButton implements PreferenceOperationViewListener{
private Operation operation;
private PreferenceOperationView view;
public Button( final Operation operation, final boolean editorOperation ){
super( new BasicTrigger(){
public void triggered() {
if( editorOperation )
editor.doOperation( operation.getOperation() );
else
doOperation( operation.getOperation() );
}
public DockAction getAction(){
return null;
}
public Dockable getDockable(){
return null;
}
}, null );
this.operation = operation;
view = operation.createView();
view.addListener( this );
getModel().setIcon( ActionContentModifier.NONE_HORIZONTAL, view.getIcon() );
getModel().setToolTipText( view.getDescription() );
setMouseOverBorder( BorderFactory.createEtchedBorder( EtchedBorder.LOWERED ) );
setMousePressedBorder( BorderFactory.createLoweredBevelBorder() );
}
@Override
public void setEnabled( boolean enabled ) {
super.setEnabled( enabled );
setFocusable( enabled );
}
public void iconChanged( PreferenceOperationView operation, Icon oldIcon, Icon newIcon ){
getModel().setIcon( ActionContentModifier.NONE_HORIZONTAL, newIcon );
}
public void descriptionChanged( PreferenceOperationView operation, String oldDescription, String newDescription ){
getModel().setToolTipText( newDescription );
}
public void destroy(){
view.removeListener( this );
operation.freeView();
}
}
}
/**
* Listens to {@link PreferenceTable#model} and updates this table when necessary.
* @author Benjamin Sigg
*
*/
private class Listener implements PreferenceModelListener{
@SuppressWarnings("unchecked")
public void preferenceAdded( PreferenceModel model, int beginIndex, int endIndex ){
for( int index = beginIndex; index <= endIndex; index++ ){
PreferenceEditor<?> editor = createEditor( model.getTypePath( index ) );
Row<?> row = new Row( editor, model.getLabel( index ), model.getDescription( index ));
rows.add( index, row );
row.setIndex( index );
}
for( int i = beginIndex, n = rows.size(); i<n; i++ ){
rows.get( i ).setIndex( i );
}
revalidate();
}
public void preferenceChanged( PreferenceModel model, int beginIndex, int endIndex ){
for( int i = beginIndex; i <= endIndex; i++ ){
Row<?> row = rows.get( i );
row.changed();
}
revalidate();
}
public void preferenceRemoved( PreferenceModel model, int beginIndex, int endIndex ){
for( int i = endIndex; i >= beginIndex; i-- ){
Row<?> row = rows.remove( i );
row.destroy();
}
for( int i = beginIndex, n = rows.size(); i<n; i++ ){
rows.get( i ).setIndex( i );
}
revalidate();
}
}
}