/*
* Copyright (c) 2008-2012, Matthias Mann
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Matthias Mann nor the names of its contributors may
* be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package de.matthiasmann.twl;
import de.matthiasmann.twl.model.AbstractListModel;
import de.matthiasmann.twl.model.AbstractTreeTableModel;
import de.matthiasmann.twl.model.AbstractTreeTableNode;
import de.matthiasmann.twl.model.ListModel;
import de.matthiasmann.twl.model.Property;
import de.matthiasmann.twl.model.PropertyList;
import de.matthiasmann.twl.model.SimplePropertyList;
import de.matthiasmann.twl.model.TreeTableModel;
import de.matthiasmann.twl.model.TreeTableNode;
import de.matthiasmann.twl.utils.TypeMapping;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A property sheet class
*
* @author Matthias Mann
*/
public class PropertySheet extends TreeTable {
public interface PropertyEditor {
public Widget getWidget();
public void valueChanged();
public void preDestroy();
public void setSelected(boolean selected);
/**
* Can be used to position the widget in a cell.
* <p>If this method returns false, the table will position the widget itself.</p>
*
* <p>This method is responsible to call setPosition and setSize on the
* widget or return false.</p>
*
* @param x the left edge of the cell
* @param y the top edge of the cell
* @param width the width of the cell
* @param height the height of the cell
*
* @return true if the position was changed by this method.
*/
public boolean positionWidget(int x, int y, int width, int height);
}
public interface PropertyEditorFactory<T> {
public PropertyEditor createEditor(Property<T> property);
}
private final SimplePropertyList rootList;
private final PropertyListCellRenderer subListRenderer;
private final CellRenderer editorRenderer;
private final TypeMapping<PropertyEditorFactory<?>> factories;
public PropertySheet() {
this(new Model());
}
@SuppressWarnings("OverridableMethodCallInConstructor")
private PropertySheet(Model model) {
super(model);
this.rootList = new SimplePropertyList("<root>");
this.subListRenderer = new PropertyListCellRenderer();
this.editorRenderer = new EditorRenderer();
this.factories = new TypeMapping<PropertyEditorFactory<?>>();
rootList.addValueChangedCallback(new TreeGenerator(rootList, model));
registerPropertyEditorFactory(String.class, new StringEditorFactory());
}
public SimplePropertyList getPropertyList() {
return rootList;
}
public<T> void registerPropertyEditorFactory(Class<T> clazz, PropertyEditorFactory<T> factory) {
if(clazz == null) {
throw new NullPointerException("clazz");
}
if(factory == null) {
throw new NullPointerException("factory");
}
factories.put(clazz, factory);
}
@Override
public void setModel(TreeTableModel model) {
if(model instanceof Model) {
super.setModel(model);
} else {
throw new UnsupportedOperationException("Do not call this method");
}
}
@Override
protected void applyTheme(ThemeInfo themeInfo) {
super.applyTheme(themeInfo);
applyThemePropertiesSheet(themeInfo);
}
protected void applyThemePropertiesSheet(ThemeInfo themeInfo) {
applyCellRendererTheme(subListRenderer);
applyCellRendererTheme(editorRenderer);
}
@Override
protected CellRenderer getCellRenderer(int row, int col, TreeTableNode node) {
if(node == null) {
node = getNodeFromRow(row);
}
if(node instanceof ListNode) {
if(col == 0) {
PropertyListCellRenderer cr = subListRenderer;
NodeState nodeState = getOrCreateNodeState(node);
cr.setCellData(row, col, node.getData(col), nodeState);
return cr;
} else {
return null;
}
} else if(col == 0) {
return super.getCellRenderer(row, col, node);
} else {
CellRenderer cr = editorRenderer;
cr.setCellData(row, col, node.getData(col));
return cr;
}
}
@SuppressWarnings("unchecked")
TreeTableNode createNode(TreeTableNode parent, Property<?> property) {
if(property.getType() == PropertyList.class) {
return new ListNode(parent, property);
} else {
Class<?> type = property.getType();
PropertyEditorFactory factory = factories.get(type);
if(factory != null) {
PropertyEditor editor = factory.createEditor(property);
if(editor != null) {
return new LeafNode(parent, property, editor);
}
} else {
Logger.getLogger(PropertySheet.class.getName()).log(Level.WARNING, "No property editor factory for type {0}", type);
}
return null;
}
}
interface PSTreeTableNode extends TreeTableNode {
public void addChild(TreeTableNode parent);
public void removeAllChildren();
}
static abstract class PropertyNode extends AbstractTreeTableNode implements Runnable, PSTreeTableNode {
protected final Property<?> property;
@SuppressWarnings("LeakingThisInConstructor")
public PropertyNode(TreeTableNode parent, Property<?> property) {
super(parent);
this.property = property;
property.addValueChangedCallback(this);
}
protected void removeCallback() {
property.removeValueChangedCallback(this);
}
@Override
public void removeAllChildren() {
super.removeAllChildren();
}
public void addChild(TreeTableNode parent) {
insertChild(parent, getNumChildren());
}
}
class TreeGenerator implements Runnable {
private final PropertyList list;
private final PSTreeTableNode parent;
public TreeGenerator(PropertyList list, PSTreeTableNode parent) {
this.list = list;
this.parent = parent;
}
public void run() {
parent.removeAllChildren();
addSubProperties();
}
void removeChildCallbacks(PSTreeTableNode parent) {
for(int i=0,n=parent.getNumChildren() ; i<n ; ++i) {
((PropertyNode)parent.getChild(i)).removeCallback();
}
}
void addSubProperties() {
for(int i=0 ; i<list.getNumProperties() ; ++i) {
TreeTableNode node = createNode(parent, list.getProperty(i));
if(node != null) {
parent.addChild(node);
}
}
}
}
static class LeafNode extends PropertyNode {
private final PropertyEditor editor;
public LeafNode(TreeTableNode parent, Property<?> property, PropertyEditor editor) {
super(parent, property);
this.editor = editor;
setLeaf(true);
}
public Object getData(int column) {
switch(column) {
case 0: return property.getName();
case 1: return editor;
default: return "???";
}
}
public void run() {
editor.valueChanged();
fireNodeChanged();
}
}
class ListNode extends PropertyNode {
protected final TreeGenerator treeGenerator;
public ListNode(TreeTableNode parent, Property<?> property) {
super(parent, property);
this.treeGenerator = new TreeGenerator(
(PropertyList)property.getPropertyValue(), this);
treeGenerator.run();
}
public Object getData(int column) {
return property.getName();
}
public void run() {
treeGenerator.run();
}
@Override
protected void removeCallback() {
super.removeCallback();
treeGenerator.removeChildCallbacks(this);
}
}
class PropertyListCellRenderer extends TreeNodeCellRenderer {
private final Widget bgRenderer;
private final Label textRenderer;
public PropertyListCellRenderer() {
bgRenderer = new Widget();
textRenderer = new Label(bgRenderer.getAnimationState());
textRenderer.setAutoSize(false);
bgRenderer.add(textRenderer);
bgRenderer.setTheme(getTheme());
}
@Override
public int getColumnSpan() {
return 2;
}
@Override
public Widget getCellRenderWidget(int x, int y, int width, int height, boolean isSelected) {
bgRenderer.setPosition(x, y);
bgRenderer.setSize(width, height);
int indent = getIndentation();
textRenderer.setPosition(x + indent, y);
textRenderer.setSize(Math.max(0, width-indent), height);
bgRenderer.getAnimationState().setAnimationState(STATE_SELECTED, isSelected);
return bgRenderer;
}
@Override
public void setCellData(int row, int column, Object data, NodeState nodeState) {
super.setCellData(row, column, data, nodeState);
textRenderer.setText((String)data);
}
@Override
protected void setSubRenderer(int row, int column, Object colData) {
}
}
static class EditorRenderer implements CellRenderer, TreeTable.CellWidgetCreator {
private PropertyEditor editor;
public void applyTheme(ThemeInfo themeInfo) {
}
public Widget getCellRenderWidget(int x, int y, int width, int height, boolean isSelected) {
editor.setSelected(isSelected);
return null;
}
public int getColumnSpan() {
return 1;
}
public int getPreferredHeight() {
return editor.getWidget().getPreferredHeight();
}
public String getTheme() {
return "PropertyEditorCellRender";
}
public void setCellData(int row, int column, Object data) {
editor = (PropertyEditor)data;
}
public Widget updateWidget(Widget existingWidget) {
return editor.getWidget();
}
public void positionWidget(Widget widget, int x, int y, int w, int h) {
if(!editor.positionWidget(x, y, w, h)) {
widget.setPosition(x, y);
widget.setSize(w, h);
}
}
}
static class Model extends AbstractTreeTableModel implements PSTreeTableNode {
public String getColumnHeaderText(int column) {
switch(column) {
case 0: return "Name";
case 1: return "Value";
default: return "???";
}
}
public int getNumColumns() {
return 2;
}
@Override
public void removeAllChildren() {
super.removeAllChildren();
}
public void addChild(TreeTableNode parent) {
insertChild(parent, getNumChildren());
}
}
static class StringEditor implements PropertyEditor, EditField.Callback {
private final EditField editField;
private final Property<String> property;
@SuppressWarnings("LeakingThisInConstructor")
public StringEditor(Property<String> property) {
this.property = property;
this.editField = new EditField();
editField.addCallback(this);
resetValue();
}
public Widget getWidget() {
return editField;
}
public void valueChanged() {
resetValue();
}
public void preDestroy() {
editField.removeCallback(this);
}
public void setSelected(boolean selected) {
}
public void callback(int key) {
if(key == Event.KEY_ESCAPE) {
resetValue();
} else if(!property.isReadOnly()) {
try {
property.setPropertyValue(editField.getText());
editField.setErrorMessage(null);
} catch (IllegalArgumentException ex) {
editField.setErrorMessage(ex.getMessage());
}
}
}
private void resetValue() {
editField.setText(property.getPropertyValue());
editField.setErrorMessage(null);
editField.setReadOnly(property.isReadOnly());
}
public boolean positionWidget(int x, int y, int width, int height) {
return false;
}
}
static class StringEditorFactory implements PropertyEditorFactory<String> {
public PropertyEditor createEditor(Property<String> property) {
return new StringEditor(property);
}
}
public static class ComboBoxEditor<T> implements PropertyEditor, Runnable {
protected final ComboBox<T> comboBox;
protected final Property<T> property;
protected final ListModel<T> model;
@SuppressWarnings({"LeakingThisInConstructor", "OverridableMethodCallInConstructor"})
public ComboBoxEditor(Property<T> property, ListModel<T> model) {
this.property = property;
this.comboBox = new ComboBox<T>(model);
this.model = model;
comboBox.addCallback(this);
resetValue();
}
public Widget getWidget() {
return comboBox;
}
public void valueChanged() {
resetValue();
}
public void preDestroy() {
comboBox.removeCallback(this);
}
public void setSelected(boolean selected) {
}
public void run() {
if(property.isReadOnly()) {
resetValue();
} else {
int idx = comboBox.getSelected();
if(idx >= 0) {
property.setPropertyValue(model.getEntry(idx));
}
}
}
protected void resetValue() {
comboBox.setSelected(findEntry(property.getPropertyValue()));
}
protected int findEntry(T value) {
for(int i=0,n=model.getNumEntries() ; i<n ; i++) {
if(model.getEntry(i).equals(value)) {
return i;
}
}
return -1;
}
public boolean positionWidget(int x, int y, int width, int height) {
return false;
}
}
public static class ComboBoxEditorFactory<T> implements PropertyEditorFactory<T> {
private final ModelForwarder modelForwarder;
public ComboBoxEditorFactory(ListModel<T> model) {
this.modelForwarder = new ModelForwarder(model);
}
public ListModel<T> getModel() {
return modelForwarder.getModel();
}
public void setModel(ListModel<T> model) {
modelForwarder.setModel(model);
}
public PropertyEditor createEditor(Property<T> property) {
return new ComboBoxEditor<T>(property, modelForwarder);
}
class ModelForwarder extends AbstractListModel<T> implements ListModel.ChangeListener {
private ListModel<T> model;
@SuppressWarnings("OverridableMethodCallInConstructor")
public ModelForwarder(ListModel<T> model) {
setModel(model);
}
public int getNumEntries() {
return model.getNumEntries();
}
public T getEntry(int index) {
return model.getEntry(index);
}
public Object getEntryTooltip(int index) {
return model.getEntryTooltip(index);
}
public boolean matchPrefix(int index, String prefix) {
return model.matchPrefix(index, prefix);
}
public ListModel<T> getModel() {
return model;
}
public void setModel(ListModel<T> model) {
if(this.model != null) {
this.model.removeChangeListener(this);
}
this.model = model;
this.model.addChangeListener(this);
fireAllChanged();
}
public void entriesInserted(int first, int last) {
fireEntriesInserted(first, last);
}
public void entriesDeleted(int first, int last) {
fireEntriesDeleted(first, last);
}
public void entriesChanged(int first, int last) {
fireEntriesChanged(first, last);
}
public void allChanged() {
fireAllChanged();
}
}
}
}