diff options
Diffstat (limited to 'src/com/vaadin')
44 files changed, 5761 insertions, 858 deletions
diff --git a/src/com/vaadin/Application.java b/src/com/vaadin/Application.java index 9a0c180a1d..61df7c9f9a 100644 --- a/src/com/vaadin/Application.java +++ b/src/com/vaadin/Application.java @@ -17,7 +17,6 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.Locale; import java.util.Properties; -import java.util.Random; import com.vaadin.service.ApplicationContext; import com.vaadin.terminal.ApplicationResource; @@ -92,9 +91,10 @@ public abstract class Application implements URIHandler, Terminal.ErrorListener, Serializable { /** - * Random window name generator. + * Id use for the next window that is opened. Access to this must be + * synchronized. */ - private static Random nameGenerator = new Random(); + private int nextWindowId = 1; /** * Application context the application is running in. @@ -327,7 +327,11 @@ public abstract class Application implements URIHandler, while (!accepted) { // Try another name - name = String.valueOf(Math.abs(nameGenerator.nextInt())); + synchronized (this) { + name = String.valueOf(nextWindowId); + nextWindowId++; + } + if (!windows.containsKey(name)) { accepted = true; } diff --git a/src/com/vaadin/annotations/package.html b/src/com/vaadin/annotations/package.html new file mode 100644 index 0000000000..d789e9b5df --- /dev/null +++ b/src/com/vaadin/annotations/package.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> +</head> + +<body bgcolor="white"> + +<p>Contains annotations used in Vaadin. Note that some annotations +are also found in other packages e.g., {@link com.vaadin.ui.ClientWidget}.</p> + +</body> +</html> diff --git a/src/com/vaadin/data/Validator.java b/src/com/vaadin/data/Validator.java index 665fec4156..21a9da9f97 100644 --- a/src/com/vaadin/data/Validator.java +++ b/src/com/vaadin/data/Validator.java @@ -11,16 +11,23 @@ import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; /** - * Object validator interface. Implementors of this class can be added to any - * {@link com.vaadin.data.Validatable} object to verify its value. The - * <code>Validatable#isValid(Object)</code> iterates all registered - * <code>Validator</code>s, calling their {@link #validate(Object)} methods. - * <code>validate(Object)</code> should throw the - * {@link Validator.InvalidValueException} if the given value is not valid by - * its standards. - * - * Validators should not have side effects on other objects as they can be - * called from Paintable.paint(). + * Interface that implements a method for validating if an {@link Object} is + * valid or not. + * <p> + * Implementors of this class can be added to any + * {@link com.vaadin.data.Validatable Validatable} implementor to verify its + * value. + * </p> + * <p> + * {@link #isValid(Object)} and {@link #validate(Object)} can be used to check + * if a value is valid. {@link #isValid(Object)} and {@link #validate(Object)} + * must use the same validation logic so that iff {@link #isValid(Object)} + * returns false, {@link #validate(Object)} throws an + * {@link InvalidValueException}. + * </p> + * <p> + * Validators must not have any side effects. + * </p> * * @author IT Mill Ltd. * @version @@ -30,29 +37,31 @@ import com.vaadin.terminal.PaintTarget; public interface Validator extends Serializable { /** - * Checks the given value against this validator. If the value is valid this - * method should do nothing, and if it's not valid, it should throw - * <code>Validator.InvalidValueException</code> + * Checks the given value against this validator. If the value is valid the + * method does nothing. If the value is invalid, an + * {@link InvalidValueException} is thrown. * * @param value * the value to check * @throws Validator.InvalidValueException - * if the value is not valid + * if the value is invalid */ public void validate(Object value) throws Validator.InvalidValueException; /** - * Tests if the given value is valid. + * Tests if the given value is valid. This method must be symmetric with + * {@link #validate(Object)} so that {@link #validate(Object)} throws an + * error iff this method returns false. * * @param value * the value to check - * @return <code>true</code> for valid value, otherwise <code>false</code>. + * @return <code>true</code> if the value is valid, <code>false</code> + * otherwise. */ public boolean isValid(Object value); /** - * Invalid value exception can be thrown by {@link Validator} when a given - * value is not valid. + * Exception that is thrown by a {@link Validator} when a value is invalid. * * @author IT Mill Ltd. * @version @@ -63,12 +72,15 @@ public interface Validator extends Serializable { public class InvalidValueException extends RuntimeException implements ErrorMessage { - /** Array of validation errors that are causing the problem. */ + /** + * Array of one or more validation errors that are causing this + * validation error. + */ private InvalidValueException[] causes = null; /** - * Constructs a new <code>InvalidValueException</code> with the - * specified detail message. + * Constructs a new {@code InvalidValueException} with the specified + * message. * * @param message * The detail message of the problem. @@ -78,16 +90,15 @@ public interface Validator extends Serializable { } /** - * Constructs a new <code>InvalidValueException</code> with a set of - * causing validation exceptions. The error message contains first the - * given message and then a list of validation errors in the given - * validatables. + * Constructs a new {@code InvalidValueException} with a set of causing + * validation exceptions. The causing validation exceptions are included + * when the exception is painted to the client. * * @param message * The detail message of the problem. * @param causes - * Array of validatables whos invalidities are possiblity - * causing the invalidity. + * One or more {@code InvalidValueException}s that caused + * this exception. */ public InvalidValueException(String message, InvalidValueException[] causes) { @@ -101,9 +112,12 @@ public interface Validator extends Serializable { } /** - * See if the error message doesn't paint anything visible. + * Check if the error message should be hidden. + * + * An empty (null or "") message is invisible unless it contains nested + * exceptions that are visible. * - * @return True iff the paint method does not paint anything visible. + * @return true if the error message should be hidden, false otherwise */ public boolean isInvisible() { String msg = getMessage(); @@ -120,10 +134,21 @@ public interface Validator extends Serializable { return true; } + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.ErrorMessage#getErrorLevel() + */ public final int getErrorLevel() { return ErrorMessage.ERROR; } + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.Paintable#paint(com.vaadin.terminal.PaintTarget) + */ public void paint(PaintTarget target) throws PaintException { target.startTag("error"); target.addAttribute("level", "error"); @@ -142,33 +167,85 @@ public interface Validator extends Serializable { target.endTag("error"); } - /* Documented in super interface */ + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.ErrorMessage#addListener(com.vaadin.terminal. + * Paintable.RepaintRequestListener) + */ public void addListener(RepaintRequestListener listener) { } - /* Documented in super interface */ + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.ErrorMessage#removeListener(com.vaadin.terminal + * .Paintable.RepaintRequestListener) + */ public void removeListener(RepaintRequestListener listener) { } - /* Documented in super interface */ + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.ErrorMessage#requestRepaint() + */ public void requestRepaint() { } - /* Documented in super interface */ + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Paintable#requestRepaintRequests() + */ public void requestRepaintRequests() { } + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Paintable#getDebugId() + */ public String getDebugId() { return null; } + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Paintable#setDebugId(java.lang.String) + */ public void setDebugId(String id) { throw new UnsupportedOperationException( - "Setting testing id for this Paintable is not implemented"); + "InvalidValueException cannot have a debug id"); + } + + /** + * Returns the {@code InvalidValueExceptions} that caused this + * exception. + * + * @return An array containing the {@code InvalidValueExceptions} that + * caused this exception. Returns an empty array if this + * exception was not caused by other exceptions. + */ + public InvalidValueException[] getCauses() { + return causes; } } + /** + * A specific type of {@link InvalidValueException} that indicates that + * validation failed because the value was empty. What empty means is up to + * the thrower. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.3.0 + */ @SuppressWarnings("serial") public class EmptyValueException extends Validator.InvalidValueException { diff --git a/src/com/vaadin/data/doc-files/Container_full.gif b/src/com/vaadin/data/doc-files/Container_full.gif Binary files differdeleted file mode 100644 index 28ee720e32..0000000000 --- a/src/com/vaadin/data/doc-files/Container_full.gif +++ /dev/null diff --git a/src/com/vaadin/data/doc-files/Container_simple.gif b/src/com/vaadin/data/doc-files/Container_simple.gif Binary files differdeleted file mode 100644 index 6db32581b8..0000000000 --- a/src/com/vaadin/data/doc-files/Container_simple.gif +++ /dev/null diff --git a/src/com/vaadin/data/doc-files/Item.gif b/src/com/vaadin/data/doc-files/Item.gif Binary files differdeleted file mode 100644 index 4cd202ceba..0000000000 --- a/src/com/vaadin/data/doc-files/Item.gif +++ /dev/null diff --git a/src/com/vaadin/data/doc-files/Property.gif b/src/com/vaadin/data/doc-files/Property.gif Binary files differdeleted file mode 100644 index 4aff19db1c..0000000000 --- a/src/com/vaadin/data/doc-files/Property.gif +++ /dev/null diff --git a/src/com/vaadin/data/doc-files/datalayer.gif b/src/com/vaadin/data/doc-files/datalayer.gif Binary files differdeleted file mode 100644 index b3d25639e0..0000000000 --- a/src/com/vaadin/data/doc-files/datalayer.gif +++ /dev/null diff --git a/src/com/vaadin/data/package.html b/src/com/vaadin/data/package.html index f6e0a2d645..a14ea1ac88 100644 --- a/src/com/vaadin/data/package.html +++ b/src/com/vaadin/data/package.html @@ -5,82 +5,45 @@ <body bgcolor="white"> -<p>Provides interfaces for the data layer which contains classes -for typed data values, data collections, and handlers. A -{@link com.vaadin.data.Property Property} is a simple typed data -value; an {@link com.vaadin.data.Item Item} is a collection of -Properties, each corresponding to a unique identifier; a -{@link com.vaadin.data.Container Container} is a collection of -identified Items with special constraints; a -{@link com.vaadin.data.Buffered Buffered} class is able to track -its changes and to commit or discard them later.</p> +<p>Contains interfaces for the data layer, mainly for binding typed +data and data collections to components, and for validating data.</p> -<h2>Package Specification</h2> +<h2>Data binding</h2> -<p>The package contains a three-tiered structure for typed data objects and -collections of them:</p> - -<center> -<p><a href=Property.html target="classFrame"><img src=doc-files/Property.gif></a></p> -<p><a href=Item.html target="classFrame"><img src=doc-files/Item.gif></a></p> -<p><a href=Container.html target="classFrame"><img src=doc-files/Container_simple.gif></a></p> -</center> +<p>The package contains a three-tiered structure for typed data +objects and collections of them:</p> <ul> - <li>The simplest of these is the - {@link com.vaadin.data.Property Property} which represents a - single typed data value. A Property may be read-only in which - case attempts to modify its contents will throw an exception. - - <li>The second level of the data layer is represented by the - {@link com.vaadin.data.Item Item} which embodies a set of - <i>Properties</i>. Each Property in an Item corresponds to a locally - unique(that is, inside the Item) identifier. - - <li>The third level is called the - {@link com.vaadin.data.Container Container} which can be - visualized as a set of Items, each corresponding to a locally unique - identifier. Note that the Container imposes a few restrictions on the - data stored in it, see further documentation in the - <a href=Container.html target="classFrame">class specification</a>. + <li>A {@link com.vaadin.data.Property Property} represents a + single, typed data value. + + <li>An {@link com.vaadin.data.Item Item} embodies a set of <i>Properties</i>. + A locally unique (inside the {@link com.vaadin.data.Item Item}) + Property identifier corresponds to each Property inside the Item.</li> + <li>A {@link com.vaadin.data.Container Container} contains a set + of Items, each corresponding to a locally unique Item identifier. Note + that Container imposes a few restrictions on the data stored in it, see + {@link com.vaadin.data.Container Container} for further information.</li> </ul> -<p>In addition to these interfaces the package contains the -{@link com.vaadin.data.Buffered Buffered} interface, which defines -the methods to make an object buffered, that is, track the changes to an -object and allow committing or discarding them at a later time.</p> - -<p>Provides interfaces for the validation framework. The framework -defines two interfaces; one for classes that need to support external -validation, and another one for the validators themselves.</p> +<p>For more information on the data model, see the <a + href="http://vaadin.com/book/-/page/datamodel.html">Data model +chapter</a> in Book of Vaadin.</p> -<h2>Validation</h2> - -<p>The most important method defined by the -{@link com.vaadin.data.Validatable Validatable} interface is -{@link com.vaadin.data.Validatable#isValid() isValid()}. It -asks all registered validators to verify if the object's value is valid -or not. Note that it depends on the validators registered for a object which -values are valid and which are not. For example, a <code>null</code> value -can be valid value for one validator but invalid for another.<p> +<h2>Buffering</h2> -<p>In addition to <code>isValid()</code>, <code>Validatable</code> defines -methods to add, remove and list validators of a validatable object.</p> +<p>A {@link com.vaadin.data.Buffered Buffered} implementor is able +to track and buffer changes and commit or discard them later.</p> -<p>{@link com.vaadin.data.Validator Validator} defines the -interface for an external validator. These validators may be added to -any <code>Validatable</code> object, and their task is to check, when -requested, that the object which they are attached to contains a valid -value. The actual validation logic is hidden in the -{@link com.vaadin.data.Validator#validate(Object) validate(Object)} -method.</p> +<h2>Validation</h2> -<p>In addition to <code>check(Object)</code>, <code>Validator</code> defines -the <code>InvalidValueException</code> which is used to signal that a -checked value is invalid, and the -{@link com.vaadin.data.Validator.Suggestive Suggestive} -subinterface which includes functionality to suggest a valid value for -the validated object.</p><!-- Put @see and @since tags down here. --> +<p>{@link com.vaadin.data.Validator Validator} implementations are +used to validate data, typically the value of a {@link +com.vaadin.ui.Field Field}. One or more {@link com.vaadin.data.Validator +Validators} can be added to a {@link com.vaadin.data.Validatable +Validatable} implementor and then used to validate the value of the +Validatable. </p> +<!-- Put @see and @since tags down here. --> </body> </html> diff --git a/src/com/vaadin/data/util/BeanItemContainer.java b/src/com/vaadin/data/util/BeanItemContainer.java index e99f219dbb..68ac574df2 100644 --- a/src/com/vaadin/data/util/BeanItemContainer.java +++ b/src/com/vaadin/data/util/BeanItemContainer.java @@ -5,7 +5,7 @@ package com.vaadin.data.util; import java.beans.PropertyDescriptor; import java.io.IOException; -import java.util.ArrayList; +import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -28,13 +28,30 @@ import com.vaadin.data.Property.ValueChangeListener; import com.vaadin.data.Property.ValueChangeNotifier; /** - * An {@link ArrayList} backed container for {@link BeanItem}s. + * An in-memory container for JavaBeans. + * + * <p> + * The properties of the container are determined automatically by introspecting + * the used JavaBean class. Only beans of the same type can be added to the + * container. + * </p> + * + * <p> + * BeanItemContainer uses the beans themselves as identifiers. The + * {@link Object#hashCode()} of a bean is used when storing and looking up beans + * so it must not change during the lifetime of the bean (it should not depend + * on any part of the bean that can be modified). Typically this restricts the + * implementation of {@link Object#equals(Object)} as well in order for it to + * fulfill the contract between {@code equals()} and {@code hashCode()}. + * </p> + * * <p> - * Bean objects act as identifiers. For this reason, they should implement - * Object.equals(Object) and Object.hashCode(). + * It is not possible to add additional properties to the container and nested + * bean properties are not supported. * </p> * * @param <BT> + * The type of the Bean * * @since 5.4 */ @@ -55,17 +72,33 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, private ListSet<BT> allItems = new ListSet<BT>(); /** - * Maps all pojos (item ids) in the container (including filtered) to their - * corresponding BeanItem. + * Maps all beans (item ids) in the container (including filtered) to their + * corresponding BeanItem. This requires the beans to implement + * {@link Object#equals(Object)} and {@link Object#hashCode()} so it is not + * affected by the contents of the bean. */ private final Map<BT, BeanItem<BT>> beanToItem = new HashMap<BT, BeanItem<BT>>(); - // internal data model to obtain property IDs etc. + /** + * The type of the beans in the container. + */ private final Class<? extends BT> type; + + /** + * A description of the properties found in beans of type {@link #type}. + * Determines the property ids that are present in the container. + */ private transient LinkedHashMap<String, PropertyDescriptor> model; + /** + * Collection of listeners interested in + * {@link Container.ItemSetChangeEvent ItemSetChangeEvent} events. + */ private List<ItemSetChangeListener> itemSetChangeListeners; + /** + * Filters currently applied to the container. + */ private Set<Filter> filters = new HashSet<Filter>(); /** @@ -73,7 +106,10 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, */ private ItemSorter itemSorter = new DefaultItemSorter(); - /* Special serialization to handle method references */ + /** + * A special deserialization method that resolves {@link #model} is needed + * as PropertyDescriptor is not {@link Serializable}. + */ private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); @@ -81,12 +117,12 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } /** - * Constructs BeanItemContainer for beans of a given type. + * Constructs a {@code BeanItemContainer} for beans of the given type. * * @param type - * the class of beans to be used with this containers. + * the type of the beans that will be added to the container. * @throws IllegalArgumentException - * If the type is null + * If {@code type} is null */ public BeanItemContainer(Class<? extends BT> type) { if (type == null) { @@ -98,11 +134,13 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } /** - * Constructs BeanItemContainer with given collection of beans in it. The - * collection must not be empty or an IllegalArgument is thrown. + * Constructs a {@code BeanItemContainer} and adds the given beans to it. + * The collection must not be empty. + * {@link BeanItemContainer#BeanItemContainer(Class)} can be used for + * creating an initially empty {@code BeanItemContainer}. * * @param collection - * non empty {@link Collection} of beans. + * a non empty {@link Collection} of beans. * @throws IllegalArgumentException * If the collection is null or empty. */ @@ -119,6 +157,13 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, addAll(collection); } + /** + * Adds all the beans in {@code collection} in one go. More efficient than + * adding them one by one. + * + * @param collection + * The collection of beans to add. Must not be null. + */ private void addAll(Collection<BT> collection) { // Pre-allocate space for the collection allItems.ensureCapacity(allItems.size() + collection.size()); @@ -135,20 +180,23 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } /** - * Unsupported operation. - * - * @see com.vaadin.data.Container.Indexed#addItemAt(int) + * Unsupported operation. Beans should be added through + * {@link #addItemAt(int, Object)}. */ public Object addItemAt(int index) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** - * Adds new item at given index. + * Adds a new bean at the given index. * * The bean is used both as the item contents and as the item identifier. * - * @see com.vaadin.data.Container.Indexed#addItemAt(int, Object) + * @param index + * Index at which the bean should be added. + * @param newItemId + * The bean to add to the container. + * @return Returns the new BeanItem or null if the operation fails. */ public BeanItem<BT> addItemAt(int index, Object newItemId) throws UnsupportedOperationException { @@ -164,7 +212,7 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } /** - * Adds new item at given index of the internal (unfiltered) list. + * Adds a bean at the given index of the internal (unfiltered) list. * <p> * The item is also added in the visible part of the list if it passes the * filters. @@ -188,8 +236,7 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, /** * Adds the bean to all internal data structures at the given position. * Fails if the bean is already in the container or is not assignable to the - * correct type. Returns the new BeanItem if the bean was added - * successfully. + * correct type. Returns a new BeanItem if the bean was added successfully. * * <p> * Caller should call {@link #filterAll()} after calling this method to @@ -229,19 +276,26 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, return beanItem; } - @SuppressWarnings("unchecked") + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#getIdByIndex(int) + */ public BT getIdByIndex(int index) { return filteredItems.get(index); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#indexOfId(java.lang.Object) + */ public int indexOfId(Object itemId) { return filteredItems.indexOf(itemId); } /** - * Unsupported operation. - * - * @see com.vaadin.data.Container.Ordered#addItemAfter(Object) + * Unsupported operation. Use {@link #addItemAfter(Object, Object)}. */ public Object addItemAfter(Object previousItemId) throws UnsupportedOperationException { @@ -249,7 +303,7 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } /** - * Adds new item after the given item. + * Adds the bean after the given bean. * * The bean is used both as the item contents and as the item identifier. * @@ -268,6 +322,11 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#firstItemId() + */ public BT firstItemId() { if (size() > 0) { return getIdByIndex(0); @@ -276,14 +335,29 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object) + */ public boolean isFirstId(Object itemId) { return firstItemId() == itemId; } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object) + */ public boolean isLastId(Object itemId) { return lastItemId() == itemId; } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#lastItemId() + */ public BT lastItemId() { if (size() > 0) { return getIdByIndex(size() - 1); @@ -292,6 +366,11 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object) + */ public BT nextItemId(Object itemId) { int index = indexOfId(itemId); if (index >= 0 && index < size() - 1) { @@ -302,6 +381,11 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object) + */ public BT prevItemId(Object itemId) { int index = indexOfId(itemId); if (index > 0) { @@ -312,13 +396,17 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /** + * Unsupported operation. Properties are determined by the introspecting the + * bean class. + */ public boolean addContainerProperty(Object propertyId, Class<?> type, Object defaultValue) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** - * Unsupported operation. + * Unsupported operation. Use {@link #addBean(Object)}. * * @see com.vaadin.data.Container#addItem() */ @@ -327,7 +415,7 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } /** - * Creates a new Item with the bean into the Container. + * Adds the bean to the Container. * * The bean is used both as the item contents and as the item identifier. * @@ -338,7 +426,7 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } /** - * Creates a new Item with the bean into the Container. + * Adds the bean to the Container. * * The bean is used both as the item contents and as the item identifier. * @@ -355,32 +443,68 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#containsId(java.lang.Object) + */ public boolean containsId(Object itemId) { // only look at visible items after filtering return filteredItems.contains(itemId); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object, + * java.lang.Object) + */ public Property getContainerProperty(Object itemId, Object propertyId) { return getItem(itemId).getItemProperty(propertyId); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerPropertyIds() + */ public Collection<String> getContainerPropertyIds() { return model.keySet(); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItem(java.lang.Object) + */ public BeanItem<BT> getItem(Object itemId) { return beanToItem.get(itemId); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItemIds() + */ @SuppressWarnings("unchecked") public Collection<BT> getItemIds() { return (Collection<BT>) filteredItems.clone(); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getType(java.lang.Object) + */ public Class<?> getType(Object propertyId) { return model.get(propertyId).getPropertyType(); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ public boolean removeAllItems() throws UnsupportedOperationException { allItems.clear(); filteredItems.clear(); @@ -393,11 +517,20 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, return true; } + /** + * Unsupported operation. Properties are determined by the introspecting the + * bean class. + */ public boolean removeContainerProperty(Object propertyId) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ public boolean removeItem(Object itemId) throws UnsupportedOperationException { if (!allItems.remove(itemId)) { @@ -412,6 +545,15 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, return true; } + /** + * Make this container listen to the given property provided it notifies + * when its value changes. + * + * @param beanItem + * The BeanItem that contains the property + * @param propertyId + * The id of the property + */ private void addValueChangeListener(BeanItem<BT> beanItem, Object propertyId) { Property property = beanItem.getItemProperty(propertyId); if (property instanceof ValueChangeNotifier) { @@ -423,6 +565,14 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /** + * Remove this container as a listener for the given property. + * + * @param item + * The BeanItem that contains the property + * @param propertyId + * The id of the property + */ private void removeValueChangeListener(BeanItem<BT> item, Object propertyId) { Property property = item.getItemProperty(propertyId); if (property instanceof ValueChangeNotifier) { @@ -430,16 +580,33 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /** + * Remove this contains as a listener for all the properties in the given + * BeanItem. + * + * @param item + * The BeanItem that contains the properties + */ private void removeAllValueChangeListeners(BeanItem<BT> item) { for (Object propertyId : item.getItemPropertyIds()) { removeValueChangeListener(item, propertyId); } } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#size() + */ public int size() { return filteredItems.size(); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ public Collection<Object> getSortableContainerPropertyIds() { LinkedList<Object> sortables = new LinkedList<Object>(); for (Object propertyId : getContainerPropertyIds()) { @@ -479,6 +646,13 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, Collections.sort(allItems, getItemSorter()); } + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin + * .data.Container.ItemSetChangeListener) + */ public void addListener(ItemSetChangeListener listener) { if (itemSetChangeListeners == null) { itemSetChangeListeners = new LinkedList<ItemSetChangeListener>(); @@ -486,12 +660,22 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, itemSetChangeListeners.add(listener); } + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin + * .data.Container.ItemSetChangeListener) + */ public void removeListener(ItemSetChangeListener listener) { if (itemSetChangeListeners != null) { itemSetChangeListeners.remove(listener); } } + /** + * Send an ItemSetChange event to all listeners. + */ private void fireItemSetChange() { if (itemSetChangeListeners != null) { final Container.ItemSetChangeEvent event = new Container.ItemSetChangeEvent() { @@ -505,6 +689,13 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.Filterable#addContainerFilter(java.lang.Object, + * java.lang.String, boolean, boolean) + */ public void addContainerFilter(Object propertyId, String filterString, boolean ignoreCase, boolean onlyMatchPrefix) { if (filters.isEmpty()) { @@ -542,6 +733,13 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /** + * Remove (from the filtered list) any items that do not match the given + * filter. + * + * @param f + * The filter used to determine if items should be removed + */ protected void filter(Filter f) { Iterator<BT> iterator = filteredItems.iterator(); while (iterator.hasNext()) { @@ -552,6 +750,11 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Filterable#removeAllContainerFilters() + */ public void removeAllContainerFilters() { if (!filters.isEmpty()) { filters = new HashSet<Filter>(); @@ -563,6 +766,13 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.Filterable#removeContainerFilters(java.lang + * .Object) + */ public void removeContainerFilters(Object propertyId) { if (!filters.isEmpty()) { for (Iterator<Filter> iterator = filters.iterator(); iterator @@ -580,15 +790,37 @@ public class BeanItemContainer<BT> implements Indexed, Sortable, Filterable, } } + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Property.ValueChangeListener#valueChange(com.vaadin.data + * .Property.ValueChangeEvent) + */ public void valueChange(ValueChangeEvent event) { // if a property that is used in a filter is changed, refresh filtering filterAll(); } + /** + * Returns the ItemSorter that is used for sorting the container. + * + * @see #setItemSorter(ItemSorter) + * + * @return The ItemSorter that is used for sorting the container + */ public ItemSorter getItemSorter() { return itemSorter; } + /** + * Sets the ItemSorter that is used for sorting the container. The + * {@link ItemSorter#compare(Object, Object)} method is called to compare + * two beans (item ids). + * + * @param itemSorter + * The ItemSorter to use when sorting the container + */ public void setItemSorter(ItemSorter itemSorter) { this.itemSorter = itemSorter; } diff --git a/src/com/vaadin/data/util/DefaultItemSorter.java b/src/com/vaadin/data/util/DefaultItemSorter.java index 1e83715ace..a2ecd38ef8 100644 --- a/src/com/vaadin/data/util/DefaultItemSorter.java +++ b/src/com/vaadin/data/util/DefaultItemSorter.java @@ -113,7 +113,10 @@ public class DefaultItemSorter implements ItemSorter { * The first item to compare. * @param item2 * The second item to compare. - * @return + * @return a negative, zero, or positive integer if the property value in + * the first item is less than, equal to, or greater than the + * property value in the second item. Negated if {@code + * sortDirection} is false. */ protected int compareProperty(Object propertyId, boolean sortDirection, Item item1, Item item2) { diff --git a/src/com/vaadin/data/util/HierarchicalContainer.java b/src/com/vaadin/data/util/HierarchicalContainer.java index 121d1cc936..108c544198 100644 --- a/src/com/vaadin/data/util/HierarchicalContainer.java +++ b/src/com/vaadin/data/util/HierarchicalContainer.java @@ -69,6 +69,10 @@ public class HierarchicalContainer extends IndexedContainer implements */ private boolean includeParentsWhenFiltering = true; + private boolean contentChangedEventsDisabled = false; + + private boolean contentsChangedEventPending; + /* * Can the specified Item have any children? Don't add a JavaDoc comment * here, we use the default documentation from implemented interface. @@ -378,6 +382,7 @@ public class HierarchicalContainer extends IndexedContainer implements */ @Override public Object addItem() { + disableContentsChangeEvents(); final Object itemId = super.addItem(); if (itemId == null) { return null; @@ -391,10 +396,35 @@ public class HierarchicalContainer extends IndexedContainer implements } } } - + enableAndFireContentsChangeEvents(); return itemId; } + @Override + protected void fireContentsChange(int addedItemIndex) { + if (contentsChangeEventsOn()) { + super.fireContentsChange(addedItemIndex); + } else { + contentsChangedEventPending = true; + } + } + + private boolean contentsChangeEventsOn() { + return !contentChangedEventsDisabled; + } + + private void disableContentsChangeEvents() { + contentChangedEventsDisabled = true; + } + + private void enableAndFireContentsChangeEvents() { + contentChangedEventsDisabled = false; + if (contentsChangedEventPending) { + fireContentsChange(-1); + } + contentsChangedEventPending = false; + } + /* * (non-Javadoc) * @@ -402,6 +432,7 @@ public class HierarchicalContainer extends IndexedContainer implements */ @Override public Item addItem(Object itemId) { + disableContentsChangeEvents(); final Item item = super.addItem(itemId); if (item == null) { return null; @@ -414,7 +445,7 @@ public class HierarchicalContainer extends IndexedContainer implements filteredRoots.add(itemId); } } - + enableAndFireContentsChangeEvents(); return item; } @@ -425,6 +456,7 @@ public class HierarchicalContainer extends IndexedContainer implements */ @Override public boolean removeAllItems() { + disableContentsChangeEvents(); final boolean success = super.removeAllItems(); if (success) { @@ -439,6 +471,7 @@ public class HierarchicalContainer extends IndexedContainer implements filteredChildren = null; } } + enableAndFireContentsChangeEvents(); return success; } @@ -449,6 +482,7 @@ public class HierarchicalContainer extends IndexedContainer implements */ @Override public boolean removeItem(Object itemId) { + disableContentsChangeEvents(); final boolean success = super.removeItem(itemId); if (success) { @@ -496,6 +530,8 @@ public class HierarchicalContainer extends IndexedContainer implements noChildrenAllowed.remove(itemId); } + enableAndFireContentsChangeEvents(); + return success; } @@ -508,7 +544,10 @@ public class HierarchicalContainer extends IndexedContainer implements * @return true if the operation succeeded */ public boolean removeItemRecursively(Object itemId) { - return removeItemRecursively(this, itemId); + disableContentsChangeEvents(); + boolean removeItemRecursively = removeItemRecursively(this, itemId); + enableAndFireContentsChangeEvents(); + return removeItemRecursively; } /** diff --git a/src/com/vaadin/data/util/package.html b/src/com/vaadin/data/util/package.html index 332d8f8143..07e3acde9e 100644 --- a/src/com/vaadin/data/util/package.html +++ b/src/com/vaadin/data/util/package.html @@ -6,32 +6,13 @@ <body bgcolor="white"> -<p>Provides various utility classes that implement the data layer -functionality.</p> +<p>Provides implementations of Property, Item and Container +interfaces, and utilities for the data layer.</p> -<p>The first {@link com.vaadin.data.Property Property} class, -{@link com.vaadin.data.util.ObjectProperty ObjectProperty}, provides -a simple class containing a typed data value. The second, -{@link com.vaadin.data.util.MethodProperty MethodProperty}, provides -a way to bind a field of an object to the Property interface using the -accessor methods for the field.</p> - -<p>The next level of the data layer, the -{@link com.vaadin.data.Item Item}, is implemented by -{@link com.vaadin.data.util.BeanItem BeanItem}, though it is only a -simple wrapper to the former to provide the Item interface for any regular -Java Bean.</p> - -<p>The third level, the {@link com.vaadin.data.Container Container}, -has several implementations in the {@link com.vaadin.data.util} -package.</p> - -<!-- <h2>Package Specification</h2> --> - -<!-- Package spec here --> - - -<!-- Put @see and @since tags down here. --> +<p>Various Property, Item and Container implementations are provided +in this package. Each implementation can have its own sets of +constraints on the data it encapsulates and on how the implementation +can be used. See the class javadocs for more information.</p> </body> </html> diff --git a/src/com/vaadin/data/validator/AbstractStringValidator.java b/src/com/vaadin/data/validator/AbstractStringValidator.java index 42dc5f25d9..359d83ef10 100644 --- a/src/com/vaadin/data/validator/AbstractStringValidator.java +++ b/src/com/vaadin/data/validator/AbstractStringValidator.java @@ -1,57 +1,71 @@ /* @ITMillApache2LicenseForJavaFiles@ */ -package com.vaadin.data.validator;
-
-/**
- * Validator base class for validating strings. See
- * {@link com.vaadin.data.validator.AbstractValidator} for more information.
- *
- * <p>
- * If the validation fails, the exception thrown contains the error message with
- * its argument 0 replaced with the string being validated.
- * </p>
- *
- * @author IT Mill Ltd.
- * @version
- * @VERSION@
- * @since 5.4
- */
-@SuppressWarnings("serial")
-public abstract class AbstractStringValidator extends AbstractValidator {
-
- /**
- * Constructs a validator for strings.
- * <p>
- * Null and empty string values are always accepted. To disallow empty
- * values, set the field being validated as required.
- * </p>
- *
- * @param errorMessage
- * the message included in the exception (with its parameter {0}
- * replaced by the string to be validated) in case the validation
- * fails
- */
- public AbstractStringValidator(String errorMessage) {
- super(errorMessage);
- }
-
- public boolean isValid(Object value) {
- if (value == null) {
- return true;
- }
- if (!(value instanceof String)) {
- return false;
- }
- return isValidString((String) value);
- }
-
- /**
- * Checks if the given string is valid.
- *
- * @param value
- * String to check. Can never be null.
- * @return true if the string is valid, false otherwise
- */
- protected abstract boolean isValidString(String value);
-}
+package com.vaadin.data.validator; + +/** + * Validator base class for validating strings. See + * {@link com.vaadin.data.validator.AbstractValidator} for more information. + * + * <p> + * To include the value that failed validation in the exception message you can + * use "{0}" in the error message. This will be replaced with the failed value + * (converted to string using {@link #toString()}) or "null" if the value is + * null. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.4 + */ +@SuppressWarnings("serial") +public abstract class AbstractStringValidator extends AbstractValidator { + + /** + * Constructs a validator for strings. + * + * <p> + * Null and empty string values are always accepted. To reject empty values, + * set the field being validated as required. + * </p> + * + * @param errorMessage + * the message to be included in an {@link InvalidValueException} + * (with "{0}" replaced by the value that failed validation). + * */ + public AbstractStringValidator(String errorMessage) { + super(errorMessage); + } + + /** + * Tests if the given value is a valid string. + * <p> + * Null values are always accepted. Values that are not {@link String}s are + * always rejected. Uses {@link #isValidString(String)} to validate the + * value. + * </p> + * + * @param value + * the value to check + * @return true if the value is a valid string, false otherwise + */ + public boolean isValid(Object value) { + if (value == null) { + return true; + } + if (!(value instanceof String)) { + return false; + } + return isValidString((String) value); + } + + /** + * Checks if the given string is valid. + * + * @param value + * String to check. Can never be null. + * @return true if the string is valid, false otherwise + */ + protected abstract boolean isValidString(String value); +} diff --git a/src/com/vaadin/data/validator/AbstractValidator.java b/src/com/vaadin/data/validator/AbstractValidator.java index af78faa442..84cf379f31 100644 --- a/src/com/vaadin/data/validator/AbstractValidator.java +++ b/src/com/vaadin/data/validator/AbstractValidator.java @@ -1,71 +1,79 @@ /* @ITMillApache2LicenseForJavaFiles@ */ -package com.vaadin.data.validator;
-
-import com.vaadin.data.Validator;
-
-/**
- * Default Validator base class. See {@link com.vaadin.data.validator.Validator}
- * for more information.
- * <p>
- * If the validation fails, the exception thrown contains the error message with
- * its argument 0 replaced with the toString() of the object being validated.
- * </p>
- *
- * @author IT Mill Ltd.
- * @version
- * @VERSION@
- * @since 5.4
- */
-@SuppressWarnings("serial")
-public abstract class AbstractValidator implements Validator {
-
- /**
- * Error message.
- */
- private String errorMessage;
-
- /**
- * Constructs a validator with an error message.
- *
- * @param errorMessage
- * the message included in the exception (with its parameter {0}
- * replaced by toString() of the object to be validated) in case
- * the validation fails
- */
- public AbstractValidator(String errorMessage) {
- this.errorMessage = errorMessage;
- }
-
- public void validate(Object value) throws InvalidValueException {
- if (!isValid(value)) {
- String message;
- if (value == null) {
- message = errorMessage.replace("{0}", "null");
- } else {
- message = errorMessage.replace("{0}", value.toString());
- }
- throw new InvalidValueException(message);
- }
- }
-
- /**
- * Gets the message to be displayed in case the value does not validate.
- *
- * @return the Error Message.
- */
- public String getErrorMessage() {
- return errorMessage;
- }
-
- /**
- * Sets the message to be displayed in case the value does not validate.
- *
- * @param errorMessage
- * the Error Message to set.
- */
- public void setErrorMessage(String errorMessage) {
- this.errorMessage = errorMessage;
- }
-}
+package com.vaadin.data.validator; + +import com.vaadin.data.Validator; + +/** + * Abstract {@link com.vaadin.data.validator.Validator Validator} implementation + * that provides a basic Validator implementation except the + * {@link #isValid(Object)} method. Sub-classes need to implement the + * {@link #isValid(Object)} method. + * <p> + * To include the value that failed validation in the exception message you can + * use "{0}" in the error message. This will be replaced with the failed value + * (converted to string using {@link #toString()}) or "null" if the value is + * null. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.4 + */ +@SuppressWarnings("serial") +public abstract class AbstractValidator implements Validator { + + /** + * Error message that is included in an {@link InvalidValueException} if + * such is thrown. + */ + private String errorMessage; + + /** + * Constructs a validator with the given error message. + * + * @param errorMessage + * the message to be included in an {@link InvalidValueException} + * (with "{0}" replaced by the value that failed validation). + */ + public AbstractValidator(String errorMessage) { + this.errorMessage = errorMessage; + } + + public void validate(Object value) throws InvalidValueException { + if (!isValid(value)) { + String message; + if (value == null) { + message = errorMessage.replace("{0}", "null"); + } else { + message = errorMessage.replace("{0}", value.toString()); + } + throw new InvalidValueException(message); + } + } + + /** + * Returns the message to be included in the exception in case the value + * does not validate. + * + * @return the error message provided in the constructor or using + * {@link #setErrorMessage(String)}. + */ + public String getErrorMessage() { + return errorMessage; + } + + /** + * Sets the message to be included in the exception in case the value does + * not validate. The exception message is typically shown to the end user. + * + * @param errorMessage + * the error message. "{0}" is automatically replaced by the + * value that did not validate. + */ + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/src/com/vaadin/data/validator/package.html b/src/com/vaadin/data/validator/package.html index 8697eb6313..c991bfc82a 100644 --- a/src/com/vaadin/data/validator/package.html +++ b/src/com/vaadin/data/validator/package.html @@ -8,13 +8,16 @@ <!-- Package summary here --> -<p>Provides validators for data contained in data-bound objects..</p> +<p>Provides various {@link com.vaadin.data.Validator} +implementations.</p> -<!-- <h2>Package Specification</h2> --> +<p>{@link com.vaadin.data.validator.AbstractValidator +AbstractValidator} provides an abstract implementation of the {@link +com.vaadin.data.Validator} interface and can be extended for custom +validation needs. {@link +com.vaadin.data.validator.AbstractStringValidator +AbstractStringValidator} can also be extended if the value is a String.</p> -<!-- Package spec here --> - -<!-- Put @see and @since tags down here. --> </body> </html> diff --git a/src/com/vaadin/launcher/DemoLauncher.java b/src/com/vaadin/launcher/DemoLauncher.java index d52df1f8ae..38b6060a27 100644 --- a/src/com/vaadin/launcher/DemoLauncher.java +++ b/src/com/vaadin/launcher/DemoLauncher.java @@ -34,7 +34,7 @@ import com.vaadin.launcher.util.BrowserLauncher; */ public class DemoLauncher { - public static void main(String[] args) { + public static void main(String[] args) throws Exception { final Map serverArgs = DevelopmentServerLauncher.parseArguments(args); boolean deployed = false; @@ -59,7 +59,7 @@ public class DemoLauncher { } } - // Start the Winstone servlet container + // Start the Jetty servlet container final String url = DevelopmentServerLauncher.runServer(serverArgs, "Demo Server"); diff --git a/src/com/vaadin/launcher/DevelopmentServerLauncher.java b/src/com/vaadin/launcher/DevelopmentServerLauncher.java index b7caa881b1..8e6a71b2a6 100644 --- a/src/com/vaadin/launcher/DevelopmentServerLauncher.java +++ b/src/com/vaadin/launcher/DevelopmentServerLauncher.java @@ -30,6 +30,7 @@ public class DevelopmentServerLauncher { * for options.
*
* @param args
+ * @throws Exception
*/
public static void main(String[] args) {
@@ -38,16 +39,19 @@ public class DevelopmentServerLauncher { // Start Jetty
System.out.println("Starting Jetty servlet container.");
- final String url = runServer(serverArgs, "Development Server Mode");
-
- // Start Browser
- if (!serverArgs.containsKey("nogui") && url != null) {
- System.out.println("Starting Web Browser.");
+ String url;
+ try {
+ url = runServer(serverArgs, "Development Server Mode");
+ // Start Browser
+ if (!serverArgs.containsKey("nogui") && url != null) {
+ System.out.println("Starting Web Browser.");
- // Open browser into application URL
- BrowserLauncher.openBrowser(url);
+ // Open browser into application URL
+ BrowserLauncher.openBrowser(url);
+ }
+ } catch (Exception e) {
+ // NOP exception already on console by jetty
}
-
}
/**
@@ -55,9 +59,11 @@ public class DevelopmentServerLauncher { *
* @param serverArgs
* @return
+ * @throws Exception
+ * @throws Exception
*/
protected static String runServer(Map<String, String> serverArgs,
- String mode) {
+ String mode) throws Exception {
// Assign default values for some arguments
assignDefault(serverArgs, "webroot", "WebContent");
@@ -81,27 +87,27 @@ public class DevelopmentServerLauncher { + serverPort
+ "\n-------------------------------------------------\n");
- try {
- final Server server = new Server();
+ final Server server = new Server();
- final Connector connector = new SelectChannelConnector();
+ final Connector connector = new SelectChannelConnector();
- connector.setPort(port);
- server.setConnectors(new Connector[] { connector });
+ connector.setPort(port);
+ server.setConnectors(new Connector[] { connector });
- final WebAppContext webappcontext = new WebAppContext();
- String path = DevelopmentServerLauncher.class.getPackage()
- .getName().replace(".", File.separator);
- webappcontext.setDefaultsDescriptor(path + File.separator
- + "jetty-webdefault.xml");
- webappcontext.setContextPath(serverArgs.get("context"));
- webappcontext.setWar(serverArgs.get("webroot"));
- server.setHandler(webappcontext);
+ final WebAppContext webappcontext = new WebAppContext();
+ String path = DevelopmentServerLauncher.class.getPackage().getName()
+ .replace(".", File.separator);
+ webappcontext.setDefaultsDescriptor(path + File.separator
+ + "jetty-webdefault.xml");
+ webappcontext.setContextPath(serverArgs.get("context"));
+ webappcontext.setWar(serverArgs.get("webroot"));
+ server.setHandler(webappcontext);
+ try {
server.start();
- } catch (final Exception e) {
- e.printStackTrace();
- return null;
+ } catch (Exception e) {
+ server.stop();
+ throw e;
}
return "http://localhost:" + port + serverArgs.get("context");
diff --git a/src/com/vaadin/package.html b/src/com/vaadin/package.html index 5a2defa80b..6142fcb8de 100644 --- a/src/com/vaadin/package.html +++ b/src/com/vaadin/package.html @@ -5,34 +5,23 @@ <body bgcolor="white"> -<p>This package is the base of Vaadin. The base package -contains the Application class, the starting point of any aplication that uses Vaadin. -The sub-packages from this point contain application components and utilities. -</p> +<p>The Vaadin base package. Contains the Application class, the +starting point of any application that uses Vaadin.</p> -<h2>Package Specification</h2> +<p>Contains all Vaadin core classes. A Vaadin application is based +on the {@link com.vaadin.Application} class and deployed as a servlet +using {@link com.vaadin.terminal.gwt.server.ApplicationServlet} or +{@link com.vaadin.terminal.gwt.server.GAEApplicationServlet} (for Google +App Engine).</p> - <b>Vaadin is composed of the following packages:</b> - <dl> - <dt>com.vaadin.data</dt> - <dd>A library of interfaces for transparently binding UI components to datasources.</dd> - <dt>com.vaadin.data.util</dt> - <dd>Basic implementations of data-intfaces and utidtties for working with data-bound components.</dd> - <dt>com.vaadin.data.validator</dt> - <dd>Classes providing data-validation for Properties.</dd> - <dt>com.vaadin.event</dt> - <dd>Interfaces defining how to send and recieve events.</dd> - <dt>com.vaadin.service</dt> - <dd>Classes provding miscelaneous utidtty services.</dd> - <dt>com.vaadin.terminal</dt> - <dd>Classes and interfaces for implementing the terminal specific adapters.</dd> - <dt>com.vaadin.terminal.web</dt> - <dd>Classes that implement support both for AJAX-based and more limited web browsers.</dd> - <dt>com.vaadin.ui</dt> - <dd>UI components.</dd> - </dl> +<p>Vaadin applications can also be deployed as portlets using {@link +com.vaadin.terminal.gwt.server.ApplicationPortlet] (JSR-168) or {@link +com.vaadin.terminal.gwt.server.ApplicationPortlet2} (JSR-286).</p> + +<p>All classes in Vaadin are serializable unless otherwise noted. +This allows Vaadin applications to run in cluster and cloud +environments.</p> -<!-- Put @see and @since tags down here. --> </body> </html> diff --git a/src/com/vaadin/service/FileTypeResolver.java b/src/com/vaadin/service/FileTypeResolver.java index 566f299376..5603a722d6 100644 --- a/src/com/vaadin/service/FileTypeResolver.java +++ b/src/com/vaadin/service/FileTypeResolver.java @@ -33,7 +33,7 @@ public class FileTypeResolver implements Serializable { * Default icon given if no icon is specified for a mime-type. */ static public Resource DEFAULT_ICON = new ThemeResource( - "icon/files/file.gif"); + "../runo/icons/16/document.png"); /** * Default mime-type. @@ -195,12 +195,12 @@ public class FileTypeResolver implements Serializable { /** * File extension to MIME type mapping. */ - static private Hashtable extToMIMEMap = new Hashtable(); + static private Hashtable<String, String> extToMIMEMap = new Hashtable<String, String>(); /** * MIME type to Icon mapping. */ - static private Hashtable MIMEToIconMap = new Hashtable(); + static private Hashtable<String, Resource> MIMEToIconMap = new Hashtable<String, Resource>(); static { @@ -218,8 +218,9 @@ public class FileTypeResolver implements Serializable { } // Initialize Icons - addIcon("inode/drive", new ThemeResource("icon/files/drive.gif")); - addIcon("inode/directory", new ThemeResource("icon/files/folder.gif")); + ThemeResource folder = new ThemeResource("../runo/icons/16/folder.png"); + addIcon("inode/drive", folder); + addIcon("inode/directory", folder); } /** @@ -254,7 +255,7 @@ public class FileTypeResolver implements Serializable { } // Return type from extension map, if found - final String type = (String) extToMIMEMap.get(ext); + final String type = extToMIMEMap.get(ext); if (type != null) { return type; } @@ -274,9 +275,11 @@ public class FileTypeResolver implements Serializable { * @return the icon corresponding to the given file */ public static Resource getIcon(String fileName) { + return getIconByMineType(getMIMEType(fileName)); + } - final String mimeType = getMIMEType(fileName); - final Resource icon = (Resource) MIMEToIconMap.get(mimeType); + private static Resource getIconByMineType(String mimeType) { + final Resource icon = MIMEToIconMap.get(mimeType); if (icon != null) { return icon; } @@ -297,16 +300,7 @@ public class FileTypeResolver implements Serializable { * @return the icon corresponding to the given file */ public static Resource getIcon(File file) { - - final String mimeType = getMIMEType(file); - final Resource icon = (Resource) MIMEToIconMap.get(mimeType); - if (icon != null) { - return icon; - } - - // If nothing is known about the file-type, general file - // icon is used - return DEFAULT_ICON; + return getIconByMineType(getMIMEType(file)); } /** @@ -371,7 +365,7 @@ public class FileTypeResolver implements Serializable { * @return unmodifiable map containing the current file extension to * mime-type mapping */ - public static Map getExtensionToMIMETypeMapping() { + public static Map<String, String> getExtensionToMIMETypeMapping() { return Collections.unmodifiableMap(extToMIMEMap); } @@ -380,7 +374,7 @@ public class FileTypeResolver implements Serializable { * * @return unmodifiable map containing the current mime-type to icon mapping */ - public static Map getMIMETypeToIconMapping() { + public static Map<String, Resource> getMIMETypeToIconMapping() { return Collections.unmodifiableMap(MIMEToIconMap); } } diff --git a/src/com/vaadin/terminal/ParameterHandler.java b/src/com/vaadin/terminal/ParameterHandler.java index 501dbcd992..be124d4bbd 100644 --- a/src/com/vaadin/terminal/ParameterHandler.java +++ b/src/com/vaadin/terminal/ParameterHandler.java @@ -7,17 +7,21 @@ package com.vaadin.terminal; import java.io.Serializable; import java.util.Map; +import com.vaadin.ui.Window; + /** - * Interface implemented by all the classes capable of handling external - * parameters. + * {@code ParameterHandler} is implemented by classes capable of handling + * external parameters. * * <p> - * Some terminals can provide external parameters for application. For example - * GET and POST parameters are passed to application as external parameters on - * Web Adapter. The parameters can be received at any time during the - * application lifecycle. All the parameter handlers implementing this interface - * and registered to {@link com.vaadin.ui.Window} receive all the parameters got - * from the terminal in the given window. + * What parameters are provided depend on what the {@link Terminal} provides and + * if the application is deployed as a servlet or portlet. URL GET parameters + * are typically provided to the {@link #handleParameters(Map)} method. + * </p> + * <p> + * A {@code ParameterHandler} must be registered to a {@code Window} using + * {@link Window#addParameterHandler(ParameterHandler)} to be called when + * parameters are available. * </p> * * @author IT Mill Ltd. @@ -28,27 +32,25 @@ import java.util.Map; public interface ParameterHandler extends Serializable { /** - * <p> - * Handles the given parameters. The parameters are given as inmodifieable - * name to value map. All parameters names are of type: - * {@link java.lang.String}. All the parameter values are arrays of strings. - * </p> + * Handles the given parameters. All parameters names are of type + * {@link String} and the values are {@link String} arrays. * * @param parameters - * the Inmodifiable name to value[] mapping. + * an unmodifiable map which contains the parameter names and + * values * */ public void handleParameters(Map<String, String[]> parameters); /** - * ParameterHandler error event. + * An ErrorEvent implementation for ParameterHandler. */ public interface ErrorEvent extends Terminal.ErrorEvent { /** - * Gets the source ParameterHandler. + * Gets the ParameterHandler that caused the error. * - * @return the source Parameter Handler. + * @return the ParameterHandler that caused the error */ public ParameterHandler getParameterHandler(); diff --git a/src/com/vaadin/terminal/URIHandler.java b/src/com/vaadin/terminal/URIHandler.java index fe1746201e..2d5facb28e 100644 --- a/src/com/vaadin/terminal/URIHandler.java +++ b/src/com/vaadin/terminal/URIHandler.java @@ -8,12 +8,10 @@ import java.io.Serializable; import java.net.URL; /** - * Interface implemented by all the classes capable of handling URI:s. - * - * <p> - * <code>URIHandler</code> can provide <code>DownloadStream</code> for - * transferring data for client. - * </p> + * A URIHandler is used for handling URI:s requested by the user and can + * optionally provide a {@link DownloadStream}. If a {@link DownloadStream} is + * returned by {@link #handleURI(URL, String)}, the stream is sent to the + * client. * * @author IT Mill Ltd. * @version @@ -23,27 +21,26 @@ import java.net.URL; public interface URIHandler extends Serializable { /** - * Handles a given relative URI. If the URI handling wants to emit a - * downloadable stream it can return download stream object. If no emitting - * stream is necessary, null should be returned instead. + * Handles a given URI. If the URI handler to emit a downloadable stream it + * should return a {@code DownloadStream} object. * * @param context - * the URl. + * the base URL * @param relativeUri - * the relative uri. - * @return the download stream object. + * a URI relative to {@code context} + * @return A downloadable stream or null if no stream is provided */ public DownloadStream handleURI(URL context, String relativeUri); /** - * URIHandler error event. + * An {@code ErrorEvent} implementation for URIHandler. */ public interface ErrorEvent extends Terminal.ErrorEvent { /** - * Gets the source URIHandler. + * Gets the URIHandler that caused this error. * - * @return the URIHandler. + * @return the URIHandler that caused the error */ public URIHandler getURIHandler(); diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java index 619404a065..9d649bcd99 100755 --- a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java @@ -12,7 +12,6 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; -import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayString; @@ -29,7 +28,6 @@ import com.google.gwt.user.client.Event; import com.google.gwt.user.client.History; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.impl.HTTPRequestImpl; import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; @@ -483,11 +481,9 @@ public class ApplicationConnection { endRequest(); } } else { - // Synchronized call, discarded response - - syncSendForce(((HTTPRequestImpl) GWT.create(HTTPRequestImpl.class)) - .createXmlHTTPRequest(), uri + "&" + PARAM_UNLOADBURST - + "=1", rd); + // Synchronized call, discarded response (leaving the page) + SynchronousXHR syncXHR = (SynchronousXHR) SynchronousXHR.create(); + syncXHR.synchronousPost(uri + "&" + PARAM_UNLOADBURST + "=1", rd); } } @@ -521,19 +517,6 @@ public class ApplicationConnection { } } - private native void syncSendForce(JavaScriptObject xmlHttpRequest, - String uri, String requestData) - /*-{ - try { - xmlHttpRequest.open("POST", uri, false); - xmlHttpRequest.setRequestHeader("Content-Type", "text/plain;charset=utf-8"); - xmlHttpRequest.send(requestData); - } catch (e) { - // No errors are managed as this is synchronous forceful send that can just fail - } - this.@com.vaadin.terminal.gwt.client.ApplicationConnection::endRequest()(); - }-*/; - private void startRequest() { activeRequests++; requestStartTime = new Date(); diff --git a/src/com/vaadin/terminal/gwt/client/CSSRule.java b/src/com/vaadin/terminal/gwt/client/CSSRule.java index d50f7ce21b..0931c23d86 100644 --- a/src/com/vaadin/terminal/gwt/client/CSSRule.java +++ b/src/com/vaadin/terminal/gwt/client/CSSRule.java @@ -13,16 +13,30 @@ public class CSSRule { private final String selector; private JavaScriptObject rules = null; - public CSSRule(String selector) { + /** + * + * @param selector + * the CSS selector to search for in the stylesheets + * @param deep + * should the search follow any @import statements? + */ + public CSSRule(final String selector, final boolean deep) { this.selector = selector; - fetchRule(selector); + fetchRule(selector, deep); } // TODO how to find the right LINK-element? We should probably give the // stylesheet a name. - private native void fetchRule(final String selector) - /*-{ - this.@com.vaadin.terminal.gwt.client.CSSRule::rules = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;)($doc.styleSheets[1], selector); + private native void fetchRule(final String selector, final boolean deep) /*-{ + var sheets = $doc.styleSheets; + for(var i = 0; i < sheets.length; i++) { + var sheet = sheets[i]; + if(sheet.href && sheet.href.indexOf("VAADIN/themes")>-1) { + this.@com.vaadin.terminal.gwt.client.CSSRule::rules = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;Z)(sheet, selector, deep); + return; + } + } + this.@com.vaadin.terminal.gwt.client.CSSRule::rules = []; }-*/; /* @@ -30,38 +44,48 @@ public class CSSRule { * 'rules' array. The array is reverse ordered (last one found is first). */ private static native JavaScriptObject searchForRule( - JavaScriptObject sheet, final String selector) - /*-{ - if(!$doc.styleSheets) + final JavaScriptObject sheet, final String selector, + final boolean deep) /*-{ + if(!$doc.styleSheets) return null; - - selector = selector.toLowerCase(); - - var allMatches = []; - - var theRules = new Array(); - if (sheet.cssRules) + + selector = selector.toLowerCase(); + + var allMatches = []; + + // IE handles imported sheet differently + if(deep && sheet.imports && sheet.imports.length > 0) { + for(var i=0; i < sheet.imports.length; i++) { + var imports = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;Z)(sheet.imports[i], selector, deep); + allMatches.concat(imports); + } + } + + var theRules = new Array(); + if (sheet.cssRules) theRules = sheet.cssRules - else if (sheet.rules) + else if (sheet.rules) theRules = sheet.rules - + var j = theRules.length; for(var i=0; i<j; i++) { - var r = theRules[i]; - if(r.type == 3) { - allMatches.unshift(@com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;)(r.styleSheet, selector)); - } else if(r.type == 1) { - var selectors = r.selectorText.toLowerCase().split(","); - var n = selectors.length; - for(var m=0; m<n; m++) { - if(selectors[m].replace(/^\s+|\s+$/g, "") == selector) { - allMatches.unshift(r); - break; // No need to loop other selectors for this rule - } - } - } + var r = theRules[i]; + if(r.type == 1 ||Â sheet.imports) { + var selectors = r.selectorText.toLowerCase().split(","); + var n = selectors.length; + for(var m=0; m<n; m++) { + if(selectors[m].replace(/^\s+|\s+$/g, "") == selector) { + allMatches.unshift(r); + break; // No need to loop other selectors for this rule + } } - + } else if(deep && r.type == 3) { + // Search @import stylesheet + var imports = @com.vaadin.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;Z)(r.styleSheet, selector, deep); + allMatches = allMatches.concat(imports); + } + } + return allMatches; }-*/; @@ -69,14 +93,15 @@ public class CSSRule { * Returns a specific property value from this CSS rule. * * @param propertyName - * @return + * camelCase CSS property name + * @return the value of the property as a String */ - public native String getPropertyValue(final String propertyName) - /*-{ - for(var i=0; i<this.@com.vaadin.terminal.gwt.client.CSSRule::rules.length; i++){ - var value = this.@com.vaadin.terminal.gwt.client.CSSRule::rules[i].style[propertyName]; - if(value) - return value; + public native String getPropertyValue(final String propertyName) /*-{ + var j = this.@com.vaadin.terminal.gwt.client.CSSRule::rules.length; + for(var i=0; i<j; i++) { + var value = this.@com.vaadin.terminal.gwt.client.CSSRule::rules[i].style[propertyName]; + if(value) + return value; } return null; }-*/; diff --git a/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java b/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java new file mode 100644 index 0000000000..991b256794 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/SynchronousXHR.java @@ -0,0 +1,21 @@ +package com.vaadin.terminal.gwt.client; + +import com.google.gwt.xhr.client.XMLHttpRequest; + +public class SynchronousXHR extends XMLHttpRequest { + + protected SynchronousXHR() { + } + + public native final void synchronousPost(String uri, String requestData) + /*-{ + try { + this.open("POST", uri, false); + this.setRequestHeader("Content-Type", "text/plain;charset=utf-8"); + this.send(requestData); + } catch (e) { + // No errors are managed as this is synchronous forceful send that can just fail + } + }-*/; + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java b/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java new file mode 100644 index 0000000000..cebdf1063f --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/FocusableScrollPanel.java @@ -0,0 +1,69 @@ +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.event.dom.client.HasScrollHandlers; +import com.google.gwt.event.dom.client.ScrollEvent; +import com.google.gwt.event.dom.client.ScrollHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.ScrollPanel; + +/** + * A scrollhandlers similar to {@link ScrollPanel}. + * + */ +public class FocusableScrollPanel extends SimpleFocusablePanel implements + HasScrollHandlers { + + public FocusableScrollPanel() { + // Prevent IE standard mode bug when a AbsolutePanel is contained. + Style style = getElement().getStyle(); + style.setOverflow(Overflow.AUTO); + style.setProperty("zoom", "1"); + style.setPosition(Position.RELATIVE); + } + + public HandlerRegistration addScrollHandler(ScrollHandler handler) { + return addDomHandler(handler, ScrollEvent.getType()); + } + + /** + * Gets the horizontal scroll position. + * + * @return the horizontal scroll position, in pixels + */ + public int getHorizontalScrollPosition() { + return getElement().getScrollLeft(); + } + + /** + * Gets the vertical scroll position. + * + * @return the vertical scroll position, in pixels + */ + public int getScrollPosition() { + return getElement().getScrollTop(); + } + + /** + * Sets the horizontal scroll position. + * + * @param position + * the new horizontal scroll position, in pixels + */ + public void setHorizontalScrollPosition(int position) { + getElement().setScrollLeft(position); + } + + /** + * Sets the vertical scroll position. + * + * @param position + * the new vertical scroll position, in pixels + */ + public void setScrollPosition(int position) { + getElement().setScrollTop(position); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java b/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java new file mode 100644 index 0000000000..b534e3faba --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/SimpleFocusablePanel.java @@ -0,0 +1,61 @@ +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.terminal.gwt.client.Focusable; + +/** + * Compared to FocusPanel in GWT this panel does not support eg. accesskeys, but + * is simpler by its dom hierarchy nor supports focusing via java api. + */ +public class SimpleFocusablePanel extends SimplePanel implements + HasFocusHandlers, HasBlurHandlers, HasKeyDownHandlers, + HasKeyPressHandlers, Focusable { + + public SimpleFocusablePanel() { + // make focusable, as we don't need access key magic we don't need to + // use FocusImpl.createFocusable + getElement().setTabIndex(0); + } + + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + public void focus() { + setFocus(true); + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/VMenuBar.java b/src/com/vaadin/terminal/gwt/client/ui/VMenuBar.java index 5c359f70d4..f80edc4bc4 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VMenuBar.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VMenuBar.java @@ -9,6 +9,15 @@ import java.util.List; import java.util.Stack; import com.google.gwt.dom.client.NodeList; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.user.client.Command; @@ -21,7 +30,6 @@ import com.google.gwt.user.client.ui.HasHTML; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.UIObject; -import com.google.gwt.user.client.ui.Widget; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.BrowserInfo; import com.vaadin.terminal.gwt.client.ContainerResizedListener; @@ -29,8 +37,9 @@ import com.vaadin.terminal.gwt.client.Paintable; import com.vaadin.terminal.gwt.client.UIDL; import com.vaadin.terminal.gwt.client.Util; -public class VMenuBar extends Widget implements Paintable, - CloseHandler<PopupPanel>, ContainerResizedListener { +public class VMenuBar extends SimpleFocusablePanel implements Paintable, + CloseHandler<PopupPanel>, ContainerResizedListener, KeyPressHandler, + KeyDownHandler, BlurHandler, FocusHandler { /** Set the CSS class name to allow styling. */ public static final String CLASSNAME = "v-menubar"; @@ -65,11 +74,25 @@ public class VMenuBar extends Widget implements Paintable, public VMenuBar() { // Create an empty horizontal menubar this(false); + + // Navigation is only handled by the root bar + addFocusHandler(this); + addBlurHandler(this); + + /* + * Firefox auto-repeat works correctly only if we use a key press + * handler, other browsers handle it correctly when using a key down + * handler + */ + if (BrowserInfo.get().isGecko()) { + addKeyPressHandler(this); + } else { + addKeyDownHandler(this); + } } public VMenuBar(boolean subMenu) { super(); - setElement(DOM.createDiv()); items = new ArrayList<CustomMenuItem>(); popup = null; @@ -884,4 +907,339 @@ public class VMenuBar extends Widget implements Paintable, } return w; } + + /* + * (non-Javadoc) + * @see com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google.gwt.event.dom.client.KeyPressEvent) + */ + public void onKeyPress(KeyPressEvent event) { + if (handleNavigation(event.getNativeEvent().getKeyCode(), event + .isControlKeyDown() + || event.isMetaKeyDown(), event.isShiftKeyDown())) { + event.preventDefault(); + } + } + + /* + * (non-Javadoc) + * @see com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt.event.dom.client.KeyDownEvent) + */ + public void onKeyDown(KeyDownEvent event) { + if (handleNavigation(event.getNativeEvent().getKeyCode(), event + .isControlKeyDown() + || event.isMetaKeyDown(), event.isShiftKeyDown())) { + event.preventDefault(); + } + } + + /** + * Get the key that moves the selection upwards. By default it is the + * up arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that moves the selection downwards. By default it is the + * down arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that moves the selection left. By default it is the + * left arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that moves the selection right. By default it is the + * right arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * Get the key that selects a menu item. By default it is the Enter key but + * by overriding this you can change the key to whatever you want. + * + * @return + */ + protected int getNavigationSelectKey() { + return KeyCodes.KEY_ENTER; + } + + /** + * Get the key that closes the menu. By default it is the escape key but by + * overriding this yoy can change the key to whatever you want. + * + * @return + */ + protected int getCloseMenuKey() { + return KeyCodes.KEY_ESCAPE; + } + + /** + * Handles the keyboard events handled by the MenuBar + * + * @param event + * The keyboard event received + * @return true iff the navigation event was handled + */ + public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + if (keycode == KeyCodes.KEY_TAB || ctrl || shift || !isEnabled()) { + // Do not handle tab key, nor ctrl keys + return false; + } + + if (keycode == getNavigationLeftKey()) { + if (getSelected() == null) { + // If nothing is selected then select the last item + setSelected(items.get(items.size() - 1)); + if (getSelected().isSeparator() || !getSelected().isEnabled()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu == null && getParentMenu() == null) { + // If this is the root menu then move to the right + int idx = items.indexOf(getSelected()); + if (idx > 0) { + setSelected(items.get(idx - 1)); + } else { + setSelected(items.get(items.size() - 1)); + } + + if (getSelected().isSeparator() || !getSelected().isEnabled()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + + } else if (getParentMenu().getParentMenu() == null) { + + // Get the root menu + VMenuBar root = getParentMenu(); + + root.getSelected().getSubMenu().setSelected(null); + root.hideChildren(); + + // Get the root menus items and select the previous one + int idx = root.getItems().indexOf(root.getSelected()); + idx = idx > 0 ? idx : root.getItems().size(); + CustomMenuItem selected = root.getItems().get(--idx); + + while (selected.isSeparator() || !selected.isEnabled()) { + idx = idx > 0 ? idx : root.getItems().size(); + selected = root.getItems().get(--idx); + } + + root.setSelected(selected); + root.showChildMenu(selected); + VMenuBar submenu = selected.getSubMenu(); + + // Select the first item in the newly open submenu + submenu.setSelected(submenu.getItems().get(0)); + + } else { + getParentMenu().getSelected().getSubMenu().setSelected(null); + getParentMenu().hideChildren(); + } + + return true; + + } else if (keycode == getNavigationRightKey()) { + + if (getSelected() == null) { + // If nothing is selected then select the first item + setSelected(items.get(0)); + if (getSelected().isSeparator() || !getSelected().isEnabled()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu == null && getParentMenu() == null) { + // If this is the root menu then move to the right + int idx = items.indexOf(getSelected()); + + if (idx < items.size() - 1) { + setSelected(items.get(idx + 1)); + } else { + setSelected(items.get(0)); + } + + if (getSelected().isSeparator() || !getSelected().isEnabled()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu == null + && getSelected().getSubMenu() != null) { + // If the item has a submenu then show it and move the selection + // there + showChildMenu(getSelected()); + menuVisible = true; + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } else if (visibleChildMenu == null) { + + // Get the root menu + VMenuBar root = getParentMenu(); + while(root.getParentMenu() != null){ + root = root.getParentMenu(); + } + + // Hide the submenu + root.hideChildren(); + + // Get the root menus items and select the next one + int idx = root.getItems().indexOf(root.getSelected()); + idx = idx < root.getItems().size()-1 ? idx : -1; + CustomMenuItem selected = root.getItems().get(++idx); + + while (selected.isSeparator() || !selected.isEnabled()) { + idx = idx < root.getItems().size() - 1 ? idx : -1; + selected = root.getItems().get(++idx); + } + + root.setSelected(selected); + root.showChildMenu(selected); + VMenuBar submenu = selected.getSubMenu(); + + // Select the first item in the newly open submenu + submenu.setSelected(submenu.getItems().get(0)); + + } else if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } + + return true; + + } else if (keycode == getNavigationUpKey()) { + + if (getSelected() == null) { + // If nothing is selected then select the last item + setSelected(items.get(items.size() - 1)); + if (getSelected().isSeparator() || !getSelected().isEnabled()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } else { + // Select the previous item if possible or loop to the last item + int idx = items.indexOf(getSelected()); + if (idx > 0) { + setSelected(items.get(idx - 1)); + } else { + setSelected(items.get(items.size() - 1)); + } + + if (getSelected().isSeparator() || !getSelected().isEnabled()) { + handleNavigation(keycode, ctrl, shift); + } + } + + return true; + + } else if (keycode == getNavigationDownKey()) { + + if (getSelected() == null) { + // If nothing is selected then select the first item + setSelected(items.get(0)); + if (getSelected().isSeparator() || !getSelected().isEnabled()) { + handleNavigation(keycode, ctrl, shift); + } + } else if (visibleChildMenu == null && getParentMenu() == null) { + // If this is the root menu the show the child menu with arrow + // down + showChildMenu(getSelected()); + menuVisible = true; + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } else if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } else { + // Select the next item if possible or loop to the first item + int idx = items.indexOf(getSelected()); + if (idx < items.size() - 1) { + setSelected(items.get(idx + 1)); + } else { + setSelected(items.get(0)); + } + + if (getSelected().isSeparator() || !getSelected().isEnabled()) { + handleNavigation(keycode, ctrl, shift); + } + } + return true; + + } else if (keycode == getCloseMenuKey()) { + setSelected(null); + hideChildren(); + menuVisible = false; + + } else if (keycode == getNavigationSelectKey()) { + if (visibleChildMenu != null) { + // Redirect all navigation to the submenu + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + menuVisible = false; + } else if (visibleChildMenu == null + && getSelected().getSubMenu() != null) { + // If the item has a submenu then show it and move the selection + // there + showChildMenu(getSelected()); + menuVisible = true; + visibleChildMenu.handleNavigation(keycode, ctrl, shift); + } else { + Command command = getSelected().getCommand(); + if (command != null) { + command.execute(); + } + + hideParents(true); + } + } + + return false; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event + * .dom.client.BlurEvent) + */ + public void onBlur(BlurEvent event) { + setSelected(null); + hideChildren(); + menuVisible = false; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + public void onFocus(FocusEvent event) { + if (getSelected() == null) { + // If nothing is selected then select the first item + setSelected(items.get(0)); + } + } } diff --git a/src/com/vaadin/terminal/gwt/client/ui/VPopupCalendar.java b/src/com/vaadin/terminal/gwt/client/ui/VPopupCalendar.java index 8d5c21c512..b8a004e699 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VPopupCalendar.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VPopupCalendar.java @@ -27,6 +27,7 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, private final VOverlay popup;
private boolean open = false;
+ private boolean parsable = true;
public VPopupCalendar() {
super();
@@ -51,7 +52,10 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, @Override
public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
boolean lastReadOnlyState = readonly;
+ parsable = uidl.getBooleanAttribute("parsable");
+
super.updateFromUIDL(uidl, client);
+
popup.setStyleName(VDateField.CLASSNAME + "-popup "
+ VDateField.CLASSNAME + "-"
+ resolutionToString(currentResolution));
@@ -174,4 +178,15 @@ public class VPopupCalendar extends VTextualDate implements Paintable, Field, return fieldExtraWidth;
}
+ @Override
+ protected void buildDate() {
+ // Save previous value
+ String previousValue = getText();
+ super.buildDate();
+
+ // Restore previous value if the input could not be parsed
+ if (!parsable) {
+ setText(previousValue);
+ }
+ }
}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java b/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java index 83bf043d98..e8cf37c1ff 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java @@ -12,6 +12,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.NodeList; @@ -23,6 +24,15 @@ import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Position; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.event.dom.client.ScrollEvent; import com.google.gwt.event.dom.client.ScrollHandler; import com.google.gwt.user.client.Command; @@ -35,12 +45,12 @@ import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.RootPanel; -import com.google.gwt.user.client.ui.ScrollPanel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; import com.vaadin.terminal.gwt.client.ApplicationConnection; import com.vaadin.terminal.gwt.client.BrowserInfo; import com.vaadin.terminal.gwt.client.Container; +import com.vaadin.terminal.gwt.client.Focusable; import com.vaadin.terminal.gwt.client.MouseEventDetails; import com.vaadin.terminal.gwt.client.Paintable; import com.vaadin.terminal.gwt.client.RenderSpace; @@ -80,14 +90,27 @@ import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation; * TODO implement unregistering for child components in Cells */ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, - VHasDropHandler { + VHasDropHandler, KeyPressHandler, KeyDownHandler, FocusHandler, + BlurHandler, Focusable { public static final String CLASSNAME = "v-table"; + public static final String CLASSNAME_SELECTION_FOCUS = CLASSNAME + "-focus"; + public static final String ITEM_CLICK_EVENT_ID = "itemClick"; + public static final String HEADER_CLICK_EVENT_ID = "handleHeaderClick"; + public static final String FOOTER_CLICK_EVENT_ID = "handleFooterClick"; + public static final String COLUMN_RESIZE_EVENT_ID = "columnResize"; private static final double CACHE_RATE_DEFAULT = 2; /** + * The default multi select mode where simple left clicks only selects one + * item, CTRL+left click selects multiple items and SHIFT-left click selects + * a range of items. + */ + private static final int MULTISELECT_MODE_DEFAULT = 0; + + /** * multiple of pagelength which component will cache when requesting more * rows */ @@ -104,19 +127,103 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private int pageLength = 15; private int lastRequestedFirstvisible = 0; // to detect "serverside scroll" - private boolean showRowHeaders = false; + protected boolean showRowHeaders = false; private String[] columnOrder; - private ApplicationConnection client; - private String paintableId; + protected ApplicationConnection client; + protected String paintableId; private boolean immediate; + private boolean nullSelectionAllowed = true; private int selectMode = Table.SELECT_MODE_NONE; private final HashSet<String> selectedRowKeys = new HashSet<String>(); + /* + * These are used when jumping between pages when pressing Home and End + */ + private boolean selectLastItemInNextRender = false; + private boolean selectFirstItemInNextRender = false; + private boolean focusFirstItemInNextRender = false; + private boolean focusLastItemInNextRender = false; + + /* + * The currently focused row + */ + private VScrollTableRow focusedRow; + + /* + * Helper to store selection range start in when using the keyboard + */ + private VScrollTableRow selectionRangeStart; + + /* + * Flag for notifying when the selection has changed and should be sent to + * the server + */ + private boolean selectionChanged = false; + + /* + * The speed (in pixels) which the scrolling scrolls vertically/horizontally + */ + private int scrollingVelocity = 10; + + private Timer scrollingVelocityTimer = null;; + + /** + * Represents a select range of rows + */ + private class SelectionRange { + /** + * The starting key of the range + */ + private int startRowKey; + + /** + * The ending key of the range + */ + private int endRowKey; + + /** + * Constuctor. + * + * @param startRowKey + * The range start. Must be less than endRowKey + * @param endRowKey + * The range end. Must be bigger than startRowKey + */ + public SelectionRange(int startRowKey, int endRowKey) { + this.startRowKey = startRowKey; + this.endRowKey = endRowKey; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return startRowKey + "-" + endRowKey; + } + + public boolean inRange(int key) { + return key >= startRowKey && key <= endRowKey; + } + + public int getStartKey() { + return startRowKey; + } + + public int getEndKey() { + return endRowKey; + } + }; + + private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>(); + private boolean initializedAndAttached = false; /** @@ -126,7 +233,9 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private final TableHead tHead = new TableHead(); - private final ScrollPanel bodyContainer = new ScrollPanel(); + private final TableFooter tFoot = new TableFooter(); + + private final FocusableScrollPanel scrollBodyPanel = new FocusableScrollPanel(); private int totalRows; @@ -149,6 +258,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private Element scrollPositionElement; private boolean enabled; private boolean showColHeaders; + private boolean showColFooters; /** flag to indicate that table body has changed */ private boolean isNewBody = true; @@ -166,21 +276,342 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private boolean rendering = false; private int dragmode; + private int multiselectmode; + public VScrollTable() { - bodyContainer.addScrollHandler(this); - bodyContainer.setStyleName(CLASSNAME + "-body"); + scrollBodyPanel.setStyleName(CLASSNAME + "-body-wrapper"); + + /* + * Firefox auto-repeat works correctly only if we use a key press + * handler, other browsers handle it correctly when using a key down + * handler + */ + if (BrowserInfo.get().isGecko()) { + scrollBodyPanel.addKeyPressHandler(this); + } else { + scrollBodyPanel.addKeyDownHandler(this); + } + + scrollBodyPanel.addFocusHandler(this); + scrollBodyPanel.addBlurHandler(this); + + scrollBodyPanel.addScrollHandler(this); + scrollBodyPanel.setStyleName(CLASSNAME + "-body"); setStyleName(CLASSNAME); + add(tHead); - add(bodyContainer); + add(scrollBodyPanel); + add(tFoot); rowRequestHandler = new RowRequestHandler(); + /* + * We need to use the sinkEvents method to catch the keyUp events so we + * can cache a single shift. KeyUpHandler cannot do this. + */ + sinkEvents(Event.ONKEYUP); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user + * .client.Event) + */ + @Override + public void onBrowserEvent(Event event) { + if (event.getTypeInt() == Event.ONKEYUP) { + if (event.getKeyCode() == KeyCodes.KEY_SHIFT) { + sendSelectedRows(); + selectionRangeStart = null; + } else if ((event.getKeyCode() == getNavigationUpKey() + || event.getKeyCode() == getNavigationDownKey() + || event.getKeyCode() == getNavigationPageUpKey() || event + .getKeyCode() == getNavigationPageDownKey()) + && !event.getShiftKey()) { + sendSelectedRows(); + + if (scrollingVelocityTimer != null) { + scrollingVelocityTimer.cancel(); + scrollingVelocityTimer = null; + scrollingVelocity = 10; + } + } + } + } + + /** + * Fires a column resize event which sends the resize information to the + * server. + * + * @param columnId + * The columnId of the column which was resized + * @param originalWidth + * The width in pixels of the column before the resize event + * @param newWidth + * The width in pixels of the column after the resize event + */ + private void fireColumnResizeEvent(String columnId, int originalWidth, + int newWidth) { + client.updateVariable(paintableId, "columnResizeEventColumn", + columnId, false); + client.updateVariable(paintableId, "columnResizeEventPrev", + originalWidth, false); + client.updateVariable(paintableId, "columnResizeEventCurr", + newWidth, immediate); + + } + + /** + * Moves the focus one step down + * + * @return Returns true if succeeded + */ + private boolean moveFocusDown() { + return moveFocusDown(0); + } + + /** + * Moves the focus down by 1+offset rows + * + * @return Returns true if succeeded, else false if the selection could not + * be move downwards + */ + private boolean moveFocusDown(int offset) { + if (selectMode > VScrollTable.SELECT_MODE_NONE) { + if (focusedRow == null && scrollBody.iterator().hasNext()) { + return setRowFocus((VScrollTableRow) scrollBody.iterator() + .next()); + } else { + VScrollTableRow next = getNextRow(focusedRow, offset); + if (next != null) { + return setRowFocus(next); + } + } + } + + return false; + } + + /** + * Moves the selection one step up + * + * @return Returns true if succeeded + */ + private boolean moveFocusUp() { + return moveFocusUp(0); + } + + /** + * Moves the focus row upwards + * + * @return Returns true if succeeded, else false if the selection could not + * be move upwards + * + */ + private boolean moveFocusUp(int offset) { + if (selectMode > VScrollTable.SELECT_MODE_NONE) { + if (focusedRow == null && scrollBody.iterator().hasNext()) { + return setRowFocus((VScrollTableRow) scrollBody.iterator() + .next()); + } else { + VScrollTableRow prev = getPreviousRow(focusedRow, offset); + if (prev != null) { + return setRowFocus(prev); + } + } + } + + return false; + } + + /** + * Selects a row where the current selection head is + * + * @param ctrlSelect + * Is the selection a ctrl+selection + * @param shiftSelect + * Is the selection a shift+selection + * @return Returns truw + */ + private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) { + if (focusedRow != null) { + // Arrows moves the selection and clears previous selections + if (selectMode > SELECT_MODE_NONE && !ctrlSelect && !shiftSelect) { + deselectAll(); + focusedRow.toggleSelection(!ctrlSelect); + selectionRangeStart = focusedRow; + } + + // Ctrl+arrows moves selection head + else if (selectMode > SELECT_MODE_NONE && ctrlSelect + && !shiftSelect) { + selectionRangeStart = focusedRow; + // No selection, only selection head is moved + } + + // Shift+arrows selection selects a range + else if (selectMode == SELECT_MODE_MULTI && !ctrlSelect + && shiftSelect) { + focusedRow.toggleShiftSelection(shiftSelect); + } + } + } + + /** + * Sends the selection to the server + */ + private void sendSelectedRows() { + // Don't send anything if selection has not changed + if (!selectionChanged) { + return; + } + + // Reset selection changed flag + selectionChanged = false; + + // Note: changing the immediateness of this + // might + // require changes to "clickEvent" immediateness + // also. + if (multiselectmode == MULTISELECT_MODE_DEFAULT) { + // Convert ranges to a set of strings + Set<String> ranges = new HashSet<String>(); + for (SelectionRange range : selectedRowRanges) { + ranges.add(range.toString()); + } + + // Send the selected row ranges + client.updateVariable(paintableId, "selectedRanges", ranges + .toArray(new String[selectedRowRanges.size()]), false); + } + + // Send the selected rows + client.updateVariable(paintableId, "selected", selectedRowKeys + .toArray(new String[selectedRowKeys.size()]), immediate); + + } + + /** + * Get the key that moves the selection head upwards. By default it is the + * up arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that moves the selection head downwards. By default it is the + * down arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that scrolls to the left in the table. By default it is the + * left arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that scroll to the right on the table. By default it is the + * right arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * Get the key that selects an item in the table. By default it is the space + * bar key but by overriding this you can change the key to whatever you + * want. + * + * @return + */ + protected int getNavigationSelectKey() { + return 32; + } + + /** + * Get the key the moves the selection one page up in the table. By default + * this is the Page Up key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationPageUpKey() { + return KeyCodes.KEY_PAGEUP; } + /** + * Get the key the moves the selection one page down in the table. By + * default this is the Page Down key but by overriding this you can change + * the key to whatever you want. + * + * @return + */ + protected int getNavigationPageDownKey() { + return KeyCodes.KEY_PAGEDOWN; + } + + /** + * Get the key the moves the selection to the beginning of the table. By + * default this is the Home key but by overriding this you can change the + * key to whatever you want. + * + * @return + */ + protected int getNavigationStartKey() { + return KeyCodes.KEY_HOME; + } + + /** + * Get the key the moves the selection to the end of the table. By default + * this is the End key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationEndKey() { + return KeyCodes.KEY_END; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal + * .gwt.client.UIDL, com.vaadin.terminal.gwt.client.ApplicationConnection) + */ @SuppressWarnings("unchecked") public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { rendering = true; + + /* + * We need to do this before updateComponent since updateComponent calls + * this.setHeight() which will calculate a new body height depending on + * the space available. + */ + showColFooters = uidl.getBooleanAttribute("colfooters"); + tFoot.setVisible(showColFooters); + if (client.updateComponent(this, uidl, true)) { rendering = false; return; @@ -199,6 +630,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, if (scrollBody != null) { if (totalRows == 0) { tHead.clear(); + tFoot.clear(); } initializedAndAttached = false; initialContentReceived = false; @@ -210,12 +642,16 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, dragmode = uidl.hasAttribute("dragmode") ? uidl .getIntAttribute("dragmode") : 0; + multiselectmode = uidl.hasAttribute("multiselectmode") ? uidl + .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT; + setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr") : CACHE_RATE_DEFAULT); recalcWidths = uidl.hasAttribute("recalcWidths"); if (recalcWidths) { tHead.clear(); + tFoot.clear(); } if (uidl.hasAttribute("pagelength")) { @@ -229,12 +665,15 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, if (firstvisible != lastRequestedFirstvisible && scrollBody != null) { // received 'surprising' firstvisible from server: scroll there firstRowInViewPort = firstvisible; - bodyContainer.setScrollPosition((int) (firstvisible * scrollBody + scrollBodyPanel.setScrollPosition((int) (firstvisible * scrollBody .getRowHeight())); } showRowHeaders = uidl.getBooleanAttribute("rowheaders"); showColHeaders = uidl.getBooleanAttribute("colheaders"); + + nullSelectionAllowed = uidl.hasAttribute("nsa") ? uidl + .getBooleanAttribute("nsa") : true; if (uidl.hasVariable("sortascending")) { sortAscending = uidl.getBooleanVariable("sortascending"); @@ -244,9 +683,11 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, if (uidl.hasVariable("selected")) { final Set<String> selectedKeys = uidl .getStringArrayVariableAsSet("selected"); - selectedRowKeys.clear(); for (String string : selectedKeys) { - selectedRowKeys.add(string); + VScrollTableRow row = getRenderedRowByKey(string); + if (row != null && !row.isSelected()) { + row.toggleSelection(false); + } } } @@ -285,6 +726,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, updateActionMap(c); } else if (c.getTag().equals("visiblecolumns")) { tHead.updateCellsFromUIDL(c); + tFoot.updateCellsFromUIDL(c); } else if (c.getTag().equals("-ac")) { ac = c; } @@ -302,6 +744,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } updateHeader(uidl.getStringArrayAttribute("vcolorder")); + updateFooter(uidl.getStringArrayAttribute("vcolorder")); + if (!recalcWidths && initializedAndAttached) { updateBody(rowData, uidl.getIntAttribute("firstrow"), uidl .getIntAttribute("rows")); @@ -313,7 +757,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // run overflow fix for scrollable area DeferredCommand.addCommand(new Command() { public void execute() { - Util.runWebkitOverflowAutoFix(bodyContainer + Util.runWebkitOverflowAutoFix(scrollBodyPanel .getElement()); } }); @@ -323,11 +767,11 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, scrollBody.removeFromParent(); lazyUnregistryBag.add(scrollBody); } - scrollBody = new VScrollTableBody(); + scrollBody = createScrollBody(); scrollBody.renderInitialRows(rowData, uidl .getIntAttribute("firstrow"), uidl.getIntAttribute("rows")); - bodyContainer.add(scrollBody); + scrollBodyPanel.add(scrollBody); initialContentReceived = true; if (isAttached()) { sizeInit(); @@ -343,10 +787,88 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, hideScrollPositionAnnotation(); purgeUnregistryBag(); + + // This is called when the Home button has been pressed and the pages + // changes + if (selectFirstItemInNextRender) { + selectFirstRenderedRow(false); + selectFirstItemInNextRender = false; + } + + if (focusFirstItemInNextRender) { + selectFirstRenderedRow(true); + focusFirstItemInNextRender = false; + } + + // This is called when the End button has been pressed and the pages + // changes + if (selectLastItemInNextRender) { + selectLastRenderedRow(false); + selectLastItemInNextRender = false; + } + + if (focusLastItemInNextRender) { + selectLastRenderedRow(true); + focusLastItemInNextRender = false; + } + + if (focusedRow != null) { + setRowFocus(getRenderedRowByKey(focusedRow.getKey())); + } + + if (!isFocusable()) { + scrollBodyPanel.getElement().setTabIndex(-1); + } else { + scrollBodyPanel.getElement().setTabIndex(0); + } + rendering = false; headerChangedDuringUpdate = false; } + protected VScrollTableBody createScrollBody() { + return new VScrollTableBody(); + } + + /** + * Selects the last rendered row in the table + * + * @param focusOnly + * Should the focus only be moved to the last row + */ + private void selectLastRenderedRow(boolean focusOnly) { + VScrollTableRow row = null; + Iterator<Widget> it = scrollBody.iterator(); + while (it.hasNext()) { + row = (VScrollTableRow) it.next(); + } + if (row != null) { + setRowFocus(row); + if (!focusOnly) { + deselectAll(); + selectFocusedRow(false, false); + sendSelectedRows(); + } + } + + } + + /** + * Selects the first rendered row + * + * @param focusOnly + * Should the focus only be moved to the first row + */ + private void selectFirstRenderedRow(boolean focusOnly) { + setRowFocus((VScrollTableRow) scrollBody.iterator().next()); + if (!focusOnly) { + deselectAll(); + selectFocusedRow(false, false); + sendSelectedRows(); + } + + } + private void setCacheRate(double d) { if (cache_rate != d) { cache_rate = d; @@ -423,6 +945,38 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } /** + * Updates footers. + * <p> + * Update headers whould be called before this method is called! + * </p> + * + * @param strings + */ + private void updateFooter(String[] strings) { + if (strings == null) { + return; + } + + // Add dummy column if row headers are present + int colIndex = 0; + if (showRowHeaders) { + tFoot.enableColumn("0", colIndex); + colIndex++; + } else { + tFoot.removeCell("0"); + } + + int i; + for (i = 0; i < strings.length; i++) { + final String cid = strings[i]; + tFoot.enableColumn(cid, colIndex); + colIndex++; + } + + tFoot.setVisible(showColFooters); + } + + /** * @param uidl * which contains row data * @param firstRow @@ -498,28 +1052,98 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } private void setColWidth(int colIndex, int w, boolean isDefinedWidth) { - final HeaderCell cell = tHead.getHeaderCell(colIndex); - cell.setWidth(w, isDefinedWidth); + // Set header column width + final HeaderCell hcell = tHead.getHeaderCell(colIndex); + hcell.setWidth(w, isDefinedWidth); + + // Set body column width scrollBody.setColWidth(colIndex, w); + + // Set footer column width + final FooterCell fcell = tFoot.getFooterCell(colIndex); + if (fcell != null) { + fcell.setWidth(w, isDefinedWidth); + } } private int getColWidth(String colKey) { return tHead.getHeaderCell(colKey).getWidth(); } + /** + * Get a rendered row by its key + * + * @param key + * The key to search with + * @return + */ private VScrollTableRow getRenderedRowByKey(String key) { + if (scrollBody != null) { + final Iterator<Widget> it = scrollBody.iterator(); + VScrollTableRow r = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (r.getKey().equals(key)) { + return r; + } + } + } + return null; + } + + /** + * Returns the next row to the given row + * + * @param row + * The row to calculate from + * + * @return The next row or null if no row exists + */ + private VScrollTableRow getNextRow(VScrollTableRow row, int offset) { final Iterator<Widget> it = scrollBody.iterator(); VScrollTableRow r = null; while (it.hasNext()) { r = (VScrollTableRow) it.next(); - if (r.getKey().equals(key)) { + if (r == row) { + r = null; + while (offset >= 0 && it.hasNext()) { + r = (VScrollTableRow) it.next(); + offset--; + } return r; } } + + return null; + } + + /** + * Returns the previous row from the given row + * + * @param row + * The row to calculate from + * @return The previous row or null if no row exists + */ + private VScrollTableRow getPreviousRow(VScrollTableRow row, int offset) { + final Iterator<Widget> it = scrollBody.iterator(); + final Iterator<Widget> offsetIt = scrollBody.iterator(); + VScrollTableRow r = null; + VScrollTableRow prev = null; + while (it.hasNext()) { + r = (VScrollTableRow) it.next(); + if (offset < 0) { + prev = (VScrollTableRow) offsetIt.next(); + } + if (r == row) { + return prev; + } + offset--; + } + return null; } - private void reOrderColumn(String columnKey, int newIndex) { + protected void reOrderColumn(String columnKey, int newIndex) { final int oldIndex = getColIndexByKey(columnKey); @@ -529,6 +1153,9 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // Change body order scrollBody.moveCol(oldIndex, newIndex); + // Change footer order + tFoot.moveCell(oldIndex, newIndex); + /* * Build new columnOrder and update it to server Note that columnOrder * also contains collapsed columns so we cannot directly build it from @@ -622,6 +1249,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, final int[] widths = new int[tHead.visibleCells.size()]; tHead.enableBrowserIntelligence(); + tFoot.enableBrowserIntelligence(); + // first loop: collect natural widths while (headCells.hasNext()) { final HeaderCell hCell = (HeaderCell) headCells.next(); @@ -647,6 +1276,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } tHead.disableBrowserIntelligence(); + tFoot.disableBrowserIntelligence(); boolean willHaveScrollbarz = willHaveScrollbars(); @@ -766,8 +1396,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, if (needsSpaceForHorizontalSrollbar) { bodyHeight += Util.getNativeScrollbarSize(); } - bodyContainer.setHeight(bodyHeight + "px"); - Util.runWebkitOverflowAutoFix(bodyContainer.getElement()); + scrollBodyPanel.setHeight(bodyHeight + "px"); + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); } isNewBody = false; @@ -777,7 +1407,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // without DeferredCommand.addCommand(new Command() { public void execute() { - bodyContainer + scrollBodyPanel .setScrollPosition((int) (firstvisible * scrollBody .getRowHeight())); firstRowInViewPort = firstvisible; @@ -820,7 +1450,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } else { int fakeheight = (int) Math.round(scrollBody.getRowHeight() * totalRows); - int availableHeight = bodyContainer.getElement().getPropertyInt( + int availableHeight = scrollBodyPanel.getElement().getPropertyInt( "clientHeight"); if (fakeheight > availableHeight) { return true; @@ -840,7 +1470,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, Style style = scrollPositionElement.getStyle(); style.setMarginLeft(getElement().getOffsetWidth() / 2 - 80, Unit.PX); - style.setMarginTop(-bodyContainer.getOffsetHeight(), Unit.PX); + style.setMarginTop(-scrollBodyPanel.getOffsetHeight(), Unit.PX); // indexes go from 1-totalRows, as rowheaders in index-mode indicate int last = (firstRowInViewPort + pageLength); @@ -1130,10 +1760,14 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, @Override public void onBrowserEvent(Event event) { if (enabled && event != null) { - if (isResizing || event.getTarget() == colResizeWidget) { + if (isResizing + || event.getEventTarget().cast() == colResizeWidget) { onResizeEvent(event); } else { handleCaptionEvent(event); + if (DOM.eventGetType(event) == Event.ONMOUSEUP) { + scrollBodyPanel.setFocus(true); + } } } } @@ -1167,6 +1801,24 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, floatingCopyOfHeaderCell = null; } + /** + * Fires a header click event after the user has clicked a column header + * cell + * + * @param event + * The click event + */ + private void fireHeaderClickedEvent(Event event) { + if (client.hasEventListeners(VScrollTable.this, + HEADER_CLICK_EVENT_ID)) { + MouseEventDetails details = new MouseEventDetails(event); + client.updateVariable(paintableId, "headerClickEvent", details + .toString(), false); + client.updateVariable(paintableId, "headerClickCID", cid, + immediate); + } + } + protected void handleCaptionEvent(Event event) { switch (DOM.eventGetType(event)) { case Event.ONMOUSEDOWN: @@ -1210,15 +1862,17 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, cid, false); } // get also cache columns at the same request - bodyContainer.setScrollPosition(0); + scrollBodyPanel.setScrollPosition(0); firstvisible = 0; rowRequestHandler.setReqFirstRow(0); rowRequestHandler.setReqRows((int) (2 * pageLength * cache_rate + pageLength)); rowRequestHandler.deferRowFetch(); } + fireHeaderClickedEvent(event); break; } + fireHeaderClickedEvent(event); break; case Event.ONMOUSEMOVE: if (dragging) { @@ -1274,6 +1928,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // readjust undefined width columns lazyAdjustColumnWidths.cancel(); lazyAdjustColumnWidths.schedule(1); + fireColumnResizeEvent(cid, originalWidth, getColWidth(cid)); break; case Event.ONMOUSEMOVE: if (isResizing) { @@ -1503,8 +2158,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, headerChangedDuringUpdate = true; } } - } + // check for orphaned header cells for (Iterator<String> cit = availableCells.keySet().iterator(); cit .hasNext();) { @@ -1514,7 +2169,6 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, cit.remove(); } } - } public void enableColumn(String cid, int index) { @@ -1662,7 +2316,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, @Override public void onBrowserEvent(Event event) { if (enabled) { - if (event.getTarget() == columnSelector) { + if (event.getEventTarget().cast() == columnSelector) { final int left = DOM.getAbsoluteLeft(columnSelector); final int top = DOM.getAbsoluteTop(columnSelector) + DOM.getElementPropertyInt(columnSelector, @@ -1796,6 +2450,529 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } /** + * A cell in the footer + */ + public class FooterCell extends Widget { + private Element td = DOM.createTD(); + private Element captionContainer = DOM.createDiv(); + private char align = ALIGN_LEFT; + private int width = -1; + private float expandRatio = 0; + private String cid; + + public FooterCell(String colId, String headerText) { + cid = colId; + + setText(headerText); + + DOM.setElementProperty(captionContainer, "className", CLASSNAME + + "-footer-container"); + + // ensure no clipping initially (problem on column additions) + DOM.setStyleAttribute(captionContainer, "overflow", "visible"); + + DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS); + + DOM.appendChild(td, captionContainer); + + DOM.sinkEvents(td, Event.MOUSEEVENTS); + + setElement(td); + } + + /** + * Sets the text of the footer + * + * @param footerText + * The text in the footer + */ + public void setText(String footerText) { + DOM.setInnerHTML(captionContainer, footerText); + } + + /** + * Set alignment of the text in the cell + * + * @param c + * The alignment which can be ALIGN_CENTER, ALIGN_LEFT, + * ALIGN_RIGHT + */ + public void setAlign(char c) { + if (align != c) { + switch (c) { + case ALIGN_CENTER: + DOM.setStyleAttribute(captionContainer, "textAlign", + "center"); + break; + case ALIGN_RIGHT: + DOM.setStyleAttribute(captionContainer, "textAlign", + "right"); + break; + default: + DOM.setStyleAttribute(captionContainer, "textAlign", ""); + break; + } + } + align = c; + } + + /** + * Get the alignment of the text int the cell + * + * @return Returns either ALIGN_CENTER, ALIGN_LEFT or ALIGN_RIGHT + */ + public char getAlign() { + return align; + } + + /** + * Sets the width of the cell + * + * @param w + * The width of the cell + * @param ensureDefinedWidth + * Ensures the the given width is not recalculated + */ + public void setWidth(int w, boolean ensureDefinedWidth) { + + if (ensureDefinedWidth) { + // on column resize expand ratio becomes zero + expandRatio = 0; + } + if (width == w) { + return; + } + width = w; + if (width <= 0) { + // go to default mode, clip content if necessary + DOM.setStyleAttribute(captionContainer, "overflow", ""); + } + if (w == -1) { + DOM.setStyleAttribute(captionContainer, "width", ""); + setWidth(""); + } else { + + /* + * Reduce width with one pixel for the right border since the + * footers does not have any spacers between them. + */ + int borderWidths = 1; + + // Set the container width (check for negative value) + if (w - borderWidths >= 0) { + captionContainer.getStyle().setPropertyPx("width", + w - borderWidths); + } else { + captionContainer.getStyle().setPropertyPx("width", 0); + } + + /* + * if we already have tBody, set the header width properly, if + * not defer it. IE will fail with complex float in table header + * unless TD width is not explicitly set. + */ + if (scrollBody != null) { + /* + * Reduce with one since footer does not have any spacers, + * instead a 1 pixel border. + */ + int tdWidth = width + scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(tdWidth + "px"); + } else { + DeferredCommand.addCommand(new Command() { + public void execute() { + int borderWidths = 1; + int tdWidth = width + + scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(tdWidth + "px"); + } + }); + } + } + } + + /** + * Sets the width to undefined + */ + public void setUndefinedWidth() { + setWidth(-1, false); + } + + /** + * Sets the expand ratio of the cell + * + * @param floatAttribute + * The expand ratio + */ + public void setExpandRatio(float floatAttribute) { + expandRatio = floatAttribute; + } + + /** + * Returns the expand ration of the cell + * + * @return The expand ratio + */ + public float getExpandRatio() { + return expandRatio; + } + + /** + * Is the cell enabled? + * + * @return True if enabled else False + */ + public boolean isEnabled() { + return getParent() != null; + } + + /** + * Handle column clicking + */ + + @Override + public void onBrowserEvent(Event event) { + if (enabled && event != null) { + handleCaptionEvent(event); + + if (DOM.eventGetType(event) == Event.ONMOUSEUP) { + scrollBodyPanel.setFocus(true); + } + } + } + + /** + * Handles a event on the captions + * + * @param event + * The event to handle + */ + protected void handleCaptionEvent(Event event) { + if (DOM.eventGetType(event) == Event.ONMOUSEUP) { + fireFooterClickedEvent(event); + } + } + + /** + * Fires a footer click event after the user has clicked a column footer + * cell + * + * @param event + * The click event + */ + private void fireFooterClickedEvent(Event event) { + if (client.hasEventListeners(VScrollTable.this, + FOOTER_CLICK_EVENT_ID)) { + MouseEventDetails details = new MouseEventDetails(event); + client.updateVariable(paintableId, "footerClickEvent", details + .toString(), false); + client.updateVariable(paintableId, "footerClickCID", cid, + immediate); + } + } + + /** + * Returns the column key of the column + * + * @return The column key + */ + public String getColKey() { + return cid; + } + } + + /** + * HeaderCell that is header cell for row headers. + * + * Reordering disabled and clicking on it resets sorting. + */ + public class RowHeadersFooterCell extends FooterCell { + + RowHeadersFooterCell() { + super("0", ""); + } + + @Override + protected void handleCaptionEvent(Event event) { + // NOP: RowHeaders cannot be reordered + // TODO It'd be nice to reset sorting here + } + } + + /** + * The footer of the table which can be seen in the bottom of the Table. + */ + public class TableFooter extends Panel { + + private static final int WRAPPER_WIDTH = 9000; + + ArrayList<Widget> visibleCells = new ArrayList<Widget>(); + HashMap<String, FooterCell> availableCells = new HashMap<String, FooterCell>(); + + Element div = DOM.createDiv(); + Element hTableWrapper = DOM.createDiv(); + Element hTableContainer = DOM.createDiv(); + Element table = DOM.createTable(); + Element headerTableBody = DOM.createTBody(); + Element tr = DOM.createTR(); + + public TableFooter() { + + DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden"); + DOM.setElementProperty(hTableWrapper, "className", CLASSNAME + + "-footer"); + + DOM.appendChild(table, headerTableBody); + DOM.appendChild(headerTableBody, tr); + DOM.appendChild(hTableContainer, table); + DOM.appendChild(hTableWrapper, hTableContainer); + DOM.appendChild(div, hTableWrapper); + setElement(div); + + setStyleName(CLASSNAME + "-footer-wrap"); + + availableCells.put("0", new RowHeadersFooterCell()); + } + + @Override + public void clear() { + for (String cid : availableCells.keySet()) { + removeCell(cid); + } + availableCells.clear(); + availableCells.put("0", new RowHeadersFooterCell()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client + * .ui.Widget) + */ + @Override + public boolean remove(Widget w) { + if (visibleCells.contains(w)) { + visibleCells.remove(w); + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), w.getElement()); + return true; + } + return false; + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.HasWidgets#iterator() + */ + public Iterator<Widget> iterator() { + return visibleCells.iterator(); + } + + /** + * Gets a footer cell which represents the given columnId + * + * @param cid + * The columnId + * + * @return The cell + */ + public FooterCell getFooterCell(String cid) { + return availableCells.get(cid); + } + + /** + * Gets a footer cell by using a column index + * + * @param index + * The index of the column + * @return The Cell + */ + public FooterCell getFooterCell(int index) { + if (index < visibleCells.size()) { + return (FooterCell) visibleCells.get(index); + } else { + return null; + } + } + + /** + * Updates the cells contents when updateUIDL request is received + * + * @param uidl + * The UIDL + */ + public void updateCellsFromUIDL(UIDL uidl) { + Iterator<?> columnIterator = uidl.getChildIterator(); + HashSet<String> updated = new HashSet<String>(); + updated.add("0"); + while (columnIterator.hasNext()) { + final UIDL col = (UIDL) columnIterator.next(); + final String cid = col.getStringAttribute("cid"); + updated.add(cid); + + String caption = col.getStringAttribute("fcaption"); + FooterCell c = getFooterCell(cid); + if (c == null) { + c = new FooterCell(cid, caption); + availableCells.put(cid, c); + if (initializedAndAttached) { + // we will need a column width recalculation + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } else { + c.setText(caption); + } + + if (col.hasAttribute("align")) { + c.setAlign(col.getStringAttribute("align").charAt(0)); + } + if (col.hasAttribute("width")) { + final String width = col.getStringAttribute("width"); + c.setWidth(Integer.parseInt(width), true); + } else if (recalcWidths) { + c.setUndefinedWidth(); + } + if (col.hasAttribute("er")) { + c.setExpandRatio(col.getFloatAttribute("er")); + } + if (col.hasAttribute("collapsed")) { + // ensure header is properly removed from parent (case when + // collapsing happens via servers side api) + if (c.isAttached()) { + c.removeFromParent(); + headerChangedDuringUpdate = true; + } + } + } + + // check for orphaned header cells + for (Iterator<String> cit = availableCells.keySet().iterator(); cit + .hasNext();) { + String cid = cit.next(); + if (!updated.contains(cid)) { + removeCell(cid); + cit.remove(); + } + } + } + + /** + * Set a footer cell for a specified column index + * + * @param index + * The index + * @param cell + * The footer cell + */ + public void setFooterCell(int index, FooterCell cell) { + if (cell.isEnabled()) { + // we're moving the cell + DOM.removeChild(tr, cell.getElement()); + orphan(cell); + } + if (index < visibleCells.size()) { + // insert to right slot + DOM.insertChild(tr, cell.getElement(), index); + adopt(cell); + visibleCells.add(index, cell); + } else if (index == visibleCells.size()) { + // simply append + DOM.appendChild(tr, cell.getElement()); + adopt(cell); + visibleCells.add(cell); + } else { + throw new RuntimeException( + "Header cells must be appended in order"); + } + } + + /** + * Remove a cell by using the columnId + * + * @param colKey + * The columnId to remove + */ + public void removeCell(String colKey) { + final FooterCell c = getFooterCell(colKey); + remove(c); + } + + /** + * Enable a column (Sets the footer cell) + * + * @param cid + * The columnId + * @param index + * The index of the column + */ + public void enableColumn(String cid, int index) { + final FooterCell c = getFooterCell(cid); + if (!c.isEnabled() || getFooterCell(index) != c) { + setFooterCell(index, c); + if (initializedAndAttached) { + headerChangedDuringUpdate = true; + } + } + } + + /** + * Disable browser measurement of the table width + */ + public void disableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH + + "px"); + } + + /** + * Enable browser measurement of the table width + */ + public void enableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", ""); + } + + /** + * Set the horizontal position in the cell in the footer. This is done + * when a horizontal scrollbar is present. + * + * @param scrollLeft + * The value of the leftScroll + */ + public void setHorizontalScrollPosition(int scrollLeft) { + if (BrowserInfo.get().isIE6()) { + hTableWrapper.getStyle().setProperty("position", "relative"); + hTableWrapper.getStyle().setPropertyPx("left", -scrollLeft); + } else { + hTableWrapper.setScrollLeft(scrollLeft); + } + } + + /** + * Swap cells when the column are dragged + * + * @param oldIndex + * The old index of the cell + * @param newIndex + * The new index of the cell + */ + public void moveCell(int oldIndex, int newIndex) { + final FooterCell hCell = getFooterCell(oldIndex); + final Element cell = hCell.getElement(); + + visibleCells.remove(oldIndex); + DOM.removeChild(tr, cell); + + DOM.insertChild(tr, cell, newIndex); + visibleCells.add(newIndex, hCell); + } + } + + /** * This Panel can only contain VScrollTableRow type of widgets. This * "simulates" very large table, keeping spacers which take room of * unrendered rows. @@ -1830,9 +3007,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private char[] aligns; - VScrollTableBody() { + protected VScrollTableBody() { constructDOM(); - setElement(container); } @@ -1862,7 +3038,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } public int getAvailableWidth() { - int availW = bodyContainer.getOffsetWidth() - getBorderWidth(); + int availW = scrollBodyPanel.getOffsetWidth() - getBorderWidth(); return availW; } @@ -1872,8 +3048,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, final Iterator<?> it = rowData.getChildIterator(); aligns = tHead.getColumnAlignments(); while (it.hasNext()) { - final VScrollTableRow row = new VScrollTableRow((UIDL) it - .next(), aligns); + final VScrollTableRow row = createRow((UIDL) it.next(), aligns); addRow(row); } if (isAttached()) { @@ -1887,7 +3062,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, final Iterator<?> it = rowData.getChildIterator(); if (firstIndex == lastRendered + 1) { while (it.hasNext()) { - final VScrollTableRow row = createRow((UIDL) it.next()); + final VScrollTableRow row = prepareRow((UIDL) it.next()); addRow(row); lastRendered++; } @@ -1897,7 +3072,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, int i = rows; while (it.hasNext()) { i--; - rowArray[i] = createRow((UIDL) it.next()); + rowArray[i] = prepareRow((UIDL) it.next()); } for (i = 0; i < rows; i++) { addRowBeforeFirstRendered(rowArray[i]); @@ -1908,7 +3083,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, while (lastRendered + 1 > firstRendered) { unlinkRow(false); } - final VScrollTableRow row = createRow((UIDL) it.next()); + final VScrollTableRow row = prepareRow((UIDL) it.next()); firstRendered = firstIndex; lastRendered = firstIndex - 1; addRow(row); @@ -1916,7 +3091,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, setContainerHeight(); fixSpacers(); while (it.hasNext()) { - addRow(createRow((UIDL) it.next())); + addRow(prepareRow((UIDL) it.next())); lastRendered++; } fixSpacers(); @@ -1962,8 +3137,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, * * @param uidl */ - private VScrollTableRow createRow(UIDL uidl) { - final VScrollTableRow row = new VScrollTableRow(uidl, aligns); + private VScrollTableRow prepareRow(UIDL uidl) { + final VScrollTableRow row = createRow(uidl, aligns); final int cells = DOM.getChildCount(row.getElement()); for (int i = 0; i < cells; i++) { final Element cell = DOM.getChild(row.getElement(), i); @@ -1978,6 +3153,10 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, return row; } + protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) { + return new VScrollTableRow(uidl, aligns); + } + private void addRowBeforeFirstRendered(VScrollTableRow row) { VScrollTableRow first = null; if (renderedRows.size() > 0) { @@ -2194,7 +3373,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // for measuring noCells = true; VScrollTableRow next = (VScrollTableRow) iterator().next(); - next.addCell("", ALIGN_LEFT, "", true); + next.addCell(null, "", ALIGN_LEFT, "", true); firstTD = item.getCells().getItem(0); } com.google.gwt.dom.client.Element wrapper = firstTD @@ -2254,9 +3433,9 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, Container { private static final int DRAGMODE_MULTIROW = 2; - ArrayList<Widget> childWidgets = new ArrayList<Widget>(); + protected ArrayList<Widget> childWidgets = new ArrayList<Widget>(); private boolean selected = false; - private final int rowKey; + protected final int rowKey; private List<UIDL> pendingComponentPaints; private String[] actionKeys = null; @@ -2268,7 +3447,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, rowElement = Document.get().createTRElement(); setElement(rowElement); DOM.sinkEvents(getElement(), Event.MOUSEEVENTS - | Event.ONDBLCLICK | Event.ONCONTEXTMENU); + | Event.ONDBLCLICK | Event.ONCONTEXTMENU + | Event.ONKEYDOWN); } private void paintComponent(Paintable p, UIDL uidl) { @@ -2323,8 +3503,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // row header if (showRowHeaders) { - addCell(buildCaptionHtmlSnippet(uidl), aligns[col++], "", - true); + addCell(uidl, buildCaptionHtmlSnippet(uidl), aligns[col++], + "", true); } if (uidl.hasAttribute("al")) { @@ -2344,17 +3524,19 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } if (cell instanceof String) { - addCell(cell.toString(), aligns[col++], style, false); + addCell(uidl, cell.toString(), aligns[col++], style, + false); } else { final Paintable cellContent = client .getPaintable((UIDL) cell); - addCell((Widget) cellContent, aligns[col++], style); + addCell(uidl, (Widget) cellContent, aligns[col++], + style); paintComponent(cellContent, (UIDL) cell); } } if (uidl.hasAttribute("selected") && !isSelected()) { - toggleSelection(); + toggleSelection(true); } } @@ -2364,11 +3546,11 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, public VScrollTableRow() { this(0); addStyleName(CLASSNAME + "-row"); - addCell("_", 'b', "", true); + addCell(null, "_", 'b', "", true); } - public void addCell(String text, char align, String style, - boolean textIsHTML) { + public void addCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML) { // String only content is optimized by not using Label widget final Element td = DOM.createTD(); final Element container = DOM.createDiv(); @@ -2398,7 +3580,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, getElement().appendChild(td); } - public void addCell(Widget w, char align, String style) { + public void addCell(UIDL rowUidl, Widget w, char align, String style) { final Element td = DOM.createTD(); final Element container = DOM.createDiv(); String className = CLASSNAME + "-cell-content"; @@ -2485,6 +3667,19 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } } + /** + * Add this to the element mouse down event by using + * element.setPropertyJSO + * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it + * then again when the mouse is depressed in the mouse up event. + * + * @return Returns the JSO preventing text selection + */ + private native JavaScriptObject applyDisableTextSelectionIEHack() + /*-{ + return function(){ return false; }; + }-*/; + /* * React on click that occur on content cells only */ @@ -2500,21 +3695,65 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, case Event.ONMOUSEUP: mDown = false; handleClickEvent(event, targetTdOrTr); + scrollBodyPanel.setFocus(true); if (event.getButton() == Event.BUTTON_LEFT && selectMode > Table.SELECT_MODE_NONE) { - toggleSelection(); - // Note: changing the immediateness of this - // might - // require changes to "clickEvent" immediateness - // also. - client - .updateVariable( - paintableId, - "selected", - selectedRowKeys - .toArray(new String[selectedRowKeys - .size()]), - immediate); + + // Ctrl+Shift click + if ((event.getCtrlKey() || event.getMetaKey()) + && event.getShiftKey() + && selectMode == SELECT_MODE_MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT) { + toggleShiftSelection(false); + setRowFocus(this); + + // Ctrl click + } else if ((event.getCtrlKey() || event + .getMetaKey()) + && selectMode == SELECT_MODE_MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT) { + toggleSelection(true); + setRowFocus(this); + + // Ctrl click (Single selection) + } else if ((event.getCtrlKey() || event + .getMetaKey() + && selectMode == SELECT_MODE_SINGLE)) { + if (!isSelected() + || (isSelected() && nullSelectionAllowed)) { + + if (!isSelected()) { + deselectAll(); + } + + toggleSelection(true); + setRowFocus(this); + } + + // Shift click + } else if (event.getShiftKey() + && selectMode == SELECT_MODE_MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT) { + toggleShiftSelection(true); + + // click + } else { + if (multiselectmode == MULTISELECT_MODE_DEFAULT) { + deselectAll(); + } + + selectionRangeStart = this; + toggleSelection(multiselectmode == MULTISELECT_MODE_DEFAULT); + setRowFocus(this); + } + + // Remove IE text selection hack + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()) + .setPropertyJSO("onselectstart", + null); + } + sendSelectedRows(); } break; case Event.ONCONTEXTMENU: @@ -2568,7 +3807,31 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } event.preventDefault(); event.stopPropagation(); + } else if (event.getCtrlKey() + || event.getShiftKey() + || event.getMetaKey() + && selectMode == SELECT_MODE_MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT) { + // Prevent default text selection in Firefox + event.preventDefault(); + + // Prevent default text selection in IE + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()) + .setPropertyJSO( + "onselectstart", + applyDisableTextSelectionIEHack()); + } + + event.stopPropagation(); } + + if (!isFocusable()) { + scrollBodyPanel.getElement().setTabIndex(-1); + } else { + scrollBodyPanel.getElement().setTabIndex(0); + } + break; case Event.ONMOUSEOUT: mDown = false; @@ -2643,25 +3906,112 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, left += Window.getScrollLeft(); client.getContextMenu().showAt(this, left, top); } - event.cancelBubble(true); + event.stopPropagation(); event.preventDefault(); } + /** + * Has the row been selected? + * + * @return Returns true if selected, else false + */ public boolean isSelected() { return selected; } - private void toggleSelection() { + /** + * Toggle the selection of the row + */ + public void toggleSelection(boolean ctrlSelect) { selected = !selected; + selectionChanged = true; if (selected) { - if (selectMode == Table.SELECT_MODE_SINGLE) { - deselectAll(); - } selectedRowKeys.add(String.valueOf(rowKey)); addStyleName("v-selected"); } else { - selectedRowKeys.remove(String.valueOf(rowKey)); removeStyleName("v-selected"); + selectedRowKeys.remove(String.valueOf(rowKey)); + } + removeKeyFromSelectedRange(rowKey); + } + + /** + * Is called when a user clicks an item when holding SHIFT key down. + * This will select a new range from the last cell clicked + * + * @param deselectPrevious + * Should the previous selected range be deselected + */ + private void toggleShiftSelection(boolean deselectPrevious) { + + /* + * Ensures that we are in multiselect mode and that we have a + * previous selection which was not a deselection + */ + if (selectMode == SELECT_MODE_SINGLE) { + // No previous selection found + deselectAll(); + toggleSelection(true); + return; + } + + // Set the selectable range + int startKey; + if (selectionRangeStart != null) { + startKey = Integer.valueOf(selectionRangeStart.getKey()); + } else { + startKey = Integer.valueOf(focusedRow.getKey()); + } + int endKey = rowKey; + if (endKey < startKey) { + // Swap keys if in the wrong order + startKey ^= endKey; + endKey ^= startKey; + startKey ^= endKey; + } + + // Deselect previous items if so desired + if (deselectPrevious) { + deselectAll(); + } + + // Select the range (not including this row) + VScrollTableRow startRow = getRenderedRowByKey(String + .valueOf(startKey)); + VScrollTableRow endRow = getRenderedRowByKey(String + .valueOf(endKey)); + + // If start row is null then we have a multipage selection from + // above + if (startRow == null) { + startRow = (VScrollTableRow) scrollBody.iterator().next(); + setRowFocus(endRow); + } + + if (endRow == null) { + setRowFocus(startRow); + } + + Iterator<Widget> rows = scrollBody.iterator(); + boolean startSelection = false; + while (rows.hasNext()) { + VScrollTableRow row = (VScrollTableRow) rows.next(); + if (row == startRow || startSelection) { + startSelection = true; + if (!row.isSelected()) { + row.toggleSelection(false); + } + selectedRowKeys.add(row.getKey()); + } + + if (row == endRow && row != null) { + startSelection = false; + } + } + + // Add range + if (startRow != endRow) { + selectedRowRanges.add(new SelectionRange(startKey, endKey)); } } @@ -2764,17 +4114,21 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } } + /** + * Deselects all items + */ public void deselectAll() { final Object[] keys = selectedRowKeys.toArray(); for (int i = 0; i < keys.length; i++) { final VScrollTableRow row = getRenderedRowByKey((String) keys[i]); if (row != null && row.isSelected()) { - row.toggleSelection(); + row.toggleSelection(false); + removeKeyFromSelectedRange(Integer.parseInt(row.getKey())); } } // still ensure all selects are removed from (not necessary rendered) selectedRowKeys.clear(); - + selectedRowRanges.clear(); } /** @@ -2790,7 +4144,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } int rowHeight = (int) scrollBody.getRowHeight(); - int bodyH = bodyContainer.getOffsetHeight(); + int bodyH = scrollBodyPanel.getOffsetHeight(); int rowsAtOnce = bodyH / rowHeight; boolean anotherPartlyVisible = ((bodyH % rowHeight) != 0); if (anotherPartlyVisible) { @@ -2806,8 +4160,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, if (currentlyVisible < pageLength && currentlyVisible < totalRows) { // shake scrollpanel to fill empty space - bodyContainer.setScrollPosition(scrollTop + 1); - bodyContainer.setScrollPosition(scrollTop - 1); + scrollBodyPanel.setScrollPosition(scrollTop + 1); + scrollBodyPanel.setScrollPosition(scrollTop - 1); } } } @@ -2839,6 +4193,11 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, super.setWidth(""); } + if (!isFocusable()) { + scrollBodyPanel.getElement().setTabIndex(-1); + } else { + scrollBodyPanel.getElement().setTabIndex(0); + } } private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300; @@ -2922,7 +4281,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, bodyHeight += Util.getNativeScrollbarSize(); } int heightBefore = getOffsetHeight(); - bodyContainer.setHeight(bodyHeight + "px"); + scrollBodyPanel.setHeight(bodyHeight + "px"); if (heightBefore != getOffsetHeight()) { Util.notifyParentOfSizeChange(VScrollTable.this, false); } @@ -2930,7 +4289,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, scrollBody.reLayoutComponents(); DeferredCommand.addCommand(new Command() { public void execute() { - Util.runWebkitOverflowAutoFix(bodyContainer.getElement()); + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); } }); } @@ -2943,7 +4302,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, */ private void setContentWidth(int pixels) { tHead.setWidth(pixels + "px"); - bodyContainer.setWidth(pixels + "px"); + scrollBodyPanel.setWidth(pixels + "px"); + tFoot.setWidth(pixels + "px"); } private int borderWidth = -1; @@ -2953,8 +4313,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, */ private int getBorderWidth() { if (borderWidth < 0) { - borderWidth = Util.measureHorizontalPaddingAndBorder(bodyContainer - .getElement(), 2); + borderWidth = Util.measureHorizontalPaddingAndBorder( + scrollBodyPanel.getElement(), 2); if (borderWidth < 0) { borderWidth = 0; } @@ -2970,11 +4330,12 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, if (height != null && !"".equals(height)) { int contentH = getOffsetHeight(); contentH -= showColHeaders ? tHead.getOffsetHeight() : 0; + contentH -= tFoot.getOffsetHeight(); contentH -= getContentAreaBorderHeight(); if (contentH < 0) { contentH = 0; } - bodyContainer.setHeight(contentH + "px"); + scrollBodyPanel.setHeight(contentH + "px"); } } @@ -2990,15 +4351,15 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, if (contentAreaBorderHeight < 0) { if (BrowserInfo.get().isIE7() || BrowserInfo.get().isIE6()) { contentAreaBorderHeight = Util - .measureVerticalBorder(bodyContainer.getElement()); + .measureVerticalBorder(scrollBodyPanel.getElement()); } else { - DOM.setStyleAttribute(bodyContainer.getElement(), "overflow", + DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow", "hidden"); - int oh = bodyContainer.getOffsetHeight(); - int ch = bodyContainer.getElement().getPropertyInt( + int oh = scrollBodyPanel.getOffsetHeight(); + int ch = scrollBodyPanel.getElement().getPropertyInt( "clientHeight"); contentAreaBorderHeight = oh - ch; - DOM.setStyleAttribute(bodyContainer.getElement(), "overflow", + DOM.setStyleAttribute(scrollBodyPanel.getElement(), "overflow", "auto"); } } @@ -3017,7 +4378,13 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // Webkit may sometimes get an odd rendering bug (white space // between header and body), see bug #3875. Running // overflow hack here to shake body element a bit. - Util.runWebkitOverflowAutoFix(bodyContainer.getElement()); + Util.runWebkitOverflowAutoFix(scrollBodyPanel.getElement()); + } + + if (!isFocusable()) { + scrollBodyPanel.getElement().setTabIndex(-1); + } else { + scrollBodyPanel.getElement().setTabIndex(0); } } @@ -3034,7 +4401,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, if (visible) { DeferredCommand.addCommand(new Command() { public void execute() { - bodyContainer + scrollBodyPanel .setScrollPosition((int) (firstRowInViewPort * scrollBody .getRowHeight())); } @@ -3051,7 +4418,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, * possibly with values caption and icon * @return html snippet containing possibly an icon + caption text */ - private String buildCaptionHtmlSnippet(UIDL uidl) { + protected String buildCaptionHtmlSnippet(UIDL uidl) { String s = uidl.getStringAttribute("caption"); if (uidl.hasAttribute("icon")) { s = "<img src=\"" @@ -3067,13 +4434,13 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, * user scrolls */ public void onScroll(ScrollEvent event) { - scrollLeft = bodyContainer.getElement().getScrollLeft(); - scrollTop = bodyContainer.getScrollPosition(); + scrollLeft = scrollBodyPanel.getElement().getScrollLeft(); + scrollTop = scrollBodyPanel.getScrollPosition(); if (!initializedAndAttached) { return; } if (!enabled) { - bodyContainer + scrollBodyPanel .setScrollPosition((int) (firstRowInViewPort * scrollBody .getRowHeight())); return; @@ -3097,6 +4464,9 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // fix headers horizontal scrolling tHead.setHorizontalScrollPosition(scrollLeft); + // fix footers horizontal scrolling + tFoot.setHorizontalScrollPosition(scrollLeft); + firstRowInViewPort = (int) Math.ceil(scrollTop / scrollBody.getRowHeight()); if (firstRowInViewPort > totalRows - pageLength) { @@ -3321,4 +4691,360 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } + protected VScrollTableRow getFocusedRow() { + return focusedRow; + } + + /** + * Moves the selection head to a specific row + * + * @param row + * The row to where the selection head should move + * @return Returns true if focus was moved successfully, else false + */ + private boolean setRowFocus(VScrollTableRow row) { + + if (selectMode == SELECT_MODE_NONE) { + return false; + } + + // Remove previous selection + if (focusedRow != null && focusedRow != row) { + focusedRow.removeStyleName(CLASSNAME_SELECTION_FOCUS); + } + + if (row != null) { + + // Trying to set focus on already focused row + if (row == focusedRow) { + return false; + } + + // Set new focused row + focusedRow = row; + + // Apply focus style to new selection + focusedRow.addStyleName(CLASSNAME_SELECTION_FOCUS); + + // Scroll up or down if needed + int rowTop = focusedRow.getElement().getAbsoluteTop(); + int scrollTop = scrollBodyPanel.getElement().getAbsoluteTop(); + int scrollBottom = scrollTop + + scrollBodyPanel.getElement().getOffsetHeight(); + if (rowTop > scrollBottom - focusedRow.getOffsetHeight()) { + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() + + focusedRow.getOffsetHeight()); + } else if (rowTop < scrollTop) { + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() + - focusedRow.getOffsetHeight()); + } + + return true; + } else { + focusedRow = null; + } + + return false; + } + + /** + * Handles the keyboard events handled by the table + * + * @param event + * The keyboard event received + * @return true iff the navigation event was handled + */ + protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + if (keycode == KeyCodes.KEY_TAB) { + // Do not handle tab key + return false; + } + + // Down navigation + if (selectMode == SELECT_MODE_NONE && keycode == getNavigationDownKey()) { + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() + + scrollingVelocity); + return true; + } else if (keycode == getNavigationDownKey()) { + if (selectMode == SELECT_MODE_MULTI && moveFocusDown()) { + selectFocusedRow(ctrl, shift); + + } else if (selectMode == SELECT_MODE_SINGLE && !shift + && moveFocusDown()) { + selectFocusedRow(ctrl, shift); + } + return true; + } + + // Up navigation + if (selectMode == SELECT_MODE_NONE && keycode == getNavigationUpKey()) { + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() + - scrollingVelocity); + return true; + } else if (keycode == getNavigationUpKey()) { + if (selectMode == SELECT_MODE_MULTI && moveFocusUp()) { + selectFocusedRow(ctrl, shift); + } else if (selectMode == SELECT_MODE_SINGLE && !shift + && moveFocusUp()) { + selectFocusedRow(ctrl, shift); + } + return true; + } + + if (keycode == getNavigationLeftKey()) { + // Left navigation + scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel + .getHorizontalScrollPosition() + - scrollingVelocity); + return true; + + } else if (keycode == getNavigationRightKey()) { + // Right navigation + scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel + .getHorizontalScrollPosition() + + scrollingVelocity); + } + + // Select navigation + if (selectMode > SELECT_MODE_NONE + && keycode == getNavigationSelectKey()) { + if (selectMode == SELECT_MODE_SINGLE) { + boolean wasSelected = focusedRow.isSelected(); + deselectAll(); + if (!wasSelected || !nullSelectionAllowed ) { + focusedRow.toggleSelection(true); + } + + } else { + focusedRow.toggleSelection(true); + } + + sendSelectedRows(); + return true; + } + + // Page Down navigation + if (keycode == getNavigationPageDownKey()) { + int rowHeight = (int) scrollBody.getRowHeight(); + int offset = pageLength * rowHeight - rowHeight; + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() + + offset); + if (selectMode > SELECT_MODE_NONE) { + if (!moveFocusDown(pageLength - 2)) { + final int lastRendered = scrollBody.getLastRendered(); + if (lastRendered == totalRows - 1) { + selectLastRenderedRow(false); + } else { + selectLastItemInNextRender = true; + } + } else { + selectFocusedRow(false, false); + sendSelectedRows(); + } + } + return true; + } + + // Page Up navigation + if (keycode == getNavigationPageUpKey()) { + int rowHeight = (int) scrollBody.getRowHeight(); + int offset = pageLength * rowHeight - rowHeight; + scrollBodyPanel.setScrollPosition(scrollBodyPanel + .getScrollPosition() + - offset); + if (selectMode > SELECT_MODE_NONE) { + if (!moveFocusUp(pageLength - 2)) { + final int firstRendered = scrollBody.getFirstRendered(); + if (firstRendered == 0) { + selectFirstRenderedRow(false); + } else { + selectFirstItemInNextRender = true; + } + } else { + selectFocusedRow(false, false); + sendSelectedRows(); + } + } + return true; + } + + // Goto start navigation + if (keycode == getNavigationStartKey()) { + if (selectMode > SELECT_MODE_NONE) { + final int firstRendered = scrollBody.getFirstRendered(); + boolean focusOnly = ctrl; + if (firstRendered == 0) { + selectFirstRenderedRow(focusOnly); + } else if (focusOnly) { + focusFirstItemInNextRender = true; + } else { + selectFirstItemInNextRender = true; + } + } + scrollBodyPanel.setScrollPosition(0); + return true; + } + + // Goto end navigation + if (keycode == getNavigationEndKey()) { + if (selectMode > SELECT_MODE_NONE) { + final int lastRendered = scrollBody.getLastRendered(); + boolean focusOnly = ctrl; + if (lastRendered == totalRows - 1) { + selectLastRenderedRow(focusOnly); + } else if (focusOnly) { + focusLastItemInNextRender = true; + } else { + selectLastItemInNextRender = true; + } + } + scrollBodyPanel.setScrollPosition(scrollBody.getOffsetHeight()); + return true; + } + + return false; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google + * .gwt.event.dom.client.KeyPressEvent) + */ + public void onKeyPress(KeyPressEvent event) { + if (handleNavigation(event.getNativeEvent().getKeyCode(), event + .isControlKeyDown() + || event.isMetaKeyDown(), event.isShiftKeyDown())) { + event.preventDefault(); + } + + // Start the velocityTimer + if (scrollingVelocityTimer == null) { + scrollingVelocityTimer = new Timer() { + @Override + public void run() { + scrollingVelocity++; + } + }; + scrollingVelocityTimer.scheduleRepeating(100); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + public void onKeyDown(KeyDownEvent event) { + if (handleNavigation(event.getNativeEvent().getKeyCode(), event + .isControlKeyDown() + || event.isMetaKeyDown(), event.isShiftKeyDown())) { + event.preventDefault(); + } + + // Start the velocityTimer + if (scrollingVelocityTimer == null) { + scrollingVelocityTimer = new Timer() { + @Override + public void run() { + scrollingVelocity++; + } + }; + scrollingVelocityTimer.scheduleRepeating(100); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + public void onFocus(FocusEvent event) { + if (isFocusable()) { + scrollBodyPanel.addStyleName("focused"); + + // Focus a row if no row is in focus + if (focusedRow == null) { + setRowFocus((VScrollTableRow) scrollBody.iterator().next()); + } + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event + * .dom.client.BlurEvent) + */ + public void onBlur(BlurEvent event) { + scrollBodyPanel.removeStyleName("focused"); + + // Unfocus any row + setRowFocus(null); + } + + /** + * Removes a key from a range if the key is found in a selected range + * + * @param key + * The key to remove + */ + private void removeKeyFromSelectedRange(int key) { + for (SelectionRange range : selectedRowRanges) { + if (range.inRange(key)) { + int start = range.getStartKey(); + int end = range.getEndKey(); + + if (start < key && end > key) { + selectedRowRanges.add(new SelectionRange(start, key - 1)); + selectedRowRanges.add(new SelectionRange(key + 1, end)); + } else if (start == key && start < end) { + selectedRowRanges.add(new SelectionRange(start + 1, end)); + } else if (end == key && start < end) { + selectedRowRanges.add(new SelectionRange(start, end - 1)); + } + + selectedRowRanges.remove(range); + + break; + } + } + } + + /** + * Can the Table be focused? + * + * @return True if the table can be focused, else false + */ + public boolean isFocusable() { + if (scrollBody != null) { + boolean hasVerticalScrollbars = scrollBody.getOffsetHeight() > scrollBodyPanel + .getOffsetHeight(); + boolean hasHorizontalScrollbars = scrollBody.getOffsetWidth() > scrollBodyPanel + .getOffsetWidth(); + return !(!hasHorizontalScrollbars && !hasVerticalScrollbars && selectMode == SELECT_MODE_NONE); + } + + return false; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Focusable#focus() + */ + public void focus() { + scrollBodyPanel.focus(); + } } diff --git a/src/com/vaadin/terminal/gwt/client/ui/VSlider.java b/src/com/vaadin/terminal/gwt/client/ui/VSlider.java index 81fb898011..d30c2b2f20 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VSlider.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VSlider.java @@ -4,6 +4,7 @@ //
package com.vaadin.terminal.gwt.client.ui;
+import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
@@ -12,14 +13,13 @@ import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.Widget;
import com.vaadin.terminal.gwt.client.ApplicationConnection;
import com.vaadin.terminal.gwt.client.BrowserInfo;
import com.vaadin.terminal.gwt.client.ContainerResizedListener;
import com.vaadin.terminal.gwt.client.Paintable;
import com.vaadin.terminal.gwt.client.UIDL;
-public class VSlider extends Widget implements Paintable, Field,
+public class VSlider extends SimpleFocusablePanel implements Paintable, Field,
ContainerResizedListener {
public static final String CLASSNAME = "v-slider";
@@ -39,6 +39,7 @@ public class VSlider extends Widget implements Paintable, Field, private boolean readonly;
private boolean scrollbarStyle;
+ private int acceleration = 1;
private int handleSize;
private double min;
private double max;
@@ -76,7 +77,6 @@ public class VSlider extends Widget implements Paintable, Field, public VSlider() {
super();
- setElement(DOM.createDiv());
base = DOM.createDiv();
handle = DOM.createDiv();
smaller = DOM.createDiv();
@@ -98,18 +98,19 @@ public class VSlider extends Widget implements Paintable, Field, DOM.setStyleAttribute(bigger, "display", "none");
DOM.setStyleAttribute(handle, "visibility", "hidden");
- DOM.sinkEvents(getElement(), Event.MOUSEEVENTS | Event.ONMOUSEWHEEL);
+ DOM.sinkEvents(getElement(), Event.MOUSEEVENTS | Event.ONMOUSEWHEEL
+ | Event.KEYEVENTS | Event.FOCUSEVENTS);
DOM.sinkEvents(base, Event.ONCLICK);
DOM.sinkEvents(handle, Event.MOUSEEVENTS);
DOM.sinkEvents(smaller, Event.ONMOUSEDOWN | Event.ONMOUSEUP
| Event.ONMOUSEOUT);
DOM.sinkEvents(bigger, Event.ONMOUSEDOWN | Event.ONMOUSEUP
| Event.ONMOUSEOUT);
-
+
feedbackPopup.addStyleName(CLASSNAME + "-feedback");
feedbackPopup.setWidget(feedback);
}
-
+
public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
this.client = client;
@@ -332,8 +333,35 @@ public class VSlider extends Widget implements Paintable, Field, decreaseValue(true);
} else if (targ == bigger) {
increaseValue(true);
- } else {
+ } else if (DOM.eventGetType(event) == Event.MOUSEEVENTS) {
processBaseEvent(event);
+ } else if ((BrowserInfo.get().isGecko() && DOM.eventGetType(event) == Event.ONKEYPRESS)
+ || (!BrowserInfo.get().isGecko() && DOM.eventGetType(event) == Event.ONKEYDOWN)) {
+
+ if (handleNavigation(event.getKeyCode(), event.getCtrlKey(), event
+ .getShiftKey())) {
+
+ feedbackPopup.show();
+
+ if (scrollTimer != null) {
+ scrollTimer.cancel();
+ }
+ scrollTimer = new Timer() {
+ @Override
+ public void run() {
+ updateValueToServer();
+ acceleration = 1;
+ }
+ };
+ scrollTimer.schedule(100);
+
+ DOM.eventPreventDefault(event);
+ DOM.eventCancelBubble(event, true);
+ }
+ } else if (DOM.eventGetType(event) == Event.ONFOCUS) {
+ feedbackPopup.show();
+ } else if (DOM.eventGetType(event) == Event.ONBLUR) {
+ feedbackPopup.hide();
}
}
@@ -478,4 +506,88 @@ public class VSlider extends Widget implements Paintable, Field, client.updateVariable(id, "value", value.doubleValue(), immediate);
}
+ /**
+ * Handles the keyboard events handled by the Slider
+ *
+ * @param event
+ * The keyboard event received
+ * @return true iff the navigation event was handled
+ */
+ public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+
+ // No support for ctrl moving
+ if (ctrl) {
+ return false;
+ }
+
+ if ((keycode == getNavigationUpKey() && vertical)
+ || (keycode == getNavigationRightKey() && !vertical)) {
+ if (shift) {
+ for (int a = 0; a < acceleration; a++) {
+ increaseValue(false);
+ }
+ acceleration++;
+ } else {
+ increaseValue(false);
+ }
+ return true;
+ } else if (keycode == getNavigationDownKey() && vertical
+ || (keycode == getNavigationLeftKey() && !vertical)) {
+ if (shift) {
+ for (int a = 0; a < acceleration; a++) {
+ decreaseValue(false);
+ }
+ acceleration++;
+ } else {
+ decreaseValue(false);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the key that increases the vertical slider. By default it is the up
+ * arrow key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationUpKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /**
+ * Get the key that decreases the vertical slider. By default it is the down
+ * arrow key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationDownKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Get the key that decreases the horizontal slider. By default it is the
+ * left arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationLeftKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Get the key that increases the horizontal slider. By default it is the
+ * right arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationRightKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/VTextualDate.java b/src/com/vaadin/terminal/gwt/client/ui/VTextualDate.java index 510676b3a4..209c5cabde 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VTextualDate.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VTextualDate.java @@ -356,4 +356,12 @@ public class VTextualDate extends VDateField implements Paintable, Field, text.setFocus(true); } + protected String getText() { + return text.getText(); + } + + protected void setText(String text) { + this.text.setText(text); + } + } diff --git a/src/com/vaadin/terminal/gwt/client/ui/VTree.java b/src/com/vaadin/terminal/gwt/client/ui/VTree.java index 42421b80de..9f5202cb68 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VTree.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VTree.java @@ -4,12 +4,25 @@ package com.vaadin.terminal.gwt.client.ui; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.Set; +import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; @@ -37,18 +50,28 @@ import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation; /** * */ -public class VTree extends FlowPanel implements Paintable, VHasDropHandler { +public class VTree extends SimpleFocusablePanel implements Paintable, + VHasDropHandler, FocusHandler, BlurHandler, KeyPressHandler, + KeyDownHandler { public static final String CLASSNAME = "v-tree"; public static final String ITEM_CLICK_EVENT_ID = "itemClick"; + public static final int MULTISELECT_MODE_DEFAULT = 0; + public static final int MULTISELECT_MODE_SIMPLE = 1; + + private final FlowPanel body = new FlowPanel(); + private Set<String> selectedIds = new HashSet<String>(); private ApplicationConnection client; private String paintableId; private boolean selectable; private boolean isMultiselect; private String currentMouseOverKey; + private TreeNode lastSelection; + private TreeNode focusedNode; + private int multiSelectMode = MULTISELECT_MODE_DEFAULT; private final HashMap<String, TreeNode> keyToNode = new HashMap<String, TreeNode>(); @@ -72,9 +95,80 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { private int dragMode; + private boolean selectionHasChanged = false; + public VTree() { super(); setStyleName(CLASSNAME); + add(body); + + addFocusHandler(this); + addBlurHandler(this); + + /* + * Firefox auto-repeat works correctly only if we use a key press + * handler, other browsers handle it correctly when using a key down + * handler + */ + if (BrowserInfo.get().isGecko() || BrowserInfo.get().isOpera()) { + addKeyPressHandler(this); + } else { + addKeyDownHandler(this); + } + + /* + * We need to use the sinkEvents method to catch the keyUp events so we + * can cache a single shift. KeyUpHandler cannot do this. At the same + * time we catch the mouse down and up events so we can apply the text + * selection patch in IE + */ + sinkEvents(Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONKEYUP); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user + * .client.Event) + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONMOUSEDOWN) { + // Prevent default text selection in IE + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()).setPropertyJSO( + "onselectstart", applyDisableTextSelectionIEHack()); + } else { + setFocus(true); + } + + } else if (event.getTypeInt() == Event.ONMOUSEUP) { + // Remove IE text selection hack + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()).setPropertyJSO( + "onselectstart", null); + } + } else if (event.getTypeInt() == Event.ONKEYUP) { + if (selectionHasChanged) { + if (event.getKeyCode() == getNavigationDownKey() + && !event.getShiftKey()) { + sendSelectionToServer(); + event.preventDefault(); + } else if (event.getKeyCode() == getNavigationUpKey() + && !event.getShiftKey()) { + sendSelectionToServer(); + event.preventDefault(); + } else if (event.getKeyCode() == KeyCodes.KEY_SHIFT) { + sendSelectionToServer(); + event.preventDefault(); + } else if (event.getKeyCode() == getNavigationSelectKey()) { + sendSelectionToServer(); + event.preventDefault(); + } + } + } } private void updateActionMap(UIDL c) { @@ -129,7 +223,7 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { isNullSelectionAllowed = uidl.getBooleanAttribute("nullselect"); - clear(); + body.clear(); for (final Iterator i = uidl.getChildIterator(); i.hasNext();) { final UIDL childUidl = (UIDL) i.next(); if ("actions".equals(childUidl.getTag())) { @@ -141,17 +235,21 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { } final TreeNode childTree = new TreeNode(); if (childTree.ie6compatnode != null) { - this.add(childTree); + body.add(childTree); } childTree.updateFromUIDL(childUidl, client); if (childTree.ie6compatnode == null) { - this.add(childTree); + body.add(childTree); } } final String selectMode = uidl.getStringAttribute("selectmode"); selectable = !"none".equals(selectMode); isMultiselect = "multi".equals(selectMode); + if (isMultiselect) { + multiSelectMode = uidl.getIntAttribute("multiselectmode"); + } + selectedIds = uidl.getStringArrayVariableAsSet("selected"); rendering = false; @@ -314,10 +412,25 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { treeNode.setSelected(false); } + sendSelectionToServer(); + } + + /** + * Sends the selection to the server + */ + private void sendSelectionToServer() { client.updateVariable(paintableId, "selected", selectedIds .toArray(new String[selectedIds.size()]), immediate); + selectionHasChanged = false; } + /** + * Is a node selected in the tree + * + * @param treeNode + * The node to check + * @return + */ public boolean isSelected(TreeNode treeNode) { return selectedIds.contains(treeNode.key); } @@ -325,6 +438,7 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { public class TreeNode extends SimplePanel implements ActionOwner { public static final String CLASSNAME = "v-tree-node"; + public static final String CLASSNAME_FOCUSED = CLASSNAME + "-focused"; public String key; @@ -348,6 +462,8 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { private int cachedHeight = -1; + private boolean focused = false; + public TreeNode() { constructDom(); sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS @@ -430,6 +546,57 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { } } + /** + * Handles mouse selection + * + * @param ctrl + * Was the ctrl-key pressed + * @param shift + * Was the shift-key pressed + * @return Returns true if event was handled, else false + */ + private boolean handleClickSelection(boolean ctrl, boolean shift) { + + if (multiSelectMode == MULTISELECT_MODE_SIMPLE || !isMultiselect) { + toggleSelection(); + setFocusedNode(this); + lastSelection = this; + } else if (multiSelectMode == MULTISELECT_MODE_DEFAULT) { + // Handle ctrl+click + if (isMultiselect && ctrl && !shift) { + toggleSelection(); + setFocusedNode(this); + lastSelection = this; + + // Handle shift+click + } else if (isMultiselect && !ctrl && shift) { + deselectAll(); + selectNodeRange(lastSelection.key, key); + sendSelectionToServer(); + + // Handle ctrl+shift click + } else if (isMultiselect && ctrl && shift) { + selectNodeRange(lastSelection.key, key); + + // Handle click + } else { + deselectAll(); + toggleSelection(); + setFocusedNode(this); + lastSelection = this; + } + } + + return true; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt + * .user.client.Event) + */ @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); @@ -451,15 +618,22 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { if (getElement() == target || ie6compatnode == target) { // state change toggleState(); - } else if (!readonly && inCaption) { + } else if (!readonly && inCaption && selectable) { // caption click = selection change && possible click event - toggleSelection(); + if (handleClickSelection(event.getCtrlKey() + || event.getMetaKey(), event.getShiftKey())) { + event.preventDefault(); + } } DOM.eventCancelBubble(event, true); } else if (type == Event.ONCONTEXTMENU) { showContextMenu(event); } + if (type == Event.ONMOUSEDOWN) { + event.preventDefault(); + } + if (dragMode != 0 || dropHandler != null) { if (type == Event.ONMOUSEDOWN) { if (nodeCaptionDiv.isOrHasChild(event.getTarget())) { @@ -674,6 +848,24 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { return childrenLoaded; } + /** + * Returns the children of the node + * + * @return A set of tree nodes + */ + public List<TreeNode> getChildren() { + List<TreeNode> nodes = new LinkedList<TreeNode>(); + + if (!isLeaf() && isChildrenLoaded()) { + Iterator<Widget> iter = childNodeContainer.iterator(); + while (iter.hasNext()) { + TreeNode node = (TreeNode) iter.next(); + nodes.add(node); + } + } + return nodes; + } + public Action[] getActions() { if (actionKeys == null) { return new Action[] {}; @@ -713,6 +905,30 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { return VTree.this.isSelected(this); } + /** + * Travels up the hierarchy looking for this node + * + * @param child + * The child which grandparent this is or is not + * @return True if this is a grandparent of the child node + */ + public boolean isGrandParentOf(TreeNode child) { + TreeNode currentNode = child; + boolean isGrandParent = false; + while (currentNode != null) { + currentNode = currentNode.getParentNode(); + if (currentNode == this) { + isGrandParent = true; + break; + } + } + return isGrandParent; + } + + public boolean isSibling(TreeNode node) { + return node.getParentNode() == getParentNode(); + } + public void showContextMenu(Event event) { if (!readonly && !disabled) { if (actionKeys != null) { @@ -740,6 +956,11 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { setWidth(captionWidth + "px"); } + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.Widget#onAttach() + */ @Override public void onAttach() { super.onAttach(); @@ -748,11 +969,49 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { } } + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.Widget#onDetach() + */ @Override protected void onDetach() { super.onDetach(); client.getContextMenu().ensureHidden(this); } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.UIObject#toString() + */ + @Override + public String toString() { + return nodeCaptionSpan.getInnerText(); + } + + /** + * Is the node focused? + * + * @param focused + * True if focused, false if not + */ + public void setFocused(boolean focused) { + if (!this.focused && focused) { + nodeCaptionDiv.addClassName(CLASSNAME_FOCUSED); + if (BrowserInfo.get().isIE6()) { + ie6compatnode.addClassName(CLASSNAME_FOCUSED); + } + this.focused = focused; + } else if (this.focused && !focused) { + nodeCaptionDiv.removeClassName(CLASSNAME_FOCUSED); + if (BrowserInfo.get().isIE6()) { + ie6compatnode.removeClassName(CLASSNAME_FOCUSED); + } + this.focused = focused; + } + } + } public VDropHandler getDropHandler() { @@ -762,4 +1021,801 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { public TreeNode getNodeByKey(String key) { return keyToNode.get(key); } + + /** + * Deselects all items in the tree + */ + public void deselectAll() { + for (String key : selectedIds) { + TreeNode node = keyToNode.get(key); + if (node != null) { + node.setSelected(false); + } + } + selectedIds.clear(); + selectionHasChanged = true; + } + + /** + * Selects a range of nodes + * + * @param startNodeKey + * The start node key + * @param endNodeKey + * The end node key + */ + private void selectNodeRange(String startNodeKey, String endNodeKey) { + + TreeNode startNode = keyToNode.get(startNodeKey); + TreeNode endNode = keyToNode.get(endNodeKey); + + // The nodes have the same parent + if (startNode.getParent() == endNode.getParent()) { + doSiblingSelection(startNode, endNode); + + // The start node is a grandparent of the end node + } else if (startNode.isGrandParentOf(endNode)) { + doRelationSelection(startNode, endNode); + + // The end node is a grandparent of the start node + } else if (endNode.isGrandParentOf(startNode)) { + doRelationSelection(endNode, startNode); + + } else { + doNoRelationSelection(startNode, endNode); + } + } + + /** + * Selects a node and deselect all other nodes + * + * @param node + * The node to select + */ + private void selectNode(TreeNode node, boolean deselectPrevious) { + if (deselectPrevious) { + deselectAll(); + } + + if (node != null) { + node.setSelected(true); + selectedIds.add(node.key); + lastSelection = node; + } + selectionHasChanged = true; + } + + /** + * Deselects a node + * + * @param node + * The node to deselect + */ + private void deselectNode(TreeNode node) { + node.setSelected(false); + selectedIds.remove(node.key); + selectionHasChanged = true; + } + + /** + * Selects all the open children to a node + * + * @param node + * The parent node + */ + private void selectAllChildren(TreeNode node, boolean includeRootNode) { + if (includeRootNode) { + node.setSelected(true); + selectedIds.add(node.key); + } + + for (TreeNode child : node.getChildren()) { + if (!child.isLeaf() && child.getState()) { + selectAllChildren(child, true); + } else { + child.setSelected(true); + selectedIds.add(child.key); + } + } + selectionHasChanged = true; + } + + /** + * Selects all children until a stop child is reached + * + * @param root + * The root not to start from + * @param stopNode + * The node to finish with + * @param includeRootNode + * Should the root node be selected + * @param includeStopNode + * Should the stop node be selected + * + * @return Returns false if the stop child was found, else true if all + * children was selected + */ + private boolean selectAllChildrenUntil(TreeNode root, TreeNode stopNode, + boolean includeRootNode, boolean includeStopNode) { + if (includeRootNode) { + root.setSelected(true); + selectedIds.add(root.key); + } + if (root.getState() && root != stopNode) { + for (TreeNode child : root.getChildren()) { + if (!child.isLeaf() && child.getState() && child != stopNode) { + if (!selectAllChildrenUntil(child, stopNode, true, + includeStopNode)) { + return false; + } + } else if (child == stopNode) { + if (includeStopNode) { + child.setSelected(true); + selectedIds.add(child.key); + } + return false; + } else if (child.isLeaf()) { + child.setSelected(true); + selectedIds.add(child.key); + } + } + } + selectionHasChanged = true; + + return true; + } + + /** + * Select a range between two nodes which have no relation to each other + * + * @param startNode + * The start node to start the selection from + * @param endNode + * The end node to end the selection to + */ + private void doNoRelationSelection(TreeNode startNode, TreeNode endNode) { + + TreeNode commonParent = getCommonGrandParent(startNode, endNode); + TreeNode startBranch = null, endBranch = null; + + // Find the children of the common parent + List<TreeNode> children; + if (commonParent != null) { + children = commonParent.getChildren(); + } else { + children = new LinkedList<TreeNode>(); + for (int w = 0; w < body.getWidgetCount(); w++) { + children.add((TreeNode) body.getWidget(w)); + } + } + + // Find the start and end branches + for (TreeNode node : children) { + if (nodeIsInBranch(startNode, node)) { + startBranch = node; + } + if (nodeIsInBranch(endNode, node)) { + endBranch = node; + } + } + + // Swap nodes if necessary + if (children.indexOf(startBranch) > children.indexOf(endBranch)) { + TreeNode temp = startBranch; + startBranch = endBranch; + endBranch = temp; + + temp = startNode; + startNode = endNode; + endNode = temp; + } + + // Select all children under the start node + selectAllChildren(startNode, true); + TreeNode startParent = startNode.getParentNode(); + TreeNode currentNode = startNode; + while (startParent != null && startParent != commonParent) { + List<TreeNode> startChildren = startParent.getChildren(); + for (int i = startChildren.indexOf(currentNode) + 1; i < startChildren + .size(); i++) { + selectAllChildren(startChildren.get(i), true); + } + + currentNode = startParent; + startParent = startParent.getParentNode(); + } + + // Select nodes until the end node is reached + for (int i = children.indexOf(startBranch) + 1; i <= children + .indexOf(endBranch); i++) { + selectAllChildrenUntil(children.get(i), endNode, true, true); + } + + // Ensure end node was selected + endNode.setSelected(true); + selectedIds.add(endNode.key); + selectionHasChanged = true; + } + + /** + * Examines the children of the branch node and returns true if a node is in + * that branch + * + * @param node + * The node to search for + * @param branch + * The branch to search in + * @return True if found, false if not found + */ + private boolean nodeIsInBranch(TreeNode node, TreeNode branch) { + if (node == branch) { + return true; + } + for (TreeNode child : branch.getChildren()) { + if (child == node) { + return true; + } + if (!child.isLeaf() && child.getState()) { + if (nodeIsInBranch(node, child)) { + return true; + } + } + } + return false; + } + + /** + * Selects a range of items which are in direct relation with each other.<br/> + * NOTE: The start node <b>MUST</b> be before the end node! + * + * @param startNode + * + * @param endNode + */ + private void doRelationSelection(TreeNode startNode, TreeNode endNode) { + TreeNode currentNode = endNode; + while (currentNode != startNode) { + currentNode.setSelected(true); + selectedIds.add(currentNode.key); + + // Traverse children above the selection + List<TreeNode> subChildren = currentNode.getParentNode() + .getChildren(); + if (subChildren.size() > 1) { + selectNodeRange(subChildren.iterator().next().key, + currentNode.key); + } else if (subChildren.size() == 1) { + TreeNode n = subChildren.get(0); + n.setSelected(true); + selectedIds.add(n.key); + } + + currentNode = currentNode.getParentNode(); + } + startNode.setSelected(true); + selectedIds.add(startNode.key); + selectionHasChanged = true; + } + + /** + * Selects a range of items which have the same parent. + * + * @param startNode + * The start node + * @param endNode + * The end node + */ + private void doSiblingSelection(TreeNode startNode, TreeNode endNode) { + TreeNode parent = startNode.getParentNode(); + + List<TreeNode> children = new LinkedList<TreeNode>(); + if (parent == null) { + // Topmost parent + for (int w = 0; w < body.getWidgetCount(); w++) { + children.add((TreeNode) body.getWidget(w)); + } + } else { + children = parent.getChildren(); + } + + // Swap start and end point if needed + if (children.indexOf(startNode) > children.indexOf(endNode)) { + TreeNode temp = startNode; + startNode = endNode; + endNode = temp; + } + + Iterator<TreeNode> childIter = children.iterator(); + boolean startFound = false; + while (childIter.hasNext()) { + TreeNode node = childIter.next(); + if (node == startNode) { + startFound = true; + } + + if (startFound && node != endNode && node.getState()) { + selectAllChildren(node, true); + } else if (startFound && node != endNode) { + node.setSelected(true); + selectedIds.add(node.key); + } + + if (node == endNode) { + node.setSelected(true); + selectedIds.add(node.key); + break; + } + } + selectionHasChanged = true; + } + + /** + * Returns the first common parent of two nodes + * + * @param node1 + * The first node + * @param node2 + * The second node + * @return The common parent or null + */ + public TreeNode getCommonGrandParent(TreeNode node1, TreeNode node2) { + // If either one does not have a grand parent then return null + if (node1.getParentNode() == null || node2.getParentNode() == null) { + return null; + } + + // If the nodes are parents of each other then return null + if (node1.isGrandParentOf(node2) || node2.isGrandParentOf(node1)) { + return null; + } + + // Get parents of node1 + List<TreeNode> parents1 = new ArrayList<TreeNode>(); + TreeNode parent1 = node1.getParentNode(); + while (parent1 != null) { + parents1.add(parent1); + parent1 = parent1.getParentNode(); + } + + // Get parents of node2 + List<TreeNode> parents2 = new ArrayList<TreeNode>(); + TreeNode parent2 = node2.getParentNode(); + while (parent2 != null) { + parents2.add(parent2); + parent2 = parent2.getParentNode(); + } + + // Search the parents for the first common parent + for (int i = 0; i < parents1.size(); i++) { + parent1 = parents1.get(i); + for (int j = 0; j < parents2.size(); j++) { + parent2 = parents2.get(j); + if (parent1 == parent2) { + return parent1; + } + } + } + + return null; + } + + /** + * Sets the node currently in focus + * + * @param node + * The node to focus or null to remove the focus completely + */ + public void setFocusedNode(TreeNode node) { + // Unfocus previously focused node + if (focusedNode != null) { + focusedNode.setFocused(false); + } + + if (node != null) { + node.setFocused(true); + } + + focusedNode = node; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + public void onFocus(FocusEvent event) { + // If no node has focus, focus the first item in the tree + if (focusedNode == null && selectable) { + setFocusedNode((TreeNode) body.getWidget(0)); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event + * .dom.client.BlurEvent) + */ + public void onBlur(BlurEvent event) { + setFocusedNode(null); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google + * .gwt.event.dom.client.KeyPressEvent) + */ + public void onKeyPress(KeyPressEvent event) { + if (handleKeyNavigation(event.getNativeEvent().getKeyCode(), event + .isControlKeyDown() + || event.isMetaKeyDown(), event.isShiftKeyDown())) { + event.preventDefault(); + event.stopPropagation(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + public void onKeyDown(KeyDownEvent event) { + if (handleKeyNavigation(event.getNativeEvent().getKeyCode(), event + .isControlKeyDown() + || event.isMetaKeyDown(), event.isShiftKeyDown())) { + event.preventDefault(); + event.stopPropagation(); + } + } + + /** + * Handles the keyboard navigation + * + * @param keycode + * The keycode of the pressed key + * @param ctrl + * Was ctrl pressed + * @param shift + * Was shift pressed + * @return Returns true if the key was handled, else false + */ + protected boolean handleKeyNavigation(int keycode, boolean ctrl, + boolean shift) { + + // Navigate down + if (keycode == getNavigationDownKey()) { + TreeNode node = null; + // If node is open and has children then move in to the children + if (!focusedNode.isLeaf() && focusedNode.getState() + && focusedNode.getChildren().size() > 0) { + node = focusedNode.getChildren().get(0); + } + + // Else move down to the next sibling + else { + node = getNextSibling(focusedNode); + if (node == null) { + // Else jump to the parent and try to select the next + // sibling there + TreeNode current = focusedNode; + while (node == null && current.getParentNode() != null) { + node = getNextSibling(current.getParentNode()); + current = current.getParentNode(); + } + } + } + + if (node != null && selectable) { + setFocusedNode(node); + if (!ctrl && !shift) { + selectNode(node, true); + } else if (shift && isMultiselect) { + deselectAll(); + selectNodeRange(lastSelection.key, node.key); + } else if (shift) { + selectNode(node, true); + } + } + return true; + } + + // Navigate up + if (keycode == getNavigationUpKey()) { + TreeNode prev = getPreviousSibling(focusedNode); + TreeNode node = null; + if (prev != null) { + node = getLastVisibleChildInTree(prev); + } else if (focusedNode.getParentNode() != null) { + node = focusedNode.getParentNode(); + } + if (node != null && selectable) { + setFocusedNode(node); + + if (!ctrl && !shift) { + selectNode(node, true); + } else if (shift && isMultiselect) { + deselectAll(); + selectNodeRange(lastSelection.key, node.key); + } else if (shift) { + selectNode(node, true); + } + } + return true; + } + + // Navigate left (close branch) + if (keycode == getNavigationLeftKey()) { + if (!focusedNode.isLeaf() && focusedNode.getState()) { + focusedNode.setState(false, true); + } else if (focusedNode.getParentNode() != null + && (focusedNode.isLeaf() || !focusedNode.getState())) { + + if (ctrl) { + setFocusedNode(focusedNode.getParentNode()); + } else if (shift) { + doRelationSelection(focusedNode.getParentNode(), + focusedNode); + setFocusedNode(focusedNode.getParentNode()); + } else { + setSelected(focusedNode, false); + setFocusedNode(focusedNode.getParentNode()); + setSelected(focusedNode, true); + } + } + return true; + } + + // Navigate right (open branch) + if (keycode == getNavigationRightKey()) { + if (!focusedNode.isLeaf() && !focusedNode.getState()) { + focusedNode.setState(true, true); + } else if (!focusedNode.isLeaf()) { + if (ctrl) { + setFocusedNode(focusedNode.getChildren().get(0)); + } else if (shift) { + setSelected(focusedNode, true); + setFocusedNode(focusedNode.getChildren().get(0)); + setSelected(focusedNode, true); + } else { + setSelected(focusedNode, false); + setFocusedNode(focusedNode.getChildren().get(0)); + setSelected(focusedNode, true); + } + } + return true; + } + + // Selection + if (keycode == getNavigationSelectKey()) { + if (!focusedNode.isSelected()) { + selectNode(focusedNode, !isMultiselect + || multiSelectMode == MULTISELECT_MODE_SIMPLE); + } else { + deselectNode(focusedNode); + } + return true; + } + + // Home selection + if (keycode == getNavigationStartKey()) { + TreeNode node = (TreeNode) body.getWidget(0); + if (!ctrl && !shift) { + selectNode(node, true); + } else if (ctrl) { + setFocusedNode(node); + } else if (shift) { + deselectAll(); + selectNodeRange(focusedNode.key, node.key); + } + sendSelectionToServer(); + return true; + } + + // End selection + if (keycode == getNavigationEndKey()) { + TreeNode lastNode = (TreeNode) body + .getWidget(body.getWidgetCount() - 1); + TreeNode node = getLastVisibleChildInTree(lastNode); + if (!ctrl && !shift) { + selectNode(node, true); + } else if (ctrl) { + setFocusedNode(node); + } else if (shift) { + deselectAll(); + selectNodeRange(focusedNode.key, node.key); + } + sendSelectionToServer(); + return true; + } + + return false; + } + + /** + * Traverses the tree to the bottom most child + * + * @param root + * The root of the tree + * @return The bottom most child + */ + private TreeNode getLastVisibleChildInTree(TreeNode root) { + if (root.isLeaf() || !root.getState() || root.getChildren().size() == 0) { + return root; + } + List<TreeNode> children = root.getChildren(); + return getLastVisibleChildInTree(children.get(children.size() - 1)); + } + + /** + * Gets the next sibling in the tree + * + * @param node + * The node to get the sibling for + * @return The sibling node or null if the node is the last sibling + */ + private TreeNode getNextSibling(TreeNode node) { + TreeNode parent = node.getParentNode(); + List<TreeNode> children; + if (parent == null) { + children = new LinkedList<TreeNode>(); + for (int w = 0; w < body.getWidgetCount(); w++) { + children.add((TreeNode) body.getWidget(w)); + } + } else { + children = parent.getChildren(); + } + + int idx = children.indexOf(node); + if (idx < children.size() - 1) { + return children.get(idx + 1); + } + + return null; + } + + /** + * Returns the previous sibling in the tree + * + * @param node + * The node to get the sibling for + * @return The sibling node or null if the node is the first sibling + */ + private TreeNode getPreviousSibling(TreeNode node) { + TreeNode parent = node.getParentNode(); + List<TreeNode> children; + if (parent == null) { + children = new LinkedList<TreeNode>(); + for (int w = 0; w < body.getWidgetCount(); w++) { + children.add((TreeNode) body.getWidget(w)); + } + } else { + children = parent.getChildren(); + } + + int idx = children.indexOf(node); + if (idx > 0) { + return children.get(idx - 1); + } + + return null; + } + + /** + * Add this to the element mouse down event by using element.setPropertyJSO + * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again + * when the mouse is depressed in the mouse up event. + * + * @return Returns the JSO preventing text selection + */ + private native JavaScriptObject applyDisableTextSelectionIEHack() + /*-{ + return function(){ return false; }; + }-*/; + + /** + * Get the key that moves the selection head upwards. By default it is the + * up arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that moves the selection head downwards. By default it is the + * down arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that scrolls to the left in the table. By default it is the + * left arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that scroll to the right on the table. By default it is the + * right arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * Get the key that selects an item in the table. By default it is the space + * bar key but by overriding this you can change the key to whatever you + * want. + * + * @return + */ + protected int getNavigationSelectKey() { + return 32; + } + + /** + * Get the key the moves the selection one page up in the table. By default + * this is the Page Up key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationPageUpKey() { + return KeyCodes.KEY_PAGEUP; + } + + /** + * Get the key the moves the selection one page down in the table. By + * default this is the Page Down key but by overriding this you can change + * the key to whatever you want. + * + * @return + */ + protected int getNavigationPageDownKey() { + return KeyCodes.KEY_PAGEDOWN; + } + + /** + * Get the key the moves the selection to the beginning of the table. By + * default this is the Home key but by overriding this you can change the + * key to whatever you want. + * + * @return + */ + protected int getNavigationStartKey() { + return KeyCodes.KEY_HOME; + } + + /** + * Get the key the moves the selection to the end of the table. By default + * this is the End key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationEndKey() { + return KeyCodes.KEY_END; + } + } diff --git a/src/com/vaadin/terminal/gwt/client/ui/VTwinColSelect.java b/src/com/vaadin/terminal/gwt/client/ui/VTwinColSelect.java index a4d53ed837..089c61080f 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VTwinColSelect.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VTwinColSelect.java @@ -8,6 +8,9 @@ import java.util.ArrayList; import java.util.Iterator;
import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.user.client.DOM;
@@ -16,9 +19,8 @@ import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Panel;
import com.vaadin.terminal.gwt.client.UIDL;
-
-public class VTwinColSelect extends VOptionGroupBase implements
- MouseDownHandler {
+ +public class VTwinColSelect extends VOptionGroupBase implements KeyDownHandler, MouseDownHandler {
private static final String CLASSNAME = "v-select-twincol";
@@ -67,9 +69,12 @@ public class VTwinColSelect extends VOptionGroupBase implements buttons.add(remove);
panel.add(buttons);
panel.add(selections);
-
+ + options.addKeyDownHandler(this);
options.addMouseDownHandler(this);
+
selections.addMouseDownHandler(this);
+ selections.addKeyDownHandler(this); }
@Override
@@ -148,47 +153,81 @@ public class VTwinColSelect extends VOptionGroupBase implements return selectedIndexes;
}
+ private void addItem() {
+ final boolean[] sel = getItemsToAdd();
+ for (int i = 0; i < sel.length; i++) {
+ if (sel[i]) {
+ final int optionIndex = i
+ - (sel.length - options.getItemCount());
+ selectedKeys.add(options.getValue(optionIndex));
+
+ // Move selection to another column
+ final String text = options.getItemText(optionIndex);
+ final String value = options.getValue(optionIndex);
+ selections.addItem(text, value);
+ selections.setItemSelected(selections.getItemCount() - 1, true);
+ options.removeItem(optionIndex);
+
+ if (options.getItemCount() > 0) {
+ options.setItemSelected(optionIndex > 0 ? optionIndex - 1
+ : 0, true);
+ }
+ }
+ }
+
+ // If no items are left move the focus to the selections
+ if (options.getItemCount() == 0) {
+ selections.setFocus(true);
+ } else {
+ options.setFocus(true);
+ }
+
+ client.updateVariable(id, "selected", selectedKeys
+ .toArray(new String[selectedKeys.size()]), isImmediate());
+ }
+
+ private void removeItem() {
+ final boolean[] sel = getItemsToRemove();
+ for (int i = 0; i < sel.length; i++) {
+ if (sel[i]) {
+ final int selectionIndex = i
+ - (sel.length - selections.getItemCount());
+ selectedKeys.remove(selections.getValue(selectionIndex));
+
+ // Move selection to another column
+ final String text = selections.getItemText(selectionIndex);
+ final String value = selections.getValue(selectionIndex);
+ options.addItem(text, value);
+ options.setItemSelected(options.getItemCount() - 1, true);
+ selections.removeItem(selectionIndex);
+
+ if (selections.getItemCount() > 0) {
+ selections.setItemSelected(
+ selectionIndex > 0 ? selectionIndex - 1 : 0, true);
+ }
+ }
+ }
+
+ // If no items are left move the focus to the selections
+ if (selections.getItemCount() == 0) {
+ options.setFocus(true);
+ } else {
+ selections.setFocus(true);
+ }
+
+ client.updateVariable(id, "selected", selectedKeys
+ .toArray(new String[selectedKeys.size()]), isImmediate());
+ }
+
@Override
public void onClick(ClickEvent event) {
super.onClick(event);
if (event.getSource() == add) {
- final boolean[] sel = getItemsToAdd();
- for (int i = 0; i < sel.length; i++) {
- if (sel[i]) {
- final int optionIndex = i
- - (sel.length - options.getItemCount());
- selectedKeys.add(options.getValue(optionIndex));
-
- // Move selection to another column
- final String text = options.getItemText(optionIndex);
- final String value = options.getValue(optionIndex);
- selections.addItem(text, value);
- selections.setItemSelected(selections.getItemCount() - 1,
- true);
- options.removeItem(optionIndex);
- }
- }
- client.updateVariable(id, "selected", selectedKeys
- .toArray(new String[selectedKeys.size()]), isImmediate());
+ addItem();
} else if (event.getSource() == remove) {
- final boolean[] sel = getItemsToRemove();
- for (int i = 0; i < sel.length; i++) {
- if (sel[i]) {
- final int selectionIndex = i
- - (sel.length - selections.getItemCount());
- selectedKeys.remove(selections.getValue(selectionIndex));
-
- // Move selection to another column
- final String text = selections.getItemText(selectionIndex);
- final String value = selections.getValue(selectionIndex);
- options.addItem(text, value);
- options.setItemSelected(options.getItemCount() - 1, true);
- selections.removeItem(selectionIndex);
- }
- }
- client.updateVariable(id, "selected", selectedKeys
- .toArray(new String[selectedKeys.size()]), isImmediate());
+ removeItem();
+
} else if (event.getSource() == options) {
// unselect all in other list, to avoid mistakes (i.e wrong button)
final int c = selections.getItemCount();
@@ -246,6 +285,79 @@ public class VTwinColSelect extends VOptionGroupBase implements public void focus() {
options.setFocus(true);
+ } +
+ /**
+ * Get the key that selects an item in the table. By default it is the Enter
+ * key but by overriding this you can change the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationSelectKey() {
+ return KeyCodes.KEY_ENTER;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+ public void onKeyDown(KeyDownEvent event) {
+ int keycode = event.getNativeKeyCode();
+
+ // Catch tab and move between select:s
+ if (keycode == KeyCodes.KEY_TAB && event.getSource() == options) {
+ // Prevent default behavior
+ event.preventDefault();
+
+ // Remove current selections
+ for (int i = 0; i < options.getItemCount(); i++) {
+ options.setItemSelected(i, false);
+ }
+
+ // Focus selections
+ selections.setFocus(true);
+ }
+
+ if (keycode == KeyCodes.KEY_TAB && event.isShiftKeyDown()
+ && event.getSource() == selections) {
+ // Prevent default behavior
+ event.preventDefault();
+
+ // Remove current selections
+ for (int i = 0; i < selections.getItemCount(); i++) {
+ selections.setItemSelected(i, false);
+ }
+
+ // Focus options
+ options.setFocus(true);
+ }
+
+ if (keycode == getNavigationSelectKey()) {
+ // Prevent default behavior
+ event.preventDefault();
+
+ // Decide which select the selection was made in
+ if (event.getSource() == options) {
+ // Prevents the selection to become a single selection when
+ // using Enter key
+ // as the selection key (default)
+ options.setFocus(false);
+
+ addItem();
+
+ } else if (event.getSource() == selections) {
+ // Prevents the selection to become a single selection when
+ // using Enter key
+ // as the selection key (default)
+ selections.setFocus(false);
+
+ removeItem();
+ }
+ }
+
}
/*
@@ -268,5 +380,5 @@ public class VTwinColSelect extends VOptionGroupBase implements }
}
- }
+ } }
diff --git a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java index 98b95b2a7c..d8664e216c 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java @@ -34,6 +34,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; +import java.util.UUID; import javax.portlet.PortletRequest; import javax.portlet.PortletResponse; @@ -319,6 +320,8 @@ public abstract class AbstractCommunicationManager implements private DragAndDropService dragAndDropService; + private static int nextUnusedWindowSuffix = 1; + /** * TODO New constructor - document me! * @@ -670,7 +673,7 @@ public abstract class AbstractCommunicationManager implements String seckey = (String) request.getSession().getAttribute( ApplicationConnection.UIDL_SECURITY_TOKEN_ID); if (seckey == null) { - seckey = "" + (int) (Math.random() * 1000000); + seckey = UUID.randomUUID().toString(); request.getSession().setAttribute( ApplicationConnection.UIDL_SECURITY_TOKEN_ID, seckey); } @@ -1543,9 +1546,12 @@ public abstract class AbstractCommunicationManager implements // If the requested window is already open, resolve conflict if (currentlyOpenWindowsInClient.containsKey(window.getName())) { String newWindowName = window.getName(); - while (currentlyOpenWindowsInClient.containsKey(newWindowName)) { - newWindowName = window.getName() + "_" - + ((int) (Math.random() * 100000000)); + + synchronized (AbstractCommunicationManager.class) { + while (currentlyOpenWindowsInClient.containsKey(newWindowName)) { + newWindowName = window.getName() + "_" + + nextUnusedWindowSuffix++; + } } window = application.getWindow(newWindowName); diff --git a/src/com/vaadin/terminal/gwt/widgetsetutils/ClassPathExplorer.java b/src/com/vaadin/terminal/gwt/widgetsetutils/ClassPathExplorer.java index abb50a05eb..aa65d80d96 100644 --- a/src/com/vaadin/terminal/gwt/widgetsetutils/ClassPathExplorer.java +++ b/src/com/vaadin/terminal/gwt/widgetsetutils/ClassPathExplorer.java @@ -68,7 +68,7 @@ public class ClassPathExplorer { }; private static List<String> rawClasspathEntries = getRawClasspathEntries(); - private static Map<URL, String> classpathLocations = getClasspathLocations(rawClasspathEntries); + private static Map<String, URL> classpathLocations = getClasspathLocations(rawClasspathEntries); private ClassPathExplorer() { } @@ -79,9 +79,9 @@ public class ClassPathExplorer { public static Collection<Class<? extends Paintable>> getPaintablesHavingWidgetAnnotation() { Collection<Class<? extends Paintable>> paintables = new HashSet<Class<? extends Paintable>>(); - Set<URL> keySet = classpathLocations.keySet(); - for (URL url : keySet) { - searchForPaintables(url, classpathLocations.get(url), paintables); + Set<String> keySet = classpathLocations.keySet(); + for (String url : keySet) { + searchForPaintables(classpathLocations.get(url), url, paintables); } return paintables; @@ -103,9 +103,9 @@ public class ClassPathExplorer { */ public static Map<String, URL> getAvailableWidgetSets() { Map<String, URL> widgetsets = new HashMap<String, URL>(); - Set<URL> keySet = classpathLocations.keySet(); - for (URL url : keySet) { - searchForWidgetSets(url, widgetsets); + Set<String> keySet = classpathLocations.keySet(); + for (String location : keySet) { + searchForWidgetSets(location, widgetsets); } StringBuilder sb = new StringBuilder(); sb.append("Widgetsets found from classpath:\n"); @@ -120,9 +120,10 @@ public class ClassPathExplorer { return widgetsets; } - private static void searchForWidgetSets(URL location, + private static void searchForWidgetSets(String locationString, Map<String, URL> widgetsets) { + URL location = classpathLocations.get(locationString); File directory = new File(location.getFile()); if (directory.exists() && !directory.isHidden()) { @@ -134,10 +135,10 @@ public class ClassPathExplorer { // remove the extension String classname = files[i].substring(0, files[i].length() - 8); - classname = classpathLocations.get(location) + "." - + classname; + String packageName = locationString + .substring(locationString.lastIndexOf("/") + 1); + classname = packageName + "." + classname; if (!widgetsets.containsKey(classname)) { - String packageName = classpathLocations.get(location); String packagePath = packageName.replaceAll("\\.", "/"); String basePath = location.getFile().replaceAll( "/" + packagePath + "$", ""); @@ -225,10 +226,10 @@ public class ClassPathExplorer { * Determine every URL location defined by the current classpath, and it's * associated package name. */ - private final static Map<URL, String> getClasspathLocations( + private final static Map<String, URL> getClasspathLocations( List<String> rawClasspathEntries) { // try to keep the order of the classpath - Map<URL, String> locations = new LinkedHashMap<URL, String>(); + Map<String, URL> locations = new LinkedHashMap<String, URL>(); for (String classpathEntry : rawClasspathEntries) { File file = new File(classpathEntry); include(null, file, locations); @@ -285,7 +286,7 @@ public class ClassPathExplorer { * @param locations */ private final static void include(String name, File file, - Map<URL, String> locations) { + Map<String, URL> locations) { if (!file.exists()) { return; } @@ -312,9 +313,10 @@ public class ClassPathExplorer { // add the present directory if (!dirs[i].isHidden() && !dirs[i].getPath().contains(File.separator + ".")) { - locations.put(new URL("file://" - + dirs[i].getCanonicalPath()), name - + dirs[i].getName()); + String key = dirs[i].getCanonicalPath() + "/" + name + + dirs[i].getName(); + locations.put(key, new URL("file://" + + dirs[i].getCanonicalPath())); } } catch (Exception ioe) { return; @@ -323,14 +325,15 @@ public class ClassPathExplorer { } } - private static void includeJar(File file, Map<URL, String> locations) { + private static void includeJar(File file, Map<String, URL> locations) { try { URL url = new URL("file:" + file.getCanonicalPath()); url = new URL("jar:" + url.toExternalForm() + "!/"); JarURLConnection conn = (JarURLConnection) url.openConnection(); JarFile jarFile = conn.getJarFile(); if (jarFile != null) { - locations.put(url, ""); + // the key does not matter here as long as it is unique + locations.put(url.toString(), url); } } catch (Exception e) { // e.printStackTrace(); @@ -340,7 +343,7 @@ public class ClassPathExplorer { } private final static void searchForPaintables(URL location, - String packageName, + String locationString, Collection<Class<? extends Paintable>> paintables) { // Get a File object for the package @@ -355,6 +358,8 @@ public class ClassPathExplorer { // remove the .class extension String classname = files[i].substring(0, files[i].length() - 6); + String packageName = locationString + .substring(locationString.lastIndexOf("/") + 1); classname = packageName + "." + classname; tryToAdd(classname, paintables); } @@ -447,11 +452,11 @@ public class ClassPathExplorer { */ public static URL getDefaultSourceDirectory() { if (logger.isLoggable(Level.FINE)) { - logger.fine("classpathLocations keys:"); - ArrayList<URL> locations = new ArrayList<URL>(classpathLocations - .keySet()); - for (URL location : locations) { - logger.fine(location.toString()); + logger.fine("classpathLocations values:"); + ArrayList<String> locations = new ArrayList<String>( + classpathLocations.keySet()); + for (String location : locations) { + logger.fine(String.valueOf(classpathLocations.get(location))); } } diff --git a/src/com/vaadin/ui/AbstractOrderedLayout.java b/src/com/vaadin/ui/AbstractOrderedLayout.java index 123c5a8dc0..89b72b0f88 100644 --- a/src/com/vaadin/ui/AbstractOrderedLayout.java +++ b/src/com/vaadin/ui/AbstractOrderedLayout.java @@ -360,4 +360,28 @@ public abstract class AbstractOrderedLayout extends AbstractLayout implements removeListener(CLICK_EVENT, LayoutClickEvent.class, listener); } + /** + * Returns the index of the given component. + * + * @param component + * The component to look up. + * @return The index of the component or -1 if the component is not a child. + */ + public int getComponentIndex(Component component) { + return components.indexOf(component); + } + + /** + * Returns the component at the given position. + * + * @param index + * The position of the component. + * @return The component at the given index. + * @throws IndexOutOfBoundsException + * If the index is out of range. + */ + public Component getComponent(int index) throws IndexOutOfBoundsException { + return components.get(index); + } + } diff --git a/src/com/vaadin/ui/AbstractSelect.java b/src/com/vaadin/ui/AbstractSelect.java index 9667de32d6..a8451e7ef6 100644 --- a/src/com/vaadin/ui/AbstractSelect.java +++ b/src/com/vaadin/ui/AbstractSelect.java @@ -127,6 +127,21 @@ public abstract class AbstractSelect extends AbstractField implements } /** + * Multi select modes that controls how multi select behaves. + */ + public enum MultiSelectMode { + /** + * The default behavior of the multi select mode + */ + DEFAULT, + + /** + * The previous more simple behavior of the multselect + */ + SIMPLE + } + + /** * Is the select in multiselect mode? */ private boolean multiSelect = false; diff --git a/src/com/vaadin/ui/Component.java b/src/com/vaadin/ui/Component.java index 251a415e83..cfc648d9b0 100644 --- a/src/com/vaadin/ui/Component.java +++ b/src/com/vaadin/ui/Component.java @@ -441,7 +441,8 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * <pre> * RichTextArea area = new RichTextArea(); * area.setCaption("You can edit stuff here"); - * area.setValue("<h1>Helpful Heading</h1>" + "<p>All this is for you to edit.</p>"); + * area.setValue("<h1>Helpful Heading</h1>" + * + "<p>All this is for you to edit.</p>"); * </pre> * * <p> @@ -573,7 +574,7 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * Gets the application object to which the component is attached. * * <p> - * The method will return {@code null} if the component has not yet been + * The method will return {@code null} if the component is not currently * attached to an application. This is often a problem in constructors of * regular components and in the initializers of custom composite * components. A standard workaround is to move the problematic @@ -716,7 +717,7 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * as an empty collection. */ public void childRequestedRepaint( - Collection<RepaintRequestListener> alreadyNotified); + Collection<RepaintRequestListener> alreadyNotified); /* Component event framework */ @@ -819,11 +820,13 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * * public void componentEvent(Event event) { * // Act according to the source of the event - * if (event.getSource() == ok && event.getClass() == Button.ClickEvent.class) + * if (event.getSource() == ok + * && event.getClass() == Button.ClickEvent.class) * getWindow().showNotification("Click!"); * * // Display source component and event class names - * status.setValue("Event from " + event.getSource().getClass().getName() + ": " + event.getClass().getName()); + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); * } * } * @@ -851,7 +854,8 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * getWindow().showNotification("Click!"); * * // Display source component and event class names - * status.setValue("Event from " + event.getSource().getClass().getName() + ": " + event.getClass().getName()); + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); * } * </pre> * @@ -898,7 +902,8 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * if (event.getSource() == ok) * getWindow().showNotification("Click!"); * - * status.setValue("Event from " + event.getSource().getClass().getName() + ": " + event.getClass().getName()); + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); * } * } * diff --git a/src/com/vaadin/ui/DateField.java b/src/com/vaadin/ui/DateField.java index 45fd45716d..52f6d60a9d 100644 --- a/src/com/vaadin/ui/DateField.java +++ b/src/com/vaadin/ui/DateField.java @@ -12,6 +12,7 @@ import java.util.Locale; import java.util.Map; import com.vaadin.data.Property; +import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.event.FieldEvents; import com.vaadin.event.FieldEvents.BlurEvent; import com.vaadin.event.FieldEvents.BlurListener; @@ -19,6 +20,7 @@ import com.vaadin.event.FieldEvents.FocusEvent; import com.vaadin.event.FieldEvents.FocusListener; import com.vaadin.terminal.PaintException; import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.UserError; import com.vaadin.terminal.gwt.client.ui.VDateField; import com.vaadin.terminal.gwt.client.ui.VPopupCalendar; @@ -119,6 +121,13 @@ public class DateField extends AbstractField implements private boolean lenient = false; + private String dateString = null; + + /** + * Was the last entered string parsable? + */ + private boolean parsingSucceeded = true; + /** * Determines if week numbers are shown in the date selector. */ @@ -130,6 +139,7 @@ public class DateField extends AbstractField implements * Constructs an empty <code>DateField</code> with no caption. */ public DateField() { + setInvalidAllowed(false); } /** @@ -140,6 +150,7 @@ public class DateField extends AbstractField implements */ public DateField(String caption) { setCaption(caption); + setInvalidAllowed(false); } /** @@ -164,6 +175,7 @@ public class DateField extends AbstractField implements * the Property to be edited with this editor. */ public DateField(Property dataSource) throws IllegalArgumentException { + setInvalidAllowed(false); if (!Date.class.isAssignableFrom(dataSource.getType())) { throw new IllegalArgumentException("Can't use " + dataSource.getType().getName() @@ -186,6 +198,7 @@ public class DateField extends AbstractField implements * the Date value. */ public DateField(String caption, Date value) { + setInvalidAllowed(false); setValue(value); setCaption(caption); } @@ -216,6 +229,7 @@ public class DateField extends AbstractField implements target.addAttribute("type", type); target.addAttribute(VDateField.WEEK_NUMBERS, isShowISOWeekNumbers()); + target.addAttribute("parsable", parsingSucceeded); // Gets the calendar final Calendar calendar = getCalendar(); @@ -264,6 +278,7 @@ public class DateField extends AbstractField implements @Override public void changeVariables(Object source, Map variables) { super.changeVariables(source, variables); + setComponentError(null); if (!isReadOnly() && (variables.containsKey("year") @@ -281,7 +296,7 @@ public class DateField extends AbstractField implements // this enables analyzing invalid input on the server Object o = variables.get("dateString"); - String dateString = null; + dateString = null; if (o != null) { dateString = o.toString(); } @@ -345,7 +360,10 @@ public class DateField extends AbstractField implements if (newDate == null && dateString != null && !"".equals(dateString)) { try { - setValue(handleUnparsableDateString(dateString)); + Date parsedDate = handleUnparsableDateString(dateString); + parsingSucceeded = true; + setValue(parsedDate, true); + /* * Ensure the value is sent to the client if the value is * set to the same as the previous (#4304). Does not repaint @@ -354,13 +372,25 @@ public class DateField extends AbstractField implements */ requestRepaint(); } catch (ConversionException e) { - // FIXME: Should not throw the exception but set an error - // message for the field. And should retain the entered - // value. - throw e; + /* + * Sets the component error to the Conversion Exceptions + * message. This can be overriden in + * handleUnparsableDateString. + */ + setComponentError(new UserError(e.getLocalizedMessage())); + + /* + * The value of the DateField should be null if an invalid + * value has been given. Not using setValue() since we do + * not want to cause the client side value to change. + */ + parsingSucceeded = false; + setInternalValue(null); + fireValueChange(true); } } else if (newDate != oldDate && (newDate == null || !newDate.equals(oldDate))) { + parsingSucceeded = true; setValue(newDate, true); // Don't require a repaint, client // updates itself } @@ -392,7 +422,7 @@ public class DateField extends AbstractField implements */ protected Date handleUnparsableDateString(String dateString) throws Property.ConversionException { - throw new Property.ConversionException(); + throw new Property.ConversionException("Date format not recognized"); } /* Property features */ @@ -430,13 +460,25 @@ public class DateField extends AbstractField implements setValue(newValue, false); } + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object, boolean) + */ @Override public void setValue(Object newValue, boolean repaintIsNotNeeded) throws Property.ReadOnlyException, Property.ConversionException { // Allows setting dates directly if (newValue == null || newValue instanceof Date) { - super.setValue(newValue, repaintIsNotNeeded); + try { + super.setValue(newValue, repaintIsNotNeeded); + parsingSucceeded = true; + } catch (InvalidValueException ive) { + // Thrown if validator fails + parsingSucceeded = false; + throw ive; + } } else { // Try to parse as string @@ -444,8 +486,11 @@ public class DateField extends AbstractField implements final SimpleDateFormat parser = new SimpleDateFormat(); final Date val = parser.parse(newValue.toString()); super.setValue(val, repaintIsNotNeeded); + parsingSucceeded = true; } catch (final ParseException e) { - throw new Property.ConversionException(e.getMessage()); + parsingSucceeded = false; + throw new Property.ConversionException( + "Date format not recognized"); } } } @@ -611,4 +656,31 @@ public class DateField extends AbstractField implements requestRepaint(); } + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractField#isEmpty() + */ + @Override + protected boolean isEmpty() { + /* + * Logically isEmpty() should return false also in the case that the + * entered value is invalid. + */ + return dateString == null || dateString.equals(""); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractField#isValid() + */ + @Override + public boolean isValid() { + /* + * For the DateField to be valid it has to be parsable also + */ + boolean parsable = isEmpty() || (!isEmpty() && getValue() != null); + return parsable && super.isValid(); + } } diff --git a/src/com/vaadin/ui/Table.java b/src/com/vaadin/ui/Table.java index ad9a0c2f3f..8c9a0afc3d 100644 --- a/src/com/vaadin/ui/Table.java +++ b/src/com/vaadin/ui/Table.java @@ -5,6 +5,7 @@ package com.vaadin.ui; import java.io.Serializable; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -93,15 +94,15 @@ public class Table extends AbstractSelect implements Action.Container, MULTIROW } - private static final int CELL_KEY = 0; + protected static final int CELL_KEY = 0; - private static final int CELL_HEADER = 1; + protected static final int CELL_HEADER = 1; - private static final int CELL_ICON = 2; + protected static final int CELL_ICON = 2; - private static final int CELL_ITEMID = 3; + protected static final int CELL_ITEMID = 3; - private static final int CELL_FIRSTCOL = 4; + protected static final int CELL_FIRSTCOL = 4; /** * Left column alignment. <b>This is the default behaviour. </b> @@ -225,6 +226,11 @@ public class Table extends AbstractSelect implements Action.Container, private final HashMap<Object, String> columnHeaders = new HashMap<Object, String>(); /** + * Holds footers for visible columns (by propertyId). + */ + private final HashMap<Object, String> columnFooters = new HashMap<Object, String>(); + + /** * Holds icons for visible columns (by propertyId). */ private final HashMap<Object, Resource> columnIcons = new HashMap<Object, Resource>(); @@ -271,6 +277,11 @@ public class Table extends AbstractSelect implements Action.Container, private int columnHeaderMode = COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID; /** + * Should the Table footer be visible? + */ + private boolean columnFootersVisible = false; + + /** * True iff the row captions are hidden. */ private boolean rowCaptionsAreHidden = true; @@ -368,6 +379,12 @@ public class Table extends AbstractSelect implements Action.Container, private DropHandler dropHandler; + private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + + private HeaderClickHandler headerClickHandler; + + private FooterClickHandler footerClickHandler; + /* Table constructors */ /** @@ -872,7 +889,7 @@ public class Table extends AbstractSelect implements Action.Container, final int index = getCurrentPageFirstItemIndex(); Object id = null; if (index >= 0 && index < size()) { - id = ((Container.Indexed) items).getIdByIndex(index); + id = getIdByIndex(index); } if (id != null && !id.equals(currentPageFirstItemId)) { currentPageFirstItemId = id; @@ -881,12 +898,16 @@ public class Table extends AbstractSelect implements Action.Container, // If there is no item id at all, use the first one if (currentPageFirstItemId == null) { - currentPageFirstItemId = ((Container.Ordered) items).firstItemId(); + currentPageFirstItemId = firstItemId(); } return currentPageFirstItemId; } + protected Object getIdByIndex(int index) { + return ((Container.Indexed) items).getIdByIndex(index); + } + /** * Setter for property currentPageFirstItemId. * @@ -898,15 +919,14 @@ public class Table extends AbstractSelect implements Action.Container, // Gets the corresponding index int index = -1; if (items instanceof Container.Indexed) { - index = ((Container.Indexed) items) - .indexOfId(currentPageFirstItemId); + index = indexOfId(currentPageFirstItemId); } else { // If the table item container does not have index, we have to // calculates the index by hand - Object id = ((Container.Ordered) items).firstItemId(); + Object id = firstItemId(); while (id != null && !id.equals(currentPageFirstItemId)) { index++; - id = ((Container.Ordered) items).nextItemId(id); + id = nextItemId(id); } if (id == null) { index = -1; @@ -940,6 +960,10 @@ public class Table extends AbstractSelect implements Action.Container, } + protected int indexOfId(Object itemId) { + return ((Container.Indexed) items).indexOfId(itemId); + } + /** * Gets the icon Resource for the specified column. * @@ -1224,8 +1248,7 @@ public class Table extends AbstractSelect implements Action.Container, // Refresh first item id if (items instanceof Container.Indexed) { try { - currentPageFirstItemId = ((Container.Indexed) items) - .getIdByIndex(newIndex); + currentPageFirstItemId = getIdByIndex(newIndex); } catch (final IndexOutOfBoundsException e) { currentPageFirstItemId = null; } @@ -1236,48 +1259,42 @@ public class Table extends AbstractSelect implements Action.Container, // container forwards / backwards // next available item forward or backward - currentPageFirstItemId = ((Container.Ordered) items).firstItemId(); + currentPageFirstItemId = firstItemId(); // Go forwards in the middle of the list (respect borders) while (currentPageFirstItemIndex < newIndex - && !((Container.Ordered) items) - .isLastId(currentPageFirstItemId)) { + && !isLastId(currentPageFirstItemId)) { currentPageFirstItemIndex++; - currentPageFirstItemId = ((Container.Ordered) items) - .nextItemId(currentPageFirstItemId); + currentPageFirstItemId = nextItemId(currentPageFirstItemId); } // If we did hit the border - if (((Container.Ordered) items).isLastId(currentPageFirstItemId)) { + if (isLastId(currentPageFirstItemId)) { currentPageFirstItemIndex = size - 1; } // Go backwards in the middle of the list (respect borders) while (currentPageFirstItemIndex > newIndex - && !((Container.Ordered) items) - .isFirstId(currentPageFirstItemId)) { + && !isFirstId(currentPageFirstItemId)) { currentPageFirstItemIndex--; - currentPageFirstItemId = ((Container.Ordered) items) - .prevItemId(currentPageFirstItemId); + currentPageFirstItemId = prevItemId(currentPageFirstItemId); } // If we did hit the border - if (((Container.Ordered) items).isFirstId(currentPageFirstItemId)) { + if (isFirstId(currentPageFirstItemId)) { currentPageFirstItemIndex = 0; } // Go forwards once more while (currentPageFirstItemIndex < newIndex - && !((Container.Ordered) items) - .isLastId(currentPageFirstItemId)) { + && !isLastId(currentPageFirstItemId)) { currentPageFirstItemIndex++; - currentPageFirstItemId = ((Container.Ordered) items) - .nextItemId(currentPageFirstItemId); + currentPageFirstItemId = nextItemId(currentPageFirstItemId); } // If for some reason we do hit border again, override // the user index request - if (((Container.Ordered) items).isLastId(currentPageFirstItemId)) { + if (isLastId(currentPageFirstItemId)) { newIndex = currentPageFirstItemIndex = size - 1; } } @@ -1381,7 +1398,7 @@ public class Table extends AbstractSelect implements Action.Container, /** * Refreshes rendered rows */ - private void refreshRenderedCells() { + protected void refreshRenderedCells() { if (getParent() == null) { return; } @@ -1442,11 +1459,11 @@ public class Table extends AbstractSelect implements Action.Container, // Gets the first item id if (items instanceof Container.Indexed) { - id = ((Container.Indexed) items).getIdByIndex(firstIndex); + id = getIdByIndex(firstIndex); } else { - id = ((Container.Ordered) items).firstItemId(); + id = firstItemId(); for (int i = 0; i < firstIndex; i++) { - id = ((Container.Ordered) items).nextItemId(id); + id = nextItemId(id); } } @@ -1554,15 +1571,14 @@ public class Table extends AbstractSelect implements Action.Container, // Gets the next item id if (items instanceof Container.Indexed) { - Container.Indexed indexed = (Container.Indexed) items; int index = firstIndex + i + 1; - if (index < indexed.size()) { - id = indexed.getIdByIndex(index); + if (index < totalRows) { + id = getIdByIndex(index); } else { id = null; } } else { - id = ((Container.Ordered) items).nextItemId(id); + id = nextItemId(id); } filledRows++; @@ -1830,6 +1846,79 @@ public class Table extends AbstractSelect implements Action.Container, resetPageBuffer(); enableContentRefreshing(true); + } + + /** + * Gets items ids from a range of key values + * + * @param startRowKey + * The start key + * @param endRowKey + * The end key + * @return + */ + private Set<Object> getItemIdsInRange(int startRowKey, int endRowKey) { + HashSet<Object> ids = new HashSet<Object>(); + + Object startItemId = itemIdMapper.get(String.valueOf(startRowKey)); + ids.add(startItemId); + + Object endItemId = itemIdMapper.get(String.valueOf(endRowKey)); + ids.add(endItemId); + + Object currentItemId = startItemId; + + Container.Ordered ordered = (Container.Ordered) items; + while (currentItemId != endItemId) { + currentItemId = ordered.nextItemId(currentItemId); + if (currentItemId != null) { + ids.add(currentItemId); + } + } + + return ids; + } + + /** + * Handles selection if selection is a multiselection + * + * @param variables + * The variables + */ + private void handleSelectedItems(Map<String, Object> variables) { + final String[] ka = (String[]) variables.get("selected"); + final String[] ranges = (String[]) variables.get("selectedRanges"); + + // Converts the key-array to id-set + final LinkedList s = new LinkedList(); + for (int i = 0; i < ka.length; i++) { + final Object id = itemIdMapper.get(ka[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + requestRepaint(); + } else if (id != null && containsId(id)) { + s.add(id); + } + } + + if (!isNullSelectionAllowed() && s.size() < 1) { + // empty selection not allowed, keep old value + requestRepaint(); + return; + } + + // Add range items + if (ranges != null) { + for (String range : ranges) { + String[] limits = range.split("-"); + int start = Integer.valueOf(limits[0]); + int end = Integer.valueOf(limits[1]); + s.addAll(getItemIdsInRange(start, end)); + } + } + + setValue(s, true); } @@ -1848,6 +1937,8 @@ public class Table extends AbstractSelect implements Action.Container, handleClickEvent(variables); + handleColumnResizeEvent(variables); + disableContentRefreshing(); if (!isSelectable() && variables.containsKey("selected")) { @@ -1857,6 +1948,18 @@ public class Table extends AbstractSelect implements Action.Container, variables.remove("selected"); } + /* + * The AbstractSelect cannot handle the multiselection properly, instead + * we handle it ourself + */ + else if (isSelectable() && isMultiSelect() + && variables.containsKey("selected") + && multiSelectMode == MultiSelectMode.DEFAULT) { + handleSelectedItems(variables); + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + super.changeVariables(source, variables); // Client might update the pagelength if Table height is fixed @@ -2000,6 +2103,8 @@ public class Table extends AbstractSelect implements Action.Container, * @param variables */ private void handleClickEvent(Map<String, Object> variables) { + + // Item click event if (variables.containsKey("clickEvent")) { String key = (String) variables.get("clickedKey"); Object itemId = itemIdMapper.get(key); @@ -2017,6 +2122,70 @@ public class Table extends AbstractSelect implements Action.Container, evt)); } } + + // Header click event + else if (variables.containsKey("headerClickEvent")) { + + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("headerClickEvent")); + + Object cid = variables.get("headerClickCID"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + } + fireEvent(new HeaderClickEvent(this, propertyId, details)); + } + + // Footer click event + else if (variables.containsKey("footerClickEvent")) { + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("footerClickEvent")); + + Object cid = variables.get("footerClickCID"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + } + fireEvent(new FooterClickEvent(this, propertyId, details)); + } + } + + /** + * Handles the column resize event sent by the client. + * + * @param variables + */ + private void handleColumnResizeEvent(Map<String, Object> variables) { + if (variables.containsKey("columnResizeEventColumn")) { + Object cid = variables.get("columnResizeEventColumn"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + } + + Object prev = variables.get("columnResizeEventPrev"); + int previousWidth = -1; + if (prev != null) { + previousWidth = Integer.valueOf(prev.toString()); + } + + Object curr = variables.get("columnResizeEventCurr"); + int currentWidth = -1; + if (curr != null) { + currentWidth = Integer.valueOf(curr.toString()); + } + + /* + * Update the sizes on the server side. If a column previously had a + * expand ratio and the user resized the column then the expand + * ratio will be turned into a static pixel size. + */ + setColumnWidth(propertyId, currentWidth); + + fireEvent(new ColumnResizeEvent(this, propertyId, previousWidth, + currentWidth)); + } } /** @@ -2065,6 +2234,10 @@ public class Table extends AbstractSelect implements Action.Container, target.addAttribute("dragmode", dragMode.ordinal()); } + if (multiSelectMode != MultiSelectMode.DEFAULT) { + target.addAttribute("multiselectmode", multiSelectMode.ordinal()); + } + // Initialize temps final Object[] colids = getVisibleColumns(); final int cols = colids.length; @@ -2073,7 +2246,6 @@ public class Table extends AbstractSelect implements Action.Container, final int pagelen = getPageLength(); final int colHeadMode = getColumnHeaderMode(); final boolean colheads = colHeadMode != COLUMN_HEADER_MODE_HIDDEN; - final boolean rowheads = getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN; final Object[][] cells = getVisibleCells(); final boolean iseditable = isEditable(); int rows; @@ -2099,7 +2271,6 @@ public class Table extends AbstractSelect implements Action.Container, // selection support LinkedList<String> selectedKeys = new LinkedList<String>(); if (isMultiSelect()) { - // only paint selections that are currently visible in the client HashSet sel = new HashSet((Set) getValue()); Collection vids = getVisibleItemIds(); for (Iterator it = vids.iterator(); it.hasNext();) { @@ -2133,6 +2304,10 @@ public class Table extends AbstractSelect implements Action.Container, target.addAttribute("cols", cols); target.addAttribute("rows", rows); + if (!isNullSelectionAllowed()) { + target.addAttribute("nsa", false); + } + target.addAttribute("firstrow", (reqFirstRowToPaint >= 0 ? reqFirstRowToPaint : firstToBeRenderedInClient)); @@ -2143,9 +2318,12 @@ public class Table extends AbstractSelect implements Action.Container, if (colheads) { target.addAttribute("colheaders", true); } - if (rowheads) { + if (getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN) { target.addAttribute("rowheaders", true); } + if (columnFootersVisible) { + target.addAttribute("colfooters", true); + } // Visible column order final Collection sortables = getSortableContainerPropertyIds(); @@ -2195,8 +2373,8 @@ public class Table extends AbstractSelect implements Action.Container, start = 0; } - for (int i = start; i < end; i++) { - final Object itemId = cells[CELL_ITEMID][i]; + for (int indexInRowbuffer = start; indexInRowbuffer < end; indexInRowbuffer++) { + final Object itemId = cells[CELL_ITEMID][indexInRowbuffer]; if (!isNullSelectionAllowed() && getNullSelectionItemId() != null && itemId == getNullSelectionItemId()) { @@ -2204,93 +2382,8 @@ public class Table extends AbstractSelect implements Action.Container, continue; } - target.startTag("tr"); - - // tr attributes - if (rowheads) { - if (cells[CELL_ICON][i] != null) { - target.addAttribute("icon", (Resource) cells[CELL_ICON][i]); - } - if (cells[CELL_HEADER][i] != null) { - target.addAttribute("caption", - (String) cells[CELL_HEADER][i]); - } - } - target.addAttribute("key", Integer.parseInt(cells[CELL_KEY][i] - .toString())); - - if (isSelected(itemId)) { - target.addAttribute("selected", true); - } - - // Actions - if (actionHandlers != null) { - final ArrayList<String> keys = new ArrayList<String>(); - for (final Iterator<Handler> ahi = actionHandlers.iterator(); ahi - .hasNext();) { - final Action[] aa = (ahi.next()).getActions(itemId, this); - if (aa != null) { - for (int ai = 0; ai < aa.length; ai++) { - final String key = actionMapper.key(aa[ai]); - actionSet.add(aa[ai]); - keys.add(key); - } - } - } - target.addAttribute("al", keys.toArray()); - } - - /* - * For each row, if a cellStyleGenerator is specified, get the - * specific style for the cell, using null as propertyId. If there - * is any, add it to the target. - */ - if (cellStyleGenerator != null) { - String rowStyle = cellStyleGenerator.getStyle(itemId, null); - if (rowStyle != null && !rowStyle.equals("")) { - target.addAttribute("rowstyle", rowStyle); - } - } - - // cells - int currentColumn = 0; - for (final Iterator<Object> it = visibleColumns.iterator(); it - .hasNext(); currentColumn++) { - final Object columnId = it.next(); - if (columnId == null || isColumnCollapsed(columnId)) { - continue; - } - /* - * For each cell, if a cellStyleGenerator is specified, get the - * specific style for the cell. If there is any, add it to the - * target. - */ - if (cellStyleGenerator != null) { - String cellStyle = cellStyleGenerator.getStyle(itemId, - columnId); - if (cellStyle != null && !cellStyle.equals("")) { - target.addAttribute("style-" - + columnIdMap.key(columnId), cellStyle); - } - } - if ((iscomponent[currentColumn] || iseditable) - && Component.class.isInstance(cells[CELL_FIRSTCOL - + currentColumn][i])) { - final Component c = (Component) cells[CELL_FIRSTCOL - + currentColumn][i]; - if (c == null) { - target.addText(""); - } else { - c.paint(target); - } - } else { - target - .addText((String) cells[CELL_FIRSTCOL - + currentColumn][i]); - } - } - - target.endTag("tr"); + paintRow(target, cells, iseditable, actionSet, iscomponent, + indexInRowbuffer, itemId); } target.endTag("rows"); @@ -2380,6 +2473,8 @@ public class Table extends AbstractSelect implements Action.Container, target.addAttribute("cid", columnIdMap.key(columnId)); final String head = getColumnHeader(columnId); target.addAttribute("caption", (head != null ? head : "")); + final String foot = getColumnFooter(columnId); + target.addAttribute("fcaption", (foot != null ? foot : "")); if (isColumnCollapsed(columnId)) { target.addAttribute("collapsed", true); } @@ -2413,6 +2508,131 @@ public class Table extends AbstractSelect implements Action.Container, } } + private void paintRow(PaintTarget target, final Object[][] cells, + final boolean iseditable, final Set<Action> actionSet, + final boolean[] iscomponent, int indexInRowbuffer, + final Object itemId) throws PaintException { + target.startTag("tr"); + + paintRowAttributes(target, cells, actionSet, indexInRowbuffer, itemId); + + // cells + int currentColumn = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); currentColumn++) { + final Object columnId = it.next(); + if (columnId == null || isColumnCollapsed(columnId)) { + continue; + } + /* + * For each cell, if a cellStyleGenerator is specified, get the + * specific style for the cell. If there is any, add it to the + * target. + */ + if (cellStyleGenerator != null) { + String cellStyle = cellStyleGenerator + .getStyle(itemId, columnId); + if (cellStyle != null && !cellStyle.equals("")) { + target.addAttribute("style-" + columnIdMap.key(columnId), + cellStyle); + } + } + if ((iscomponent[currentColumn] || iseditable) + && Component.class.isInstance(cells[CELL_FIRSTCOL + + currentColumn][indexInRowbuffer])) { + final Component c = (Component) cells[CELL_FIRSTCOL + + currentColumn][indexInRowbuffer]; + if (c == null) { + target.addText(""); + } else { + c.paint(target); + } + } else { + target + .addText((String) cells[CELL_FIRSTCOL + currentColumn][indexInRowbuffer]); + } + } + + target.endTag("tr"); + } + + private void paintRowAttributes(PaintTarget target, final Object[][] cells, + final Set<Action> actionSet, int indexInRowbuffer, + final Object itemId) throws PaintException { + // tr attributes + + paintRowIcon(target, cells, indexInRowbuffer); + paintRowHeader(target, cells, indexInRowbuffer); + target.addAttribute("key", Integer + .parseInt(cells[CELL_KEY][indexInRowbuffer].toString())); + + if (isSelected(itemId)) { + target.addAttribute("selected", true); + } + + // Actions + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + for (final Iterator<Handler> ahi = actionHandlers.iterator(); ahi + .hasNext();) { + final Action[] aa = (ahi.next()).getActions(itemId, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final String key = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + keys.add(key); + } + } + } + target.addAttribute("al", keys.toArray()); + } + + /* + * For each row, if a cellStyleGenerator is specified, get the specific + * style for the cell, using null as propertyId. If there is any, add it + * to the target. + */ + if (cellStyleGenerator != null) { + String rowStyle = cellStyleGenerator.getStyle(itemId, null); + if (rowStyle != null && !rowStyle.equals("")) { + target.addAttribute("rowstyle", rowStyle); + } + } + paintRowAttributes(target, itemId); + } + + protected void paintRowHeader(PaintTarget target, Object[][] cells, + int indexInRowbuffer) throws PaintException { + if (getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN) { + if (cells[CELL_HEADER][indexInRowbuffer] != null) { + target.addAttribute("caption", + (String) cells[CELL_HEADER][indexInRowbuffer]); + } + } + + } + + protected void paintRowIcon(PaintTarget target, final Object[][] cells, + int indexInRowbuffer) throws PaintException { + if (getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN + && cells[CELL_ICON][indexInRowbuffer] != null) { + target.addAttribute("icon", + (Resource) cells[CELL_ICON][indexInRowbuffer]); + } + } + + /** + * A method where extended Table implementations may add their custom + * attributes for rows. + * + * @param target + * @param itemId + */ + protected void paintRowAttributes(PaintTarget target, Object itemId) + throws PaintException { + + } + /** * Gets the cached visible table contents. * @@ -2551,7 +2771,7 @@ public class Table extends AbstractSelect implements Action.Container, requestRepaint(); } - private void resetPageBuffer() { + protected void resetPageBuffer() { firstToBeRenderedInClient = -1; lastToBeRenderedInClient = -1; reqFirstRowToPaint = -1; @@ -2614,8 +2834,7 @@ public class Table extends AbstractSelect implements Action.Container, */ @Override public boolean removeItem(Object itemId) { - final Object nextItemId = ((Container.Ordered) items) - .nextItemId(itemId); + final Object nextItemId = nextItemId(itemId); final boolean ret = super.removeItem(itemId); if (ret && (itemId != null) && (itemId.equals(currentPageFirstItemId))) { currentPageFirstItemId = nextItemId; @@ -2641,6 +2860,7 @@ public class Table extends AbstractSelect implements Action.Container, columnAlignments.remove(propertyId); columnIcons.remove(propertyId); columnHeaders.remove(propertyId); + columnFooters.remove(propertyId); return super.removeContainerProperty(propertyId); } @@ -2860,18 +3080,6 @@ public class Table extends AbstractSelect implements Action.Container, } /** - * Focusing to this component is not supported. - * - * @throws UnsupportedOperationException - * if invoked. - * @see com.vaadin.ui.AbstractField#focus() - */ - @Override - public void focus() throws UnsupportedOperationException { - throw new UnsupportedOperationException(); - } - - /** * Gets the ID of the Item following the Item that corresponds to itemId. * * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object) @@ -3433,6 +3641,27 @@ public class Table extends AbstractSelect implements Action.Container, } /** + * Sets the behavior of how the multi-select mode should behave when the + * table is both selectable and in multi-select mode. + * + * @param mode + * The select mode of the table + */ + public void setMultiSelectMode(MultiSelectMode mode) { + multiSelectMode = mode; + requestRepaint(); + } + + /** + * Returns the select mode in which multi-select is used. + * + * @return The multi select mode + */ + public MultiSelectMode getMultiSelectMode() { + return multiSelectMode; + } + + /** * Lazy loading accept criterion for Table. Accepted target rows are loaded * from server once per drag and drop operation. Developer must override one * method that decides on which rows the currently dragged data can be @@ -3525,4 +3754,418 @@ public class Table extends AbstractSelect implements Action.Container, } + /** + * Click event fired when clicking on the Table headers. The event includes + * a reference the the Table the event originated from, the property id of + * the column which header was pressed and details about the mouse event + * itself. + */ + public static class HeaderClickEvent extends Component.Event { + public static final Method HEADER_CLICK_METHOD; + + static { + try { + // Set the header click method + HEADER_CLICK_METHOD = HeaderClickHandler.class + .getDeclaredMethod("handleHeaderClick", + new Class[] { HeaderClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + // The property id of the column which header was pressed + private Object columnPropertyId; + + // The mouse details + private MouseEventDetails details; + + public HeaderClickEvent(Component source, Object propertyId, + MouseEventDetails details) { + super(source); + this.details = details; + columnPropertyId = propertyId; + } + + /** + * Gets the property id of the column which header was pressed + * + * @return The column propety id + */ + public Object getPropertyId() { + return columnPropertyId; + } + + /** + * Returns the details of the mouse event like the mouse coordinates, + * button pressed etc. + * + * @return The mouse details + */ + public MouseEventDetails getEventDetails() { + return details; + } + } + + /** + * Click event fired when clicking on the Table footers. The event includes + * a reference the the Table the event originated from, the property id of + * the column which header was pressed and details about the mouse event + * itself. + */ + public static class FooterClickEvent extends Component.Event { + public static final Method FOOTER_CLICK_METHOD; + + static { + try { + // Set the header click method + FOOTER_CLICK_METHOD = FooterClickHandler.class + .getDeclaredMethod("handleFooterClick", + new Class[] { FooterClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + // The property id of the column which header was pressed + private Object columnPropertyId; + + // The mouse details + private MouseEventDetails details; + + /** + * Constructor + * + * @param source + * The source of the component + * @param propertyId + * The propertyId of the column + * @param details + * The mouse details of the click + */ + public FooterClickEvent(Component source, Object propertyId, + MouseEventDetails details) { + super(source); + columnPropertyId = propertyId; + this.details = details; + } + + /** + * Gets the property id of the column which header was pressed + * + * @return The column propety id + */ + public Object getPropertyId() { + return columnPropertyId; + } + + /** + * Returns the details of the mouse event like the mouse coordinates, + * button pressed etc. + * + * @return The mouse details + */ + public MouseEventDetails getEventDetails() { + return details; + } + } + + /** + * Interface for the handler listening to column header mouse click events. + * The handleHeaderClick method is called when the user presses a header + * column cell. + */ + public interface HeaderClickHandler { + + /** + * Called when a user clicks a header column cell + * + * @param event + * The event which contains information about the column and + * the mouse click event + */ + public void handleHeaderClick(HeaderClickEvent event); + } + + /** + * Interface for the handler listening to column footer mouse click events. + * The handleHeaderClick method is called when the user presses a footer + * column cell. + */ + public interface FooterClickHandler { + + /** + * Called when a user clicks a footer column cell + * + * @param event + * The event which contains information about the column and + * the mouse click event + */ + public void handleFooterClick(FooterClickEvent event); + } + + /** + * Sets the header click handler which handles the click events when the + * user clicks on a column header cell in the Table. + * <p> + * The handler will receive events which contains information about which + * column was clicked and some details about the mouse event. + * </p> + * + * @param handler + * The handler which should handle the header click events. + */ + public void setHeaderClickHandler(HeaderClickHandler handler) { + if (headerClickHandler != handler) { + if (handler == null && headerClickHandler != null) { + // Remove header click handler + removeListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, headerClickHandler); + + headerClickHandler = handler; + } else if (headerClickHandler != null) { + // Replace header click handler + removeListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, headerClickHandler); + + headerClickHandler = handler; + + addListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, headerClickHandler, + HeaderClickEvent.HEADER_CLICK_METHOD); + } else if (handler != null) { + // Set a new header click handler + headerClickHandler = handler; + addListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, headerClickHandler, + HeaderClickEvent.HEADER_CLICK_METHOD); + } + } + } + + /** + * Sets the footer click handler which handles the click events when the + * user clicks on a column footer cell in the Table. + * <p> + * The handler will recieve events which contains information about which + * column was clicked and some details about the mouse event. + * </p> + * + * @param handler + * The handler which should handle the footer click events + */ + public void setFooterClickHandler(FooterClickHandler handler) { + if (footerClickHandler != handler) { + if (handler == null && footerClickHandler != null) { + // Remove header click handler + removeListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, footerClickHandler); + footerClickHandler = handler; + } else if (footerClickHandler != null) { + // Replace footer click handler + removeListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, footerClickHandler); + footerClickHandler = handler; + addListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, footerClickHandler, + FooterClickEvent.FOOTER_CLICK_METHOD); + } else if (handler != null) { + // Set a new footer click handler + footerClickHandler = handler; + addListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, footerClickHandler, + FooterClickEvent.FOOTER_CLICK_METHOD); + } + } + } + + /** + * Returns the header click handler which receives click events from the + * columns header cells when they are clicked on. + * + * @return + */ + public HeaderClickHandler getHeaderClickHandler() { + return headerClickHandler; + } + + /** + * Returns the footer click handler which recieves click events from the + * columns footer cells when they are clicked on. + * + * @return + */ + public FooterClickHandler getFooterClickHandler() { + return footerClickHandler; + } + + /** + * Gets the footer caption beneath the rows + * + * @param propertyId + * The propertyId of the column * + * @return The caption of the footer or NULL if not set + */ + public String getColumnFooter(Object propertyId) { + return columnFooters.get(propertyId); + } + + /** + * Sets the column footer caption. The column footer caption is the text + * displayed beneath the column if footers have been set visible. + * + * @param propertyId + * The properyId of the column + * + * @param footer + * The caption of the footer + */ + public void setColumnFooter(Object propertyId, String footer) { + if (footer == null) { + columnFooters.remove(propertyId); + return; + } + columnFooters.put(propertyId, footer); + + requestRepaint(); + } + + /** + * Sets the footer visible in the bottom of the table. + * <p> + * The footer can be used to add column related data like sums to the bottom + * of the Table using setColumnFooter(Object propertyId, String footer). + * </p> + * + * @param visible + * Should the footer be visible + */ + public void setFooterVisible(boolean visible) { + columnFootersVisible = visible; + + // Assures the visual refresh + refreshRenderedCells(); + } + + /** + * Is the footer currently visible? + * + * @return Returns true if visible else false + */ + public boolean isFooterVisible() { + return columnFootersVisible; + } + + /** + * This event is fired when a column is resized. The event contains the + * columns property id which was fired, the previous width of the column and + * the width of the column after the resize. + */ + public static class ColumnResizeEvent extends Component.Event{ + public static final Method COLUMN_RESIZE_METHOD; + + static { + try { + COLUMN_RESIZE_METHOD = ColumnResizeListener.class + .getDeclaredMethod("columnResize", + new Class[] { ColumnResizeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + private final int previousWidth; + private final int currentWidth; + private final Object columnPropertyId; + + /** + * Constructor + * + * @param source + * The source of the event + * @param propertyId + * The columns property id + * @param previous + * The width in pixels of the column before the resize event + * @param current + * The width in pixels of the column after the resize event + */ + public ColumnResizeEvent(Component source, Object propertyId, + int previous, int current) { + super(source); + previousWidth = previous; + currentWidth = current; + columnPropertyId = propertyId; + } + + /** + * Get the column property id of the column that was resized. + * + * @return The column property id + */ + public Object getPropertyId() { + return columnPropertyId; + } + + /** + * Get the width in pixels of the column before the resize event + * + * @return Width in pixels + */ + public int getPreviousWidth() { + return previousWidth; + } + + /** + * Get the width in pixels of the column after the resize event + * + * @return Width in pixels + */ + public int getCurrentWidth() { + return currentWidth; + } + } + + /** + * Interface for listening to column resize events. + */ + public interface ColumnResizeListener{ + + /** + * This method is triggered when the column has been resized + * + * @param event + * The event which contains the column property id, the + * previous width of the column and the current width of the + * column + */ + public void columnResize(ColumnResizeEvent event); + } + + /** + * Adds a column resize listener to the Table. A column resize listener is + * called when a user resizes a columns width. + * + * @param listener + * The listener to attach to the Table + */ + public void addListener(ColumnResizeListener listener) { + addListener(VScrollTable.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, listener, + ColumnResizeEvent.COLUMN_RESIZE_METHOD); + } + + /** + * Removes a column resize listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeListener(ColumnResizeListener listener) { + removeListener(VScrollTable.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, listener); + } } diff --git a/src/com/vaadin/ui/Tree.java b/src/com/vaadin/ui/Tree.java index 2b38d5a6eb..11b049d803 100644 --- a/src/com/vaadin/ui/Tree.java +++ b/src/com/vaadin/ui/Tree.java @@ -119,7 +119,9 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, } private TreeDragMode dragMode = TreeDragMode.NONE; - + + private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + /* Tree constructors */ /** @@ -338,6 +340,29 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, requestRepaint(); } } + + /** + * Sets the behavior of the multiselect mode + * + * @param mode + * The mode to set + */ + public void setMultiselectMode(MultiSelectMode mode) { + if (multiSelectMode != mode && mode != null) { + multiSelectMode = mode; + requestRepaint(); + } + } + + /** + * Returns the mode the multiselect is in. The mode controls how + * multiselection can be done. + * + * @return The mode + */ + public MultiSelectMode getMultiselectMode() { + return multiSelectMode; + } /* Component API */ @@ -396,6 +421,15 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, } } + // AbstractSelect cannot handle multiselection so we handle + // it ourself + if (variables.containsKey("selected") && isMultiSelect() + && multiSelectMode == MultiSelectMode.DEFAULT) { + handleSelectedItems(variables); + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + // Selections are handled by the select component super.changeVariables(source, variables); @@ -419,6 +453,37 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, } /** + * Handles the selection + * + * @param variables + * The variables sent to the server from the client + */ + private void handleSelectedItems(Map<String, Object> variables) { + final String[] ka = (String[]) variables.get("selected"); + + // Converts the key-array to id-set + final LinkedList s = new LinkedList(); + for (int i = 0; i < ka.length; i++) { + final Object id = itemIdMapper.get(ka[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + requestRepaint(); + } else if (id != null && containsId(id)) { + s.add(id); + } + } + + if (!isNullSelectionAllowed() && s.size() < 1) { + // empty selection not allowed, keep old value + requestRepaint(); + return; + } + + setValue(s, true); + } + + /** * Paints any needed component-specific things to the given UIDL stream. * * @see com.vaadin.ui.AbstractComponent#paintContent(PaintTarget) @@ -442,6 +507,10 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, if (isSelectable()) { target.addAttribute("selectmode", (isMultiSelect() ? "multi" : "single")); + if (isMultiSelect()) { + target.addAttribute("multiselectmode", multiSelectMode + .ordinal()); + } } else { target.addAttribute("selectmode", "none"); } diff --git a/src/com/vaadin/ui/Window.java b/src/com/vaadin/ui/Window.java index ea3d583c4e..266cafa484 100644 --- a/src/com/vaadin/ui/Window.java +++ b/src/com/vaadin/ui/Window.java @@ -32,7 +32,44 @@ import com.vaadin.terminal.URIHandler; import com.vaadin.terminal.gwt.client.ui.VView; /** - * Application window component. + * A component that represents an application (browser native) window or a sub + * window. + * <p> + * If the window is a application window or a sub window depends on how it is + * added to the application. Adding a {@code Window} to a {@code Window} using + * {@link Window#addWindow(Window)} makes it a sub window and adding a {@code + * Window} to the {@code Application} using + * {@link Application#addWindow(Window)} makes it an application window. + * </p> + * <p> + * An application window is the base of any view in a Vaadin application. All + * applications contain a main application window (set using + * {@link Application#setMainWindow(Window)} which is what is initially shown to + * the user. The contents of a window is set using + * {@link #setContent(ComponentContainer)}. The contents can in turn contain + * other components. For multi-tab applications there is one window instance per + * opened tab. + * </p> + * <p> + * A sub window is floating popup style window that can be added to an + * application window. Like the application window its content is set using + * {@link #setContent(ComponentContainer)}. A sub window can be positioned on + * the screen using absolute coordinates (pixels). The default content of the + * Window is set to be suitable for application windows. For sub windows it + * might be necessary to set the size of the content to work as expected. + * </p> + * <p> + * Window caption is displayed in the browser title bar for application level + * windows and in the window header for sub windows. + * </p> + * <p> + * Certain methods in this class are only meaningful for sub windows and other + * parts only for application windows. These are marked using <b>Sub window + * only</b> and <b>Application window only</b> respectively in the javadoc. + * </p> + * <p> + * Sub window is to be split into a separate component in Vaadin 7. + * </p> * * @author IT Mill Ltd. * @version @@ -44,148 +81,159 @@ import com.vaadin.terminal.gwt.client.ui.VView; public class Window extends Panel implements URIHandler, ParameterHandler { /** - * Window with no border. + * <b>Application window only</b>. A border style used for opening resources + * in a window without a border. */ public static final int BORDER_NONE = 0; /** - * Window with only minimal border. + * <b>Application window only</b>. A border style used for opening resources + * in a window with a minimal border. */ public static final int BORDER_MINIMAL = 1; /** - * Window with default borders. + * <b>Application window only</b>. A border style that indicates that the + * default border style should be used when opening resources. */ public static final int BORDER_DEFAULT = 2; /** - * The terminal this window is attached to. + * <b>Application window only</b>. The terminal this window is attached to. */ private Terminal terminal = null; /** - * The application this window is attached to. + * <b>Application window only</b>. The application this window is attached + * to or null. */ private Application application = null; /** - * List of URI handlers for this window. + * <b>Application window only</b>. List of URI handlers for this window. */ private LinkedList<URIHandler> uriHandlerList = null; /** - * List of parameter handlers for this window. + * <b>Application window only</b>. List of parameter handlers for this + * window. */ private LinkedList<ParameterHandler> parameterHandlerList = null; - /** Set of subwindows */ + /** + * <b>Application window only</b>. List of sub windows in this window. A sub + * window cannot have other sub windows. + */ private final LinkedHashSet<Window> subwindows = new LinkedHashSet<Window>(); /** - * Explicitly specified theme of this window. If null, application theme is - * used. + * <b>Application window only</b>. Explicitly specified theme of this window + * or null if the application theme should be used. */ private String theme = null; /** - * Resources to be opened automatically on next repaint. + * <b>Application window only</b>. Resources to be opened automatically on + * next repaint. The list is automatically cleared when it has been sent to + * the client. */ private final LinkedList<OpenResource> openList = new LinkedList<OpenResource>(); /** - * The name of the window. + * <b>Application window only</b>. Unique name of the window used to + * identify it. */ private String name = null; /** - * Window border mode. + * <b>Application window only.</b> Border mode of the Window. */ private int border = BORDER_DEFAULT; /** - * Distance of Window top border in pixels from top border of the containing - * (main window) or -1 if unspecified. + * <b>Sub window only</b>. Top offset in pixels for the sub window (relative + * to the parent application window) or -1 if unspecified. */ private int positionY = -1; /** - * Distance of Window left border in pixels from left border of the - * containing (main window) or -1 if unspecified . + * <b>Sub window only</b>. Left offset in pixels for the sub window + * (relative to the parent application window) or -1 if unspecified. */ private int positionX = -1; + /** + * <b>Application window only</b>. A list of notifications that are waiting + * to be sent to the client. Cleared (set to null) when the notifications + * have been sent. + */ private LinkedList<Notification> notifications; + /** + * <b>Sub window only</b>. Modality flag for sub window. + */ private boolean modal = false; + /** + * <b>Sub window only</b>. Controls if the end user can resize the window. + */ private boolean resizable = true; + /** + * <b>Sub window only</b>. Controls if the end user can move the window by + * dragging. + */ private boolean draggable = true; + /** + * <b>Sub window only</b>. Flag which is true if the window is centered on + * the screen. + */ private boolean centerRequested = false; + /** + * Component that should be focused after the next repaint. Null if no focus + * change should take place. + */ private Focusable pendingFocus; + /** + * <b>Application window only</b>. A list of javascript commands that are + * waiting to be sent to the client. Cleared (set to null) when the commands + * have been sent. + */ private ArrayList<String> jsExecQueue = null; + /** + * The component that should be scrolled into view after the next repaint. + * Null if nothing should be scrolled into view. + */ private Component scrollIntoView; - /* ********************************************************************* */ - /** - * Creates a new empty unnamed window with default layout. - * - * <p> - * To show the window in application, it must be added to application with - * <code>Application.addWindow</code> method. - * </p> - * - * <p> - * The windows are scrollable by default. - * </p> - * - * @param caption - * the Title of the window. + * Creates a new unnamed window with a default layout. */ public Window() { this("", null); } /** - * Creates a new empty window with default layout. - * - * <p> - * To show the window in application, it must be added to application with - * <code>Application.addWindow</code> method. - * </p> - * - * <p> - * The windows are scrollable by default. - * </p> + * Creates a new unnamed window with a default layout and given title. * * @param caption - * the Title of the window. + * the title of the window. */ public Window(String caption) { this(caption, null); } /** - * Creates a new window. - * - * <p> - * To show the window in application, it must be added to application with - * <code>Application.addWindow</code> method. - * </p> - * - * <p> - * The windows are scrollable by default. - * </p> + * Creates a new unnamed window with the given content and title. * * @param caption - * the Title of the window. - * @param layout - * the Layout of the window. + * the title of the window. + * @param content + * the contents of the window */ public Window(String caption, ComponentContainer content) { super(caption, content); @@ -461,14 +509,13 @@ public class Window extends Panel implements URIHandler, ParameterHandler { } /** - * Sets the theme for this window. - * - * Setting theme for subwindows is not supported. + * <b>Application window only</b>. Sets the theme for this window. * * The terminal will reload its host page on theme changes. * * @param theme - * the New theme for this window. Null implies the default theme. + * the new theme for this window or null to use the application + * theme. */ public void setTheme(String theme) { if (getParent() != null) { @@ -480,7 +527,7 @@ public class Window extends Panel implements URIHandler, ParameterHandler { } /** - * Paints the content of this component. + * Paints the contents of this component. * * @param event * the Paint Event. @@ -750,16 +797,23 @@ public class Window extends Panel implements URIHandler, ParameterHandler { } /** - * Returns the border. + * Returns the border style of the window. * - * @return the border. + * @see #setBorder(int) + * @return the border style for the window */ public int getBorder() { return border; } /** - * Sets the border. + * Sets the border style for this window. Valid values are + * {@link Window#BORDER_NONE}, {@link Window#BORDER_MINIMAL}, + * {@link Window#BORDER_DEFAULT}. + * <p> + * <b>Note!</b> Setting this seems to currently have no effect whatsoever on + * the window. + * </p> * * @param border * the border to set. diff --git a/src/com/vaadin/ui/themes/Runo.java b/src/com/vaadin/ui/themes/Runo.java index 29cc7089a6..88011addc4 100644 --- a/src/com/vaadin/ui/themes/Runo.java +++ b/src/com/vaadin/ui/themes/Runo.java @@ -7,6 +7,10 @@ public class Runo extends BaseTheme { public static final String THEME_NAME = "Runo"; + public static String themeName() { + return THEME_NAME.toLowerCase(); + } + /*************************************************************************** * * Button styles @@ -18,6 +22,18 @@ public class Runo extends BaseTheme { */ public static final String BUTTON_SMALL = "small"; + /** + * Big sized button, use to gather much attention for some particular action + */ + public static final String BUTTON_BIG = "big"; + + /** + * Default action style for buttons (the button that should get activated + * when the user presses 'enter' in a form). Use sparingly, only one default + * button per view should be visible. + */ + public static final String BUTTON_DEFAULT = "default"; + /*************************************************************************** * * Panel styles @@ -40,4 +56,128 @@ public class Runo extends BaseTheme { */ public static final String TABSHEET_SMALL = "light"; + /*************************************************************************** + * + * SplitPanel styles + * + **************************************************************************/ + + /** + * Reduces the width/height of the split handle. Useful when you don't want + * the split handle to touch the sides of the containing layout. + */ + public static final String SPLITPANEL_REDUCED = "rounded"; + + /** + * Reduces the visual size of the split handle to one pixel (the active drag + * size is still larger). + */ + public static final String SPLITPANEL_SMALL = "small"; + + /*************************************************************************** + * + * Label styles + * + **************************************************************************/ + + /** + * Largest title/header size. Use for main sections in your application. + */ + public static final String LABEL_H1 = "h1"; + + /** + * Similar style as in panel captions. Useful for sub-sections within a + * view. + */ + public static final String LABEL_H2 = "h2"; + + /** + * Small font size. Useful for contextual help texts and similar less + * frequently needed information. Use with modesty, since this style will be + * more harder to read due to its smaller size and contrast. + */ + public static final String LABEL_SMALL = "small"; + + /*************************************************************************** + * + * Layout styles + * + **************************************************************************/ + + /** + * An alternative background color for layouts. Use on top of white + * background (e.g. inside Panels, TabSheets and sub-windows). + */ + public static final String LAYOUT_DARKER = "darker"; + + /** + * Add a drop shadow around the layout and its contained components. + * Produces a rectangular shadow, even if the contained component would have + * a different shape. + * <p> + * Note: does not work in Internet Explorer 6 + */ + public static final String CSSLAYOUT_SHADOW = "box-shadow"; + + /** + * Adds necessary styles to the layout to make it look selectable (i.e. + * clickable). Add a click listener for the layout, and toggle the + * {@link #CSSLAYOUT_SELECTABLE_SELECTED} style for the same layout to make + * it look selected or not. + */ + public static final String CSSLAYOUT_SELECTABLE = "selectable"; + public static final String CSSLAYOUT_SELECTABLE_SELECTED = "selectable-selected"; + + /*************************************************************************** + * + * TextField styles + * + **************************************************************************/ + + /** + * Small sized text field with small font + */ + public static final String TEXTFIELD_SMALL = "small"; + + /*************************************************************************** + * + * Table styles + * + **************************************************************************/ + + /** + * Smaller header and item fonts. + */ + public static final String TABLE_SMALL = "small"; + + /** + * Removes the border and background color from the table. Removes + * alternating row background colors as well. + */ + public static final String TABLE_BORDERLESS = "borderless"; + + /*************************************************************************** + * + * Accordion styles + * + **************************************************************************/ + + /** + * A detached looking accordion, providing space around its captions and + * content. Doesn't necessarily need a Panel or other container to wrap it + * in order to make it look right. + */ + public static final String ACCORDION_LIGHT = "light"; + + /*************************************************************************** + * + * Window styles + * + **************************************************************************/ + + /** + * Smaller header and a darker background color for the window. Useful for + * smaller dialog-like windows. + */ + public static final String WINDOW_DIALOG = "dialog"; } |