diff options
Diffstat (limited to 'src/com/vaadin/terminal')
179 files changed, 38290 insertions, 0 deletions
diff --git a/src/com/vaadin/terminal/ApplicationResource.java b/src/com/vaadin/terminal/ApplicationResource.java new file mode 100644 index 0000000000..85da38cc93 --- /dev/null +++ b/src/com/vaadin/terminal/ApplicationResource.java @@ -0,0 +1,75 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +import com.vaadin.Application; + +/** + * This interface must be implemented by classes wishing to provide Application + * resources. + * <p> + * <code>ApplicationResource</code> are a set of named resources (pictures, + * sounds, etc) associated with some specific application. Having named + * application resources provides a convenient method for having inter-theme + * common resources for an application. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface ApplicationResource extends Resource, Serializable { + + /** + * Default cache time. + */ + public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24; + + /** + * Gets resource as stream. + */ + public DownloadStream getStream(); + + /** + * Gets the application of the resource. + */ + public Application getApplication(); + + /** + * Gets the virtual filename for this resource. + * + * @return the file name associated to this resource. + */ + public String getFilename(); + + /** + * Gets the length of cache expiration time. + * + * <p> + * This gives the adapter the possibility cache streams sent to the client. + * The caching may be made in adapter or at the client if the client + * supports caching. Default is <code>DEFAULT_CACHETIME</code>. + * </p> + * + * @return Cache time in milliseconds + */ + public long getCacheTime(); + + /** + * Gets the size of the download buffer used for this resource. + * + * <p> + * If the buffer size is 0, the buffer size is decided by the terminal + * adapter. The default value is 0. + * </p> + * + * @return int the size of the buffer in bytes. + */ + public int getBufferSize(); + +} diff --git a/src/com/vaadin/terminal/ClassResource.java b/src/com/vaadin/terminal/ClassResource.java new file mode 100644 index 0000000000..6d604aeea2 --- /dev/null +++ b/src/com/vaadin/terminal/ClassResource.java @@ -0,0 +1,178 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +import com.vaadin.Application; +import com.vaadin.service.FileTypeResolver; + +/** + * <code>ClassResource</code> is a named resource accessed with the class + * loader. + * + * This can be used to access resources such as icons, files, etc. + * + * @see java.lang.Class#getResource(java.lang.String) + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ClassResource implements ApplicationResource, Serializable { + + /** + * Default buffer size for this stream resource. + */ + private int bufferSize = 0; + + /** + * Default cache time for this stream resource. + */ + private long cacheTime = DEFAULT_CACHETIME; + + /** + * Associated class used for indetifying the source of the resource. + */ + private final Class associatedClass; + + /** + * Name of the resource is relative to the associated class. + */ + private final String resourceName; + + /** + * Application used for serving the class. + */ + private final Application application; + + /** + * Creates a new application resource instance. The resource id is relative + * to the location of the application class. + * + * @param resourceName + * the Unique identifier of the resource within the application. + * @param application + * the application this resource will be added to. + */ + public ClassResource(String resourceName, Application application) { + associatedClass = application.getClass(); + this.resourceName = resourceName; + this.application = application; + if (resourceName == null) { + throw new NullPointerException(); + } + application.addResource(this); + } + + /** + * Creates a new application resource instance. + * + * @param associatedClass + * the class of the which the resource is associated. + * @param resourceName + * the Unique identifier of the resource within the application. + * @param application + * the application this resource will be added to. + */ + public ClassResource(Class associatedClass, String resourceName, + Application application) { + this.associatedClass = associatedClass; + this.resourceName = resourceName; + this.application = application; + if (resourceName == null || associatedClass == null) { + throw new NullPointerException(); + } + application.addResource(this); + } + + /** + * Gets the MIME type of this resource. + * + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + public String getMIMEType() { + return FileTypeResolver.getMIMEType(resourceName); + } + + /** + * Gets the application of this resource. + * + * @see com.vaadin.terminal.ApplicationResource#getApplication() + */ + public Application getApplication() { + return application; + } + + /** + * Gets the virtual filename for this resource. + * + * @return the file name associated to this resource. + * @see com.vaadin.terminal.ApplicationResource#getFilename() + */ + public String getFilename() { + int index = 0; + int next = 0; + while ((next = resourceName.indexOf('/', index)) > 0 + && next + 1 < resourceName.length()) { + index = next + 1; + } + return resourceName.substring(index); + } + + /** + * Gets resource as stream. + * + * @see com.vaadin.terminal.ApplicationResource#getStream() + */ + public DownloadStream getStream() { + final DownloadStream ds = new DownloadStream(associatedClass + .getResourceAsStream(resourceName), getMIMEType(), + getFilename()); + ds.setBufferSize(getBufferSize()); + ds.setCacheTime(cacheTime); + return ds; + } + + /* documented in superclass */ + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer used for this resource. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /* documented in superclass */ + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets the length of cache expiration time. + * + * <p> + * This gives the adapter the possibility cache streams sent to the client. + * The caching may be made in adapter or at the client if the client + * supports caching. Zero or negavive value disbales the caching of this + * stream. + * </p> + * + * @param cacheTime + * the cache time in milliseconds. + * + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } +} diff --git a/src/com/vaadin/terminal/CompositeErrorMessage.java b/src/com/vaadin/terminal/CompositeErrorMessage.java new file mode 100644 index 0000000000..776e6c919c --- /dev/null +++ b/src/com/vaadin/terminal/CompositeErrorMessage.java @@ -0,0 +1,188 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * Class for combining multiple error messages together. + * + * @author IT Mill Ltd + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CompositeErrorMessage implements ErrorMessage, Serializable { + + /** + * Array of all the errors. + */ + private final List errors; + + /** + * Level of the error. + */ + private int level; + + /** + * Constructor for CompositeErrorMessage. + * + * @param errorMessages + * the Array of error messages that are listed togeter. Nulls are + * ignored, but at least one message is required. + */ + public CompositeErrorMessage(ErrorMessage[] errorMessages) { + errors = new ArrayList(errorMessages.length); + level = Integer.MIN_VALUE; + + for (int i = 0; i < errorMessages.length; i++) { + addErrorMessage(errorMessages[i]); + } + + if (errors.size() == 0) { + throw new IllegalArgumentException( + "Composite error message must have at least one error"); + } + + } + + /** + * Constructor for CompositeErrorMessage. + * + * @param errorMessages + * the Collection of error messages that are listed togeter. At + * least one message is required. + */ + public CompositeErrorMessage(Collection errorMessages) { + errors = new ArrayList(errorMessages.size()); + level = Integer.MIN_VALUE; + + for (final Iterator i = errorMessages.iterator(); i.hasNext();) { + addErrorMessage((ErrorMessage) i.next()); + } + + if (errors.size() == 0) { + throw new IllegalArgumentException( + "Composite error message must have at least one error"); + } + } + + /** + * The error level is the largest error level in + * + * @see com.vaadin.terminal.ErrorMessage#getErrorLevel() + */ + public final int getErrorLevel() { + return level; + } + + /** + * Adds a error message into this composite message. Updates the level + * field. + * + * @param error + * the error message to be added. Duplicate errors are ignored. + */ + private void addErrorMessage(ErrorMessage error) { + if (error != null && !errors.contains(error)) { + errors.add(error); + final int l = error.getErrorLevel(); + if (l > level) { + level = l; + } + } + } + + /** + * Gets Error Iterator. + * + * @return the error iterator. + */ + public Iterator iterator() { + return errors.iterator(); + } + + /** + * @see com.vaadin.terminal.Paintable#paint(com.vaadin.terminal.PaintTarget) + */ + public void paint(PaintTarget target) throws PaintException { + + if (errors.size() == 1) { + ((ErrorMessage) errors.iterator().next()).paint(target); + } else { + target.startTag("error"); + + if (level > 0 && level <= ErrorMessage.INFORMATION) { + target.addAttribute("level", "info"); + } else if (level <= ErrorMessage.WARNING) { + target.addAttribute("level", "warning"); + } else if (level <= ErrorMessage.ERROR) { + target.addAttribute("level", "error"); + } else if (level <= ErrorMessage.CRITICAL) { + target.addAttribute("level", "critical"); + } else { + target.addAttribute("level", "system"); + } + + // Paint all the exceptions + for (final Iterator i = errors.iterator(); i.hasNext();) { + ((ErrorMessage) i.next()).paint(target); + } + + target.endTag("error"); + } + } + + /* Documented in super interface */ + public void addListener(RepaintRequestListener listener) { + } + + /* Documented in super interface */ + public void removeListener(RepaintRequestListener listener) { + } + + /* Documented in super interface */ + public void requestRepaint() { + } + + /* Documented in super interface */ + public void requestRepaintRequests() { + } + + /** + * Returns a comma separated list of the error messages. + * + * @return String, comma separated list of error messages. + */ + @Override + public String toString() { + String retval = "["; + int pos = 0; + for (final Iterator i = errors.iterator(); i.hasNext();) { + if (pos > 0) { + retval += ","; + } + pos++; + retval += i.next().toString(); + } + retval += "]"; + + return retval; + } + + public String getDebugId() { + return null; + } + + public void setDebugId(String id) { + throw new UnsupportedOperationException( + "Setting testing id for this Paintable is not implemented"); + } +} diff --git a/src/com/vaadin/terminal/DownloadStream.java b/src/com/vaadin/terminal/DownloadStream.java new file mode 100644 index 0000000000..d46f104273 --- /dev/null +++ b/src/com/vaadin/terminal/DownloadStream.java @@ -0,0 +1,206 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.InputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Downloadable stream. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class DownloadStream implements Serializable { + + /** + * Maximum cache time. + */ + public static final long MAX_CACHETIME = Long.MAX_VALUE; + + /** + * Default cache time. + */ + public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24; + + private InputStream stream; + + private String contentType; + + private String fileName; + + private Map params; + + private long cacheTime = DEFAULT_CACHETIME; + + private int bufferSize = 0; + + /** + * Creates a new instance of DownloadStream. + */ + public DownloadStream(InputStream stream, String contentType, + String fileName) { + setStream(stream); + setContentType(contentType); + setFileName(fileName); + } + + /** + * Gets downloadable stream. + * + * @return output stream. + */ + public InputStream getStream() { + return stream; + } + + /** + * Sets the stream. + * + * @param stream + * The stream to set + */ + public void setStream(InputStream stream) { + this.stream = stream; + } + + /** + * Gets stream content type. + * + * @return type of the stream content. + */ + public String getContentType() { + return contentType; + } + + /** + * Sets stream content type. + * + * @param contentType + * the contentType to set + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Returns the file name. + * + * @return the name of the file. + */ + public String getFileName() { + return fileName; + } + + /** + * Sets the file name. + * + * @param fileName + * the file name to set. + */ + public void setFileName(String fileName) { + this.fileName = fileName; + } + + /** + * Sets a paramater for download stream. Parameters are optional information + * about the downloadable stream and their meaning depends on the used + * adapter. For example in WebAdapter they are interpreted as HTTP response + * headers. + * + * If the parameters by this name exists, the old value is replaced. + * + * @param name + * the Name of the parameter to set. + * @param value + * the Value of the parameter to set. + */ + public void setParameter(String name, String value) { + if (params == null) { + params = new HashMap(); + } + params.put(name, value); + } + + /** + * Gets a paramater for download stream. Parameters are optional information + * about the downloadable stream and their meaning depends on the used + * adapter. For example in WebAdapter they are interpreted as HTTP response + * headers. + * + * @param name + * the Name of the parameter to set. + * @return Value of the parameter or null if the parameter does not exist. + */ + public String getParameter(String name) { + if (params != null) { + return (String) params.get(name); + } + return null; + } + + /** + * Gets the names of the parameters. + * + * @return Iterator of names or null if no parameters are set. + */ + public Iterator getParameterNames() { + if (params != null) { + return params.keySet().iterator(); + } + return null; + } + + /** + * Gets length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Default is + * <code>DEFAULT_CACHETIME</code>. + * + * @return Cache time in milliseconds + */ + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Zero or negavive + * value disbales the caching of this stream. + * + * @param cacheTime + * the cache time in milliseconds. + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } + + /** + * Gets the size of the download buffer. + * + * @return int The size of the buffer in bytes. + */ + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + +} diff --git a/src/com/vaadin/terminal/ErrorMessage.java b/src/com/vaadin/terminal/ErrorMessage.java new file mode 100644 index 0000000000..428a3016df --- /dev/null +++ b/src/com/vaadin/terminal/ErrorMessage.java @@ -0,0 +1,80 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * Interface for rendering error messages to terminal. All the visible errors + * shown to user must implement this interface. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface ErrorMessage extends Paintable, Serializable { + + /** + * Error code for system errors and bugs. + */ + public static final int SYSTEMERROR = 5000; + + /** + * Error code for critical error messages. + */ + public static final int CRITICAL = 4000; + + /** + * Error code for regular error messages. + */ + public static final int ERROR = 3000; + + /** + * Error code for warning messages. + */ + public static final int WARNING = 2000; + + /** + * Error code for informational messages. + */ + public static final int INFORMATION = 1000; + + /** + * Gets the errors level. + * + * @return the level of error as an integer. + */ + public int getErrorLevel(); + + /** + * Error messages are inmodifiable and thus listeners are not needed. This + * method should be implemented as empty. + * + * @param listener + * the listener to be added. + * @see com.vaadin.terminal.Paintable#addListener(Paintable.RepaintRequestListener) + */ + public void addListener(RepaintRequestListener listener); + + /** + * Error messages are inmodifiable and thus listeners are not needed. This + * method should be implemented as empty. + * + * @param listener + * the listener to be removed. + * @see com.vaadin.terminal.Paintable#removeListener(Paintable.RepaintRequestListener) + */ + public void removeListener(RepaintRequestListener listener); + + /** + * Error messages are inmodifiable and thus listeners are not needed. This + * method should be implemented as empty. + * + * @see com.vaadin.terminal.Paintable#requestRepaint() + */ + public void requestRepaint(); + +} diff --git a/src/com/vaadin/terminal/ExternalResource.java b/src/com/vaadin/terminal/ExternalResource.java new file mode 100644 index 0000000000..c07adf7d9d --- /dev/null +++ b/src/com/vaadin/terminal/ExternalResource.java @@ -0,0 +1,75 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.net.URL; + +import com.vaadin.service.FileTypeResolver; + +/** + * <code>ExternalResource</code> implements source for resources fetched from + * location specified by URL:s. The resources are fetched directly by the client + * terminal and are not fetched trough the terminal adapter. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public class ExternalResource implements Resource, Serializable { + + /** + * Url of the download. + */ + private String sourceURL = null; + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + */ + public ExternalResource(URL sourceURL) { + if (sourceURL == null) { + throw new RuntimeException("Source must be non-null"); + } + + this.sourceURL = sourceURL.toString(); + } + + /** + * Creates a new download component for downloading directly from given URL. + * + * @param sourceURL + * the source URL. + */ + public ExternalResource(String sourceURL) { + if (sourceURL == null) { + throw new RuntimeException("Source must be non-null"); + } + + this.sourceURL = sourceURL.toString(); + } + + /** + * Gets the URL of the external resource. + * + * @return the URL of the external resource. + */ + public String getURL() { + return sourceURL; + } + + /** + * Gets the MIME type of the resource. + * + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + public String getMIMEType() { + return FileTypeResolver.getMIMEType(getURL().toString()); + } + +} diff --git a/src/com/vaadin/terminal/FileResource.java b/src/com/vaadin/terminal/FileResource.java new file mode 100644 index 0000000000..46f1a6c028 --- /dev/null +++ b/src/com/vaadin/terminal/FileResource.java @@ -0,0 +1,155 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +import com.vaadin.Application; +import com.vaadin.service.FileTypeResolver; + +/** + * <code>FileResources</code> are files or directories on local filesystem. The + * files and directories are served through URI:s to the client terminal and + * thus must be registered to an URI context before they can be used. The + * resource is automatically registered to the application when it is created. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class FileResource implements ApplicationResource { + + /** + * Default buffer size for this stream resource. + */ + private int bufferSize = 0; + + /** + * File where the downloaded content is fetched from. + */ + private File sourceFile; + + /** + * Application. + */ + private final Application application; + + /** + * Default cache time for this stream resource. + */ + private long cacheTime = DownloadStream.DEFAULT_CACHETIME; + + /** + * Creates a new file resource for providing given file for client + * terminals. + */ + public FileResource(File sourceFile, Application application) { + this.application = application; + setSourceFile(sourceFile); + application.addResource(this); + } + + /** + * Gets the resource as stream. + * + * @see com.vaadin.terminal.ApplicationResource#getStream() + */ + public DownloadStream getStream() { + try { + final DownloadStream ds = new DownloadStream(new FileInputStream( + sourceFile), getMIMEType(), getFilename()); + ds.setCacheTime(cacheTime); + return ds; + } catch (final FileNotFoundException e) { + // No logging for non-existing files at this level. + return null; + } + } + + /** + * Gets the source file. + * + * @return the source File. + */ + public File getSourceFile() { + return sourceFile; + } + + /** + * Sets the source file. + * + * @param sourceFile + * the source file to set. + */ + public void setSourceFile(File sourceFile) { + this.sourceFile = sourceFile; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getApplication() + */ + public Application getApplication() { + return application; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getFilename() + */ + public String getFilename() { + return sourceFile.getName(); + } + + /** + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + public String getMIMEType() { + return FileTypeResolver.getMIMEType(sourceFile); + } + + /** + * Gets the length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Default is + * <code>DownloadStream.DEFAULT_CACHETIME</code>. + * + * @return Cache time in milliseconds. + */ + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets the length of cache expiration time. This gives the adapter the + * possibility cache streams sent to the client. The caching may be made in + * adapter or at the client if the client supports caching. Zero or negavive + * value disbales the caching of this stream. + * + * @param cacheTime + * the cache time in milliseconds. + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } + + /* documented in superclass */ + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer used for this resource. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + +} diff --git a/src/com/vaadin/terminal/KeyMapper.java b/src/com/vaadin/terminal/KeyMapper.java new file mode 100644 index 0000000000..706b5d27e7 --- /dev/null +++ b/src/com/vaadin/terminal/KeyMapper.java @@ -0,0 +1,88 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.Hashtable; + +/** + * <code>KeyMapper</code> is the simple two-way map for generating textual keys + * for objects and retrieving the objects later with the key. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class KeyMapper implements Serializable { + + private int lastKey = 0; + + private final Hashtable objectKeyMap = new Hashtable(); + + private final Hashtable keyObjectMap = new Hashtable(); + + /** + * Gets key for an object. + * + * @param o + * the object. + */ + public String key(Object o) { + + if (o == null) { + return "null"; + } + + // If the object is already mapped, use existing key + String key = (String) objectKeyMap.get(o); + if (key != null) { + return key; + } + + // If the object is not yet mapped, map it + key = String.valueOf(++lastKey); + objectKeyMap.put(o, key); + keyObjectMap.put(key, o); + + return key; + } + + /** + * Retrieves object with the key. + * + * @param key + * the name with the desired value. + * @return the object with the key. + */ + public Object get(String key) { + + return keyObjectMap.get(key); + } + + /** + * Removes object from the mapper. + * + * @param removeobj + * the object to be removed. + */ + public void remove(Object removeobj) { + final String key = (String) objectKeyMap.get(removeobj); + + if (key != null) { + objectKeyMap.remove(key); + keyObjectMap.remove(removeobj); + } + } + + /** + * Removes all objects from the mapper. + */ + public void removeAll() { + objectKeyMap.clear(); + keyObjectMap.clear(); + } +} diff --git a/src/com/vaadin/terminal/PaintException.java b/src/com/vaadin/terminal/PaintException.java new file mode 100644 index 0000000000..af025bfcaa --- /dev/null +++ b/src/com/vaadin/terminal/PaintException.java @@ -0,0 +1,41 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.IOException; +import java.io.Serializable; + +/** + * <code>PaintExcepection</code> is thrown if painting of a component fails. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class PaintException extends IOException implements Serializable { + + /** + * Constructs an instance of <code>PaintExeception</code> with the specified + * detail message. + * + * @param msg + * the detail message. + */ + public PaintException(String msg) { + super(msg); + } + + /** + * Constructs an instance of <code>PaintExeception</code> from IOException. + * + * @param exception + * the original exception. + */ + public PaintException(IOException exception) { + super(exception.getMessage()); + } +} diff --git a/src/com/vaadin/terminal/PaintTarget.java b/src/com/vaadin/terminal/PaintTarget.java new file mode 100644 index 0000000000..4740c509d3 --- /dev/null +++ b/src/com/vaadin/terminal/PaintTarget.java @@ -0,0 +1,382 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * This interface defines the methods for painting XML to the UIDL stream. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface PaintTarget extends Serializable{ + + /** + * Prints single XMLsection. + * + * Prints full XML section. The section data is escaped from XML tags and + * surrounded by XML start and end-tags. + * + * @param sectionTagName + * the name of the tag. + * @param sectionData + * the scetion data. + * @throws PaintException + * if the paint operation failed. + */ + public void addSection(String sectionTagName, String sectionData) + throws PaintException; + + /** + * Prints element start tag of a paintable section. Starts a paintable + * section using the given tag. The PaintTarget may implement a caching + * scheme, that checks the paintable has actually changed or can a cached + * version be used instead. This method should call the startTag method. + * <p> + * If the Paintable is found in cache and this function returns true it may + * omit the content and close the tag, in which case cached content should + * be used. + * </p> + * + * @param paintable + * the paintable to start. + * @param tag + * the name of the start tag. + * @return <code>true</code> if paintable found in cache, <code>false</code> + * otherwise. + * @throws PaintException + * if the paint operation failed. + * @see #startTag(String) + * @since 3.1 + */ + public boolean startTag(Paintable paintable, String tag) + throws PaintException; + + /** + * Paints a component reference as an attribute to current tag. This method + * is meant to enable component interactions on client side. With reference + * the client side component can communicate directly to other component. + * + * Note! This is still an experimental feature and API is likely to change + * in future. + * + * @param paintable + * the Paintable to reference + * @param referenceName + * @throws PaintException + * + * @since 5.2 + */ + public void paintReference(Paintable paintable, String referenceName) + throws PaintException; + + /** + * Prints element start tag. + * + * <pre> + * Todo: + * Checking of input values + * </pre> + * + * @param tagName + * the name of the start tag. + * @throws PaintException + * if the paint operation failed. + */ + public void startTag(String tagName) throws PaintException; + + /** + * Prints element end tag. + * + * If the parent tag is closed before every child tag is closed an + * PaintException is raised. + * + * @param tagName + * the name of the end tag. + * @throws PaintException + * if the paint operation failed. + */ + public void endTag(String tagName) throws PaintException; + + /** + * Adds a boolean attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, boolean value) throws PaintException; + + /** + * Adds a integer attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, int value) throws PaintException; + + /** + * Adds a resource attribute to component. Atributes must be added before + * any content is written. + * + * @param name + * the Attribute name + * @param value + * the Attribute value + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, Resource value) throws PaintException; + + /** + * Adds a long attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, long value) throws PaintException; + + /** + * Adds a float attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, float value) throws PaintException; + + /** + * Adds a double attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, double value) throws PaintException; + + /** + * Adds a string attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Boolean attribute name. + * @param value + * the Boolean attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, String value) throws PaintException; + + /** + * Adds a string type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, String value) + throws PaintException; + + /** + * Adds a int type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, int value) + throws PaintException; + + /** + * Adds a long type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, long value) + throws PaintException; + + /** + * Adds a float type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, float value) + throws PaintException; + + /** + * Adds a double type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, double value) + throws PaintException; + + /** + * Adds a boolean type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, boolean value) + throws PaintException; + + /** + * Adds a string array type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, String[] value) + throws PaintException; + + /** + * Adds a upload stream type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addUploadStreamVariable(VariableOwner owner, String name) + throws PaintException; + + /** + * Prints single XML section. + * <p> + * Prints full XML section. The section data must be XML and it is + * surrounded by XML start and end-tags. + * </p> + * + * @param sectionTagName + * the tag name. + * @param sectionData + * the section data to be printed. + * @param namespace + * the namespace. + * @throws PaintException + * if the paint operation failed. + */ + public void addXMLSection(String sectionTagName, String sectionData, + String namespace) throws PaintException; + + /** + * Adds UIDL directly. The UIDL must be valid in accordance with the + * UIDL.dtd + * + * @param uidl + * the UIDL to be added. + * @throws PaintException + * if the paint operation failed. + */ + public void addUIDL(java.lang.String uidl) throws PaintException; + + /** + * Adds text node. All the contents of the text are XML-escaped. + * + * @param text + * the Text to add + * @throws PaintException + * if the paint operation failed. + */ + void addText(String text) throws PaintException; + + /** + * Adds CDATA node to target UIDL-tree. + * + * @param text + * the Character data to add + * @throws PaintException + * if the paint operation failed. + * @since 3.1 + */ + void addCharacterData(String text) throws PaintException; + + public void addAttribute(String string, Object[] keys); +} diff --git a/src/com/vaadin/terminal/Paintable.java b/src/com/vaadin/terminal/Paintable.java new file mode 100644 index 0000000000..b3fbdc3d9c --- /dev/null +++ b/src/com/vaadin/terminal/Paintable.java @@ -0,0 +1,142 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.EventObject; + +/** + * Interface implemented by all classes that can be painted. Classes + * implementing this interface know how to output themselves to a UIDL stream + * and that way describing to the terminal how it should be displayed in the UI. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Paintable extends java.util.EventListener, Serializable { + + /** + * <p> + * Paints the Paintable into a UIDL stream. This method creates the UIDL + * sequence describing it and outputs it to the given UIDL stream. + * </p> + * + * <p> + * It is called when the contents of the component should be painted in + * response to the component first being shown or having been altered so + * that its visual representation is changed. + * </p> + * + * @param target + * the target UIDL stream where the component should paint itself + * to. + * @throws PaintException + * if the paint operation failed. + */ + public void paint(PaintTarget target) throws PaintException; + + /** + * Requests that the paintable should be repainted as soon as possible. + */ + public void requestRepaint(); + + /** + * Adds an unique id for component that get's transferred to terminal for + * testing purposes. Keeping identifiers unique throughout the Application + * instance is on programmers responsibility. + * + * @param id + * A short (< 20 chars) alphanumeric id + */ + public void setDebugId(String id); + + /** + * Get's currently set debug identifier + * + * @return current debug id, null if not set + */ + public String getDebugId(); + + /** + * Repaint request event is thrown when the paintable needs to be repainted. + * This is typically done when the <code>paint</code> method would return + * dissimilar UIDL from the previous call of the method. + */ + @SuppressWarnings("serial") + public class RepaintRequestEvent extends EventObject { + + /** + * Constructs a new event. + * + * @param source + * the paintable needing repaint. + */ + public RepaintRequestEvent(Paintable source) { + super(source); + } + + /** + * Gets the paintable needing repainting. + * + * @return Paintable for which the <code>paint</code> method will return + * dissimilar UIDL from the previous call of the method. + */ + public Paintable getPaintable() { + return (Paintable) getSource(); + } + } + + /** + * Listens repaint requests. The <code>repaintRequested</code> method is + * called when the paintable needs to be repainted. This is typically done + * when the <code>paint</code> method would return dissimilar UIDL from the + * previous call of the method. + */ + public interface RepaintRequestListener extends Serializable { + + /** + * Receives repaint request events. + * + * @param event + * the repaint request event specifying the paintable source. + */ + public void repaintRequested(RepaintRequestEvent event); + } + + /** + * Adds repaint request listener. In order to assure that no repaint + * requests are missed, the new repaint listener should paint the paintable + * right after adding itself as listener. + * + * @param listener + * the listener to be added. + */ + public void addListener(RepaintRequestListener listener); + + /** + * Removes repaint request listener. + * + * @param listener + * the listener to be removed. + */ + public void removeListener(RepaintRequestListener listener); + + /** + * Request sending of repaint events on any further visible changes. + * Normally the paintable only send up to one repaint request for listeners + * after paint as the paintable as the paintable assumes that the listeners + * already know about the repaint need. This method resets the assumtion. + * Paint implicitly does the assumtion reset functionality implemented by + * this method. + * <p> + * This method is normally used only by the terminals to note paintables + * about implicit repaints (painting the component without actually invoking + * paint method). + * </p> + */ + public void requestRepaintRequests(); +} diff --git a/src/com/vaadin/terminal/ParameterHandler.java b/src/com/vaadin/terminal/ParameterHandler.java new file mode 100644 index 0000000000..93b1b6bcb7 --- /dev/null +++ b/src/com/vaadin/terminal/ParameterHandler.java @@ -0,0 +1,57 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.Map; + +/** + * Interface implemented by all the 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. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +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> + * + * @param parameters + * the Inmodifiable name to value[] mapping. + * + */ + public void handleParameters(Map parameters); + + /** + * ParameterHandler error event. + */ + public interface ErrorEvent extends Terminal.ErrorEvent { + + /** + * Gets the source ParameterHandler. + * + * @return the source Parameter Handler. + */ + public ParameterHandler getParameterHandler(); + + } + +} diff --git a/src/com/vaadin/terminal/Resource.java b/src/com/vaadin/terminal/Resource.java new file mode 100644 index 0000000000..07753aad16 --- /dev/null +++ b/src/com/vaadin/terminal/Resource.java @@ -0,0 +1,26 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * <code>Resource</code> provided to the client terminal. Support for actually + * displaying the resource type is left to the terminal. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Resource extends Serializable{ + + /** + * Gets the MIME type of the resource. + * + * @return the MIME type of the resource. + */ + public String getMIMEType(); +} diff --git a/src/com/vaadin/terminal/Scrollable.java b/src/com/vaadin/terminal/Scrollable.java new file mode 100644 index 0000000000..5f57a77e76 --- /dev/null +++ b/src/com/vaadin/terminal/Scrollable.java @@ -0,0 +1,98 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * <p> + * This interface is implemented by all visual objects that can be scrolled. The + * unit of scrolling is pixel. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Scrollable extends Serializable { + + /** + * Gets scroll left offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled right. + * </p> + * + * @return Horizontal scrolling position in pixels. + */ + public int getScrollLeft(); + + /** + * Sets scroll left offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled right. + * </p> + * + * @param pixelsScrolled + * the xOffset. + */ + public void setScrollLeft(int pixelsScrolled); + + /** + * Gets scroll top offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled down. + * </p> + * + * @return Vertical scrolling position in pixels. + */ + public int getScrollTop(); + + /** + * Sets scroll top offset. + * + * <p> + * Scrolling offset is the number of pixels this scrollable has been + * scrolled down. + * </p> + * + * @param pixelsScrolled + * the yOffset. + */ + public void setScrollTop(int pixelsScrolled); + + /** + * Is the scrolling enabled. + * + * <p> + * Enabling scrolling allows the user to scroll the scrollable view + * interactively + * </p> + * + * @return <code>true</code> if the scrolling is allowed, otherwise + * <code>false</code>. + */ + public boolean isScrollable(); + + /** + * Enables or disables scrolling.. + * + * <p> + * Enabling scrolling allows the user to scroll the scrollable view + * interactively + * </p> + * + * @param isScrollingEnabled + * true if the scrolling is allowed. + */ + public void setScrollable(boolean isScrollingEnabled); + +} diff --git a/src/com/vaadin/terminal/Sizeable.java b/src/com/vaadin/terminal/Sizeable.java new file mode 100644 index 0000000000..6c18f746f2 --- /dev/null +++ b/src/com/vaadin/terminal/Sizeable.java @@ -0,0 +1,244 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * Interface to be implemented by components wishing to display some object that + * may be dynamically resized during runtime. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Sizeable extends Serializable{ + + /** + * Unit code representing pixels. + */ + public static final int UNITS_PIXELS = 0; + + /** + * Unit code representing points (1/72nd of an inch). + */ + public static final int UNITS_POINTS = 1; + + /** + * Unit code representing picas (12 points). + */ + public static final int UNITS_PICAS = 2; + + /** + * Unit code representing the font-size of the relevant font. + */ + public static final int UNITS_EM = 3; + + /** + * Unit code representing the x-height of the relevant font. + */ + public static final int UNITS_EX = 4; + + /** + * Unit code representing millimeters. + */ + public static final int UNITS_MM = 5; + + /** + * Unit code representing centimeters. + */ + public static final int UNITS_CM = 6; + + /** + * Unit code representing inches. + */ + public static final int UNITS_INCH = 7; + + /** + * Unit code representing in percentage of the containing element defined by + * terminal. + */ + public static final int UNITS_PERCENTAGE = 8; + + public static final float SIZE_UNDEFINED = -1; + + /** + * Textual representations of units symbols. Supported units and their + * symbols are: + * <ul> + * <li><code>UNITS_PIXELS</code>: "px"</li> + * <li><code>UNITS_POINTS</code>: "pt"</li> + * <li><code>UNITS_PICAS</code>: "pc"</li> + * <li><code>UNITS_EM</code>: "em"</li> + * <li><code>UNITS_EX</code>: "ex"</li> + * <li><code>UNITS_MM</code>: "mm"</li> + * <li><code>UNITS_CM</code>. "cm"</li> + * <li><code>UNITS_INCH</code>: "in"</li> + * <li><code>UNITS_PERCENTAGE</code>: "%"</li> + * </ul> + * These can be used like <code>Sizeable.UNIT_SYMBOLS[UNITS_PIXELS]</code>. + */ + public static final String[] UNIT_SYMBOLS = { "px", "pt", "pc", "em", "ex", + "mm", "cm", "in", "%" }; + + /** + * Gets the width of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @return width of the object in units specified by widthUnits property. + */ + public float getWidth(); + + /** + * Sets the width of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @param width + * the width of the object in units specified by widthUnits + * property. + * @deprecated Consider using {@link #setWidth(String)} instead. This method + * works, but is error-prone since the unit must be set + * separately (and components might have different default + * unit). + */ + @Deprecated + public void setWidth(float width); + + /** + * Gets the height of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @return height of the object in units specified by heightUnits property. + */ + public float getHeight(); + + /** + * Sets the height of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @param height + * the height of the object in units specified by heightUnits + * property. + * @deprecated Consider using {@link #setHeight(String)} or + * {@link #setHeight(float, int)} instead. This method works, + * but is error-prone since the unit must be set separately (and + * components might have different default unit). + */ + @Deprecated + public void setHeight(float height); + + /** + * Gets the width property units. + * + * @return units used in width property. + */ + public int getWidthUnits(); + + /** + * Sets the width property units. + * + * @param units + * the units used in width property. + * @deprecated Consider setting width and unit simultaneously using + * {@link #setWidth(String)} or {@link #setWidth(float, int)}, + * which is less error-prone. + */ + @Deprecated + public void setWidthUnits(int units); + + /** + * Gets the height property units. + * + * @return units used in height property. + */ + public int getHeightUnits(); + + /** + * Sets the height property units. + * + * @param units + * the units used in height property. + * @deprecated Consider setting height and unit simultaneously using + * {@link #setHeight(String)} or {@link #setHeight(float, int)}, + * which is less error-prone. + */ + @Deprecated + public void setHeightUnits(int units); + + /** + * Sets the height of the component using String presentation. + * + * String presentation is similar to what is used in Cascading Style Sheets. + * Size can be length or percentage of available size. + * + * The empty string ("") or null will unset the height and set the units to + * pixels. + * + * See <a + * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS + * specification</a> for more details. + * + * @param height + * in CSS style string representation + */ + public void setHeight(String height); + + /** + * Sets the width of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @param width + * the width of the object. + * @param unit + * the unit used for the width. Possible values include + * UNITS_PIXELS, UNITS_POINTS, UNITS_PICAS, UNITS_EM, UNITS_EX, + * UNITS_MM, UNITS_CM, UNITS_INCH, UNITS_PERCENTAGE, UNITS_ROWS. + */ + public void setWidth(float width, int unit); + + /** + * Sets the height of the object. Negative number implies unspecified size + * (terminal is free to set the size). + * + * @param height + * the height of the object. + * @param unit + * the unit used for the width. Possible values include + * UNITS_PIXELS, UNITS_POINTS, UNITS_PICAS, UNITS_EM, UNITS_EX, + * UNITS_MM, UNITS_CM, UNITS_INCH, UNITS_PERCENTAGE, UNITS_ROWS. + */ + public void setHeight(float height, int unit); + + /** + * Sets the width of the component using String presentation. + * + * String presentation is similar to what is used in Cascading Style Sheets. + * Size can be length or percentage of available size. + * + * The empty string ("") or null will unset the width and set the units to + * pixels. + * + * See <a + * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS + * specification</a> for more details. + * + * @param width + * in CSS style string representation, null or empty string to + * reset + */ + public void setWidth(String width); + + /** + * Sets the size to 100% x 100%. + */ + public void setSizeFull(); + + /** + * Clears any size settings. + */ + public void setSizeUndefined(); + +} diff --git a/src/com/vaadin/terminal/StreamResource.java b/src/com/vaadin/terminal/StreamResource.java new file mode 100644 index 0000000000..7b4c040229 --- /dev/null +++ b/src/com/vaadin/terminal/StreamResource.java @@ -0,0 +1,215 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.InputStream; + +import com.vaadin.Application; +import com.vaadin.service.FileTypeResolver; + +/** + * <code>StreamResource</code> is a resource provided to the client directly by + * the application. The strean resource is fetched from URI that is most often + * in the context of the application or window. The resource is automatically + * registered to window in creation. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class StreamResource implements ApplicationResource { + + /** + * Source stream the downloaded content is fetched from. + */ + private StreamSource streamSource = null; + + /** + * Explicit mime-type. + */ + private String MIMEType = null; + + /** + * Filename. + */ + private String filename; + + /** + * Application. + */ + private final Application application; + + /** + * Default buffer size for this stream resource. + */ + private int bufferSize = 0; + + /** + * Default cache time for this stream resource. + */ + private long cacheTime = DEFAULT_CACHETIME; + + /** + * Creates a new stream resource for downloading from stream. + * + * @param streamSource + * the source Stream. + * @param filename + * the name of the file. + * @param application + * the Application object. + */ + public StreamResource(StreamSource streamSource, String filename, + Application application) { + + this.application = application; + setFilename(filename); + setStreamSource(streamSource); + + // Register to application + application.addResource(this); + + } + + /** + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + public String getMIMEType() { + if (MIMEType != null) { + return MIMEType; + } + return FileTypeResolver.getMIMEType(filename); + } + + /** + * Sets the mime type of the resource. + * + * @param MIMEType + * the MIME type to be set. + */ + public void setMIMEType(String MIMEType) { + this.MIMEType = MIMEType; + } + + /** + * Returns the source for this <code>StreamResource</code>. StreamSource is + * queried when the resource is about to be streamed to the client. + * + * @return Source of the StreamResource. + */ + public StreamSource getStreamSource() { + return streamSource; + } + + /** + * Sets the source for this <code>StreamResource</code>. + * <code>StreamSource</code> is queried when the resource is about to be + * streamed to the client. + * + * @param streamSource + * the source to set. + */ + public void setStreamSource(StreamSource streamSource) { + this.streamSource = streamSource; + } + + /** + * Gets the filename. + * + * @return the filename. + */ + public String getFilename() { + return filename; + } + + /** + * Sets the filename. + * + * @param filename + * the filename to set. + */ + public void setFilename(String filename) { + this.filename = filename; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getApplication() + */ + public Application getApplication() { + return application; + } + + /** + * @see com.vaadin.terminal.ApplicationResource#getStream() + */ + public DownloadStream getStream() { + final StreamSource ss = getStreamSource(); + if (ss == null) { + return null; + } + final DownloadStream ds = new DownloadStream(ss.getStream(), + getMIMEType(), getFilename()); + ds.setBufferSize(getBufferSize()); + ds.setCacheTime(cacheTime); + return ds; + } + + /** + * Interface implemented by the source of a StreamResource. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface StreamSource { + + /** + * Returns new input stream that is used for reading the resource. + */ + public InputStream getStream(); + } + + /* documented in superclass */ + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the size of the download buffer used for this resource. + * + * @param bufferSize + * the size of the buffer in bytes. + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /* documented in superclass */ + public long getCacheTime() { + return cacheTime; + } + + /** + * Sets the length of cache expiration time. + * + * <p> + * This gives the adapter the possibility cache streams sent to the client. + * The caching may be made in adapter or at the client if the client + * supports caching. Zero or negavive value disbales the caching of this + * stream. + * </p> + * + * @param cacheTime + * the cache time in milliseconds. + * + */ + public void setCacheTime(long cacheTime) { + this.cacheTime = cacheTime; + } + +} diff --git a/src/com/vaadin/terminal/SystemError.java b/src/com/vaadin/terminal/SystemError.java new file mode 100644 index 0000000000..6f29970f6b --- /dev/null +++ b/src/com/vaadin/terminal/SystemError.java @@ -0,0 +1,132 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * <code>SystemError</code> is a runtime exception caused by error in system. + * The system error can be shown to the user as it implements + * <code>ErrorMessage</code> interface, but contains technical information such + * as stack trace and exception. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class SystemError extends RuntimeException implements ErrorMessage { + + /** + * The cause of the system error. The cause is stored separately as JDK 1.3 + * does not support causes natively. + */ + private Throwable cause = null; + + /** + * Constructor for SystemError with error message specified. + * + * @param message + * the Textual error description. + */ + public SystemError(String message) { + super(message); + } + + /** + * Constructor for SystemError with causing exception and error message. + * + * @param message + * the Textual error description. + * @param cause + * the throwable causing the system error. + */ + public SystemError(String message, Throwable cause) { + super(message); + this.cause = cause; + } + + /** + * Constructor for SystemError with cause. + * + * @param cause + * the throwable causing the system error. + */ + public SystemError(Throwable cause) { + this.cause = cause; + } + + /** + * @see com.vaadin.terminal.ErrorMessage#getErrorLevel() + */ + public final int getErrorLevel() { + return ErrorMessage.SYSTEMERROR; + } + + /** + * @see com.vaadin.terminal.Paintable#paint(com.vaadin.terminal.PaintTarget) + */ + public void paint(PaintTarget target) throws PaintException { + + target.startTag("error"); + target.addAttribute("level", "system"); + + // Paint the error message + final String message = getLocalizedMessage(); + if (message != null) { + target.addSection("h2", message); + } + + // Paint the exception + if (cause != null) { + target.addSection("h3", "Exception"); + final StringWriter buffer = new StringWriter(); + cause.printStackTrace(new PrintWriter(buffer)); + target.addSection("pre", buffer.toString()); + } + + target.endTag("error"); + + } + + /** + * Gets cause for the error. + * + * @return the cause. + * @see java.lang.Throwable#getCause() + */ + @Override + public Throwable getCause() { + return cause; + } + + /* Documented in super interface */ + public void addListener(RepaintRequestListener listener) { + } + + /* Documented in super interface */ + public void removeListener(RepaintRequestListener listener) { + } + + /* Documented in super interface */ + public void requestRepaint() { + } + + /* Documented in super interface */ + public void requestRepaintRequests() { + } + + public String getDebugId() { + return null; + } + + public void setDebugId(String id) { + throw new UnsupportedOperationException( + "Setting testing id for this Paintable is not implemented"); + } + +} diff --git a/src/com/vaadin/terminal/Terminal.java b/src/com/vaadin/terminal/Terminal.java new file mode 100644 index 0000000000..4494bdc144 --- /dev/null +++ b/src/com/vaadin/terminal/Terminal.java @@ -0,0 +1,65 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; + +/** + * Interface for different terminal types. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Terminal extends Serializable { + + /** + * Gets the name of the default theme. + * + * @return the Name of the terminal window. + */ + public String getDefaultTheme(); + + /** + * Gets the width of the terminal window in pixels. + * + * @return the Width of the terminal window. + */ + public int getScreenWidth(); + + /** + * Gets the height of the terminal window in pixels. + * + * @return the Height of the terminal window. + */ + public int getScreenHeight(); + + /** + * Terminal error event. + */ + public interface ErrorEvent extends Serializable{ + + /** + * Gets the contained throwable. + */ + public Throwable getThrowable(); + + } + + /** + * Terminal error listener interface. + */ + public interface ErrorListener extends Serializable{ + + /** + * Invoked when terminal error occurs. + * + * @param event + * the fired event. + */ + public void terminalError(Terminal.ErrorEvent event); + } +} diff --git a/src/com/vaadin/terminal/ThemeResource.java b/src/com/vaadin/terminal/ThemeResource.java new file mode 100644 index 0000000000..995ae0170d --- /dev/null +++ b/src/com/vaadin/terminal/ThemeResource.java @@ -0,0 +1,95 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import com.vaadin.service.FileTypeResolver; + +/** + * <code>ThemeResource</code> is a named theme dependant resource provided and + * managed by a theme. The actual resource contents are dynamically resolved to + * comply with the used theme by the terminal adapter. This is commonly used to + * provide static images, flash, java-applets, etc for the terminals. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ThemeResource implements Resource { + + /** + * Id of the terminal managed resource. + */ + private String resourceID = null; + + /** + * Creates a resource. + * + * @param resourceId + * the Id of the resource. + */ + public ThemeResource(String resourceId) { + if (resourceId == null) { + throw new NullPointerException("Resource ID must not be null"); + } + if (resourceId.length() == 0) { + throw new IllegalArgumentException("Resource ID can not be empty"); + } + if (resourceId.charAt(0) == '/') { + throw new IllegalArgumentException( + "Resource ID must be relative (can not begin with /)"); + } + + resourceID = resourceId; + } + + /** + * Tests if the given object equals this Resource. + * + * @param obj + * the object to be tested for equality. + * @return <code>true</code> if the given object equals this Icon, + * <code>false</code> if not. + * @see java.lang.Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + return obj instanceof ThemeResource + && resourceID.equals(((ThemeResource) obj).resourceID); + } + + /** + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return resourceID.hashCode(); + } + + /** + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return resourceID.toString(); + } + + /** + * Gets the resource id. + * + * @return the resource id. + */ + public String getResourceId() { + return resourceID; + } + + /** + * @see com.vaadin.terminal.Resource#getMIMEType() + */ + public String getMIMEType() { + return FileTypeResolver.getMIMEType(getResourceId()); + } +} diff --git a/src/com/vaadin/terminal/URIHandler.java b/src/com/vaadin/terminal/URIHandler.java new file mode 100644 index 0000000000..fe1746201e --- /dev/null +++ b/src/com/vaadin/terminal/URIHandler.java @@ -0,0 +1,51 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +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> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +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. + * + * @param context + * the URl. + * @param relativeUri + * the relative uri. + * @return the download stream object. + */ + public DownloadStream handleURI(URL context, String relativeUri); + + /** + * URIHandler error event. + */ + public interface ErrorEvent extends Terminal.ErrorEvent { + + /** + * Gets the source URIHandler. + * + * @return the URIHandler. + */ + public URIHandler getURIHandler(); + + } +} diff --git a/src/com/vaadin/terminal/UploadStream.java b/src/com/vaadin/terminal/UploadStream.java new file mode 100644 index 0000000000..95351e071d --- /dev/null +++ b/src/com/vaadin/terminal/UploadStream.java @@ -0,0 +1,50 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.InputStream; +import java.io.Serializable; + +/** + * Defines a variable type, that is used for passing uploaded files from + * terminal. Most often, file upload is implented using the + * {@link com.vaadin.ui.Upload Upload} component. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface UploadStream extends Serializable { + + /** + * Gets the name of the stream. + * + * @return the name of the stream. + */ + public String getStreamName(); + + /** + * Gets the input stream. + * + * @return the Input stream. + */ + public InputStream getStream(); + + /** + * Gets the input stream content type. + * + * @return the content type of the input stream. + */ + public String getContentType(); + + /** + * Gets stream content name. Stream content name usually differs from the + * actual stream name. It is used to identify the content of the stream. + * + * @return the Name of the stream content. + */ + public String getContentName(); +} diff --git a/src/com/vaadin/terminal/UserError.java b/src/com/vaadin/terminal/UserError.java new file mode 100644 index 0000000000..bcce0ed283 --- /dev/null +++ b/src/com/vaadin/terminal/UserError.java @@ -0,0 +1,154 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +/** + * <code>UserError</code> is a controlled error occurred in application. User + * errors are occur in normal usage of the application and guide the user. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class UserError implements ErrorMessage { + + /** + * Content mode, where the error contains only plain text. + */ + public static final int CONTENT_TEXT = 0; + + /** + * Content mode, where the error contains preformatted text. + */ + public static final int CONTENT_PREFORMATTED = 1; + + /** + * Formatted content mode, where the contents is XML restricted to the UIDL + * 1.0 formatting markups. + */ + public static final int CONTENT_UIDL = 2; + + /** + * Content mode. + */ + private int mode = CONTENT_TEXT; + + /** + * Message in content mode. + */ + private final String msg; + + /** + * Error level. + */ + private int level = ErrorMessage.ERROR; + + /** + * Creates a textual error message of level ERROR. + * + * @param textErrorMessage + * the text of the error message. + */ + public UserError(String textErrorMessage) { + msg = textErrorMessage; + } + + /** + * Creates a error message with level and content mode. + * + * @param message + * the error message. + * @param contentMode + * the content Mode. + * @param errorLevel + * the level of error. + */ + public UserError(String message, int contentMode, int errorLevel) { + + // Check the parameters + if (contentMode < 0 || contentMode > 2) { + throw new java.lang.IllegalArgumentException( + "Unsupported content mode: " + contentMode); + } + + msg = message; + mode = contentMode; + level = errorLevel; + } + + /* Documenten in interface */ + public int getErrorLevel() { + return level; + } + + /* Documenten in interface */ + public void addListener(RepaintRequestListener listener) { + } + + /* Documenten in interface */ + public void removeListener(RepaintRequestListener listener) { + } + + /* Documenten in interface */ + public void requestRepaint() { + } + + /* Documenten in interface */ + public void paint(PaintTarget target) throws PaintException { + + target.startTag("error"); + + // Error level + if (level >= ErrorMessage.SYSTEMERROR) { + target.addAttribute("level", "system"); + } else if (level >= ErrorMessage.CRITICAL) { + target.addAttribute("level", "critical"); + } else if (level >= ErrorMessage.ERROR) { + target.addAttribute("level", "error"); + } else if (level >= ErrorMessage.WARNING) { + target.addAttribute("level", "warning"); + } else { + target.addAttribute("level", "info"); + } + + // Paint the message + switch (mode) { + case CONTENT_TEXT: + target.addText(msg); + break; + case CONTENT_UIDL: + target.addUIDL(msg); + break; + case CONTENT_PREFORMATTED: + target.startTag("pre"); + target.addText(msg); + target.endTag("pre"); + } + + target.endTag("error"); + } + + /* Documenten in interface */ + public void requestRepaintRequests() { + } + + /* Documented in superclass */ + @Override + public String toString() { + return msg; + } + + public String getDebugId() { + return null; + } + + public void setDebugId(String id) { + throw new UnsupportedOperationException( + "Setting testing id for this Paintable is not implemented"); + } + +} diff --git a/src/com/vaadin/terminal/VariableOwner.java b/src/com/vaadin/terminal/VariableOwner.java new file mode 100644 index 0000000000..5f6fc03877 --- /dev/null +++ b/src/com/vaadin/terminal/VariableOwner.java @@ -0,0 +1,82 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal; + +import java.io.Serializable; +import java.util.Map; + +/** + * <p> + * Listener interface for UI variable changes. The user communicates with the + * application using the so-called <i>variables</i>. When the user makes a + * change using the UI the terminal trasmits the changed variables to the + * application, and the components owning those variables may then process those + * changes. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface VariableOwner extends Serializable { + + /** + * Called when one or more variables handled by the implementing class are + * changed. + * + * @param source + * the Source of the variable change. This is the origin of the + * event. For example in Web Adapter this is the request. + * @param variables + * the Mapping from variable names to new variable values. + */ + public void changeVariables(Object source, Map variables); + + /** + * <p> + * Tests if the variable owner is enabled or not. The terminal should not + * send any variable changes to disabled variable owners. + * </p> + * + * @return <code>true</code> if the variable owner is enabled, + * <code>false</code> if not + */ + public boolean isEnabled(); + + /** + * <p> + * Tests if the variable owner is in immediate mode or not. Being in + * immediate mode means that all variable changes are required to be sent + * back from the terminal immediately when they occur. + * </p> + * + * <p> + * <strong>Note:</strong> <code>VariableOwner</code> does not include a set- + * method for the immediateness property. This is because not all + * VariableOwners wish to offer the functionality. Such VariableOwners are + * never in the immediate mode, thus they always return <code>false</code> + * in {@link #isImmediate()}. + * </p> + * + * @return <code>true</code> if the component is in immediate mode, + * <code>false</code> if not. + */ + public boolean isImmediate(); + + /** + * VariableOwner error event. + */ + public interface ErrorEvent extends Terminal.ErrorEvent { + + /** + * Gets the source VariableOwner. + * + * @return the variable owner. + */ + public VariableOwner getVariableOwner(); + + } +} diff --git a/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml b/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml new file mode 100644 index 0000000000..1606dea3c3 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml @@ -0,0 +1,29 @@ +<module> + <!-- + This GWT module defines the IT Mill Toolkit DefaultWidgetSet. This is + the module you want to extend when creating an extended widget set, or + when creating a specialized widget set with a subset of the + components. + --> + <!-- + NOTE that your WidgetSet entry-point (.java) should have the same + "logical" name (a.k.a SimpleName) as the specification (.gwt.xml). + --> + <!-- + E.g: com/example/gwt/MyWidgetSet.gwt.xml should point to the + entry-point + com.example.gwt.client[.some.package].MyWidgetSet.java + --> + + <inherits name="com.google.gwt.user.User" /> + + <inherits name="com.google.gwt.http.HTTP" /> + + <inherits name="com.google.gwt.xml.XML" /> + + <inherits name="com.google.gwt.json.JSON" /> + + <source path="client" /> + + <entry-point class="com.vaadin.terminal.gwt.client.DefaultWidgetSet" /> +</module> diff --git a/src/com/vaadin/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml b/src/com/vaadin/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml new file mode 100644 index 0000000000..d8d8607213 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml @@ -0,0 +1,6 @@ +<module> + <!-- + "NoEntry" -concept deprecated, inherit DefaultWidgetSet instead. + --> + <inherits name="com.vaadin.terminal.gwt.DefaultWidgetSet" /> +</module>
\ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java b/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java new file mode 100644 index 0000000000..564a87445e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java @@ -0,0 +1,201 @@ +package com.vaadin.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JavaScriptObject; + +public class ApplicationConfiguration { + + // can only be inited once, to avoid multiple-entrypoint-problem + private static WidgetSet initedWidgetSet; + + private String id; + private String themeUri; + private String pathInfo; + private String appUri; + private JavaScriptObject versionInfo; + private String windowName; + private String communicationErrorCaption; + private String communicationErrorMessage; + private String communicationErrorUrl; + private boolean useDebugIdInDom = true; + + private static ArrayList<ApplicationConnection> unstartedApplications = new ArrayList<ApplicationConnection>(); + private static ArrayList<ApplicationConnection> runningApplications = new ArrayList<ApplicationConnection>(); + + public String getRootPanelId() { + return id; + } + + public String getApplicationUri() { + return appUri; + } + + public String getPathInfo() { + return pathInfo; + } + + public String getThemeUri() { + return themeUri; + } + + public void setAppId(String appId) { + id = appId; + } + + public void setInitialWindowName(String name) { + windowName = name; + } + + public String getInitialWindowName() { + return windowName; + } + + public JavaScriptObject getVersionInfoJSObject() { + return versionInfo; + } + + public String getCommunicationErrorCaption() { + return communicationErrorCaption; + } + + public String getCommunicationErrorMessage() { + return communicationErrorMessage; + } + + public String getCommunicationErrorUrl() { + return communicationErrorUrl; + } + + private native void loadFromDOM() + /*-{ + + var id = this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::id; + if($wnd.itmill.toolkitConfigurations && $wnd.itmill.toolkitConfigurations[id]) { + var jsobj = $wnd.itmill.toolkitConfigurations[id]; + var uri = jsobj.appUri; + if(uri[uri.length -1] != "/") { + uri = uri + "/"; + } + this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::appUri = uri; + this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::pathInfo = jsobj.pathInfo; + this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::themeUri = jsobj.themeUri; + if(jsobj.windowName) { + this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::windowName = jsobj.windowName; + } + if('useDebugIdInDom' in jsobj && typeof(jsobj.useDebugIdInDom) == "boolean") { + this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::useDebugIdInDom = jsobj.useDebugIdInDom; + } + if(jsobj.versionInfo) { + this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::versionInfo = jsobj.versionInfo; + } + if(jsobj.comErrMsg) { + this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::communicationErrorCaption = jsobj.comErrMsg.caption; + this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::communicationErrorMessage = jsobj.comErrMsg.message; + this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::communicationErrorUrl = jsobj.comErrMsg.url; + } + + } else { + $wnd.alert("Toolkit app failed to initialize: " + this.id); + } + + }-*/; + + /** + * Inits the ApplicationConfiguration by reading the DOM and instantiating + * ApplicationConenctions accordingly. Call {@link #startNextApplication()} + * to actually start the applications. + * + * @param widgetset + * the widgetset that is running the apps + */ + public static void initConfigurations(WidgetSet widgetset) { + String wsname = widgetset.getClass().getName(); + String module = GWT.getModuleName(); + int lastdot = module.lastIndexOf("."); + String base = module.substring(0, lastdot); + String simpleName = module.substring(lastdot + 1); + if (!wsname.startsWith(base) || !wsname.endsWith(simpleName)) { + // WidgetSet module name does not match implementation name; + // probably inherited WidgetSet with entry-point. Skip. + GWT.log("Ignored init for " + wsname + " when starting " + module, + null); + return; + } + + if (initedWidgetSet != null) { + // Something went wrong: multiple widgetsets inited + String msg = "Tried to init " + widgetset.getClass().getName() + + ", but " + initedWidgetSet.getClass().getName() + + " is already inited."; + System.err.println(msg); + throw new IllegalStateException(msg); + } + initedWidgetSet = widgetset; + ArrayList<String> appIds = new ArrayList<String>(); + loadAppIdListFromDOM(appIds); + + for (Iterator<String> it = appIds.iterator(); it.hasNext();) { + String appId = it.next(); + ApplicationConfiguration appConf = getConfigFromDOM(appId); + ApplicationConnection a = new ApplicationConnection(widgetset, + appConf); + unstartedApplications.add(a); + } + } + + /** + * Starts the next unstarted application. The WidgetSet should call this + * once to start the first application; after that, each application should + * call this once it has started. This ensures that the applications are + * started synchronously, which is neccessary to avoid session-id problems. + * + * @return true if an unstarted application was found + */ + public static boolean startNextApplication() { + if (unstartedApplications.size() > 0) { + ApplicationConnection a = unstartedApplications.remove(0); + a.start(); + runningApplications.add(a); + return true; + } else { + return false; + } + } + + public static List<ApplicationConnection> getRunningApplications() { + return runningApplications; + } + + private native static void loadAppIdListFromDOM(ArrayList<String> list) + /*-{ + var j; + for(j in $wnd.itmill.toolkitConfigurations) { + list.@java.util.Collection::add(Ljava/lang/Object;)(j); + } + }-*/; + + public static ApplicationConfiguration getConfigFromDOM(String appId) { + ApplicationConfiguration conf = new ApplicationConfiguration(); + conf.setAppId(appId); + conf.loadFromDOM(); + return conf; + } + + public native String getServletVersion() + /*-{ + return this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::versionInfo.toolkitVersion; + }-*/; + + public native String getApplicationVersion() + /*-{ + return this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::versionInfo.applicationVersion; + }-*/; + + public boolean useDebugIdInDOM() { + return useDebugIdInDom; + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java new file mode 100755 index 0000000000..db13b1f205 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java @@ -0,0 +1,1633 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Vector; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.http.client.Request; +import com.google.gwt.http.client.RequestBuilder; +import com.google.gwt.http.client.RequestCallback; +import com.google.gwt.http.client.RequestException; +import com.google.gwt.http.client.Response; +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONParser; +import com.google.gwt.json.client.JSONString; +import com.google.gwt.json.client.JSONValue; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +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.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; +import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize; +import com.vaadin.terminal.gwt.client.RenderInformation.Size; +import com.vaadin.terminal.gwt.client.ui.Field; +import com.vaadin.terminal.gwt.client.ui.IContextMenu; +import com.vaadin.terminal.gwt.client.ui.INotification; +import com.vaadin.terminal.gwt.client.ui.IView; +import com.vaadin.terminal.gwt.client.ui.INotification.HideEvent; + +/** + * Entry point classes define <code>onModuleLoad()</code>. + */ +public class ApplicationConnection { + private static final String MODIFIED_CLASSNAME = "i-modified"; + + private static final String REQUIRED_CLASSNAME_EXT = "-required"; + + private static final String ERROR_CLASSNAME_EXT = "-error"; + + public static final String VAR_RECORD_SEPARATOR = "\u001e"; + + public static final String VAR_FIELD_SEPARATOR = "\u001f"; + + public static final String VAR_BURST_SEPARATOR = "\u001d"; + + public static final String VAR_ARRAYITEM_SEPARATOR = "\u001c"; + + public static final String UIDL_SECURITY_HEADER = "com.itmill.seckey"; + + public static final String PARAM_UNLOADBURST = "onunloadburst"; + + private static String uidl_security_key = "init"; + + private final HashMap<String, String> resourcesMap = new HashMap<String, String>(); + + private static Console console; + + private final Vector<String> pendingVariables = new Vector<String>(); + + private final ComponentDetailMap idToPaintableDetail = ComponentDetailMap + .create(); + + private final WidgetSet widgetSet; + + private IContextMenu contextMenu = null; + + private Timer loadTimer; + private Timer loadTimer2; + private Timer loadTimer3; + private Element loadElement; + + private final IView view; + + private boolean applicationRunning = false; + + private int activeRequests = 0; + + /** Parameters for this application connection loaded from the web-page */ + private final ApplicationConfiguration configuration; + + /** List of pending variable change bursts that must be submitted in order */ + private final Vector<Vector<String>> pendingVariableBursts = new Vector<Vector<String>>(); + + /** Timer for automatic refirect to SessionExpiredURL */ + private Timer redirectTimer; + + /** redirectTimer scheduling interval in seconds */ + private int sessionExpirationInterval; + + private ArrayList<Paintable> relativeSizeChanges = new ArrayList<Paintable>();; + private ArrayList<Paintable> componentCaptionSizeChanges = new ArrayList<Paintable>();; + + private Date requestStartTime; + + private boolean validatingLayouts = false; + + private Set<Paintable> zeroWidthComponents = null; + + private Set<Paintable> zeroHeightComponents = null; + + public ApplicationConnection(WidgetSet widgetSet, + ApplicationConfiguration cnf) { + this.widgetSet = widgetSet; + configuration = cnf; + windowName = configuration.getInitialWindowName(); + if (isDebugMode()) { + console = new IDebugConsole(this, cnf, !isQuietDebugMode()); + } else { + console = new NullConsole(); + } + + ComponentLocator componentLocator = new ComponentLocator(this); + + String appRootPanelName = cnf.getRootPanelId(); + // remove the end (window name) of autogenarated rootpanel id + appRootPanelName = appRootPanelName.replaceFirst("-\\d+$", ""); + + initializeTestingToolsHooks(componentLocator, appRootPanelName); + + initializeClientHooks(); + + view = new IView(cnf.getRootPanelId()); + showLoadingIndicator(); + + } + + /** + * Starts this application. Don't call this method directly - it's called by + * {@link ApplicationConfiguration#startNextApplication()}, which should be + * called once this application has started (first response received) or + * failed to start. This ensures that the applications are started in order, + * to avoid session-id problems. + */ + void start() { + makeUidlRequest("", true, false, false); + } + + private native void initializeTestingToolsHooks( + ComponentLocator componentLocator, String TTAppId) + /*-{ + var ap = this; + var client = {}; + client.isActive = function() { + return ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::hasActiveRequest()(); + } + var vi = ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::getVersionInfo()(); + if (vi) { + client.getVersionInfo = function() { + return vi; + } + } + + client.getElementByPath = function(id) { + return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id); + } + client.getPathForElement = function(element) { + return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element); + } + + if(!$wnd.itmill.clients) { + $wnd.itmill.clients = {}; + } + + $wnd.itmill.clients[TTAppId] = client; + }-*/; + + /** + * Helper for tt initialization + */ + @SuppressWarnings("unused") + private JavaScriptObject getVersionInfo() { + return configuration.getVersionInfoJSObject(); + } + + /** + * Publishes a JavaScript API for mash-up applications. + * <ul> + * <li><code>itmill.forceSync()</code> sends pending variable changes, in + * effect synchronizing the server and client state. This is done for all + * applications on host page.</li> + * </ul> + * + * TODO make this multi-app aware + */ + private native void initializeClientHooks() + /*-{ + var app = this; + var oldSync; + if($wnd.itmill.forceSync) { + oldSync = $wnd.itmill.forceSync; + } + $wnd.itmill.forceSync = function() { + if(oldSync) { + oldSync(); + } + app.@com.vaadin.terminal.gwt.client.ApplicationConnection::sendPendingVariableChanges()(); + } + var oldForceLayout; + if($wnd.itmill.forceLayout) { + oldForceLayout = $wnd.itmill.forceLayout; + } + $wnd.itmill.forceLayout = function() { + if(oldForceLayout) { + oldForceLayout(); + } + app.@com.vaadin.terminal.gwt.client.ApplicationConnection::forceLayout()(); + } + }-*/; + + public static Console getConsole() { + return console; + } + + /** + * Checks if client side is in debug mode. Practically this is invoked by + * adding ?debug parameter to URI. + * + * @return true if client side is currently been debugged + */ + public native static boolean isDebugMode() + /*-{ + if($wnd.itmill.debug) { + var parameters = $wnd.location.search; + var re = /debug[^\/]*$/; + return re.test(parameters); + } else { + return false; + } + }-*/; + + private native static boolean isQuietDebugMode() + /*-{ + var uri = $wnd.location; + var re = /debug=q[^\/]*$/; + return re.test(uri); + }-*/; + + public String getAppUri() { + return configuration.getApplicationUri(); + }; + + public boolean hasActiveRequest() { + return (activeRequests > 0); + } + + private void makeUidlRequest(String requestData, boolean repaintAll, + boolean forceSync, boolean analyzeLayouts) { + startRequest(); + + // Security: double cookie submission pattern + requestData = uidl_security_key + VAR_BURST_SEPARATOR + requestData; + + console.log("Making UIDL Request with params: " + requestData); + String uri = getAppUri() + "UIDL" + configuration.getPathInfo(); + if (repaintAll) { + uri += "?repaintAll=1"; + if (analyzeLayouts) { + uri += "&analyzeLayouts=1"; + } + } + if (windowName != null && windowName.length() > 0) { + uri += (repaintAll ? "&" : "?") + "windowName=" + windowName; + } + + if (!forceSync) { + final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, + uri); + // TODO enable timeout + // rb.setTimeoutMillis(timeoutMillis); + rb.setHeader("Content-Type", "text/plain;charset=utf-8"); + try { + rb.sendRequest(requestData, new RequestCallback() { + public void onError(Request request, Throwable exception) { + showCommunicationError(exception.getMessage()); + endRequest(); + if (!applicationRunning) { + // start failed, let's try to start the next app + ApplicationConfiguration.startNextApplication(); + } + } + + public void onResponseReceived(Request request, + Response response) { + console.log("Server visit took " + + String.valueOf((new Date()).getTime() + - requestStartTime.getTime()) + "ms"); + + switch (response.getStatusCode()) { + case 0: + showCommunicationError("Invalid status code 0 (server down?)"); + return; + // TODO could add more cases + } + if ("init".equals(uidl_security_key)) { + // Read security key + String key = response + .getHeader(UIDL_SECURITY_HEADER); + if (null != key) { + uidl_security_key = key; + } + } + if (applicationRunning) { + handleReceivedJSONMessage(response); + } else { + applicationRunning = true; + handleWhenCSSLoaded(response); + ApplicationConfiguration.startNextApplication(); + } + } + + int cssWaits = 0; + static final int MAX_CSS_WAITS = 20; + + private void handleWhenCSSLoaded(final Response response) { + int heightOfLoadElement = DOM.getElementPropertyInt( + loadElement, "offsetHeight"); + if (heightOfLoadElement == 0 + && cssWaits < MAX_CSS_WAITS) { + (new Timer() { + @Override + public void run() { + handleWhenCSSLoaded(response); + } + }).schedule(50); + console + .log("Assuming CSS loading is not complete, " + + "postponing render phase. " + + "(.i-loading-indicator height == 0)"); + cssWaits++; + } else { + handleReceivedJSONMessage(response); + if (cssWaits >= MAX_CSS_WAITS) { + console + .error("CSS files may have not loaded properly."); + } + } + } + + }); + + } catch (final RequestException e) { + ClientExceptionHandler.displayError(e); + endRequest(); + } + } else { + // Synchronized call, discarded response + + syncSendForce(((HTTPRequestImpl) GWT.create(HTTPRequestImpl.class)) + .createXmlHTTPRequest(), uri + "&" + PARAM_UNLOADBURST + + "=1", requestData); + } + } + + /** + * Shows the communication error notification. The 'details' only go to the + * console for now. + * + * @param details + * Optional details for debugging. + */ + private void showCommunicationError(String details) { + console.error("Communication error: " + details); + String html = ""; + if (configuration.getCommunicationErrorCaption() != null) { + html += "<h1>" + configuration.getCommunicationErrorCaption() + + "</h1>"; + } + if (configuration.getCommunicationErrorMessage() != null) { + html += "<p>" + configuration.getCommunicationErrorMessage() + + "</p>"; + } + if (html.length() > 0) { + INotification n = new INotification(1000 * 60 * 45); + n.addEventListener(new NotificationRedirect(configuration + .getCommunicationErrorUrl())); + n + .show(html, INotification.CENTERED_TOP, + INotification.STYLE_SYSTEM); + } else { + redirect(configuration.getCommunicationErrorUrl()); + } + } + + 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 + } + + }-*/; + + private void startRequest() { + activeRequests++; + requestStartTime = new Date(); + // show initial throbber + if (loadTimer == null) { + loadTimer = new Timer() { + @Override + public void run() { + /* + * IE7 does not properly cancel the event with + * loadTimer.cancel() so we have to check that we really + * should make it visible + */ + if (loadTimer != null) { + showLoadingIndicator(); + } + + } + }; + // First one kicks in at 300ms + } + loadTimer.schedule(300); + } + + private void endRequest() { + if (applicationRunning) { + checkForPendingVariableBursts(); + } + activeRequests--; + // deferring to avoid flickering + DeferredCommand.addCommand(new Command() { + public void execute() { + if (activeRequests == 0) { + hideLoadingIndicator(); + } + } + }); + } + + /** + * This method is called after applying uidl change set to application. + * + * It will clean current and queued variable change sets. And send next + * change set if it exists. + */ + private void checkForPendingVariableBursts() { + cleanVariableBurst(pendingVariables); + if (pendingVariableBursts.size() > 0) { + for (Iterator<Vector<String>> iterator = pendingVariableBursts + .iterator(); iterator.hasNext();) { + cleanVariableBurst(iterator.next()); + } + Vector<String> nextBurst = pendingVariableBursts.firstElement(); + pendingVariableBursts.remove(0); + buildAndSendVariableBurst(nextBurst, false); + } + } + + /** + * Cleans given queue of variable changes of such changes that came from + * components that do not exist anymore. + * + * @param variableBurst + */ + private void cleanVariableBurst(Vector<String> variableBurst) { + for (int i = 1; i < variableBurst.size(); i += 2) { + String id = variableBurst.get(i); + id = id.substring(0, id.indexOf(VAR_FIELD_SEPARATOR)); + if (!idToPaintableDetail.containsKey(id)) { + // variable owner does not exist anymore + variableBurst.remove(i - 1); + variableBurst.remove(i - 1); + i -= 2; + ApplicationConnection.getConsole().log( + "Removed variable from removed component: " + id); + } + } + } + + private void showLoadingIndicator() { + // show initial throbber + if (loadElement == null) { + loadElement = DOM.createDiv(); + DOM.setStyleAttribute(loadElement, "position", "absolute"); + DOM.appendChild(view.getElement(), loadElement); + ApplicationConnection.getConsole().log("inserting load indicator"); + } + DOM.setElementProperty(loadElement, "className", "i-loading-indicator"); + DOM.setStyleAttribute(loadElement, "display", "block"); + // Initialize other timers + loadTimer2 = new Timer() { + @Override + public void run() { + DOM.setElementProperty(loadElement, "className", + "i-loading-indicator-delay"); + } + }; + // Second one kicks in at 1500ms from request start + loadTimer2.schedule(1200); + + loadTimer3 = new Timer() { + @Override + public void run() { + DOM.setElementProperty(loadElement, "className", + "i-loading-indicator-wait"); + } + }; + // Third one kicks in at 5000ms from request start + loadTimer3.schedule(4700); + } + + private void hideLoadingIndicator() { + if (loadTimer != null) { + loadTimer.cancel(); + if (loadTimer2 != null) { + loadTimer2.cancel(); + loadTimer3.cancel(); + } + loadTimer = null; + } + if (loadElement != null) { + DOM.setStyleAttribute(loadElement, "display", "none"); + } + } + + private void handleReceivedJSONMessage(Response response) { + final Date start = new Date(); + String jsonText = response.getText(); + // for(;;);[realjson] + jsonText = jsonText.substring(9, jsonText.length() - 1); + JSONValue json; + try { + json = JSONParser.parse(jsonText); + } catch (final com.google.gwt.json.client.JSONException e) { + endRequest(); + showCommunicationError(e.getMessage() + " - Original JSON-text:"); + console.log(jsonText); + return; + } + // Handle redirect + final JSONObject redirect = (JSONObject) ((JSONObject) json) + .get("redirect"); + if (redirect != null) { + final JSONString url = (JSONString) redirect.get("url"); + if (url != null) { + console.log("redirecting to " + url.stringValue()); + redirect(url.stringValue()); + return; + } + } + + // Store resources + final JSONObject resources = (JSONObject) ((JSONObject) json) + .get("resources"); + for (final Iterator<String> i = resources.keySet().iterator(); i + .hasNext();) { + final String key = i.next(); + resourcesMap.put(key, ((JSONString) resources.get(key)) + .stringValue()); + } + + // Store locale data + if (((JSONObject) json).containsKey("locales")) { + final JSONArray l = (JSONArray) ((JSONObject) json).get("locales"); + for (int i = 0; i < l.size(); i++) { + LocaleService.addLocale((JSONObject) l.get(i)); + } + } + + JSONObject meta = null; + if (((JSONObject) json).containsKey("meta")) { + meta = ((JSONObject) json).get("meta").isObject(); + if (meta.containsKey("repaintAll")) { + view.clear(); + idToPaintableDetail.clear(); + if (meta.containsKey("invalidLayouts")) { + validatingLayouts = true; + zeroWidthComponents = new HashSet<Paintable>(); + zeroHeightComponents = new HashSet<Paintable>(); + } + } + if (meta.containsKey("timedRedirect")) { + final JSONObject timedRedirect = meta.get("timedRedirect") + .isObject(); + redirectTimer = new Timer() { + @Override + public void run() { + redirect(timedRedirect.get("url").isString() + .stringValue()); + } + }; + sessionExpirationInterval = Integer.parseInt(timedRedirect.get( + "interval").toString()); + } + } + if (redirectTimer != null) { + redirectTimer.schedule(1000 * sessionExpirationInterval); + } + // Process changes + final JSONArray changes = (JSONArray) ((JSONObject) json) + .get("changes"); + + Vector<Paintable> updatedWidgets = new Vector<Paintable>(); + relativeSizeChanges.clear(); + componentCaptionSizeChanges.clear(); + + for (int i = 0; i < changes.size(); i++) { + try { + final UIDL change = new UIDL((JSONArray) changes.get(i)); + try { + console.dirUIDL(change); + } catch (final Exception e) { + ClientExceptionHandler.displayError(e); + // TODO: dir doesn't work in any browser although it should + // work (works in hosted mode) + // it partially did at some part but now broken. + } + final UIDL uidl = change.getChildUIDL(0); + // TODO optimize + final Paintable paintable = getPaintable(uidl.getId()); + if (paintable != null) { + paintable.updateFromUIDL(uidl, this); + // paintable may have changed during render to another + // implementation, use the new one for updated widgets map + updatedWidgets.add(idToPaintableDetail.get(uidl.getId()) + .getComponent()); + } else { + if (!uidl.getTag().equals("window")) { + ClientExceptionHandler + .displayError("Received update for " + + uidl.getTag() + + ", but there is no such paintable (" + + uidl.getId() + ") rendered."); + } else { + view.updateFromUIDL(uidl, this); + } + } + } catch (final Throwable e) { + ClientExceptionHandler.displayError(e); + } + } + + // Check which widgets' size has been updated + Set<Paintable> sizeUpdatedWidgets = new HashSet<Paintable>(); + + updatedWidgets.addAll(relativeSizeChanges); + sizeUpdatedWidgets.addAll(componentCaptionSizeChanges); + + for (Paintable paintable : updatedWidgets) { + ComponentDetail detail = idToPaintableDetail.get(getPid(paintable)); + Widget widget = (Widget) paintable; + Size oldSize = detail.getOffsetSize(); + Size newSize = new Size(widget.getOffsetWidth(), widget + .getOffsetHeight()); + + if (oldSize == null || !oldSize.equals(newSize)) { + sizeUpdatedWidgets.add(paintable); + detail.setOffsetSize(newSize); + } + + } + + Util.componentSizeUpdated(sizeUpdatedWidgets); + + if (meta != null) { + if (meta.containsKey("appError")) { + JSONObject error = meta.get("appError").isObject(); + JSONValue val = error.get("caption"); + String html = ""; + if (val.isString() != null) { + html += "<h1>" + val.isString().stringValue() + "</h1>"; + } + val = error.get("message"); + if (val.isString() != null) { + html += "<p>" + val.isString().stringValue() + "</p>"; + } + val = error.get("url"); + String url = null; + if (val.isString() != null) { + url = val.isString().stringValue(); + } + + if (html.length() != 0) { + /* 45 min */ + INotification n = new INotification(1000 * 60 * 45); + n.addEventListener(new NotificationRedirect(url)); + n.show(html, INotification.CENTERED_TOP, + INotification.STYLE_SYSTEM); + } else { + redirect(url); + } + applicationRunning = false; + } + if (validatingLayouts) { + getConsole().printLayoutProblems( + meta.get("invalidLayouts").isArray(), this, + zeroHeightComponents, zeroWidthComponents); + zeroHeightComponents = null; + zeroWidthComponents = null; + validatingLayouts = false; + + } + } + + final long prosessingTime = (new Date().getTime()) - start.getTime(); + console.log(" Processing time was " + String.valueOf(prosessingTime) + + "ms for " + jsonText.length() + " characters of JSON"); + console.log("Referenced paintables: " + idToPaintableDetail.size()); + + endRequest(); + } + + /** + * This method assures that all pending variable changes are sent to server. + * Method uses synchronized xmlhttprequest and does not return before the + * changes are sent. No UIDL updates are processed and thut UI is left in + * inconsistent state. This method should be called only when closing + * windows - normally sendPendingVariableChanges() should be used. + */ + public void sendPendingVariableChangesSync() { + if (applicationRunning) { + pendingVariableBursts.add(pendingVariables); + Vector<String> nextBurst = pendingVariableBursts.firstElement(); + pendingVariableBursts.remove(0); + buildAndSendVariableBurst(nextBurst, true); + } + } + + // Redirect browser, null reloads current page + private static native void redirect(String url) + /*-{ + if (url) { + $wnd.location = url; + } else { + $wnd.location.reload(false); + } + }-*/; + + public void registerPaintable(String id, Paintable paintable) { + ComponentDetail componentDetail = new ComponentDetail(); + componentDetail.setComponent(paintable); + idToPaintableDetail.put(id, componentDetail); + setPid(((Widget) paintable).getElement(), id); + } + + private native void setPid(Element el, String pid) + /*-{ + el.tkPid = pid; + }-*/; + + public String getPid(Paintable paintable) { + return getPid(((Widget) paintable).getElement()); + } + + public native String getPid(Element el) + /*-{ + return el.tkPid; + }-*/; + + public Element getElementByPid(String pid) { + return ((Widget) getPaintable(pid)).getElement(); + } + + public void unregisterPaintable(Paintable p) { + if (p == null) { + ApplicationConnection.getConsole().error( + "WARN: Trying to unregister null paintable"); + return; + } + String id = getPid(p); + idToPaintableDetail.remove(id); + if (p instanceof HasWidgets) { + unregisterChildPaintables((HasWidgets) p); + } + } + + public void unregisterChildPaintables(HasWidgets container) { + final Iterator<Widget> it = container.iterator(); + while (it.hasNext()) { + final Widget w = it.next(); + if (w instanceof Paintable) { + unregisterPaintable((Paintable) w); + } else if (w instanceof HasWidgets) { + unregisterChildPaintables((HasWidgets) w); + } + } + } + + /** + * Returns Paintable element by its id + * + * @param id + * Paintable ID + */ + public Paintable getPaintable(String id) { + ComponentDetail componentDetail = idToPaintableDetail.get(id); + if (componentDetail == null) { + return null; + } else { + return componentDetail.getComponent(); + } + } + + private void addVariableToQueue(String paintableId, String variableName, + String encodedValue, boolean immediate, char type) { + final String id = paintableId + VAR_FIELD_SEPARATOR + variableName + + VAR_FIELD_SEPARATOR + type; + for (int i = 1; i < pendingVariables.size(); i += 2) { + if ((pendingVariables.get(i)).equals(id)) { + pendingVariables.remove(i - 1); + pendingVariables.remove(i - 1); + break; + } + } + pendingVariables.add(encodedValue); + pendingVariables.add(id); + if (immediate) { + sendPendingVariableChanges(); + } + } + + /** + * This method sends currently queued variable changes to server. It is + * called when immediate variable update must happen. + * + * To ensure correct order for variable changes (due servers multithreading + * or network), we always wait for active request to be handler before + * sending a new one. If there is an active request, we will put varible + * "burst" to queue that will be purged after current request is handled. + * + */ + @SuppressWarnings("unchecked") + public void sendPendingVariableChanges() { + if (applicationRunning) { + if (hasActiveRequest()) { + // skip empty queues if there are pending bursts to be sent + if (pendingVariables.size() > 0 + || pendingVariableBursts.size() == 0) { + Vector<String> burst = (Vector<String>) pendingVariables + .clone(); + pendingVariableBursts.add(burst); + pendingVariables.clear(); + } + } else { + buildAndSendVariableBurst(pendingVariables, false); + } + } + } + + /** + * Build the variable burst and send it to server. + * + * When sync is forced, we also force sending of all pending variable-bursts + * at the same time. This is ok as we can assume that DOM will newer be + * updated after this. + * + * @param pendingVariables + * Vector of variablechanges to send + * @param forceSync + * Should we use synchronous request? + */ + private void buildAndSendVariableBurst(Vector<String> pendingVariables, + boolean forceSync) { + final StringBuffer req = new StringBuffer(); + + while (!pendingVariables.isEmpty()) { + for (int i = 0; i < pendingVariables.size(); i++) { + if (i > 0) { + if (i % 2 == 0) { + req.append(VAR_RECORD_SEPARATOR); + } else { + req.append(VAR_FIELD_SEPARATOR); + } + } + req.append(pendingVariables.get(i)); + } + + pendingVariables.clear(); + // Append all the busts to this synchronous request + if (forceSync && !pendingVariableBursts.isEmpty()) { + pendingVariables = pendingVariableBursts.firstElement(); + pendingVariableBursts.remove(0); + req.append(VAR_BURST_SEPARATOR); + } + } + makeUidlRequest(req.toString(), false, forceSync, false); + } + + public void updateVariable(String paintableId, String variableName, + Paintable newValue, boolean immediate) { + String pid = (newValue != null) ? getPid(newValue) : null; + addVariableToQueue(paintableId, variableName, pid, immediate, 'p'); + } + + public void updateVariable(String paintableId, String variableName, + String newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, newValue, immediate, 's'); + } + + public void updateVariable(String paintableId, String variableName, + int newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, "" + newValue, immediate, + 'i'); + } + + public void updateVariable(String paintableId, String variableName, + long newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, "" + newValue, immediate, + 'l'); + } + + public void updateVariable(String paintableId, String variableName, + float newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, "" + newValue, immediate, + 'f'); + } + + public void updateVariable(String paintableId, String variableName, + double newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, "" + newValue, immediate, + 'd'); + } + + public void updateVariable(String paintableId, String variableName, + boolean newValue, boolean immediate) { + addVariableToQueue(paintableId, variableName, newValue ? "true" + : "false", immediate, 'b'); + } + + public void updateVariable(String paintableId, String variableName, + Object[] values, boolean immediate) { + final StringBuffer buf = new StringBuffer(); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + buf.append(VAR_ARRAYITEM_SEPARATOR); + } + buf.append(values[i].toString()); + } + addVariableToQueue(paintableId, variableName, buf.toString(), + immediate, 'a'); + } + + /** + * Update generic component features. + * + * <h2>Selecting correct implementation</h2> + * + * <p> + * The implementation of a component depends on many properties, including + * styles, component features, etc. Sometimes the user changes those + * properties after the component has been created. Calling this method in + * the beginning of your updateFromUIDL -method automatically replaces your + * component with more appropriate if the requested implementation changes. + * </p> + * + * <h2>Caption, icon, error messages and description</h2> + * + * <p> + * Component can delegate management of caption, icon, error messages and + * description to parent layout. This is optional an should be decided by + * component author + * </p> + * + * <h2>Component visibility and disabling</h2> + * + * This method will manage component visibility automatically and if + * component is an instanceof FocusWidget, also handle component disabling + * when needed. + * + * @param component + * Widget to be updated, expected to implement an instance of + * Paintable + * @param uidl + * UIDL to be painted + * @param manageCaption + * True if you want to delegate caption, icon, description and + * error message management to parent. + * + * @return Returns true iff no further painting is needed by caller + */ + public boolean updateComponent(Widget component, UIDL uidl, + boolean manageCaption) { + String pid = getPid(component.getElement()); + if (pid == null) { + getConsole().error( + "Trying to update an unregistered component: " + + Util.getSimpleName(component)); + return true; + } + + ComponentDetail componentDetail = idToPaintableDetail.get(pid); + + if (componentDetail == null) { + getConsole().error( + "ComponentDetail not found for " + + Util.getSimpleName(component) + " with PID " + + pid + ". This should not happen."); + return true; + } + + // If the server request that a cached instance should be used, do + // nothing + if (uidl.getBooleanAttribute("cached")) { + return true; + } + + // Visibility + boolean visible = !uidl.getBooleanAttribute("invisible"); + boolean wasVisible = component.isVisible(); + component.setVisible(visible); + if (wasVisible != visible) { + // Changed invisibile <-> visible + if (wasVisible && manageCaption) { + // Must hide caption when component is hidden + final Container parent = Util.getLayout(component); + if (parent != null) { + parent.updateCaption((Paintable) component, uidl); + } + + } + } + + if (!visible) { + // component is invisible, delete old size to notify parent, if + // later make visible + componentDetail.setOffsetSize(null); + return true; + } + + // Switch to correct implementation if needed + if (!widgetSet.isCorrectImplementation(component, uidl)) { + final Container parent = Util.getLayout(component); + if (parent != null) { + final Widget w = (Widget) widgetSet.createWidget(uidl); + parent.replaceChildComponent(component, w); + unregisterPaintable((Paintable) component); + registerPaintable(uidl.getId(), (Paintable) w); + ((Paintable) w).updateFromUIDL(uidl, this); + return true; + } + } + + boolean enabled = !uidl.getBooleanAttribute("disabled"); + if (component instanceof FocusWidget) { + FocusWidget fw = (FocusWidget) component; + fw.setEnabled(enabled); + if (uidl.hasAttribute("tabindex")) { + fw.setTabIndex(uidl.getIntAttribute("tabindex")); + } + } + + StringBuffer styleBuf = new StringBuffer(); + final String primaryName = component.getStylePrimaryName(); + styleBuf.append(primaryName); + + // first disabling and read-only status + if (!enabled) { + styleBuf.append(" "); + styleBuf.append("i-disabled"); + } + if (uidl.getBooleanAttribute("readonly")) { + styleBuf.append(" "); + styleBuf.append("i-readonly"); + } + + // add additional styles as css classes, prefixed with component default + // stylename + if (uidl.hasAttribute("style")) { + final String[] styles = uidl.getStringAttribute("style").split(" "); + for (int i = 0; i < styles.length; i++) { + styleBuf.append(" "); + styleBuf.append(primaryName); + styleBuf.append("-"); + styleBuf.append(styles[i]); + styleBuf.append(" "); + styleBuf.append(styles[i]); + } + } + + // add modified classname to Fields + if (uidl.hasAttribute("modified") && component instanceof Field) { + styleBuf.append(" "); + styleBuf.append(MODIFIED_CLASSNAME); + } + + TooltipInfo tooltipInfo = componentDetail.getTooltipInfo(); + if (uidl.hasAttribute("description")) { + tooltipInfo.setTitle(uidl.getStringAttribute("description")); + } else { + tooltipInfo.setTitle(null); + } + + // add error classname to components w/ error + if (uidl.hasAttribute("error")) { + styleBuf.append(" "); + styleBuf.append(primaryName); + styleBuf.append(ERROR_CLASSNAME_EXT); + + tooltipInfo.setErrorUidl(uidl.getErrors()); + } else { + tooltipInfo.setErrorUidl(null); + } + + // add required style to required components + if (uidl.hasAttribute("required")) { + styleBuf.append(" "); + styleBuf.append(primaryName); + styleBuf.append(REQUIRED_CLASSNAME_EXT); + } + + // Styles + disabled & readonly + component.setStyleName(styleBuf.toString()); + + // Set captions + if (manageCaption) { + final Container parent = Util.getLayout(component); + if (parent != null) { + parent.updateCaption((Paintable) component, uidl); + } + } + + if (configuration.useDebugIdInDOM() && uidl.getId().startsWith("PID_S")) { + DOM.setElementProperty(component.getElement(), "id", uidl.getId() + .substring(5)); + } + + /* + * updateComponentSize need to be after caption update so caption can be + * taken into account + */ + + updateComponentSize(componentDetail, uidl); + + return false; + } + + private void updateComponentSize(ComponentDetail cd, UIDL uidl) { + String w = uidl.hasAttribute("width") ? uidl + .getStringAttribute("width") : ""; + + String h = uidl.hasAttribute("height") ? uidl + .getStringAttribute("height") : ""; + + float relativeWidth = Util.parseRelativeSize(w); + float relativeHeight = Util.parseRelativeSize(h); + + // First update maps so they are correct in the setHeight/setWidth calls + if (relativeHeight >= 0.0 || relativeWidth >= 0.0) { + // One or both is relative + FloatSize relativeSize = new FloatSize(relativeWidth, + relativeHeight); + if (cd.getRelativeSize() == null && cd.getOffsetSize() != null) { + // The component has changed from absolute size to relative size + relativeSizeChanges.add(cd.getComponent()); + } + cd.setRelativeSize(relativeSize); + } else if (relativeHeight < 0.0 && relativeWidth < 0.0) { + if (cd.getRelativeSize() != null) { + // The component has changed from relative size to absolute size + relativeSizeChanges.add(cd.getComponent()); + } + cd.setRelativeSize(null); + } + + Widget component = (Widget) cd.getComponent(); + // Set absolute sizes + if (relativeHeight < 0.0) { + component.setHeight(h); + } + if (relativeWidth < 0.0) { + component.setWidth(w); + } + + // Set relative sizes + if (relativeHeight >= 0.0 || relativeWidth >= 0.0) { + // One or both is relative + handleComponentRelativeSize(cd); + } + + } + + /** + * Traverses recursively child widgets until ContainerResizedListener child + * widget is found. They will delegate it further if needed. + * + * @param container + */ + private boolean runningLayout = false; + + public void runDescendentsLayout(HasWidgets container) { + if (runningLayout) { + // getConsole().log( + // "Already running descendents layout. Not running again for " + // + Util.getSimpleName(container)); + return; + } + runningLayout = true; + internalRunDescendentsLayout(container); + runningLayout = false; + } + + /** + * This will cause re-layouting of all components. Mainly used for + * development. Published to JavaScript. + */ + public void forceLayout() { + Set<Paintable> set = new HashSet<Paintable>(); + for (ComponentDetail cd : idToPaintableDetail.values()) { + set.add(cd.getComponent()); + } + Util.componentSizeUpdated(set); + } + + private void internalRunDescendentsLayout(HasWidgets container) { + // getConsole().log( + // "runDescendentsLayout(" + Util.getSimpleName(container) + ")"); + final Iterator<Widget> childWidgets = container.iterator(); + while (childWidgets.hasNext()) { + final Widget child = childWidgets.next(); + + if (child instanceof Paintable) { + + if (handleComponentRelativeSize(child)) { + /* + * Only need to propagate event if "child" has a relative + * size + */ + + if (child instanceof ContainerResizedListener) { + ((ContainerResizedListener) child).iLayout(); + } + + if (child instanceof HasWidgets) { + final HasWidgets childContainer = (HasWidgets) child; + internalRunDescendentsLayout(childContainer); + } + } + } else if (child instanceof HasWidgets) { + // propagate over non Paintable HasWidgets + internalRunDescendentsLayout((HasWidgets) child); + } + + } + } + + /** + * Converts relative sizes into pixel sizes. + * + * @param child + * @return true if the child has a relative size + */ + private boolean handleComponentRelativeSize(ComponentDetail cd) { + if (cd == null) { + return false; + } + boolean debugSizes = false; + + FloatSize relativeSize = cd.getRelativeSize(); + if (relativeSize == null) { + return false; + } + Widget widget = (Widget) cd.getComponent(); + + boolean horizontalScrollBar = false; + boolean verticalScrollBar = false; + + Container parent = Util.getLayout(widget); + RenderSpace renderSpace; + + // Parent-less components (like sub-windows) are relative to browser + // window. + if (parent == null) { + renderSpace = new RenderSpace(Window.getClientWidth(), Window + .getClientHeight()); + } else { + renderSpace = parent.getAllocatedSpace(widget); + } + + if (relativeSize.getHeight() >= 0) { + if (renderSpace != null) { + + if (renderSpace.getScrollbarSize() > 0) { + if (relativeSize.getWidth() > 100) { + horizontalScrollBar = true; + } else if (relativeSize.getWidth() < 0 + && renderSpace.getWidth() > 0) { + int offsetWidth = widget.getOffsetWidth(); + int width = renderSpace.getWidth(); + if (offsetWidth > width) { + horizontalScrollBar = true; + } + } + } + + int height = renderSpace.getHeight(); + if (horizontalScrollBar) { + height -= renderSpace.getScrollbarSize(); + } + if (validatingLayouts && height <= 0) { + zeroHeightComponents.add(cd.getComponent()); + } + + height = (int) (height * relativeSize.getHeight() / 100.0); + + if (height < 0) { + height = 0; + } + + if (debugSizes) { + getConsole() + .log( + "Widget " + + Util.getSimpleName(widget) + + "/" + + getPid(widget.getElement()) + + " relative height " + + relativeSize.getHeight() + + "% of " + + renderSpace.getHeight() + + "px (reported by " + + + Util.getSimpleName(parent) + + "/" + + (parent == null ? "?" : parent + .hashCode()) + ") : " + + height + "px"); + } + widget.setHeight(height + "px"); + } else { + widget.setHeight(relativeSize.getHeight() + "%"); + ApplicationConnection.getConsole().error( + Util.getLayout(widget).getClass().getName() + + " did not produce allocatedSpace for " + + widget.getClass().getName()); + } + } + + if (relativeSize.getWidth() >= 0) { + + if (renderSpace != null) { + + int width = renderSpace.getWidth(); + + if (renderSpace.getScrollbarSize() > 0) { + if (relativeSize.getHeight() > 100) { + verticalScrollBar = true; + } else if (relativeSize.getHeight() < 0 + && renderSpace.getHeight() > 0 + && widget.getOffsetHeight() > renderSpace + .getHeight()) { + verticalScrollBar = true; + } + } + + if (verticalScrollBar) { + width -= renderSpace.getScrollbarSize(); + } + if (validatingLayouts && width <= 0) { + zeroWidthComponents.add(cd.getComponent()); + } + + width = (int) (width * relativeSize.getWidth() / 100.0); + + if (width < 0) { + width = 0; + } + + if (debugSizes) { + getConsole().log( + "Widget " + Util.getSimpleName(widget) + "/" + + getPid(widget.getElement()) + + " relative width " + + relativeSize.getWidth() + "% of " + + renderSpace.getWidth() + + "px (reported by " + + Util.getSimpleName(parent) + "/" + + (parent == null ? "?" : getPid(parent)) + + ") : " + width + "px"); + } + widget.setWidth(width + "px"); + } else { + widget.setWidth(relativeSize.getWidth() + "%"); + ApplicationConnection.getConsole().error( + Util.getLayout(widget).getClass().getName() + + " did not produce allocatedSpace for " + + widget.getClass().getName()); + } + } + + return true; + } + + /** + * Converts relative sizes into pixel sizes. + * + * @param child + * @return true if the child has a relative size + */ + public boolean handleComponentRelativeSize(Widget child) { + return handleComponentRelativeSize(idToPaintableDetail.get(getPid(child + .getElement()))); + + } + + public FloatSize getRelativeSize(Widget widget) { + return idToPaintableDetail.get(getPid(widget.getElement())) + .getRelativeSize(); + } + + /** + * Get either existing or new Paintable for given UIDL. + * + * If corresponding Paintable has been previously painted, return it. + * Otherwise create and register a new Paintable from UIDL. Caller must + * update the returned Paintable from UIDL after it has been connected to + * parent. + * + * @param uidl + * UIDL to create Paintable from. + * @return Either existing or new Paintable corresponding to UIDL. + */ + public Paintable getPaintable(UIDL uidl) { + final String id = uidl.getId(); + Paintable w = getPaintable(id); + if (w != null) { + return w; + } else { + w = widgetSet.createWidget(uidl); + registerPaintable(id, w); + return w; + + } + } + + /** + * Returns a Paintable element by its root element + * + * @param element + * Root element of the paintable + */ + public Paintable getPaintable(Element element) { + return getPaintable(getPid(element)); + } + + public String getResource(String name) { + return resourcesMap.get(name); + } + + /** + * Singleton method to get instance of app's context menu. + * + * @return IContextMenu object + */ + public IContextMenu getContextMenu() { + if (contextMenu == null) { + contextMenu = new IContextMenu(); + DOM.setElementProperty(contextMenu.getElement(), "id", + "PID_TOOLKIT_CM"); + } + return contextMenu; + } + + /** + * Translates custom protocols in UIRL URI's to be recognizable by browser. + * All uri's from UIDL should be routed via this method before giving them + * to browser due URI's in UIDL may contain custom protocols like theme://. + * + * @param toolkitUri + * toolkit URI from uidl + * @return translated URI ready for browser + */ + public String translateToolkitUri(String toolkitUri) { + if (toolkitUri == null) { + return null; + } + if (toolkitUri.startsWith("theme://")) { + final String themeUri = configuration.getThemeUri(); + if (themeUri == null) { + console + .error("Theme not set: ThemeResource will not be found. (" + + toolkitUri + ")"); + } + toolkitUri = themeUri + toolkitUri.substring(7); + } + return toolkitUri; + } + + public String getThemeUri() { + return configuration.getThemeUri(); + } + + /** + * Listens for Notification hide event, and redirects. Used for system + * messages, such as session expired. + * + */ + private class NotificationRedirect implements INotification.EventListener { + String url; + + NotificationRedirect(String url) { + this.url = url; + } + + public void notificationHidden(HideEvent event) { + redirect(url); + } + + } + + /* Extended title handling */ + + /** + * Data showed in tooltips are stored centrilized as it may be needed in + * varios place: caption, layouts, and in owner components themselves. + * + * Updating TooltipInfo is done in updateComponent method. + * + */ + public TooltipInfo getTitleInfo(Paintable titleOwner) { + if (null == titleOwner) { + return null; + } + ComponentDetail pd = idToPaintableDetail.get(getPid(titleOwner)); + if (null != pd) { + return pd.getTooltipInfo(); + } else { + return null; + } + } + + private final ITooltip tooltip = new ITooltip(this); + + /** + * Component may want to delegate Tooltip handling to client. Layouts add + * Tooltip (description, errors) to caption, but some components may want + * them to appear one other elements too. + * + * Events wanted by this handler are same as in Tooltip.TOOLTIP_EVENTS + * + * @param event + * @param owner + */ + public void handleTooltipEvent(Event event, Paintable owner) { + tooltip.handleTooltipEvent(event, owner); + + } + + /** + * Adds PNG-fix conditionally (only for IE6) to the specified IMG -element. + * + * @param el + * the IMG element to fix + */ + public void addPngFix(Element el) { + BrowserInfo b = BrowserInfo.get(); + if (b.isIE6()) { + Util.addPngFix(el, getThemeUri() + + "/../default/common/img/blank.gif"); + } + } + + /* + * Helper to run layout functions triggered by child components with a + * decent interval. + */ + private final Timer layoutTimer = new Timer() { + + private boolean isPending = false; + + @Override + public void schedule(int delayMillis) { + if (!isPending) { + super.schedule(delayMillis); + isPending = true; + } + } + + @Override + public void run() { + getConsole().log( + "Running re-layout of " + view.getClass().getName()); + runDescendentsLayout(view); + isPending = false; + } + }; + + /** + * Components can call this function to run all layout functions. This is + * usually done, when component knows that its size has changed. + */ + public void requestLayoutPhase() { + layoutTimer.schedule(500); + } + + private String windowName = null; + + /** + * Reset the name of the current browser-window. This should reflect the + * window-name used in the server, but might be different from the + * window-object target-name on client. + * + * @param stringAttribute + * New name for the window. + */ + public void setWindowName(String newName) { + windowName = newName; + } + + public void captionSizeUpdated(Paintable component) { + componentCaptionSizeChanges.add(component); + } + + public void analyzeLayouts() { + makeUidlRequest("", true, false, true); + } + + public IView getView() { + return view; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/BrowserInfo.java b/src/com/vaadin/terminal/gwt/client/BrowserInfo.java new file mode 100644 index 0000000000..8e41137e56 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/BrowserInfo.java @@ -0,0 +1,225 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import com.google.gwt.user.client.ui.RootPanel; + +/** + * Class used to query information about web browser. + * + * Browser details are detected only once and those are stored in this singleton + * class. + * + */ +public class BrowserInfo { + + private static BrowserInfo instance; + + private static String cssClass = null; + + static { + // Add browser dependent i-* classnames to body to help css hacks + String browserClassnames = get().getCSSClass(); + RootPanel.get().addStyleName(browserClassnames); + } + + /** + * Singleton method to get BrowserInfo object. + * + * @return instance of BrowserInfo object + */ + public static BrowserInfo get() { + if (instance == null) { + instance = new BrowserInfo(); + } + return instance; + } + + private boolean isGecko; + private boolean isAppleWebKit; + private boolean isSafari; + private boolean isOpera; + private boolean isIE; + private float version = -1; + + private BrowserInfo() { + try { + String ua = getBrowserString().toLowerCase(); + // browser engine name + isGecko = ua.indexOf("gecko") != -1 && ua.indexOf("webkit") == -1; + isAppleWebKit = ua.indexOf("applewebkit") != -1; + + // browser name + isSafari = ua.indexOf("safari") != -1; + isOpera = ua.indexOf("opera") != -1; + isIE = ua.indexOf("msie") != -1 && !isOpera + && (ua.indexOf("webtv") == -1); + + if (isGecko) { + String tmp = ua.substring(ua.indexOf("rv:") + 3); + tmp = tmp.replaceFirst("(\\.[0-9]+).+", "$1"); + version = Float.parseFloat(tmp); + } + if (isAppleWebKit) { + String tmp = ua.substring(ua.indexOf("webkit/") + 7); + tmp = tmp.replaceFirst("([0-9]+)[^0-9].+", "$1"); + version = Float.parseFloat(tmp); + + } + + if (isIE) { + String ieVersionString = ua.substring(ua.indexOf("msie ") + 5); + ieVersionString = ieVersionString.substring(0, ieVersionString + .indexOf(";")); + version = Float.parseFloat(ieVersionString); + + if (version == 8 && isIE8InIE7CompatibilityMode()) { + version = 7; + } + + } + } catch (Exception e) { + ClientExceptionHandler.displayError(e); + } + } + + /** + * Returns a string representing the browser in use, for use in CSS + * classnames. The classnames will be space separated abbrevitaions, + * optionally with a version appended. + * + * Abbreviaions: Firefox: ff Internet Explorer: ie Safari: sa Opera: op + * + * Browsers that CSS-wise behave like each other will get the same + * abbreviation (this usually depends on the rendering engine). + * + * This is quite simple at the moment, more heuristics will be added when + * needed. + * + * Examples: Internet Explorer 6: ".i-ie .i-ie6", Firefox 3.0.4: + * ".i-ff .i-ff3", Opera 9.60: ".i-op .i-op96" + * + * @return + */ + public String getCSSClass() { + String prefix = "i-"; + + if (cssClass == null) { + String bs = getBrowserString().toLowerCase(); + String b = ""; + String v = ""; + if (bs.indexOf(" firefox/") != -1) { + b = "ff"; + int i = bs.indexOf(" firefox/") + 9; + v = b + bs.substring(i, i + 1); + } else if (bs.indexOf(" chrome/") != -1) { + // TODO update when Chrome is more stable + b = "sa"; + v = "ch"; + } else if (bs.indexOf(" safari") != -1) { + b = "sa"; + int i = bs.indexOf(" version/") + 9; + v = b + bs.substring(i, i + 1); + } else if (bs.indexOf(" msie ") != -1) { + b = "ie"; + int i = bs.indexOf(" msie ") + 6; + String ieVersion = bs.substring(i, i + 1); + + if (ieVersion != null && ieVersion.equals("8") + && isIE8InIE7CompatibilityMode()) { + ieVersion = "7"; + } + + v = b + ieVersion; + } else if (bs.indexOf("opera/") != -1) { + b = "op"; + int i = bs.indexOf("opera/") + 6; + v = b + bs.substring(i, i + 3).replace(".", ""); + } + cssClass = prefix + b + " " + prefix + v; + } + + return cssClass; + } + + private native boolean isIE8InIE7CompatibilityMode() + /*-{ + var mode = $wnd.document.documentMode; + if (!mode) + return false; + return (mode == 7); + }-*/; + + public boolean isIE() { + return isIE; + } + + public boolean isSafari() { + return isSafari; + } + + public boolean isIE6() { + return isIE && version == 6; + } + + public boolean isIE7() { + return isIE && version == 7; + } + + public boolean isIE8() { + return isIE && version == 8; + } + + public boolean isGecko() { + return isGecko; + } + + public boolean isFF2() { + return isGecko && version == 1.8; + } + + public boolean isFF3() { + return isGecko && version == 1.9; + } + + public float getGeckoVersion() { + return (isGecko ? version : -1); + } + + public float getWebkitVersion() { + return (isAppleWebKit ? version : -1); + } + + public float getIEVersion() { + return (isIE ? version : -1); + } + + public boolean isOpera() { + return isOpera; + } + + public native static String getBrowserString() + /*-{ + return $wnd.navigator.userAgent; + }-*/; + + public static void test() { + Console c = ApplicationConnection.getConsole(); + + c.log("getBrowserString() " + getBrowserString()); + c.log("isIE() " + get().isIE()); + c.log("isIE6() " + get().isIE6()); + c.log("isIE7() " + get().isIE7()); + c.log("isIE8() " + get().isIE8()); + c.log("isFF2() " + get().isFF2()); + c.log("isSafari() " + get().isSafari()); + c.log("getGeckoVersion() " + get().getGeckoVersion()); + c.log("getWebkitVersion() " + get().getWebkitVersion()); + c.log("getIEVersion() " + get().getIEVersion()); + c.log("isIE() " + get().isIE()); + c.log("isIE() " + get().isIE()); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/CSSRule.java b/src/com/vaadin/terminal/gwt/client/CSSRule.java new file mode 100644 index 0000000000..eda6008c2b --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/CSSRule.java @@ -0,0 +1,85 @@ +package com.vaadin.terminal.gwt.client; + +import com.google.gwt.core.client.JavaScriptObject; + +/** + * Utility class for fetching CSS properties from DOM StyleSheets JS object. + */ +public class CSSRule { + + private final String selector; + private JavaScriptObject rules = null; + + public CSSRule(String selector) { + this.selector = selector; + fetchRule(selector); + } + + // 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); + }-*/; + + /* + * Loops through all current style rules and collects all matching to + * '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) + return null; + + selector = selector.toLowerCase(); + + var allMatches = []; + + var theRules = new Array(); + if (sheet.cssRules) + theRules = sheet.cssRules + 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 + } + } + } + } + + return allMatches; + }-*/; + + /** + * Returns a specific property value from this CSS rule. + * + * @param propertyName + * @return + */ + 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; + } + return null; + }-*/; + + public String getSelector() { + return selector; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java b/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java new file mode 100644 index 0000000000..724bd24a69 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java @@ -0,0 +1,27 @@ +package com.vaadin.terminal.gwt.client;
+
+public class ClientExceptionHandler {
+
+ public static void displayError(Throwable e) {
+ displayError(e.getMessage());
+ e.printStackTrace();
+ }
+
+ public static void displayError(String msg) {
+
+ Console console = ApplicationConnection.getConsole();
+
+ if (console != null) {
+ console.error(msg);
+ // } else {
+ // System.err.println(msg);
+ }
+ }
+
+ public static void displayError(String msg, Throwable e) {
+ displayError(msg);
+ displayError(e);
+
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ComponentDetail.java b/src/com/vaadin/terminal/gwt/client/ComponentDetail.java new file mode 100644 index 0000000000..8ee91ad9e4 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ComponentDetail.java @@ -0,0 +1,88 @@ +package com.vaadin.terminal.gwt.client; + +import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize; +import com.vaadin.terminal.gwt.client.RenderInformation.Size; + +class ComponentDetail { + private String pid; + private Paintable component; + private TooltipInfo tooltipInfo = new TooltipInfo(); + private FloatSize relativeSize; + private Size offsetSize; + + /** + * @return the pid + */ + String getPid() { + return pid; + } + + /** + * @param pid + * the pid to set + */ + void setPid(String pid) { + this.pid = pid; + } + + /** + * @return the component + */ + Paintable getComponent() { + return component; + } + + /** + * @param component + * the component to set + */ + void setComponent(Paintable component) { + this.component = component; + } + + /** + * @return the tooltipInfo + */ + TooltipInfo getTooltipInfo() { + return tooltipInfo; + } + + /** + * @param tooltipInfo + * the tooltipInfo to set + */ + void setTooltipInfo(TooltipInfo tooltipInfo) { + this.tooltipInfo = tooltipInfo; + } + + /** + * @return the relativeSize + */ + FloatSize getRelativeSize() { + return relativeSize; + } + + /** + * @param relativeSize + * the relativeSize to set + */ + void setRelativeSize(FloatSize relativeSize) { + this.relativeSize = relativeSize; + } + + /** + * @return the offsetSize + */ + Size getOffsetSize() { + return offsetSize; + } + + /** + * @param offsetSize + * the offsetSize to set + */ + void setOffsetSize(Size offsetSize) { + this.offsetSize = offsetSize; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java b/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java new file mode 100644 index 0000000000..cab3160922 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java @@ -0,0 +1,72 @@ +package com.vaadin.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.Collection; + +import com.google.gwt.core.client.JavaScriptObject; + +final class ComponentDetailMap extends JavaScriptObject { + + protected ComponentDetailMap() { + } + + static ComponentDetailMap create() { + return (ComponentDetailMap) JavaScriptObject.createObject(); + } + + boolean isEmpty() { + return size() == 0; + } + + final native boolean containsKey(String key) + /*-{ + return this.hasOwnProperty(key); + }-*/; + + final native ComponentDetail get(String key) + /*-{ + return this[key]; + }-*/; + + final native void put(String id, ComponentDetail value) + /*-{ + this[id] = value; + }-*/; + + final native void remove(String id) + /*-{ + delete this[id]; + }-*/; + + final native int size() + /*-{ + var count = 0; + for(var key in this) { + count++; + } + return count; + }-*/; + + final native void clear() + /*-{ + for(var key in this) { + if(this.hasOwnProperty(key)) { + delete this[key]; + } + } + }-*/; + + private final native void fillWithValues(Collection<ComponentDetail> list) + /*-{ + for(var key in this) { + list.@java.util.Collection::add(Ljava/lang/Object;)(this[key]); + } + }-*/; + + final Collection<ComponentDetail> values() { + ArrayList<ComponentDetail> list = new ArrayList<ComponentDetail>(); + fillWithValues(list); + return list; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ComponentLocator.java b/src/com/vaadin/terminal/gwt/client/ComponentLocator.java new file mode 100644 index 0000000000..ccd0022876 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ComponentLocator.java @@ -0,0 +1,343 @@ +package com.vaadin.terminal.gwt.client; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ui.IView; +import com.vaadin.terminal.gwt.client.ui.IWindow; +import com.vaadin.terminal.gwt.client.ui.SubPartAware; + +/** + * ComponentLocator provides methods for uniquely identifying DOM elements using + * string expressions. This class is EXPERIMENTAL and subject to change. + */ +public class ComponentLocator { + + /** + * Separator used in the string expression between a parent and a child + * widget. + */ + private static final String PARENTCHILD_SEPARATOR = "/"; + + /** + * Separator used in the string expression between a widget and the widget's + * sub part. NOT CURRENTLY IN USE. + */ + private static final String SUBPART_SEPARATOR = "#"; + + private ApplicationConnection client; + + public ComponentLocator(ApplicationConnection client) { + this.client = client; + } + + /** + * EXPERIMENTAL. + * + * Generates a string expression (path) which uniquely identifies the target + * element . The getElementByPath method can be used for the inverse + * operation, i.e. locating an element based on the string expression. + * + * @since 5.4 + * @param targetElement + * The element to generate a path for. + * @return A string expression uniquely identifying the target element or + * null if a string expression could not be created. + */ + public String getPathForElement(Element targetElement) { + String pid = null; + + Element e = targetElement; + + while (true) { + pid = client.getPid(e); + if (pid != null) { + break; + } + + e = DOM.getParent(e); + if (e == null) { + break; + } + } + + if (e == null || pid == null) { + + // Still test for context menu option + String subPartName = client.getContextMenu().getSubPartName( + targetElement); + if (subPartName != null) { + // IContextMenu, singleton attached directly to rootpanel + return "/IContextMenu[0]" + SUBPART_SEPARATOR + subPartName; + + } + return null; + } + + Widget w = (Widget) client.getPaintable(pid); + if (w == null) { + return null; + } + // ApplicationConnection.getConsole().log( + // "First parent widget: " + Util.getSimpleName(w)); + + String path = getPathForWidget(w); + // ApplicationConnection.getConsole().log( + // "getPathFromWidget returned " + path); + if (w.getElement() == targetElement) { + // ApplicationConnection.getConsole().log( + // "Path for " + Util.getSimpleName(w) + ": " + path); + + return path; + } else if (w instanceof SubPartAware) { + return path + SUBPART_SEPARATOR + + ((SubPartAware) w).getSubPartName(targetElement); + } else { + path = path + getDOMPathForElement(targetElement, w.getElement()); + // ApplicationConnection.getConsole().log( + // "Path with dom addition for " + Util.getSimpleName(w) + // + ": " + path); + + return path; + } + } + + private Element getElementByDOMPath(Element baseElement, String path) { + String parts[] = path.split(PARENTCHILD_SEPARATOR); + Element element = baseElement; + + for (String part : parts) { + if (part.startsWith("domChild[")) { + String childIndexString = part.substring("domChild[".length(), + part.length() - 1); + try { + int childIndex = Integer.parseInt(childIndexString); + element = DOM.getChild(element, childIndex); + } catch (Exception e) { + // ApplicationConnection.getConsole().error( + // "Failed to parse integer in " + childIndexString); + return null; + } + } + } + + return element; + } + + private String getDOMPathForElement(Element element, Element baseElement) { + Element e = element; + String path = ""; + while (true) { + Element parent = DOM.getParent(e); + if (parent == null) { + return "ERROR, baseElement is not a parent to element"; + } + + int childIndex = -1; + + int childCount = DOM.getChildCount(parent); + for (int i = 0; i < childCount; i++) { + if (e == DOM.getChild(parent, i)) { + childIndex = i; + break; + } + } + if (childIndex == -1) { + return "ERROR, baseElement is not a parent to element."; + } + + path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]" + + path; + + if (parent == baseElement) { + break; + } + + e = parent; + } + + return path; + } + + /** + * EXPERIMENTAL. + * + * Locates an element by using a string expression (path) which uniquely + * identifies the element. The getPathForElement method can be used for the + * inverse operation, i.e. generating a string expression for a target + * element. + * + * @since 5.4 + * @param path + * The string expression which uniquely identifies the target + * element. + * @return The DOM element identified by the path or null if the element + * could not be located. + */ + public Element getElementByPath(String path) { + // ApplicationConnection.getConsole() + // .log("getElementByPath(" + path + ")"); + + // Path is of type "PID/componentPart" + String parts[] = path.split(SUBPART_SEPARATOR, 2); + String widgetPath = parts[0]; + Widget w = getWidgetFromPath(widgetPath); + if (w == null) { + return null; + } + + if (parts.length == 1) { + int pos = widgetPath.indexOf("domChild"); + if (pos == -1) { + return w.getElement(); + } + + // Contains dom reference to a sub element of the widget + String subPath = widgetPath.substring(pos); + return getElementByDOMPath(w.getElement(), subPath); + } else if (parts.length == 2) { + if (w instanceof SubPartAware) { + // ApplicationConnection.getConsole().log( + // "subPartAware: " + parts[1]); + return ((SubPartAware) w).getSubPartElement(parts[1]); + } else { + // ApplicationConnection.getConsole().error( + // "getElementByPath failed because " + // + Util.getSimpleName(w) + // + " is not SubPartAware"); + return null; + } + } + + return null; + } + + private String getPathForWidget(Widget w) { + if (w == null) { + return ""; + } + + String pid = client.getPid(w.getElement()); + if (isStaticPid(pid)) { + return pid; + } + + if (w instanceof IView) { + return ""; + } else if (w instanceof IWindow) { + IWindow win = (IWindow) w; + ArrayList<IWindow> subWindowList = client.getView() + .getSubWindowList(); + int indexOfSubWindow = subWindowList.indexOf(win); + return PARENTCHILD_SEPARATOR + "IWindow[" + indexOfSubWindow + "]"; + } + + Widget parent = w.getParent(); + + String basePath = getPathForWidget(parent); + + String simpleName = Util.getSimpleName(w); + + Iterator<Widget> i = ((HasWidgets) parent).iterator(); + int pos = 0; + while (i.hasNext()) { + Object child = i.next(); + if (child == w) { + return basePath + PARENTCHILD_SEPARATOR + simpleName + "[" + + pos + "]"; + } + String simpleName2 = Util.getSimpleName(child); + if (simpleName.equals(simpleName2)) { + pos++; + } + } + + return "NOTFOUND"; + } + + private Widget getWidgetFromPath(String path) { + Widget w = null; + String parts[] = path.split(PARENTCHILD_SEPARATOR); + + // ApplicationConnection.getConsole().log( + // "getWidgetFromPath(" + path + ")"); + + for (String part : parts) { + // ApplicationConnection.getConsole().log("Part: " + part); + // ApplicationConnection.getConsole().log( + // "Widget: " + Util.getSimpleName(w)); + if (part.equals("")) { + w = client.getView(); + } else if (w == null) { + w = (Widget) client.getPaintable(part); + } else if (part.startsWith("domChild[")) { + break; + } else if (w instanceof HasWidgets) { + HasWidgets parent = (HasWidgets) w; + + String[] split = part.split("\\["); + + Iterator<? extends Widget> i; + String widgetClassName = split[0]; + if (widgetClassName.equals("IWindow")) { + i = client.getView().getSubWindowList().iterator(); + } else if (widgetClassName.equals("IContextMenu")) { + return client.getContextMenu(); + } else { + i = parent.iterator(); + } + + boolean ok = false; + int pos = Integer.parseInt(split[1].substring(0, split[1] + .length() - 1)); + // ApplicationConnection.getConsole().log( + // "Looking for child " + pos); + while (i.hasNext()) { + // ApplicationConnection.getConsole().log("- child found"); + + Widget child = i.next(); + String simpleName2 = Util.getSimpleName(child); + + if (widgetClassName.equals(simpleName2)) { + if (pos == 0) { + w = child; + ok = true; + break; + } + pos--; + } + } + + if (!ok) { + // Did not find the child + // ApplicationConnection.getConsole().error( + // "getWidgetFromPath(" + path + ") - did not find '" + // + part + "' for " + // + Util.getSimpleName(parent)); + + return null; + } + } else { + // ApplicationConnection.getConsole().error( + // "getWidgetFromPath(" + path + ") - failed for '" + part + // + "'"); + return null; + } + } + + return w; + } + + private boolean isStaticPid(String pid) { + if (pid == null) { + return false; + } + + return pid.startsWith("PID_S"); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/Console.java b/src/com/vaadin/terminal/gwt/client/Console.java new file mode 100644 index 0000000000..2d433f8997 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/Console.java @@ -0,0 +1,26 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import java.util.Set; + +import com.google.gwt.json.client.JSONArray; + +public interface Console { + + public abstract void log(String msg); + + public abstract void error(String msg); + + public abstract void printObject(Object msg); + + public abstract void dirUIDL(UIDL u); + + public abstract void printLayoutProblems(JSONArray array, + ApplicationConnection applicationConnection, + Set<Paintable> zeroHeightComponents, + Set<Paintable> zeroWidthComponents); + +}
\ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/client/Container.java b/src/com/vaadin/terminal/gwt/client/Container.java new file mode 100644 index 0000000000..28c31fa101 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/Container.java @@ -0,0 +1,71 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import java.util.Set; + +import com.google.gwt.user.client.ui.Widget; + +public interface Container extends Paintable { + + /** + * Replace child of this layout with another component. + * + * Each layout must be able to switch children. To to this, one must just + * give references to a current and new child. + * + * @param oldComponent + * Child to be replaced + * @param newComponent + * Child that replaces the oldComponent + */ + void replaceChildComponent(Widget oldComponent, Widget newComponent); + + /** + * Is a given component child of this layout. + * + * @param component + * Component to test. + * @return true iff component is a child of this layout. + */ + boolean hasChildComponent(Widget component); + + /** + * Update child components caption, description and error message. + * + * <p> + * Each component is responsible for maintaining its caption, description + * and error message. In most cases components doesn't want to do that and + * those elements reside outside of the component. Because of this layouts + * must provide service for it's childen to show those elements for them. + * </p> + * + * @param component + * Child component for which service is requested. + * @param uidl + * UIDL of the child component. + */ + void updateCaption(Paintable component, UIDL uidl); + + /** + * Called when a child components size has been updated in the rendering + * phase. + * + * @param children + * Set of child widgets whose size have changed + * @return true if the size of the Container remains the same, false if the + * event need to be propagated to the Containers parent + */ + boolean requestLayout(Set<Paintable> children); + + /** + * Returns the size currently allocated for the child component. + * + * @param child + * @return + */ + RenderSpace getAllocatedSpace(Widget child); + +} diff --git a/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java b/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java new file mode 100644 index 0000000000..bfefb5dd05 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java @@ -0,0 +1,21 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +/** + * ContainerResizedListener interface is useful for Widgets that support + * relative sizes and who need some additional sizing logic. + */ +public interface ContainerResizedListener { + /** + * This function is run when container box has been resized. Object + * implementing ContainerResizedListener is responsible to call the same + * function on its ancestors that implement NeedsLayout in case their + * container has resized. runAnchestorsLayout(HasWidgets parent) function + * from Util class may be a good helper for this. + * + */ + public void iLayout(); +} diff --git a/src/com/vaadin/terminal/gwt/client/DateTimeService.java b/src/com/vaadin/terminal/gwt/client/DateTimeService.java new file mode 100644 index 0000000000..e16d9b078d --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/DateTimeService.java @@ -0,0 +1,237 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Date;
+
+/**
+ * This class provides date/time parsing services to all components on the
+ * client side.
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+@SuppressWarnings("deprecation")
+public class DateTimeService {
+ public static int RESOLUTION_YEAR = 0;
+ public static int RESOLUTION_MONTH = 1;
+ public static int RESOLUTION_DAY = 2;
+ public static int RESOLUTION_HOUR = 3;
+ public static int RESOLUTION_MIN = 4;
+ public static int RESOLUTION_SEC = 5;
+ public static int RESOLUTION_MSEC = 6;
+
+ private String currentLocale;
+
+ private static int[] maxDaysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30,
+ 31, 30, 31 };
+
+ /**
+ * Creates a new date time service with the application default locale.
+ */
+ public DateTimeService() {
+ currentLocale = LocaleService.getDefaultLocale();
+ }
+
+ /**
+ * Creates a new date time service with a given locale.
+ *
+ * @param locale
+ * e.g. fi, en etc.
+ * @throws LocaleNotLoadedException
+ */
+ public DateTimeService(String locale) throws LocaleNotLoadedException {
+ setLocale(locale);
+ }
+
+ public void setLocale(String locale) throws LocaleNotLoadedException {
+ if (LocaleService.getAvailableLocales().contains(locale)) {
+ currentLocale = locale;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public String getLocale() {
+ return currentLocale;
+ }
+
+ public String getMonth(int month) {
+ try {
+ return LocaleService.getMonthNames(currentLocale)[month];
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return null;
+ }
+
+ public String getShortMonth(int month) {
+ try {
+ return LocaleService.getShortMonthNames(currentLocale)[month];
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return null;
+ }
+
+ public String getDay(int day) {
+ try {
+ return LocaleService.getDayNames(currentLocale)[day];
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return null;
+ }
+
+ public String getShortDay(int day) {
+ try {
+ return LocaleService.getShortDayNames(currentLocale)[day];
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return null;
+ }
+
+ public int getFirstDayOfWeek() {
+ try {
+ return LocaleService.getFirstDayOfWeek(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return 0;
+ }
+
+ public boolean isTwelveHourClock() {
+ try {
+ return LocaleService.isTwelveHourClock(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return false;
+ }
+
+ public String getClockDelimeter() {
+ try {
+ return LocaleService.getClockDelimiter(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return ":";
+ }
+
+ public String[] getAmPmStrings() {
+ try {
+ return LocaleService.getAmPmStrings(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ final String[] temp = new String[2];
+ temp[0] = "AM";
+ temp[1] = "PM";
+ return temp;
+ }
+
+ public int getStartWeekDay(Date date) {
+ final Date dateForFirstOfThisMonth = new Date(date.getYear(), date
+ .getMonth(), 1);
+ int firstDay;
+ try {
+ firstDay = LocaleService.getFirstDayOfWeek(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ firstDay = 0;
+ ClientExceptionHandler.displayError(e);
+ }
+ int start = dateForFirstOfThisMonth.getDay() - firstDay;
+ if (start < 0) {
+ start = 6;
+ }
+ return start;
+ }
+
+ public static int getNumberOfDaysInMonth(Date date) {
+ final int month = date.getMonth();
+ if (month == 1 && true == isLeapYear(date)) {
+ return 29;
+ }
+ return maxDaysInMonth[month];
+ }
+
+ public static boolean isLeapYear(Date date) {
+ // Instantiate the date for 1st March of that year
+ final Date firstMarch = new Date(date.getYear(), 2, 1);
+
+ // Go back 1 day
+ final long firstMarchTime = firstMarch.getTime();
+ final long lastDayTimeFeb = firstMarchTime - (24 * 60 * 60 * 1000); // NUM_MILLISECS_A_DAY
+
+ // Instantiate new Date with this time
+ final Date febLastDay = new Date(lastDayTimeFeb);
+
+ // Check for date in this new instance
+ return (29 == febLastDay.getDate()) ? true : false;
+ }
+
+ public static boolean isSameDay(Date d1, Date d2) {
+ return (getDayInt(d1) == getDayInt(d2));
+ }
+
+ public static boolean isInRange(Date date, Date rangeStart, Date rangeEnd,
+ int resolution) {
+ Date s;
+ Date e;
+ if (rangeStart.after(rangeEnd)) {
+ s = rangeEnd;
+ e = rangeStart;
+ } else {
+ e = rangeEnd;
+ s = rangeStart;
+ }
+ long start = s.getYear() * 10000000000l;
+ long end = e.getYear() * 10000000000l;
+ long target = date.getYear() * 10000000000l;
+
+ if (resolution == RESOLUTION_YEAR) {
+ return (start <= target && end >= target);
+ }
+ start += s.getMonth() * 100000000;
+ end += e.getMonth() * 100000000;
+ target += date.getMonth() * 100000000;
+ if (resolution == RESOLUTION_MONTH) {
+ return (start <= target && end >= target);
+ }
+ start += s.getDate() * 1000000;
+ end += e.getDate() * 1000000;
+ target += date.getDate() * 1000000;
+ if (resolution == RESOLUTION_DAY) {
+ return (start <= target && end >= target);
+ }
+ start += s.getHours() * 10000;
+ end += e.getHours() * 10000;
+ target += date.getHours() * 10000;
+ if (resolution == RESOLUTION_HOUR) {
+ return (start <= target && end >= target);
+ }
+ start += s.getMinutes() * 100;
+ end += e.getMinutes() * 100;
+ target += date.getMinutes() * 100;
+ if (resolution == RESOLUTION_MIN) {
+ return (start <= target && end >= target);
+ }
+ start += s.getSeconds();
+ end += e.getSeconds();
+ target += date.getSeconds();
+ return (start <= target && end >= target);
+
+ }
+
+ private static int getDayInt(Date date) {
+ final int y = date.getYear();
+ final int m = date.getMonth();
+ final int d = date.getDate();
+
+ return ((y + 1900) * 10000 + m * 100 + d) * 1000000000;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/DefaultWidgetSet.java b/src/com/vaadin/terminal/gwt/client/DefaultWidgetSet.java new file mode 100644 index 0000000000..104ca1d232 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/DefaultWidgetSet.java @@ -0,0 +1,280 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ui.IAbsoluteLayout; +import com.vaadin.terminal.gwt.client.ui.IAccordion; +import com.vaadin.terminal.gwt.client.ui.IButton; +import com.vaadin.terminal.gwt.client.ui.ICheckBox; +import com.vaadin.terminal.gwt.client.ui.ICustomComponent; +import com.vaadin.terminal.gwt.client.ui.ICustomLayout; +import com.vaadin.terminal.gwt.client.ui.IDateFieldCalendar; +import com.vaadin.terminal.gwt.client.ui.IEmbedded; +import com.vaadin.terminal.gwt.client.ui.IFilterSelect; +import com.vaadin.terminal.gwt.client.ui.IForm; +import com.vaadin.terminal.gwt.client.ui.IFormLayout; +import com.vaadin.terminal.gwt.client.ui.IGridLayout; +import com.vaadin.terminal.gwt.client.ui.IHorizontalLayout; +import com.vaadin.terminal.gwt.client.ui.ILabel; +import com.vaadin.terminal.gwt.client.ui.ILink; +import com.vaadin.terminal.gwt.client.ui.IListSelect; +import com.vaadin.terminal.gwt.client.ui.IMenuBar; +import com.vaadin.terminal.gwt.client.ui.INativeSelect; +import com.vaadin.terminal.gwt.client.ui.IOptionGroup; +import com.vaadin.terminal.gwt.client.ui.IOrderedLayout; +import com.vaadin.terminal.gwt.client.ui.IPanel; +import com.vaadin.terminal.gwt.client.ui.IPasswordField; +import com.vaadin.terminal.gwt.client.ui.IPopupCalendar; +import com.vaadin.terminal.gwt.client.ui.IPopupView; +import com.vaadin.terminal.gwt.client.ui.IProgressIndicator; +import com.vaadin.terminal.gwt.client.ui.IScrollTable; +import com.vaadin.terminal.gwt.client.ui.ISlider; +import com.vaadin.terminal.gwt.client.ui.ISplitPanelHorizontal; +import com.vaadin.terminal.gwt.client.ui.ISplitPanelVertical; +import com.vaadin.terminal.gwt.client.ui.ITablePaging; +import com.vaadin.terminal.gwt.client.ui.ITabsheet; +import com.vaadin.terminal.gwt.client.ui.ITextArea; +import com.vaadin.terminal.gwt.client.ui.ITextField; +import com.vaadin.terminal.gwt.client.ui.ITextualDate; +import com.vaadin.terminal.gwt.client.ui.ITree; +import com.vaadin.terminal.gwt.client.ui.ITwinColSelect; +import com.vaadin.terminal.gwt.client.ui.IUnknownComponent; +import com.vaadin.terminal.gwt.client.ui.IUpload; +import com.vaadin.terminal.gwt.client.ui.IUriFragmentUtility; +import com.vaadin.terminal.gwt.client.ui.IVerticalLayout; +import com.vaadin.terminal.gwt.client.ui.IWindow; +import com.vaadin.terminal.gwt.client.ui.richtextarea.IRichTextArea; + +public class DefaultWidgetSet implements WidgetSet { + + /** + * This is the entry point method. It will start the first + */ + public void onModuleLoad() { + ApplicationConfiguration.initConfigurations(this); + ApplicationConfiguration.startNextApplication(); // start first app + } + + public Paintable createWidget(UIDL uidl) { + final Class classType = resolveWidgetType(uidl); + if (ICheckBox.class == classType) { + return new ICheckBox(); + } else if (IButton.class == classType) { + return new IButton(); + } else if (IWindow.class == classType) { + return new IWindow(); + } else if (IOrderedLayout.class == classType) { + return new IOrderedLayout(); + } else if (IVerticalLayout.class == classType) { + return new IVerticalLayout(); + } else if (IHorizontalLayout.class == classType) { + return new IHorizontalLayout(); + } else if (ILabel.class == classType) { + return new ILabel(); + } else if (ILink.class == classType) { + return new ILink(); + } else if (IGridLayout.class == classType) { + return new IGridLayout(); + } else if (ITree.class == classType) { + return new ITree(); + } else if (IOptionGroup.class == classType) { + return new IOptionGroup(); + } else if (ITwinColSelect.class == classType) { + return new ITwinColSelect(); + } else if (INativeSelect.class == classType) { + return new INativeSelect(); + } else if (IListSelect.class == classType) { + return new IListSelect(); + } else if (IPanel.class == classType) { + return new IPanel(); + } else if (ITabsheet.class == classType) { + return new ITabsheet(); + } else if (IEmbedded.class == classType) { + return new IEmbedded(); + } else if (ICustomLayout.class == classType) { + return new ICustomLayout(); + } else if (ICustomComponent.class == classType) { + return new ICustomComponent(); + } else if (ITextArea.class == classType) { + return new ITextArea(); + } else if (IPasswordField.class == classType) { + return new IPasswordField(); + } else if (ITextField.class == classType) { + return new ITextField(); + } else if (ITablePaging.class == classType) { + return new ITablePaging(); + } else if (IScrollTable.class == classType) { + return new IScrollTable(); + } else if (IDateFieldCalendar.class == classType) { + return new IDateFieldCalendar(); + } else if (ITextualDate.class == classType) { + return new ITextualDate(); + } else if (IPopupCalendar.class == classType) { + return new IPopupCalendar(); + } else if (ISlider.class == classType) { + return new ISlider(); + } else if (IForm.class == classType) { + return new IForm(); + } else if (IFormLayout.class == classType) { + return new IFormLayout(); + } else if (IUpload.class == classType) { + return new IUpload(); + } else if (ISplitPanelHorizontal.class == classType) { + return new ISplitPanelHorizontal(); + } else if (ISplitPanelVertical.class == classType) { + return new ISplitPanelVertical(); + } else if (IFilterSelect.class == classType) { + return new IFilterSelect(); + } else if (IProgressIndicator.class == classType) { + return new IProgressIndicator(); + } else if (IRichTextArea.class == classType) { + return new IRichTextArea(); + } else if (IAccordion.class == classType) { + return new IAccordion(); + } else if (IMenuBar.class == classType) { + return new IMenuBar(); + } else if (IPopupView.class == classType) { + return new IPopupView(); + } else if (IUriFragmentUtility.class == classType) { + return new IUriFragmentUtility(); + } else if (IAbsoluteLayout.class == classType) { + return new IAbsoluteLayout(); + } + + return new IUnknownComponent(); + + } + + protected Class resolveWidgetType(UIDL uidl) { + final String tag = uidl.getTag(); + if ("button".equals(tag)) { + if ("switch".equals(uidl.getStringAttribute("type"))) { + return ICheckBox.class; + } else { + return IButton.class; + } + } else if ("window".equals(tag)) { + return IWindow.class; + } else if ("orderedlayout".equals(tag)) { + return IOrderedLayout.class; + } else if ("verticallayout".equals(tag)) { + return IVerticalLayout.class; + } else if ("horizontallayout".equals(tag)) { + return IHorizontalLayout.class; + } else if ("label".equals(tag)) { + return ILabel.class; + } else if ("link".equals(tag)) { + return ILink.class; + } else if ("gridlayout".equals(tag)) { + return IGridLayout.class; + } else if ("tree".equals(tag)) { + return ITree.class; + } else if ("select".equals(tag)) { + if (uidl.hasAttribute("type")) { + final String type = uidl.getStringAttribute("type"); + if (type.equals("twincol")) { + return ITwinColSelect.class; + } + if (type.equals("optiongroup")) { + return IOptionGroup.class; + } + if (type.equals("native")) { + return INativeSelect.class; + } + if (type.equals("list")) { + return IListSelect.class; + } + } else { + if (uidl.hasAttribute("selectmode") + && uidl.getStringAttribute("selectmode") + .equals("multi")) { + return IListSelect.class; + } else { + return IFilterSelect.class; + } + } + } else if ("panel".equals(tag)) { + return IPanel.class; + } else if ("tabsheet".equals(tag)) { + return ITabsheet.class; + } else if ("accordion".equals(tag)) { + return IAccordion.class; + } else if ("embedded".equals(tag)) { + return IEmbedded.class; + } else if ("customlayout".equals(tag)) { + return ICustomLayout.class; + } else if ("customcomponent".equals(tag)) { + return ICustomComponent.class; + } else if ("textfield".equals(tag)) { + if (uidl.getBooleanAttribute("richtext")) { + return IRichTextArea.class; + } else if (uidl.hasAttribute("multiline")) { + return ITextArea.class; + } else if (uidl.getBooleanAttribute("secret")) { + return IPasswordField.class; + } else { + return ITextField.class; + } + } else if ("table".equals(tag)) { + return IScrollTable.class; + } else if ("pagingtable".equals(tag)) { + return ITablePaging.class; + } else if ("datefield".equals(tag)) { + if (uidl.hasAttribute("type")) { + if ("inline".equals(uidl.getStringAttribute("type"))) { + return IDateFieldCalendar.class; + } else if ("popup".equals(uidl.getStringAttribute("type"))) { + return IPopupCalendar.class; + } + } + // popup calendar is the default + return IPopupCalendar.class; + } else if ("slider".equals(tag)) { + return ISlider.class; + } else if ("form".equals(tag)) { + return IForm.class; + } else if ("formlayout".equals(tag)) { + return IFormLayout.class; + } else if ("upload".equals(tag)) { + return IUpload.class; + } else if ("hsplitpanel".equals(tag)) { + return ISplitPanelHorizontal.class; + } else if ("vsplitpanel".equals(tag)) { + return ISplitPanelVertical.class; + } else if ("progressindicator".equals(tag)) { + return IProgressIndicator.class; + } else if ("menubar".equals(tag)) { + return IMenuBar.class; + } else if ("popupview".equals(tag)) { + return IPopupView.class; + } else if ("urifragment".equals(tag)) { + return IUriFragmentUtility.class; + } else if (IAbsoluteLayout.TAGNAME.equals(tag)) { + return IAbsoluteLayout.class; + } + + return IUnknownComponent.class; + } + + /** + * Kept here to support 5.2 era widget sets + * + * @deprecated use resolveWidgetType instead + */ + @Deprecated + protected String resolveWidgetTypeName(UIDL uidl) { + Class type = resolveWidgetType(uidl); + return type.getName(); + } + + public boolean isCorrectImplementation(Widget currentWidget, UIDL uidl) { + // TODO remove backwardscompatibility check + return currentWidget.getClass() == resolveWidgetType(uidl) + || currentWidget.getClass().getName().equals( + resolveWidgetTypeName(uidl)); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/Focusable.java b/src/com/vaadin/terminal/gwt/client/Focusable.java new file mode 100644 index 0000000000..bf225ad61d --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/Focusable.java @@ -0,0 +1,16 @@ +package com.vaadin.terminal.gwt.client; + +/** + * GWT's HasFocus is way too overkill for just receiving focus in simple + * components. Toolkit uses this interface in addition to GWT's HasFocus to pass + * focus requests from server to actual ui widgets in browsers. + * + * So in to make your server side focusable component receive focus on client + * side it must either implement this or HasFocus interface. + */ +public interface Focusable { + /** + * Sets focus to this widget. + */ + public void focus(); +} diff --git a/src/com/vaadin/terminal/gwt/client/ICaption.java b/src/com/vaadin/terminal/gwt/client/ICaption.java new file mode 100644 index 0000000000..972a844bf9 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ICaption.java @@ -0,0 +1,443 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.terminal.gwt.client.ui.Icon; + +public class ICaption extends HTML { + + public static final String CLASSNAME = "i-caption"; + + private final Paintable owner; + + private Element errorIndicatorElement; + + private Element requiredFieldIndicator; + + private Icon icon; + + private Element captionText; + + private Element clearElement; + + private final ApplicationConnection client; + + private boolean placedAfterComponent = false; + private boolean iconOnloadHandled = false; + + private int maxWidth = -1; + + private static String ATTRIBUTE_ICON = "icon"; + private static String ATTRIBUTE_CAPTION = "caption"; + private static String ATTRIBUTE_DESCRIPTION = "description"; + private static String ATTRIBUTE_REQUIRED = "required"; + private static String ATTRIBUTE_ERROR = "error"; + private static String ATTRIBUTE_HIDEERRORS = "hideErrors"; + + /** + * + * @param component + * optional owner of caption. If not set, getOwner will return + * null + * @param client + */ + public ICaption(Paintable component, ApplicationConnection client) { + super(); + this.client = client; + owner = component; + setStyleName(CLASSNAME); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + + } + + /** + * Updates the caption from UIDL. + * + * @param uidl + * @return true if the position where the caption should be placed has + * changed + */ + public boolean updateCaption(UIDL uidl) { + setVisible(!uidl.getBooleanAttribute("invisible")); + + boolean wasPlacedAfterComponent = placedAfterComponent; + + placedAfterComponent = true; + + String style = CLASSNAME; + if (uidl.hasAttribute("style")) { + final String[] styles = uidl.getStringAttribute("style").split(" "); + for (int i = 0; i < styles.length; i++) { + style += " " + CLASSNAME + "-" + styles[i]; + } + } + + if (uidl.hasAttribute("disabled")) { + style += " " + "i-disabled"; + } + + setStyleName(style); + + if (uidl.hasAttribute(ATTRIBUTE_ICON)) { + if (icon == null) { + icon = new Icon(client); + icon.setWidth("0px"); + icon.setHeight("0px"); + + DOM.insertChild(getElement(), icon.getElement(), + getInsertPosition(ATTRIBUTE_ICON)); + } + placedAfterComponent = false; + + iconOnloadHandled = false; + icon.setUri(uidl.getStringAttribute(ATTRIBUTE_ICON)); + + } else if (icon != null) { + // Remove existing + DOM.removeChild(getElement(), icon.getElement()); + icon = null; + } + + if (uidl.hasAttribute(ATTRIBUTE_CAPTION)) { + if (captionText == null) { + captionText = DOM.createDiv(); + captionText.setClassName("i-captiontext"); + + DOM.insertChild(getElement(), captionText, + getInsertPosition(ATTRIBUTE_CAPTION)); + } + + // Update caption text + String c = uidl.getStringAttribute(ATTRIBUTE_CAPTION); + if (c == null) { + c = ""; + } else { + placedAfterComponent = false; + } + DOM.setInnerText(captionText, c); + } else if (captionText != null) { + // Remove existing + DOM.removeChild(getElement(), captionText); + captionText = null; + } + + if (uidl.hasAttribute(ATTRIBUTE_DESCRIPTION)) { + if (captionText != null) { + addStyleDependentName("hasdescription"); + } else { + removeStyleDependentName("hasdescription"); + } + } + + if (uidl.getBooleanAttribute(ATTRIBUTE_REQUIRED)) { + if (requiredFieldIndicator == null) { + requiredFieldIndicator = DOM.createDiv(); + requiredFieldIndicator + .setClassName("i-required-field-indicator"); + DOM.setInnerText(requiredFieldIndicator, "*"); + + DOM.insertChild(getElement(), requiredFieldIndicator, + getInsertPosition(ATTRIBUTE_REQUIRED)); + } + } else if (requiredFieldIndicator != null) { + // Remove existing + DOM.removeChild(getElement(), requiredFieldIndicator); + requiredFieldIndicator = null; + } + + if (uidl.hasAttribute(ATTRIBUTE_ERROR) + && !uidl.getBooleanAttribute(ATTRIBUTE_HIDEERRORS)) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + DOM.setInnerHTML(errorIndicatorElement, " "); + DOM.setElementProperty(errorIndicatorElement, "className", + "i-errorindicator"); + + DOM.insertChild(getElement(), errorIndicatorElement, + getInsertPosition(ATTRIBUTE_ERROR)); + } + } else if (errorIndicatorElement != null) { + // Remove existing + DOM.removeChild(getElement(), errorIndicatorElement); + errorIndicatorElement = null; + } + + if (clearElement == null) { + clearElement = DOM.createDiv(); + DOM.setStyleAttribute(clearElement, "clear", "both"); + DOM.setStyleAttribute(clearElement, "width", "0px"); + DOM.setStyleAttribute(clearElement, "height", "0px"); + DOM.setStyleAttribute(clearElement, "overflow", "hidden"); + DOM.appendChild(getElement(), clearElement); + } + + return (wasPlacedAfterComponent != placedAfterComponent); + } + + private int getInsertPosition(String element) { + int pos = 0; + if (element.equals(ATTRIBUTE_ICON)) { + return pos; + } + if (icon != null) { + pos++; + } + + if (element.equals(ATTRIBUTE_CAPTION)) { + return pos; + } + + if (captionText != null) { + pos++; + } + + if (element.equals(ATTRIBUTE_REQUIRED)) { + return pos; + } + if (requiredFieldIndicator != null) { + pos++; + } + + // if (element.equals(ATTRIBUTE_ERROR)) { + // } + return pos; + + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + final Element target = DOM.eventGetTarget(event); + if (client != null && owner != null && target != getElement()) { + client.handleTooltipEvent(event, owner); + } + + if (DOM.eventGetType(event) == Event.ONLOAD + && icon.getElement() == target && !iconOnloadHandled) { + icon.setWidth(""); + icon.setHeight(""); + + /* + * IE6 pngFix causes two onload events to be fired and we want to + * react only to the first one + */ + iconOnloadHandled = true; + + // if max width defined, recalculate + if (maxWidth != -1) { + setMaxWidth(maxWidth); + } else { + String width = getElement().getStyle().getProperty("width"); + if (width != null && !width.equals("")) { + setWidth(getRequiredWidth() + "px"); + } + } + + /* + * The size of the icon might affect the size of the component so we + * must report the size change to the parent TODO consider moving + * the responsibility of reacting to ONLOAD from ICaption to layouts + */ + if (owner != null) { + Util.notifyParentOfSizeChange(owner, true); + } else { + ApplicationConnection + .getConsole() + .log( + "Warning: Icon load event was not propagated because ICaption owner is unknown."); + } + } + } + + public static boolean isNeeded(UIDL uidl) { + if (uidl.getStringAttribute(ATTRIBUTE_CAPTION) != null) { + return true; + } + if (uidl.hasAttribute(ATTRIBUTE_ERROR)) { + return true; + } + if (uidl.hasAttribute(ATTRIBUTE_ICON)) { + return true; + } + if (uidl.hasAttribute(ATTRIBUTE_REQUIRED)) { + return true; + } + + return false; + } + + /** + * Returns Paintable for which this Caption belongs to. + * + * @return owner Widget + */ + public Paintable getOwner() { + return owner; + } + + public boolean shouldBePlacedAfterComponent() { + return placedAfterComponent; + } + + public int getRenderedWidth() { + int width = 0; + + if (icon != null) { + width += Util.getRequiredWidth(icon.getElement()); + } + + if (captionText != null) { + width += Util.getRequiredWidth(captionText); + } + if (requiredFieldIndicator != null) { + width += Util.getRequiredWidth(requiredFieldIndicator); + } + if (errorIndicatorElement != null) { + width += Util.getRequiredWidth(errorIndicatorElement); + } + + return width; + + } + + public int getRequiredWidth() { + int width = 0; + + if (icon != null) { + width += Util.getRequiredWidth(icon.getElement()); + } + if (captionText != null) { + int textWidth = captionText.getScrollWidth(); + if (BrowserInfo.get().isFF3()) { + /* + * In Firefox3 the caption might require more space than the + * scrollWidth returns as scrollWidth is rounded down. + */ + int requiredWidth = Util.getRequiredWidth(captionText); + if (requiredWidth > textWidth) { + textWidth = requiredWidth; + } + + } + width += textWidth; + } + if (requiredFieldIndicator != null) { + width += Util.getRequiredWidth(requiredFieldIndicator); + } + if (errorIndicatorElement != null) { + width += Util.getRequiredWidth(errorIndicatorElement); + } + + return width; + + } + + public int getHeight() { + int height = 0; + int h; + + if (icon != null) { + h = icon.getOffsetHeight(); + if (h > height) { + height = h; + } + } + + if (captionText != null) { + h = captionText.getOffsetHeight(); + if (h > height) { + height = h; + } + } + if (requiredFieldIndicator != null) { + h = requiredFieldIndicator.getOffsetHeight(); + if (h > height) { + height = h; + } + } + if (errorIndicatorElement != null) { + h = errorIndicatorElement.getOffsetHeight(); + if (h > height) { + height = h; + } + } + + return height; + } + + public void setAlignment(String alignment) { + DOM.setStyleAttribute(getElement(), "textAlign", alignment); + } + + public void setMaxWidth(int maxWidth) { + this.maxWidth = maxWidth; + DOM.setStyleAttribute(getElement(), "width", maxWidth + "px"); + + if (icon != null) { + DOM.setStyleAttribute(icon.getElement(), "width", ""); + } + + if (captionText != null) { + DOM.setStyleAttribute(captionText, "width", ""); + } + + int requiredWidth = getRequiredWidth(); + /* + * ApplicationConnection.getConsole().log( "Caption maxWidth: " + + * maxWidth + ", requiredWidth: " + requiredWidth); + */ + if (requiredWidth > maxWidth) { + // Needs to truncate and clip + int availableWidth = maxWidth; + + // DOM.setStyleAttribute(getElement(), "width", maxWidth + "px"); + if (requiredFieldIndicator != null) { + availableWidth -= Util.getRequiredWidth(requiredFieldIndicator); + } + + if (errorIndicatorElement != null) { + availableWidth -= Util.getRequiredWidth(errorIndicatorElement); + } + + if (availableWidth < 0) { + availableWidth = 0; + } + + if (icon != null) { + int iconRequiredWidth = Util + .getRequiredWidth(icon.getElement()); + if (availableWidth > iconRequiredWidth) { + availableWidth -= iconRequiredWidth; + } else { + DOM.setStyleAttribute(icon.getElement(), "width", + availableWidth + "px"); + availableWidth = 0; + } + } + if (captionText != null) { + int captionWidth = Util.getRequiredWidth(captionText); + if (availableWidth > captionWidth) { + availableWidth -= captionWidth; + + } else { + DOM.setStyleAttribute(captionText, "width", availableWidth + + "px"); + availableWidth = 0; + } + + } + + } + } + + protected Element getTextElement() { + return captionText; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ICaptionWrapper.java b/src/com/vaadin/terminal/gwt/client/ICaptionWrapper.java new file mode 100644 index 0000000000..15f1c5e820 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ICaptionWrapper.java @@ -0,0 +1,32 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Widget; + +public class ICaptionWrapper extends FlowPanel { + + public static final String CLASSNAME = "i-captionwrapper"; + ICaption caption; + Paintable widget; + + public ICaptionWrapper(Paintable toBeWrapped, ApplicationConnection client) { + caption = new ICaption(toBeWrapped, client); + add(caption); + widget = toBeWrapped; + add((Widget) widget); + setStyleName(CLASSNAME); + } + + public void updateCaption(UIDL uidl) { + caption.updateCaption(uidl); + setVisible(!uidl.getBooleanAttribute("invisible")); + } + + public Paintable getPaintable() { + return widget; + } +} diff --git a/src/com/vaadin/terminal/gwt/client/IDebugConsole.java b/src/com/vaadin/terminal/gwt/client/IDebugConsole.java new file mode 100755 index 0000000000..6986aea2ed --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/IDebugConsole.java @@ -0,0 +1,466 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import java.util.List; +import java.util.Set; + +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONValue; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.EventPreview; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.Window.Location; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.Tree; +import com.google.gwt.user.client.ui.TreeItem; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ui.IToolkitOverlay; + +public final class IDebugConsole extends IToolkitOverlay implements Console { + + /** + * Builds number. For example 0-custom_tag in 5.0.0-custom_tag. + */ + public static final String VERSION; + + /* Initialize version numbers from string replaced by build-script. */ + static { + if ("@VERSION@".equals("@" + "VERSION" + "@")) { + VERSION = "5.9.9-INTERNAL-NONVERSIONED-DEBUG-BUILD"; + } else { + VERSION = "@VERSION@"; + } + } + + Element caption = DOM.createDiv(); + + private Panel panel; + + private Button clear = new Button("Clear console"); + private Button restart = new Button("Restart app"); + private Button forceLayout = new Button("Force layout"); + private Button analyzeLayout = new Button("Analyze layouts"); + private HorizontalPanel actions; + private boolean collapsed = false; + + private boolean resizing; + private int startX; + private int startY; + private int initialW; + private int initialH; + + private boolean moving = false; + + private int origTop; + + private int origLeft; + + private ApplicationConnection client; + + private static final String help = "Drag=move, shift-drag=resize, doubleclick=min/max." + + "Use debug=quiet to log only to browser console."; + + public IDebugConsole(ApplicationConnection client, + ApplicationConfiguration cnf, boolean showWindow) { + super(false, false); + + this.client = client; + + panel = new FlowPanel(); + if (showWindow) { + DOM.appendChild(getContainerElement(), caption); + setWidget(panel); + caption.setClassName("i-debug-console-caption"); + setStyleName("i-debug-console"); + DOM.setStyleAttribute(getElement(), "zIndex", 20000 + ""); + DOM.setStyleAttribute(getElement(), "overflow", "hidden"); + + sinkEvents(Event.ONDBLCLICK); + + sinkEvents(Event.MOUSEEVENTS); + + panel.setStyleName("i-debug-console-content"); + + caption.setInnerHTML("Debug window"); + caption.setTitle(help); + + show(); + minimize(); + + actions = new HorizontalPanel(); + actions.add(clear); + actions.add(restart); + actions.add(forceLayout); + actions.add(analyzeLayout); + + panel.add(actions); + + panel.add(new HTML("<i>" + help + "</i>")); + + clear.addClickListener(new ClickListener() { + public void onClick(Widget sender) { + int width = panel.getOffsetWidth(); + int height = panel.getOffsetHeight(); + panel = new FlowPanel(); + panel.setPixelSize(width, height); + panel.setStyleName("i-debug-console-content"); + panel.add(actions); + setWidget(panel); + } + }); + + restart.addClickListener(new ClickListener() { + public void onClick(Widget sender) { + + String queryString = Window.Location.getQueryString(); + if (queryString != null + && queryString.contains("restartApplications")) { + Window.Location.reload(); + } else { + String url = Location.getHref(); + String separator = "?"; + if (url.contains("?")) { + separator = "&"; + } + if (!url.contains("restartApplication")) { + url += separator; + url += "restartApplication"; + } + if (!"".equals(Location.getHash())) { + String hash = Location.getHash(); + url = url.replace(hash, "") + hash; + } + Window.Location.replace(url); + } + + } + }); + + forceLayout.addClickListener(new ClickListener() { + public void onClick(Widget sender) { + IDebugConsole.this.client.forceLayout(); + } + }); + + analyzeLayout.addClickListener(new ClickListener() { + public void onClick(Widget sender) { + List<ApplicationConnection> runningApplications = ApplicationConfiguration + .getRunningApplications(); + for (ApplicationConnection applicationConnection : runningApplications) { + applicationConnection.analyzeLayouts(); + } + } + }); + analyzeLayout + .setTitle("Analyzes currently rendered view and " + + "reports possible common problems in usage of relative sizes." + + "Will cause server visit/rendering of whole screen + lose of" + + " all non committed variables form client side."); + + } + + log("Toolkit application servlet version: " + cnf.getServletVersion()); + log("Widget set is built on version: " + VERSION); + log("Application version: " + cnf.getApplicationVersion()); + + if (!cnf.getServletVersion().equals(VERSION)) { + error("Warning: your widget set seems to be built with a different " + + "version than the one used on server. Unexpected " + + "behavior may occur."); + } + } + + private EventPreview dragpreview = new EventPreview() { + + public boolean onEventPreview(Event event) { + onBrowserEvent(event); + return false; + } + }; + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEDOWN: + if (DOM.eventGetShiftKey(event)) { + resizing = true; + DOM.setCapture(getElement()); + startX = DOM.eventGetScreenX(event); + startY = DOM.eventGetScreenY(event); + initialW = IDebugConsole.this.getOffsetWidth(); + initialH = IDebugConsole.this.getOffsetHeight(); + DOM.eventCancelBubble(event, true); + DOM.eventPreventDefault(event); + DOM.addEventPreview(dragpreview); + } else if (DOM.eventGetTarget(event) == caption) { + moving = true; + startX = DOM.eventGetScreenX(event); + startY = DOM.eventGetScreenY(event); + origTop = getAbsoluteTop(); + origLeft = getAbsoluteLeft(); + DOM.eventCancelBubble(event, true); + DOM.eventPreventDefault(event); + DOM.addEventPreview(dragpreview); + } + + break; + case Event.ONMOUSEMOVE: + if (resizing) { + int deltaX = startX - DOM.eventGetScreenX(event); + int detalY = startY - DOM.eventGetScreenY(event); + int w = initialW - deltaX; + if (w < 30) { + w = 30; + } + int h = initialH - detalY; + if (h < 40) { + h = 40; + } + IDebugConsole.this.setPixelSize(w, h); + DOM.eventCancelBubble(event, true); + DOM.eventPreventDefault(event); + } else if (moving) { + int deltaX = startX - DOM.eventGetScreenX(event); + int detalY = startY - DOM.eventGetScreenY(event); + int left = origLeft - deltaX; + if (left < 0) { + left = 0; + } + int top = origTop - detalY; + if (top < 0) { + top = 0; + } + IDebugConsole.this.setPopupPosition(left, top); + DOM.eventCancelBubble(event, true); + DOM.eventPreventDefault(event); + } + break; + case Event.ONLOSECAPTURE: + case Event.ONMOUSEUP: + if (resizing) { + DOM.releaseCapture(getElement()); + resizing = false; + } else if (moving) { + DOM.releaseCapture(getElement()); + moving = false; + } + DOM.removeEventPreview(dragpreview); + break; + case Event.ONDBLCLICK: + if (DOM.eventGetTarget(event) == caption) { + if (collapsed) { + panel.setVisible(true); + setPixelSize(220, 300); + } else { + panel.setVisible(false); + setPixelSize(120, 20); + } + collapsed = !collapsed; + } + break; + default: + break; + } + + } + + private void minimize() { + setPixelSize(400, 150); + setPopupPosition(Window.getClientWidth() - 410, Window + .getClientHeight() - 160); + } + + @Override + public void setPixelSize(int width, int height) { + panel.setHeight((height - 20) + "px"); + panel.setWidth((width - 2) + "px"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Console#log(java.lang.String) + */ + public void log(String msg) { + panel.add(new HTML(msg)); + System.out.println(msg); + consoleLog(msg); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.Console#error(java.lang.String) + */ + public void error(String msg) { + panel.add((new HTML(msg))); + System.err.println(msg); + consoleErr(msg); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.Console#printObject(java.lang. + * Object) + */ + public void printObject(Object msg) { + panel.add((new Label(msg.toString()))); + consoleLog(msg.toString()); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.Console#dirUIDL(com.vaadin + * .terminal.gwt.client.UIDL) + */ + public void dirUIDL(UIDL u) { + panel.add(u.print_r()); + consoleLog(u.getChildrenAsXML()); + } + + private static native void consoleLog(String msg) + /*-{ + if($wnd.console && $wnd.console.log) { + $wnd.console.log(msg); + } + }-*/; + + private static native void consoleErr(String msg) + /*-{ + if($wnd.console) { + if ($wnd.console.error) + $wnd.console.error(msg); + else if ($wnd.console.log) + $wnd.console.log(msg); + } + }-*/; + + public void printLayoutProblems(JSONArray array, ApplicationConnection ac, + Set<Paintable> zeroHeightComponents, + Set<Paintable> zeroWidthComponents) { + int size = array.size(); + panel.add(new HTML("<div>************************</di>" + + "<h4>Layouts analyzed on server, total top level problems: " + + size + " </h4>")); + if (size > 0) { + Tree tree = new Tree(); + TreeItem root = new TreeItem("Root problems"); + for (int i = 0; i < size; i++) { + JSONObject error = array.get(i).isObject(); + printLayoutError(error, root, ac); + } + panel.add(tree); + tree.addItem(root); + + } + if (zeroHeightComponents.size() > 0 || zeroWidthComponents.size() > 0) { + panel.add(new HTML("<h4> Client side notifications</h4>" + + " <em>Following relative sized components where " + + "rendered to zero size container on client side." + + " Note that these are not necessary invalid " + + "states. Just reported here as they might be.</em>")); + if (zeroHeightComponents.size() > 0) { + panel.add(new HTML( + "<p><strong>Vertically zero size:</strong><p>")); + printClientSideDetectedIssues(zeroHeightComponents, ac); + } + if (zeroWidthComponents.size() > 0) { + panel.add(new HTML( + "<p><strong>Horizontally zero size:</strong><p>")); + printClientSideDetectedIssues(zeroWidthComponents, ac); + } + } + log("************************"); + } + + private void printClientSideDetectedIssues( + Set<Paintable> zeroHeightComponents, ApplicationConnection ac) { + for (final Paintable paintable : zeroHeightComponents) { + final Container layout = Util.getLayout((Widget) paintable); + + VerticalPanel errorDetails = new VerticalPanel(); + errorDetails.add(new Label("" + Util.getSimpleName(paintable) + + " inside " + Util.getSimpleName(layout))); + final CheckBox emphasisInUi = new CheckBox( + "Emphasis components parent in UI (actual component is not visible)"); + emphasisInUi.addClickListener(new ClickListener() { + public void onClick(Widget sender) { + if (paintable != null) { + Element element2 = ((Widget) layout).getElement(); + Widget.setStyleName(element2, "invalidlayout", + emphasisInUi.isChecked()); + } + } + }); + errorDetails.add(emphasisInUi); + panel.add(errorDetails); + } + } + + private void printLayoutError(JSONObject error, TreeItem parent, + final ApplicationConnection ac) { + final String pid = error.get("id").isString().stringValue(); + final Paintable paintable = ac.getPaintable(pid); + + TreeItem errorNode = new TreeItem(); + VerticalPanel errorDetails = new VerticalPanel(); + errorDetails.add(new Label(Util.getSimpleName(paintable) + " id: " + + pid)); + if (error.containsKey("heightMsg")) { + errorDetails.add(new Label("Height problem: " + + error.get("heightMsg"))); + } + if (error.containsKey("widthMsg")) { + errorDetails.add(new Label("Width problem: " + + error.get("widthMsg"))); + } + final CheckBox emphasisInUi = new CheckBox("Emphasis component in UI"); + emphasisInUi.addClickListener(new ClickListener() { + public void onClick(Widget sender) { + if (paintable != null) { + Element element2 = ((Widget) paintable).getElement(); + Widget.setStyleName(element2, "invalidlayout", emphasisInUi + .isChecked()); + } + } + }); + errorDetails.add(emphasisInUi); + errorNode.setWidget(errorDetails); + if (error.containsKey("subErrors")) { + HTML l = new HTML( + "<em>Expand this node to show problems that may be dependent on this problem.</em>"); + errorDetails.add(l); + JSONArray array = error.get("subErrors").isArray(); + for (int i = 0; i < array.size(); i++) { + JSONValue value = array.get(i); + if (value != null && value.isObject() != null) { + printLayoutError(value.isObject(), errorNode, ac); + } else { + System.out.print(value); + } + } + + } + parent.addItem(errorNode); + } +} diff --git a/src/com/vaadin/terminal/gwt/client/IErrorMessage.java b/src/com/vaadin/terminal/gwt/client/IErrorMessage.java new file mode 100644 index 0000000000..0f5f45663e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/IErrorMessage.java @@ -0,0 +1,73 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import java.util.Iterator; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.terminal.gwt.client.ui.IToolkitOverlay; + +public class IErrorMessage extends FlowPanel { + public static final String CLASSNAME = "i-errormessage"; + + public IErrorMessage() { + super(); + setStyleName(CLASSNAME); + } + + public void updateFromUIDL(UIDL uidl) { + clear(); + if (uidl.getChildCount() == 0) { + add(new HTML(" ")); + } else { + for (final Iterator it = uidl.getChildIterator(); it.hasNext();) { + final Object child = it.next(); + if (child instanceof String) { + final String errorMessage = (String) child; + add(new HTML(errorMessage)); + } else if (child instanceof UIDL.XML) { + final UIDL.XML xml = (UIDL.XML) child; + add(new HTML(xml.getXMLAsString())); + } else { + final IErrorMessage childError = new IErrorMessage(); + add(childError); + childError.updateFromUIDL((UIDL) child); + } + } + } + } + + /** + * Shows this error message next to given element. + * + * @param indicatorElement + */ + public void showAt(Element indicatorElement) { + IToolkitOverlay errorContainer = (IToolkitOverlay) getParent(); + if (errorContainer == null) { + errorContainer = new IToolkitOverlay(); + errorContainer.setWidget(this); + } + errorContainer.setPopupPosition(DOM.getAbsoluteLeft(indicatorElement) + + 2 + * DOM.getElementPropertyInt(indicatorElement, "offsetHeight"), + DOM.getAbsoluteTop(indicatorElement) + + 2 + * DOM.getElementPropertyInt(indicatorElement, + "offsetHeight")); + errorContainer.show(); + + } + + public void hide() { + final IToolkitOverlay errorContainer = (IToolkitOverlay) getParent(); + if (errorContainer != null) { + errorContainer.hide(); + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ITooltip.java b/src/com/vaadin/terminal/gwt/client/ITooltip.java new file mode 100644 index 0000000000..bbea0b4bae --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ITooltip.java @@ -0,0 +1,225 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.client; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +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.FlowPanel; +import com.vaadin.terminal.gwt.client.ui.IToolkitOverlay; + +/** + * TODO open for extension + */ +public class ITooltip extends IToolkitOverlay { + private static final String CLASSNAME = "i-tooltip"; + private static final int MARGIN = 4; + public static final int TOOLTIP_EVENTS = Event.ONKEYDOWN + | Event.ONMOUSEOVER | Event.ONMOUSEOUT | Event.ONMOUSEMOVE + | Event.ONCLICK; + protected static final int MAX_WIDTH = 500; + private static final int QUICK_OPEN_TIMEOUT = 1000; + private static final int CLOSE_TIMEOUT = 300; + private static final int OPEN_DELAY = 750; + private static final int QUICK_OPEN_DELAY = 100; + IErrorMessage em = new IErrorMessage(); + Element description = DOM.createDiv(); + private Paintable tooltipOwner; + private boolean closing = false; + private boolean opening = false; + private ApplicationConnection ac; + // Open next tooltip faster. Disabled after 2 sec of showTooltip-silence. + private boolean justClosed = false; + + public ITooltip(ApplicationConnection client) { + super(false, false, true); + ac = client; + setStyleName(CLASSNAME); + FlowPanel layout = new FlowPanel(); + setWidget(layout); + layout.add(em); + DOM.setElementProperty(description, "className", CLASSNAME + "-text"); + DOM.appendChild(layout.getElement(), description); + } + + private void show(TooltipInfo info) { + boolean hasContent = false; + if (info.getErrorUidl() != null) { + em.setVisible(true); + em.updateFromUIDL(info.getErrorUidl()); + hasContent = true; + } else { + em.setVisible(false); + } + if (info.getTitle() != null && !"".equals(info.getTitle())) { + DOM.setInnerHTML(description, info.getTitle()); + DOM.setStyleAttribute(description, "display", ""); + hasContent = true; + } else { + DOM.setInnerHTML(description, ""); + DOM.setStyleAttribute(description, "display", "none"); + } + if (hasContent) { + setPopupPositionAndShow(new PositionCallback() { + public void setPosition(int offsetWidth, int offsetHeight) { + + if (offsetWidth > MAX_WIDTH) { + setWidth(MAX_WIDTH + "px"); + } + + offsetWidth = getOffsetWidth(); + + int x = tooltipEventMouseX + 10 + Window.getScrollLeft(); + int y = tooltipEventMouseY + 10 + Window.getScrollTop(); + + if (x + offsetWidth + MARGIN - Window.getScrollLeft() > Window + .getClientWidth()) { + x = Window.getClientWidth() - offsetWidth - MARGIN; + } + + if (y + offsetHeight + MARGIN - Window.getScrollTop() > Window + .getClientHeight()) { + y = tooltipEventMouseY - 5 - offsetHeight; + } + + setPopupPosition(x, y); + sinkEvents(Event.ONMOUSEOVER | Event.ONMOUSEOUT); + } + }); + } else { + hide(); + } + } + + public void showTooltip(Paintable owner, Event event) { + if (closing && tooltipOwner == owner) { + // return to same tooltip, cancel closing + closeTimer.cancel(); + closing = false; + justClosedTimer.cancel(); + justClosed = false; + return; + } + + if (closing) { + closeNow(); + } + + updatePosition(event); + + if (opening) { + showTimer.cancel(); + } + tooltipOwner = owner; + if (justClosed) { + showTimer.schedule(QUICK_OPEN_DELAY); + } else { + showTimer.schedule(OPEN_DELAY); + } + opening = true; + } + + private void closeNow() { + if (closing) { + hide(); + tooltipOwner = null; + setWidth(""); + closing = false; + } + } + + private Timer showTimer = new Timer() { + @Override + public void run() { + TooltipInfo info = ac.getTitleInfo(tooltipOwner); + if (null != info) { + show(info); + } + opening = false; + } + }; + + private Timer closeTimer = new Timer() { + @Override + public void run() { + closeNow(); + justClosedTimer.schedule(2000); + justClosed = true; + } + }; + + private Timer justClosedTimer = new Timer() { + @Override + public void run() { + justClosed = false; + } + }; + + public void hideTooltip() { + if (opening) { + showTimer.cancel(); + opening = false; + tooltipOwner = null; + } + if (!isAttached()) { + return; + } + if (closing) { + // already about to close + return; + } + closeTimer.schedule(CLOSE_TIMEOUT); + closing = true; + justClosed = true; + justClosedTimer.schedule(QUICK_OPEN_TIMEOUT); + + } + + private int tooltipEventMouseX; + private int tooltipEventMouseY; + + public void updatePosition(Event event) { + tooltipEventMouseX = DOM.eventGetClientX(event); + tooltipEventMouseY = DOM.eventGetClientY(event); + + } + + public void handleTooltipEvent(Event event, Paintable owner) { + final int type = DOM.eventGetType(event); + if ((ITooltip.TOOLTIP_EVENTS & type) == type) { + if (type == Event.ONMOUSEOVER) { + showTooltip(owner, event); + } else if (type == Event.ONMOUSEMOVE) { + updatePosition(event); + } else { + hideTooltip(); + } + } else { + // non-tooltip event, hide tooltip + hideTooltip(); + } + } + + @Override + public void onBrowserEvent(Event event) { + final int type = DOM.eventGetType(event); + // cancel closing event if tooltip is mouseovered; the user might want + // to scroll of cut&paste + + switch (type) { + case Event.ONMOUSEOVER: + closeTimer.cancel(); + closing = false; + break; + case Event.ONMOUSEOUT: + hideTooltip(); + break; + default: + // NOP + } + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java b/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java new file mode 100644 index 0000000000..6312801daf --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java @@ -0,0 +1,13 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+@SuppressWarnings("serial")
+public class LocaleNotLoadedException extends Exception {
+
+ public LocaleNotLoadedException(String locale) {
+ super(locale);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/LocaleService.java b/src/com/vaadin/terminal/gwt/client/LocaleService.java new file mode 100644 index 0000000000..b62709fb2a --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/LocaleService.java @@ -0,0 +1,197 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONBoolean;
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+
+/**
+ * Date / time etc. localisation service for all widgets. Caches all loaded
+ * locales as JSONObjects.
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+public class LocaleService {
+
+ private static Map cache = new HashMap();
+ private static String defaultLocale;
+
+ public static void addLocale(JSONObject json) {
+ final String key = ((JSONString) json.get("name")).stringValue();
+ if (cache.containsKey(key)) {
+ cache.remove(key);
+ }
+ cache.put(key, json);
+ if (cache.size() == 1) {
+ setDefaultLocale(key);
+ }
+ }
+
+ public static void setDefaultLocale(String locale) {
+ defaultLocale = locale;
+ }
+
+ public static String getDefaultLocale() {
+ return defaultLocale;
+ }
+
+ public static Set getAvailableLocales() {
+ return cache.keySet();
+ }
+
+ public static String[] getMonthNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray mn = (JSONArray) l.get("mn");
+ final String[] temp = new String[12];
+ temp[0] = ((JSONString) mn.get(0)).stringValue();
+ temp[1] = ((JSONString) mn.get(1)).stringValue();
+ temp[2] = ((JSONString) mn.get(2)).stringValue();
+ temp[3] = ((JSONString) mn.get(3)).stringValue();
+ temp[4] = ((JSONString) mn.get(4)).stringValue();
+ temp[5] = ((JSONString) mn.get(5)).stringValue();
+ temp[6] = ((JSONString) mn.get(6)).stringValue();
+ temp[7] = ((JSONString) mn.get(7)).stringValue();
+ temp[8] = ((JSONString) mn.get(8)).stringValue();
+ temp[9] = ((JSONString) mn.get(9)).stringValue();
+ temp[10] = ((JSONString) mn.get(10)).stringValue();
+ temp[11] = ((JSONString) mn.get(11)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getShortMonthNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray smn = (JSONArray) l.get("smn");
+ final String[] temp = new String[12];
+ temp[0] = ((JSONString) smn.get(0)).stringValue();
+ temp[1] = ((JSONString) smn.get(1)).stringValue();
+ temp[2] = ((JSONString) smn.get(2)).stringValue();
+ temp[3] = ((JSONString) smn.get(3)).stringValue();
+ temp[4] = ((JSONString) smn.get(4)).stringValue();
+ temp[5] = ((JSONString) smn.get(5)).stringValue();
+ temp[6] = ((JSONString) smn.get(6)).stringValue();
+ temp[7] = ((JSONString) smn.get(7)).stringValue();
+ temp[8] = ((JSONString) smn.get(8)).stringValue();
+ temp[9] = ((JSONString) smn.get(9)).stringValue();
+ temp[10] = ((JSONString) smn.get(10)).stringValue();
+ temp[11] = ((JSONString) smn.get(11)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getDayNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray dn = (JSONArray) l.get("dn");
+ final String[] temp = new String[7];
+ temp[0] = ((JSONString) dn.get(0)).stringValue();
+ temp[1] = ((JSONString) dn.get(1)).stringValue();
+ temp[2] = ((JSONString) dn.get(2)).stringValue();
+ temp[3] = ((JSONString) dn.get(3)).stringValue();
+ temp[4] = ((JSONString) dn.get(4)).stringValue();
+ temp[5] = ((JSONString) dn.get(5)).stringValue();
+ temp[6] = ((JSONString) dn.get(6)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getShortDayNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray sdn = (JSONArray) l.get("sdn");
+ final String[] temp = new String[7];
+ temp[0] = ((JSONString) sdn.get(0)).stringValue();
+ temp[1] = ((JSONString) sdn.get(1)).stringValue();
+ temp[2] = ((JSONString) sdn.get(2)).stringValue();
+ temp[3] = ((JSONString) sdn.get(3)).stringValue();
+ temp[4] = ((JSONString) sdn.get(4)).stringValue();
+ temp[5] = ((JSONString) sdn.get(5)).stringValue();
+ temp[6] = ((JSONString) sdn.get(6)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static int getFirstDayOfWeek(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONNumber fdow = (JSONNumber) l.get("fdow");
+ return (int) fdow.getValue();
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String getDateFormat(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONString df = (JSONString) l.get("df");
+ return df.stringValue();
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static boolean isTwelveHourClock(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONBoolean thc = (JSONBoolean) l.get("thc");
+ return thc.booleanValue();
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String getClockDelimiter(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONString hmd = (JSONString) l.get("hmd");
+ return hmd.stringValue();
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getAmPmStrings(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray ampm = (JSONArray) l.get("ampm");
+ final String[] temp = new String[2];
+ temp[0] = ((JSONString) ampm.get(0)).stringValue();
+ temp[1] = ((JSONString) ampm.get(1)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/MouseEventDetails.java b/src/com/vaadin/terminal/gwt/client/MouseEventDetails.java new file mode 100644 index 0000000000..175dfd8ce2 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/MouseEventDetails.java @@ -0,0 +1,93 @@ +package com.vaadin.terminal.gwt.client; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; + +/** + * Helper class to store and transfer mouse event details. + */ +public class MouseEventDetails { + public static final int BUTTON_LEFT = Event.BUTTON_LEFT; + public static final int BUTTON_MIDDLE = Event.BUTTON_MIDDLE; + public static final int BUTTON_RIGHT = Event.BUTTON_RIGHT; + + private static final char DELIM = ','; + + private int button; + private int clientX; + private int clientY; + private boolean altKey; + private boolean ctrlKey; + private boolean metaKey; + private boolean shiftKey; + private int type; + + public int getButton() { + return button; + } + + public int getClientX() { + return clientX; + } + + public int getClientY() { + return clientY; + } + + public boolean isAltKey() { + return altKey; + } + + public boolean isCtrlKey() { + return ctrlKey; + } + + public boolean isMetaKey() { + return metaKey; + } + + public boolean isShiftKey() { + return shiftKey; + } + + public MouseEventDetails(Event evt) { + button = DOM.eventGetButton(evt); + clientX = DOM.eventGetClientX(evt); + clientY = DOM.eventGetClientY(evt); + altKey = DOM.eventGetAltKey(evt); + ctrlKey = DOM.eventGetCtrlKey(evt); + metaKey = DOM.eventGetMetaKey(evt); + shiftKey = DOM.eventGetShiftKey(evt); + type = DOM.eventGetType(evt); + } + + private MouseEventDetails() { + } + + @Override + public String toString() { + return "" + button + DELIM + clientX + DELIM + clientY + DELIM + altKey + + DELIM + ctrlKey + DELIM + metaKey + DELIM + shiftKey + DELIM + + type; + } + + public static MouseEventDetails deSerialize(String serializedString) { + MouseEventDetails instance = new MouseEventDetails(); + String[] fields = serializedString.split(","); + + instance.button = Integer.parseInt(fields[0]); + instance.clientX = Integer.parseInt(fields[1]); + instance.clientY = Integer.parseInt(fields[2]); + instance.altKey = Boolean.valueOf(fields[3]).booleanValue(); + instance.ctrlKey = Boolean.valueOf(fields[4]).booleanValue(); + instance.metaKey = Boolean.valueOf(fields[5]).booleanValue(); + instance.shiftKey = Boolean.valueOf(fields[6]).booleanValue(); + instance.type = Integer.parseInt(fields[7]); + return instance; + } + + public boolean isDoubleClick() { + return type == Event.ONDBLCLICK; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/NullConsole.java b/src/com/vaadin/terminal/gwt/client/NullConsole.java new file mode 100644 index 0000000000..24c692d44a --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/NullConsole.java @@ -0,0 +1,36 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import java.util.Set; + +import com.google.gwt.json.client.JSONArray; + +/** + * Client side console implementation for non-debug mode that discards all + * messages. + * + */ +public class NullConsole implements Console { + + public void dirUIDL(UIDL u) { + } + + public void error(String msg) { + } + + public void log(String msg) { + } + + public void printObject(Object msg) { + } + + public void printLayoutProblems(JSONArray array, + ApplicationConnection applicationConnection, + Set<Paintable> zeroHeightComponents, + Set<Paintable> zeroWidthComponents) { + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/Paintable.java b/src/com/vaadin/terminal/gwt/client/Paintable.java new file mode 100644 index 0000000000..d4ff755763 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/Paintable.java @@ -0,0 +1,10 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +public interface Paintable { + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client); +} diff --git a/src/com/vaadin/terminal/gwt/client/RenderInformation.java b/src/com/vaadin/terminal/gwt/client/RenderInformation.java new file mode 100644 index 0000000000..4dd7d1ee2f --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/RenderInformation.java @@ -0,0 +1,125 @@ +package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.user.client.Element;
+
+/**
+ * Contains size information about a rendered container and its content area.
+ *
+ * @author Artur Signell
+ *
+ */
+public class RenderInformation {
+
+ private RenderSpace contentArea = new RenderSpace();
+ private Size renderedSize = new Size(-1, -1);
+
+ public void setContentAreaWidth(int w) {
+ contentArea.setWidth(w);
+ }
+
+ public void setContentAreaHeight(int h) {
+ contentArea.setHeight(h);
+ }
+
+ public RenderSpace getContentAreaSize() {
+ return contentArea;
+
+ }
+
+ public Size getRenderedSize() {
+ return renderedSize;
+ }
+
+ /**
+ * Update the size of the widget.
+ *
+ * @param widget
+ *
+ * @return true if the size has changed since last update
+ */
+ public boolean updateSize(Element element) {
+ Size newSize = new Size(element.getOffsetWidth(), element
+ .getOffsetHeight());
+ if (newSize.equals(renderedSize)) {
+ return false;
+ } else {
+ renderedSize = newSize;
+ return true;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "RenderInformation [contentArea=" + contentArea
+ + ",renderedSize=" + renderedSize + "]";
+
+ }
+
+ public static class FloatSize {
+
+ private float width, height;
+
+ public FloatSize(float width, float height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ public float getWidth() {
+ return width;
+ }
+
+ public void setWidth(float width) {
+ this.width = width;
+ }
+
+ public float getHeight() {
+ return height;
+ }
+
+ public void setHeight(float height) {
+ this.height = height;
+ }
+
+ }
+
+ public static class Size {
+
+ private int width, height;
+
+ @Override
+ public boolean equals(Object obj) {
+ Size other = (Size) obj;
+ return other.width == width && other.height == height;
+ }
+
+ public Size() {
+ }
+
+ public Size(int width, int height) {
+ this.height = height;
+ this.width = width;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ @Override
+ public String toString() {
+ return "Size [width=" + width + ",height=" + height + "]";
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/RenderSpace.java b/src/com/vaadin/terminal/gwt/client/RenderSpace.java new file mode 100644 index 0000000000..42ec8f7fcb --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/RenderSpace.java @@ -0,0 +1,53 @@ +package com.vaadin.terminal.gwt.client; + +import com.vaadin.terminal.gwt.client.RenderInformation.Size; + +/** + * Contains information about render area. + */ +public class RenderSpace extends Size { + + private int scrollBarSize = 0; + + public RenderSpace(int width, int height) { + super(width, height); + } + + public RenderSpace() { + } + + public RenderSpace(int width, int height, boolean useNativeScrollbarSize) { + super(width, height); + if (useNativeScrollbarSize) { + scrollBarSize = Util.getNativeScrollbarSize(); + } + } + + /** + * Returns pixels available vertically for contained widget, including + * possible scrollbars. + */ + @Override + public int getHeight() { + return super.getHeight(); + } + + /** + * Returns pixels available horizontally for contained widget, including + * possible scrollbars. + */ + @Override + public int getWidth() { + return super.getWidth(); + } + + /** + * In case containing block has oveflow: auto, this method must return + * number of pixels used by scrollbar. Returning zero means either that no + * scrollbar will be visible. + */ + public int getScrollbarSize() { + return scrollBarSize; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/StyleConstants.java b/src/com/vaadin/terminal/gwt/client/StyleConstants.java new file mode 100644 index 0000000000..9d8f228d3d --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/StyleConstants.java @@ -0,0 +1,17 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +public class StyleConstants { + + public static final String MARGIN_TOP = "margin-top"; + public static final String MARGIN_RIGHT = "margin-right"; + public static final String MARGIN_BOTTOM = "margin-bottom"; + public static final String MARGIN_LEFT = "margin-left"; + + public static final String VERTICAL_SPACING = "vspacing"; + public static final String HORIZONTAL_SPACING = "hspacing"; + +} diff --git a/src/com/vaadin/terminal/gwt/client/TooltipInfo.java b/src/com/vaadin/terminal/gwt/client/TooltipInfo.java new file mode 100644 index 0000000000..9a66bb14c8 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/TooltipInfo.java @@ -0,0 +1,28 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ +package com.vaadin.terminal.gwt.client; + +public class TooltipInfo { + + private String title; + + private UIDL errorUidl; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public UIDL getErrorUidl() { + return errorUidl; + } + + public void setErrorUidl(UIDL errorUidl) { + this.errorUidl = errorUidl; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/UIDL.java b/src/com/vaadin/terminal/gwt/client/UIDL.java new file mode 100644 index 0000000000..2205f6b6dd --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/UIDL.java @@ -0,0 +1,472 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONBoolean; +import com.google.gwt.json.client.JSONNumber; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONString; +import com.google.gwt.json.client.JSONValue; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.Tree; +import com.google.gwt.user.client.ui.TreeItem; +import com.google.gwt.user.client.ui.TreeListener; + +public class UIDL { + + JSONArray json; + + public UIDL(JSONArray json) { + this.json = json; + } + + public String getId() { + final JSONValue val = ((JSONObject) json.get(1)).get("id"); + if (val == null) { + return null; + } + return ((JSONString) val).stringValue(); + } + + public String getTag() { + return ((JSONString) json.get(0)).stringValue(); + } + + public String getStringAttribute(String name) { + final JSONValue val = ((JSONObject) json.get(1)).get(name); + if (val == null) { + return null; + } + return ((JSONString) val).stringValue(); + } + + public Set getAttributeNames() { + final HashSet attrs = new HashSet(((JSONObject) json.get(1)).keySet()); + attrs.remove("v"); + return attrs; + } + + public int getIntAttribute(String name) { + final JSONValue val = ((JSONObject) json.get(1)).get(name); + if (val == null) { + return 0; + } + final double num = ((JSONNumber) val).getValue(); + return (int) num; + } + + public long getLongAttribute(String name) { + final JSONValue val = ((JSONObject) json.get(1)).get(name); + if (val == null) { + return 0; + } + final double num = ((JSONNumber) val).getValue(); + return (long) num; + } + + public float getFloatAttribute(String name) { + final JSONValue val = ((JSONObject) json.get(1)).get(name); + if (val == null) { + return 0; + } + final double num = ((JSONNumber) val).getValue(); + return (float) num; + } + + public double getDoubleAttribute(String name) { + final JSONValue val = ((JSONObject) json.get(1)).get(name); + if (val == null) { + return 0; + } + final double num = ((JSONNumber) val).getValue(); + return num; + } + + public boolean getBooleanAttribute(String name) { + final JSONValue val = ((JSONObject) json.get(1)).get(name); + if (val == null) { + return false; + } + return ((JSONBoolean) val).booleanValue(); + } + + public String[] getStringArrayAttribute(String name) { + final JSONArray a = (JSONArray) ((JSONObject) json.get(1)).get(name); + final String[] s = new String[a.size()]; + for (int i = 0; i < a.size(); i++) { + s[i] = ((JSONString) a.get(i)).stringValue(); + } + return s; + } + + public int[] getIntArrayAttribute(String name) { + final JSONArray a = (JSONArray) ((JSONObject) json.get(1)).get(name); + final int[] s = new int[a.size()]; + for (int i = 0; i < a.size(); i++) { + s[i] = Integer.parseInt(((JSONString) a.get(i)).stringValue()); + } + return s; + } + + public HashSet getStringArrayAttributeAsSet(String string) { + final JSONArray a = getArrayVariable(string); + final HashSet s = new HashSet(); + for (int i = 0; i < a.size(); i++) { + s.add(((JSONString) a.get(i)).stringValue()); + } + return s; + } + + /** + * Get attributes value as string whateever the type is + * + * @param name + * @return string presentation of attribute + */ + private String getAttribute(String name) { + return json.get(1).isObject().get(name).toString(); + } + + public boolean hasAttribute(String name) { + return ((JSONObject) json.get(1)).get(name) != null; + } + + public UIDL getChildUIDL(int i) { + + final JSONValue c = json.get(i + 2); + if (c == null) { + return null; + } + if (c.isArray() != null) { + return new UIDL(c.isArray()); + } + throw new IllegalStateException("Child node " + i + + " is not of type UIDL"); + } + + public String getChildString(int i) { + + final JSONValue c = json.get(i + 2); + if (c.isString() != null) { + return ((JSONString) c).stringValue(); + } + throw new IllegalStateException("Child node " + i + + " is not of type String"); + } + + public Iterator getChildIterator() { + + return new Iterator() { + + int index = 2; + + public void remove() { + throw new UnsupportedOperationException(); + } + + public Object next() { + + if (json.size() > index) { + final JSONValue c = json.get(index++); + if (c.isString() != null) { + return c.isString().stringValue(); + } else if (c.isArray() != null) { + return new UIDL(c.isArray()); + } else if (c.isObject() != null) { + return new XML(c.isObject()); + } else { + throw new IllegalStateException("Illegal child " + c + + " in tag " + getTag() + " at index " + index); + } + } + return null; + } + + public boolean hasNext() { + return json.size() > index; + } + + }; + } + + public int getNumberOfChildren() { + return json.size() - 2; + } + + @Override + public String toString() { + String s = "<" + getTag(); + + for (final Iterator i = getAttributeNames().iterator(); i.hasNext();) { + final String name = i.next().toString(); + s += " " + name + "="; + final JSONValue v = ((JSONObject) json.get(1)).get(name); + if (v.isString() != null) { + s += v; + } else { + s += "\"" + v + "\""; + } + } + + s += ">\n"; + + final Iterator i = getChildIterator(); + while (i.hasNext()) { + final Object c = i.next(); + s += c.toString(); + } + + s += "</" + getTag() + ">\n"; + + return s; + } + + public String getChildrenAsXML() { + String s = ""; + final Iterator i = getChildIterator(); + while (i.hasNext()) { + final Object c = i.next(); + s += c.toString(); + } + return s; + } + + public IUIDLBrowser print_r() { + return new IUIDLBrowser(); + } + + private class IUIDLBrowser extends Tree { + public IUIDLBrowser() { + + DOM.setStyleAttribute(getElement(), "position", ""); + + final TreeItem root = new TreeItem(getTag()); + addItem(root); + root.addItem(""); + addTreeListener(new TreeListener() { + + public void onTreeItemStateChanged(TreeItem item) { + if (item == root) { + removeItem(root); + IUIDLBrowser.this.removeTreeListener(this); + addItem(dir()); + final Iterator it = treeItemIterator(); + while (it.hasNext()) { + ((TreeItem) it.next()).setState(true); + } + } + } + + public void onTreeItemSelected(TreeItem item) { + } + + }); + + } + + @Override + protected boolean isKeyboardNavigationEnabled(TreeItem currentItem) { + return false; + } + + } + + public TreeItem dir() { + + String nodeName = getTag(); + for (final Iterator i = getAttributeNames().iterator(); i.hasNext();) { + final String name = i.next().toString(); + final String value = getAttribute(name); + nodeName += " " + name + "=" + value; + } + final TreeItem item = new TreeItem(nodeName); + + try { + TreeItem tmp = null; + for (final Iterator i = getVariableHash().keySet().iterator(); i + .hasNext();) { + final String name = i.next().toString(); + String value = ""; + try { + value = getStringVariable(name); + } catch (final Exception e) { + try { + final JSONArray a = getArrayVariable(name); + value = a.toString(); + } catch (final Exception e2) { + try { + final int intVal = getIntVariable(name); + value = String.valueOf(intVal); + } catch (final Exception e3) { + value = "unknown"; + } + } + } + if (tmp == null) { + tmp = new TreeItem("variables"); + } + tmp.addItem(name + "=" + value); + } + if (tmp != null) { + item.addItem(tmp); + } + } catch (final Exception e) { + // Ignored, no variables + } + + final Iterator i = getChildIterator(); + while (i.hasNext()) { + final Object child = i.next(); + try { + final UIDL c = (UIDL) child; + item.addItem(c.dir()); + + } catch (final Exception e) { + item.addItem(child.toString()); + } + } + return item; + } + + private JSONObject getVariableHash() { + final JSONObject v = (JSONObject) ((JSONObject) json.get(1)).get("v"); + if (v == null) { + throw new IllegalArgumentException("No variables defined in tag."); + } + return v; + } + + public boolean hasVariable(String name) { + Object v = null; + try { + v = getVariableHash().get(name); + } catch (final IllegalArgumentException e) { + } + return v != null; + } + + public String getStringVariable(String name) { + final JSONString t = (JSONString) getVariableHash().get(name); + if (t == null) { + throw new IllegalArgumentException("No such variable: " + name); + } + return t.stringValue(); + } + + public int getIntVariable(String name) { + final JSONNumber t = (JSONNumber) getVariableHash().get(name); + if (t == null) { + throw new IllegalArgumentException("No such variable: " + name); + } + return (int) t.getValue(); + } + + public long getLongVariable(String name) { + final JSONNumber t = (JSONNumber) getVariableHash().get(name); + if (t == null) { + throw new IllegalArgumentException("No such variable: " + name); + } + return (long) t.getValue(); + } + + public float getFloatVariable(String name) { + final JSONNumber t = (JSONNumber) getVariableHash().get(name); + if (t == null) { + throw new IllegalArgumentException("No such variable: " + name); + } + return (float) t.getValue(); + } + + public double getDoubleVariable(String name) { + final JSONNumber t = (JSONNumber) getVariableHash().get(name); + if (t == null) { + throw new IllegalArgumentException("No such variable: " + name); + } + return t.getValue(); + } + + public boolean getBooleanVariable(String name) { + final JSONBoolean t = (JSONBoolean) getVariableHash().get(name); + if (t == null) { + throw new IllegalArgumentException("No such variable: " + name); + } + return t.booleanValue(); + } + + private JSONArray getArrayVariable(String name) { + final JSONArray t = (JSONArray) getVariableHash().get(name); + if (t == null) { + throw new IllegalArgumentException("No such variable: " + name); + } + return t; + } + + public String[] getStringArrayVariable(String name) { + final JSONArray a = getArrayVariable(name); + final String[] s = new String[a.size()]; + for (int i = 0; i < a.size(); i++) { + s[i] = ((JSONString) a.get(i)).stringValue(); + } + return s; + } + + public Set<String> getStringArrayVariableAsSet(String name) { + final JSONArray a = getArrayVariable(name); + final HashSet<String> s = new HashSet<String>(); + for (int i = 0; i < a.size(); i++) { + s.add(((JSONString) a.get(i)).stringValue()); + } + return s; + } + + public int[] getIntArrayVariable(String name) { + final JSONArray a = getArrayVariable(name); + final int[] s = new int[a.size()]; + for (int i = 0; i < a.size(); i++) { + final JSONValue v = a.get(i); + s[i] = v.isNumber() != null ? (int) ((JSONNumber) v).getValue() + : Integer.parseInt(v.toString()); + } + return s; + } + + public class XML { + JSONObject x; + + private XML(JSONObject x) { + this.x = x; + } + + public String getXMLAsString() { + final StringBuffer sb = new StringBuffer(); + for (final Iterator it = x.keySet().iterator(); it.hasNext();) { + final String tag = (String) it.next(); + sb.append("<"); + sb.append(tag); + sb.append(">"); + sb.append(x.get(tag).isString().stringValue()); + sb.append("</"); + sb.append(tag); + sb.append(">"); + } + return sb.toString(); + } + } + + public int getChildCount() { + return json.size() - 2; + } + + public UIDL getErrors() { + final JSONArray a = (JSONArray) ((JSONObject) json.get(1)).get("error"); + return new UIDL(a); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/Util.java b/src/com/vaadin/terminal/gwt/client/Util.java new file mode 100644 index 0000000000..99e9e8cce1 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/Util.java @@ -0,0 +1,703 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize; + +public class Util { + + /** + * Helper method for debugging purposes. + * + * Stops execution on firefox browsers on a breakpoint. + * + */ + public static native void browserDebugger() + /*-{ + if($wnd.console) + debugger; + }-*/; + + private static final int LAZY_SIZE_CHANGE_TIMEOUT = 400; + private static Set<Paintable> latelyChangedWidgets = new HashSet<Paintable>(); + + private static Timer lazySizeChangeTimer = new Timer() { + private boolean lazySizeChangeTimerScheduled = false; + + @Override + public void run() { + componentSizeUpdated(latelyChangedWidgets); + latelyChangedWidgets.clear(); + lazySizeChangeTimerScheduled = false; + } + + @Override + public void schedule(int delayMillis) { + if (lazySizeChangeTimerScheduled) { + cancel(); + } else { + lazySizeChangeTimerScheduled = true; + } + super.schedule(delayMillis); + } + }; + + /** + * This helper method can be called if components size have been changed + * outside rendering phase. It notifies components parent about the size + * change so it can react. + * + * When using this method, developer should consider if size changes could + * be notified lazily. If lazy flag is true, method will save widget and + * wait for a moment until it notifies parents in chunks. This may vastly + * optimize layout in various situation. Example: if component have a lot of + * images their onload events may fire "layout phase" many times in a short + * period. + * + * @param widget + * @param lazy + * run componentSizeUpdated lazyly + */ + public static void notifyParentOfSizeChange(Paintable widget, boolean lazy) { + if (lazy) { + latelyChangedWidgets.add(widget); + lazySizeChangeTimer.schedule(LAZY_SIZE_CHANGE_TIMEOUT); + } else { + Set<Paintable> widgets = new HashSet<Paintable>(); + widgets.add(widget); + Util.componentSizeUpdated(widgets); + } + } + + /** + * Called when the size of one or more widgets have changed during + * rendering. Finds parent container and notifies them of the size change. + * + * @param widgets + */ + public static void componentSizeUpdated(Set<Paintable> widgets) { + if (widgets.isEmpty()) { + return; + } + + Map<Container, Set<Paintable>> childWidgets = new HashMap<Container, Set<Paintable>>(); + + for (Paintable widget : widgets) { + // ApplicationConnection.getConsole().log( + // "Widget " + Util.getSimpleName(widget) + " size updated"); + Widget parent = ((Widget) widget).getParent(); + while (parent != null && !(parent instanceof Container)) { + parent = parent.getParent(); + } + if (parent != null) { + Set<Paintable> set = childWidgets.get(parent); + if (set == null) { + set = new HashSet<Paintable>(); + childWidgets.put((Container) parent, set); + } + set.add(widget); + } + } + + Set<Paintable> parentChanges = new HashSet<Paintable>(); + for (Container parent : childWidgets.keySet()) { + if (!parent.requestLayout(childWidgets.get(parent))) { + parentChanges.add(parent); + } + } + + componentSizeUpdated(parentChanges); + } + + public static float parseRelativeSize(String size) { + if (size == null || !size.endsWith("%")) { + return -1; + } + + try { + return Float.parseFloat(size.substring(0, size.length() - 1)); + } catch (Exception e) { + ClientExceptionHandler.displayError( + "Unable to parse relative size", e); + } + + return -1; + } + + /** + * Returns closest parent Widget in hierarchy that implements Container + * interface + * + * @param component + * @return closest parent Container + */ + public static Container getLayout(Widget component) { + Widget parent = component.getParent(); + while (parent != null && !(parent instanceof Container)) { + parent = parent.getParent(); + } + if (parent != null) { + assert ((Container) parent).hasChildComponent(component); + + return (Container) parent; + } + return null; + } + + /** + * Detects if current browser is IE. + * + * @deprecated use BrowserInfo class instead + * + * @return true if IE + */ + @Deprecated + public static boolean isIE() { + return BrowserInfo.get().isIE(); + } + + /** + * Detects if current browser is IE6. + * + * @deprecated use BrowserInfo class instead + * + * @return true if IE6 + */ + @Deprecated + public static boolean isIE6() { + return BrowserInfo.get().isIE6(); + } + + /** + * @deprecated use BrowserInfo class instead + * @return + */ + @Deprecated + public static boolean isIE7() { + return BrowserInfo.get().isIE7(); + } + + /** + * @deprecated use BrowserInfo class instead + * @return + */ + @Deprecated + public static boolean isFF2() { + return BrowserInfo.get().isFF2(); + } + + private static final Element escapeHtmlHelper = DOM.createDiv(); + + /** + * Converts html entities to text. + * + * @param html + * @return escaped string presentation of given html + */ + public static String escapeHTML(String html) { + DOM.setInnerText(escapeHtmlHelper, html); + return DOM.getInnerHTML(escapeHtmlHelper); + } + + /** + * Adds transparent PNG fix to image element; only use for IE6. + * + * @param el + * IMG element + * @param blankImageUrl + * URL to transparent one-pixel gif + */ + public native static void addPngFix(Element el, String blankImageUrl) + /*-{ + el.attachEvent("onload", function() { + var src = el.src; + if (src.indexOf(".png")<1) return; + var w = el.width||16; + var h = el.height||16; + el.src =blankImageUrl; + el.style.height = h+"px"; + el.style.width = w+"px"; + el.style.padding = "0px"; + el.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+src+"', sizingMethod='scale')"; + },false); + }-*/; + + /** + * Clones given element as in JavaScript. + * + * Deprecate this if there appears similar method into GWT someday. + * + * @param element + * @param deep + * clone child tree also + * @return + */ + public static native Element cloneNode(Element element, boolean deep) + /*-{ + return element.cloneNode(deep); + }-*/; + + public static int measureHorizontalPaddingAndBorder(Element element, + int paddingGuess) { + String originalWidth = DOM.getStyleAttribute(element, "width"); + int originalOffsetWidth = element.getOffsetWidth(); + int widthGuess = (originalOffsetWidth - paddingGuess); + if (widthGuess < 1) { + widthGuess = 1; + } + DOM.setStyleAttribute(element, "width", widthGuess + "px"); + int padding = element.getOffsetWidth() - widthGuess; + + DOM.setStyleAttribute(element, "width", originalWidth); + return padding; + } + + public static int measureVerticalPaddingAndBorder(Element element, + int paddingGuess) { + String originalHeight = DOM.getStyleAttribute(element, "height"); + int originalOffsetHeight = element.getOffsetHeight(); + int widthGuess = (originalOffsetHeight - paddingGuess); + if (widthGuess < 1) { + widthGuess = 1; + } + DOM.setStyleAttribute(element, "height", widthGuess + "px"); + int padding = element.getOffsetHeight() - widthGuess; + + DOM.setStyleAttribute(element, "height", originalHeight); + return padding; + } + + public static int measureHorizontalBorder(Element element) { + int borders; + if (BrowserInfo.get().isIE()) { + String width = element.getStyle().getProperty("width"); + String height = element.getStyle().getProperty("height"); + + int offsetWidth = element.getOffsetWidth(); + int offsetHeight = element.getOffsetHeight(); + if (BrowserInfo.get().isIE6()) { + if (offsetHeight < 1) { + offsetHeight = 1; + } + if (offsetWidth < 1) { + offsetWidth = 10; + } + element.getStyle().setPropertyPx("height", offsetHeight); + } + element.getStyle().setPropertyPx("width", offsetWidth); + + borders = element.getOffsetWidth() + - element.getPropertyInt("clientWidth"); + + element.getStyle().setProperty("width", width); + if (BrowserInfo.get().isIE6()) { + element.getStyle().setProperty("height", height); + } + } else { + borders = element.getOffsetWidth() + - element.getPropertyInt("clientWidth"); + } + assert borders >= 0; + + return borders; + } + + public static int measureVerticalBorder(Element element) { + int borders; + if (BrowserInfo.get().isIE()) { + String width = element.getStyle().getProperty("width"); + String height = element.getStyle().getProperty("height"); + + int offsetWidth = element.getOffsetWidth(); + int offsetHeight = element.getOffsetHeight(); + // if (BrowserInfo.get().isIE6()) { + if (offsetHeight < 1) { + offsetHeight = 1; + } + if (offsetWidth < 1) { + offsetWidth = 10; + } + element.getStyle().setPropertyPx("width", offsetWidth); + // } + + element.getStyle().setPropertyPx("height", offsetHeight); + + borders = element.getOffsetHeight() + - element.getPropertyInt("clientHeight"); + + element.getStyle().setProperty("height", height); + // if (BrowserInfo.get().isIE6()) { + element.getStyle().setProperty("width", width); + // } + } else { + borders = element.getOffsetHeight() + - element.getPropertyInt("clientHeight"); + } + assert borders >= 0; + + return borders; + } + + public static int measureMarginLeft(Element element) { + return element.getAbsoluteLeft() + - element.getParentElement().getAbsoluteLeft(); + } + + public static int setHeightExcludingPaddingAndBorder(Widget widget, + String height, int paddingBorderGuess) { + if (height.equals("")) { + setHeight(widget, ""); + return paddingBorderGuess; + } else if (height.endsWith("px")) { + int pixelHeight = Integer.parseInt(height.substring(0, height + .length() - 2)); + return setHeightExcludingPaddingAndBorder(widget.getElement(), + pixelHeight, paddingBorderGuess, false); + } else { + // Set the height in unknown units + setHeight(widget, height); + // Use the offsetWidth + return setHeightExcludingPaddingAndBorder(widget.getElement(), + widget.getOffsetHeight(), paddingBorderGuess, true); + } + } + + private static void setWidth(Widget widget, String width) { + DOM.setStyleAttribute(widget.getElement(), "width", width); + } + + private static void setHeight(Widget widget, String height) { + DOM.setStyleAttribute(widget.getElement(), "height", height); + } + + public static int setWidthExcludingPaddingAndBorder(Widget widget, + String width, int paddingBorderGuess) { + if (width.equals("")) { + setWidth(widget, ""); + return paddingBorderGuess; + } else if (width.endsWith("px")) { + int pixelWidth = Integer.parseInt(width.substring(0, + width.length() - 2)); + return setWidthExcludingPaddingAndBorder(widget.getElement(), + pixelWidth, paddingBorderGuess, false); + } else { + setWidth(widget, width); + return setWidthExcludingPaddingAndBorder(widget.getElement(), + widget.getOffsetWidth(), paddingBorderGuess, true); + } + } + + public static int setWidthExcludingPaddingAndBorder(Element element, + int requestedWidth, int horizontalPaddingBorderGuess, + boolean requestedWidthIncludesPaddingBorder) { + + int widthGuess = requestedWidth - horizontalPaddingBorderGuess; + if (widthGuess < 0) { + widthGuess = 0; + } + + DOM.setStyleAttribute(element, "width", widthGuess + "px"); + int captionOffsetWidth = DOM.getElementPropertyInt(element, + "offsetWidth"); + + int actualPadding = captionOffsetWidth - widthGuess; + + if (requestedWidthIncludesPaddingBorder) { + actualPadding += actualPadding; + } + + if (actualPadding != horizontalPaddingBorderGuess) { + int w = requestedWidth - actualPadding; + if (w < 0) { + // Cannot set negative width even if we would want to + w = 0; + } + DOM.setStyleAttribute(element, "width", w + "px"); + + } + + return actualPadding; + + } + + public static int setHeightExcludingPaddingAndBorder(Element element, + int requestedHeight, int verticalPaddingBorderGuess, + boolean requestedHeightIncludesPaddingBorder) { + + int heightGuess = requestedHeight - verticalPaddingBorderGuess; + if (heightGuess < 0) { + heightGuess = 0; + } + + DOM.setStyleAttribute(element, "height", heightGuess + "px"); + int captionOffsetHeight = DOM.getElementPropertyInt(element, + "offsetHeight"); + + int actualPadding = captionOffsetHeight - heightGuess; + + if (requestedHeightIncludesPaddingBorder) { + actualPadding += actualPadding; + } + + if (actualPadding != verticalPaddingBorderGuess) { + int h = requestedHeight - actualPadding; + if (h < 0) { + // Cannot set negative height even if we would want to + h = 0; + } + DOM.setStyleAttribute(element, "height", h + "px"); + + } + + return actualPadding; + + } + + public static String getSimpleName(Object widget) { + if (widget == null) { + return "(null)"; + } + + String name = widget.getClass().getName(); + return name.substring(name.lastIndexOf('.') + 1); + } + + public static void setFloat(Element element, String value) { + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(element, "styleFloat", value); + } else { + DOM.setStyleAttribute(element, "cssFloat", value); + } + } + + private static int detectedScrollbarSize = -1; + + public static int getNativeScrollbarSize() { + if (detectedScrollbarSize < 0) { + Element scroller = DOM.createDiv(); + scroller.getStyle().setProperty("width", "50px"); + scroller.getStyle().setProperty("height", "50px"); + scroller.getStyle().setProperty("overflow", "scroll"); + scroller.getStyle().setProperty("position", "absolute"); + scroller.getStyle().setProperty("marginLeft", "-5000px"); + RootPanel.getBodyElement().appendChild(scroller); + detectedScrollbarSize = scroller.getOffsetWidth() + - scroller.getPropertyInt("clientWidth"); + + // Asserting the detected value causes a problem + // at least in Hosted Mode Browser/Linux/GWT-1.5.3, so + // use a default if detection fails. + if (detectedScrollbarSize == 0) { + detectedScrollbarSize = 20; + } + + RootPanel.getBodyElement().removeChild(scroller); + + } + return detectedScrollbarSize; + } + + /** + * Run workaround for webkits overflow auto issue. + * + * See: our buh #2138 and https://bugs.webkit.org/show_bug.cgi?id=21462 + * + * @param elem + * with overflow auto + */ + public static void runWebkitOverflowAutoFix(final Element elem) { + // add max version if fix landes sometime to webkit + if (BrowserInfo.get().getWebkitVersion() > 0) { + elem.getStyle().setProperty("overflow", "hidden"); + + DeferredCommand.addCommand(new Command() { + public void execute() { + // Dough, safari scoll auto means actually just a moped + elem.getStyle().setProperty("overflow", "auto"); + } + }); + } + + } + + /** + * Parses the UIDL parameter and fetches the relative size of the component. + * If a dimension is not specified as relative it will return -1. If the + * UIDL does not contain width or height specifications this will return + * null. + * + * @param uidl + * @return + */ + public static FloatSize parseRelativeSize(UIDL uidl) { + boolean hasAttribute = false; + String w = ""; + String h = ""; + if (uidl.hasAttribute("width")) { + hasAttribute = true; + w = uidl.getStringAttribute("width"); + } + if (uidl.hasAttribute("height")) { + hasAttribute = true; + h = uidl.getStringAttribute("height"); + } + + if (!hasAttribute) { + return null; + } + + float relativeWidth = Util.parseRelativeSize(w); + float relativeHeight = Util.parseRelativeSize(h); + + FloatSize relativeSize = new FloatSize(relativeWidth, relativeHeight); + return relativeSize; + + } + + public static boolean isCached(UIDL uidl) { + return uidl.getBooleanAttribute("cached"); + } + + public static void alert(String string) { + if (true) { + Window.alert(string); + } + } + + public static boolean equals(Object a, Object b) { + if (a == null) { + return b == null; + } + + return a.equals(b); + } + + public static void updateRelativeChildrenAndSendSizeUpdateEvent( + ApplicationConnection client, HasWidgets container) { + updateRelativeChildrenAndSendSizeUpdateEvent(client, container, + (Paintable) container); + } + + public static void updateRelativeChildrenAndSendSizeUpdateEvent( + ApplicationConnection client, HasWidgets container, Paintable widget) { + /* + * Relative sized children must be updated first so the component has + * the correct outer dimensions when signaling a size change to the + * parent. + */ + Iterator<Widget> childIterator = container.iterator(); + while (childIterator.hasNext()) { + Widget w = childIterator.next(); + client.handleComponentRelativeSize(w); + } + + HashSet<Paintable> widgets = new HashSet<Paintable>(); + widgets.add(widget); + Util.componentSizeUpdated(widgets); + } + + public static native int getRequiredWidth( + com.google.gwt.dom.client.Element element) + /*-{ + var width; + if (element.getBoundingClientRect != null) { + var rect = element.getBoundingClientRect(); + width = Math.ceil(rect.right - rect.left); + } else { + width = element.offsetWidth; + } + return width; + }-*/; + + public static native int getRequiredHeight( + com.google.gwt.dom.client.Element element) + /*-{ + var height; + if (element.getBoundingClientRect != null) { + var rect = element.getBoundingClientRect(); + height = Math.ceil(rect.bottom - rect.top); + } else { + height = element.offsetHeight; + } + return height; + }-*/; + + public static int getRequiredWidth(Widget widget) { + return getRequiredWidth(widget.getElement()); + } + + public static int getRequiredHeight(Widget widget) { + return getRequiredHeight(widget.getElement()); + } + + /** + * Detects what is currently the overflow style attribute in given element. + * + * @param pe + * the element to detect + * @return true if auto or scroll + */ + public static boolean mayHaveScrollBars(com.google.gwt.dom.client.Element pe) { + String overflow = getComputedStyle(pe, "overflow"); + if (overflow != null) { + if (overflow.equals("auto") || overflow.equals("scroll")) { + return true; + } else { + return false; + } + } else { + return false; + } + } + + /** + * A simple helper method to detect "computed style" (aka style sheets + + * element styles). Values returned differ a lot depending on browsers. + * Always be very careful when using this. + * + * @param el + * the element from which the style property is detected + * @param p + * the property to detect + * @return String value of style property + */ + private static native String getComputedStyle( + com.google.gwt.dom.client.Element el, String p) + /*-{ + try { + + if (el.currentStyle) { + // IE + return el.currentStyle[p]; + } else if (window.getComputedStyle) { + // Sa, FF, Opera + var view = el.ownerDocument.defaultView; + return view.getComputedStyle(el,null).getPropertyValue(p); + } else { + // fall back for non IE, Sa, FF, Opera + return ""; + } + } catch (e) { + return ""; + } + + }-*/; + +} diff --git a/src/com/vaadin/terminal/gwt/client/WidgetSet.java b/src/com/vaadin/terminal/gwt/client/WidgetSet.java new file mode 100644 index 0000000000..df005353c0 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/WidgetSet.java @@ -0,0 +1,34 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client; + +import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.user.client.ui.Widget; + +public interface WidgetSet extends EntryPoint { + + /** + * Create an uninitialized component that best matches given UIDL. The + * component must be a {@link Widget} that implements {@link Paintable}. + * + * @param uidl + * UIDL to be painted with returned component. + * @return New uninitialized and unregistered component that can paint given + * UIDL. + */ + public Paintable createWidget(UIDL uidl); + + /** + * Test if the given component implementation conforms to UIDL. + * + * @param currentWidget + * Current implementation of the component + * @param uidl + * UIDL to test against + * @return true iff createWidget would return a new component of the same + * class than currentWidget + */ + public boolean isCorrectImplementation(Widget currentWidget, UIDL uidl); +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/Action.java b/src/com/vaadin/terminal/gwt/client/ui/Action.java new file mode 100644 index 0000000000..4d02f0a259 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/Action.java @@ -0,0 +1,55 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.Command; + +/** + * + */ +public abstract class Action implements Command { + + protected ActionOwner owner; + + protected String iconUrl = null; + + protected String caption = ""; + + public Action(ActionOwner owner) { + this.owner = owner; + } + + /** + * Executed when action fired + */ + public abstract void execute(); + + public String getHTML() { + final StringBuffer sb = new StringBuffer(); + sb.append("<div>"); + if (getIconUrl() != null) { + sb.append("<img src=\"" + getIconUrl() + "\" alt=\"icon\" />"); + } + sb.append(getCaption()); + sb.append("</div>"); + return sb.toString(); + } + + public String getCaption() { + return caption; + } + + public void setCaption(String caption) { + this.caption = caption; + } + + public String getIconUrl() { + return iconUrl; + } + + public void setIconUrl(String url) { + iconUrl = url; + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java b/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java new file mode 100644 index 0000000000..42ea146837 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java @@ -0,0 +1,16 @@ +package com.vaadin.terminal.gwt.client.ui; + +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public interface ActionOwner { + + /** + * @return Array of IActions + */ + public Action[] getActions(); + + public ApplicationConnection getClient(); + + public String getPaintableId(); + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/AlignmentInfo.java b/src/com/vaadin/terminal/gwt/client/ui/AlignmentInfo.java new file mode 100644 index 0000000000..28d06c0cc2 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/AlignmentInfo.java @@ -0,0 +1,89 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +public final class AlignmentInfo { + /** Bitmask values for client server communication */ + public static class Bits { + public static final int ALIGNMENT_LEFT = 1; + public static final int ALIGNMENT_RIGHT = 2; + public static final int ALIGNMENT_TOP = 4; + public static final int ALIGNMENT_BOTTOM = 8; + public static final int ALIGNMENT_HORIZONTAL_CENTER = 16; + public static final int ALIGNMENT_VERTICAL_CENTER = 32; + } + + public static final AlignmentInfo LEFT = new AlignmentInfo( + Bits.ALIGNMENT_LEFT); + public static final AlignmentInfo RIGHT = new AlignmentInfo( + Bits.ALIGNMENT_RIGHT); + public static final AlignmentInfo TOP = new AlignmentInfo( + Bits.ALIGNMENT_TOP); + public static final AlignmentInfo BOTTOM = new AlignmentInfo( + Bits.ALIGNMENT_BOTTOM); + public static final AlignmentInfo CENTER = new AlignmentInfo( + Bits.ALIGNMENT_HORIZONTAL_CENTER); + public static final AlignmentInfo MIDDLE = new AlignmentInfo( + Bits.ALIGNMENT_VERTICAL_CENTER); + public static final AlignmentInfo TOP_LEFT = new AlignmentInfo( + Bits.ALIGNMENT_TOP + Bits.ALIGNMENT_LEFT); + + private final int bitMask; + + public AlignmentInfo(int bitMask) { + this.bitMask = bitMask; + } + + public AlignmentInfo(AlignmentInfo horizontal, AlignmentInfo vertical) { + this(horizontal.getBitMask() + vertical.getBitMask()); + } + + public int getBitMask() { + return bitMask; + } + + public boolean isTop() { + return (bitMask & Bits.ALIGNMENT_TOP) == Bits.ALIGNMENT_TOP; + } + + public boolean isBottom() { + return (bitMask & Bits.ALIGNMENT_BOTTOM) == Bits.ALIGNMENT_BOTTOM; + } + + public boolean isLeft() { + return (bitMask & Bits.ALIGNMENT_LEFT) == Bits.ALIGNMENT_LEFT; + } + + public boolean isRight() { + return (bitMask & Bits.ALIGNMENT_RIGHT) == Bits.ALIGNMENT_RIGHT; + } + + public boolean isVerticalCenter() { + return (bitMask & Bits.ALIGNMENT_VERTICAL_CENTER) == Bits.ALIGNMENT_VERTICAL_CENTER; + } + + public boolean isHorizontalCenter() { + return (bitMask & Bits.ALIGNMENT_HORIZONTAL_CENTER) == Bits.ALIGNMENT_HORIZONTAL_CENTER; + } + + public String getVerticalAlignment() { + if (isBottom()) { + return "bottom"; + } else if (isVerticalCenter()) { + return "middle"; + } + return "top"; + } + + public String getHorizontalAlignment() { + if (isRight()) { + return "right"; + } else if (isHorizontalCenter()) { + return "center"; + } + return "left"; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java b/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java new file mode 100644 index 0000000000..cfff056e69 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java @@ -0,0 +1,126 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+
+import com.vaadin.terminal.gwt.client.DateTimeService;
+
+public class CalendarEntry {
+ private final String styleName;
+ private Date start;
+ private Date end;
+ private String title;
+ private String description;
+ private boolean notime;
+
+ public CalendarEntry(String styleName, Date start, Date end, String title,
+ String description, boolean notime) {
+ this.styleName = styleName;
+ if (notime) {
+ Date d = new Date(start.getTime());
+ d.setSeconds(0);
+ d.setMinutes(0);
+ this.start = d;
+ if (end != null) {
+ d = new Date(end.getTime());
+ d.setSeconds(0);
+ d.setMinutes(0);
+ this.end = d;
+ } else {
+ end = start;
+ }
+ } else {
+ this.start = start;
+ this.end = end;
+ }
+ this.title = title;
+ this.description = description;
+ this.notime = notime;
+ }
+
+ public CalendarEntry(String styleName, Date start, Date end, String title,
+ String description) {
+ this(styleName, start, end, title, description, false);
+ }
+
+ public String getStyleName() {
+ return styleName;
+ }
+
+ public Date getStart() {
+ return start;
+ }
+
+ public void setStart(Date start) {
+ this.start = start;
+ }
+
+ public Date getEnd() {
+ return end;
+ }
+
+ public void setEnd(Date end) {
+ this.end = end;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public boolean isNotime() {
+ return notime;
+ }
+
+ public void setNotime(boolean notime) {
+ this.notime = notime;
+ }
+
+ public String getStringForDate(Date d) {
+ // TODO format from DateTimeService
+ String s = "";
+ if (!notime) {
+ if (!DateTimeService.isSameDay(d, start)) {
+ s += (start.getYear() + 1900) + "." + (start.getMonth() + 1)
+ + "." + start.getDate() + " ";
+ }
+ int i = start.getHours();
+ s += (i < 10 ? "0" : "") + i;
+ s += ":";
+ i = start.getMinutes();
+ s += (i < 10 ? "0" : "") + i;
+ if (!start.equals(end)) {
+ s += " - ";
+ if (!DateTimeService.isSameDay(start, end)) {
+ s += (end.getYear() + 1900) + "." + (end.getMonth() + 1)
+ + "." + end.getDate() + " ";
+ }
+ i = end.getHours();
+ s += (i < 10 ? "0" : "") + i;
+ s += ":";
+ i = end.getMinutes();
+ s += (i < 10 ? "0" : "") + i;
+ }
+ s += " ";
+ }
+ if (title != null) {
+ s += title;
+ }
+ return s;
+ }
+
+}
\ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/client/ui/Field.java b/src/com/vaadin/terminal/gwt/client/ui/Field.java new file mode 100644 index 0000000000..66f6497230 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/Field.java @@ -0,0 +1,13 @@ +/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+/**
+ * This interface indicates that the component is a Field (serverside), and
+ * wants (for instance) to automatically get the i-modified classname.
+ *
+ */
+public interface Field {
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IAbsoluteLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IAbsoluteLayout.java new file mode 100644 index 0000000000..06134e9515 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IAbsoluteLayout.java @@ -0,0 +1,378 @@ +package com.vaadin.terminal.gwt.client.ui; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.SimplePanel; +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.ICaption; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; + +public class IAbsoluteLayout extends ComplexPanel implements Container { + + /** Tag name for widget creation */ + public static final String TAGNAME = "absolutelayout"; + + /** Class name, prefix in styling */ + public static final String CLASSNAME = "i-absolutelayout"; + + private DivElement marginElement; + + protected final Element canvas = DOM.createDiv(); + + private int excessPixelsHorizontal; + + private int excessPixelsVertical; + + private Object previousStyleName; + + private Map<String, AbsoluteWrapper> pidToComponentWrappper = new HashMap<String, AbsoluteWrapper>(); + + protected ApplicationConnection client; + + private boolean rendering; + + public IAbsoluteLayout() { + setElement(Document.get().createDivElement()); + setStyleName(CLASSNAME); + marginElement = Document.get().createDivElement(); + canvas.getStyle().setProperty("position", "relative"); + marginElement.appendChild(canvas); + getElement().appendChild(marginElement); + } + + public RenderSpace getAllocatedSpace(Widget child) { + // TODO needs some special handling for components with only on edge + // horizontally or vertically defined + AbsoluteWrapper wrapper = (AbsoluteWrapper) child.getParent(); + int w; + if (wrapper.left != null && wrapper.right != null) { + w = wrapper.getOffsetWidth(); + } else if (wrapper.right != null) { + // left == null + // available width == right edge == offsetleft + width + w = wrapper.getOffsetWidth() + wrapper.getElement().getOffsetLeft(); + } else { + // left != null && right == null || left == null && + // right == null + // available width == canvas width - offset left + w = canvas.getOffsetWidth() - wrapper.getElement().getOffsetLeft(); + } + int h; + if (wrapper.top != null && wrapper.bottom != null) { + h = wrapper.getOffsetHeight(); + } else if (wrapper.bottom != null) { + // top not defined, available space 0... bottom of wrapper + h = wrapper.getElement().getOffsetTop() + wrapper.getOffsetHeight(); + } else { + // top defined or both undefined, available space == canvas - top + h = canvas.getOffsetHeight() - wrapper.getElement().getOffsetTop(); + } + + return new RenderSpace(w, h); + } + + public boolean hasChildComponent(Widget component) { + for (Iterator<Entry<String, AbsoluteWrapper>> iterator = pidToComponentWrappper + .entrySet().iterator(); iterator.hasNext();) { + if (iterator.next().getValue().paintable == component) { + return true; + } + } + return false; + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + for (Widget wrapper : getChildren()) { + AbsoluteWrapper w = (AbsoluteWrapper) wrapper; + if (w.getWidget() == oldComponent) { + w.setWidget(newComponent); + return; + } + } + } + + public boolean requestLayout(Set<Paintable> children) { + // component inside an absolute panel never affects parent nor the + // layout + return true; + } + + public void updateCaption(Paintable component, UIDL uidl) { + AbsoluteWrapper parent2 = (AbsoluteWrapper) ((Widget) component) + .getParent(); + parent2.updateCaption(uidl); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + rendering = true; + this.client = client; + // TODO margin handling + if (client.updateComponent(this, uidl, true)) { + rendering = false; + return; + } + + HashSet<String> unrenderedPids = new HashSet<String>( + pidToComponentWrappper.keySet()); + + for (Iterator<UIDL> childIterator = uidl.getChildIterator(); childIterator + .hasNext();) { + UIDL cc = childIterator.next(); + UIDL componentUIDL = cc.getChildUIDL(0); + unrenderedPids.remove(componentUIDL.getId()); + getWrapper(client, componentUIDL).updateFromUIDL(cc); + } + + for (String pid : unrenderedPids) { + AbsoluteWrapper absoluteWrapper = pidToComponentWrappper.get(pid); + pidToComponentWrappper.remove(pid); + absoluteWrapper.destroy(); + } + rendering = false; + } + + private AbsoluteWrapper getWrapper(ApplicationConnection client, + UIDL componentUIDL) { + AbsoluteWrapper wrapper = pidToComponentWrappper.get(componentUIDL + .getId()); + if (wrapper == null) { + wrapper = new AbsoluteWrapper(client.getPaintable(componentUIDL)); + pidToComponentWrappper.put(componentUIDL.getId(), wrapper); + add(wrapper); + } + return wrapper; + + } + + @Override + public void add(Widget child) { + super.add(child, canvas); + } + + @Override + public void setStyleName(String style) { + super.setStyleName(style); + if (previousStyleName == null || !previousStyleName.equals(style)) { + excessPixelsHorizontal = -1; + excessPixelsVertical = -1; + } + } + + @Override + public void setWidth(String width) { + super.setWidth(width); + // TODO do this so that canvas gets the sized properly (the area + // inside marginals) + canvas.getStyle().setProperty("width", width); + + if (!rendering) { + if (BrowserInfo.get().isIE6()) { + relayoutWrappersForIe6(); + } + relayoutRelativeChildren(); + } + } + + private void relayoutRelativeChildren() { + for (Widget widget : getChildren()) { + if (widget instanceof AbsoluteWrapper) { + AbsoluteWrapper w = (AbsoluteWrapper) widget; + client.handleComponentRelativeSize(w.getWidget()); + w.updateCaptionPosition(); + } + } + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + // TODO do this so that canvas gets the sized properly (the area + // inside marginals) + canvas.getStyle().setProperty("height", height); + + if (!rendering) { + if (BrowserInfo.get().isIE6()) { + relayoutWrappersForIe6(); + } + relayoutRelativeChildren(); + } + } + + private void relayoutWrappersForIe6() { + for (Widget wrapper : getChildren()) { + if (wrapper instanceof AbsoluteWrapper) { + ((AbsoluteWrapper) wrapper).ie6Layout(); + } + } + } + + public class AbsoluteWrapper extends SimplePanel { + private String css; + private String left; + private String top; + private String right; + private String bottom; + private String zIndex; + + private Paintable paintable; + private ICaption caption; + + public AbsoluteWrapper(Paintable paintable) { + this.paintable = paintable; + setStyleName(CLASSNAME + "-wrapper"); + } + + public void updateCaption(UIDL uidl) { + + boolean captionIsNeeded = ICaption.isNeeded(uidl); + if (captionIsNeeded) { + if (caption == null) { + caption = new ICaption(paintable, client); + IAbsoluteLayout.this.add(caption); + } + caption.updateCaption(uidl); + updateCaptionPosition(); + } else { + if (caption != null) { + caption.removeFromParent(); + caption = null; + } + } + } + + public void destroy() { + if (caption != null) { + caption.removeFromParent(); + } + client.unregisterPaintable(paintable); + removeFromParent(); + } + + public void updateFromUIDL(UIDL componentUIDL) { + setPosition(componentUIDL.getStringAttribute("css")); + if (getWidget() != paintable) { + setWidget((Widget) paintable); + } + UIDL childUIDL = componentUIDL.getChildUIDL(0); + paintable.updateFromUIDL(childUIDL, client); + if (childUIDL.hasAttribute("cached")) { + // child may need relative size adjustment if wrapper details + // have changed this could be optimized (check if wrapper size + // has changed) + client.handleComponentRelativeSize((Widget) paintable); + } + } + + public void setPosition(String stringAttribute) { + if (css == null || !css.equals(stringAttribute)) { + css = stringAttribute; + top = right = bottom = left = zIndex = null; + if (!css.equals("")) { + String[] properties = css.split(";"); + for (int i = 0; i < properties.length; i++) { + String[] keyValue = properties[i].split(":"); + if (keyValue[0].equals("left")) { + left = keyValue[1]; + } else if (keyValue[0].equals("top")) { + top = keyValue[1]; + } else if (keyValue[0].equals("right")) { + right = keyValue[1]; + } else if (keyValue[0].equals("bottom")) { + bottom = keyValue[1]; + } else if (keyValue[0].equals("z-index")) { + zIndex = keyValue[1]; + } + } + } + // ensure ne values + Style style = getElement().getStyle(); + style.setProperty("zIndex", zIndex); + style.setProperty("top", top); + style.setProperty("left", left); + style.setProperty("right", right); + style.setProperty("bottom", bottom); + + if (BrowserInfo.get().isIE6()) { + ie6Layout(); + } + } + updateCaptionPosition(); + } + + private void updateCaptionPosition() { + if (caption != null) { + Style style = caption.getElement().getStyle(); + style.setProperty("position", "absolute"); + style.setPropertyPx("left", getElement().getOffsetLeft()); + style.setPropertyPx("top", getElement().getOffsetTop() + - caption.getHeight()); + } + } + + private void ie6Layout() { + // special handling for IE6 is needed, it does not support + // setting both left/right or top/bottom + Style style = getElement().getStyle(); + if (bottom != null && top != null) { + // define height for wrapper to simulate bottom property + int bottompixels = measureForIE6(bottom); + ApplicationConnection.getConsole().log("ALB" + bottompixels); + int height = canvas.getOffsetHeight() - bottompixels + - getElement().getOffsetTop(); + ApplicationConnection.getConsole().log("ALB" + height); + if (height < 0) { + height = 0; + } + style.setPropertyPx("height", height); + } else { + // reset possibly existing value + style.setProperty("height", ""); + } + if (left != null && right != null) { + // define width for wrapper to simulate right property + int rightPixels = measureForIE6(right); + ApplicationConnection.getConsole().log("ALR" + rightPixels); + int width = canvas.getOffsetWidth() - rightPixels + - getElement().getOffsetWidth(); + ApplicationConnection.getConsole().log("ALR" + width); + if (width < 0) { + width = 0; + } + style.setPropertyPx("width", width); + } else { + // reset possibly existing value + style.setProperty("width", ""); + } + } + + } + + private Element measureElement; + + private int measureForIE6(String cssLength) { + if (measureElement == null) { + measureElement = DOM.createDiv(); + measureElement.getStyle().setProperty("position", "absolute"); + canvas.appendChild(measureElement); + } + measureElement.getStyle().setProperty("width", cssLength); + return measureElement.getOffsetWidth(); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IAccordion.java b/src/com/vaadin/terminal/gwt/client/ui/IAccordion.java new file mode 100644 index 0000000000..43c92cab32 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IAccordion.java @@ -0,0 +1,647 @@ +package com.vaadin.terminal.gwt.client.ui; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.ComplexPanel; +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.ICaption; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderInformation; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class IAccordion extends ITabsheetBase implements + ContainerResizedListener { + + public static final String CLASSNAME = "i-accordion"; + + private Set<Paintable> paintables = new HashSet<Paintable>(); + + private String height; + + private String width = ""; + + private HashMap<StackItem, UIDL> lazyUpdateMap = new HashMap<StackItem, UIDL>(); + + private RenderSpace renderSpace = new RenderSpace(0, 0, true); + + private StackItem openTab = null; + + private boolean rendering = false; + + private int selectedUIDLItemIndex = -1; + + private RenderInformation renderInformation = new RenderInformation(); + + public IAccordion() { + super(CLASSNAME); + // IE6 needs this to calculate offsetHeight correctly + if (BrowserInfo.get().isIE6()) { + DOM.setStyleAttribute(getElement(), "zoom", "1"); + } + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + rendering = true; + selectedUIDLItemIndex = -1; + super.updateFromUIDL(uidl, client); + /* + * Render content after all tabs have been created and we know how large + * the content area is + */ + if (selectedUIDLItemIndex >= 0) { + StackItem selectedItem = getStackItem(selectedUIDLItemIndex); + UIDL selectedTabUIDL = lazyUpdateMap.remove(selectedItem); + open(selectedUIDLItemIndex); + + selectedItem.setContent(selectedTabUIDL); + } else if (!uidl.getBooleanAttribute("cached") && openTab != null) { + close(openTab); + } + + iLayout(); + // finally render possible hidden tabs + if (lazyUpdateMap.size() > 0) { + for (Iterator iterator = lazyUpdateMap.keySet().iterator(); iterator + .hasNext();) { + StackItem item = (StackItem) iterator.next(); + item.setContent(lazyUpdateMap.get(item)); + } + lazyUpdateMap.clear(); + } + + renderInformation.updateSize(getElement()); + + rendering = false; + } + + @Override + protected void renderTab(UIDL tabUidl, int index, boolean selected, + boolean hidden) { + StackItem item; + int itemIndex; + if (getWidgetCount() <= index) { + // Create stackItem and render caption + item = new StackItem(tabUidl); + if (getWidgetCount() == 0) { + item.addStyleDependentName("first"); + } + itemIndex = getWidgetCount(); + add(item, getElement()); + } else { + item = getStackItem(index); + item = moveStackItemIfNeeded(item, index, tabUidl); + itemIndex = index; + } + item.updateCaption(tabUidl); + + item.setVisible(!hidden); + + if (selected) { + selectedUIDLItemIndex = itemIndex; + } + + if (tabUidl.getChildCount() > 0) { + lazyUpdateMap.put(item, tabUidl.getChildUIDL(0)); + } + } + + /** + * This method tries to find out if a tab has been rendered with a different + * index previously. If this is the case it re-orders the children so the + * same StackItem is used for rendering this time. E.g. if the first tab has + * been removed all tabs which contain cached content must be moved 1 step + * up to preserve the cached content. + * + * @param item + * @param newIndex + * @param tabUidl + * @return + */ + private StackItem moveStackItemIfNeeded(StackItem item, int newIndex, + UIDL tabUidl) { + UIDL tabContentUIDL = null; + Paintable tabContent = null; + if (tabUidl.getChildCount() > 0) { + tabContentUIDL = tabUidl.getChildUIDL(0); + tabContent = client.getPaintable(tabContentUIDL); + } + + Widget itemWidget = item.getComponent(); + if (tabContent != null) { + if (tabContent != itemWidget) { + /* + * This is not the same widget as before, find out if it has + * been moved + */ + int oldIndex = -1; + StackItem oldItem = null; + for (int i = 0; i < getWidgetCount(); i++) { + Widget w = getWidget(i); + oldItem = (StackItem) w; + if (tabContent == oldItem.getComponent()) { + oldIndex = i; + break; + } + } + + if (oldIndex != -1 && oldIndex > newIndex) { + /* + * The tab has previously been rendered in another position + * so we must move the cached content to correct position. + * We move only items with oldIndex > newIndex to prevent + * moving items already rendered in this update. If for + * instance tabs 1,2,3 are removed and added as 3,2,1 we + * cannot re-use "1" when we get to the third tab. + */ + insert(oldItem, getElement(), newIndex, true); + return oldItem; + } + } + } else { + // Tab which has never been loaded. Must assure we use an empty + // StackItem + Widget oldWidget = item.getComponent(); + if (oldWidget != null) { + item = new StackItem(tabUidl); + insert(item, getElement(), newIndex, true); + } + } + return item; + } + + private void open(int itemIndex) { + StackItem item = (StackItem) getWidget(itemIndex); + boolean alreadyOpen = false; + if (openTab != null) { + if (openTab.isOpen()) { + if (openTab == item) { + alreadyOpen = true; + } else { + openTab.close(); + } + } + } + + if (!alreadyOpen) { + item.open(); + activeTabIndex = itemIndex; + openTab = item; + } + + // Update the size for the open tab + updateOpenTabSize(); + } + + private void close(StackItem item) { + if (!item.isOpen()) { + return; + } + + item.close(); + activeTabIndex = -1; + openTab = null; + + } + + @Override + protected void selectTab(final int index, final UIDL contentUidl) { + StackItem item = getStackItem(index); + if (index != activeTabIndex) { + open(index); + iLayout(); + // TODO Check if this is needed + client.runDescendentsLayout(this); + + } + item.setContent(contentUidl); + } + + public void onSelectTab(StackItem item) { + final int index = getWidgetIndex(item); + if (index != activeTabIndex && !disabled && !readonly + && !disabledTabKeys.contains(tabKeys.get(index))) { + addStyleDependentName("loading"); + client + .updateVariable(id, "selected", "" + tabKeys.get(index), + true); + } + } + + @Override + public void setWidth(String width) { + if (this.width.equals(width)) { + return; + } + + super.setWidth(width); + this.width = width; + if (!rendering) { + updateOpenTabSize(); + + if (isDynamicHeight()) { + Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, + openTab, this); + updateOpenTabSize(); + } + + if (isDynamicHeight()) { + openTab.setHeightFromWidget(); + } + iLayout(); + } + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + this.height = height; + + if (!rendering) { + updateOpenTabSize(); + } + + } + + /** + * Sets the size of the open tab + */ + private void updateOpenTabSize() { + if (openTab == null) { + renderSpace.setHeight(0); + renderSpace.setWidth(0); + return; + } + + // WIDTH + if (!isDynamicWidth()) { + int w = getOffsetWidth(); + openTab.setWidth(w); + renderSpace.setWidth(w); + } else { + renderSpace.setWidth(0); + } + + // HEIGHT + if (!isDynamicHeight()) { + int usedPixels = 0; + for (Widget w : getChildren()) { + StackItem item = (StackItem) w; + if (item == openTab) { + usedPixels += item.getCaptionHeight(); + } else { + // This includes the captionNode borders + usedPixels += item.getHeight(); + } + } + + int offsetHeight = getOffsetHeight(); + + int spaceForOpenItem = offsetHeight - usedPixels; + + if (spaceForOpenItem < 0) { + spaceForOpenItem = 0; + } + + renderSpace.setHeight(spaceForOpenItem); + openTab.setHeight(spaceForOpenItem); + } else { + renderSpace.setHeight(0); + openTab.setHeightFromWidget(); + + } + + } + + public void iLayout() { + if (openTab == null) { + return; + } + + if (isDynamicWidth()) { + int maxWidth = 40; + for (Widget w : getChildren()) { + StackItem si = (StackItem) w; + int captionWidth = si.getCaptionWidth(); + if (captionWidth > maxWidth) { + maxWidth = captionWidth; + } + } + int widgetWidth = openTab.getWidgetWidth(); + if (widgetWidth > maxWidth) { + maxWidth = widgetWidth; + } + super.setWidth(maxWidth + "px"); + openTab.setWidth(maxWidth); + } + + Util.runWebkitOverflowAutoFix(openTab.getContainerElement()); + + } + + /** + * + */ + protected class StackItem extends ComplexPanel implements ClickListener { + + public void setHeight(int height) { + if (height == -1) { + super.setHeight(""); + DOM.setStyleAttribute(content, "height", "0px"); + } else { + super.setHeight((height + getCaptionHeight()) + "px"); + DOM.setStyleAttribute(content, "height", height + "px"); + DOM + .setStyleAttribute(content, "top", getCaptionHeight() + + "px"); + + } + } + + public Widget getComponent() { + if (getWidgetCount() < 2) { + return null; + } + return getWidget(1); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + } + + public void setHeightFromWidget() { + Widget paintable = getPaintable(); + if (paintable == null) { + return; + } + + int paintableHeight = (paintable).getElement().getOffsetHeight(); + setHeight(paintableHeight); + + } + + /** + * Returns caption width including padding + * + * @return + */ + public int getCaptionWidth() { + if (caption == null) { + return 0; + } + + int captionWidth = caption.getRequiredWidth(); + int padding = Util.measureHorizontalPaddingAndBorder(caption + .getElement(), 18); + return captionWidth + padding; + } + + public void setWidth(int width) { + if (width == -1) { + super.setWidth(""); + } else { + super.setWidth(width + "px"); + } + } + + public int getHeight() { + return getOffsetHeight(); + } + + public int getCaptionHeight() { + return captionNode.getOffsetHeight(); + } + + private ICaption caption; + private boolean open = false; + private Element content = DOM.createDiv(); + private Element captionNode = DOM.createDiv(); + + public StackItem(UIDL tabUidl) { + setElement(DOM.createDiv()); + caption = new ICaption(null, client); + caption.addClickListener(this); + if (BrowserInfo.get().isIE6()) { + DOM.setEventListener(captionNode, this); + DOM.sinkEvents(captionNode, Event.BUTTON_LEFT); + } + super.add(caption, captionNode); + DOM.appendChild(captionNode, caption.getElement()); + DOM.appendChild(getElement(), captionNode); + DOM.appendChild(getElement(), content); + setStylePrimaryName(CLASSNAME + "-item"); + DOM.setElementProperty(content, "className", CLASSNAME + + "-item-content"); + DOM.setElementProperty(captionNode, "className", CLASSNAME + + "-item-caption"); + close(); + } + + @Override + public void onBrowserEvent(Event event) { + onSelectTab(this); + } + + public Element getContainerElement() { + return content; + } + + public Widget getPaintable() { + if (getWidgetCount() > 1) { + return getWidget(1); + } else { + return null; + } + } + + public void replacePaintable(Paintable newPntbl) { + if (getWidgetCount() > 1) { + client.unregisterPaintable((Paintable) getWidget(1)); + paintables.remove(getWidget(1)); + remove(1); + } + add((Widget) newPntbl, content); + paintables.add(newPntbl); + } + + public void open() { + open = true; + DOM.setStyleAttribute(content, "top", getCaptionHeight() + "px"); + DOM.setStyleAttribute(content, "left", "0px"); + DOM.setStyleAttribute(content, "visibility", ""); + addStyleDependentName("open"); + } + + public void hide() { + DOM.setStyleAttribute(content, "visibility", "hidden"); + } + + public void close() { + DOM.setStyleAttribute(content, "visibility", "hidden"); + DOM.setStyleAttribute(content, "top", "-100000px"); + DOM.setStyleAttribute(content, "left", "-100000px"); + removeStyleDependentName("open"); + setHeight(-1); + open = false; + } + + public boolean isOpen() { + return open; + } + + public void setContent(UIDL contentUidl) { + final Paintable newPntbl = client.getPaintable(contentUidl); + if (getPaintable() == null) { + add((Widget) newPntbl, content); + paintables.add(newPntbl); + } else if (getPaintable() != newPntbl) { + replacePaintable(newPntbl); + } + newPntbl.updateFromUIDL(contentUidl, client); + if (contentUidl.getBooleanAttribute("cached")) { + /* + * The size of a cached, relative sized component must be + * updated to report correct size. + */ + client.handleComponentRelativeSize((Widget) newPntbl); + } + if (isOpen() && isDynamicHeight()) { + setHeightFromWidget(); + } + } + + public void onClick(Widget sender) { + onSelectTab(this); + } + + public void updateCaption(UIDL uidl) { + caption.updateCaption(uidl); + } + + public int getWidgetWidth() { + return DOM.getFirstChild(content).getOffsetWidth(); + } + + public boolean contains(Paintable p) { + return (getPaintable() == p); + } + + public boolean isCaptionVisible() { + return caption.isVisible(); + } + + } + + @Override + protected void clearPaintables() { + clear(); + } + + public boolean isDynamicHeight() { + return height == null || height.equals(""); + } + + public boolean isDynamicWidth() { + return width == null || width.equals(""); + } + + @Override + protected Iterator getPaintableIterator() { + return paintables.iterator(); + } + + public boolean hasChildComponent(Widget component) { + if (paintables.contains(component)) { + return true; + } else { + return false; + } + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + for (Widget w : getChildren()) { + StackItem item = (StackItem) w; + if (item.getPaintable() == oldComponent) { + item.replacePaintable((Paintable) newComponent); + return; + } + } + } + + public void updateCaption(Paintable component, UIDL uidl) { + /* Accordion does not render its children's captions */ + } + + public boolean requestLayout(Set<Paintable> child) { + if (!isDynamicHeight() && !isDynamicWidth()) { + /* + * If the height and width has been specified for this container the + * child components cannot make the size of the layout change + */ + + return true; + } + + updateOpenTabSize(); + + if (renderInformation.updateSize(getElement())) { + /* + * Size has changed so we let the child components know about the + * new size. + */ + iLayout(); + // TODO Check if this is needed + client.runDescendentsLayout(this); + + return false; + } else { + /* + * Size has not changed so we do not need to propagate the event + * further + */ + return true; + } + + } + + public RenderSpace getAllocatedSpace(Widget child) { + return renderSpace; + } + + @Override + protected int getTabCount() { + return getWidgetCount(); + } + + @Override + protected void removeTab(int index) { + StackItem item = getStackItem(index); + remove(item); + } + + @Override + protected Paintable getTab(int index) { + if (index < getWidgetCount()) { + return (Paintable) (getStackItem(index)).getPaintable(); + } + + return null; + } + + private StackItem getStackItem(int index) { + return (StackItem) getWidget(index); + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IButton.java b/src/com/vaadin/terminal/gwt/client/ui/IButton.java new file mode 100644 index 0000000000..4a1dcbad07 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IButton.java @@ -0,0 +1,190 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.ClickListener; +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.ITooltip; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class IButton extends Button implements Paintable { + + private String width = null; + + public static final String CLASSNAME = "i-button"; + + // Used only for IE, because it doesn't support :active CSS selector + private static final String CLASSNAME_DOWN = "i-pressed"; + + String id; + + ApplicationConnection client; + + private Element errorIndicatorElement; + + private final Element captionElement = DOM.createSpan(); + + private Icon icon; + + /** + * Helper flat to handle special-case where the button is moved from under + * mouse while clicking it. In this case mouse leaves the button without + * moving. + */ + private boolean clickPending; + + public IButton() { + setStyleName(CLASSNAME); + + DOM.appendChild(getElement(), captionElement); + + addClickListener(new ClickListener() { + public void onClick(Widget sender) { + if (id == null || client == null) { + return; + } + /* + * TODO isolate workaround. Safari don't always seem to fire + * onblur previously focused component before button is clicked. + */ + IButton.this.setFocus(true); + client.updateVariable(id, "state", true, true); + clickPending = false; + } + }); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + sinkEvents(Event.ONMOUSEDOWN); + sinkEvents(Event.ONMOUSEUP); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + // Ensure correct implementation, + // but don't let container manage caption etc. + if (client.updateComponent(this, uidl, false)) { + return; + } + + // Save details + this.client = client; + id = uidl.getId(); + + // Set text + setText(uidl.getStringAttribute("caption")); + + // handle error + if (uidl.hasAttribute("error")) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + DOM.setElementProperty(errorIndicatorElement, "className", + "i-errorindicator"); + } + DOM.insertChild(getElement(), errorIndicatorElement, 0); + + // Fix for IE6, IE7 + if (BrowserInfo.get().isIE()) { + DOM.setInnerText(errorIndicatorElement, " "); + } + + } else if (errorIndicatorElement != null) { + DOM.removeChild(getElement(), errorIndicatorElement); + errorIndicatorElement = null; + } + + if (uidl.hasAttribute("readonly")) { + setEnabled(false); + } + + if (uidl.hasAttribute("icon")) { + if (icon == null) { + icon = new Icon(client); + DOM.insertChild(getElement(), icon.getElement(), 0); + } + icon.setUri(uidl.getStringAttribute("icon")); + } else { + if (icon != null) { + DOM.removeChild(getElement(), icon.getElement()); + icon = null; + } + } + if (BrowserInfo.get().isIE7()) { + if (width.equals("")) { + setWidth(getOffsetWidth() + "px"); + } + } + } + + @Override + public void setText(String text) { + DOM.setInnerText(captionElement, text); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + + if (DOM.eventGetType(event) == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + + } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN + && event.getButton() == Event.BUTTON_LEFT) { + clickPending = true; + if (BrowserInfo.get().isIE()) { + // Only for IE, because it doesn't support :active CSS selector + // Simple check is cheaper than DOM manipulation + addStyleName(CLASSNAME_DOWN); + } + } else if (DOM.eventGetType(event) == Event.ONMOUSEMOVE) { + clickPending = false; + } else if (DOM.eventGetType(event) == Event.ONMOUSEOUT) { + if (clickPending) { + click(); + } + if (BrowserInfo.get().isIE()) { + removeStyleName(CLASSNAME_DOWN); + } + clickPending = false; + } else if (DOM.eventGetType(event) == Event.ONMOUSEUP) { + if (BrowserInfo.get().isIE()) { + removeStyleName(CLASSNAME_DOWN); + } + } + + if (client != null) { + client.handleTooltipEvent(event, this); + } + } + + @Override + public void setWidth(String width) { + /* Workaround for IE7 button size part 1 (#2014) */ + if (BrowserInfo.get().isIE7() && this.width != null) { + if (this.width.equals(width)) { + return; + } + + if (width == null) { + width = ""; + } + } + + this.width = width; + super.setWidth(width); + + /* Workaround for IE7 button size part 2 (#2014) */ + if (BrowserInfo.get().isIE7()) { + super.setWidth(width); + } + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ICalendarPanel.java b/src/com/vaadin/terminal/gwt/client/ui/ICalendarPanel.java new file mode 100644 index 0000000000..31566a75b9 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ICalendarPanel.java @@ -0,0 +1,520 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.MouseListener;
+import com.google.gwt.user.client.ui.MouseListenerCollection;
+import com.google.gwt.user.client.ui.SourcesMouseEvents;
+import com.google.gwt.user.client.ui.SourcesTableEvents;
+import com.google.gwt.user.client.ui.TableListener;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.DateTimeService;
+import com.vaadin.terminal.gwt.client.LocaleService;
+
+public class ICalendarPanel extends FlexTable implements MouseListener {
+
+ private final IDateField datefield;
+
+ private IEventButton prevYear;
+
+ private IEventButton nextYear;
+
+ private IEventButton prevMonth;
+
+ private IEventButton nextMonth;
+
+ private ITime time;
+
+ private Date minDate = null;
+
+ private Date maxDate = null;
+
+ private CalendarEntrySource entrySource;
+
+ /* Needed to identify resolution changes */
+ private int resolution = IDateField.RESOLUTION_YEAR;
+
+ /* Needed to identify locale changes */
+ private String locale = LocaleService.getDefaultLocale();
+
+ public ICalendarPanel(IDateField parent) {
+ datefield = parent;
+ setStyleName(IDateField.CLASSNAME + "-calendarpanel");
+ // buildCalendar(true);
+ addTableListener(new DateClickListener(this));
+ }
+
+ public ICalendarPanel(IDateField parent, Date min, Date max) {
+ datefield = parent;
+ setStyleName(IDateField.CLASSNAME + "-calendarpanel");
+ // buildCalendar(true);
+ addTableListener(new DateClickListener(this));
+
+ }
+
+ private void buildCalendar(boolean forceRedraw) {
+ final boolean needsMonth = datefield.getCurrentResolution() > IDateField.RESOLUTION_YEAR;
+ boolean needsBody = datefield.getCurrentResolution() >= IDateField.RESOLUTION_DAY;
+ final boolean needsTime = datefield.getCurrentResolution() >= IDateField.RESOLUTION_HOUR;
+ forceRedraw = prevYear == null ? true : forceRedraw;
+ buildCalendarHeader(forceRedraw, needsMonth);
+ clearCalendarBody(!needsBody);
+ if (needsBody) {
+ buildCalendarBody();
+ }
+ if (needsTime) {
+ buildTime(forceRedraw);
+ } else if (time != null) {
+ remove(time);
+ time = null;
+ }
+ }
+
+ private void clearCalendarBody(boolean remove) {
+ if (!remove) {
+ for (int row = 2; row < 8; row++) {
+ for (int col = 0; col < 7; col++) {
+ setHTML(row, col, " ");
+ }
+ }
+ } else if (getRowCount() > 2) {
+ while (getRowCount() > 2) {
+ removeRow(2);
+ }
+ }
+ }
+
+ private void buildCalendarHeader(boolean forceRedraw, boolean needsMonth) {
+ if (forceRedraw) {
+ if (prevMonth == null) { // Only do once
+ prevYear = new IEventButton();
+ prevYear.setHTML("«");
+ prevYear.setStyleName("i-button-prevyear");
+ nextYear = new IEventButton();
+ nextYear.setHTML("»");
+ nextYear.setStyleName("i-button-nextyear");
+ prevYear.addMouseListener(this);
+ nextYear.addMouseListener(this);
+ setWidget(0, 0, prevYear);
+ setWidget(0, 4, nextYear);
+
+ if (needsMonth) {
+ prevMonth = new IEventButton();
+ prevMonth.setHTML("‹");
+ prevMonth.setStyleName("i-button-prevmonth");
+ nextMonth = new IEventButton();
+ nextMonth.setHTML("›");
+ nextMonth.setStyleName("i-button-nextmonth");
+ prevMonth.addMouseListener(this);
+ nextMonth.addMouseListener(this);
+ setWidget(0, 3, nextMonth);
+ setWidget(0, 1, prevMonth);
+ }
+
+ getFlexCellFormatter().setColSpan(0, 2, 3);
+ getRowFormatter().addStyleName(0,
+ IDateField.CLASSNAME + "-calendarpanel-header");
+ } else if (!needsMonth) {
+ // Remove month traverse buttons
+ prevMonth.removeMouseListener(this);
+ nextMonth.removeMouseListener(this);
+ remove(prevMonth);
+ remove(nextMonth);
+ prevMonth = null;
+ nextMonth = null;
+ }
+
+ // Print weekday names
+ final int firstDay = datefield.getDateTimeService()
+ .getFirstDayOfWeek();
+ for (int i = 0; i < 7; i++) {
+ int day = i + firstDay;
+ if (day > 6) {
+ day = 0;
+ }
+ if (datefield.getCurrentResolution() > IDateField.RESOLUTION_MONTH) {
+ setHTML(1, i, "<strong>"
+ + datefield.getDateTimeService().getShortDay(day)
+ + "</strong>");
+ } else {
+ setHTML(1, i, "");
+ }
+ }
+ }
+
+ final String monthName = needsMonth ? datefield.getDateTimeService()
+ .getMonth(datefield.getShowingDate().getMonth()) : "";
+ final int year = datefield.getShowingDate().getYear() + 1900;
+ setHTML(0, 2, "<span class=\"" + IDateField.CLASSNAME
+ + "-calendarpanel-month\">" + monthName + " " + year
+ + "</span>");
+ }
+
+ private void buildCalendarBody() {
+ // date actually selected?
+ Date currentDate = datefield.getCurrentDate();
+ Date showing = datefield.getShowingDate();
+ boolean selected = (currentDate != null
+ && currentDate.getMonth() == showing.getMonth() && currentDate
+ .getYear() == showing.getYear());
+
+ final int startWeekDay = datefield.getDateTimeService()
+ .getStartWeekDay(datefield.getShowingDate());
+ final int numDays = DateTimeService.getNumberOfDaysInMonth(datefield
+ .getShowingDate());
+ int dayCount = 0;
+ final Date today = new Date();
+ final Date curr = new Date(datefield.getShowingDate().getTime());
+ for (int row = 2; row < 8; row++) {
+ for (int col = 0; col < 7; col++) {
+ if (!(row == 2 && col < startWeekDay)) {
+ if (dayCount < numDays) {
+ final int selectedDate = ++dayCount;
+ String title = "";
+ if (entrySource != null) {
+ curr.setDate(dayCount);
+ final List entries = entrySource.getEntries(curr,
+ IDateField.RESOLUTION_DAY);
+ if (entries != null) {
+ for (final Iterator it = entries.iterator(); it
+ .hasNext();) {
+ final CalendarEntry entry = (CalendarEntry) it
+ .next();
+ title += (title.length() > 0 ? ", " : "")
+ + entry.getStringForDate(curr);
+ }
+ }
+ }
+ final String baseclass = IDateField.CLASSNAME
+ + "-calendarpanel-day";
+ String cssClass = baseclass;
+ if (!isEnabledDate(curr)) {
+ cssClass += " " + baseclass + "-disabled";
+ }
+ if (selected
+ && datefield.getShowingDate().getDate() == dayCount) {
+ cssClass += " " + baseclass + "-selected";
+ }
+ if (today.getDate() == dayCount
+ && today.getMonth() == datefield
+ .getShowingDate().getMonth()
+ && today.getYear() == datefield
+ .getShowingDate().getYear()) {
+ cssClass += " " + baseclass + "-today";
+ }
+ if (title.length() > 0) {
+ cssClass += " " + baseclass + "-entry";
+ }
+ setHTML(row, col, "<span title=\"" + title
+ + "\" class=\"" + cssClass + "\">"
+ + selectedDate + "</span>");
+ } else {
+ break;
+ }
+
+ }
+ }
+ }
+ }
+
+ private void buildTime(boolean forceRedraw) {
+ if (time == null) {
+ time = new ITime(datefield);
+ setText(8, 0, ""); // Add new row
+ getFlexCellFormatter().setColSpan(8, 0, 7);
+ setWidget(8, 0, time);
+ }
+ time.updateTime(forceRedraw);
+ }
+
+ /**
+ *
+ * @param forceRedraw
+ * Build all from scratch, in case of e.g. locale changes
+ */
+ public void updateCalendar() {
+ // Locale and resolution changes force a complete redraw
+ buildCalendar(locale != datefield.getCurrentLocale()
+ || resolution != datefield.getCurrentResolution());
+ if (datefield instanceof ITextualDate) {
+ ((ITextualDate) datefield).buildDate();
+ }
+ locale = datefield.getCurrentLocale();
+ resolution = datefield.getCurrentResolution();
+ }
+
+ private boolean isEnabledDate(Date date) {
+ if ((minDate != null && date.before(minDate))
+ || (maxDate != null && date.after(maxDate))) {
+ return false;
+ }
+ return true;
+ }
+
+ private void processClickEvent(Widget sender, boolean updateVariable) {
+ if (!datefield.isEnabled() || datefield.isReadonly()) {
+ return;
+ }
+ Date showingDate = datefield.getShowingDate();
+ if (!updateVariable) {
+ if (sender == prevYear) {
+ showingDate.setYear(showingDate.getYear() - 1);
+ updateCalendar();
+ } else if (sender == nextYear) {
+ showingDate.setYear(showingDate.getYear() + 1);
+ updateCalendar();
+ } else if (sender == prevMonth) {
+ int currentMonth = showingDate.getMonth();
+ showingDate.setMonth(currentMonth - 1);
+
+ /*
+ * If the selected date was e.g. 31.12 the new date would be
+ * 31.11 but this date is invalid so the new date will be 1.12.
+ * This is taken care of by decreasing the date until we have
+ * the correct month.
+ */
+ while (showingDate.getMonth() == currentMonth) {
+ showingDate.setDate(showingDate.getDate() - 1);
+ }
+
+ updateCalendar();
+ } else if (sender == nextMonth) {
+ int currentMonth = showingDate.getMonth();
+ showingDate.setMonth(currentMonth + 1);
+ int requestedMonth = (currentMonth + 1) % 12;
+
+ /*
+ * If the selected date was e.g. 31.3 the new date would be 31.4
+ * but this date is invalid so the new date will be 1.5. This is
+ * taken care of by decreasing the date until we have the
+ * correct month.
+ */
+ while (showingDate.getMonth() != requestedMonth) {
+ showingDate.setDate(showingDate.getDate() - 1);
+ }
+
+ updateCalendar();
+ }
+ } else {
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_YEAR
+ || datefield.getCurrentResolution() == IDateField.RESOLUTION_MONTH) {
+ // Due to current UI, update variable if res=year/month
+ datefield.setCurrentDate(new Date(showingDate.getTime()));
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MONTH) {
+ datefield.getClient().updateVariable(datefield.getId(),
+ "month", datefield.getCurrentDate().getMonth() + 1,
+ false);
+ }
+ datefield.getClient().updateVariable(datefield.getId(), "year",
+ datefield.getCurrentDate().getYear() + 1900,
+ datefield.isImmediate());
+
+ /* Must update the value in the textfield also */
+ updateCalendar();
+ }
+ }
+ }
+
+ private Timer timer;
+
+ public void onMouseDown(final Widget sender, int x, int y) {
+ // Allow user to click-n-hold for fast-forward or fast-rewind.
+ // Timer is first used for a 500ms delay after mousedown. After that has
+ // elapsed, another timer is triggered to go off every 150ms. Both
+ // timers are cancelled on mouseup or mouseout.
+ if (sender instanceof IEventButton) {
+ processClickEvent(sender, false);
+ timer = new Timer() {
+ @Override
+ public void run() {
+ timer = new Timer() {
+ @Override
+ public void run() {
+ processClickEvent(sender, false);
+ }
+ };
+ timer.scheduleRepeating(150);
+ }
+ };
+ timer.schedule(500);
+ }
+ }
+
+ public void onMouseEnter(Widget sender) {
+ }
+
+ public void onMouseLeave(Widget sender) {
+ if (timer != null) {
+ timer.cancel();
+ }
+ }
+
+ public void onMouseMove(Widget sender, int x, int y) {
+ }
+
+ public void onMouseUp(Widget sender, int x, int y) {
+ if (timer != null) {
+ timer.cancel();
+ }
+ processClickEvent(sender, true);
+ }
+
+ private class IEventButton extends IButton implements SourcesMouseEvents {
+
+ private MouseListenerCollection mouseListeners;
+
+ public IEventButton() {
+ super();
+ sinkEvents(Event.FOCUSEVENTS | Event.KEYEVENTS | Event.ONCLICK
+ | Event.MOUSEEVENTS);
+ }
+
+ public void addMouseListener(MouseListener listener) {
+ if (mouseListeners == null) {
+ mouseListeners = new MouseListenerCollection();
+ }
+ mouseListeners.add(listener);
+ }
+
+ public void removeMouseListener(MouseListener listener) {
+ if (mouseListeners != null) {
+ mouseListeners.remove(listener);
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEDOWN:
+ case Event.ONMOUSEUP:
+ case Event.ONMOUSEMOVE:
+ case Event.ONMOUSEOVER:
+ case Event.ONMOUSEOUT:
+ if (mouseListeners != null) {
+ mouseListeners.fireMouseEvent(this, event);
+ }
+ break;
+ }
+ }
+ }
+
+ private class DateClickListener implements TableListener {
+
+ private final ICalendarPanel cal;
+
+ public DateClickListener(ICalendarPanel panel) {
+ cal = panel;
+ }
+
+ public void onCellClicked(SourcesTableEvents sender, int row, int col) {
+ if (sender != cal || row < 2 || row > 7
+ || !cal.datefield.isEnabled() || cal.datefield.isReadonly()) {
+ return;
+ }
+
+ final String text = cal.getText(row, col);
+ if (text.equals(" ")) {
+ return;
+ }
+
+ try {
+ final Integer day = new Integer(text);
+ final Date newDate = cal.datefield.getShowingDate();
+ newDate.setDate(day.intValue());
+ if (!isEnabledDate(newDate)) {
+ return;
+ }
+ if (cal.datefield.getCurrentDate() == null) {
+ cal.datefield.setCurrentDate(new Date(newDate.getTime()));
+
+ // Init variables with current time
+ datefield.getClient().updateVariable(cal.datefield.getId(),
+ "hour", newDate.getHours(), false);
+ datefield.getClient().updateVariable(cal.datefield.getId(),
+ "min", newDate.getMinutes(), false);
+ datefield.getClient().updateVariable(cal.datefield.getId(),
+ "sec", newDate.getSeconds(), false);
+ datefield.getClient().updateVariable(cal.datefield.getId(),
+ "msec", datefield.getMilliseconds(), false);
+ }
+
+ cal.datefield.getCurrentDate().setTime(newDate.getTime());
+ cal.datefield.getClient().updateVariable(cal.datefield.getId(),
+ "day", cal.datefield.getCurrentDate().getDate(), false);
+ cal.datefield.getClient().updateVariable(cal.datefield.getId(),
+ "month", cal.datefield.getCurrentDate().getMonth() + 1,
+ false);
+ cal.datefield.getClient().updateVariable(cal.datefield.getId(),
+ "year",
+ cal.datefield.getCurrentDate().getYear() + 1900,
+ cal.datefield.isImmediate());
+
+ if (datefield instanceof ITextualDate
+ && resolution < IDateField.RESOLUTION_HOUR) {
+ ((IToolkitOverlay) getParent()).hide();
+ } else {
+ updateCalendar();
+ }
+
+ } catch (final NumberFormatException e) {
+ // Not a number, ignore and stop here
+ return;
+ }
+ }
+
+ }
+
+ public void setLimits(Date min, Date max) {
+ if (min != null) {
+ final Date d = new Date(min.getTime());
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(1);
+ minDate = d;
+ } else {
+ minDate = null;
+ }
+ if (max != null) {
+ final Date d = new Date(max.getTime());
+ d.setHours(24);
+ d.setMinutes(59);
+ d.setSeconds(59);
+ maxDate = d;
+ } else {
+ maxDate = null;
+ }
+ }
+
+ public void setCalendarEntrySource(CalendarEntrySource entrySource) {
+ this.entrySource = entrySource;
+ }
+
+ public CalendarEntrySource getCalendarEntrySource() {
+ return entrySource;
+ }
+
+ public interface CalendarEntrySource {
+ public List getEntries(Date date, int resolution);
+ }
+
+ /**
+ * Sets focus to Calendar panel.
+ *
+ * @param focus
+ */
+ public void setFocus(boolean focus) {
+ nextYear.setFocus(focus);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ICheckBox.java b/src/com/vaadin/terminal/gwt/client/ui/ICheckBox.java new file mode 100644 index 0000000000..397dc89828 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ICheckBox.java @@ -0,0 +1,140 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ITooltip; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class ICheckBox extends com.google.gwt.user.client.ui.CheckBox implements + Paintable, Field { + + public static final String CLASSNAME = "i-checkbox"; + + String id; + + boolean immediate; + + ApplicationConnection client; + + private Element errorIndicatorElement; + + private Icon icon; + + private boolean isBlockMode = false; + + public ICheckBox() { + setStyleName(CLASSNAME); + addClickListener(new ClickListener() { + + public void onClick(Widget sender) { + if (id == null || client == null) { + return; + } + client.updateVariable(id, "state", isChecked(), immediate); + } + + }); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + Element el = DOM.getFirstChild(getElement()); + while (el != null) { + DOM.sinkEvents(el, + (DOM.getEventsSunk(el) | ITooltip.TOOLTIP_EVENTS)); + el = DOM.getNextSibling(el); + } + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // Save details + this.client = client; + id = uidl.getId(); + + // Ensure correct implementation + if (client.updateComponent(this, uidl, false)) { + return; + } + + if (uidl.hasAttribute("error")) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + errorIndicatorElement.setInnerHTML(" "); + DOM.setElementProperty(errorIndicatorElement, "className", + "i-errorindicator"); + DOM.appendChild(getElement(), errorIndicatorElement); + DOM.sinkEvents(errorIndicatorElement, ITooltip.TOOLTIP_EVENTS + | Event.ONCLICK); + } + } else if (errorIndicatorElement != null) { + DOM.setStyleAttribute(errorIndicatorElement, "display", "none"); + } + + if (uidl.hasAttribute("readonly")) { + setEnabled(false); + } + + if (uidl.hasAttribute("icon")) { + if (icon == null) { + icon = new Icon(client); + DOM.insertChild(getElement(), icon.getElement(), 1); + icon.sinkEvents(ITooltip.TOOLTIP_EVENTS); + icon.sinkEvents(Event.ONCLICK); + } + icon.setUri(uidl.getStringAttribute("icon")); + } else if (icon != null) { + // detach icon + DOM.removeChild(getElement(), icon.getElement()); + icon = null; + } + + // Set text + setText(uidl.getStringAttribute("caption")); + setChecked(uidl.getBooleanVariable("state")); + immediate = uidl.getBooleanAttribute("immediate"); + } + + @Override + public void onBrowserEvent(Event event) { + if (icon != null && (event.getTypeInt() == Event.ONCLICK) + && (event.getTarget() == icon.getElement())) { + setChecked(!isChecked()); + } + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + } + if (client != null) { + client.handleTooltipEvent(event, this); + } + } + + @Override + public void setWidth(String width) { + setBlockMode(); + super.setWidth(width); + } + + @Override + public void setHeight(String height) { + setBlockMode(); + super.setHeight(height); + } + + /** + * makes container element (span) to be block element to enable sizing. + */ + private void setBlockMode() { + if (!isBlockMode) { + DOM.setStyleAttribute(getElement(), "display", "block"); + isBlockMode = true; + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IContextMenu.java b/src/com/vaadin/terminal/gwt/client/ui/IContextMenu.java new file mode 100644 index 0000000000..72a87ea188 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IContextMenu.java @@ -0,0 +1,158 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.TableRowElement; +import com.google.gwt.dom.client.TableSectionElement; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.MenuItem; +import com.google.gwt.user.client.ui.PopupPanel; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public class IContextMenu extends IToolkitOverlay implements SubPartAware { + + private ActionOwner actionOwner; + + private final CMenuBar menu = new CMenuBar(); + + private int left; + + private int top; + + /** + * This method should be used only by Client object as only one per client + * should exists. Request an instance via client.getContextMenu(); + * + * @param cli + * to be set as an owner of menu + */ + public IContextMenu() { + super(true, false, true); + setWidget(menu); + setStyleName("i-contextmenu"); + } + + /** + * Sets the element from which to build menu + * + * @param ao + */ + public void setActionOwner(ActionOwner ao) { + actionOwner = ao; + } + + /** + * Shows context menu at given location. + * + * @param left + * @param top + */ + public void showAt(int left, int top) { + this.left = left; + this.top = top; + menu.clearItems(); + final Action[] actions = actionOwner.getActions(); + for (int i = 0; i < actions.length; i++) { + final Action a = actions[i]; + menu.addItem(new MenuItem(a.getHTML(), true, a)); + } + + setPopupPositionAndShow(new PositionCallback() { + public void setPosition(int offsetWidth, int offsetHeight) { + // mac FF gets bad width due GWT popups overflow hacks, + // re-determine width + offsetWidth = menu.getOffsetWidth(); + int left = IContextMenu.this.left; + int top = IContextMenu.this.top; + if (offsetWidth + left > Window.getClientWidth()) { + left = left - offsetWidth; + if (left < 0) { + left = 0; + } + } + if (offsetHeight + top > Window.getClientHeight()) { + top = top - offsetHeight; + if (top < 0) { + top = 0; + } + } + setPopupPosition(left, top); + } + }); + } + + public void showAt(ActionOwner ao, int left, int top) { + setActionOwner(ao); + showAt(left, top); + } + + /** + * Extend standard Gwt MenuBar to set proper settings and to override + * onPopupClosed method so that PopupPanel gets closed. + */ + class CMenuBar extends MenuBar { + public CMenuBar() { + super(true); + } + + @Override + public void onPopupClosed(PopupPanel sender, boolean autoClosed) { + super.onPopupClosed(sender, autoClosed); + hide(); + } + + /* + * public void onBrowserEvent(Event event) { // Remove current selection + * when mouse leaves if (DOM.eventGetType(event) == Event.ONMOUSEOUT) { + * Element to = DOM.eventGetToElement(event); if + * (!DOM.isOrHasChild(getElement(), to)) { DOM.setElementProperty( + * super.getSelectedItem().getElement(), "className", + * super.getSelectedItem().getStylePrimaryName()); } } + * + * super.onBrowserEvent(event); } + */ + } + + public Element getSubPartElement(String subPart) { + int index = Integer.parseInt(subPart.substring(6)); + ApplicationConnection.getConsole().log( + "Searching element for selection index " + index); + Element wrapperdiv = menu.getElement(); + com.google.gwt.dom.client.TableSectionElement tBody = (TableSectionElement) wrapperdiv + .getFirstChildElement().getFirstChildElement(); + TableRowElement item = tBody.getRows().getItem(index); + com.google.gwt.dom.client.Element clickableDivElement = item + .getFirstChildElement().getFirstChildElement(); + return clickableDivElement.cast(); + } + + public String getSubPartName(Element subElement) { + if (getElement().isOrHasChild(subElement)) { + com.google.gwt.dom.client.Element e = subElement; + { + while (e != null && !e.getTagName().toLowerCase().equals("tr")) { + e = e.getParentElement(); + ApplicationConnection.getConsole().log("Found row"); + } + } + com.google.gwt.dom.client.TableSectionElement parentElement = (TableSectionElement) e + .getParentElement(); + NodeList<TableRowElement> rows = parentElement.getRows(); + for (int i = 0; i < rows.getLength(); i++) { + if (rows.getItem(i) == e) { + ApplicationConnection.getConsole().log( + "Found index for row" + 1); + return "option" + i; + } + } + return null; + } else { + return null; + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ICustomComponent.java b/src/com/vaadin/terminal/gwt/client/ui/ICustomComponent.java new file mode 100644 index 0000000000..1e92111efa --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ICustomComponent.java @@ -0,0 +1,153 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.Set; + +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Container; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class ICustomComponent extends SimplePanel implements Container { + + private static final String CLASSNAME = "i-customcomponent"; + private String height; + private ApplicationConnection client; + private boolean rendering; + private String width; + private RenderSpace renderSpace = new RenderSpace(); + + public ICustomComponent() { + super(); + setStyleName(CLASSNAME); + } + + public void updateFromUIDL(UIDL uidl, final ApplicationConnection client) { + rendering = true; + if (client.updateComponent(this, uidl, true)) { + rendering = false; + return; + } + this.client = client; + + final UIDL child = uidl.getChildUIDL(0); + if (child != null) { + final Paintable p = client.getPaintable(child); + if (p != getWidget()) { + if (getWidget() != null) { + client.unregisterPaintable((Paintable) getWidget()); + clear(); + } + setWidget((Widget) p); + } + p.updateFromUIDL(child, client); + } + + boolean updateDynamicSize = updateDynamicSize(); + if (updateDynamicSize) { + DeferredCommand.addCommand(new Command() { + public void execute() { + // FIXME deferred relative size update needed to fix some + // scrollbar issues in sampler. This must be the wrong way + // to do it. Might be that some other component is broken. + client.handleComponentRelativeSize(ICustomComponent.this); + + } + }); + } + + renderSpace.setWidth(getElement().getOffsetWidth()); + renderSpace.setHeight(getElement().getOffsetHeight()); + + rendering = false; + } + + private boolean updateDynamicSize() { + boolean updated = false; + if (isDynamicWidth()) { + int childWidth = Util.getRequiredWidth(getWidget()); + getElement().getStyle().setPropertyPx("width", childWidth); + updated = true; + } + if (isDynamicHeight()) { + int childHeight = Util.getRequiredHeight(getWidget()); + getElement().getStyle().setPropertyPx("height", childHeight); + updated = true; + } + + return updated; + } + + private boolean isDynamicWidth() { + return width == null || width.equals(""); + } + + private boolean isDynamicHeight() { + return height == null || height.equals(""); + } + + public boolean hasChildComponent(Widget component) { + if (getWidget() == component) { + return true; + } else { + return false; + } + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + if (hasChildComponent(oldComponent)) { + clear(); + setWidget(newComponent); + } else { + throw new IllegalStateException(); + } + } + + public void updateCaption(Paintable component, UIDL uidl) { + // NOP, custom component dont render composition roots caption + } + + public boolean requestLayout(Set<Paintable> child) { + return !updateDynamicSize(); + } + + public RenderSpace getAllocatedSpace(Widget child) { + return renderSpace; + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + renderSpace.setHeight(getElement().getOffsetHeight()); + + if (!height.equals(this.height)) { + this.height = height; + if (!rendering) { + client.runDescendentsLayout(this); + } + } + } + + @Override + public void setWidth(String width) { + super.setWidth(width); + renderSpace.setWidth(getElement().getOffsetWidth()); + + if (!width.equals(this.width)) { + this.width = width; + if (!rendering) { + client.runDescendentsLayout(this); + } + } + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ICustomLayout.java b/src/com/vaadin/terminal/gwt/client/ui/ICustomLayout.java new file mode 100644 index 0000000000..ac7827f22c --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ICustomLayout.java @@ -0,0 +1,644 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.dom.client.ImageElement; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.ComplexPanel; +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.ContainerResizedListener; +import com.vaadin.terminal.gwt.client.ICaption; +import com.vaadin.terminal.gwt.client.ICaptionWrapper; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize; + +/** + * Custom Layout implements complex layout defined with HTML template. + * + * @author IT Mill + * + */ +public class ICustomLayout extends ComplexPanel implements Paintable, + Container, ContainerResizedListener { + + public static final String CLASSNAME = "i-customlayout"; + + /** Location-name to containing element in DOM map */ + private final HashMap locationToElement = new HashMap(); + + /** Location-name to contained widget map */ + private final HashMap<String, Widget> locationToWidget = new HashMap<String, Widget>(); + + /** Widget to captionwrapper map */ + private final HashMap widgetToCaptionWrapper = new HashMap(); + + /** Name of the currently rendered style */ + String currentTemplateName; + + /** Unexecuted scripts loaded from the template */ + private String scripts = ""; + + /** Paintable ID of this paintable */ + private String pid; + + private ApplicationConnection client; + + /** Has the template been loaded from contents passed in UIDL **/ + private boolean hasTemplateContents = false; + + private Element elementWithNativeResizeFunction; + + private String height = ""; + + private String width = ""; + + private HashMap<String, FloatSize> locationToExtraSize = new HashMap<String, FloatSize>(); + + public ICustomLayout() { + setElement(DOM.createDiv()); + // Clear any unwanted styling + DOM.setStyleAttribute(getElement(), "border", "none"); + DOM.setStyleAttribute(getElement(), "margin", "0"); + DOM.setStyleAttribute(getElement(), "padding", "0"); + + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(getElement(), "position", "relative"); + } + + setStyleName(CLASSNAME); + } + + /** + * Sets widget to given location. + * + * If location already contains a widget it will be removed. + * + * @param widget + * Widget to be set into location. + * @param location + * location name where widget will be added + * + * @throws IllegalArgumentException + * if no such location is found in the layout. + */ + public void setWidget(Widget widget, String location) { + + if (widget == null) { + return; + } + + // If no given location is found in the layout, and exception is throws + Element elem = (Element) locationToElement.get(location); + if (elem == null && hasTemplate()) { + throw new IllegalArgumentException("No location " + location + + " found"); + } + + // Get previous widget + final Widget previous = locationToWidget.get(location); + // NOP if given widget already exists in this location + if (previous == widget) { + return; + } + + if (previous != null) { + remove(previous); + } + + // if template is missing add element in order + if (!hasTemplate()) { + elem = getElement(); + } + + // Add widget to location + super.add(widget, elem); + locationToWidget.put(location, widget); + } + + /** Update the layout from UIDL */ + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + this.client = client; + // ApplicationConnection manages generic component features + if (client.updateComponent(this, uidl, true)) { + return; + } + + pid = uidl.getId(); + if (!hasTemplate()) { + // Update HTML template only once + initializeHTML(uidl, client); + } + + // Evaluate scripts + eval(scripts); + scripts = null; + + iLayout(); + // TODO Check if this is needed + client.runDescendentsLayout(this); + + Set oldWidgets = new HashSet(); + oldWidgets.addAll(locationToWidget.values()); + + // For all contained widgets + for (final Iterator i = uidl.getChildIterator(); i.hasNext();) { + final UIDL uidlForChild = (UIDL) i.next(); + if (uidlForChild.getTag().equals("location")) { + final String location = uidlForChild.getStringAttribute("name"); + final Paintable child = client.getPaintable(uidlForChild + .getChildUIDL(0)); + try { + setWidget((Widget) child, location); + child.updateFromUIDL(uidlForChild.getChildUIDL(0), client); + } catch (final IllegalArgumentException e) { + // If no location is found, this component is not visible + } + oldWidgets.remove(child); + } + } + for (Iterator iterator = oldWidgets.iterator(); iterator.hasNext();) { + Widget oldWidget = (Widget) iterator.next(); + if (oldWidget.isAttached()) { + // slot of this widget is emptied, remove it + remove(oldWidget); + } + } + + iLayout(); + // TODO Check if this is needed + client.runDescendentsLayout(this); + + } + + /** Initialize HTML-layout. */ + private void initializeHTML(UIDL uidl, ApplicationConnection client) { + + final String newTemplateContents = uidl + .getStringAttribute("templateContents"); + final String newTemplate = uidl.getStringAttribute("template"); + + currentTemplateName = null; + hasTemplateContents = false; + + String template = ""; + if (newTemplate != null) { + // Get the HTML-template from client + template = client.getResource("layouts/" + newTemplate + ".html"); + if (template == null) { + template = "<em>Layout file layouts/" + + newTemplate + + ".html is missing. Components will be drawn for debug purposes.</em>"; + } else { + currentTemplateName = newTemplate; + } + } else { + hasTemplateContents = true; + template = newTemplateContents; + } + + // Connect body of the template to DOM + template = extractBodyAndScriptsFromTemplate(template); + + // TODO prefix img src:s here with a regeps, cannot work further with IE + + String themeUri = client.getThemeUri(); + String relImgPrefix = themeUri + "/layouts/"; + + // prefix all relative image elements to point to theme dir with a + // regexp search + template = template.replaceAll( + "<((?:img)|(?:IMG))\\s([^>]*)src=\"((?![a-z]+:)[^/][^\"]+)\"", + "<$1 $2src=\"" + relImgPrefix + "$3\""); + // also support src attributes without quotes + template = template + .replaceAll( + "<((?:img)|(?:IMG))\\s([^>]*)src=[^\"]((?![a-z]+:)[^/][^ />]+)[ />]", + "<$1 $2src=\"" + relImgPrefix + "$3\""); + // also prefix relative style="...url(...)..." + template = template + .replaceAll( + "(<[^>]+style=\"[^\"]*url\\()((?![a-z]+:)[^/][^\"]+)(\\)[^>]*>)", + "$1 " + relImgPrefix + "$2 $3"); + + getElement().setInnerHTML(template); + + // Remap locations to elements + locationToElement.clear(); + scanForLocations(getElement()); + + initImgElements(); + + elementWithNativeResizeFunction = DOM.getFirstChild(getElement()); + if (elementWithNativeResizeFunction == null) { + elementWithNativeResizeFunction = getElement(); + } + publishResizedFunction(elementWithNativeResizeFunction); + + } + + private native boolean uriEndsWithSlash() + /*-{ + var path = $wnd.location.pathname; + if(path.charAt(path.length - 1) == "/") + return true; + return false; + }-*/; + + private boolean hasTemplate() { + if (currentTemplateName == null && !hasTemplateContents) { + return false; + } else { + return true; + } + } + + /** Collect locations from template */ + private void scanForLocations(Element elem) { + + final String location = elem.getAttribute("location"); + if (!"".equals(location)) { + locationToElement.put(location, elem); + elem.setInnerHTML(""); + int x = Util.measureHorizontalPaddingAndBorder(elem, 0); + int y = Util.measureVerticalPaddingAndBorder(elem, 0); + + FloatSize fs = new FloatSize(x, y); + + locationToExtraSize.put(location, fs); + + } else { + final int len = DOM.getChildCount(elem); + for (int i = 0; i < len; i++) { + scanForLocations(DOM.getChild(elem, i)); + } + } + } + + /** Evaluate given script in browser document */ + private static native void eval(String script) + /*-{ + try { + if (script != null) + eval("{ var document = $doc; var window = $wnd; "+ script + "}"); + } catch (e) { + } + }-*/; + + /** + * Img elements needs some special handling in custom layout. Img elements + * will get their onload events sunk. This way custom layout can notify + * parent about possible size change. + */ + private void initImgElements() { + NodeList<com.google.gwt.dom.client.Element> nodeList = getElement() + .getElementsByTagName("IMG"); + for (int i = 0; i < nodeList.getLength(); i++) { + com.google.gwt.dom.client.ImageElement img = (ImageElement) nodeList + .getItem(i); + DOM.sinkEvents((Element) img.cast(), Event.ONLOAD); + } + } + + /** + * Extract body part and script tags from raw html-template. + * + * Saves contents of all script-tags to private property: scripts. Returns + * contents of the body part for the html without script-tags. Also replaces + * all _UID_ tags with an unique id-string. + * + * @param html + * Original HTML-template received from server + * @return html that is used to create the HTMLPanel. + */ + private String extractBodyAndScriptsFromTemplate(String html) { + + // Replace UID:s + html = html.replaceAll("_UID_", pid + "__"); + + // Exctract script-tags + scripts = ""; + int endOfPrevScript = 0; + int nextPosToCheck = 0; + String lc = html.toLowerCase(); + String res = ""; + int scriptStart = lc.indexOf("<script", nextPosToCheck); + while (scriptStart > 0) { + res += html.substring(endOfPrevScript, scriptStart); + scriptStart = lc.indexOf(">", scriptStart); + final int j = lc.indexOf("</script>", scriptStart); + scripts += html.substring(scriptStart + 1, j) + ";"; + nextPosToCheck = endOfPrevScript = j + "</script>".length(); + scriptStart = lc.indexOf("<script", nextPosToCheck); + } + res += html.substring(endOfPrevScript); + + // Extract body + html = res; + lc = html.toLowerCase(); + int startOfBody = lc.indexOf("<body"); + if (startOfBody < 0) { + res = html; + } else { + res = ""; + startOfBody = lc.indexOf(">", startOfBody) + 1; + final int endOfBody = lc.indexOf("</body>", startOfBody); + if (endOfBody > startOfBody) { + res = html.substring(startOfBody, endOfBody); + } else { + res = html.substring(startOfBody); + } + } + + return res; + } + + /** Replace child components */ + public void replaceChildComponent(Widget from, Widget to) { + final String location = getLocation(from); + if (location == null) { + throw new IllegalArgumentException(); + } + setWidget(to, location); + } + + /** Does this layout contain given child */ + public boolean hasChildComponent(Widget component) { + return locationToWidget.containsValue(component); + } + + /** Update caption for given widget */ + public void updateCaption(Paintable component, UIDL uidl) { + ICaptionWrapper wrapper = (ICaptionWrapper) widgetToCaptionWrapper + .get(component); + if (ICaption.isNeeded(uidl)) { + if (wrapper == null) { + final String loc = getLocation((Widget) component); + super.remove((Widget) component); + wrapper = new ICaptionWrapper(component, client); + super.add(wrapper, (Element) locationToElement.get(loc)); + widgetToCaptionWrapper.put(component, wrapper); + } + wrapper.updateCaption(uidl); + } else { + if (wrapper != null) { + final String loc = getLocation((Widget) component); + super.remove(wrapper); + super.add((Widget) wrapper.getPaintable(), + (Element) locationToElement.get(loc)); + widgetToCaptionWrapper.remove(component); + } + } + } + + /** Get the location of an widget */ + public String getLocation(Widget w) { + for (final Iterator i = locationToWidget.keySet().iterator(); i + .hasNext();) { + final String location = (String) i.next(); + if (locationToWidget.get(location) == w) { + return location; + } + } + return null; + } + + /** Removes given widget from the layout */ + @Override + public boolean remove(Widget w) { + client.unregisterPaintable((Paintable) w); + final String location = getLocation(w); + if (location != null) { + locationToWidget.remove(location); + } + final ICaptionWrapper cw = (ICaptionWrapper) widgetToCaptionWrapper + .get(w); + if (cw != null) { + widgetToCaptionWrapper.remove(w); + return super.remove(cw); + } else if (w != null) { + return super.remove(w); + } + return false; + } + + /** Adding widget without specifying location is not supported */ + @Override + public void add(Widget w) { + throw new UnsupportedOperationException(); + } + + /** Clear all widgets from the layout */ + @Override + public void clear() { + super.clear(); + locationToWidget.clear(); + widgetToCaptionWrapper.clear(); + } + + public void iLayout() { + iLayoutJS(DOM.getFirstChild(getElement())); + } + + /** + * This method is published to JS side with the same name into first DOM + * node of custom layout. This way if one implements some resizeable + * containers in custom layout he/she can notify children after resize. + */ + public void notifyChildrenOfSizeChange() { + client.runDescendentsLayout(this); + } + + @Override + public void onDetach() { + super.onDetach(); + detachResizedFunction(elementWithNativeResizeFunction); + } + + private native void detachResizedFunction(Element element) + /*-{ + element.notifyChildrenOfSizeChange = null; + }-*/; + + private native void publishResizedFunction(Element element) + /*-{ + var self = this; + element.notifyChildrenOfSizeChange = function() { + self.@com.vaadin.terminal.gwt.client.ui.ICustomLayout::notifyChildrenOfSizeChange()(); + }; + }-*/; + + /** + * In custom layout one may want to run layout functions made with + * JavaScript. This function tests if one exists (with name "iLayoutJS" in + * layouts first DOM node) and runs et. Return value is used to determine if + * children needs to be notified of size changes. + * + * Note! When implementing a JS layout function you most likely want to call + * notifyChildrenOfSizeChange() function on your custom layouts main + * element. That method is used to control whether child components layout + * functions are to be run. + * + * @param el + * @return true if layout function exists and was run successfully, else + * false. + */ + private native boolean iLayoutJS(Element el) + /*-{ + if(el && el.iLayoutJS) { + try { + el.iLayoutJS(); + return true; + } catch (e) { + return false; + } + } else { + return false; + } + }-*/; + + public boolean requestLayout(Set<Paintable> child) { + updateRelativeSizedComponents(true, true); + + if (width.equals("") || height.equals("")) { + /* Automatically propagated upwards if the size can change */ + return false; + } + + return true; + } + + public RenderSpace getAllocatedSpace(Widget child) { + com.google.gwt.dom.client.Element pe = child.getElement() + .getParentElement(); + + FloatSize extra = locationToExtraSize.get(getLocation(child)); + return new RenderSpace(pe.getOffsetWidth() - (int) extra.getWidth(), pe + .getOffsetHeight() + - (int) extra.getHeight(), Util.mayHaveScrollBars(pe)); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + event.cancelBubble(true); + } + } + + @Override + public void setHeight(String height) { + if (this.height.equals(height)) { + return; + } + + boolean shrinking = true; + if (isLarger(height, this.height)) { + shrinking = false; + } + + this.height = height; + super.setHeight(height); + + /* + * If the height shrinks we must remove all components with relative + * height from the DOM, update their height when they do not affect the + * available space and finally restore them to the original state + */ + if (shrinking) { + updateRelativeSizedComponents(false, true); + } + } + + @Override + public void setWidth(String width) { + if (this.width.equals(width)) { + return; + } + + boolean shrinking = true; + if (isLarger(width, this.width)) { + shrinking = false; + } + + super.setWidth(width); + this.width = width; + + /* + * If the width shrinks we must remove all components with relative + * width from the DOM, update their width when they do not affect the + * available space and finally restore them to the original state + */ + if (shrinking) { + updateRelativeSizedComponents(true, false); + } + } + + private void updateRelativeSizedComponents(boolean relativeWidth, + boolean relativeHeight) { + + Set<Widget> relativeSizeWidgets = new HashSet<Widget>(); + + for (Widget widget : locationToWidget.values()) { + FloatSize relativeSize = client.getRelativeSize(widget); + if (relativeSize != null) { + if ((relativeWidth && (relativeSize.getWidth() >= 0.0f)) + || (relativeHeight && (relativeSize.getHeight() >= 0.0f))) { + + relativeSizeWidgets.add(widget); + widget.getElement().getStyle().setProperty("position", + "absolute"); + } + } + } + + for (Widget widget : relativeSizeWidgets) { + client.handleComponentRelativeSize(widget); + widget.getElement().getStyle().setProperty("position", ""); + } + } + + /** + * Compares newSize with currentSize and returns true if it is clear that + * newSize is larger than currentSize. Returns false if newSize is smaller + * or if it is unclear which one is smaller. + * + * @param newSize + * @param currentSize + * @return + */ + private boolean isLarger(String newSize, String currentSize) { + if (newSize.equals("") || currentSize.equals("")) { + return false; + } + + if (!newSize.endsWith("px") || !currentSize.endsWith("px")) { + return false; + } + + int newSizePx = Integer.parseInt(newSize.substring(0, + newSize.length() - 2)); + int currentSizePx = Integer.parseInt(currentSize.substring(0, + currentSize.length() - 2)); + + boolean larger = newSizePx > currentSizePx; + return larger; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IDateField.java b/src/com/vaadin/terminal/gwt/client/ui/IDateField.java new file mode 100644 index 0000000000..b62def236e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IDateField.java @@ -0,0 +1,233 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ClientExceptionHandler;
+import com.vaadin.terminal.gwt.client.DateTimeService;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.LocaleNotLoadedException;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IDateField extends FlowPanel implements Paintable, Field {
+
+ public static final String CLASSNAME = "i-datefield";
+
+ protected String id;
+
+ protected ApplicationConnection client;
+
+ protected boolean immediate;
+
+ public static final int RESOLUTION_YEAR = 0;
+ public static final int RESOLUTION_MONTH = 1;
+ public static final int RESOLUTION_DAY = 2;
+ public static final int RESOLUTION_HOUR = 3;
+ public static final int RESOLUTION_MIN = 4;
+ public static final int RESOLUTION_SEC = 5;
+ public static final int RESOLUTION_MSEC = 6;
+
+ protected int currentResolution = RESOLUTION_YEAR;
+
+ protected String currentLocale;
+
+ protected boolean readonly;
+
+ protected boolean enabled;
+
+ protected Date date = null;
+ // e.g when paging a calendar, before actually selecting
+ protected Date showingDate = new Date();
+
+ protected DateTimeService dts;
+
+ public IDateField() {
+ setStyleName(CLASSNAME);
+ dts = new DateTimeService();
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (client != null) {
+ client.handleTooltipEvent(event, this);
+ }
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Ensure correct implementation and let layout manage caption
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ // Save details
+ this.client = client;
+ id = uidl.getId();
+ immediate = uidl.getBooleanAttribute("immediate");
+
+ readonly = uidl.getBooleanAttribute("readonly");
+ enabled = !uidl.getBooleanAttribute("disabled");
+
+ if (uidl.hasAttribute("locale")) {
+ final String locale = uidl.getStringAttribute("locale");
+ try {
+ dts.setLocale(locale);
+ currentLocale = locale;
+ } catch (final LocaleNotLoadedException e) {
+ currentLocale = dts.getLocale();
+ ClientExceptionHandler.displayError(
+ "Tried to use an unloaded locale \"" + locale
+ + "\". Using default locale (" + currentLocale
+ + ").", e);
+ }
+ }
+
+ int newResolution;
+ if (uidl.hasVariable("msec")) {
+ newResolution = RESOLUTION_MSEC;
+ } else if (uidl.hasVariable("sec")) {
+ newResolution = RESOLUTION_SEC;
+ } else if (uidl.hasVariable("min")) {
+ newResolution = RESOLUTION_MIN;
+ } else if (uidl.hasVariable("hour")) {
+ newResolution = RESOLUTION_HOUR;
+ } else if (uidl.hasVariable("day")) {
+ newResolution = RESOLUTION_DAY;
+ } else if (uidl.hasVariable("month")) {
+ newResolution = RESOLUTION_MONTH;
+ } else {
+ newResolution = RESOLUTION_YEAR;
+ }
+
+ currentResolution = newResolution;
+
+ final int year = uidl.getIntVariable("year");
+ final int month = (currentResolution >= RESOLUTION_MONTH) ? uidl
+ .getIntVariable("month") : -1;
+ final int day = (currentResolution >= RESOLUTION_DAY) ? uidl
+ .getIntVariable("day") : -1;
+ final int hour = (currentResolution >= RESOLUTION_HOUR) ? uidl
+ .getIntVariable("hour") : 0;
+ final int min = (currentResolution >= RESOLUTION_MIN) ? uidl
+ .getIntVariable("min") : 0;
+ final int sec = (currentResolution >= RESOLUTION_SEC) ? uidl
+ .getIntVariable("sec") : 0;
+ final int msec = (currentResolution >= RESOLUTION_MSEC) ? uidl
+ .getIntVariable("msec") : 0;
+
+ // Construct new date for this datefield (only if not null)
+ if (year > -1) {
+ date = new Date((long) getTime(year, month, day, hour, min, sec,
+ msec));
+ showingDate.setTime(date.getTime());
+ } else {
+ date = null;
+ showingDate = new Date();
+ }
+
+ }
+
+ /*
+ * We need this redundant native function because Java's Date object doesn't
+ * have a setMilliseconds method.
+ */
+ private static native double getTime(int y, int m, int d, int h, int mi,
+ int s, int ms)
+ /*-{
+ try {
+ var date = new Date(2000,1,1,1); // don't use current date here
+ if(y && y >= 0) date.setFullYear(y);
+ if(m && m >= 1) date.setMonth(m-1);
+ if(d && d >= 0) date.setDate(d);
+ if(h >= 0) date.setHours(h);
+ if(mi >= 0) date.setMinutes(mi);
+ if(s >= 0) date.setSeconds(s);
+ if(ms >= 0) date.setMilliseconds(ms);
+ return date.getTime();
+ } catch (e) {
+ // TODO print some error message on the console
+ //console.log(e);
+ return (new Date()).getTime();
+ }
+ }-*/;
+
+ public int getMilliseconds() {
+ return (int) (date.getTime() - date.getTime() / 1000 * 1000);
+ }
+
+ public void setMilliseconds(int ms) {
+ date.setTime(date.getTime() / 1000 * 1000 + ms);
+ }
+
+ public int getShowingMilliseconds() {
+ return (int) (showingDate.getTime() - showingDate.getTime() / 1000 * 1000);
+ }
+
+ public void setShowingMilliseconds(int ms) {
+ showingDate.setTime(showingDate.getTime() / 1000 * 1000 + ms);
+ }
+
+ public int getCurrentResolution() {
+ return currentResolution;
+ }
+
+ public void setCurrentResolution(int currentResolution) {
+ this.currentResolution = currentResolution;
+ }
+
+ public String getCurrentLocale() {
+ return currentLocale;
+ }
+
+ public void setCurrentLocale(String currentLocale) {
+ this.currentLocale = currentLocale;
+ }
+
+ public Date getCurrentDate() {
+ return date;
+ }
+
+ public void setCurrentDate(Date date) {
+ this.date = date;
+ }
+
+ public Date getShowingDate() {
+ return showingDate;
+ }
+
+ public void setShowingDate(Date date) {
+ showingDate = date;
+ }
+
+ public boolean isImmediate() {
+ return immediate;
+ }
+
+ public boolean isReadonly() {
+ return readonly;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public DateTimeService getDateTimeService() {
+ return dts;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public ApplicationConnection getClient() {
+ return client;
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IDateFieldCalendar.java b/src/com/vaadin/terminal/gwt/client/ui/IDateFieldCalendar.java new file mode 100644 index 0000000000..24f614702e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IDateFieldCalendar.java @@ -0,0 +1,26 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IDateFieldCalendar extends IDateField {
+
+ private final ICalendarPanel date;
+
+ public IDateFieldCalendar() {
+ super();
+ date = new ICalendarPanel(this);
+ add(date);
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ super.updateFromUIDL(uidl, client);
+ date.updateCalendar();
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IEmbedded.java b/src/com/vaadin/terminal/gwt/client/ui/IEmbedded.java new file mode 100644 index 0000000000..8c8340b470 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IEmbedded.java @@ -0,0 +1,225 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.Iterator; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.ObjectElement; +import com.google.gwt.dom.client.Style; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class IEmbedded extends HTML implements Paintable { + private static String CLASSNAME = "i-embedded"; + + private String height; + private String width; + private Element browserElement; + + private ApplicationConnection client; + + public IEmbedded() { + setStyleName(CLASSNAME); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (client.updateComponent(this, uidl, true)) { + return; + } + this.client = client; + + boolean clearBrowserElement = true; + + if (uidl.hasAttribute("type")) { + final String type = uidl.getStringAttribute("type"); + if (type.equals("image")) { + Element el = null; + boolean created = false; + NodeList nodes = getElement().getChildNodes(); + if (nodes != null && nodes.getLength() == 1) { + Node n = nodes.getItem(0); + if (n.getNodeType() == Node.ELEMENT_NODE) { + Element e = (Element) n; + if (e.getTagName().equals("IMG")) { + el = e; + } + } + } + if (el == null) { + setHTML(""); + el = DOM.createImg(); + created = true; + client.addPngFix(el); + DOM.sinkEvents(el, Event.ONLOAD); + } + + // Set attributes + Style style = el.getStyle(); + String w = uidl.getStringAttribute("width"); + if (w != null) { + style.setProperty("width", w); + } else { + style.setProperty("width", ""); + } + String h = uidl.getStringAttribute("height"); + if (h != null) { + style.setProperty("height", h); + } else { + style.setProperty("height", ""); + } + DOM.setElementProperty(el, "src", getSrc(uidl, client)); + + if (created) { + // insert in dom late + getElement().appendChild(el); + } + + } else if (type.equals("browser")) { + if (browserElement == null) { + setHTML("<iframe width=\"100%\" height=\"100%\" frameborder=\"0\" src=\"" + + getSrc(uidl, client) + + "\" name=\"" + + uidl.getId() + "\"></iframe>"); + browserElement = DOM.getFirstChild(getElement()); + } else { + DOM.setElementAttribute(browserElement, "src", getSrc(uidl, + client)); + } + clearBrowserElement = false; + } else { + ApplicationConnection.getConsole().log( + "Unknown Embedded type '" + type + "'"); + } + } else if (uidl.hasAttribute("mimetype")) { + final String mime = uidl.getStringAttribute("mimetype"); + if (mime.equals("application/x-shockwave-flash")) { + setHTML("<object width=\"" + width + "\" height=\"" + height + + "\"><param name=\"movie\" value=\"" + + getSrc(uidl, client) + "\"><embed src=\"" + + getSrc(uidl, client) + "\" width=\"" + width + + "\" height=\"" + height + "\"></embed></object>"); + } else if (mime.equals("image/svg+xml")) { + String data; + if (getParameter("data", uidl) == null) { + data = getSrc(uidl, client); + } else { + data = "data:image/svg+xml," + getParameter("data", uidl); + } + setHTML(""); + ObjectElement obj = Document.get().createObjectElement(); + obj.setType(mime); + obj.setData(data); + if (width != null) { + obj.getStyle().setProperty("width", "100%"); + } + if (height != null) { + obj.getStyle().setProperty("height", "100%"); + } + getElement().appendChild(obj); + + } else { + ApplicationConnection.getConsole().log( + "Unknown Embedded mimetype '" + mime + "'"); + } + } else { + ApplicationConnection.getConsole().log( + "Unknown Embedded; no type or mimetype attribute"); + } + + if (clearBrowserElement) { + browserElement = null; + } + + } + + private static String getParameter(String paramName, UIDL uidl) { + Iterator childIterator = uidl.getChildIterator(); + while (childIterator.hasNext()) { + Object child = childIterator.next(); + if (child instanceof UIDL) { + UIDL childUIDL = (UIDL) child; + if (childUIDL.getTag().equals("embeddedparam") + && childUIDL.getStringAttribute("name").equals( + paramName)) { + return childUIDL.getStringAttribute("value"); + } + + } + } + return null; + } + + /** + * Helper to return translated src-attribute from embedded's UIDL + * + * @param uidl + * @param client + * @return + */ + private String getSrc(UIDL uidl, ApplicationConnection client) { + String url = client.translateToolkitUri(uidl.getStringAttribute("src")); + if (url == null) { + return ""; + } + return url; + } + + @Override + public void setWidth(String width) { + this.width = width; + if (isDynamicHeight()) { + int oldHeight = getOffsetHeight(); + super.setWidth(width); + int newHeight = getOffsetHeight(); + /* + * Must notify parent if the height changes as a result of a width + * change + */ + if (oldHeight != newHeight) { + Util.notifyParentOfSizeChange(this, false); + } + } else { + super.setWidth(width); + } + + } + + private boolean isDynamicHeight() { + return height == null || height.equals(""); + } + + @Override + public void setHeight(String height) { + this.height = height; + super.setHeight(height); + } + + @Override + protected void onDetach() { + // Force browser to fire unload event when component is detached from + // the view (IE doesn't do this automatically) + if (browserElement != null) { + DOM.setElementAttribute(browserElement, "src", "javascript:false"); + } + super.onDetach(); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (DOM.eventGetType(event) == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IFilterSelect.java b/src/com/vaadin/terminal/gwt/client/ui/IFilterSelect.java new file mode 100644 index 0000000000..ec9ecf1cf6 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IFilterSelect.java @@ -0,0 +1,1059 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.FocusListener; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.KeyboardListener; +import com.google.gwt.user.client.ui.LoadListener; +import com.google.gwt.user.client.ui.PopupListener; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; +import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.Focusable; +import com.vaadin.terminal.gwt.client.ITooltip; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +/** + * + * TODO needs major refactoring (to be extensible etc) + */ +public class IFilterSelect extends Composite implements Paintable, Field, + KeyboardListener, ClickListener, FocusListener, Focusable { + + public class FilterSelectSuggestion implements Suggestion, Command { + + private final String key; + private final String caption; + private String iconUri; + + public FilterSelectSuggestion(UIDL uidl) { + key = uidl.getStringAttribute("key"); + caption = uidl.getStringAttribute("caption"); + if (uidl.hasAttribute("icon")) { + iconUri = client.translateToolkitUri(uidl + .getStringAttribute("icon")); + } + } + + public String getDisplayString() { + final StringBuffer sb = new StringBuffer(); + if (iconUri != null) { + sb.append("<img src=\""); + sb.append(iconUri); + sb.append("\" alt=\"\" class=\"i-icon\" />"); + } + sb.append("<span>" + Util.escapeHTML(caption) + "</span>"); + return sb.toString(); + } + + public String getReplacementString() { + return caption; + } + + public int getOptionKey() { + return Integer.parseInt(key); + } + + public String getIconUri() { + return iconUri; + } + + public void execute() { + onSuggestionSelected(this); + } + } + + public class SuggestionPopup extends IToolkitOverlay implements + PositionCallback, PopupListener { + + private static final String Z_INDEX = "30000"; + + private final SuggestionMenu menu; + + private final Element up = DOM.createDiv(); + private final Element down = DOM.createDiv(); + private final Element status = DOM.createDiv(); + + private boolean isPagingEnabled = true; + + private long lastAutoClosed; + + private int popupOuterPadding = -1; + + private int topPosition; + + SuggestionPopup() { + super(true, false, true); + menu = new SuggestionMenu(); + setWidget(menu); + setStyleName(CLASSNAME + "-suggestpopup"); + DOM.setStyleAttribute(getElement(), "zIndex", Z_INDEX); + + final Element root = getContainerElement(); + + DOM.setInnerHTML(up, "<span>Prev</span>"); + DOM.sinkEvents(up, Event.ONCLICK); + DOM.setInnerHTML(down, "<span>Next</span>"); + DOM.sinkEvents(down, Event.ONCLICK); + DOM.insertChild(root, up, 0); + DOM.appendChild(root, down); + DOM.appendChild(root, status); + DOM.setElementProperty(status, "className", CLASSNAME + "-status"); + + addPopupListener(this); + } + + public void showSuggestions( + Collection<FilterSelectSuggestion> currentSuggestions, + int currentPage, int totalSuggestions) { + + // Add TT anchor point + DOM.setElementProperty(getElement(), "id", + "TOOLKIT_COMBOBOX_OPTIONLIST"); + + menu.setSuggestions(currentSuggestions); + final int x = IFilterSelect.this.getAbsoluteLeft(); + topPosition = tb.getAbsoluteTop(); + topPosition += tb.getOffsetHeight(); + setPopupPosition(x, topPosition); + + final int first = currentPage * PAGELENTH + + (nullSelectionAllowed && currentPage > 0 ? 0 : 1); + final int last = first + currentSuggestions.size() - 1; + final int matches = totalSuggestions + - (nullSelectionAllowed ? 1 : 0); + if (last > 0) { + // nullsel not counted, as requested by user + DOM.setInnerText(status, (matches == 0 ? 0 : first) + + "-" + + ("".equals(lastFilter) && nullSelectionAllowed + && currentPage == 0 ? last - 1 : last) + "/" + + matches); + } else { + DOM.setInnerText(status, ""); + } + // We don't need to show arrows or statusbar if there is only one + // page + if (matches <= PAGELENTH) { + setPagingEnabled(false); + } else { + setPagingEnabled(true); + } + setPrevButtonActive(first > 1); + setNextButtonActive(last < matches); + + // clear previously fixed width + menu.setWidth(""); + DOM.setStyleAttribute(DOM.getFirstChild(menu.getElement()), + "width", ""); + + setPopupPositionAndShow(this); + } + + private void setNextButtonActive(boolean b) { + if (b) { + DOM.sinkEvents(down, Event.ONCLICK); + DOM.setElementProperty(down, "className", CLASSNAME + + "-nextpage"); + } else { + DOM.sinkEvents(down, 0); + DOM.setElementProperty(down, "className", CLASSNAME + + "-nextpage-off"); + } + } + + private void setPrevButtonActive(boolean b) { + if (b) { + DOM.sinkEvents(up, Event.ONCLICK); + DOM + .setElementProperty(up, "className", CLASSNAME + + "-prevpage"); + } else { + DOM.sinkEvents(up, 0); + DOM.setElementProperty(up, "className", CLASSNAME + + "-prevpage-off"); + } + + } + + public void selectNextItem() { + final MenuItem cur = menu.getSelectedItem(); + final int index = 1 + menu.getItems().indexOf(cur); + if (menu.getItems().size() > index) { + final MenuItem newSelectedItem = (MenuItem) menu.getItems() + .get(index); + menu.selectItem(newSelectedItem); + tb.setText(newSelectedItem.getText()); + tb.setSelectionRange(lastFilter.length(), newSelectedItem + .getText().length() + - lastFilter.length()); + + } else if (hasNextPage()) { + lastIndex = index - 1; // save for paging + filterOptions(currentPage + 1, lastFilter); + } + } + + public void selectPrevItem() { + final MenuItem cur = menu.getSelectedItem(); + final int index = -1 + menu.getItems().indexOf(cur); + if (index > -1) { + final MenuItem newSelectedItem = (MenuItem) menu.getItems() + .get(index); + menu.selectItem(newSelectedItem); + tb.setText(newSelectedItem.getText()); + tb.setSelectionRange(lastFilter.length(), newSelectedItem + .getText().length() + - lastFilter.length()); + } else if (index == -1) { + if (currentPage > 0) { + lastIndex = index + 1; // save for paging + filterOptions(currentPage - 1, lastFilter); + } + } else { + final MenuItem newSelectedItem = (MenuItem) menu.getItems() + .get(menu.getItems().size() - 1); + menu.selectItem(newSelectedItem); + tb.setText(newSelectedItem.getText()); + tb.setSelectionRange(lastFilter.length(), newSelectedItem + .getText().length() + - lastFilter.length()); + } + } + + @Override + public void onBrowserEvent(Event event) { + final Element target = DOM.eventGetTarget(event); + if (DOM.compare(target, up) + || DOM.compare(target, DOM.getChild(up, 0))) { + filterOptions(currentPage - 1, lastFilter); + } else if (DOM.compare(target, down) + || DOM.compare(target, DOM.getChild(down, 0))) { + filterOptions(currentPage + 1, lastFilter); + } + tb.setFocus(true); + } + + public void setPagingEnabled(boolean paging) { + if (isPagingEnabled == paging) { + return; + } + if (paging) { + DOM.setStyleAttribute(down, "display", ""); + DOM.setStyleAttribute(up, "display", ""); + DOM.setStyleAttribute(status, "display", ""); + } else { + DOM.setStyleAttribute(down, "display", "none"); + DOM.setStyleAttribute(up, "display", "none"); + DOM.setStyleAttribute(status, "display", "none"); + } + isPagingEnabled = paging; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.PopupPanel$PositionCallback#setPosition + * (int, int) + */ + public void setPosition(int offsetWidth, int offsetHeight) { + + int top = -1; + int left = -1; + + // reset menu size and retrieve its "natural" size + menu.setHeight(""); + if (currentPage > 0) { + // fix height to avoid height change when getting to last page + menu.fixHeightTo(PAGELENTH); + } + offsetHeight = getOffsetHeight(); + + final int desiredWidth = getMainWidth(); + int naturalMenuWidth = DOM.getElementPropertyInt(DOM + .getFirstChild(menu.getElement()), "offsetWidth"); + + if (popupOuterPadding == -1) { + popupOuterPadding = Util.measureHorizontalPaddingAndBorder( + getElement(), 2); + } + + if (naturalMenuWidth < desiredWidth) { + menu.setWidth((desiredWidth - popupOuterPadding) + "px"); + DOM.setStyleAttribute(DOM.getFirstChild(menu.getElement()), + "width", "100%"); + naturalMenuWidth = desiredWidth; + } + + if (BrowserInfo.get().isIE()) { + /* + * IE requires us to specify the width for the container + * element. Otherwise it will be 100% wide + */ + int rootWidth = naturalMenuWidth - popupOuterPadding; + DOM.setStyleAttribute(getContainerElement(), "width", rootWidth + + "px"); + } + + if (offsetHeight + getPopupTop() > Window.getClientHeight() + + Window.getScrollTop()) { + // popup on top of input instead + top = getPopupTop() - offsetHeight + - IFilterSelect.this.getOffsetHeight(); + if (top < 0) { + top = 0; + } + } else { + top = getPopupTop(); + /* + * Take popup top margin into account. getPopupTop() returns the + * top value including the margin but the value we give must not + * include the margin. + */ + int topMargin = (top - topPosition); + top -= topMargin; + } + + // fetch real width (mac FF bugs here due GWT popups overflow:auto ) + offsetWidth = DOM.getElementPropertyInt(DOM.getFirstChild(menu + .getElement()), "offsetWidth"); + if (offsetWidth + getPopupLeft() > Window.getClientWidth() + + Window.getScrollLeft()) { + left = IFilterSelect.this.getAbsoluteLeft() + + IFilterSelect.this.getOffsetWidth() + + Window.getScrollLeft() - offsetWidth; + if (left < 0) { + left = 0; + } + } else { + left = getPopupLeft(); + } + setPopupPosition(left, top); + + } + + /** + * @return true if popup was just closed + */ + public boolean isJustClosed() { + final long now = (new Date()).getTime(); + return (lastAutoClosed > 0 && (now - lastAutoClosed) < 200); + } + + public void onPopupClosed(PopupPanel sender, boolean autoClosed) { + if (autoClosed) { + lastAutoClosed = (new Date()).getTime(); + } + } + + /** + * Updates style names in suggestion popup to help theme building. + */ + public void updateStyleNames(UIDL uidl) { + if (uidl.hasAttribute("style")) { + setStyleName(CLASSNAME + "-suggestpopup"); + final String[] styles = uidl.getStringAttribute("style").split( + " "); + for (int i = 0; i < styles.length; i++) { + addStyleDependentName(styles[i]); + } + } + } + + } + + public class SuggestionMenu extends MenuBar { + + SuggestionMenu() { + super(true); + setStyleName(CLASSNAME + "-suggestmenu"); + } + + /** + * Fixes menus height to use same space as full page would use. Needed + * to avoid height changes when quickly "scrolling" to last page + */ + public void fixHeightTo(int pagelenth) { + if (currentSuggestions.size() > 0) { + final int pixels = pagelenth * (getOffsetHeight() - 2) + / currentSuggestions.size(); + setHeight((pixels + 2) + "px"); + } + } + + public void setSuggestions( + Collection<FilterSelectSuggestion> suggestions) { + clearItems(); + final Iterator<FilterSelectSuggestion> it = suggestions.iterator(); + while (it.hasNext()) { + final FilterSelectSuggestion s = it.next(); + final MenuItem mi = new MenuItem(s.getDisplayString(), true, s); + + com.google.gwt.dom.client.Element child = mi.getElement() + .getFirstChildElement(); + while (child != null) { + if (child.getNodeName().toLowerCase().equals("img")) { + DOM + .sinkEvents((Element) child.cast(), + (DOM.getEventsSunk((Element) child + .cast()) | Event.ONLOAD)); + } + child = child.getNextSiblingElement(); + } + + this.addItem(mi); + if (s == currentSuggestion) { + selectItem(mi); + } + } + } + + public void doSelectedItemAction() { + final MenuItem item = getSelectedItem(); + final String enteredItemValue = tb.getText(); + // check for exact match in menu + int p = getItems().size(); + if (p > 0) { + for (int i = 0; i < p; i++) { + final MenuItem potentialExactMatch = (MenuItem) getItems() + .get(i); + if (potentialExactMatch.getText().equals(enteredItemValue)) { + selectItem(potentialExactMatch); + doItemAction(potentialExactMatch, true); + suggestionPopup.hide(); + return; + } + } + } + if (allowNewItem) { + + if (!prompting && !enteredItemValue.equals(lastNewItemString)) { + /* + * Store last sent new item string to avoid double sends + */ + lastNewItemString = enteredItemValue; + client.updateVariable(paintableId, "newitem", + enteredItemValue, immediate); + } + } else if (item != null + && !"".equals(lastFilter) + && item.getText().toLowerCase().startsWith( + lastFilter.toLowerCase())) { + doItemAction(item, true); + } else { + if (currentSuggestion != null) { + String text = currentSuggestion.getReplacementString(); + /* TODO? + if (text.equals("")) { + addStyleDependentName(CLASSNAME_PROMPT); + tb.setText(inputPrompt); + prompting = true; + } else { + tb.setText(text); + prompting = false; + removeStyleDependentName(CLASSNAME_PROMPT); + } + */ + selectedOptionKey = currentSuggestion.key; + } + } + suggestionPopup.hide(); + } + + @Override + public void onBrowserEvent(Event event) { + if (event.getTypeInt() == Event.ONLOAD) { + if (suggestionPopup.isVisible()) { + setWidth(""); + DOM.setStyleAttribute(DOM.getFirstChild(getElement()), + "width", ""); + suggestionPopup.setPopupPositionAndShow(suggestionPopup); + } + } + super.onBrowserEvent(event); + } + } + + public static final int FILTERINGMODE_OFF = 0; + public static final int FILTERINGMODE_STARTSWITH = 1; + public static final int FILTERINGMODE_CONTAINS = 2; + + private static final String CLASSNAME = "i-filterselect"; + + public static final int PAGELENTH = 10; + + private final FlowPanel panel = new FlowPanel(); + + private final TextBox tb = new TextBox() { + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (client != null) { + client.handleTooltipEvent(event, IFilterSelect.this); + } + } + }; + + private final SuggestionPopup suggestionPopup = new SuggestionPopup(); + + private final HTML popupOpener = new HTML(""); + + private final Image selectedItemIcon = new Image(); + + private ApplicationConnection client; + + private String paintableId; + + private int currentPage; + + private final Collection<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>(); + + private boolean immediate; + + private String selectedOptionKey; + + private boolean filtering = false; + + private String lastFilter = ""; + private int lastIndex = -1; // last selected index when using arrows + + private FilterSelectSuggestion currentSuggestion; + + private int totalMatches; + private boolean allowNewItem; + private boolean nullSelectionAllowed; + private boolean enabled; + + // shown in unfocused empty field, disappears on focus (e.g "Search here") + private static final String CLASSNAME_PROMPT = "prompt"; + private static final String ATTR_INPUTPROMPT = "prompt"; + private String inputPrompt = ""; + private boolean prompting = false; + + // Set true when popupopened has been clicked. Cleared on each UIDL-update. + // This handles the special case where are not filtering yet and the + // selected value has changed on the server-side. See #2119 + private boolean popupOpenerClicked; + private String width = null; + private int textboxPadding = -1; + private int componentPadding = -1; + private int suggestionPopupMinWidth = 0; + /* + * Stores the last new item string to avoid double submissions. Cleared on + * uidl updates + */ + private String lastNewItemString; + private boolean focused = false; + + public IFilterSelect() { + selectedItemIcon.setVisible(false); + selectedItemIcon.setStyleName("i-icon"); + selectedItemIcon.addLoadListener(new LoadListener() { + public void onError(Widget sender) { + } + + public void onLoad(Widget sender) { + updateRootWidth(); + updateSelectedIconPosition(); + } + }); + + panel.add(selectedItemIcon); + tb.sinkEvents(ITooltip.TOOLTIP_EVENTS); + panel.add(tb); + panel.add(popupOpener); + initWidget(panel); + setStyleName(CLASSNAME); + tb.addKeyboardListener(this); + tb.setStyleName(CLASSNAME + "-input"); + tb.addFocusListener(this); + popupOpener.setStyleName(CLASSNAME + "-button"); + popupOpener.addClickListener(this); + } + + public boolean hasNextPage() { + if (totalMatches > (currentPage + 1) * PAGELENTH) { + return true; + } else { + return false; + } + } + + public void filterOptions(int page) { + filterOptions(page, tb.getText()); + } + + public void filterOptions(int page, String filter) { + if (filter.equals(lastFilter) && currentPage == page) { + if (!suggestionPopup.isAttached()) { + suggestionPopup.showSuggestions(currentSuggestions, + currentPage, totalMatches); + } + return; + } + if (!filter.equals(lastFilter)) { + // we are on subsequent page and text has changed -> reset page + if ("".equals(filter)) { + // let server decide + page = -1; + } else { + page = 0; + } + } + + filtering = true; + client.updateVariable(paintableId, "filter", filter, false); + client.updateVariable(paintableId, "page", page, true); + lastFilter = filter; + currentPage = page; + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + paintableId = uidl.getId(); + this.client = client; + + boolean readonly = uidl.hasAttribute("readonly"); + boolean disabled = uidl.hasAttribute("disabled"); + + if (disabled || readonly) { + tb.setEnabled(false); + enabled = false; + } else { + tb.setEnabled(true); + enabled = true; + } + + if (client.updateComponent(this, uidl, true)) { + return; + } + + // not a FocusWidget -> needs own tabindex handling + if (uidl.hasAttribute("tabindex")) { + tb.setTabIndex(uidl.getIntAttribute("tabindex")); + } + + immediate = uidl.hasAttribute("immediate"); + + nullSelectionAllowed = uidl.hasAttribute("nullselect"); + + currentPage = uidl.getIntVariable("page"); + + if (uidl.hasAttribute(ATTR_INPUTPROMPT)) { + // input prompt changed from server + inputPrompt = uidl.getStringAttribute(ATTR_INPUTPROMPT); + } else { + inputPrompt = ""; + } + + suggestionPopup.setPagingEnabled(true); + suggestionPopup.updateStyleNames(uidl); + + allowNewItem = uidl.hasAttribute("allownewitem"); + lastNewItemString = null; + + currentSuggestions.clear(); + final UIDL options = uidl.getChildUIDL(0); + totalMatches = uidl.getIntAttribute("totalMatches"); + + String captions = inputPrompt; + + for (final Iterator i = options.getChildIterator(); i.hasNext();) { + final UIDL optionUidl = (UIDL) i.next(); + final FilterSelectSuggestion suggestion = new FilterSelectSuggestion( + optionUidl); + currentSuggestions.add(suggestion); + if (optionUidl.hasAttribute("selected")) { + if (!filtering || popupOpenerClicked) { + tb.setText(suggestion.getReplacementString()); + selectedOptionKey = "" + suggestion.getOptionKey(); + } + currentSuggestion = suggestion; + setSelectedItemIcon(suggestion.getIconUri()); + } + + // Collect captions so we can calculate minimum width for textarea + if (captions.length() > 0) { + captions += "|"; + } + captions += suggestion.getReplacementString(); + } + + if ((!filtering || popupOpenerClicked) && uidl.hasVariable("selected") + && uidl.getStringArrayVariable("selected").length == 0) { + // select nulled + if (!filtering || !popupOpenerClicked) { + setPromptingOn(); + } + selectedOptionKey = null; + } + + if (filtering + && lastFilter.toLowerCase().equals( + uidl.getStringVariable("filter"))) { + suggestionPopup.showSuggestions(currentSuggestions, currentPage, + totalMatches); + filtering = false; + if (!popupOpenerClicked && lastIndex != -1) { + // we're paging w/ arrows + if (lastIndex == 0) { + // going up, select last item + int lastItem = PAGELENTH - 1; + List items = suggestionPopup.menu.getItems(); + /* + * The first page can contain less than 10 items if the null + * selection item is filtered away + */ + if (lastItem >= items.size()) { + lastItem = items.size() - 1; + } + suggestionPopup.menu.selectItem((MenuItem) items + .get(lastItem)); + } else { + // going down, select first item + suggestionPopup.menu + .selectItem((MenuItem) suggestionPopup.menu + .getItems().get(0)); + } + lastIndex = -1; // reset + } + } + + // Calculate minumum textarea width + suggestionPopupMinWidth = minWidth(captions); + + popupOpenerClicked = false; + + updateRootWidth(); + } + + private void setPromptingOn() { + prompting = true; + addStyleDependentName(CLASSNAME_PROMPT); + tb.setText(inputPrompt); + } + + private void setPromptingOff(String text) { + tb.setText(text); + prompting = false; + removeStyleDependentName(CLASSNAME_PROMPT); + } + + public void onSuggestionSelected(FilterSelectSuggestion suggestion) { + currentSuggestion = suggestion; + String newKey; + if (suggestion.key.equals("")) { + // "nullselection" + newKey = ""; + } else { + // normal selection + newKey = String.valueOf(suggestion.getOptionKey()); + } + + String text = suggestion.getReplacementString(); + if ("".equals(newKey) && !focused) { + setPromptingOn(); + } else { + setPromptingOff(text); + } + setSelectedItemIcon(suggestion.getIconUri()); + if (!newKey.equals(selectedOptionKey)) { + selectedOptionKey = newKey; + client.updateVariable(paintableId, "selected", + new String[] { selectedOptionKey }, immediate); + // currentPage = -1; // forget the page + } + suggestionPopup.hide(); + } + + private void setSelectedItemIcon(String iconUri) { + if (iconUri == null) { + selectedItemIcon.setVisible(false); + updateRootWidth(); + } else { + selectedItemIcon.setUrl(iconUri); + selectedItemIcon.setVisible(true); + updateRootWidth(); + updateSelectedIconPosition(); + } + } + + private void updateSelectedIconPosition() { + // Position icon vertically to middle + int availableHeight = getOffsetHeight(); + int iconHeight = Util.getRequiredHeight(selectedItemIcon); + int marginTop = (availableHeight - iconHeight) / 2; + DOM.setStyleAttribute(selectedItemIcon.getElement(), "marginTop", + marginTop + "px"); + } + + public void onKeyDown(Widget sender, char keyCode, int modifiers) { + if (enabled && suggestionPopup.isAttached()) { + switch (keyCode) { + case KeyboardListener.KEY_DOWN: + suggestionPopup.selectNextItem(); + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + break; + case KeyboardListener.KEY_UP: + suggestionPopup.selectPrevItem(); + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + break; + case KeyboardListener.KEY_PAGEDOWN: + if (hasNextPage()) { + filterOptions(currentPage + 1, lastFilter); + } + break; + case KeyboardListener.KEY_PAGEUP: + if (currentPage > 0) { + filterOptions(currentPage - 1, lastFilter); + } + break; + case KeyboardListener.KEY_ENTER: + case KeyboardListener.KEY_TAB: + suggestionPopup.menu.doSelectedItemAction(); + break; + } + } + } + + public void onKeyPress(Widget sender, char keyCode, int modifiers) { + + } + + public void onKeyUp(Widget sender, char keyCode, int modifiers) { + if (enabled) { + switch (keyCode) { + case KeyboardListener.KEY_ENTER: + case KeyboardListener.KEY_TAB: + case KeyboardListener.KEY_SHIFT: + case KeyboardListener.KEY_CTRL: + case KeyboardListener.KEY_ALT: + ; // NOP + break; + case KeyboardListener.KEY_DOWN: + case KeyboardListener.KEY_UP: + case KeyboardListener.KEY_PAGEDOWN: + case KeyboardListener.KEY_PAGEUP: + if (suggestionPopup.isAttached()) { + break; + } else { + // open popup as from gadget + filterOptions(-1, ""); + lastFilter = ""; + tb.selectAll(); + break; + } + case KeyboardListener.KEY_ESCAPE: + if (currentSuggestion != null) { + String text = currentSuggestion.getReplacementString(); + setPromptingOff(text); + selectedOptionKey = currentSuggestion.key; + } else { + setPromptingOn(); + selectedOptionKey = null; + } + lastFilter = ""; + suggestionPopup.hide(); + break; + default: + filterOptions(currentPage); + break; + } + } + } + + /** + * Listener for popupopener + */ + public void onClick(Widget sender) { + if (enabled) { + // ask suggestionPopup if it was just closed, we are using GWT + // Popup's auto close feature + if (!suggestionPopup.isJustClosed()) { + filterOptions(-1, ""); + popupOpenerClicked = true; + lastFilter = ""; + } else if (selectedOptionKey == null) { + tb.setText(inputPrompt); + prompting = true; + } + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + tb.setFocus(true); + tb.selectAll(); + + } + } + + /* + * Calculate minumum width for FilterSelect textarea + */ + private native int minWidth(String captions) + /*-{ + if(!captions || captions.length <= 0) + return 0; + captions = captions.split("|"); + var d = $wnd.document.createElement("div"); + var html = ""; + for(var i=0; i < captions.length; i++) { + html += "<div>" + captions[i] + "</div>"; + // TODO apply same CSS classname as in suggestionmenu + } + d.style.position = "absolute"; + d.style.top = "0"; + d.style.left = "0"; + d.style.visibility = "hidden"; + d.innerHTML = html; + $wnd.document.body.appendChild(d); + var w = d.offsetWidth; + $wnd.document.body.removeChild(d); + return w; + }-*/; + + public void onFocus(Widget sender) { + focused = true; + if (prompting) { + setPromptingOff(""); + } + addStyleDependentName("focus"); + } + + public void onLostFocus(Widget sender) { + focused = false; + if (!suggestionPopup.isAttached() || suggestionPopup.isJustClosed()) { + // typing so fast the popup was never opened, or it's just closed + suggestionPopup.menu.doSelectedItemAction(); + } + if (selectedOptionKey == null) { + setPromptingOn(); + } + removeStyleDependentName("focus"); + } + + public void focus() { + focused = true; + if (prompting) { + setPromptingOff(""); + } + tb.setFocus(true); + } + + @Override + public void setWidth(String width) { + if (width == null || width.equals("")) { + this.width = null; + } else { + this.width = width; + } + Util.setWidthExcludingPaddingAndBorder(this, width, 4); + updateRootWidth(); + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + Util.setHeightExcludingPaddingAndBorder(tb, height, 3); + } + + private void updateRootWidth() { + if (width == null) { + /* + * When the width is not specified we must specify width for root + * div so the popupopener won't wrap to the next line and also so + * the size of the combobox won't change over time. + */ + int tbWidth = Util.getRequiredWidth(tb); + int openerWidth = Util.getRequiredWidth(popupOpener); + int iconWidth = Util.getRequiredWidth(selectedItemIcon); + + int w = tbWidth + openerWidth + iconWidth; + if (suggestionPopupMinWidth > w) { + setTextboxWidth(suggestionPopupMinWidth); + w = suggestionPopupMinWidth; + } else { + /* + * Firefox3 has its own way of doing rendering so we need to + * specify the width for the TextField to make sure it actually + * is rendered as wide as FF3 says it is + */ + tb.setWidth((tbWidth - getTextboxPadding()) + "px"); + } + super.setWidth((w) + "px"); + // Freeze the initial width, so that it won't change even if the + // icon size changes + width = w + "px"; + + } else { + /* + * When the width is specified we also want to explicitly specify + * widths for textbox and popupopener + */ + setTextboxWidth(getMainWidth() - getComponentPadding()); + + } + } + + private int getMainWidth() { + int componentWidth; + if (BrowserInfo.get().isIE6()) { + // Required in IE when textfield is wider than this.width + DOM.setStyleAttribute(getElement(), "overflow", "hidden"); + componentWidth = getOffsetWidth(); + DOM.setStyleAttribute(getElement(), "overflow", ""); + } else { + componentWidth = getOffsetWidth(); + } + return componentWidth; + } + + private void setTextboxWidth(int componentWidth) { + int padding = getTextboxPadding(); + int popupOpenerWidth = Util.getRequiredWidth(popupOpener); + int iconWidth = Util.getRequiredWidth(selectedItemIcon); + int textboxWidth = componentWidth - padding - popupOpenerWidth + - iconWidth; + if (textboxWidth < 0) { + textboxWidth = 0; + } + tb.setWidth(textboxWidth + "px"); + } + + private int getTextboxPadding() { + if (textboxPadding < 0) { + textboxPadding = Util.measureHorizontalPaddingAndBorder(tb + .getElement(), 4); + } + return textboxPadding; + } + + private int getComponentPadding() { + if (componentPadding < 0) { + componentPadding = Util.measureHorizontalPaddingAndBorder( + getElement(), 3); + } + return componentPadding; + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IForm.java b/src/com/vaadin/terminal/gwt/client/ui/IForm.java new file mode 100644 index 0000000000..9e6c0f5ae2 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IForm.java @@ -0,0 +1,288 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Set;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ComplexPanel;
+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.IErrorMessage;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderInformation;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class IForm extends ComplexPanel implements Container {
+
+ private String height = "";
+
+ private String width = "";
+
+ public static final String CLASSNAME = "i-form";
+
+ private Container lo;
+ private Element legend = DOM.createLegend();
+ private Element caption = DOM.createSpan();
+ private Element errorIndicatorElement = DOM.createDiv();
+ private Element desc = DOM.createDiv();
+ private Icon icon;
+ private IErrorMessage errorMessage = new IErrorMessage();
+
+ private Element fieldContainer = DOM.createDiv();
+
+ private Element footerContainer = DOM.createDiv();
+
+ private Element fieldSet = DOM.createFieldSet();
+
+ private Container footer;
+
+ private ApplicationConnection client;
+
+ private RenderInformation renderInformation = new RenderInformation();
+
+ private int borderPaddingHorizontal;
+
+ private int borderPaddingVertical;
+
+ private boolean rendering = false;
+
+ public IForm() {
+ setElement(DOM.createDiv());
+ DOM.appendChild(getElement(), fieldSet);
+ setStyleName(CLASSNAME);
+ DOM.appendChild(fieldSet, legend);
+ DOM.appendChild(legend, caption);
+ DOM.setElementProperty(errorIndicatorElement, "className",
+ "i-errorindicator");
+ DOM.setStyleAttribute(errorIndicatorElement, "display", "none");
+ DOM.setInnerText(errorIndicatorElement, " "); // needed for IE
+ DOM.setElementProperty(desc, "className", "i-form-description");
+ DOM.appendChild(fieldSet, desc);
+ DOM.appendChild(fieldSet, fieldContainer);
+ errorMessage.setVisible(false);
+ errorMessage.setStyleName(CLASSNAME + "-errormessage");
+ DOM.appendChild(fieldSet, errorMessage.getElement());
+ DOM.appendChild(fieldSet, footerContainer);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ rendering = true;
+ boolean measure = false;
+ if (this.client == null) {
+ this.client = client;
+ measure = true;
+ }
+
+ if (client.updateComponent(this, uidl, false)) {
+ rendering = false;
+ return;
+ }
+
+ if (measure) {
+ // Measure the border when the style names have been set
+ borderPaddingVertical = getOffsetHeight();
+ int ow = getOffsetWidth();
+ int dow = desc.getOffsetWidth();
+ borderPaddingHorizontal = ow - dow;
+ }
+
+ boolean legendEmpty = true;
+ if (uidl.hasAttribute("caption")) {
+ DOM.setInnerText(caption, uidl.getStringAttribute("caption"));
+ legendEmpty = false;
+ } else {
+ DOM.setInnerText(caption, "");
+ }
+ if (uidl.hasAttribute("icon")) {
+ if (icon == null) {
+ icon = new Icon(client);
+ DOM.insertChild(legend, icon.getElement(), 0);
+ }
+ icon.setUri(uidl.getStringAttribute("icon"));
+ legendEmpty = false;
+ } else {
+ if (icon != null) {
+ DOM.removeChild(legend, icon.getElement());
+ }
+ }
+ if (legendEmpty) {
+ DOM.setStyleAttribute(legend, "display", "none");
+ } else {
+ DOM.setStyleAttribute(legend, "display", "");
+ }
+
+ if (uidl.hasAttribute("error")) {
+ final UIDL errorUidl = uidl.getErrors();
+ errorMessage.updateFromUIDL(errorUidl);
+ errorMessage.setVisible(true);
+
+ } else {
+ errorMessage.setVisible(false);
+ }
+
+ if (uidl.hasAttribute("description")) {
+ DOM.setInnerHTML(desc, uidl.getStringAttribute("description"));
+ } else {
+ DOM.setInnerHTML(desc, "");
+ }
+
+ updateSize();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+
+ final UIDL layoutUidl = uidl.getChildUIDL(0);
+ Container newLo = (Container) client.getPaintable(layoutUidl);
+ if (lo == null) {
+ lo = newLo;
+ add((Widget) lo, fieldContainer);
+ } else if (lo != newLo) {
+ client.unregisterPaintable(lo);
+ remove((Widget) lo);
+ lo = newLo;
+ add((Widget) lo, fieldContainer);
+ }
+ lo.updateFromUIDL(layoutUidl, client);
+
+ if (uidl.getChildCount() > 1) {
+ // render footer
+ Container newFooter = (Container) client.getPaintable(uidl
+ .getChildUIDL(1));
+ if (footer == null) {
+ add((Widget) newFooter, footerContainer);
+ footer = newFooter;
+ } else if (newFooter != footer) {
+ remove((Widget) footer);
+ client.unregisterPaintable(footer);
+ add((Widget) newFooter, footerContainer);
+ }
+ footer = newFooter;
+ footer.updateFromUIDL(uidl.getChildUIDL(1), client);
+ } else {
+ if (footer != null) {
+ remove((Widget) footer);
+ client.unregisterPaintable(footer);
+ }
+ }
+
+ rendering = false;
+ }
+
+ public void updateSize() {
+
+ renderInformation.updateSize(getElement());
+
+ renderInformation.setContentAreaHeight(renderInformation
+ .getRenderedSize().getHeight()
+ - borderPaddingVertical);
+ if (BrowserInfo.get().isIE6()) {
+ getElement().getStyle().setProperty("overflow", "hidden");
+ }
+ renderInformation.setContentAreaWidth(renderInformation
+ .getRenderedSize().getWidth()
+ - borderPaddingHorizontal);
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ if (child == lo) {
+ int hPixels = 0;
+ if (!"".equals(height)) {
+ hPixels = getOffsetHeight();
+ hPixels -= borderPaddingVertical;
+ hPixels -= footerContainer.getOffsetHeight();
+ hPixels -= errorMessage.getOffsetHeight();
+ hPixels -= desc.getOffsetHeight();
+
+ }
+
+ return new RenderSpace(renderInformation.getContentAreaSize()
+ .getWidth(), hPixels);
+ } else if (child == footer) {
+ return new RenderSpace(footerContainer.getOffsetWidth(), 0);
+ } else {
+ ApplicationConnection.getConsole().error(
+ "Invalid child requested RenderSpace information");
+ return null;
+ }
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ return component != null && (component == lo || component == footer);
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ if (!hasChildComponent(oldComponent)) {
+ throw new IllegalArgumentException(
+ "Old component is not inside this Container");
+ }
+ remove(oldComponent);
+ if (oldComponent == lo) {
+ lo = (Container) newComponent;
+ add((Widget) lo, fieldContainer);
+ } else {
+ footer = (Container) newComponent;
+ add((Widget) footer, footerContainer);
+ }
+
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+
+ if (height != null && width != null) {
+ /*
+ * If the height and width has been specified the child components
+ * cannot make the size of the layout change
+ */
+
+ return true;
+ }
+
+ if (renderInformation.updateSize(getElement())) {
+ return false;
+ } else {
+ return true;
+ }
+
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ // NOP form don't render caption for neither field layout nor footer
+ // layout
+ }
+
+ @Override
+ public void setHeight(String height) {
+ if (this.height.equals(height)) {
+ return;
+ }
+
+ this.height = height;
+ super.setHeight(height);
+
+ updateSize();
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (Util.equals(this.width, width)) {
+ return;
+ }
+
+ this.width = width;
+ super.setWidth(width);
+
+ updateSize();
+
+ if (!rendering && height.equals("")) {
+ // Width might affect height
+ Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, this);
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IFormLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IFormLayout.java new file mode 100644 index 0000000000..6d4e62b7a8 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IFormLayout.java @@ -0,0 +1,464 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.FlexTable; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Container; +import com.vaadin.terminal.gwt.client.ITooltip; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.StyleConstants; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +/** + * Two col Layout that places caption on left col and field on right col + */ +public class IFormLayout extends SimplePanel implements Container { + + private final static String CLASSNAME = "i-formlayout"; + + private ApplicationConnection client; + private IFormLayoutTable table; + + private String width = ""; + private String height = ""; + + private boolean rendering = false; + + public IFormLayout() { + super(); + setStylePrimaryName(CLASSNAME); + table = new IFormLayoutTable(); + setWidget(table); + } + + public class IFormLayoutTable extends FlexTable { + + private static final int COLUMN_CAPTION = 0; + private static final int COLUMN_ERRORFLAG = 1; + private static final int COLUMN_WIDGET = 2; + + private HashMap<Paintable, Caption> componentToCaption = new HashMap<Paintable, Caption>(); + private HashMap<Paintable, ErrorFlag> componentToError = new HashMap<Paintable, ErrorFlag>(); + + public IFormLayoutTable() { + DOM.setElementProperty(getElement(), "cellPadding", "0"); + DOM.setElementProperty(getElement(), "cellSpacing", "0"); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + final IMarginInfo margins = new IMarginInfo(uidl + .getIntAttribute("margins")); + + Element margin = getElement(); + setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_TOP, + margins.hasTop()); + setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_RIGHT, + margins.hasRight()); + setStyleName(margin, + CLASSNAME + "-" + StyleConstants.MARGIN_BOTTOM, margins + .hasBottom()); + setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_LEFT, + margins.hasLeft()); + + setStyleName(margin, CLASSNAME + "-" + "spacing", uidl + .hasAttribute("spacing")); + + int i = 0; + for (final Iterator it = uidl.getChildIterator(); it.hasNext(); i++) { + prepareCell(i, 1); + final UIDL childUidl = (UIDL) it.next(); + final Paintable p = client.getPaintable(childUidl); + Caption caption = componentToCaption.get(p); + if (caption == null) { + caption = new Caption(p, client); + componentToCaption.put(p, caption); + } + ErrorFlag error = componentToError.get(p); + if (error == null) { + error = new ErrorFlag(); + componentToError.put(p, error); + } + prepareCell(i, COLUMN_WIDGET); + final Paintable oldComponent = (Paintable) getWidget(i, + COLUMN_WIDGET); + if (oldComponent == null) { + setWidget(i, COLUMN_WIDGET, (Widget) p); + } else if (oldComponent != p) { + client.unregisterPaintable(oldComponent); + setWidget(i, COLUMN_WIDGET, (Widget) p); + } + getCellFormatter().setStyleName(i, COLUMN_WIDGET, + CLASSNAME + "-contentcell"); + getCellFormatter().setStyleName(i, COLUMN_CAPTION, + CLASSNAME + "-captioncell"); + setWidget(i, COLUMN_CAPTION, caption); + + setContentWidth(i); + + getCellFormatter().setStyleName(i, COLUMN_ERRORFLAG, + CLASSNAME + "-errorcell"); + setWidget(i, COLUMN_ERRORFLAG, error); + + p.updateFromUIDL(childUidl, client); + + String rowstyles = CLASSNAME + "-row"; + if (i == 0) { + rowstyles += " " + CLASSNAME + "-firstrow"; + } + if (!it.hasNext()) { + rowstyles += " " + CLASSNAME + "-lastrow"; + } + + getRowFormatter().setStyleName(i, rowstyles); + + } + + while (getRowCount() > i) { + final Paintable p = (Paintable) getWidget(i, COLUMN_WIDGET); + client.unregisterPaintable(p); + componentToCaption.remove(p); + removeRow(i); + } + + /* + * Must update relative sized fields last when it is clear how much + * space they are allowed to use + */ + for (Paintable p : componentToCaption.keySet()) { + client.handleComponentRelativeSize((Widget) p); + } + } + + public void setContentWidths() { + for (int row = 0; row < getRowCount(); row++) { + setContentWidth(row); + } + } + + private void setContentWidth(int row) { + String width = ""; + if (!isDynamicWidth()) { + width = "100%"; + } + getCellFormatter().setWidth(row, COLUMN_WIDGET, width); + } + + public void replaceChildComponent(Widget oldComponent, + Widget newComponent) { + int i; + for (i = 0; i < getRowCount(); i++) { + Widget candidate = getWidget(i, COLUMN_WIDGET); + if (oldComponent == candidate) { + final Caption newCap = new Caption( + (Paintable) newComponent, client); + componentToCaption.put((Paintable) newComponent, newCap); + ErrorFlag error = componentToError.get(newComponent); + if (error == null) { + error = new ErrorFlag(); + componentToError.put((Paintable) newComponent, error); + } + + setWidget(i, COLUMN_CAPTION, newCap); + setWidget(i, COLUMN_ERRORFLAG, error); + setWidget(i, COLUMN_WIDGET, newComponent); + break; + } + } + + } + + public boolean hasChildComponent(Widget component) { + return componentToCaption.containsKey(component); + } + + public void updateCaption(Paintable component, UIDL uidl) { + final Caption c = componentToCaption.get(component); + if (c != null) { + c.updateCaption(uidl); + } + final ErrorFlag e = componentToError.get(component); + if (e != null) { + e.updateFromUIDL(uidl, component); + } + + } + + public int getAllocatedWidth(Widget child, int availableWidth) { + Caption caption = componentToCaption.get(child); + ErrorFlag error = componentToError.get(child); + int width = availableWidth; + + if (caption != null) { + width -= DOM.getParent(caption.getElement()).getOffsetWidth(); + } + if (error != null) { + width -= DOM.getParent(error.getElement()).getOffsetWidth(); + } + + return width; + } + + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + rendering = true; + + this.client = client; + + if (client.updateComponent(this, uidl, true)) { + rendering = false; + return; + } + + table.updateFromUIDL(uidl, client); + + rendering = false; + } + + public boolean isDynamicWidth() { + return width.equals(""); + } + + public boolean hasChildComponent(Widget component) { + return table.hasChildComponent(component); + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + table.replaceChildComponent(oldComponent, newComponent); + } + + public void updateCaption(Paintable component, UIDL uidl) { + table.updateCaption(component, uidl); + } + + public class Caption extends HTML { + + public static final String CLASSNAME = "i-caption"; + + private final Paintable owner; + + private Element requiredFieldIndicator; + + private Icon icon; + + private Element captionText; + + private final ApplicationConnection client; + + /** + * + * @param component + * optional owner of caption. If not set, getOwner will + * return null + * @param client + */ + public Caption(Paintable component, ApplicationConnection client) { + super(); + this.client = client; + owner = component; + setStyleName(CLASSNAME); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + } + + public void updateCaption(UIDL uidl) { + setVisible(!uidl.getBooleanAttribute("invisible")); + + setStyleName(getElement(), "i-disabled", uidl + .hasAttribute("disabled")); + + boolean isEmpty = true; + + if (uidl.hasAttribute("icon")) { + if (icon == null) { + icon = new Icon(client); + + DOM.insertChild(getElement(), icon.getElement(), 0); + } + icon.setUri(uidl.getStringAttribute("icon")); + isEmpty = false; + } else { + if (icon != null) { + DOM.removeChild(getElement(), icon.getElement()); + icon = null; + } + + } + + if (uidl.hasAttribute("caption")) { + if (captionText == null) { + captionText = DOM.createSpan(); + DOM.insertChild(getElement(), captionText, icon == null ? 0 + : 1); + } + String c = uidl.getStringAttribute("caption"); + if (c == null) { + c = ""; + } else { + isEmpty = false; + } + DOM.setInnerText(captionText, c); + } else { + // TODO should span also be removed + } + + if (uidl.hasAttribute("description")) { + if (captionText != null) { + addStyleDependentName("hasdescription"); + } else { + removeStyleDependentName("hasdescription"); + } + } + + if (uidl.getBooleanAttribute("required")) { + if (requiredFieldIndicator == null) { + requiredFieldIndicator = DOM.createSpan(); + DOM.setInnerText(requiredFieldIndicator, "*"); + DOM.setElementProperty(requiredFieldIndicator, "className", + "i-required-field-indicator"); + DOM.appendChild(getElement(), requiredFieldIndicator); + } + } else { + if (requiredFieldIndicator != null) { + DOM.removeChild(getElement(), requiredFieldIndicator); + requiredFieldIndicator = null; + } + } + + // Workaround for IE weirdness, sometimes returns bad height in some + // circumstances when Caption is empty. See #1444 + // IE7 bugs more often. I wonder what happens when IE8 arrives... + if (Util.isIE()) { + if (isEmpty) { + setHeight("0px"); + DOM.setStyleAttribute(getElement(), "overflow", "hidden"); + } else { + setHeight(""); + DOM.setStyleAttribute(getElement(), "overflow", ""); + } + + } + + } + + /** + * Returns Paintable for which this Caption belongs to. + * + * @return owner Widget + */ + public Paintable getOwner() { + return owner; + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (client != null) { + client.handleTooltipEvent(event, owner); + } + } + } + + private class ErrorFlag extends HTML { + private static final String CLASSNAME = IFormLayout.CLASSNAME + + "-error-indicator"; + Element errorIndicatorElement; + private Paintable owner; + + public ErrorFlag() { + setStyleName(CLASSNAME); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + } + + public void updateFromUIDL(UIDL uidl, Paintable component) { + owner = component; + if (uidl.hasAttribute("error") + && !uidl.getBooleanAttribute("hideErrors")) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + DOM.setInnerHTML(errorIndicatorElement, " "); + DOM.setElementProperty(errorIndicatorElement, "className", + "i-errorindicator"); + DOM.appendChild(getElement(), errorIndicatorElement); + } + + } else if (errorIndicatorElement != null) { + DOM.removeChild(getElement(), errorIndicatorElement); + errorIndicatorElement = null; + } + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (owner != null) { + client.handleTooltipEvent(event, owner); + } + } + + } + + public boolean requestLayout(Set<Paintable> child) { + if (height.equals("") || width.equals("")) { + // A dynamic size might change due to children changes + return false; + } + + return true; + } + + public RenderSpace getAllocatedSpace(Widget child) { + int width = 0; + int height = 0; + + if (!this.width.equals("")) { + int availableWidth = getOffsetWidth(); + width = table.getAllocatedWidth(child, availableWidth); + } + + return new RenderSpace(width, height, false); + } + + @Override + public void setHeight(String height) { + if (this.height.equals(height)) { + return; + } + + this.height = height; + super.setHeight(height); + } + + @Override + public void setWidth(String width) { + if (this.width.equals(width)) { + return; + } + + this.width = width; + super.setWidth(width); + + if (!rendering) { + table.setContentWidths(); + if (height.equals("")) { + // Width might affect height + Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, this); + } + } + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IGridLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IGridLayout.java new file mode 100644 index 0000000000..07bb7584b4 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IGridLayout.java @@ -0,0 +1,1018 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Container; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.StyleConstants; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.layout.CellBasedLayout; +import com.vaadin.terminal.gwt.client.ui.layout.ChildComponentContainer; + +public class IGridLayout extends SimplePanel implements Paintable, Container { + + public static final String CLASSNAME = "i-gridlayout"; + + private DivElement margin = Document.get().createDivElement(); + + private final AbsolutePanel canvas = new AbsolutePanel(); + + private ApplicationConnection client; + + protected HashMap<Widget, ChildComponentContainer> widgetToComponentContainer = new HashMap<Widget, ChildComponentContainer>(); + + private HashMap<Paintable, Cell> paintableToCell = new HashMap<Paintable, Cell>(); + + private int spacingPixelsHorizontal; + private int spacingPixelsVertical; + + private int[] columnWidths; + private int[] rowHeights; + + private String height; + + private String width; + + private int[] colExpandRatioArray; + + private int[] rowExpandRatioArray; + + private int[] minColumnWidths; + + private int[] minRowHeights; + + private boolean rendering; + + private HashMap<Widget, ChildComponentContainer> nonRenderedWidgets; + + private boolean sizeChangedDuringRendering = false; + + public IGridLayout() { + super(); + getElement().appendChild(margin); + setStyleName(CLASSNAME); + setWidget(canvas); + } + + @Override + protected Element getContainerElement() { + return margin.cast(); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + rendering = true; + this.client = client; + + if (client.updateComponent(this, uidl, true)) { + rendering = false; + return; + } + + boolean mightToggleVScrollBar = "".equals(height) && !"".equals(width); + boolean mightToggleHScrollBar = "".equals(width) && !"".equals(height); + int wBeforeRender = 0; + int hBeforeRender = 0; + if (mightToggleHScrollBar || mightToggleVScrollBar) { + wBeforeRender = canvas.getOffsetWidth(); + hBeforeRender = getOffsetHeight(); + } + canvas.setWidth("0px"); + + handleMargins(uidl); + detectSpacing(uidl); + + int cols = uidl.getIntAttribute("w"); + int rows = uidl.getIntAttribute("h"); + + columnWidths = new int[cols]; + rowHeights = new int[rows]; + + if (cells == null) { + cells = new Cell[cols][rows]; + } else if (cells.length != cols || cells[0].length != rows) { + Cell[][] newCells = new Cell[cols][rows]; + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells[i].length; j++) { + if (i < cols && j < rows) { + newCells[i][j] = cells[i][j]; + } + } + } + cells = newCells; + } + + nonRenderedWidgets = (HashMap<Widget, ChildComponentContainer>) widgetToComponentContainer + .clone(); + + final int[] alignments = uidl.getIntArrayAttribute("alignments"); + int alignmentIndex = 0; + + LinkedList<Cell> pendingCells = new LinkedList<Cell>(); + + LinkedList<Cell> relativeHeighted = new LinkedList<Cell>(); + + for (final Iterator i = uidl.getChildIterator(); i.hasNext();) { + final UIDL r = (UIDL) i.next(); + if ("gr".equals(r.getTag())) { + for (final Iterator j = r.getChildIterator(); j.hasNext();) { + final UIDL c = (UIDL) j.next(); + if ("gc".equals(c.getTag())) { + Cell cell = getCell(c); + if (cell.hasContent()) { + boolean rendered = cell.renderIfNoRelativeWidth(); + cell.alignment = alignments[alignmentIndex++]; + if (!rendered) { + pendingCells.add(cell); + } + + if (cell.colspan > 1) { + storeColSpannedCell(cell); + } else if (rendered) { + // strore non-colspanned widths to columnWidth + // array + if (columnWidths[cell.col] < cell.getWidth()) { + columnWidths[cell.col] = cell.getWidth(); + } + } + if (cell.hasRelativeHeight()) { + relativeHeighted.add(cell); + } + } + } + } + } + } + + distributeColSpanWidths(); + colExpandRatioArray = uidl.getIntArrayAttribute("colExpand"); + rowExpandRatioArray = uidl.getIntArrayAttribute("rowExpand"); + + minColumnWidths = cloneArray(columnWidths); + expandColumns(); + + renderRemainingComponentsWithNoRelativeHeight(pendingCells); + + detectRowHeights(); + + expandRows(); + + renderRemainingComponents(pendingCells); + + for (Cell cell : relativeHeighted) { + Widget widget2 = cell.cc.getWidget(); + client.handleComponentRelativeSize(widget2); + cell.cc.updateWidgetSize(); + } + + layoutCells(); + + // clean non rendered components + for (Widget w : nonRenderedWidgets.keySet()) { + ChildComponentContainer childComponentContainer = widgetToComponentContainer + .get(w); + paintableToCell.remove(w); + widgetToComponentContainer.remove(w); + childComponentContainer.removeFromParent(); + client.unregisterPaintable((Paintable) w); + } + nonRenderedWidgets = null; + + rendering = false; + sizeChangedDuringRendering = false; + + boolean needsRelativeSizeCheck = false; + + if (mightToggleHScrollBar && wBeforeRender != canvas.getOffsetWidth()) { + needsRelativeSizeCheck = true; + } + if (mightToggleVScrollBar && hBeforeRender != getOffsetHeight()) { + needsRelativeSizeCheck = true; + } + if (needsRelativeSizeCheck) { + client.handleComponentRelativeSize(this); + } + } + + private static int[] cloneArray(int[] toBeCloned) { + int[] clone = new int[toBeCloned.length]; + for (int i = 0; i < clone.length; i++) { + clone[i] = toBeCloned[i] * 1; + } + return clone; + } + + private void expandRows() { + if (!"".equals(height)) { + int usedSpace = minRowHeights[0]; + for (int i = 1; i < minRowHeights.length; i++) { + usedSpace += spacingPixelsVertical + minRowHeights[i]; + } + int availableSpace = getOffsetHeight() - marginTopAndBottom; + int excessSpace = availableSpace - usedSpace; + int distributed = 0; + if (excessSpace > 0) { + for (int i = 0; i < rowHeights.length; i++) { + int ew = excessSpace * rowExpandRatioArray[i] / 1000; + rowHeights[i] = minRowHeights[i] + ew; + distributed += ew; + } + excessSpace -= distributed; + int c = 0; + while (excessSpace > 0) { + rowHeights[c % rowHeights.length]++; + excessSpace--; + c++; + } + } + } + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + if (!height.equals(this.height)) { + this.height = height; + if (rendering) { + sizeChangedDuringRendering = true; + } else { + expandRows(); + layoutCells(); + for (Paintable c : paintableToCell.keySet()) { + client.handleComponentRelativeSize((Widget) c); + } + } + } + } + + @Override + public void setWidth(String width) { + super.setWidth(width); + if (!width.equals(this.width)) { + this.width = width; + if (rendering) { + sizeChangedDuringRendering = true; + } else { + int[] oldWidths = cloneArray(columnWidths); + expandColumns(); + boolean heightChanged = false; + HashSet<Integer> dirtyRows = null; + for (int i = 0; i < oldWidths.length; i++) { + if (columnWidths[i] != oldWidths[i]) { + Cell[] column = cells[i]; + for (int j = 0; j < column.length; j++) { + Cell c = column[j]; + if (c != null && c.cc != null + && c.widthCanAffectHeight()) { + c.cc.setContainerSize(c.getAvailableWidth(), c + .getAvailableHeight()); + client.handleComponentRelativeSize(c.cc + .getWidget()); + c.cc.updateWidgetSize(); + int newHeight = c.getHeight(); + if (columnWidths[i] < oldWidths[i] + && newHeight > minRowHeights[j]) { + minRowHeights[j] = newHeight; + if (newHeight > rowHeights[j]) { + rowHeights[j] = newHeight; + heightChanged = true; + } + } else if (newHeight < minRowHeights[j]) { + // need to recalculate new minimum height + // for this row + if (dirtyRows == null) { + dirtyRows = new HashSet<Integer>(); + } + dirtyRows.add(j); + } + } + } + } + } + if (dirtyRows != null) { + /* flag indicating that there is a potential row shrinking */ + boolean rowMayShrink = false; + for (Integer rowIndex : dirtyRows) { + int oldMinimum = minRowHeights[rowIndex]; + int newMinimum = 0; + for (int colIndex = 0; colIndex < columnWidths.length; colIndex++) { + Cell cell = cells[colIndex][rowIndex]; + if (cell != null && !cell.hasRelativeHeight() + && cell.getHeight() > newMinimum) { + newMinimum = cell.getHeight(); + } + } + if (newMinimum < oldMinimum) { + minRowHeights[rowIndex] = rowHeights[rowIndex] = newMinimum; + rowMayShrink = true; + } + } + if (rowMayShrink) { + distributeRowSpanHeights(); + minRowHeights = cloneArray(rowHeights); + heightChanged = true; + } + + } + layoutCells(); + for (Paintable c : paintableToCell.keySet()) { + client.handleComponentRelativeSize((Widget) c); + } + if (heightChanged && "".equals(height)) { + Util.notifyParentOfSizeChange(this, false); + } + } + } + } + + private void expandColumns() { + if (!"".equals(width)) { + int usedSpace = minColumnWidths[0]; + for (int i = 1; i < minColumnWidths.length; i++) { + usedSpace += spacingPixelsHorizontal + minColumnWidths[i]; + } + canvas.setWidth(""); + int availableSpace = canvas.getOffsetWidth(); + int excessSpace = availableSpace - usedSpace; + int distributed = 0; + if (excessSpace > 0) { + for (int i = 0; i < columnWidths.length; i++) { + int ew = excessSpace * colExpandRatioArray[i] / 1000; + columnWidths[i] = minColumnWidths[i] + ew; + distributed += ew; + } + excessSpace -= distributed; + int c = 0; + while (excessSpace > 0) { + columnWidths[c % columnWidths.length]++; + excessSpace--; + c++; + } + } + } + } + + private void layoutCells() { + int x = 0; + int y = 0; + for (int i = 0; i < cells.length; i++) { + y = 0; + for (int j = 0; j < cells[i].length; j++) { + Cell cell = cells[i][j]; + if (cell != null) { + cell.layout(x, y); + } + y += rowHeights[j] + spacingPixelsVertical; + } + x += columnWidths[i] + spacingPixelsHorizontal; + } + + if ("".equals(width)) { + canvas.setWidth((x - spacingPixelsHorizontal) + "px"); + } else { + // main element defines width + canvas.setWidth(""); + } + int canvasHeight; + if ("".equals(height)) { + canvasHeight = y - spacingPixelsVertical; + } else { + canvasHeight = getOffsetHeight() - marginTopAndBottom; + } + canvas.setHeight(canvasHeight + "px"); + } + + private void renderRemainingComponents(LinkedList<Cell> pendingCells) { + for (Cell cell : pendingCells) { + cell.render(); + } + } + + private void detectRowHeights() { + + // collect min rowheight from non-rowspanned cells + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells[i].length; j++) { + Cell cell = cells[i][j]; + if (cell != null) { + /* + * Setting fixing container width may in some situations + * affect height. Example: Label with wrapping text without + * or with relative width. + */ + if (cell.cc != null && cell.widthCanAffectHeight()) { + cell.cc.setWidth(cell.getAvailableWidth() + "px"); + cell.cc.updateWidgetSize(); + } + if (cell.rowspan == 1) { + if (!cell.hasRelativeHeight() + && rowHeights[j] < cell.getHeight()) { + rowHeights[j] = cell.getHeight(); + } + } else { + storeRowSpannedCell(cell); + } + } + } + } + + distributeRowSpanHeights(); + + minRowHeights = cloneArray(rowHeights); + } + + private void storeRowSpannedCell(Cell cell) { + SpanList l = null; + for (SpanList list : rowSpans) { + if (list.span < cell.rowspan) { + continue; + } else { + // insert before this + l = list; + break; + } + } + if (l == null) { + l = new SpanList(cell.rowspan); + rowSpans.add(l); + } else if (l.span != cell.rowspan) { + SpanList newL = new SpanList(cell.rowspan); + rowSpans.add(rowSpans.indexOf(l), newL); + l = newL; + } + l.cells.add(cell); + } + + private void renderRemainingComponentsWithNoRelativeHeight( + LinkedList<Cell> pendingCells) { + + for (Iterator iterator = pendingCells.iterator(); iterator.hasNext();) { + Cell cell = (Cell) iterator.next(); + if (!cell.hasRelativeHeight()) { + cell.render(); + iterator.remove(); + } + } + + } + + /** + * Iterates colspanned cells, ensures cols have enough space to accommodate + * them + */ + private void distributeColSpanWidths() { + for (SpanList list : colSpans) { + for (Cell cell : list.cells) { + int width = cell.getWidth(); + int allocated = columnWidths[cell.col]; + for (int i = 1; i < cell.colspan; i++) { + allocated += spacingPixelsHorizontal + + columnWidths[cell.col + i]; + } + if (allocated < width) { + // columnWidths needs to be expanded due colspanned cell + int neededExtraSpace = width - allocated; + int spaceForColunms = neededExtraSpace / cell.colspan; + for (int i = 0; i < cell.colspan; i++) { + int col = cell.col + i; + columnWidths[col] += spaceForColunms; + neededExtraSpace -= spaceForColunms; + } + if (neededExtraSpace > 0) { + for (int i = 0; i < cell.colspan; i++) { + int col = cell.col + i; + columnWidths[col] += 1; + neededExtraSpace -= 1; + if (neededExtraSpace == 0) { + break; + } + } + } + } + } + } + } + + /** + * Iterates rowspanned cells, ensures rows have enough space to accommodate + * them + */ + private void distributeRowSpanHeights() { + for (SpanList list : rowSpans) { + for (Cell cell : list.cells) { + int height = cell.getHeight(); + int allocated = rowHeights[cell.row]; + for (int i = 1; i < cell.rowspan; i++) { + allocated += spacingPixelsVertical + + rowHeights[cell.row + i]; + } + if (allocated < height) { + // columnWidths needs to be expanded due colspanned cell + int neededExtraSpace = height - allocated; + int spaceForColunms = neededExtraSpace / cell.rowspan; + for (int i = 0; i < cell.rowspan; i++) { + int row = cell.row + i; + rowHeights[row] += spaceForColunms; + neededExtraSpace -= spaceForColunms; + } + if (neededExtraSpace > 0) { + for (int i = 0; i < cell.rowspan; i++) { + int row = cell.row + i; + rowHeights[row] += 1; + neededExtraSpace -= 1; + if (neededExtraSpace == 0) { + break; + } + } + } + } + } + } + } + + private LinkedList<SpanList> colSpans = new LinkedList<SpanList>(); + private LinkedList<SpanList> rowSpans = new LinkedList<SpanList>(); + + private int marginTopAndBottom; + + private class SpanList { + final int span; + List<Cell> cells = new LinkedList<Cell>(); + + public SpanList(int span) { + this.span = span; + } + } + + private void storeColSpannedCell(Cell cell) { + SpanList l = null; + for (SpanList list : colSpans) { + if (list.span < cell.colspan) { + continue; + } else { + // insert before this + l = list; + break; + } + } + if (l == null) { + l = new SpanList(cell.colspan); + colSpans.add(l); + } else if (l.span != cell.colspan) { + + SpanList newL = new SpanList(cell.colspan); + colSpans.add(colSpans.indexOf(l), newL); + l = newL; + } + l.cells.add(cell); + } + + private void detectSpacing(UIDL uidl) { + DivElement spacingmeter = Document.get().createDivElement(); + spacingmeter.setClassName(CLASSNAME + "-" + "spacing-" + + (uidl.getBooleanAttribute("spacing") ? "on" : "off")); + spacingmeter.getStyle().setProperty("width", "0"); + spacingmeter.getStyle().setProperty("height", "0"); + canvas.getElement().appendChild(spacingmeter); + spacingPixelsHorizontal = spacingmeter.getOffsetWidth(); + spacingPixelsVertical = spacingmeter.getOffsetHeight(); + canvas.getElement().removeChild(spacingmeter); + } + + private void handleMargins(UIDL uidl) { + final IMarginInfo margins = new IMarginInfo(uidl + .getIntAttribute("margins")); + + String styles = CLASSNAME + "-margin"; + if (margins.hasTop()) { + styles += " " + CLASSNAME + "-" + StyleConstants.MARGIN_TOP; + } + if (margins.hasRight()) { + styles += " " + CLASSNAME + "-" + StyleConstants.MARGIN_RIGHT; + } + if (margins.hasBottom()) { + styles += " " + CLASSNAME + "-" + StyleConstants.MARGIN_BOTTOM; + } + if (margins.hasLeft()) { + styles += " " + CLASSNAME + "-" + StyleConstants.MARGIN_LEFT; + } + margin.setClassName(styles); + + marginTopAndBottom = margin.getOffsetHeight() + - canvas.getOffsetHeight(); + } + + public boolean hasChildComponent(Widget component) { + return paintableToCell.containsKey(component); + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + ChildComponentContainer componentContainer = widgetToComponentContainer + .remove(oldComponent); + if (componentContainer == null) { + return; + } + + componentContainer.setWidget(newComponent); + widgetToComponentContainer.put(newComponent, componentContainer); + + paintableToCell.put((Paintable) newComponent, paintableToCell + .get(oldComponent)); + } + + public void updateCaption(Paintable component, UIDL uidl) { + ChildComponentContainer cc = widgetToComponentContainer.get(component); + if (cc != null) { + cc.updateCaption(uidl, client); + } + if (!rendering) { + // ensure rel size details are updated + paintableToCell.get(component).updateRelSizeStatus(uidl); + } + } + + public boolean requestLayout(final Set<Paintable> changedChildren) { + boolean needsLayout = false; + boolean reDistributeColSpanWidths = false; + boolean reDistributeRowSpanHeights = false; + int offsetHeight = canvas.getOffsetHeight(); + int offsetWidth = canvas.getOffsetWidth(); + if ("".equals(width) || "".equals(height)) { + needsLayout = true; + } + ArrayList<Integer> dirtyColumns = new ArrayList<Integer>(); + ArrayList<Integer> dirtyRows = new ArrayList<Integer>(); + for (Paintable paintable : changedChildren) { + + Cell cell = paintableToCell.get(paintable); + if (!cell.hasRelativeHeight() || !cell.hasRelativeWidth()) { + // cell sizes will only stay still if only relatively + // sized + // components + // check if changed child affects min col widths + cell.cc.setWidth(""); + cell.cc.setHeight(""); + + cell.cc.updateWidgetSize(); + int width = cell.getWidth(); + int allocated = columnWidths[cell.col]; + for (int i = 1; i < cell.colspan; i++) { + allocated += spacingPixelsHorizontal + + columnWidths[cell.col + i]; + } + if (allocated < width) { + needsLayout = true; + if (cell.colspan == 1) { + // do simple column width expansion + columnWidths[cell.col] = minColumnWidths[cell.col] = width; + } else { + // mark that col span expansion is needed + reDistributeColSpanWidths = true; + } + } else if (allocated != width) { + // size is smaller thant allocated, column might + // shrink + dirtyColumns.add(cell.col); + } + + int height = cell.getHeight(); + + allocated = rowHeights[cell.row]; + for (int i = 1; i < cell.rowspan; i++) { + allocated += spacingPixelsVertical + + rowHeights[cell.row + i]; + } + if (allocated < height) { + needsLayout = true; + if (cell.rowspan == 1) { + // do simple row expansion + rowHeights[cell.row] = minRowHeights[cell.row] = height; + } else { + // mark that row span expansion is needed + reDistributeRowSpanHeights = true; + } + } else if (allocated != height) { + // size is smaller than allocated, row might shrink + dirtyRows.add(cell.row); + } + } + } + + if (dirtyColumns.size() > 0) { + for (Integer colIndex : dirtyColumns) { + int colW = 0; + for (int i = 0; i < rowHeights.length; i++) { + Cell cell = cells[colIndex][i]; + if (cell != null && cell.getChildUIDL() != null + && !cell.hasRelativeWidth() && cell.colspan == 1) { + int width = cell.getWidth(); + if (width > colW) { + colW = width; + } + } + } + minColumnWidths[colIndex] = colW; + } + needsLayout = true; + // ensure colspanned columns have enough space + columnWidths = cloneArray(minColumnWidths); + distributeColSpanWidths(); + reDistributeColSpanWidths = false; + } + + if (reDistributeColSpanWidths) { + distributeColSpanWidths(); + } + + if (dirtyRows.size() > 0) { + needsLayout = true; + for (Integer rowIndex : dirtyRows) { + // recalculate min row height + int rowH = minRowHeights[rowIndex] = 0; + // loop all columns on row rowIndex + for (int i = 0; i < columnWidths.length; i++) { + Cell cell = cells[i][rowIndex]; + if (cell != null && cell.getChildUIDL() != null + && !cell.hasRelativeHeight() && cell.rowspan == 1) { + int h = cell.getHeight(); + if (h > rowH) { + rowH = h; + } + } + } + minRowHeights[rowIndex] = rowH; + } + // TODO could check only some row spans + rowHeights = cloneArray(minRowHeights); + distributeRowSpanHeights(); + reDistributeRowSpanHeights = false; + } + + if (reDistributeRowSpanHeights) { + distributeRowSpanHeights(); + } + + if (needsLayout) { + expandColumns(); + expandRows(); + layoutCells(); + // loop all relative sized components and update their size + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < cells[i].length; j++) { + Cell cell = cells[i][j]; + if (cell != null + && cell.cc != null + && (cell.hasRelativeHeight() || cell + .hasRelativeWidth())) { + client.handleComponentRelativeSize(cell.cc.getWidget()); + } + } + } + } + if (canvas.getOffsetHeight() != offsetHeight + || canvas.getOffsetWidth() != offsetWidth) { + return false; + } else { + return true; + } + } + + public RenderSpace getAllocatedSpace(Widget child) { + Cell cell = paintableToCell.get(child); + assert cell != null; + return cell.getAllocatedSpace(); + } + + private Cell[][] cells; + + /** + * Private helper class. + */ + private class Cell { + private boolean relHeight = false; + private boolean relWidth = false; + private boolean widthCanAffectHeight = false; + + public Cell(UIDL c) { + row = c.getIntAttribute("y"); + col = c.getIntAttribute("x"); + setUidl(c); + } + + public boolean widthCanAffectHeight() { + return widthCanAffectHeight; + } + + public boolean hasRelativeHeight() { + return relHeight; + } + + public RenderSpace getAllocatedSpace() { + return new RenderSpace(getAvailableWidth() + - cc.getCaptionWidthAfterComponent(), getAvailableHeight() + - cc.getCaptionHeightAboveComponent()); + } + + public boolean hasContent() { + return childUidl != null; + } + + /** + * @return total of spanned cols + */ + private int getAvailableWidth() { + int width = columnWidths[col]; + for (int i = 1; i < colspan; i++) { + width += spacingPixelsHorizontal + columnWidths[col + i]; + } + return width; + } + + /** + * @return total of spanned rows + */ + private int getAvailableHeight() { + int height = rowHeights[row]; + for (int i = 1; i < rowspan; i++) { + height += spacingPixelsVertical + rowHeights[row + i]; + } + return height; + } + + public void layout(int x, int y) { + if (cc != null && cc.isAttached()) { + canvas.setWidgetPosition(cc, x, y); + cc.setContainerSize(getAvailableWidth(), getAvailableHeight()); + cc.setAlignment(new AlignmentInfo(alignment)); + cc.updateAlignments(getAvailableWidth(), getAvailableHeight()); + } + } + + public int getWidth() { + if (cc != null) { + int w = cc.getWidgetSize().getWidth() + + cc.getCaptionWidthAfterComponent(); + return w; + } else { + return 0; + } + } + + public int getHeight() { + if (cc != null) { + return cc.getWidgetSize().getHeight() + + cc.getCaptionHeightAboveComponent(); + } else { + return 0; + } + } + + public boolean renderIfNoRelativeWidth() { + if (childUidl == null) { + return false; + } + if (!hasRelativeWidth()) { + render(); + return true; + } else { + return false; + } + } + + protected boolean hasRelativeWidth() { + return relWidth; + } + + protected void render() { + assert childUidl != null; + + Paintable paintable = client.getPaintable(childUidl); + assert paintable != null; + if (cc == null || cc.getWidget() != paintable) { + if (widgetToComponentContainer.containsKey(paintable)) { + cc = widgetToComponentContainer.get(paintable); + cc.setWidth(""); + cc.setHeight(""); + } else { + cc = new ChildComponentContainer((Widget) paintable, + CellBasedLayout.ORIENTATION_VERTICAL); + widgetToComponentContainer.put((Widget) paintable, cc); + paintableToCell.put(paintable, this); + cc.setWidth(""); + canvas.add(cc, 0, 0); + } + } + cc.renderChild(childUidl, client, -1); + if (sizeChangedDuringRendering && Util.isCached(childUidl)) { + client.handleComponentRelativeSize(cc.getWidget()); + } + cc.updateWidgetSize(); + nonRenderedWidgets.remove(paintable); + } + + public UIDL getChildUIDL() { + return childUidl; + } + + final int row; + final int col; + int colspan = 1; + int rowspan = 1; + UIDL childUidl; + int alignment; + ChildComponentContainer cc; + + public void setUidl(UIDL c) { + // Set cell width + colspan = c.hasAttribute("w") ? c.getIntAttribute("w") : 1; + // Set cell height + rowspan = c.hasAttribute("h") ? c.getIntAttribute("h") : 1; + // ensure we will lose reference to old cells, now overlapped by + // this cell + for (int i = 0; i < colspan; i++) { + for (int j = 0; j < rowspan; j++) { + if (i > 0 || j > 0) { + cells[col + i][row + j] = null; + } + } + } + + c = c.getChildUIDL(0); // we are interested about childUidl + if (childUidl != null) { + if (c == null) { + // content has vanished, old content will be removed from + // canvas + // later durin render phase + cc = null; + } else if (cc != null + && cc.getWidget() != client.getPaintable(c)) { + // content has changed + cc = null; + if (widgetToComponentContainer.containsKey(client + .getPaintable(c))) { + // cc exist for this component (moved) use that for this + // cell + cc = widgetToComponentContainer.get(client + .getPaintable(c)); + cc.setWidth(""); + cc.setHeight(""); + } + } + } + childUidl = c; + updateRelSizeStatus(c); + } + + protected void updateRelSizeStatus(UIDL uidl) { + if (uidl != null && !uidl.getBooleanAttribute("cached")) { + if (uidl.hasAttribute("height") + && uidl.getStringAttribute("height").contains("%")) { + relHeight = true; + } else { + relHeight = false; + } + if (uidl.hasAttribute("width")) { + widthCanAffectHeight = relWidth = uidl.getStringAttribute( + "width").contains("%"); + if (uidl.hasAttribute("height")) { + widthCanAffectHeight = false; + } + } else { + widthCanAffectHeight = !uidl.hasAttribute("height"); + relWidth = false; + } + } + } + } + + private Cell getCell(UIDL c) { + int row = c.getIntAttribute("y"); + int col = c.getIntAttribute("x"); + Cell cell = cells[col][row]; + if (cell == null) { + cell = new Cell(c); + cells[col][row] = cell; + } else { + cell.setUidl(c); + } + return cell; + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IHorizontalLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IHorizontalLayout.java new file mode 100644 index 0000000000..eeb2c1d1df --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IHorizontalLayout.java @@ -0,0 +1,11 @@ +package com.vaadin.terminal.gwt.client.ui;
+
+public class IHorizontalLayout extends IOrderedLayout {
+
+ public static final String CLASSNAME = "i-horizontallayout";
+
+ public IHorizontalLayout() {
+ super(CLASSNAME, ORIENTATION_HORIZONTAL);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ILabel.java b/src/com/vaadin/terminal/gwt/client/ui/ILabel.java new file mode 100644 index 0000000000..c1ef09d8bf --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ILabel.java @@ -0,0 +1,123 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.PreElement; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ITooltip; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class ILabel extends HTML implements Paintable { + + public static final String CLASSNAME = "i-label"; + private static final String CLASSNAME_UNDEFINED_WIDTH = "i-label-undef-w"; + + private ApplicationConnection client; + private int verticalPaddingBorder = 0; + private int horizontalPaddingBorder = 0; + + public ILabel() { + super(); + setStyleName(CLASSNAME); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + } + + public ILabel(String text) { + super(text); + setStyleName(CLASSNAME); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + event.cancelBubble(true); + return; + } + if (client != null) { + client.handleTooltipEvent(event, this); + } + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + if (client.updateComponent(this, uidl, true)) { + return; + } + + this.client = client; + + boolean sinkOnloads = false; + + final String mode = uidl.getStringAttribute("mode"); + if (mode == null || "text".equals(mode)) { + setText(uidl.getChildString(0)); + } else if ("pre".equals(mode)) { + PreElement preElement = Document.get().createPreElement(); + preElement.setInnerText(uidl.getChildUIDL(0).getChildString(0)); + // clear existing content + setHTML(""); + // add preformatted text to dom + getElement().appendChild(preElement); + } else if ("uidl".equals(mode)) { + setHTML(uidl.getChildrenAsXML()); + } else if ("xhtml".equals(mode)) { + UIDL content = uidl.getChildUIDL(0).getChildUIDL(0); + if (content.getChildCount() > 0) { + setHTML(content.getChildString(0)); + } else { + setHTML(""); + } + sinkOnloads = true; + } else if ("xml".equals(mode)) { + setHTML(uidl.getChildUIDL(0).getChildString(0)); + } else if ("raw".equals(mode)) { + setHTML(uidl.getChildUIDL(0).getChildString(0)); + sinkOnloads = true; + } else { + setText(""); + } + if (sinkOnloads) { + sinkOnloadsForContainedImgs(); + } + } + + private void sinkOnloadsForContainedImgs() { + NodeList<Element> images = getElement().getElementsByTagName("img"); + for (int i = 0; i < images.getLength(); i++) { + Element img = images.getItem(i); + DOM.sinkEvents((com.google.gwt.user.client.Element) img, + Event.ONLOAD); + } + + } + + @Override + public void setHeight(String height) { + verticalPaddingBorder = Util.setHeightExcludingPaddingAndBorder(this, + height, verticalPaddingBorder); + } + + @Override + public void setWidth(String width) { + horizontalPaddingBorder = Util.setWidthExcludingPaddingAndBorder(this, + width, horizontalPaddingBorder); + if (width == null || width.equals("")) { + setStyleName(getElement(), CLASSNAME_UNDEFINED_WIDTH, true); + } else { + setStyleName(getElement(), CLASSNAME_UNDEFINED_WIDTH, false); + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ILink.java b/src/com/vaadin/terminal/gwt/client/ui/ILink.java new file mode 100644 index 0000000000..2d09c2186e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ILink.java @@ -0,0 +1,182 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.ClickListener; +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.ITooltip; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class ILink extends HTML implements Paintable, ClickListener { + + public static final String CLASSNAME = "i-link"; + + private static final int BORDER_STYLE_DEFAULT = 0; + private static final int BORDER_STYLE_MINIMAL = 1; + private static final int BORDER_STYLE_NONE = 2; + + private String src; + + private String target; + + private int borderStyle = BORDER_STYLE_DEFAULT; + + private boolean enabled; + + private boolean readonly; + + private int targetWidth; + + private int targetHeight; + + private Element errorIndicatorElement; + + private final Element anchor = DOM.createAnchor(); + + private final Element captionElement = DOM.createSpan(); + + private Icon icon; + + private ApplicationConnection client; + + public ILink() { + super(); + getElement().appendChild(anchor); + anchor.appendChild(captionElement); + addClickListener(this); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + setStyleName(CLASSNAME); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + // Ensure correct implementation, + // but don't let container manage caption etc. + if (client.updateComponent(this, uidl, false)) { + return; + } + + this.client = client; + + enabled = uidl.hasAttribute("disabled") ? false : true; + readonly = uidl.hasAttribute("readonly") ? true : false; + + if (uidl.hasAttribute("name")) { + target = uidl.getStringAttribute("name"); + anchor.setAttribute("target", target); + } + if (uidl.hasAttribute("src")) { + src = client.translateToolkitUri(uidl.getStringAttribute("src")); + anchor.setAttribute("href", src); + } + + if (uidl.hasAttribute("border")) { + if ("none".equals(uidl.getStringAttribute("border"))) { + borderStyle = BORDER_STYLE_NONE; + } else { + borderStyle = BORDER_STYLE_MINIMAL; + } + } else { + borderStyle = BORDER_STYLE_DEFAULT; + } + + targetHeight = uidl.hasAttribute("targetHeight") ? uidl + .getIntAttribute("targetHeight") : -1; + targetWidth = uidl.hasAttribute("targetWidth") ? uidl + .getIntAttribute("targetWidth") : -1; + + // Set link caption + captionElement.setInnerText(uidl.getStringAttribute("caption")); + + // handle error + if (uidl.hasAttribute("error")) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + DOM.setElementProperty(errorIndicatorElement, "className", + "i-errorindicator"); + } + DOM.insertChild(getElement(), errorIndicatorElement, 0); + } else if (errorIndicatorElement != null) { + DOM.setStyleAttribute(errorIndicatorElement, "display", "none"); + } + + if (uidl.hasAttribute("icon")) { + if (icon == null) { + icon = new Icon(client); + anchor.insertBefore(icon.getElement(), captionElement); + } + icon.setUri(uidl.getStringAttribute("icon")); + } + + } + + public void onClick(Widget sender) { + if (enabled && !readonly) { + if (target == null) { + target = "_self"; + } + String features; + switch (borderStyle) { + case BORDER_STYLE_NONE: + features = "menubar=no,location=no,status=no"; + break; + case BORDER_STYLE_MINIMAL: + features = "menubar=yes,location=no,status=no"; + break; + default: + features = ""; + break; + } + + if (targetWidth > 0) { + features += (features.length() > 0 ? "," : "") + "width=" + + targetWidth; + } + if (targetHeight > 0) { + features += (features.length() > 0 ? "," : "") + "height=" + + targetHeight; + } + + if (features.length() > 0) { + // if 'special features' are set, use window.open(), unless + // a modifier key is held (ctrl to open in new tab etc) + Event e = DOM.eventGetCurrentEvent(); + if (!e.getCtrlKey() && !e.getAltKey() && !e.getShiftKey() + && !e.getMetaKey()) { + Window.open(src, target, features); + e.preventDefault(); + } + } + } + } + + @Override + public void onBrowserEvent(Event event) { + final Element target = DOM.eventGetTarget(event); + if (event.getTypeInt() == Event.ONLOAD) { + Util.notifyParentOfSizeChange(this, true); + } + if (client != null) { + client.handleTooltipEvent(event, this); + } + if (target == captionElement || target == anchor + || (icon != null && target == icon.getElement())) { + super.onBrowserEvent(event); + } + if (!enabled) { + event.preventDefault(); + } + + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IListSelect.java b/src/com/vaadin/terminal/gwt/client/ui/IListSelect.java new file mode 100644 index 0000000000..301c7aa65e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IListSelect.java @@ -0,0 +1,141 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.Iterator; +import java.util.Vector; + +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ITooltip; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; + +public class IListSelect extends IOptionGroupBase { + + public static final String CLASSNAME = "i-select"; + + private static final int VISIBLE_COUNT = 10; + + protected TooltipListBox select; + + private int lastSelectedIndex = -1; + + public IListSelect() { + super(new TooltipListBox(true), CLASSNAME); + select = (TooltipListBox) optionsContainer; + select.setSelect(this); + select.addChangeListener(this); + select.addClickListener(this); + select.setStyleName(CLASSNAME + "-select"); + select.setVisibleItemCount(VISIBLE_COUNT); + } + + @Override + protected void buildOptions(UIDL uidl) { + select.setClient(client); + select.setMultipleSelect(isMultiselect()); + select.setEnabled(!isDisabled() && !isReadonly()); + select.clear(); + if (!isMultiselect() && isNullSelectionAllowed() + && !isNullSelectionItemAvailable()) { + // can't unselect last item in singleselect mode + select.addItem("", null); + } + for (final Iterator i = uidl.getChildIterator(); i.hasNext();) { + final UIDL optionUidl = (UIDL) i.next(); + select.addItem(optionUidl.getStringAttribute("caption"), optionUidl + .getStringAttribute("key")); + if (optionUidl.hasAttribute("selected")) { + select.setItemSelected(select.getItemCount() - 1, true); + } + } + if (getRows() > 0) { + select.setVisibleItemCount(getRows()); + } + } + + @Override + protected Object[] getSelectedItems() { + final Vector selectedItemKeys = new Vector(); + for (int i = 0; i < select.getItemCount(); i++) { + if (select.isItemSelected(i)) { + selectedItemKeys.add(select.getValue(i)); + } + } + return selectedItemKeys.toArray(); + } + + @Override + public void onChange(Widget sender) { + final int si = select.getSelectedIndex(); + if (si == -1 && !isNullSelectionAllowed()) { + select.setSelectedIndex(lastSelectedIndex); + } else { + lastSelectedIndex = si; + if (isMultiselect()) { + client.updateVariable(id, "selected", getSelectedItems(), + isImmediate()); + } else { + client.updateVariable(id, "selected", new String[] { "" + + getSelectedItem() }, isImmediate()); + } + } + } + + @Override + public void setHeight(String height) { + select.setHeight(height); + super.setHeight(height); + } + + @Override + public void setWidth(String width) { + select.setWidth(width); + super.setWidth(width); + } + + @Override + protected void setTabIndex(int tabIndex) { + ((TooltipListBox) optionsContainer).setTabIndex(tabIndex); + } + + public void focus() { + select.setFocus(true); + } + +} + +/** + * Extended ListBox to listen tooltip events and forward them to generic + * handler. + */ +class TooltipListBox extends ListBox { + private ApplicationConnection client; + private Paintable pntbl; + + TooltipListBox(boolean isMultiselect) { + super(isMultiselect); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + } + + public void setClient(ApplicationConnection client) { + this.client = client; + } + + public void setSelect(Paintable s) { + pntbl = s; + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (client != null) { + client.handleTooltipEvent(event, pntbl); + } + } +}
\ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/client/ui/IMarginInfo.java b/src/com/vaadin/terminal/gwt/client/ui/IMarginInfo.java new file mode 100644 index 0000000000..936639356c --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IMarginInfo.java @@ -0,0 +1,76 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.io.Serializable; + +@SuppressWarnings("serial") +public class IMarginInfo implements Serializable { + + private static final int TOP = 1; + private static final int RIGHT = 2; + private static final int BOTTOM = 4; + private static final int LEFT = 8; + + private int bitMask; + + public IMarginInfo(int bitMask) { + this.bitMask = bitMask; + } + + public IMarginInfo(boolean top, boolean right, boolean bottom, boolean left) { + setMargins(top, right, bottom, left); + } + + public void setMargins(boolean top, boolean right, boolean bottom, + boolean left) { + bitMask = top ? TOP : 0; + bitMask += right ? RIGHT : 0; + bitMask += bottom ? BOTTOM : 0; + bitMask += left ? LEFT : 0; + } + + public void setMargins(IMarginInfo marginInfo) { + bitMask = marginInfo.bitMask; + } + + public boolean hasLeft() { + return (bitMask & LEFT) == LEFT; + } + + public boolean hasRight() { + return (bitMask & RIGHT) == RIGHT; + } + + public boolean hasTop() { + return (bitMask & TOP) == TOP; + } + + public boolean hasBottom() { + return (bitMask & BOTTOM) == BOTTOM; + } + + public int getBitMask() { + return bitMask; + } + + public void setMargins(boolean enabled) { + if (enabled) { + bitMask = TOP + RIGHT + BOTTOM + LEFT; + } else { + bitMask = 0; + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IMarginInfo)) { + return false; + } + + return ((IMarginInfo) obj).bitMask == bitMask; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IMenuBar.java b/src/com/vaadin/terminal/gwt/client/ui/IMenuBar.java new file mode 100644 index 0000000000..bb9a3d53e6 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IMenuBar.java @@ -0,0 +1,644 @@ +package com.vaadin.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; + +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.HasHTML; +import com.google.gwt.user.client.ui.PopupListener; +import com.google.gwt.user.client.ui.PopupPanel; +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.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; + +public class IMenuBar extends Widget implements Paintable, PopupListener { + + /** Set the CSS class name to allow styling. */ + public static final String CLASSNAME = "i-menubar"; + + /** For server connections **/ + protected String uidlId; + protected ApplicationConnection client; + + protected final IMenuBar hostReference = this; + protected String submenuIcon = null; + protected boolean collapseItems = true; + protected CustomMenuItem moreItem = null; + + // Construct an empty command to be used when the item has no command + // associated + protected static final Command emptyCommand = null; + + /** Widget fields **/ + protected boolean subMenu; + protected ArrayList<CustomMenuItem> items; + protected Element containerElement; + protected IToolkitOverlay popup; + protected IMenuBar visibleChildMenu; + protected IMenuBar parentMenu; + protected CustomMenuItem selected; + + public IMenuBar() { + // Create an empty horizontal menubar + this(false); + } + + public IMenuBar(boolean subMenu) { + super(); + setElement(DOM.createDiv()); + + items = new ArrayList<CustomMenuItem>(); + popup = null; + visibleChildMenu = null; + + Element table = DOM.createTable(); + Element tbody = DOM.createTBody(); + DOM.appendChild(getElement(), table); + DOM.appendChild(table, tbody); + + if (!subMenu) { + setStyleName(CLASSNAME); + Element tr = DOM.createTR(); + DOM.appendChild(tbody, tr); + containerElement = tr; + } else { + setStyleName(CLASSNAME + "-submenu"); + containerElement = tbody; + } + this.subMenu = subMenu; + + sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT); + } + + /** + * This method must be implemented to update the client-side component from + * UIDL data received from server. + * + * This method is called when the page is loaded for the first time, and + * every time UI changes in the component are received from the server. + */ + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // This call should be made first. Ensure correct implementation, + // and let the containing layout manage caption, etc. + if (client.updateComponent(this, uidl, true)) { + return; + } + + // For future connections + this.client = client; + uidlId = uidl.getId(); + + // Empty the menu every time it receives new information + if (!getItems().isEmpty()) { + clearItems(); + } + + UIDL options = uidl.getChildUIDL(0); + + if (options.hasAttribute("submenuIcon")) { + submenuIcon = client.translateToolkitUri(uidl.getChildUIDL(0) + .getStringAttribute("submenuIcon")); + } else { + submenuIcon = null; + } + + collapseItems = options.getBooleanAttribute("collapseItems"); + + if (collapseItems) { + UIDL moreItemUIDL = options.getChildUIDL(0); + StringBuffer itemHTML = new StringBuffer(); + + if (moreItemUIDL.hasAttribute("icon")) { + itemHTML.append("<img src=\"" + + client.translateToolkitUri(moreItemUIDL + .getStringAttribute("icon")) + + "\" align=\"left\" />"); + } + itemHTML.append(moreItemUIDL.getStringAttribute("text")); + + moreItem = new CustomMenuItem(itemHTML.toString(), emptyCommand); + } + + UIDL uidlItems = uidl.getChildUIDL(1); + Iterator<UIDL> itr = uidlItems.getChildIterator(); + Stack<Iterator<UIDL>> iteratorStack = new Stack<Iterator<UIDL>>(); + Stack<IMenuBar> menuStack = new Stack<IMenuBar>(); + IMenuBar currentMenu = this; + + while (itr.hasNext()) { + UIDL item = (UIDL) itr.next(); + CustomMenuItem currentItem = null; + + String itemText = item.getStringAttribute("text"); + final int itemId = item.getIntAttribute("id"); + + boolean itemHasCommand = item.getBooleanAttribute("command"); + + // Construct html from the text and the optional icon + StringBuffer itemHTML = new StringBuffer(); + + if (item.hasAttribute("icon")) { + itemHTML.append("<img src=\"" + + client.translateToolkitUri(item + .getStringAttribute("icon")) + + "\" align=\"left\" />"); + } + + itemHTML.append(itemText); + + if (currentMenu != this && item.getChildCount() > 0 + && submenuIcon != null) { + itemHTML.append("<img src=\"" + submenuIcon + + "\" align=\"right\" />"); + } + + Command cmd = null; + + if (itemHasCommand) { + // Construct a command that fires onMenuClick(int) with the + // item's id-number + cmd = new Command() { + public void execute() { + hostReference.onMenuClick(itemId); + } + }; + } + + currentItem = currentMenu.addItem(itemHTML.toString(), cmd); + + if (item.getChildCount() > 0) { + menuStack.push(currentMenu); + iteratorStack.push(itr); + itr = item.getChildIterator(); + currentMenu = new IMenuBar(true); + currentItem.setSubMenu(currentMenu); + } + + while (!itr.hasNext() && !iteratorStack.empty()) { + itr = iteratorStack.pop(); + currentMenu = (IMenuBar) menuStack.pop(); + } + }// while + + // we might need to collapse the top-level menu + if (collapseItems) { + int topLevelWidth = 0; + + int ourWidth = getOffsetWidth(); + + int i = 0; + for (; i < getItems().size() && topLevelWidth < ourWidth; i++) { + CustomMenuItem item = (CustomMenuItem) getItems().get(i); + topLevelWidth += item.getOffsetWidth(); + } + + if (topLevelWidth > getOffsetWidth()) { + ArrayList<CustomMenuItem> toBeCollapsed = new ArrayList<CustomMenuItem>(); + IMenuBar collapsed = new IMenuBar(true); + for (int j = i - 2; j < getItems().size(); j++) { + toBeCollapsed.add(getItems().get(j)); + } + + for (int j = 0; j < toBeCollapsed.size(); j++) { + CustomMenuItem item = (CustomMenuItem) toBeCollapsed.get(j); + removeItem(item); + + // it's ugly, but we have to insert the submenu icon + if (item.getSubMenu() != null && submenuIcon != null) { + StringBuffer itemText = new StringBuffer(item.getHTML()); + itemText.append("<img src=\""); + itemText.append(submenuIcon); + itemText.append("\" align=\"right\" />"); + item.setHTML(itemText.toString()); + } + + collapsed.addItem(item); + } + + moreItem.setSubMenu(collapsed); + addItem(moreItem); + } + } + }// updateFromUIDL + + /** + * This is called by the items in the menu and it communicates the + * information to the server + * + * @param clickedItemId + * id of the item that was clicked + */ + public void onMenuClick(int clickedItemId) { + // Updating the state to the server can not be done before + // the server connection is known, i.e., before updateFromUIDL() + // has been called. + if (uidlId != null && client != null) { + // Communicate the user interaction parameters to server. This call + // will initiate an AJAX request to the server. + client.updateVariable(uidlId, "clickedId", clickedItemId, true); + } + } + + /** Widget methods **/ + + /** + * Returns a list of items in this menu + */ + public List<CustomMenuItem> getItems() { + return items; + } + + /** + * Remove all the items in this menu + */ + public void clearItems() { + Element e = getContainingElement(); + while (DOM.getChildCount(e) > 0) { + DOM.removeChild(e, DOM.getChild(e, 0)); + } + items.clear(); + } + + /** + * Returns the containing element of the menu + * + * @return + */ + public Element getContainingElement() { + return containerElement; + } + + /** + * Returns a new child element to add an item to + * + * @return + */ + public Element getNewChildElement() { + if (subMenu) { + Element tr = DOM.createTR(); + DOM.appendChild(getContainingElement(), tr); + return tr; + } else { + return getContainingElement(); + } + + } + + /** + * Add a new item to this menu + * + * @param html + * items text + * @param cmd + * items command + * @return the item created + */ + public CustomMenuItem addItem(String html, Command cmd) { + CustomMenuItem item = new CustomMenuItem(html, cmd); + addItem(item); + return item; + } + + /** + * Add a new item to this menu + * + * @param item + */ + public void addItem(CustomMenuItem item) { + DOM.appendChild(getNewChildElement(), item.getElement()); + item.setParentMenu(this); + item.setSelected(false); + items.add(item); + } + + /** + * Remove the given item from this menu + * + * @param item + */ + public void removeItem(CustomMenuItem item) { + if (items.contains(item)) { + int index = items.indexOf(item); + Element container = getContainingElement(); + + DOM.removeChild(container, DOM.getChild(container, index)); + items.remove(index); + } + } + + /* + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user + * .client.Event) + */ + @Override + public void onBrowserEvent(Event e) { + super.onBrowserEvent(e); + + Element targetElement = DOM.eventGetTarget(e); + CustomMenuItem targetItem = null; + for (int i = 0; i < items.size(); i++) { + CustomMenuItem item = (CustomMenuItem) items.get(i); + if (DOM.isOrHasChild(item.getElement(), targetElement)) { + targetItem = item; + } + } + + if (targetItem != null) { + switch (DOM.eventGetType(e)) { + + case Event.ONCLICK: + itemClick(targetItem); + break; + + case Event.ONMOUSEOVER: + itemOver(targetItem); + break; + + case Event.ONMOUSEOUT: + itemOut(targetItem); + break; + } + } + } + + /** + * When an item is clicked + * + * @param item + */ + public void itemClick(CustomMenuItem item) { + if (item.getCommand() != null) { + setSelected(null); + + if (visibleChildMenu != null) { + visibleChildMenu.hideChildren(); + } + + hideParents(); + DeferredCommand.addCommand(item.getCommand()); + + } else { + if (item.getSubMenu() != null + && item.getSubMenu() != visibleChildMenu) { + setSelected(item); + showChildMenu(item); + } + } + } + + /** + * When the user hovers the mouse over the item + * + * @param item + */ + public void itemOver(CustomMenuItem item) { + setSelected(item); + + boolean menuWasVisible = visibleChildMenu != null; + + if (menuWasVisible && visibleChildMenu != item.getSubMenu()) { + popup.hide(); + visibleChildMenu = null; + } + + if (item.getSubMenu() != null && (parentMenu != null || menuWasVisible) + && visibleChildMenu != item.getSubMenu()) { + showChildMenu(item); + } + } + + /** + * When the mouse is moved away from an item + * + * @param item + */ + public void itemOut(CustomMenuItem item) { + if (visibleChildMenu != item.getSubMenu() || visibleChildMenu == null) { + hideChildMenu(item); + setSelected(null); + } + } + + /** + * Shows the child menu of an item. The caller must ensure that the item has + * a submenu. + * + * @param item + */ + public void showChildMenu(CustomMenuItem item) { + popup = new IToolkitOverlay(true, false, true); + popup.setWidget(item.getSubMenu()); + popup.addPopupListener(this); + + if (subMenu) { + popup.setPopupPosition(item.getParentMenu().getAbsoluteLeft() + + item.getParentMenu().getOffsetWidth(), item + .getAbsoluteTop()); + } else { + popup.setPopupPosition(item.getAbsoluteLeft(), item.getParentMenu() + .getAbsoluteTop() + + item.getParentMenu().getOffsetHeight()); + } + + item.getSubMenu().onShow(); + visibleChildMenu = item.getSubMenu(); + item.getSubMenu().setParentMenu(this); + + popup.show(); + } + + /** + * Hides the submenu of an item + * + * @param item + */ + public void hideChildMenu(CustomMenuItem item) { + if (visibleChildMenu != null + && !(visibleChildMenu == item.getSubMenu())) { + popup.hide(); + + } + } + + /** + * When the menu is shown. + */ + public void onShow() { + if (!items.isEmpty()) { + ((CustomMenuItem) items.get(0)).setSelected(true); + } + } + + /** + * Recursively hide all child menus + */ + public void hideChildren() { + if (visibleChildMenu != null) { + visibleChildMenu.hideChildren(); + popup.hide(); + } + } + + /** + * Recursively hide all parent menus + */ + public void hideParents() { + + if (visibleChildMenu != null) { + popup.hide(); + setSelected(null); + } + + if (getParentMenu() != null) { + getParentMenu().hideParents(); + } + } + + /** + * Returns the parent menu of this menu, or null if this is the top-level + * menu + * + * @return + */ + public IMenuBar getParentMenu() { + return parentMenu; + } + + /** + * Set the parent menu of this menu + * + * @param parent + */ + public void setParentMenu(IMenuBar parent) { + parentMenu = parent; + } + + /** + * Returns the currently selected item of this menu, or null if nothing is + * selected + * + * @return + */ + public CustomMenuItem getSelected() { + return selected; + } + + /** + * Set the currently selected item of this menu + * + * @param item + */ + public void setSelected(CustomMenuItem item) { + // If we had something selected, unselect + if (item != selected && selected != null) { + selected.setSelected(false); + } + // If we have a valid selection, select it + if (item != null) { + item.setSelected(true); + } + + selected = item; + } + + /** + * Listener method, fired when this menu is closed + */ + public void onPopupClosed(PopupPanel sender, boolean autoClosed) { + hideChildren(); + if (autoClosed) { + hideParents(); + } + // setSelected(null); + visibleChildMenu = null; + popup = null; + + } + + /** + * + * A class to hold information on menu items + * + */ + private class CustomMenuItem extends UIObject implements HasHTML { + + protected String html = null; + protected Command command = null; + protected IMenuBar subMenu = null; + protected IMenuBar parentMenu = null; + + public CustomMenuItem(String html, Command cmd) { + setElement(DOM.createTD()); + + setHTML(html); + setCommand(cmd); + setSelected(false); + + addStyleName("menuitem"); + } + + public void setSelected(boolean selected) { + if (selected) { + addStyleDependentName("selected"); + } else { + removeStyleDependentName("selected"); + } + } + + /* + * setters and getters for the fields + */ + + public void setSubMenu(IMenuBar subMenu) { + this.subMenu = subMenu; + } + + public IMenuBar getSubMenu() { + return subMenu; + } + + public void setParentMenu(IMenuBar parentMenu) { + this.parentMenu = parentMenu; + } + + public IMenuBar getParentMenu() { + return parentMenu; + } + + public void setCommand(Command command) { + this.command = command; + } + + public Command getCommand() { + return command; + } + + public String getHTML() { + return html; + } + + public void setHTML(String html) { + this.html = html; + DOM.setInnerHTML(getElement(), html); + } + + public String getText() { + return html; + } + + public void setText(String text) { + setHTML(text); + + } + } + +}// class IMenuBar diff --git a/src/com/vaadin/terminal/gwt/client/ui/INativeSelect.java b/src/com/vaadin/terminal/gwt/client/ui/INativeSelect.java new file mode 100644 index 0000000000..3b063bb5bf --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/INativeSelect.java @@ -0,0 +1,111 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.Iterator; +import java.util.Vector; + +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class INativeSelect extends IOptionGroupBase implements Field { + + public static final String CLASSNAME = "i-select"; + + protected TooltipListBox select; + + public INativeSelect() { + super(new TooltipListBox(false), CLASSNAME); + select = (TooltipListBox) optionsContainer; + select.setSelect(this); + select.setVisibleItemCount(1); + select.addChangeListener(this); + select.setStyleName(CLASSNAME + "-select"); + + } + + @Override + protected void buildOptions(UIDL uidl) { + select.setClient(client); + select.setEnabled(!isDisabled() && !isReadonly()); + select.clear(); + if (isNullSelectionAllowed() && !isNullSelectionItemAvailable()) { + // can't unselect last item in singleselect mode + select.addItem("", null); + } + boolean selected = false; + for (final Iterator i = uidl.getChildIterator(); i.hasNext();) { + final UIDL optionUidl = (UIDL) i.next(); + select.addItem(optionUidl.getStringAttribute("caption"), optionUidl + .getStringAttribute("key")); + if (optionUidl.hasAttribute("selected")) { + select.setItemSelected(select.getItemCount() - 1, true); + selected = true; + } + } + if (!selected && !isNullSelectionAllowed()) { + // null-select not allowed, but value not selected yet; add null and + // remove when something is selected + select.insertItem("", null, 0); + select.setItemSelected(0, true); + } + if (BrowserInfo.get().isIE6()) { + // lazy size change - IE6 uses naive dropdown that does not have a + // proper size yet + Util.notifyParentOfSizeChange(this, true); + } + } + + @Override + protected Object[] getSelectedItems() { + final Vector selectedItemKeys = new Vector(); + for (int i = 0; i < select.getItemCount(); i++) { + if (select.isItemSelected(i)) { + selectedItemKeys.add(select.getValue(i)); + } + } + return selectedItemKeys.toArray(); + } + + @Override + public void onChange(Widget sender) { + + if (select.isMultipleSelect()) { + client.updateVariable(id, "selected", getSelectedItems(), + isImmediate()); + } else { + client.updateVariable(id, "selected", new String[] { "" + + getSelectedItem() }, isImmediate()); + } + if (!isNullSelectionAllowed() && "null".equals(select.getValue(0))) { + // remove temporary empty item + select.removeItem(0); + } + } + + @Override + public void setHeight(String height) { + select.setHeight(height); + super.setHeight(height); + } + + @Override + public void setWidth(String width) { + select.setWidth(width); + super.setWidth(width); + } + + @Override + protected void setTabIndex(int tabIndex) { + ((TooltipListBox) optionsContainer).setTabIndex(tabIndex); + } + + public void focus() { + select.setFocus(true); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/INotification.java b/src/com/vaadin/terminal/gwt/client/ui/INotification.java new file mode 100644 index 0000000000..5ebb6c710e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/INotification.java @@ -0,0 +1,323 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.EventObject;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+
+public class INotification extends IToolkitOverlay {
+
+ public static final int CENTERED = 1;
+ public static final int CENTERED_TOP = 2;
+ public static final int CENTERED_BOTTOM = 3;
+ public static final int TOP_LEFT = 4;
+ public static final int TOP_RIGHT = 5;
+ public static final int BOTTOM_LEFT = 6;
+ public static final int BOTTOM_RIGHT = 7;
+
+ public static final int DELAY_FOREVER = -1;
+ public static final int DELAY_NONE = 0;
+
+ private static final String STYLENAME = "i-Notification";
+ private static final int mouseMoveThreshold = 7;
+ private static final int Z_INDEX_BASE = 20000;
+ public static final String STYLE_SYSTEM = "system";
+ private static final int FADE_ANIMATION_INTERVAL = 50; // == 20 fps
+
+ private int startOpacity = 90;
+ private int fadeMsec = 400;
+ private int delayMsec = 1000;
+
+ private Timer fader;
+ private Timer delay;
+
+ private int x = -1;
+ private int y = -1;
+
+ private String temporaryStyle;
+
+ private ArrayList<EventListener> listeners;
+
+ public INotification() {
+ setStylePrimaryName(STYLENAME);
+ sinkEvents(Event.ONCLICK);
+ DOM.setStyleAttribute(getElement(), "zIndex", "" + Z_INDEX_BASE);
+ }
+
+ public INotification(int delayMsec) {
+ this();
+ this.delayMsec = delayMsec;
+ }
+
+ public INotification(int delayMsec, int fadeMsec, int startOpacity) {
+ this(delayMsec);
+ this.fadeMsec = fadeMsec;
+ this.startOpacity = startOpacity;
+ }
+
+ public void startDelay() {
+ DOM.removeEventPreview(this);
+ if (delayMsec > 0) {
+ if (delay == null) {
+ delay = new Timer() {
+ @Override
+ public void run() {
+ fade();
+ }
+ };
+ delay.schedule(delayMsec);
+ }
+ } else if (delayMsec == 0) {
+ fade();
+ }
+ }
+
+ @Override
+ public void show() {
+ show(CENTERED);
+ }
+
+ public void show(String style) {
+ show(CENTERED, style);
+ }
+
+ public void show(int position) {
+ show(position, null);
+ }
+
+ public void show(Widget widget, int position, String style) {
+ setWidget(widget);
+ show(position, style);
+ }
+
+ public void show(String html, int position, String style) {
+ setWidget(new HTML(html));
+ show(position, style);
+ }
+
+ public void show(int position, String style) {
+ setOpacity(getElement(), startOpacity);
+ if (style != null) {
+ temporaryStyle = style;
+ addStyleName(style);
+ }
+ super.show();
+ setPosition(position);
+ }
+
+ @Override
+ public void hide() {
+ DOM.removeEventPreview(this);
+ cancelDelay();
+ cancelFade();
+ if (temporaryStyle != null) {
+ removeStyleName(temporaryStyle);
+ temporaryStyle = null;
+ }
+ super.hide();
+ fireEvent(new HideEvent(this));
+ }
+
+ public void fade() {
+ DOM.removeEventPreview(this);
+ cancelDelay();
+ fader = new Timer() {
+ private final long start = new Date().getTime();
+
+ @Override
+ public void run() {
+ /*
+ * To make animation smooth, don't count that event happens on
+ * time. Reduce opacity according to the actual time spent
+ * instead of fixed decrement.
+ */
+ long now = new Date().getTime();
+ long timeEplaced = now - start;
+ float remainingFraction = 1 - timeEplaced / (float) fadeMsec;
+ int opacity = (int) (startOpacity * remainingFraction);
+ if (opacity <= 0) {
+ cancel();
+ hide();
+ if (BrowserInfo.get().isOpera()) {
+ // tray notification on opera needs to explicitly define
+ // size, reset it
+ DOM.setStyleAttribute(getElement(), "width", "");
+ DOM.setStyleAttribute(getElement(), "height", "");
+ }
+ } else {
+ setOpacity(getElement(), opacity);
+ }
+ }
+ };
+ fader.scheduleRepeating(FADE_ANIMATION_INTERVAL);
+ }
+
+ public void setPosition(int position) {
+ final Element el = getElement();
+ DOM.setStyleAttribute(el, "top", "");
+ DOM.setStyleAttribute(el, "left", "");
+ DOM.setStyleAttribute(el, "bottom", "");
+ DOM.setStyleAttribute(el, "right", "");
+ switch (position) {
+ case TOP_LEFT:
+ DOM.setStyleAttribute(el, "top", "0px");
+ DOM.setStyleAttribute(el, "left", "0px");
+ break;
+ case TOP_RIGHT:
+ DOM.setStyleAttribute(el, "top", "0px");
+ DOM.setStyleAttribute(el, "right", "0px");
+ break;
+ case BOTTOM_RIGHT:
+ DOM.setStyleAttribute(el, "position", "absolute");
+ if (BrowserInfo.get().isOpera()) {
+ // tray notification on opera needs explicitly defined size
+ DOM.setStyleAttribute(el, "width", getOffsetWidth() + "px");
+ DOM.setStyleAttribute(el, "height", getOffsetHeight() + "px");
+ }
+ DOM.setStyleAttribute(el, "bottom", "0px");
+ DOM.setStyleAttribute(el, "right", "0px");
+ break;
+ case BOTTOM_LEFT:
+ DOM.setStyleAttribute(el, "bottom", "0px");
+ DOM.setStyleAttribute(el, "left", "0px");
+ break;
+ case CENTERED_TOP:
+ center();
+ DOM.setStyleAttribute(el, "top", "0px");
+ break;
+ case CENTERED_BOTTOM:
+ center();
+ DOM.setStyleAttribute(el, "top", "");
+ DOM.setStyleAttribute(el, "bottom", "0px");
+ break;
+ default:
+ case CENTERED:
+ center();
+ break;
+ }
+ }
+
+ private void cancelFade() {
+ if (fader != null) {
+ fader.cancel();
+ fader = null;
+ }
+ }
+
+ private void cancelDelay() {
+ if (delay != null) {
+ delay.cancel();
+ delay = null;
+ }
+ }
+
+ private void setOpacity(Element el, int opacity) {
+ DOM.setStyleAttribute(el, "opacity", "" + (opacity / 100.0));
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(el, "filter", "Alpha(opacity=" + opacity
+ + ")");
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ DOM.removeEventPreview(this);
+ if (fader == null) {
+ fade();
+ }
+ }
+
+ @Override
+ public boolean onEventPreview(Event event) {
+ int type = DOM.eventGetType(event);
+ // "modal"
+ if (delayMsec == -1 || temporaryStyle == STYLE_SYSTEM) {
+ if (type == Event.ONCLICK) {
+ if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) {
+ fade();
+ return false;
+ }
+ }
+ if (temporaryStyle == STYLE_SYSTEM) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ // default
+ switch (type) {
+ case Event.ONMOUSEMOVE:
+
+ if (x < 0) {
+ x = DOM.eventGetClientX(event);
+ y = DOM.eventGetClientY(event);
+ } else if (Math.abs(DOM.eventGetClientX(event) - x) > mouseMoveThreshold
+ || Math.abs(DOM.eventGetClientY(event) - y) > mouseMoveThreshold) {
+ startDelay();
+ }
+ break;
+ case Event.ONMOUSEDOWN:
+ case Event.ONMOUSEWHEEL:
+ case Event.ONSCROLL:
+ startDelay();
+ break;
+ case Event.ONKEYDOWN:
+ if (event.getRepeat()) {
+ return true;
+ }
+ startDelay();
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ public void addEventListener(EventListener listener) {
+ if (listeners == null) {
+ listeners = new ArrayList<EventListener>();
+ }
+ listeners.add(listener);
+ }
+
+ public void removeEventListener(EventListener listener) {
+ if (listeners == null) {
+ return;
+ }
+ listeners.remove(listener);
+ }
+
+ private void fireEvent(HideEvent event) {
+ if (listeners != null) {
+ for (Iterator<EventListener> it = listeners.iterator(); it
+ .hasNext();) {
+ EventListener l = it.next();
+ l.notificationHidden(event);
+ }
+ }
+ }
+
+ public class HideEvent extends EventObject {
+ private static final long serialVersionUID = 4428671753988459560L;
+
+ public HideEvent(Object source) {
+ super(source);
+ }
+ }
+
+ public interface EventListener extends java.util.EventListener {
+ public void notificationHidden(HideEvent event);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IOptionGroup.java b/src/com/vaadin/terminal/gwt/client/ui/IOptionGroup.java new file mode 100644 index 0000000000..cec3d07be8 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IOptionGroup.java @@ -0,0 +1,102 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.HasFocus;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.RadioButton;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IOptionGroup extends IOptionGroupBase {
+
+ public static final String CLASSNAME = "i-select-optiongroup";
+
+ private final Panel panel;
+
+ private final Map optionsToKeys;
+
+ public IOptionGroup() {
+ super(CLASSNAME);
+ panel = (Panel) optionsContainer;
+ optionsToKeys = new HashMap();
+ }
+
+ /*
+ * Return true if no elements were changed, false otherwise.
+ */
+ @Override
+ protected void buildOptions(UIDL uidl) {
+ panel.clear();
+ for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {
+ final UIDL opUidl = (UIDL) it.next();
+ CheckBox op;
+ if (isMultiselect()) {
+ op = new ICheckBox();
+ op.setText(opUidl.getStringAttribute("caption"));
+ } else {
+ op = new RadioButton(id, opUidl.getStringAttribute("caption"));
+ op.setStyleName("i-radiobutton");
+ }
+ op.addStyleName(CLASSNAME_OPTION);
+ op.setChecked(opUidl.getBooleanAttribute("selected"));
+ op.setEnabled(!opUidl.getBooleanAttribute("disabled")
+ && !isReadonly() && !isDisabled());
+ op.addClickListener(this);
+ optionsToKeys.put(op, opUidl.getStringAttribute("key"));
+ panel.add(op);
+ }
+ }
+
+ @Override
+ protected Object[] getSelectedItems() {
+ return selectedKeys.toArray();
+ }
+
+ @Override
+ public void onClick(Widget sender) {
+ super.onClick(sender);
+ if (sender instanceof CheckBox) {
+ final boolean selected = ((CheckBox) sender).isChecked();
+ final String key = (String) optionsToKeys.get(sender);
+ if (!isMultiselect()) {
+ selectedKeys.clear();
+ }
+ if (selected) {
+ selectedKeys.add(key);
+ } else {
+ selectedKeys.remove(key);
+ }
+ client.updateVariable(id, "selected", getSelectedItems(),
+ isImmediate());
+ }
+ }
+
+ @Override
+ protected void setTabIndex(int tabIndex) {
+ for (Iterator iterator = panel.iterator(); iterator.hasNext();) {
+ if (isMultiselect()) {
+ ICheckBox cb = (ICheckBox) iterator.next();
+ cb.setTabIndex(tabIndex);
+ } else {
+ RadioButton rb = (RadioButton) iterator.next();
+ rb.setTabIndex(tabIndex);
+ }
+ }
+ }
+
+ public void focus() {
+ Iterator<Widget> iterator = panel.iterator();
+ if (iterator.hasNext()) {
+ ((HasFocus) iterator.next()).setFocus(true);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IOptionGroupBase.java b/src/com/vaadin/terminal/gwt/client/ui/IOptionGroupBase.java new file mode 100644 index 0000000000..d91ade2639 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IOptionGroupBase.java @@ -0,0 +1,229 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Set;
+
+import com.google.gwt.user.client.ui.ChangeListener;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.KeyboardListener;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+abstract class IOptionGroupBase extends Composite implements Paintable, Field,
+ ClickListener, ChangeListener, KeyboardListener, Focusable {
+
+ public static final String CLASSNAME_OPTION = "i-select-option";
+
+ protected ApplicationConnection client;
+
+ protected String id;
+
+ protected Set selectedKeys;
+
+ private boolean immediate;
+
+ private boolean multiselect;
+
+ private boolean disabled;
+
+ private boolean readonly;
+
+ private int cols = 0;
+
+ private int rows = 0;
+
+ private boolean nullSelectionAllowed = true;
+
+ private boolean nullSelectionItemAvailable = false;
+
+ /**
+ * Widget holding the different options (e.g. ListBox or Panel for radio
+ * buttons) (optional, fallbacks to container Panel)
+ */
+ protected Widget optionsContainer;
+
+ /**
+ * Panel containing the component
+ */
+ private final Panel container;
+
+ private ITextField newItemField;
+
+ private IButton newItemButton;
+
+ public IOptionGroupBase(String classname) {
+ container = new FlowPanel();
+ initWidget(container);
+ optionsContainer = container;
+ container.setStyleName(classname);
+ immediate = false;
+ multiselect = false;
+ }
+
+ /*
+ * Call this if you wish to specify your own container for the option
+ * elements (e.g. SELECT)
+ */
+ public IOptionGroupBase(Widget w, String classname) {
+ this(classname);
+ optionsContainer = w;
+ container.add(optionsContainer);
+ }
+
+ protected boolean isImmediate() {
+ return immediate;
+ }
+
+ protected boolean isMultiselect() {
+ return multiselect;
+ }
+
+ protected boolean isDisabled() {
+ return disabled;
+ }
+
+ protected boolean isReadonly() {
+ return readonly;
+ }
+
+ protected boolean isNullSelectionAllowed() {
+ return nullSelectionAllowed;
+ }
+
+ protected boolean isNullSelectionItemAvailable() {
+ return nullSelectionItemAvailable;
+ }
+
+ /**
+ * @return "cols" specified in uidl, 0 if not specified
+ */
+ protected int getColumns() {
+ return cols;
+ }
+
+ /**
+ * @return "rows" specified in uidl, 0 if not specified
+ */
+
+ protected int getRows() {
+ return rows;
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ this.client = client;
+ id = uidl.getId();
+
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ selectedKeys = uidl.getStringArrayVariableAsSet("selected");
+
+ readonly = uidl.getBooleanAttribute("readonly");
+ disabled = uidl.getBooleanAttribute("disabled");
+ multiselect = "multi".equals(uidl.getStringAttribute("selectmode"));
+ immediate = uidl.getBooleanAttribute("immediate");
+ nullSelectionAllowed = uidl.getBooleanAttribute("nullselect");
+ nullSelectionItemAvailable = uidl.getBooleanAttribute("nullselectitem");
+
+ cols = uidl.getIntAttribute("cols");
+ rows = uidl.getIntAttribute("rows");
+
+ final UIDL ops = uidl.getChildUIDL(0);
+
+ if (getColumns() > 0) {
+ container.setWidth(getColumns() + "em");
+ if (container != optionsContainer) {
+ optionsContainer.setWidth("100%");
+ }
+ }
+
+ buildOptions(ops);
+
+ if (uidl.getBooleanAttribute("allownewitem")) {
+ if (newItemField == null) {
+ newItemButton = new IButton();
+ newItemButton.setText("+");
+ newItemButton.setWidth("1.5em");
+ newItemButton.addClickListener(this);
+ newItemField = new ITextField();
+ newItemField.addKeyboardListener(this);
+ // newItemField.setColumns(16);
+ if (getColumns() > 0) {
+ newItemField.setWidth((getColumns() - 2) + "em");
+ }
+ }
+ newItemField.setEnabled(!disabled && !readonly);
+ newItemButton.setEnabled(!disabled && !readonly);
+
+ if (newItemField == null || newItemField.getParent() != container) {
+ container.add(newItemField);
+ container.add(newItemButton);
+ }
+ } else if (newItemField != null) {
+ container.remove(newItemField);
+ container.remove(newItemButton);
+ }
+
+ setTabIndex(uidl.hasAttribute("tabindex") ? uidl
+ .getIntAttribute("tabindex") : 0);
+
+ }
+
+ abstract protected void setTabIndex(int tabIndex);
+
+ public void onClick(Widget sender) {
+ if (sender == newItemButton && !newItemField.getText().equals("")) {
+ client.updateVariable(id, "newitem", newItemField.getText(), true);
+ newItemField.setText("");
+ }
+ }
+
+ public void onChange(Widget sender) {
+ if (multiselect) {
+ client
+ .updateVariable(id, "selected", getSelectedItems(),
+ immediate);
+ } else {
+ client.updateVariable(id, "selected", new String[] { ""
+ + getSelectedItem() }, immediate);
+ }
+ }
+
+ public void onKeyPress(Widget sender, char keyCode, int modifiers) {
+ if (sender == newItemField && keyCode == KeyboardListener.KEY_ENTER) {
+ newItemButton.click();
+ }
+ }
+
+ public void onKeyUp(Widget sender, char keyCode, int modifiers) {
+ // Ignore, subclasses may override
+ }
+
+ public void onKeyDown(Widget sender, char keyCode, int modifiers) {
+ // Ignore, subclasses may override
+ }
+
+ protected abstract void buildOptions(UIDL uidl);
+
+ protected abstract Object[] getSelectedItems();
+
+ protected Object getSelectedItem() {
+ final Object[] sel = getSelectedItems();
+ if (sel.length > 0) {
+ return sel[0];
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IOrderedLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IOrderedLayout.java new file mode 100644 index 0000000000..305b930351 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IOrderedLayout.java @@ -0,0 +1,876 @@ +package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Set;
+
+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.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
+import com.vaadin.terminal.gwt.client.RenderInformation.Size;
+import com.vaadin.terminal.gwt.client.ui.layout.CellBasedLayout;
+import com.vaadin.terminal.gwt.client.ui.layout.ChildComponentContainer;
+
+public class IOrderedLayout extends CellBasedLayout {
+
+ public static final String CLASSNAME = "i-orderedlayout";
+
+ private int orientation;
+
+ // Can be removed once OrderedLayout is removed
+ private boolean allowOrientationUpdate = false;
+
+ /**
+ * Size of the layout excluding any margins.
+ */
+ private Size activeLayoutSize = new Size(0, 0);
+
+ private boolean isRendering = false;
+
+ private String width = "";
+
+ private boolean sizeHasChangedDuringRendering = false;
+
+ public IOrderedLayout() {
+ this(CLASSNAME, ORIENTATION_VERTICAL);
+ allowOrientationUpdate = true;
+ }
+
+ protected IOrderedLayout(String className, int orientation) {
+ setStyleName(className);
+ this.orientation = orientation;
+
+ STYLENAME_SPACING = className + "-spacing";
+ STYLENAME_MARGIN_TOP = className + "-margin-top";
+ STYLENAME_MARGIN_RIGHT = className + "-margin-right";
+ STYLENAME_MARGIN_BOTTOM = className + "-margin-bottom";
+ STYLENAME_MARGIN_LEFT = className + "-margin-left";
+
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ isRendering = true;
+ super.updateFromUIDL(uidl, client);
+
+ // Only non-cached, visible UIDL:s can introduce changes
+ if (uidl.getBooleanAttribute("cached")
+ || uidl.getBooleanAttribute("invisible")) {
+ isRendering = false;
+ return;
+ }
+
+ if (allowOrientationUpdate) {
+ handleOrientationUpdate(uidl);
+ }
+
+ // IStopWatch w = new IStopWatch("OrderedLayout.updateFromUIDL");
+
+ ArrayList<Widget> uidlWidgets = new ArrayList<Widget>(uidl
+ .getChildCount());
+ ArrayList<ChildComponentContainer> relativeSizeComponents = new ArrayList<ChildComponentContainer>();
+ ArrayList<UIDL> relativeSizeComponentUIDL = new ArrayList<UIDL>();
+
+ int pos = 0;
+ for (final Iterator<UIDL> it = uidl.getChildIterator(); it.hasNext();) {
+ final UIDL childUIDL = it.next();
+ final Paintable child = client.getPaintable(childUIDL);
+ Widget widget = (Widget) child;
+
+ // Create container for component
+ ChildComponentContainer childComponentContainer = getComponentContainer(widget);
+
+ if (childComponentContainer == null) {
+ // This is a new component
+ childComponentContainer = createChildContainer(widget);
+ }
+
+ addOrMoveChild(childComponentContainer, pos++);
+
+ /*
+ * Components which are to be expanded in the same orientation as
+ * the layout are rendered later when it is clear how much space
+ * they can use
+ */
+ if (!Util.isCached(childUIDL)) {
+ FloatSize relativeSize = Util.parseRelativeSize(childUIDL);
+ childComponentContainer.setRelativeSize(relativeSize);
+ }
+
+ if (childComponentContainer.isComponentRelativeSized(orientation)) {
+ relativeSizeComponents.add(childComponentContainer);
+ relativeSizeComponentUIDL.add(childUIDL);
+ } else {
+ if (isDynamicWidth()) {
+ childComponentContainer.renderChild(childUIDL, client, 0);
+ } else {
+ childComponentContainer.renderChild(childUIDL, client,
+ activeLayoutSize.getWidth());
+ }
+ if (sizeHasChangedDuringRendering && Util.isCached(childUIDL)) {
+ // notify cached relative sized component about size
+ // chance
+ client.handleComponentRelativeSize(childComponentContainer
+ .getWidget());
+ }
+ }
+
+ uidlWidgets.add(widget);
+
+ }
+
+ // w.mark("Rendering of "
+ // + (uidlWidgets.size() - relativeSizeComponents.size())
+ // + " absolute size components done");
+
+ /*
+ * Remove any children after pos. These are the ones that previously
+ * were in the layout but have now been removed
+ */
+ removeChildrenAfter(pos);
+
+ // w.mark("Old children removed");
+
+ /* Fetch alignments and expand ratio from UIDL */
+ updateAlignmentsAndExpandRatios(uidl, uidlWidgets);
+ // w.mark("Alignments and expand ratios updated");
+
+ /* Fetch widget sizes from rendered components */
+ updateWidgetSizes();
+ // w.mark("Widget sizes updated");
+
+ recalculateLayout();
+ // w.mark("Layout size calculated (" + activeLayoutSize +
+ // ") offsetSize: "
+ // + getOffsetWidth() + "," + getOffsetHeight());
+
+ /* Render relative size components */
+ for (int i = 0; i < relativeSizeComponents.size(); i++) {
+ ChildComponentContainer childComponentContainer = relativeSizeComponents
+ .get(i);
+ UIDL childUIDL = relativeSizeComponentUIDL.get(i);
+
+ if (isDynamicWidth()) {
+ childComponentContainer.renderChild(childUIDL, client, 0);
+ } else {
+ childComponentContainer.renderChild(childUIDL, client,
+ activeLayoutSize.getWidth());
+ }
+
+ if (Util.isCached(childUIDL)) {
+ /*
+ * We must update the size of the relative sized component if
+ * the expand ratio or something else in the layout changes
+ * which affects the size of a relative sized component
+ */
+ client.handleComponentRelativeSize(childComponentContainer
+ .getWidget());
+ }
+
+ // childComponentContainer.updateWidgetSize();
+ }
+
+ // w.mark("Rendering of " + (relativeSizeComponents.size())
+ // + " relative size components done");
+
+ /* Fetch widget sizes for relative size components */
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+
+ /* Update widget size from DOM */
+ childComponentContainer.updateWidgetSize();
+ }
+
+ // w.mark("Widget sizes updated");
+
+ /*
+ * Components with relative size in main direction may affect the layout
+ * size in the other direction
+ */
+ if ((isHorizontal() && isDynamicHeight())
+ || (isVertical() && isDynamicWidth())) {
+ layoutSizeMightHaveChanged();
+ }
+ // w.mark("Layout dimensions updated");
+
+ /* Update component spacing */
+ updateContainerMargins();
+
+ /*
+ * Update component sizes for components with relative size in non-main
+ * direction
+ */
+ if (updateRelativeSizesInNonMainDirection()) {
+ // Sizes updated - might affect the other dimension so we need to
+ // recheck the widget sizes and recalculate layout dimensions
+ updateWidgetSizes();
+ layoutSizeMightHaveChanged();
+ }
+ calculateAlignments();
+ // w.mark("recalculateComponentSizesAndAlignments done");
+
+ setRootSize();
+
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * This should fix the issue with padding not always taken into
+ * account for the containers leading to no spacing between
+ * elements.
+ */
+ root.getStyle().setProperty("zoom", "1");
+ }
+
+ // w.mark("runDescendentsLayout done");
+ isRendering = false;
+ sizeHasChangedDuringRendering = false;
+ }
+
+ private void layoutSizeMightHaveChanged() {
+ Size oldSize = new Size(activeLayoutSize.getWidth(), activeLayoutSize
+ .getHeight());
+ calculateLayoutDimensions();
+
+ /*
+ * If layout dimension changes we must also update container sizes
+ */
+ if (!oldSize.equals(activeLayoutSize)) {
+ calculateContainerSize();
+ }
+ }
+
+ private void updateWidgetSizes() {
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+
+ /*
+ * Update widget size from DOM
+ */
+ childComponentContainer.updateWidgetSize();
+ }
+ }
+
+ private void recalculateLayout() {
+
+ /* Calculate space for relative size components */
+ int spaceForExpansion = calculateLayoutDimensions();
+
+ if (!widgetToComponentContainer.isEmpty()) {
+ /* Divide expansion space between component containers */
+ expandComponentContainers(spaceForExpansion);
+
+ /* Update container sizes */
+ calculateContainerSize();
+ }
+
+ }
+
+ private void expandComponentContainers(int spaceForExpansion) {
+ int remaining = spaceForExpansion;
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+ remaining -= childComponentContainer.expand(orientation,
+ spaceForExpansion);
+ }
+
+ if (remaining > 0) {
+ // Some left-over pixels due to rounding errors
+
+ // Add one pixel to each container until there are no pixels left
+
+ Iterator<Widget> widgetIterator = iterator();
+ while (widgetIterator.hasNext() && remaining-- > 0) {
+ ChildComponentContainer childComponentContainer = (ChildComponentContainer) widgetIterator
+ .next();
+ childComponentContainer.expandExtra(orientation, 1);
+ }
+ }
+
+ }
+
+ private void handleOrientationUpdate(UIDL uidl) {
+ int newOrientation = ORIENTATION_VERTICAL;
+ if ("horizontal".equals(uidl.getStringAttribute("orientation"))) {
+ newOrientation = ORIENTATION_HORIZONTAL;
+ }
+
+ if (orientation != newOrientation) {
+ orientation = newOrientation;
+
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+ childComponentContainer.setOrientation(orientation);
+ }
+ }
+
+ }
+
+ /**
+ * Updated components with relative height in horizontal layouts and
+ * components with relative width in vertical layouts. This is only needed
+ * if the height (horizontal layout) or width (vertical layout) has not been
+ * specified.
+ */
+ private boolean updateRelativeSizesInNonMainDirection() {
+ int updateDirection = 1 - orientation;
+ if ((updateDirection == ORIENTATION_HORIZONTAL && !isDynamicWidth())
+ || (updateDirection == ORIENTATION_VERTICAL && !isDynamicHeight())) {
+ return false;
+ }
+
+ boolean updated = false;
+ for (ChildComponentContainer componentContainer : widgetToComponentContainer
+ .values()) {
+ if (componentContainer.isComponentRelativeSized(updateDirection)) {
+ client.handleComponentRelativeSize(componentContainer
+ .getWidget());
+ }
+
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ private int calculateLayoutDimensions() {
+ int summedWidgetWidth = 0;
+ int summedWidgetHeight = 0;
+
+ int maxWidgetWidth = 0;
+ int maxWidgetHeight = 0;
+
+ // Calculate layout dimensions from component dimensions
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+
+ int widgetHeight = 0;
+ int widgetWidth = 0;
+ if (childComponentContainer.isComponentRelativeSized(orientation)) {
+ if (orientation == ORIENTATION_HORIZONTAL) {
+ widgetHeight = getWidgetHeight(childComponentContainer);
+ } else {
+ widgetWidth = getWidgetWidth(childComponentContainer);
+ }
+ } else {
+ widgetWidth = getWidgetWidth(childComponentContainer);
+ widgetHeight = getWidgetHeight(childComponentContainer);
+ }
+
+ summedWidgetWidth += widgetWidth;
+ summedWidgetHeight += widgetHeight;
+
+ maxWidgetHeight = Math.max(maxWidgetHeight, widgetHeight);
+ maxWidgetWidth = Math.max(maxWidgetWidth, widgetWidth);
+ }
+
+ if (isHorizontal()) {
+ summedWidgetWidth += activeSpacing.hSpacing
+ * (widgetToComponentContainer.size() - 1);
+ } else {
+ summedWidgetHeight += activeSpacing.vSpacing
+ * (widgetToComponentContainer.size() - 1);
+ }
+
+ Size layoutSize = updateLayoutDimensions(summedWidgetWidth,
+ summedWidgetHeight, maxWidgetWidth, maxWidgetHeight);
+
+ int remainingSpace;
+ if (isHorizontal()) {
+ remainingSpace = layoutSize.getWidth() - summedWidgetWidth;
+ } else {
+ remainingSpace = layoutSize.getHeight() - summedWidgetHeight;
+ }
+ if (remainingSpace < 0) {
+ remainingSpace = 0;
+ }
+
+ // ApplicationConnection.getConsole().log(
+ // "Layout size: " + activeLayoutSize);
+ return remainingSpace;
+ }
+
+ private int getWidgetHeight(ChildComponentContainer childComponentContainer) {
+ Size s = childComponentContainer.getWidgetSize();
+ return s.getHeight()
+ + childComponentContainer.getCaptionHeightAboveComponent();
+ }
+
+ private int getWidgetWidth(ChildComponentContainer childComponentContainer) {
+ Size s = childComponentContainer.getWidgetSize();
+ int widgetWidth = s.getWidth()
+ + childComponentContainer.getCaptionWidthAfterComponent();
+
+ /*
+ * If the component does not have a specified size in the main direction
+ * the caption may determine the space used by the component
+ */
+ if (!childComponentContainer.widgetHasSizeSpecified(orientation)) {
+ int captionWidth = childComponentContainer
+ .getCaptionRequiredWidth();
+
+ if (captionWidth > widgetWidth) {
+ widgetWidth = captionWidth;
+ }
+ }
+
+ return widgetWidth;
+ }
+
+ private void calculateAlignments() {
+ int w = 0;
+ int h = 0;
+
+ if (isHorizontal()) {
+ // HORIZONTAL
+ h = activeLayoutSize.getHeight();
+ if (!isDynamicWidth()) {
+ w = -1;
+ }
+
+ } else {
+ // VERTICAL
+ w = activeLayoutSize.getWidth();
+ if (!isDynamicHeight()) {
+ h = -1;
+ }
+ }
+
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+ childComponentContainer.updateAlignments(w, h);
+ }
+
+ }
+
+ private void calculateContainerSize() {
+
+ /*
+ * Container size here means the size the container gets from the
+ * component. The expansion size is not include in this but taken
+ * separately into account.
+ */
+ int height = 0, width = 0;
+ Iterator<Widget> widgetIterator = iterator();
+ if (isHorizontal()) {
+ height = activeLayoutSize.getHeight();
+ int availableWidth = activeLayoutSize.getWidth();
+ boolean first = true;
+ while (widgetIterator.hasNext()) {
+ ChildComponentContainer childComponentContainer = (ChildComponentContainer) widgetIterator
+ .next();
+ if (!childComponentContainer
+ .isComponentRelativeSized(ORIENTATION_HORIZONTAL)) {
+ /*
+ * Only components with non-relative size in the main
+ * direction has a container size
+ */
+ width = childComponentContainer.getWidgetSize().getWidth()
+ + childComponentContainer
+ .getCaptionWidthAfterComponent();
+
+ /*
+ * If the component does not have a specified size in the
+ * main direction the caption may determine the space used
+ * by the component
+ */
+ if (!childComponentContainer
+ .widgetHasSizeSpecified(orientation)) {
+ int captionWidth = childComponentContainer
+ .getCaptionRequiredWidth();
+ // ApplicationConnection.getConsole().log(
+ // "Component width: " + width
+ // + ", caption width: " + captionWidth);
+ if (captionWidth > width) {
+ width = captionWidth;
+ }
+ }
+ } else {
+ width = 0;
+ }
+
+ if (!isDynamicWidth()) {
+ if (availableWidth == 0) {
+ /*
+ * Let the overflowing components overflow. IE has
+ * problems with zero sizes.
+ */
+ // width = 0;
+ // height = 0;
+ } else if (width > availableWidth) {
+ width = availableWidth;
+
+ if (!first) {
+ width -= activeSpacing.hSpacing;
+ }
+ availableWidth = 0;
+ } else {
+ availableWidth -= width;
+ if (!first) {
+ availableWidth -= activeSpacing.hSpacing;
+ }
+ }
+
+ first = false;
+ }
+
+ childComponentContainer.setContainerSize(width, height);
+ }
+ } else {
+ width = activeLayoutSize.getWidth();
+ while (widgetIterator.hasNext()) {
+ ChildComponentContainer childComponentContainer = (ChildComponentContainer) widgetIterator
+ .next();
+
+ if (!childComponentContainer
+ .isComponentRelativeSized(ORIENTATION_VERTICAL)) {
+ /*
+ * Only components with non-relative size in the main
+ * direction has a container size
+ */
+ height = childComponentContainer.getWidgetSize()
+ .getHeight()
+ + childComponentContainer
+ .getCaptionHeightAboveComponent();
+ } else {
+ height = 0;
+ }
+
+ childComponentContainer.setContainerSize(width, height);
+ }
+
+ }
+
+ }
+
+ private Size updateLayoutDimensions(int totalComponentWidth,
+ int totalComponentHeight, int maxComponentWidth,
+ int maxComponentHeight) {
+
+ /* Only need to calculate dynamic dimensions */
+ if (!isDynamicHeight() && !isDynamicWidth()) {
+ return activeLayoutSize;
+ }
+
+ int activeLayoutWidth = 0;
+ int activeLayoutHeight = 0;
+
+ // Update layout dimensions
+ if (isHorizontal()) {
+ // Horizontal
+ if (isDynamicWidth()) {
+ activeLayoutWidth = totalComponentWidth;
+ }
+
+ if (isDynamicHeight()) {
+ activeLayoutHeight = maxComponentHeight;
+ }
+
+ } else {
+ // Vertical
+ if (isDynamicWidth()) {
+ activeLayoutWidth = maxComponentWidth;
+ }
+
+ if (isDynamicHeight()) {
+ activeLayoutHeight = totalComponentHeight;
+ }
+ }
+
+ if (isDynamicWidth()) {
+ setActiveLayoutWidth(activeLayoutWidth);
+ setOuterLayoutWidth(activeLayoutSize.getWidth());
+ }
+
+ if (isDynamicHeight()) {
+ setActiveLayoutHeight(activeLayoutHeight);
+ setOuterLayoutHeight(activeLayoutSize.getHeight());
+ }
+
+ return activeLayoutSize;
+ }
+
+ private void setActiveLayoutWidth(int activeLayoutWidth) {
+ if (activeLayoutWidth < 0) {
+ activeLayoutWidth = 0;
+ }
+ activeLayoutSize.setWidth(activeLayoutWidth);
+ }
+
+ private void setActiveLayoutHeight(int activeLayoutHeight) {
+ if (activeLayoutHeight < 0) {
+ activeLayoutHeight = 0;
+ }
+ activeLayoutSize.setHeight(activeLayoutHeight);
+
+ }
+
+ private void setOuterLayoutWidth(int activeLayoutWidth) {
+ super.setWidth((activeLayoutWidth + activeMargins.getHorizontal())
+ + "px");
+
+ }
+
+ private void setOuterLayoutHeight(int activeLayoutHeight) {
+ super.setHeight((activeLayoutHeight + activeMargins.getVertical())
+ + "px");
+
+ }
+
+ /**
+ * Updates the spacing between components. Needs to be done only when
+ * components are added/removed.
+ */
+ private void updateContainerMargins() {
+ ChildComponentContainer firstChildComponent = getFirstChildComponentContainer();
+ if (firstChildComponent != null) {
+ firstChildComponent.setMarginLeft(0);
+ firstChildComponent.setMarginTop(0);
+
+ for (ChildComponentContainer childComponent : widgetToComponentContainer
+ .values()) {
+ if (childComponent == firstChildComponent) {
+ continue;
+ }
+
+ if (isHorizontal()) {
+ childComponent.setMarginLeft(activeSpacing.hSpacing);
+ } else {
+ childComponent.setMarginTop(activeSpacing.vSpacing);
+ }
+ }
+ }
+ }
+
+ private boolean isHorizontal() {
+ return orientation == ORIENTATION_HORIZONTAL;
+ }
+
+ private boolean isVertical() {
+ return orientation == ORIENTATION_VERTICAL;
+ }
+
+ private ChildComponentContainer createChildContainer(Widget child) {
+
+ // Create a container DIV for the child
+ ChildComponentContainer childComponent = new ChildComponentContainer(
+ child, orientation);
+
+ return childComponent;
+
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ int width = 0;
+ int height = 0;
+ ChildComponentContainer childComponentContainer = getComponentContainer(child);
+ // WIDTH CALCULATION
+ if (isVertical()) {
+ width = activeLayoutSize.getWidth();
+ width -= childComponentContainer.getCaptionWidthAfterComponent();
+ } else if (!isDynamicWidth()) {
+ // HORIZONTAL
+ width = childComponentContainer.getContSize().getWidth();
+ width -= childComponentContainer.getCaptionWidthAfterComponent();
+ }
+
+ // HEIGHT CALCULATION
+ if (isHorizontal()) {
+ height = activeLayoutSize.getHeight();
+ height -= childComponentContainer.getCaptionHeightAboveComponent();
+ } else if (!isDynamicHeight()) {
+ // VERTICAL
+ height = childComponentContainer.getContSize().getHeight();
+ height -= childComponentContainer.getCaptionHeightAboveComponent();
+ }
+
+ // ApplicationConnection.getConsole().log(
+ // "allocatedSpace for " + Util.getSimpleName(child) + ": "
+ // + width + "," + height);
+ RenderSpace space = new RenderSpace(width, height);
+ return space;
+ }
+
+ private void recalculateLayoutAndComponentSizes() {
+ recalculateLayout();
+
+ if (!(isDynamicHeight() && isDynamicWidth())) {
+ /* First update relative sized components */
+ for (ChildComponentContainer componentContainer : widgetToComponentContainer
+ .values()) {
+ client.handleComponentRelativeSize(componentContainer
+ .getWidget());
+
+ // Update widget size from DOM
+ componentContainer.updateWidgetSize();
+ }
+ }
+
+ if (isDynamicHeight()) {
+ /*
+ * Height is not necessarily correct anymore as the height of
+ * components might have changed if the width has changed.
+ */
+
+ /*
+ * Get the new widget sizes from DOM and calculate new container
+ * sizes
+ */
+ updateWidgetSizes();
+
+ /* Update layout dimensions based on widget sizes */
+ recalculateLayout();
+ }
+
+ updateRelativeSizesInNonMainDirection();
+ calculateAlignments();
+
+ setRootSize();
+ }
+
+ private void setRootSize() {
+ root.getStyle().setPropertyPx("width", activeLayoutSize.getWidth());
+ root.getStyle().setPropertyPx("height", activeLayoutSize.getHeight());
+ }
+
+ public boolean requestLayout(Set<Paintable> children) {
+ for (Paintable p : children) {
+ /* Update widget size from DOM */
+ ChildComponentContainer componentContainer = getComponentContainer((Widget) p);
+ // This should no longer be needed (after #2563)
+ // if (isDynamicWidth()) {
+ // componentContainer.setUnlimitedContainerWidth();
+ // } else {
+ // componentContainer.setLimitedContainerWidth(activeLayoutSize
+ // .getWidth());
+ // }
+
+ componentContainer.updateWidgetSize();
+
+ /*
+ * If this is the result of an caption icon onload event the caption
+ * size may have changed
+ */
+ componentContainer.updateCaptionSize();
+ }
+
+ Size sizeBefore = new Size(activeLayoutSize.getWidth(),
+ activeLayoutSize.getHeight());
+
+ recalculateLayoutAndComponentSizes();
+ boolean sameSize = (sizeBefore.equals(activeLayoutSize));
+ if (!sameSize) {
+ /* Must inform child components about possible size updates */
+ client.runDescendentsLayout(this);
+ }
+
+ /* Automatically propagated upwards if the size has changed */
+
+ return sameSize;
+ }
+
+ @Override
+ public void setHeight(String height) {
+ Size sizeBefore = new Size(activeLayoutSize.getWidth(),
+ activeLayoutSize.getHeight());
+
+ super.setHeight(height);
+
+ if (height != null && !height.equals("")) {
+ setActiveLayoutHeight(getOffsetHeight()
+ - activeMargins.getVertical());
+ }
+
+ if (isRendering) {
+ sizeHasChangedDuringRendering = true;
+ } else {
+ recalculateLayoutAndComponentSizes();
+ boolean sameSize = (sizeBefore.equals(activeLayoutSize));
+ if (!sameSize) {
+ /* Must inform child components about possible size updates */
+ client.runDescendentsLayout(this);
+ }
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (this.width.equals(width)) {
+ return;
+ }
+ Size sizeBefore = new Size(activeLayoutSize.getWidth(),
+ activeLayoutSize.getHeight());
+
+ super.setWidth(width);
+ this.width = width;
+ if (width != null && !width.equals("")) {
+ setActiveLayoutWidth(getOffsetWidth()
+ - activeMargins.getHorizontal());
+ }
+
+ if (isRendering) {
+ sizeHasChangedDuringRendering = true;
+ } else {
+ recalculateLayoutAndComponentSizes();
+ boolean sameSize = (sizeBefore.equals(activeLayoutSize));
+ if (!sameSize) {
+ /* Must inform child components about possible size updates */
+ client.runDescendentsLayout(this);
+ }
+ /*
+ * If the height changes as a consequence of this we must inform the
+ * parent also
+ */
+ if (isDynamicHeight()
+ && sizeBefore.getHeight() != activeLayoutSize.getHeight()) {
+ Util.notifyParentOfSizeChange(this, false);
+ }
+
+ }
+ }
+
+ protected void updateAlignmentsAndExpandRatios(UIDL uidl,
+ ArrayList<Widget> renderedWidgets) {
+
+ /*
+ * UIDL contains component alignments as a comma separated list.
+ *
+ * See com.vaadin.terminal.gwt.client.ui.AlignmentInfo.java for
+ * possible values.
+ */
+ final int[] alignments = uidl.getIntArrayAttribute("alignments");
+
+ /*
+ * UIDL contains normalized expand ratios as a comma separated list.
+ */
+ final int[] expandRatios = uidl.getIntArrayAttribute("expandRatios");
+
+ for (int i = 0; i < renderedWidgets.size(); i++) {
+ Widget widget = renderedWidgets.get(i);
+
+ ChildComponentContainer container = getComponentContainer(widget);
+
+ // Calculate alignment info
+ container.setAlignment(new AlignmentInfo(alignments[i]));
+
+ // Update expand ratio
+ container.setExpandRatio(expandRatios[i]);
+ }
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ ChildComponentContainer componentContainer = getComponentContainer((Widget) component);
+ componentContainer.updateCaption(uidl, client);
+ if (!isRendering) {
+ /*
+ * This was a component-only update and the possible size change
+ * must be propagated to the layout
+ */
+ client.captionSizeUpdated(component);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IPanel.java b/src/com/vaadin/terminal/gwt/client/ui/IPanel.java new file mode 100644 index 0000000000..7be9a112b3 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IPanel.java @@ -0,0 +1,516 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.Set; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.SimplePanel; +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.Paintable; +import com.vaadin.terminal.gwt.client.RenderInformation; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class IPanel extends SimplePanel implements Container { + + public static final String CLASSNAME = "i-panel"; + + ApplicationConnection client; + + String id; + + private final Element captionNode = DOM.createDiv(); + + private final Element captionText = DOM.createSpan(); + + private Icon icon; + + private final Element bottomDecoration = DOM.createDiv(); + + private final Element contentNode = DOM.createDiv(); + + private Element errorIndicatorElement; + + private String height; + + private Paintable layout; + + ShortcutActionHandler shortcutHandler; + + private String width = ""; + + private Element geckoCaptionMeter; + + private int scrollTop; + + private int scrollLeft; + + private RenderInformation renderInformation = new RenderInformation(); + + private int borderPaddingHorizontal = -1; + + private int borderPaddingVertical = -1; + + private int captionPaddingHorizontal = -1; + + private int captionMarginLeft = -1; + + private boolean rendering; + + private int contentMarginLeft = -1; + + private String previousStyleName; + + public IPanel() { + super(); + DivElement captionWrap = Document.get().createDivElement(); + captionWrap.appendChild(captionNode); + captionNode.appendChild(captionText); + + captionWrap.setClassName(CLASSNAME + "-captionwrap"); + captionNode.setClassName(CLASSNAME + "-caption"); + contentNode.setClassName(CLASSNAME + "-content"); + bottomDecoration.setClassName(CLASSNAME + "-deco"); + + getElement().appendChild(captionWrap); + getElement().appendChild(contentNode); + getElement().appendChild(bottomDecoration); + setStyleName(CLASSNAME); + DOM.sinkEvents(getElement(), Event.ONKEYDOWN); + DOM.sinkEvents(contentNode, Event.ONSCROLL); + contentNode.getStyle().setProperty("position", "relative"); + getElement().getStyle().setProperty("overflow", "hidden"); + } + + @Override + protected Element getContainerElement() { + return contentNode; + } + + private void setCaption(String text) { + DOM.setInnerHTML(captionText, text); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + rendering = true; + if (!uidl.hasAttribute("cached")) { + // Handle caption displaying and style names, prior generics. + // Affects size + // calculations + + // Restore default stylenames + contentNode.setClassName(CLASSNAME + "-content"); + bottomDecoration.setClassName(CLASSNAME + "-deco"); + captionNode.setClassName(CLASSNAME + "-caption"); + boolean hasCaption = false; + if (uidl.hasAttribute("caption") + && !uidl.getStringAttribute("caption").equals("")) { + setCaption(uidl.getStringAttribute("caption")); + hasCaption = true; + } else { + setCaption(""); + captionNode.setClassName(CLASSNAME + "-nocaption"); + } + + // Add proper stylenames for all elements. This way we can prevent + // unwanted CSS selector inheritance. + if (uidl.hasAttribute("style")) { + final String[] styles = uidl.getStringAttribute("style").split( + " "); + final String captionBaseClass = CLASSNAME + + (hasCaption ? "-caption" : "-nocaption"); + final String contentBaseClass = CLASSNAME + "-content"; + final String decoBaseClass = CLASSNAME + "-deco"; + String captionClass = captionBaseClass; + String contentClass = contentBaseClass; + String decoClass = decoBaseClass; + for (int i = 0; i < styles.length; i++) { + captionClass += " " + captionBaseClass + "-" + styles[i]; + contentClass += " " + contentBaseClass + "-" + styles[i]; + decoClass += " " + decoBaseClass + "-" + styles[i]; + } + captionNode.setClassName(captionClass); + contentNode.setClassName(contentClass); + bottomDecoration.setClassName(decoClass); + + } + } + // Ensure correct implementation + if (client.updateComponent(this, uidl, false)) { + rendering = false; + return; + } + + this.client = client; + id = uidl.getId(); + + setIconUri(uidl, client); + + handleError(uidl); + + // Render content + final UIDL layoutUidl = uidl.getChildUIDL(0); + final Paintable newLayout = client.getPaintable(layoutUidl); + if (newLayout != layout) { + if (layout != null) { + client.unregisterPaintable(layout); + } + setWidget((Widget) newLayout); + layout = newLayout; + } + layout.updateFromUIDL(layoutUidl, client); + + runHacks(false); + // We may have actions attached to this panel + if (uidl.getChildCount() > 1) { + final int cnt = uidl.getChildCount(); + for (int i = 1; i < cnt; i++) { + UIDL childUidl = uidl.getChildUIDL(i); + if (childUidl.getTag().equals("actions")) { + if (shortcutHandler == null) { + shortcutHandler = new ShortcutActionHandler(id, client); + } + shortcutHandler.updateActionMap(childUidl); + } + } + } + + if (uidl.hasVariable("scrollTop") + && uidl.getIntVariable("scrollTop") != scrollTop) { + scrollTop = uidl.getIntVariable("scrollTop"); + DOM.setElementPropertyInt(contentNode, "scrollTop", scrollTop); + } + + if (uidl.hasVariable("scrollLeft") + && uidl.getIntVariable("scrollLeft") != scrollLeft) { + scrollLeft = uidl.getIntVariable("scrollLeft"); + DOM.setElementPropertyInt(contentNode, "scrollLeft", scrollLeft); + } + + rendering = false; + + } + + @Override + public void setStyleName(String style) { + if (!style.equals(previousStyleName)) { + super.setStyleName(style); + detectContainerBorders(); + previousStyleName = style; + } + } + + private void handleError(UIDL uidl) { + if (uidl.hasAttribute("error")) { + if (errorIndicatorElement == null) { + errorIndicatorElement = DOM.createDiv(); + DOM.setElementProperty(errorIndicatorElement, "className", + "i-errorindicator"); + DOM.sinkEvents(errorIndicatorElement, Event.MOUSEEVENTS); + sinkEvents(Event.MOUSEEVENTS); + } + DOM.insertBefore(captionNode, errorIndicatorElement, captionText); + } else if (errorIndicatorElement != null) { + DOM.removeChild(captionNode, errorIndicatorElement); + errorIndicatorElement = null; + } + } + + private void setIconUri(UIDL uidl, ApplicationConnection client) { + final String iconUri = uidl.hasAttribute("icon") ? uidl + .getStringAttribute("icon") : null; + if (iconUri == null) { + if (icon != null) { + DOM.removeChild(captionNode, icon.getElement()); + icon = null; + } + } else { + if (icon == null) { + icon = new Icon(client); + DOM.insertChild(captionNode, icon.getElement(), 0); + } + icon.setUri(iconUri); + } + } + + public void runHacks(boolean runGeckoFix) { + if (BrowserInfo.get().isIE6() && width != null && !width.equals("")) { + /* + * IE6 requires overflow-hidden elements to have a width specified + * so we calculate the width of the content and caption nodes when + * no width has been specified. + */ + /* + * Fixes #1923 IPanel: Horizontal scrollbar does not appear in IE6 + * with wide content + */ + + /* + * Caption must be shrunk for parent measurements to return correct + * result in IE6 + */ + DOM.setStyleAttribute(captionNode, "width", "1px"); + + int parentPadding = Util.measureHorizontalPaddingAndBorder( + getElement(), 0); + + int parentWidthExcludingPadding = getElement().getOffsetWidth() + - parentPadding; + + Util.setWidthExcludingPaddingAndBorder(captionNode, + parentWidthExcludingPadding - getCaptionMarginLeft(), 26, + false); + + int contentMarginLeft = getContentMarginLeft(); + + Util.setWidthExcludingPaddingAndBorder(contentNode, + parentWidthExcludingPadding - contentMarginLeft, 2, false); + + } + + if ((BrowserInfo.get().isIE() || BrowserInfo.get().isFF2()) + && (width == null || width.equals(""))) { + /* + * IE and FF2 needs width to be specified for the root DIV so we + * calculate that from the sizes of the caption and layout + */ + int captionWidth = captionText.getOffsetWidth() + + getCaptionMarginLeft() + getCaptionPaddingHorizontal(); + int layoutWidth = ((Widget) layout).getOffsetWidth() + + getContainerBorderWidth(); + int width = layoutWidth; + if (captionWidth > width) { + width = captionWidth; + } + + if (BrowserInfo.get().isIE7()) { + Util.setWidthExcludingPaddingAndBorder(captionNode, width + - getCaptionMarginLeft(), 26, false); + } + + super.setWidth(width + "px"); + } + + if (runGeckoFix && BrowserInfo.get().isGecko()) { + // workaround for #1764 + if (width == null || width.equals("")) { + if (geckoCaptionMeter == null) { + geckoCaptionMeter = DOM.createDiv(); + DOM.appendChild(captionNode, geckoCaptionMeter); + } + int captionWidth = DOM.getElementPropertyInt(captionText, + "offsetWidth"); + int availWidth = DOM.getElementPropertyInt(geckoCaptionMeter, + "offsetWidth"); + if (captionWidth == availWidth) { + /* + * Caption width defines panel width -> Gecko based browsers + * somehow fails to float things right, without the + * "noncode" below + */ + setWidth(getOffsetWidth() + "px"); + } else { + DOM.setStyleAttribute(captionNode, "width", ""); + } + } + } + + client.runDescendentsLayout(this); + + Util.runWebkitOverflowAutoFix(contentNode); + + } + + @Override + public void onBrowserEvent(Event event) { + final Element target = DOM.eventGetTarget(event); + final int type = DOM.eventGetType(event); + if (type == Event.ONKEYDOWN && shortcutHandler != null) { + shortcutHandler.handleKeyboardEvent(event); + return; + } + if (type == Event.ONSCROLL) { + int newscrollTop = DOM.getElementPropertyInt(contentNode, + "scrollTop"); + int newscrollLeft = DOM.getElementPropertyInt(contentNode, + "scrollLeft"); + if (client != null + && (newscrollLeft != scrollLeft || newscrollTop != scrollTop)) { + scrollLeft = newscrollLeft; + scrollTop = newscrollTop; + client.updateVariable(id, "scrollTop", scrollTop, false); + client.updateVariable(id, "scrollLeft", scrollLeft, false); + } + } else if (captionNode.isOrHasChild(target)) { + if (client != null) { + client.handleTooltipEvent(event, this); + } + } + } + + @Override + public void setHeight(String height) { + this.height = height; + super.setHeight(height); + if (height != null && height != "") { + final int targetHeight = getOffsetHeight(); + int containerHeight = targetHeight - captionNode.getOffsetHeight() + - bottomDecoration.getOffsetHeight() + - getContainerBorderHeight(); + if (containerHeight < 0) { + containerHeight = 0; + } + DOM + .setStyleAttribute(contentNode, "height", containerHeight + + "px"); + } else { + DOM.setStyleAttribute(contentNode, "height", ""); + } + if (!rendering) { + runHacks(true); + } + } + + private int getCaptionMarginLeft() { + if (captionMarginLeft < 0) { + detectContainerBorders(); + } + return captionMarginLeft; + } + + private int getContentMarginLeft() { + if (contentMarginLeft < 0) { + detectContainerBorders(); + } + return contentMarginLeft; + } + + private int getCaptionPaddingHorizontal() { + if (captionPaddingHorizontal < 0) { + detectContainerBorders(); + } + return captionPaddingHorizontal; + } + + private int getContainerBorderHeight() { + if (borderPaddingVertical < 0) { + detectContainerBorders(); + } + return borderPaddingVertical; + } + + @Override + public void setWidth(String width) { + if (this.width.equals(width)) { + return; + } + + this.width = width; + super.setWidth(width); + if (!rendering) { + runHacks(true); + + if (height.equals("")) { + // Width change may affect height + Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, this); + } + + } + } + + private int getContainerBorderWidth() { + if (borderPaddingHorizontal < 0) { + detectContainerBorders(); + } + return borderPaddingHorizontal; + } + + private void detectContainerBorders() { + DOM.setStyleAttribute(contentNode, "overflow", "hidden"); + + borderPaddingHorizontal = Util.measureHorizontalBorder(contentNode); + borderPaddingVertical = Util.measureVerticalBorder(contentNode); + + DOM.setStyleAttribute(contentNode, "overflow", "auto"); + + captionPaddingHorizontal = Util.measureHorizontalPaddingAndBorder( + captionNode, 26); + + captionMarginLeft = Util.measureMarginLeft(captionNode); + contentMarginLeft = Util.measureMarginLeft(contentNode); + + } + + public boolean hasChildComponent(Widget component) { + if (component != null && component == layout) { + return true; + } else { + return false; + } + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + // TODO This is untested as no layouts require this + if (oldComponent != layout) { + return; + } + + setWidget(newComponent); + layout = (Paintable) newComponent; + } + + public RenderSpace getAllocatedSpace(Widget child) { + int w = 0; + int h = 0; + + if (width != null && !width.equals("")) { + w = getOffsetWidth() - getContainerBorderWidth(); + if (w < 0) { + w = 0; + } + } + + if (height != null && !height.equals("")) { + h = contentNode.getOffsetHeight() - getContainerBorderHeight(); + if (h < 0) { + h = 0; + } + } + + return new RenderSpace(w, h, true); + } + + public boolean requestLayout(Set<Paintable> child) { + if (height != null && height != "" && width != null && width != "") { + /* + * If the height and width has been specified the child components + * cannot make the size of the layout change + */ + return true; + } + runHacks(false); + return !renderInformation.updateSize(getElement()); + } + + public void updateCaption(Paintable component, UIDL uidl) { + // NOP: layouts caption, errors etc not rendered in Panel + } + + @Override + protected void onAttach() { + super.onAttach(); + detectContainerBorders(); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IPasswordField.java b/src/com/vaadin/terminal/gwt/client/ui/IPasswordField.java new file mode 100644 index 0000000000..569076e353 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IPasswordField.java @@ -0,0 +1,21 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+
+/**
+ * This class represents a password field.
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+public class IPasswordField extends ITextField {
+
+ public IPasswordField() {
+ super(DOM.createInputPassword());
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IPopupCalendar.java b/src/com/vaadin/terminal/gwt/client/ui/IPopupCalendar.java new file mode 100644 index 0000000000..0b876f1cb2 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IPopupCalendar.java @@ -0,0 +1,130 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.PopupListener;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IPopupCalendar extends ITextualDate implements Paintable, Field,
+ ClickListener, PopupListener {
+
+ private final Button calendarToggle;
+
+ private final ICalendarPanel calendar;
+
+ private final IToolkitOverlay popup;
+ private boolean open = false;
+
+ public IPopupCalendar() {
+ super();
+
+ calendarToggle = new Button();
+ calendarToggle.setStyleName(CLASSNAME + "-button");
+ calendarToggle.setText("");
+ calendarToggle.addClickListener(this);
+ add(calendarToggle);
+
+ calendar = new ICalendarPanel(this);
+ popup = new IToolkitOverlay(true, true, true);
+ popup.setStyleName(IDateField.CLASSNAME + "-popup");
+ popup.setWidget(calendar);
+ popup.addPopupListener(this);
+
+ DOM.setElementProperty(calendar.getElement(), "id",
+ "PID_TOOLKIT_POPUPCAL");
+
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ super.updateFromUIDL(uidl, client);
+ if (date != null) {
+ calendar.updateCalendar();
+ }
+ calendarToggle.setEnabled(enabled);
+ }
+
+ public void onClick(Widget sender) {
+ if (sender == calendarToggle && !open) {
+ open = true;
+ calendar.updateCalendar();
+ // clear previous values
+ popup.setWidth("");
+ popup.setHeight("");
+ popup.setPopupPositionAndShow(new PositionCallback() {
+ public void setPosition(int offsetWidth, int offsetHeight) {
+ final int w = offsetWidth;
+ final int h = offsetHeight;
+ int t = calendarToggle.getAbsoluteTop();
+ int l = calendarToggle.getAbsoluteLeft();
+ if (l + w > Window.getClientWidth()
+ + Window.getScrollLeft()) {
+ l = Window.getClientWidth() + Window.getScrollLeft()
+ - w;
+ }
+ if (t + h + calendarToggle.getOffsetHeight() + 30 > Window
+ .getClientHeight()
+ + Window.getScrollTop()) {
+ t = Window.getClientHeight() + Window.getScrollTop()
+ - h - calendarToggle.getOffsetHeight() - 30;
+ l += calendarToggle.getOffsetWidth();
+ }
+
+ // fix size
+ popup.setWidth(w + "px");
+ popup.setHeight(h + "px");
+
+ popup.setPopupPosition(l, t
+ + calendarToggle.getOffsetHeight() + 2);
+
+ setFocus(true);
+ }
+ });
+ }
+ }
+
+ public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+ if (sender == popup) {
+ buildDate();
+ // Sigh.
+ Timer t = new Timer() {
+ @Override
+ public void run() {
+ open = false;
+ }
+ };
+ t.schedule(100);
+ }
+ }
+
+ /**
+ * Sets focus to Calendar panel.
+ *
+ * @param focus
+ */
+ public void setFocus(boolean focus) {
+ calendar.setFocus(focus);
+ }
+
+ @Override
+ protected int getFieldExtraWidth() {
+ if (fieldExtraWidth < 0) {
+ fieldExtraWidth = super.getFieldExtraWidth();
+ fieldExtraWidth += calendarToggle.getOffsetWidth();
+ }
+ return fieldExtraWidth;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IPopupView.java b/src/com/vaadin/terminal/gwt/client/ui/IPopupView.java new file mode 100644 index 0000000000..8e89b451c4 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IPopupView.java @@ -0,0 +1,417 @@ +package com.vaadin.terminal.gwt.client.ui; + +import java.util.HashSet; +import java.util.Set; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HasFocus; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.PopupListener; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Container; +import com.vaadin.terminal.gwt.client.ICaption; +import com.vaadin.terminal.gwt.client.ICaptionWrapper; +import com.vaadin.terminal.gwt.client.ITooltip; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.RenderInformation.Size; + +public class IPopupView extends HTML implements Container { + + public static final String CLASSNAME = "i-popupview"; + + /** For server-client communication */ + private String uidlId; + private ApplicationConnection client; + + /** This variable helps to communicate popup visibility to the server */ + private boolean hostPopupVisible; + + private final CustomPopup popup; + private final Label loading = new Label("Loading..."); + + /** + * loading constructor + */ + public IPopupView() { + super(); + popup = new CustomPopup(); + + setStyleName(CLASSNAME); + popup.setStylePrimaryName(CLASSNAME + "-popup"); + + setHTML("(No HTML defined for PopupView)"); + popup.setWidget(loading); + + // When we click to open the popup... + addClickListener(new ClickListener() { + public void onClick(Widget sender) { + updateState(true); + } + }); + + // ..and when we close it + popup.addPopupListener(new PopupListener() { + public void onPopupClosed(PopupPanel sender, boolean autoClosed) { + updateState(false); + } + }); + + popup.setAnimationEnabled(true); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + } + + /** + * + * + * @see com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal.gwt.client.UIDL, + * com.vaadin.terminal.gwt.client.ApplicationConnection) + */ + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // This call should be made first. Ensure correct implementation, + // and don't let the containing layout manage caption. + if (client.updateComponent(this, uidl, false)) { + return; + } + // These are for future server connections + this.client = client; + uidlId = uidl.getId(); + + hostPopupVisible = uidl.getBooleanVariable("popupVisibility"); + + setHTML(uidl.getStringAttribute("html")); + + if (uidl.hasAttribute("hideOnMouseOut")) { + popup.setHideOnMouseOut(uidl.getBooleanAttribute("hideOnMouseOut")); + } + + // Render the popup if visible and show it. + if (hostPopupVisible) { + UIDL popupUIDL = uidl.getChildUIDL(0); + + // showPopupOnTop(popup, hostReference); + preparePopup(popup); + popup.updateFromUIDL(popupUIDL, client); + if (uidl.hasAttribute("style")) { + final String[] styles = uidl.getStringAttribute("style").split( + " "); + final StringBuffer styleBuf = new StringBuffer(); + final String primaryName = popup.getStylePrimaryName(); + styleBuf.append(primaryName); + for (int i = 0; i < styles.length; i++) { + styleBuf.append(" "); + styleBuf.append(primaryName); + styleBuf.append("-"); + styleBuf.append(styles[i]); + } + popup.setStyleName(styleBuf.toString()); + } else { + popup.setStyleName(popup.getStylePrimaryName()); + } + showPopup(popup); + + // The popup shouldn't be visible, try to hide it. + } else { + popup.hide(); + } + }// updateFromUIDL + + /** + * Update popup visibility to server + * + * @param visibility + */ + private void updateState(boolean visible) { + // If we know the server connection + // then update the current situation + if (uidlId != null && client != null && isAttached()) { + client.updateVariable(uidlId, "popupVisibility", visible, true); + } + } + + private void preparePopup(final CustomPopup popup) { + popup.setVisible(false); + popup.show(); + } + + private void showPopup(final CustomPopup popup) { + int windowTop = RootPanel.get().getAbsoluteTop(); + int windowLeft = RootPanel.get().getAbsoluteLeft(); + int windowRight = windowLeft + RootPanel.get().getOffsetWidth(); + int windowBottom = windowTop + RootPanel.get().getOffsetHeight(); + + int offsetWidth = popup.getOffsetWidth(); + int offsetHeight = popup.getOffsetHeight(); + + int hostHorizontalCenter = IPopupView.this.getAbsoluteLeft() + + IPopupView.this.getOffsetWidth() / 2; + int hostVerticalCenter = IPopupView.this.getAbsoluteTop() + + IPopupView.this.getOffsetHeight() / 2; + + int left = hostHorizontalCenter - offsetWidth / 2; + int top = hostVerticalCenter - offsetHeight / 2; + + // Superclass takes care of top and left + if ((left + offsetWidth) > windowRight) { + left -= (left + offsetWidth) - windowRight; + } + + if ((top + offsetHeight) > windowBottom) { + top -= (top + offsetHeight) - windowBottom; + } + + popup.setPopupPosition(left, top); + + popup.setVisible(true); + } + + /** + * Make sure that we remove the popup when the main widget is removed. + * + * @see com.google.gwt.user.client.ui.Widget#onUnload() + */ + @Override + protected void onDetach() { + popup.hide(); + super.onDetach(); + } + + private static native void nativeBlur(Element e) + /*-{ + if(e && e.blur) { + e.blur(); + } + }-*/; + + private class CustomPopup extends IToolkitOverlay { + + private Paintable popupComponentPaintable = null; + private Widget popupComponentWidget = null; + private ICaptionWrapper captionWrapper = null; + + private boolean hasHadMouseOver = false; + private boolean hideOnMouseOut = true; + private final Set<Element> activeChildren = new HashSet<Element>(); + private boolean hiding = false; + + public CustomPopup() { + super(true, false, true); // autoHide, not modal, dropshadow + } + + // For some reason ONMOUSEOUT events are not always received, so we have + // to use ONMOUSEMOVE that doesn't target the popup + @Override + public boolean onEventPreview(Event event) { + Element target = DOM.eventGetTarget(event); + boolean eventTargetsPopup = DOM.isOrHasChild(getElement(), target); + int type = DOM.eventGetType(event); + + // Catch children that use keyboard, so we can unfocus them when + // hiding + if (eventTargetsPopup && type == Event.ONKEYPRESS) { + activeChildren.add(target); + } + + if (eventTargetsPopup && type == Event.ONMOUSEMOVE) { + hasHadMouseOver = true; + } + + if (!eventTargetsPopup && type == Event.ONMOUSEMOVE) { + + if (hasHadMouseOver && hideOnMouseOut) { + hide(); + return true; + } + } + + return super.onEventPreview(event); + } + + @Override + public void hide(boolean autoClosed) { + hiding = true; + syncChildren(); + unregisterPaintables(); + if (popupComponentWidget != null && popupComponentWidget != loading) { + remove(popupComponentWidget); + } + hasHadMouseOver = false; + super.hide(autoClosed); + } + + @Override + public void show() { + hiding = false; + super.show(); + } + + /** + * Try to sync all known active child widgets to server + */ + public void syncChildren() { + // Notify children with focus + if ((popupComponentWidget instanceof HasFocus)) { + ((HasFocus) popupComponentWidget).setFocus(false); + } + + // Notify children that have used the keyboard + for (Element e : activeChildren) { + try { + nativeBlur(e); + } catch (Exception ignored) { + } + } + activeChildren.clear(); + } + + @Override + public boolean remove(Widget w) { + + popupComponentPaintable = null; + popupComponentWidget = null; + captionWrapper = null; + + return super.remove(w); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + + Paintable newPopupComponent = client.getPaintable(uidl + .getChildUIDL(0)); + + if (newPopupComponent != popupComponentPaintable) { + + setWidget((Widget) newPopupComponent); + + popupComponentWidget = (Widget) newPopupComponent; + + popupComponentPaintable = newPopupComponent; + } + + popupComponentPaintable + .updateFromUIDL(uidl.getChildUIDL(0), client); + + } + + public void unregisterPaintables() { + if (popupComponentPaintable != null) { + client.unregisterPaintable(popupComponentPaintable); + } + } + + public void setHideOnMouseOut(boolean hideOnMouseOut) { + this.hideOnMouseOut = hideOnMouseOut; + } + + /* + * + * We need a hack make popup act as a child of IPopupView in toolkits + * component tree, but work in default GWT manner when closing or + * opening. + * + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.Widget#getParent() + */ + @Override + public Widget getParent() { + if (!isAttached() || hiding) { + return super.getParent(); + } else { + return IPopupView.this; + } + } + + @Override + protected void onDetach() { + super.onDetach(); + hiding = false; + } + + @Override + public Element getContainerElement() { + return super.getContainerElement(); + } + + }// class CustomPopup + + // Container methods + + public RenderSpace getAllocatedSpace(Widget child) { + Size popupExtra = calculatePopupExtra(); + + return new RenderSpace(RootPanel.get().getOffsetWidth() + - popupExtra.getWidth(), RootPanel.get().getOffsetHeight() + - popupExtra.getHeight()); + } + + /** + * Calculate extra space taken by the popup decorations + * + * @return + */ + protected Size calculatePopupExtra() { + Element pe = popup.getElement(); + Element ipe = popup.getContainerElement(); + + // border + padding + int width = Util.getRequiredWidth(pe) - Util.getRequiredWidth(ipe); + int height = Util.getRequiredHeight(pe) - Util.getRequiredHeight(ipe); + + return new Size(width, height); + } + + public boolean hasChildComponent(Widget component) { + if (popup.popupComponentWidget != null) { + return popup.popupComponentWidget == component; + } else { + return false; + } + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + popup.setWidget(newComponent); + popup.popupComponentWidget = newComponent; + } + + public boolean requestLayout(Set<Paintable> child) { + return true; + } + + public void updateCaption(Paintable component, UIDL uidl) { + if (ICaption.isNeeded(uidl)) { + if (popup.captionWrapper != null) { + popup.captionWrapper.updateCaption(uidl); + } else { + popup.captionWrapper = new ICaptionWrapper(component, client); + popup.setWidget(popup.captionWrapper); + popup.captionWrapper.updateCaption(uidl); + } + } else { + if (popup.captionWrapper != null) { + popup.setWidget(popup.popupComponentWidget); + } + } + + popup.popupComponentWidget = (Widget) component; + popup.popupComponentPaintable = component; + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (client != null) { + client.handleTooltipEvent(event, this); + } + } + +}// class IPopupView diff --git a/src/com/vaadin/terminal/gwt/client/ui/IProgressIndicator.java b/src/com/vaadin/terminal/gwt/client/ui/IProgressIndicator.java new file mode 100644 index 0000000000..0490dd048f --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IProgressIndicator.java @@ -0,0 +1,100 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; + +public class IProgressIndicator extends Widget implements Paintable { + + private static final String CLASSNAME = "i-progressindicator"; + Element wrapper = DOM.createDiv(); + Element indicator = DOM.createDiv(); + private ApplicationConnection client; + private final Poller poller; + private boolean indeterminate = false; + private boolean pollerSuspendedDueDetach; + + public IProgressIndicator() { + setElement(DOM.createDiv()); + getElement().appendChild(wrapper); + setStyleName(CLASSNAME); + wrapper.appendChild(indicator); + indicator.setClassName(CLASSNAME + "-indicator"); + wrapper.setClassName(CLASSNAME + "-wrapper"); + poller = new Poller(); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (client.updateComponent(this, uidl, true)) { + return; + } + + poller.cancel(); + this.client = client; + if (client.updateComponent(this, uidl, true)) { + return; + } + + indeterminate = uidl.getBooleanAttribute("indeterminate"); + + if (indeterminate) { + String basename = CLASSNAME + "-indeterminate"; + IProgressIndicator.setStyleName(getElement(), basename, true); + IProgressIndicator.setStyleName(getElement(), basename + + "-disabled", uidl.getBooleanAttribute("disabled")); + } else { + try { + final float f = Float.parseFloat(uidl + .getStringAttribute("state")); + final int size = Math.round(100 * f); + DOM.setStyleAttribute(indicator, "width", size + "%"); + } catch (final Exception e) { + } + } + + if (!uidl.getBooleanAttribute("disabled")) { + poller.scheduleRepeating(uidl.getIntAttribute("pollinginterval")); + } + } + + @Override + protected void onAttach() { + super.onAttach(); + if (pollerSuspendedDueDetach) { + poller.run(); + } + } + + @Override + protected void onDetach() { + super.onDetach(); + poller.cancel(); + pollerSuspendedDueDetach = true; + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (!visible) { + poller.cancel(); + } + } + + class Poller extends Timer { + + @Override + public void run() { + client.sendPendingVariableChanges(); + } + + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IScrollTable.java b/src/com/vaadin/terminal/gwt/client/ui/IScrollTable.java new file mode 100644 index 0000000000..2b085ebd87 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IScrollTable.java @@ -0,0 +1,2841 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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 java.util.Vector; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.TableCellElement; +import com.google.gwt.dom.client.TableRowElement; +import com.google.gwt.dom.client.TableSectionElement; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +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.FlowPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.ScrollListener; +import com.google.gwt.user.client.ui.ScrollPanel; +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.MouseEventDetails; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.IScrollTable.IScrollTableBody.IScrollTableRow; + +/** + * IScrollTable + * + * IScrollTable is a FlowPanel having two widgets in it: * TableHead component * + * ScrollPanel + * + * TableHead contains table's header and widgets + logic for resizing, + * reordering and hiding columns. + * + * ScrollPanel contains IScrollTableBody object which handles content. To save + * some bandwidth and to improve clients responsiveness with loads of data, in + * IScrollTableBody all rows are not necessary rendered. There are "spacers" in + * IScrollTableBody to use the exact same space as non-rendered rows would use. + * This way we can use seamlessly traditional scrollbars and scrolling to fetch + * more rows instead of "paging". + * + * In IScrollTable we listen to scroll events. On horizontal scrolling we also + * update TableHeads scroll position which has its scrollbars hidden. On + * vertical scroll events we will check if we are reaching the end of area where + * we have rows rendered and + * + * TODO implement unregistering for child components in Cells + */ +public class IScrollTable extends FlowPanel implements Table, ScrollListener { + + public static final String CLASSNAME = "i-table"; + /** + * multiple of pagelength which component will cache when requesting more + * rows + */ + private static final double CACHE_RATE = 2; + /** + * fraction of pageLenght which can be scrolled without making new request + */ + private static final double CACHE_REACT_RATE = 1.5; + + public static final char ALIGN_CENTER = 'c'; + public static final char ALIGN_LEFT = 'b'; + public static final char ALIGN_RIGHT = 'e'; + private int firstRowInViewPort = 0; + private int pageLength = 15; + private int lastRequestedFirstvisible = 0; // to detect "serverside scroll" + + private boolean showRowHeaders = false; + + private String[] columnOrder; + + private ApplicationConnection client; + private String paintableId; + + private boolean immediate; + + private int selectMode = Table.SELECT_MODE_NONE; + + private final HashSet<String> selectedRowKeys = new HashSet<String>(); + + private boolean initializedAndAttached = false; + + /** + * Flag to indicate if a column width recalculation is needed due update. + */ + private boolean headerChangedDuringUpdate = false; + + private final TableHead tHead = new TableHead(); + + private final ScrollPanel bodyContainer = new ScrollPanel(); + + private int totalRows; + + private Set<String> collapsedColumns; + + private final RowRequestHandler rowRequestHandler; + private IScrollTableBody tBody; + private int firstvisible = 0; + private boolean sortAscending; + private String sortColumn; + private boolean columnReordering; + + /** + * This map contains captions and icon urls for actions like: * "33_c" -> + * "Edit" * "33_i" -> "http://dom.com/edit.png" + */ + private final HashMap<Object, String> actionMap = new HashMap<Object, String>(); + private String[] visibleColOrder; + private boolean initialContentReceived = false; + private Element scrollPositionElement; + private boolean enabled; + private boolean showColHeaders; + + /** flag to indicate that table body has changed */ + private boolean isNewBody = true; + + private boolean emitClickEvents; + + /* + * Read from the "recalcWidths" -attribute. When it is true, the table will + * recalculate the widths for columns - desirable in some cases. For #1983, + * marked experimental. + */ + boolean recalcWidths = false; + + private final ArrayList<Panel> lazyUnregistryBag = new ArrayList<Panel>(); + private String height; + private String width = ""; + private boolean rendering = false; + + public IScrollTable() { + bodyContainer.addScrollListener(this); + bodyContainer.setStyleName(CLASSNAME + "-body"); + + setStyleName(CLASSNAME); + add(tHead); + add(bodyContainer); + + rowRequestHandler = new RowRequestHandler(); + + } + + @SuppressWarnings("unchecked") + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + rendering = true; + if (client.updateComponent(this, uidl, true)) { + rendering = false; + return; + } + + // we may have pending cache row fetch, cancel it. See #2136 + rowRequestHandler.cancel(); + + enabled = !uidl.hasAttribute("disabled"); + + this.client = client; + paintableId = uidl.getStringAttribute("id"); + immediate = uidl.getBooleanAttribute("immediate"); + emitClickEvents = uidl.getBooleanAttribute("listenClicks"); + final int newTotalRows = uidl.getIntAttribute("totalrows"); + if (newTotalRows != totalRows) { + if (tBody != null) { + if (totalRows == 0) { + tHead.clear(); + } + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + totalRows = newTotalRows; + } + + recalcWidths = uidl.hasAttribute("recalcWidths"); + + pageLength = uidl.getIntAttribute("pagelength"); + if (pageLength == 0) { + pageLength = totalRows; + } + firstvisible = uidl.hasVariable("firstvisible") ? uidl + .getIntVariable("firstvisible") : 0; + if (firstvisible != lastRequestedFirstvisible && tBody != null) { + // received 'surprising' firstvisible from server: scroll there + firstRowInViewPort = firstvisible; + bodyContainer + .setScrollPosition(firstvisible * tBody.getRowHeight()); + } + + showRowHeaders = uidl.getBooleanAttribute("rowheaders"); + showColHeaders = uidl.getBooleanAttribute("colheaders"); + + if (uidl.hasVariable("sortascending")) { + sortAscending = uidl.getBooleanVariable("sortascending"); + sortColumn = uidl.getStringVariable("sortcolumn"); + } + + if (uidl.hasVariable("selected")) { + final Set<String> selectedKeys = uidl + .getStringArrayVariableAsSet("selected"); + selectedRowKeys.clear(); + for (String string : selectedKeys) { + selectedRowKeys.add(string); + } + } + + if (uidl.hasAttribute("selectmode")) { + if (uidl.getBooleanAttribute("readonly")) { + selectMode = Table.SELECT_MODE_NONE; + } else if (uidl.getStringAttribute("selectmode").equals("multi")) { + selectMode = Table.SELECT_MODE_MULTI; + } else if (uidl.getStringAttribute("selectmode").equals("single")) { + selectMode = Table.SELECT_MODE_SINGLE; + } else { + selectMode = Table.SELECT_MODE_NONE; + } + } + + if (uidl.hasVariable("columnorder")) { + columnReordering = true; + columnOrder = uidl.getStringArrayVariable("columnorder"); + } + + if (uidl.hasVariable("collapsedcolumns")) { + tHead.setColumnCollapsingAllowed(true); + collapsedColumns = uidl + .getStringArrayVariableAsSet("collapsedcolumns"); + } else { + tHead.setColumnCollapsingAllowed(false); + } + + UIDL rowData = null; + for (final Iterator it = uidl.getChildIterator(); it.hasNext();) { + final UIDL c = (UIDL) it.next(); + if (c.getTag().equals("rows")) { + rowData = c; + } else if (c.getTag().equals("actions")) { + updateActionMap(c); + } else if (c.getTag().equals("visiblecolumns")) { + tHead.updateCellsFromUIDL(c); + } + } + updateHeader(uidl.getStringArrayAttribute("vcolorder")); + + if (!recalcWidths && initializedAndAttached) { + updateBody(rowData, uidl.getIntAttribute("firstrow"), uidl + .getIntAttribute("rows")); + if (headerChangedDuringUpdate) { + lazyAdjustColumnWidths.schedule(1); + } + } else { + if (tBody != null) { + tBody.removeFromParent(); + lazyUnregistryBag.add(tBody); + } + tBody = new IScrollTableBody(); + + tBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"), + uidl.getIntAttribute("rows")); + bodyContainer.add(tBody); + initialContentReceived = true; + if (isAttached()) { + sizeInit(); + } + } + hideScrollPositionAnnotation(); + purgeUnregistryBag(); + rendering = false; + headerChangedDuringUpdate = false; + } + + /** + * Unregisters Paintables in "trashed" HasWidgets (IScrollTableBodys or + * IScrollTableRows). This is done lazily as Table must survive from + * "subtreecaching" logic. + */ + private void purgeUnregistryBag() { + for (Iterator<Panel> iterator = lazyUnregistryBag.iterator(); iterator + .hasNext();) { + client.unregisterChildPaintables(iterator.next()); + } + lazyUnregistryBag.clear(); + } + + private void updateActionMap(UIDL c) { + final Iterator<?> it = c.getChildIterator(); + while (it.hasNext()) { + final UIDL action = (UIDL) it.next(); + final String key = action.getStringAttribute("key"); + final String caption = action.getStringAttribute("caption"); + actionMap.put(key + "_c", caption); + if (action.hasAttribute("icon")) { + // TODO need some uri handling ?? + actionMap.put(key + "_i", client.translateToolkitUri(action + .getStringAttribute("icon"))); + } + } + + } + + public String getActionCaption(String actionKey) { + return actionMap.get(actionKey + "_c"); + } + + public String getActionIcon(String actionKey) { + return actionMap.get(actionKey + "_i"); + } + + private void updateHeader(String[] strings) { + if (strings == null) { + return; + } + + int visibleCols = strings.length; + int colIndex = 0; + if (showRowHeaders) { + tHead.enableColumn("0", colIndex); + visibleCols++; + visibleColOrder = new String[visibleCols]; + visibleColOrder[colIndex] = "0"; + colIndex++; + } else { + visibleColOrder = new String[visibleCols]; + tHead.removeCell("0"); + } + + int i; + for (i = 0; i < strings.length; i++) { + final String cid = strings[i]; + visibleColOrder[colIndex] = cid; + tHead.enableColumn(cid, colIndex); + colIndex++; + } + + tHead.setVisible(showColHeaders); + + } + + /** + * @param uidl + * which contains row data + * @param firstRow + * first row in data set + * @param reqRows + * amount of rows in data set + */ + private void updateBody(UIDL uidl, int firstRow, int reqRows) { + if (uidl == null || reqRows < 1) { + // container is empty, remove possibly existing rows + if (firstRow < 0) { + while (tBody.getLastRendered() > tBody.firstRendered) { + tBody.unlinkRow(false); + } + tBody.unlinkRow(false); + } + return; + } + + tBody.renderRows(uidl, firstRow, reqRows); + + final int optimalFirstRow = (int) (firstRowInViewPort - pageLength + * CACHE_RATE); + boolean cont = true; + while (cont && tBody.getLastRendered() > optimalFirstRow + && tBody.getFirstRendered() < optimalFirstRow) { + // client.console.log("removing row from start"); + cont = tBody.unlinkRow(true); + } + final int optimalLastRow = (int) (firstRowInViewPort + pageLength + pageLength + * CACHE_RATE); + cont = true; + while (cont && tBody.getLastRendered() > optimalLastRow) { + // client.console.log("removing row from the end"); + cont = tBody.unlinkRow(false); + } + tBody.fixSpacers(); + + } + + /** + * Gives correct column index for given column key ("cid" in UIDL). + * + * @param colKey + * @return column index of visible columns, -1 if column not visible + */ + private int getColIndexByKey(String colKey) { + // return 0 if asked for rowHeaders + if ("0".equals(colKey)) { + return 0; + } + for (int i = 0; i < visibleColOrder.length; i++) { + if (visibleColOrder[i].equals(colKey)) { + return i; + } + } + return -1; + } + + private boolean isCollapsedColumn(String colKey) { + if (collapsedColumns == null) { + return false; + } + if (collapsedColumns.contains(colKey)) { + return true; + } + return false; + } + + private String getColKeyByIndex(int index) { + return tHead.getHeaderCell(index).getColKey(); + } + + private void setColWidth(int colIndex, int w, boolean isDefinedWidth) { + final HeaderCell cell = tHead.getHeaderCell(colIndex); + cell.setWidth(w, isDefinedWidth); + tBody.setColWidth(colIndex, w); + } + + private int getColWidth(String colKey) { + return tHead.getHeaderCell(colKey).getWidth(); + } + + private IScrollTableRow getRenderedRowByKey(String key) { + final Iterator<Widget> it = tBody.iterator(); + IScrollTableRow r = null; + while (it.hasNext()) { + r = (IScrollTableRow) it.next(); + if (r.getKey().equals(key)) { + return r; + } + } + return null; + } + + private void reOrderColumn(String columnKey, int newIndex) { + + final int oldIndex = getColIndexByKey(columnKey); + + // Change header order + tHead.moveCell(oldIndex, newIndex); + + // Change body order + tBody.moveCol(oldIndex, newIndex); + + /* + * Build new columnOrder and update it to server Note that columnOrder + * also contains collapsed columns so we cannot directly build it from + * cells vector Loop the old columnOrder and append in order to new + * array unless on moved columnKey. On new index also put the moved key + * i == index on columnOrder, j == index on newOrder + */ + final String oldKeyOnNewIndex = visibleColOrder[newIndex]; + if (showRowHeaders) { + newIndex--; // columnOrder don't have rowHeader + } + // add back hidden rows, + for (int i = 0; i < columnOrder.length; i++) { + if (columnOrder[i].equals(oldKeyOnNewIndex)) { + break; // break loop at target + } + if (isCollapsedColumn(columnOrder[i])) { + newIndex++; + } + } + // finally we can build the new columnOrder for server + final String[] newOrder = new String[columnOrder.length]; + for (int i = 0, j = 0; j < newOrder.length; i++) { + if (j == newIndex) { + newOrder[j] = columnKey; + j++; + } + if (i == columnOrder.length) { + break; + } + if (columnOrder[i].equals(columnKey)) { + continue; + } + newOrder[j] = columnOrder[i]; + j++; + } + columnOrder = newOrder; + // also update visibleColumnOrder + int i = showRowHeaders ? 1 : 0; + for (int j = 0; j < newOrder.length; j++) { + final String cid = newOrder[j]; + if (!isCollapsedColumn(cid)) { + visibleColOrder[i++] = cid; + } + } + client.updateVariable(paintableId, "columnorder", columnOrder, false); + } + + @Override + protected void onAttach() { + super.onAttach(); + if (initialContentReceived) { + sizeInit(); + } + } + + @Override + protected void onDetach() { + rowRequestHandler.cancel(); + super.onDetach(); + // ensure that scrollPosElement will be detached + if (scrollPositionElement != null) { + final Element parent = DOM.getParent(scrollPositionElement); + if (parent != null) { + DOM.removeChild(parent, scrollPositionElement); + } + } + } + + /** + * Run only once when component is attached and received its initial + * content. This function : * Syncs headers and bodys "natural widths and + * saves the values. * Sets proper width and height * Makes deferred request + * to get some cache rows + */ + private void sizeInit() { + /* + * We will use browsers table rendering algorithm to find proper column + * widths. If content and header take less space than available, we will + * divide extra space relatively to each column which has not width set. + * + * Overflow pixels are added to last column. + */ + + Iterator<Widget> headCells = tHead.iterator(); + int i = 0; + int totalExplicitColumnsWidths = 0; + int total = 0; + float expandRatioDivider = 0; + + final int[] widths = new int[tHead.visibleCells.size()]; + + tHead.enableBrowserIntelligence(); + // first loop: collect natural widths + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + int w = hCell.getWidth(); + if (hCell.isDefinedWidth()) { + // server has defined column width explicitly + totalExplicitColumnsWidths += w; + } else { + if (hCell.getExpandRatio() > 0) { + expandRatioDivider += hCell.getExpandRatio(); + w = 0; + } else { + // get and store greater of header width and column width, + // and + // store it as a minimumn natural col width + w = hCell.getNaturalColumnWidth(i); + } + hCell.setNaturalMinimumColumnWidth(w); + } + widths[i] = w; + total += w; + i++; + } + + tHead.disableBrowserIntelligence(); + + boolean willHaveScrollbarz = willHaveScrollbars(); + + // fix "natural" width if width not set + if (width == null || "".equals(width)) { + int w = total; + w += tBody.getCellExtraWidth() * visibleColOrder.length; + if (willHaveScrollbarz) { + w += Util.getNativeScrollbarSize(); + } + setContentWidth(w); + } + + int availW = tBody.getAvailableWidth(); + if (BrowserInfo.get().isIE()) { + // Hey IE, are you really sure about this? + availW = tBody.getAvailableWidth(); + } + availW -= tBody.getCellExtraWidth() * visibleColOrder.length; + + if (willHaveScrollbarz) { + availW -= Util.getNativeScrollbarSize(); + } + + boolean needsReLayout = false; + + if (availW > total) { + // natural size is smaller than available space + final int extraSpace = availW - total; + final int totalWidthR = total - totalExplicitColumnsWidths; + if (totalWidthR > 0) { + needsReLayout = true; + + if (expandRatioDivider > 0) { + // visible columns have some active expand ratios, excess + // space is divided according to them + headCells = tHead.iterator(); + i = 0; + while (headCells.hasNext()) { + HeaderCell hCell = (HeaderCell) headCells.next(); + if (hCell.getExpandRatio() > 0) { + int w = widths[i]; + final int newSpace = (int) (extraSpace * (hCell + .getExpandRatio() / expandRatioDivider)); + w += newSpace; + widths[i] = w; + } + i++; + } + } else { + // now we will share this sum relatively to those without + // explicit width + headCells = tHead.iterator(); + i = 0; + while (headCells.hasNext()) { + HeaderCell hCell = (HeaderCell) headCells.next(); + if (!hCell.isDefinedWidth()) { + int w = widths[i]; + final int newSpace = extraSpace * w / totalWidthR; + w += newSpace; + widths[i] = w; + } + i++; + } + } + } + + } else { + // bodys size will be more than available and scrollbar will appear + } + + // last loop: set possibly modified values or reset if new tBody + i = 0; + headCells = tHead.iterator(); + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + if (isNewBody || hCell.getWidth() == -1) { + final int w = widths[i]; + setColWidth(i, w, false); + } + i++; + } + if (needsReLayout) { + tBody.reLayoutComponents(); + } + + /* + * Fix "natural" height if height is not set. This must be after width + * fixing so the components' widths have been adjusted. + */ + if (height == null || "".equals(height)) { + /* + * We must force an update of the row height as this point as it + * might have been (incorrectly) calculated earlier + */ + if (pageLength == totalRows) { + /* + * A hack to support variable height rows when paging is off. + * Generally this is not supported by scrolltable. We want to + * show all rows so the bodyHeight should be equal to the table + * height. + */ + int bodyHeight = tBody.getOffsetHeight(); + bodyContainer.setHeight(bodyHeight + "px"); + Util.runWebkitOverflowAutoFix(bodyContainer.getElement()); + } else { + int bodyHeight = (tBody.getRowHeight(true) * pageLength); + bodyContainer.setHeight(bodyHeight + "px"); + } + } + + isNewBody = false; + + if (firstvisible > 0) { + // Deferred due some Firefox oddities. IE & Safari could survive + // without + DeferredCommand.addCommand(new Command() { + public void execute() { + bodyContainer.setScrollPosition(firstvisible + * tBody.getRowHeight()); + firstRowInViewPort = firstvisible; + } + }); + } + + if (enabled) { + // Do we need cache rows + if (tBody.getLastRendered() + 1 < firstRowInViewPort + pageLength + + CACHE_REACT_RATE * pageLength) { + if (totalRows - 1 > tBody.getLastRendered()) { + // fetch cache rows + rowRequestHandler + .setReqFirstRow(tBody.getLastRendered() + 1); + rowRequestHandler + .setReqRows((int) (pageLength * CACHE_RATE)); + rowRequestHandler.deferRowFetch(1); + } + } + } + initializedAndAttached = true; + } + + private boolean willHaveScrollbars() { + if (!(height != null && !height.equals(""))) { + if (pageLength < totalRows) { + return true; + } + } else { + int fakeheight = tBody.getRowHeight() * totalRows; + int availableHeight = bodyContainer.getElement().getPropertyInt( + "clientHeight"); + if (fakeheight > availableHeight) { + return true; + } + } + return false; + } + + /** + * This method has logic which rows needs to be requested from server when + * user scrolls + */ + public void onScroll(Widget widget, int scrollLeft, int scrollTop) { + if (!initializedAndAttached) { + return; + } + if (!enabled) { + bodyContainer.setScrollPosition(firstRowInViewPort + * tBody.getRowHeight()); + return; + } + + rowRequestHandler.cancel(); + + // fix headers horizontal scrolling + tHead.setHorizontalScrollPosition(scrollLeft); + + firstRowInViewPort = (int) Math.ceil(scrollTop + / (double) tBody.getRowHeight()); + + int postLimit = (int) (firstRowInViewPort + pageLength + pageLength + * CACHE_REACT_RATE); + if (postLimit > totalRows - 1) { + postLimit = totalRows - 1; + } + int preLimit = (int) (firstRowInViewPort - pageLength + * CACHE_REACT_RATE); + if (preLimit < 0) { + preLimit = 0; + } + final int lastRendered = tBody.getLastRendered(); + final int firstRendered = tBody.getFirstRendered(); + + if (postLimit <= lastRendered && preLimit >= firstRendered) { + // remember which firstvisible we requested, in case the server has + // a differing opinion + lastRequestedFirstvisible = firstRowInViewPort; + client.updateVariable(paintableId, "firstvisible", + firstRowInViewPort, false); + return; // scrolled withing "non-react area" + } + + if (firstRowInViewPort - pageLength * CACHE_RATE > lastRendered + || firstRowInViewPort + pageLength + pageLength * CACHE_RATE < firstRendered) { + // need a totally new set + rowRequestHandler + .setReqFirstRow((int) (firstRowInViewPort - pageLength + * CACHE_RATE)); + int last = firstRowInViewPort + (int) CACHE_RATE * pageLength + + pageLength; + if (last > totalRows) { + last = totalRows - 1; + } + rowRequestHandler.setReqRows(last + - rowRequestHandler.getReqFirstRow() + 1); + rowRequestHandler.deferRowFetch(); + return; + } + if (preLimit < firstRendered) { + // need some rows to the beginning of the rendered area + rowRequestHandler + .setReqFirstRow((int) (firstRowInViewPort - pageLength + * CACHE_RATE)); + rowRequestHandler.setReqRows(firstRendered + - rowRequestHandler.getReqFirstRow()); + rowRequestHandler.deferRowFetch(); + + return; + } + if (postLimit > lastRendered) { + // need some rows to the end of the rendered area + rowRequestHandler.setReqFirstRow(lastRendered + 1); + rowRequestHandler.setReqRows((int) ((firstRowInViewPort + + pageLength + pageLength * CACHE_RATE) - lastRendered)); + rowRequestHandler.deferRowFetch(); + } + + } + + private void announceScrollPosition() { + if (scrollPositionElement == null) { + scrollPositionElement = DOM.createDiv(); + DOM.setElementProperty(scrollPositionElement, "className", + "i-table-scrollposition"); + DOM.appendChild(getElement(), scrollPositionElement); + } + + DOM.setStyleAttribute(scrollPositionElement, "position", "absolute"); + DOM.setStyleAttribute(scrollPositionElement, "marginLeft", (DOM + .getElementPropertyInt(getElement(), "offsetWidth") / 2 - 80) + + "px"); + DOM.setStyleAttribute(scrollPositionElement, "marginTop", -(DOM + .getElementPropertyInt(getElement(), "offsetHeight") - 2) + + "px"); + + // indexes go from 1-totalRows, as rowheaders in index-mode indicate + int last = (firstRowInViewPort + (bodyContainer.getOffsetHeight() / tBody + .getRowHeight())); + if (last > totalRows) { + last = totalRows; + } + DOM.setInnerHTML(scrollPositionElement, "<span>" + + (firstRowInViewPort + 1) + " – " + last + "..." + + "</span>"); + DOM.setStyleAttribute(scrollPositionElement, "display", "block"); + } + + private void hideScrollPositionAnnotation() { + if (scrollPositionElement != null) { + DOM.setStyleAttribute(scrollPositionElement, "display", "none"); + } + } + + private class RowRequestHandler extends Timer { + + private int reqFirstRow = 0; + private int reqRows = 0; + + public void deferRowFetch() { + deferRowFetch(250); + } + + public void deferRowFetch(int msec) { + if (reqRows > 0 && reqFirstRow < totalRows) { + schedule(msec); + + // tell scroll position to user if currently "visible" rows are + // not rendered + if ((firstRowInViewPort + pageLength > tBody.getLastRendered()) + || (firstRowInViewPort < tBody.getFirstRendered())) { + announceScrollPosition(); + } else { + hideScrollPositionAnnotation(); + } + } + } + + public void setReqFirstRow(int reqFirstRow) { + if (reqFirstRow < 0) { + reqFirstRow = 0; + } else if (reqFirstRow >= totalRows) { + reqFirstRow = totalRows - 1; + } + this.reqFirstRow = reqFirstRow; + } + + public void setReqRows(int reqRows) { + this.reqRows = reqRows; + } + + @Override + public void run() { + if (client.hasActiveRequest()) { + // if client connection is busy, don't bother loading it more + schedule(250); + + } else { + + int firstToBeRendered = tBody.firstRendered; + if (reqFirstRow < firstToBeRendered) { + firstToBeRendered = reqFirstRow; + } else if (firstRowInViewPort - (int) (CACHE_RATE * pageLength) > firstToBeRendered) { + firstToBeRendered = firstRowInViewPort + - (int) (CACHE_RATE * pageLength); + if (firstToBeRendered < 0) { + firstToBeRendered = 0; + } + } + + int lastToBeRendered = tBody.lastRendered; + + if (reqFirstRow + reqRows - 1 > lastToBeRendered) { + lastToBeRendered = reqFirstRow + reqRows - 1; + } else if (firstRowInViewPort + pageLength + pageLength + * CACHE_RATE < lastToBeRendered) { + lastToBeRendered = (firstRowInViewPort + pageLength + (int) (pageLength * CACHE_RATE)); + if (lastToBeRendered >= totalRows) { + lastToBeRendered = totalRows - 1; + } + // due Safari 3.1 bug (see #2607), verify reqrows, original + // problem unknown, but this should catch the issue + if (reqFirstRow + reqRows - 1 > lastToBeRendered) { + reqRows = lastToBeRendered - reqFirstRow; + } + } + + client.updateVariable(paintableId, "firstToBeRendered", + firstToBeRendered, false); + + client.updateVariable(paintableId, "lastToBeRendered", + lastToBeRendered, false); + // remember which firstvisible we requested, in case the server + // has + // a differing opinion + lastRequestedFirstvisible = firstRowInViewPort; + client.updateVariable(paintableId, "firstvisible", + firstRowInViewPort, false); + client.updateVariable(paintableId, "reqfirstrow", reqFirstRow, + false); + client.updateVariable(paintableId, "reqrows", reqRows, true); + + } + } + + public int getReqFirstRow() { + return reqFirstRow; + } + + public int getReqRows() { + return reqRows; + } + + /** + * Sends request to refresh content at this position. + */ + public void refreshContent() { + int first = (int) (firstRowInViewPort - pageLength * CACHE_RATE); + int reqRows = (int) (2 * pageLength * CACHE_RATE + pageLength); + if (first < 0) { + reqRows = reqRows + first; + first = 0; + } + setReqFirstRow(first); + setReqRows(reqRows); + run(); + } + } + + public class HeaderCell extends Widget { + + Element td = DOM.createTD(); + + Element captionContainer = DOM.createDiv(); + + Element colResizeWidget = DOM.createDiv(); + + Element floatingCopyOfHeaderCell; + + private boolean sortable = false; + private final String cid; + private boolean dragging; + + private int dragStartX; + private int colIndex; + private int originalWidth; + + private boolean isResizing; + + private int headerX; + + private boolean moved; + + private int closestSlot; + + private int width = -1; + + private int naturalWidth = -1; + + private char align = ALIGN_LEFT; + + boolean definedWidth = false; + + private float expandRatio = 0; + + public void setSortable(boolean b) { + sortable = b; + } + + public void setNaturalMinimumColumnWidth(int w) { + naturalWidth = w; + } + + public HeaderCell(String colId, String headerText) { + cid = colId; + + DOM.setElementProperty(colResizeWidget, "className", CLASSNAME + + "-resizer"); + DOM.sinkEvents(colResizeWidget, Event.MOUSEEVENTS); + + setText(headerText); + + DOM.appendChild(td, colResizeWidget); + + DOM.setElementProperty(captionContainer, "className", CLASSNAME + + "-caption-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); + } + + public void setWidth(int w, boolean ensureDefinedWidth) { + if (ensureDefinedWidth) { + definedWidth = true; + // on column resize expand ratio becomes zero + expandRatio = 0; + } + if (width == w) { + return; + } + if (width == -1) { + // go to default mode, clip content if necessary + DOM.setStyleAttribute(captionContainer, "overflow", ""); + } + width = w; + if (w == -1) { + DOM.setStyleAttribute(captionContainer, "width", ""); + setWidth(""); + } else { + + captionContainer.getStyle().setPropertyPx("width", w); + + /* + * 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 (tBody != null) { + int tdWidth = width + tBody.getCellExtraWidth(); + setWidth(tdWidth + "px"); + } else { + DeferredCommand.addCommand(new Command() { + public void execute() { + int tdWidth = width + tBody.getCellExtraWidth(); + setWidth(tdWidth + "px"); + } + }); + } + } + } + + public void setUndefinedWidth() { + definedWidth = false; + setWidth(-1, false); + } + + /** + * Detects if width is fixed by developer on server side or resized to + * current width by user. + * + * @return true if defined, false if "natural" width + */ + public boolean isDefinedWidth() { + return definedWidth; + } + + public int getWidth() { + return width; + } + + public void setText(String headerText) { + DOM.setInnerHTML(captionContainer, headerText); + } + + public String getColKey() { + return cid; + } + + private void setSorted(boolean sorted) { + if (sorted) { + if (sortAscending) { + this.setStyleName(CLASSNAME + "-header-cell-asc"); + } else { + this.setStyleName(CLASSNAME + "-header-cell-desc"); + } + } else { + this.setStyleName(CLASSNAME + "-header-cell"); + } + } + + /** + * Handle column reordering. + */ + @Override + public void onBrowserEvent(Event event) { + if (enabled && event != null) { + if (isResizing || event.getTarget() == colResizeWidget) { + onResizeEvent(event); + } else { + handleCaptionEvent(event); + } + } + } + + private void createFloatingCopy() { + floatingCopyOfHeaderCell = DOM.createDiv(); + DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td)); + floatingCopyOfHeaderCell = DOM + .getChild(floatingCopyOfHeaderCell, 1); + DOM.setElementProperty(floatingCopyOfHeaderCell, "className", + CLASSNAME + "-header-drag"); + updateFloatingCopysPosition(DOM.getAbsoluteLeft(td), DOM + .getAbsoluteTop(td)); + DOM.appendChild(RootPanel.get().getElement(), + floatingCopyOfHeaderCell); + } + + private void updateFloatingCopysPosition(int x, int y) { + x -= DOM.getElementPropertyInt(floatingCopyOfHeaderCell, + "offsetWidth") / 2; + DOM.setStyleAttribute(floatingCopyOfHeaderCell, "left", x + "px"); + if (y > 0) { + DOM.setStyleAttribute(floatingCopyOfHeaderCell, "top", (y + 7) + + "px"); + } + } + + private void hideFloatingCopy() { + DOM.removeChild(RootPanel.get().getElement(), + floatingCopyOfHeaderCell); + floatingCopyOfHeaderCell = null; + } + + protected void handleCaptionEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEDOWN: + if (columnReordering) { + dragging = true; + moved = false; + colIndex = getColIndexByKey(cid); + DOM.setCapture(getElement()); + headerX = tHead.getAbsoluteLeft(); + DOM.eventPreventDefault(event); // prevent selecting text + } + break; + case Event.ONMOUSEUP: + if (columnReordering) { + dragging = false; + DOM.releaseCapture(getElement()); + if (moved) { + hideFloatingCopy(); + tHead.removeSlotFocus(); + if (closestSlot != colIndex + && closestSlot != (colIndex + 1)) { + if (closestSlot > colIndex) { + reOrderColumn(cid, closestSlot - 1); + } else { + reOrderColumn(cid, closestSlot); + } + } + } + } + + if (!moved) { + // mouse event was a click to header -> sort column + if (sortable) { + if (sortColumn.equals(cid)) { + // just toggle order + client.updateVariable(paintableId, "sortascending", + !sortAscending, false); + } else { + // set table scrolled by this column + client.updateVariable(paintableId, "sortcolumn", + cid, false); + } + // get also cache columns at the same request + bodyContainer.setScrollPosition(0); + firstvisible = 0; + rowRequestHandler.setReqFirstRow(0); + rowRequestHandler.setReqRows((int) (2 * pageLength + * CACHE_RATE + pageLength)); + rowRequestHandler.deferRowFetch(); + } + break; + } + break; + case Event.ONMOUSEMOVE: + if (dragging) { + if (!moved) { + createFloatingCopy(); + moved = true; + } + final int x = DOM.eventGetClientX(event) + + DOM.getElementPropertyInt(tHead.hTableWrapper, + "scrollLeft"); + int slotX = headerX; + closestSlot = colIndex; + int closestDistance = -1; + int start = 0; + if (showRowHeaders) { + start++; + } + final int visibleCellCount = tHead.getVisibleCellCount(); + for (int i = start; i <= visibleCellCount; i++) { + if (i > 0) { + final String colKey = getColKeyByIndex(i - 1); + slotX += getColWidth(colKey); + } + final int dist = Math.abs(x - slotX); + if (closestDistance == -1 || dist < closestDistance) { + closestDistance = dist; + closestSlot = i; + } + } + tHead.focusSlot(closestSlot); + + updateFloatingCopysPosition(DOM.eventGetClientX(event), -1); + } + break; + default: + break; + } + } + + private void onResizeEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEDOWN: + isResizing = true; + DOM.setCapture(getElement()); + dragStartX = DOM.eventGetClientX(event); + colIndex = getColIndexByKey(cid); + originalWidth = getWidth(); + DOM.eventPreventDefault(event); + break; + case Event.ONMOUSEUP: + isResizing = false; + DOM.releaseCapture(getElement()); + // readjust undefined width columns + lazyAdjustColumnWidths.cancel(); + lazyAdjustColumnWidths.schedule(1); + break; + case Event.ONMOUSEMOVE: + if (isResizing) { + final int deltaX = DOM.eventGetClientX(event) - dragStartX; + if (deltaX == 0) { + return; + } + + int newWidth = originalWidth + deltaX; + if (newWidth < tBody.getCellExtraWidth()) { + newWidth = tBody.getCellExtraWidth(); + } + setColWidth(colIndex, newWidth, true); + } + break; + default: + break; + } + } + + public String getCaption() { + return DOM.getInnerText(captionContainer); + } + + public boolean isEnabled() { + return getParent() != null; + } + + 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; + } + + public char getAlign() { + return align; + } + + /** + * Detects the natural minimum width for the column of this header cell. + * If column is resized by user or the width is defined by server the + * actual width is returned. Else the natural min width is returned. + * + * @param columnIndex + * column index hint, if -1 (unknown) it will be detected + * + * @return + */ + public int getNaturalColumnWidth(int columnIndex) { + if (isDefinedWidth()) { + return width; + } else { + if (naturalWidth < 0) { + // This is recently revealed column. Try to detect a proper + // value (greater of header and data + // cols) + + final int hw = ((Element) getElement().getLastChild()) + .getOffsetWidth() + + tBody.getCellExtraWidth(); + if (columnIndex < 0) { + columnIndex = 0; + for (Iterator<Widget> it = tHead.iterator(); it + .hasNext(); columnIndex++) { + if (it.next() == this) { + break; + } + } + } + final int cw = tBody.getColWidth(columnIndex); + naturalWidth = (hw > cw ? hw : cw); + } + return naturalWidth; + } + } + + public void setExpandRatio(float floatAttribute) { + expandRatio = floatAttribute; + } + + public float getExpandRatio() { + return expandRatio; + } + + } + + /** + * HeaderCell that is header cell for row headers. + * + * Reordering disabled and clicking on it resets sorting. + */ + public class RowHeadersHeaderCell extends HeaderCell { + + RowHeadersHeaderCell() { + super("0", ""); + } + + @Override + protected void handleCaptionEvent(Event event) { + // NOP: RowHeaders cannot be reordered + // TODO It'd be nice to reset sorting here + } + } + + public class TableHead extends Panel implements ActionOwner { + + private static final int WRAPPER_WIDTH = 9000; + + Vector<Widget> visibleCells = new Vector<Widget>(); + + HashMap<String, HeaderCell> availableCells = new HashMap<String, HeaderCell>(); + + Element div = DOM.createDiv(); + Element hTableWrapper = DOM.createDiv(); + Element hTableContainer = DOM.createDiv(); + Element table = DOM.createTable(); + Element headerTableBody = DOM.createTBody(); + Element tr = DOM.createTR(); + + private final Element columnSelector = DOM.createDiv(); + + private int focusedSlot = -1; + + public TableHead() { + if (BrowserInfo.get().isIE()) { + table.setPropertyInt("cellSpacing", 0); + } + + DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden"); + DOM.setElementProperty(hTableWrapper, "className", CLASSNAME + + "-header"); + + // TODO move styles to CSS + DOM.setElementProperty(columnSelector, "className", CLASSNAME + + "-column-selector"); + DOM.setStyleAttribute(columnSelector, "display", "none"); + + DOM.appendChild(table, headerTableBody); + DOM.appendChild(headerTableBody, tr); + DOM.appendChild(hTableContainer, table); + DOM.appendChild(hTableWrapper, hTableContainer); + DOM.appendChild(div, hTableWrapper); + DOM.appendChild(div, columnSelector); + setElement(div); + + setStyleName(CLASSNAME + "-header-wrap"); + + DOM.sinkEvents(columnSelector, Event.ONCLICK); + + availableCells.put("0", new RowHeadersHeaderCell()); + } + + @Override + public void clear() { + for (String cid : availableCells.keySet()) { + removeCell(cid); + } + availableCells.clear(); + availableCells.put("0", new RowHeadersHeaderCell()); + } + + public void updateCellsFromUIDL(UIDL uidl) { + Iterator<?> it = uidl.getChildIterator(); + HashSet<String> updated = new HashSet<String>(); + updated.add("0"); + while (it.hasNext()) { + final UIDL col = (UIDL) it.next(); + final String cid = col.getStringAttribute("cid"); + updated.add(cid); + + String caption = buildCaptionHtmlSnippet(col); + HeaderCell c = getHeaderCell(cid); + if (c == null) { + c = new HeaderCell(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("sortable")) { + c.setSortable(true); + if (cid.equals(sortColumn)) { + c.setSorted(true); + } else { + c.setSorted(false); + } + } else { + c.setSortable(false); + } + + 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")); + } + } + // check for orphaned header cells + for (String cid : availableCells.keySet()) { + if (!updated.contains(cid)) { + removeCell(cid); + it.remove(); + } + } + + } + + public void enableColumn(String cid, int index) { + final HeaderCell c = getHeaderCell(cid); + if (!c.isEnabled() || getHeaderCell(index) != c) { + setHeaderCell(index, c); + if (initializedAndAttached) { + headerChangedDuringUpdate = true; + } + } + } + + public int getVisibleCellCount() { + return visibleCells.size(); + } + + public void setHorizontalScrollPosition(int scrollLeft) { + DOM.setElementPropertyInt(hTableWrapper, "scrollLeft", scrollLeft); + } + + public void setColumnCollapsingAllowed(boolean cc) { + if (cc) { + DOM.setStyleAttribute(columnSelector, "display", "block"); + } else { + DOM.setStyleAttribute(columnSelector, "display", "none"); + } + } + + public void disableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH + + "px"); + } + + public void enableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", ""); + } + + public void setHeaderCell(int index, HeaderCell 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.insertElementAt(cell, index); + + } 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"); + } + } + + public HeaderCell getHeaderCell(int index) { + if (index < visibleCells.size()) { + return (HeaderCell) visibleCells.get(index); + } else { + return null; + } + } + + /** + * Get's HeaderCell by it's column Key. + * + * Note that this returns HeaderCell even if it is currently collapsed. + * + * @param cid + * Column key of accessed HeaderCell + * @return HeaderCell + */ + public HeaderCell getHeaderCell(String cid) { + return availableCells.get(cid); + } + + public void moveCell(int oldIndex, int newIndex) { + final HeaderCell hCell = getHeaderCell(oldIndex); + final Element cell = hCell.getElement(); + + visibleCells.remove(oldIndex); + DOM.removeChild(tr, cell); + + DOM.insertChild(tr, cell, newIndex); + visibleCells.insertElementAt(hCell, newIndex); + } + + public Iterator<Widget> iterator() { + return visibleCells.iterator(); + } + + @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; + } + + public void removeCell(String colKey) { + final HeaderCell c = getHeaderCell(colKey); + remove(c); + } + + private void focusSlot(int index) { + removeSlotFocus(); + if (index > 0) { + DOM.setElementProperty(DOM.getFirstChild(DOM.getChild(tr, + index - 1)), "className", CLASSNAME + "-resizer " + + CLASSNAME + "-focus-slot-right"); + } else { + DOM.setElementProperty(DOM.getFirstChild(DOM + .getChild(tr, index)), "className", CLASSNAME + + "-resizer " + CLASSNAME + "-focus-slot-left"); + } + focusedSlot = index; + } + + private void removeSlotFocus() { + if (focusedSlot < 0) { + return; + } + if (focusedSlot == 0) { + DOM.setElementProperty(DOM.getFirstChild(DOM.getChild(tr, + focusedSlot)), "className", CLASSNAME + "-resizer"); + } else if (focusedSlot > 0) { + DOM.setElementProperty(DOM.getFirstChild(DOM.getChild(tr, + focusedSlot - 1)), "className", CLASSNAME + "-resizer"); + } + focusedSlot = -1; + } + + @Override + public void onBrowserEvent(Event event) { + if (enabled) { + if (event.getTarget() == columnSelector) { + final int left = DOM.getAbsoluteLeft(columnSelector); + final int top = DOM.getAbsoluteTop(columnSelector) + + DOM.getElementPropertyInt(columnSelector, + "offsetHeight"); + client.getContextMenu().showAt(this, left, top); + } + } + } + + class VisibleColumnAction extends Action { + + String colKey; + private boolean collapsed; + + public VisibleColumnAction(String colKey) { + super(IScrollTable.TableHead.this); + this.colKey = colKey; + caption = tHead.getHeaderCell(colKey).getCaption(); + } + + @Override + public void execute() { + client.getContextMenu().hide(); + // toggle selected column + if (collapsedColumns.contains(colKey)) { + collapsedColumns.remove(colKey); + } else { + tHead.removeCell(colKey); + collapsedColumns.add(colKey); + lazyAdjustColumnWidths.schedule(1); + } + + // update variable to server + client.updateVariable(paintableId, "collapsedcolumns", + collapsedColumns.toArray(), false); + // let rowRequestHandler determine proper rows + rowRequestHandler.refreshContent(); + } + + public void setCollapsed(boolean b) { + collapsed = b; + } + + /** + * Override default method to distinguish on/off columns + */ + @Override + public String getHTML() { + final StringBuffer buf = new StringBuffer(); + if (collapsed) { + buf.append("<span class=\"i-off\">"); + } else { + buf.append("<span class=\"i-on\">"); + } + buf.append(super.getHTML()); + buf.append("</span>"); + + return buf.toString(); + } + + } + + /* + * Returns columns as Action array for column select popup + */ + public Action[] getActions() { + Object[] cols; + if (columnReordering) { + cols = columnOrder; + } else { + // if columnReordering is disabled, we need different way to get + // all available columns + cols = visibleColOrder; + cols = new Object[visibleColOrder.length + + collapsedColumns.size()]; + int i; + for (i = 0; i < visibleColOrder.length; i++) { + cols[i] = visibleColOrder[i]; + } + for (final Iterator<String> it = collapsedColumns.iterator(); it + .hasNext();) { + cols[i++] = it.next(); + } + } + final Action[] actions = new Action[cols.length]; + + for (int i = 0; i < cols.length; i++) { + final String cid = (String) cols[i]; + final HeaderCell c = getHeaderCell(cid); + final VisibleColumnAction a = new VisibleColumnAction(c + .getColKey()); + a.setCaption(c.getCaption()); + if (!c.isEnabled()) { + a.setCollapsed(true); + } + actions[i] = a; + } + return actions; + } + + public ApplicationConnection getClient() { + return client; + } + + public String getPaintableId() { + return paintableId; + } + + /** + * Returns column alignments for visible columns + */ + public char[] getColumnAlignments() { + final Iterator<Widget> it = visibleCells.iterator(); + final char[] aligns = new char[visibleCells.size()]; + int colIndex = 0; + while (it.hasNext()) { + aligns[colIndex++] = ((HeaderCell) it.next()).getAlign(); + } + return aligns; + } + + } + + /** + * This Panel can only contain IScrollTableRow type of widgets. This + * "simulates" very large table, keeping spacers which take room of + * unrendered rows. + * + */ + public class IScrollTableBody extends Panel { + + public static final int DEFAULT_ROW_HEIGHT = 24; + + private int rowHeight = -1; + + private final List<Widget> renderedRows = new Vector<Widget>(); + + /** + * Due some optimizations row height measuring is deferred and initial + * set of rows is rendered detached. Flag set on when table body has + * been attached in dom and rowheight has been measured. + */ + private boolean tBodyMeasurementsDone = false; + + Element preSpacer = DOM.createDiv(); + Element postSpacer = DOM.createDiv(); + + Element container = DOM.createDiv(); + + TableSectionElement tBodyElement = Document.get().createTBodyElement(); + Element table = DOM.createTable(); + + private int firstRendered; + + private int lastRendered; + + private char[] aligns; + + IScrollTableBody() { + constructDOM(); + + setElement(container); + } + + private void constructDOM() { + DOM.setElementProperty(table, "className", CLASSNAME + "-table"); + if (BrowserInfo.get().isIE()) { + table.setPropertyInt("cellSpacing", 0); + } + DOM.setElementProperty(preSpacer, "className", CLASSNAME + + "-row-spacer"); + DOM.setElementProperty(postSpacer, "className", CLASSNAME + + "-row-spacer"); + + table.appendChild(tBodyElement); + DOM.appendChild(container, preSpacer); + DOM.appendChild(container, table); + DOM.appendChild(container, postSpacer); + + } + + public int getAvailableWidth() { + int availW = bodyContainer.getOffsetWidth() - getBorderWidth(); + return availW; + } + + public void renderInitialRows(UIDL rowData, int firstIndex, int rows) { + firstRendered = firstIndex; + lastRendered = firstIndex + rows - 1; + final Iterator<?> it = rowData.getChildIterator(); + aligns = tHead.getColumnAlignments(); + while (it.hasNext()) { + final IScrollTableRow row = new IScrollTableRow((UIDL) it + .next(), aligns); + addRow(row); + } + if (isAttached()) { + fixSpacers(); + } + } + + public void renderRows(UIDL rowData, int firstIndex, int rows) { + // FIXME REVIEW + aligns = tHead.getColumnAlignments(); + final Iterator<?> it = rowData.getChildIterator(); + if (firstIndex == lastRendered + 1) { + while (it.hasNext()) { + final IScrollTableRow row = createRow((UIDL) it.next()); + addRow(row); + lastRendered++; + } + fixSpacers(); + } else if (firstIndex + rows == firstRendered) { + final IScrollTableRow[] rowArray = new IScrollTableRow[rows]; + int i = rows; + while (it.hasNext()) { + i--; + rowArray[i] = createRow((UIDL) it.next()); + } + for (i = 0; i < rows; i++) { + addRowBeforeFirstRendered(rowArray[i]); + firstRendered--; + } + } else { + // completely new set of rows + while (lastRendered + 1 > firstRendered) { + unlinkRow(false); + } + final IScrollTableRow row = createRow((UIDL) it.next()); + firstRendered = firstIndex; + lastRendered = firstIndex - 1; + addRow(row); + lastRendered++; + setContainerHeight(); + fixSpacers(); + while (it.hasNext()) { + addRow(createRow((UIDL) it.next())); + lastRendered++; + } + fixSpacers(); + } + // this may be a new set of rows due content change, + // ensure we have proper cache rows + int reactFirstRow = (int) (firstRowInViewPort - pageLength + * CACHE_REACT_RATE); + int reactLastRow = (int) (firstRowInViewPort + pageLength + pageLength + * CACHE_REACT_RATE); + if (reactFirstRow < 0) { + reactFirstRow = 0; + } + if (reactLastRow > totalRows) { + reactLastRow = totalRows - 1; + } + if (lastRendered < reactLastRow) { + // get some cache rows below visible area + rowRequestHandler.setReqFirstRow(lastRendered + 1); + rowRequestHandler.setReqRows(reactLastRow - lastRendered - 1); + rowRequestHandler.deferRowFetch(1); + } else if (tBody.getFirstRendered() > reactFirstRow) { + /* + * Branch for fetching cache above visible area. + * + * If cache needed for both before and after visible area, this + * will be rendered after-cache is reveived and rendered. So in + * some rare situations table may take two cache visits to + * server. + */ + rowRequestHandler.setReqFirstRow(reactFirstRow); + rowRequestHandler.setReqRows(firstRendered - reactFirstRow); + rowRequestHandler.deferRowFetch(1); + } + } + + /** + * This method is used to instantiate new rows for this table. It + * automatically sets correct widths to rows cells and assigns correct + * client reference for child widgets. + * + * This method can be called only after table has been initialized + * + * @param uidl + */ + private IScrollTableRow createRow(UIDL uidl) { + final IScrollTableRow row = new IScrollTableRow(uidl, aligns); + final int cells = DOM.getChildCount(row.getElement()); + for (int i = 0; i < cells; i++) { + final Element cell = DOM.getChild(row.getElement(), i); + int w = IScrollTable.this.getColWidth(getColKeyByIndex(i)); + if (w < 0) { + w = 0; + } + cell.getFirstChildElement().getStyle() + .setPropertyPx("width", w); + cell.getStyle().setPropertyPx("width", w); + } + return row; + } + + private void addRowBeforeFirstRendered(IScrollTableRow row) { + IScrollTableRow first = null; + if (renderedRows.size() > 0) { + first = (IScrollTableRow) renderedRows.get(0); + } + if (first != null && first.getStyleName().indexOf("-odd") == -1) { + row.addStyleName(CLASSNAME + "-row-odd"); + } else { + row.addStyleName(CLASSNAME + "-row"); + } + if (row.isSelected()) { + row.addStyleName("i-selected"); + } + tBodyElement.insertBefore(row.getElement(), tBodyElement + .getFirstChild()); + adopt(row); + renderedRows.add(0, row); + } + + private void addRow(IScrollTableRow row) { + IScrollTableRow last = null; + if (renderedRows.size() > 0) { + last = (IScrollTableRow) renderedRows + .get(renderedRows.size() - 1); + } + if (last != null && last.getStyleName().indexOf("-odd") == -1) { + row.addStyleName(CLASSNAME + "-row-odd"); + } else { + row.addStyleName(CLASSNAME + "-row"); + } + if (row.isSelected()) { + row.addStyleName("i-selected"); + } + tBodyElement.appendChild(row.getElement()); + adopt(row); + renderedRows.add(row); + } + + public Iterator<Widget> iterator() { + return renderedRows.iterator(); + } + + /** + * @return false if couldn't remove row + */ + public boolean unlinkRow(boolean fromBeginning) { + if (lastRendered - firstRendered < 0) { + return false; + } + int index; + if (fromBeginning) { + index = 0; + firstRendered++; + } else { + index = renderedRows.size() - 1; + lastRendered--; + } + if (index >= 0) { + final IScrollTableRow toBeRemoved = (IScrollTableRow) renderedRows + .get(index); + lazyUnregistryBag.add(toBeRemoved); + tBodyElement.removeChild(toBeRemoved.getElement()); + orphan(toBeRemoved); + renderedRows.remove(index); + fixSpacers(); + return true; + } else { + return false; + } + } + + @Override + public boolean remove(Widget w) { + throw new UnsupportedOperationException(); + } + + @Override + protected void onAttach() { + super.onAttach(); + setContainerHeight(); + } + + /** + * Fix container blocks height according to totalRows to avoid + * "bouncing" when scrolling + */ + private void setContainerHeight() { + fixSpacers(); + DOM.setStyleAttribute(container, "height", totalRows + * getRowHeight() + "px"); + } + + private void fixSpacers() { + int prepx = getRowHeight() * firstRendered; + if (prepx < 0) { + prepx = 0; + } + DOM.setStyleAttribute(preSpacer, "height", prepx + "px"); + int postpx = getRowHeight() * (totalRows - 1 - lastRendered); + if (postpx < 0) { + postpx = 0; + } + DOM.setStyleAttribute(postSpacer, "height", postpx + "px"); + } + + public int getRowHeight() { + return getRowHeight(false); + } + + public int getRowHeight(boolean forceUpdate) { + if (tBodyMeasurementsDone && !forceUpdate) { + return rowHeight; + } else { + + if (tBodyElement.getRows().getLength() > 0) { + rowHeight = getTableHeight() + / tBodyElement.getRows().getLength(); + } else { + if (isAttached()) { + // measure row height by adding a dummy row + IScrollTableRow scrollTableRow = new IScrollTableRow(); + tBodyElement.appendChild(scrollTableRow.getElement()); + getRowHeight(forceUpdate); + tBodyElement.removeChild(scrollTableRow.getElement()); + } else { + // TODO investigate if this can never happen anymore + return DEFAULT_ROW_HEIGHT; + } + } + tBodyMeasurementsDone = true; + return rowHeight; + } + } + + public int getTableHeight() { + return table.getOffsetHeight(); + } + + /** + * Returns the width available for column content. + * + * @param columnIndex + * @return + */ + public int getColWidth(int columnIndex) { + if (tBodyMeasurementsDone) { + NodeList<TableRowElement> rows = tBodyElement.getRows(); + if (rows.getLength() == 0) { + // no rows yet rendered + return 0; + } else { + com.google.gwt.dom.client.Element wrapperdiv = rows + .getItem(0).getCells().getItem(columnIndex) + .getFirstChildElement(); + return wrapperdiv.getOffsetWidth(); + } + } else { + return 0; + } + } + + /** + * Sets the content width of a column. + * + * Due IE limitation, we must set the width to a wrapper elements inside + * table cells (with overflow hidden, which does not work on td + * elements). + * + * To get this work properly crossplatform, we will also set the width + * of td. + * + * @param colIndex + * @param w + */ + public void setColWidth(int colIndex, int w) { + NodeList<TableRowElement> rows2 = tBodyElement.getRows(); + final int rows = rows2.getLength(); + for (int i = 0; i < rows; i++) { + TableRowElement row = rows2.getItem(i); + TableCellElement cell = row.getCells().getItem(colIndex); + cell.getFirstChildElement().getStyle() + .setPropertyPx("width", w); + cell.getStyle().setPropertyPx("width", w); + } + } + + private int cellExtraWidth = -1; + + /** + * Method to return the space used for cell paddings + border. + */ + private int getCellExtraWidth() { + if (cellExtraWidth < 0) { + detectExtrawidth(); + } + return cellExtraWidth; + } + + private void detectExtrawidth() { + NodeList<TableRowElement> rows = tBodyElement.getRows(); + if (rows.getLength() == 0) { + /* need to temporary add empty row and detect */ + IScrollTableRow scrollTableRow = new IScrollTableRow(); + tBodyElement.appendChild(scrollTableRow.getElement()); + detectExtrawidth(); + tBodyElement.removeChild(scrollTableRow.getElement()); + } else { + TableRowElement item = rows.getItem(0); + TableCellElement firstTD = item.getCells().getItem(0); + com.google.gwt.dom.client.Element wrapper = firstTD + .getFirstChildElement(); + cellExtraWidth = firstTD.getOffsetWidth() + - wrapper.getOffsetWidth(); + } + } + + private void reLayoutComponents() { + for (Widget w : this) { + IScrollTableRow r = (IScrollTableRow) w; + for (Widget widget : r) { + client.handleComponentRelativeSize(widget); + } + } + } + + public int getLastRendered() { + return lastRendered; + } + + public int getFirstRendered() { + return firstRendered; + } + + public void moveCol(int oldIndex, int newIndex) { + + // loop all rows and move given index to its new place + final Iterator<?> rows = iterator(); + while (rows.hasNext()) { + final IScrollTableRow row = (IScrollTableRow) rows.next(); + + final Element td = DOM.getChild(row.getElement(), oldIndex); + DOM.removeChild(row.getElement(), td); + + DOM.insertChild(row.getElement(), td, newIndex); + + } + + } + + public class IScrollTableRow extends Panel implements ActionOwner, + Container { + + Vector<Widget> childWidgets = new Vector<Widget>(); + private boolean selected = false; + private final int rowKey; + private List<UIDL> pendingComponentPaints; + + private String[] actionKeys = null; + private TableRowElement rowElement; + + private IScrollTableRow(int rowKey) { + this.rowKey = rowKey; + rowElement = Document.get().createTRElement(); + setElement(rowElement); + DOM.sinkEvents(getElement(), Event.ONCLICK | Event.ONDBLCLICK + | Event.ONCONTEXTMENU); + } + + private void paintComponent(Paintable p, UIDL uidl) { + if (isAttached()) { + p.updateFromUIDL(uidl, client); + } else { + if (pendingComponentPaints == null) { + pendingComponentPaints = new LinkedList<UIDL>(); + } + pendingComponentPaints.add(uidl); + } + } + + @Override + protected void onAttach() { + super.onAttach(); + if (pendingComponentPaints != null) { + for (UIDL uidl : pendingComponentPaints) { + Paintable paintable = client.getPaintable(uidl); + paintable.updateFromUIDL(uidl, client); + } + } + } + + public String getKey() { + return String.valueOf(rowKey); + } + + public IScrollTableRow(UIDL uidl, char[] aligns) { + this(uidl.getIntAttribute("key")); + + String rowStyle = uidl.getStringAttribute("rowstyle"); + if (rowStyle != null) { + addStyleName(CLASSNAME + "-row-" + rowStyle); + } + + tHead.getColumnAlignments(); + int col = 0; + int visibleColumnIndex = -1; + + // row header + if (showRowHeaders) { + addCell(buildCaptionHtmlSnippet(uidl), aligns[col++], "", + true); + } + + if (uidl.hasAttribute("al")) { + actionKeys = uidl.getStringArrayAttribute("al"); + } + + final Iterator<?> cells = uidl.getChildIterator(); + while (cells.hasNext()) { + final Object cell = cells.next(); + visibleColumnIndex++; + + String columnId = visibleColOrder[visibleColumnIndex]; + + String style = ""; + if (uidl.hasAttribute("style-" + columnId)) { + style = uidl.getStringAttribute("style-" + columnId); + } + + if (cell instanceof String) { + addCell(cell.toString(), aligns[col++], style, false); + } else { + final Paintable cellContent = client + .getPaintable((UIDL) cell); + + addCell((Widget) cellContent, aligns[col++], style); + paintComponent(cellContent, (UIDL) cell); + } + } + if (uidl.hasAttribute("selected") && !isSelected()) { + toggleSelection(); + } + } + + /** + * Add a dummy row, used for measurements if Table is empty. + */ + public IScrollTableRow() { + this(0); + addStyleName(CLASSNAME + "-row"); + addCell("_", 'b', "", true); + } + + public void addCell(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(); + String className = CLASSNAME + "-cell-content"; + if (style != null && !style.equals("")) { + className += " " + CLASSNAME + "-cell-content-" + style; + } + td.setClassName(className); + container.setClassName(CLASSNAME + "-cell-wrapper"); + if (textIsHTML) { + container.setInnerHTML(text); + } else { + container.setInnerText(text); + } + if (align != ALIGN_LEFT) { + switch (align) { + case ALIGN_CENTER: + container.getStyle().setProperty("textAlign", "center"); + break; + case ALIGN_RIGHT: + default: + container.getStyle().setProperty("textAlign", "right"); + break; + } + } + td.appendChild(container); + getElement().appendChild(td); + } + + public void addCell(Widget w, char align, String style) { + final Element td = DOM.createTD(); + final Element container = DOM.createDiv(); + String className = CLASSNAME + "-cell-content"; + if (style != null && !style.equals("")) { + className += " " + CLASSNAME + "-cell-content-" + style; + } + td.setClassName(className); + container.setClassName(CLASSNAME + "-cell-wrapper"); + // TODO most components work with this, but not all (e.g. + // Select) + // Old comment: make widget cells respect align. + // text-align:center for IE, margin: auto for others + if (align != ALIGN_LEFT) { + switch (align) { + case ALIGN_CENTER: + container.getStyle().setProperty("textAlign", "center"); + break; + case ALIGN_RIGHT: + default: + container.getStyle().setProperty("textAlign", "right"); + break; + } + } + td.appendChild(container); + getElement().appendChild(td); + // ensure widget not attached to another element (possible tBody + // change) + w.removeFromParent(); + container.appendChild(w.getElement()); + adopt(w); + childWidgets.add(w); + } + + public Iterator<Widget> iterator() { + return childWidgets.iterator(); + } + + @Override + public boolean remove(Widget w) { + if (childWidgets.contains(w)) { + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), w + .getElement()); + childWidgets.remove(w); + return true; + } else { + return false; + } + } + + private void handleClickEvent(Event event, Element targetTdOrTr) { + if (emitClickEvents) { + boolean doubleClick = (DOM.eventGetType(event) == Event.ONDBLCLICK); + + /* This row was clicked */ + client.updateVariable(paintableId, "clickedKey", "" + + rowKey, false); + + if (getElement() == targetTdOrTr.getParentElement()) { + /* A specific column was clicked */ + int childIndex = DOM.getChildIndex(getElement(), + targetTdOrTr); + String colKey = null; + colKey = tHead.getHeaderCell(childIndex).getColKey(); + client.updateVariable(paintableId, "clickedColKey", + colKey, false); + } + + MouseEventDetails details = new MouseEventDetails(event); + // Note: the 'immediate' logic would need to be more + // involved (see #2104), but iscrolltable always sends + // select event, even though nullselectionallowed wont let + // the change trough. Will need to be updated if that is + // changed. + client + .updateVariable( + paintableId, + "clickEvent", + details.toString(), + !(!doubleClick + && selectMode > Table.SELECT_MODE_NONE && immediate)); + } + } + + /* + * React on click that occur on content cells only + */ + @Override + public void onBrowserEvent(Event event) { + if (enabled) { + Element targetTdOrTr = getEventTargetTdOrTr(event); + if (targetTdOrTr != null) { + switch (DOM.eventGetType(event)) { + case Event.ONCLICK: + handleClickEvent(event, targetTdOrTr); + if (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(), immediate); + } + break; + case Event.ONDBLCLICK: + handleClickEvent(event, targetTdOrTr); + break; + case Event.ONCONTEXTMENU: + showContextMenu(event); + break; + default: + break; + } + } + } + super.onBrowserEvent(event); + } + + /** + * Finds the TD that the event interacts with. Returns null if the + * target of the event should not be handled. If the event target is + * the row directly this method returns the TR element instead of + * the TD. + * + * @param event + * @return TD or TR element that the event targets (the actual event + * target is this element or a child of it) + */ + private Element getEventTargetTdOrTr(Event event) { + Element targetTdOrTr = null; + + final Element eventTarget = DOM.eventGetTarget(event); + final Element eventTargetParent = DOM.getParent(eventTarget); + final Element eventTargetGrandParent = DOM + .getParent(eventTargetParent); + + final Element thisTrElement = getElement(); + + if (eventTarget == thisTrElement) { + // This was a click on the TR element + targetTdOrTr = eventTarget; + // rowTarget = true; + } else if (thisTrElement == eventTargetParent) { + // Target parent is the TR, so the actual target is the TD + targetTdOrTr = eventTarget; + } else if (thisTrElement == eventTargetGrandParent) { + // Target grand parent is the TR, so the parent is the TD + targetTdOrTr = eventTargetParent; + } else { + /* + * This is a workaround to make Labels and Embedded in a + * Table clickable (see #2688). It is really not a fix as it + * does not work for a custom component (not extending + * ILabel/IEmbedded) or for read only textfields etc. + */ + Element tdElement = eventTargetParent; + while (DOM.getParent(tdElement) != thisTrElement) { + tdElement = DOM.getParent(tdElement); + } + + Element componentElement = tdElement.getFirstChildElement() + .getFirstChildElement().cast(); + Widget widget = (Widget) client + .getPaintable(componentElement); + if (widget instanceof ILabel || widget instanceof IEmbedded) { + targetTdOrTr = tdElement; + } + } + + return targetTdOrTr; + } + + public void showContextMenu(Event event) { + if (enabled && actionKeys != null) { + int left = event.getClientX(); + int top = event.getClientY(); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + client.getContextMenu().showAt(this, left, top); + } + event.cancelBubble(true); + event.preventDefault(); + } + + public boolean isSelected() { + return selected; + } + + private void toggleSelection() { + selected = !selected; + if (selected) { + if (selectMode == Table.SELECT_MODE_SINGLE) { + deselectAll(); + } + selectedRowKeys.add(String.valueOf(rowKey)); + addStyleName("i-selected"); + } else { + selectedRowKeys.remove(String.valueOf(rowKey)); + removeStyleName("i-selected"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.IActionOwner#getActions + * () + */ + public Action[] getActions() { + if (actionKeys == null) { + return new Action[] {}; + } + final Action[] actions = new Action[actionKeys.length]; + for (int i = 0; i < actions.length; i++) { + final String actionKey = actionKeys[i]; + final TreeAction a = new TreeAction(this, String + .valueOf(rowKey), actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + actions[i] = a; + } + return actions; + } + + public ApplicationConnection getClient() { + return client; + } + + public String getPaintableId() { + return paintableId; + } + + public RenderSpace getAllocatedSpace(Widget child) { + int w = 0; + int i = getColIndexOf(child); + HeaderCell headerCell = tHead.getHeaderCell(i); + if (headerCell != null) { + if (initializedAndAttached) { + w = headerCell.getWidth(); + } else { + // header offset width is not absolutely correct value, + // but a best guess (expecting similar content in all + // columns -> + // if one component is relative width so are others) + w = headerCell.getOffsetWidth() - getCellExtraWidth(); + } + } + return new RenderSpace(w, getRowHeight()); + } + + private int getColIndexOf(Widget child) { + com.google.gwt.dom.client.Element widgetCell = child + .getElement().getParentElement().getParentElement(); + NodeList<TableCellElement> cells = rowElement.getCells(); + for (int i = 0; i < cells.getLength(); i++) { + if (cells.getItem(i) == widgetCell) { + return i; + } + } + return -1; + } + + public boolean hasChildComponent(Widget component) { + return childWidgets.contains(component); + } + + public void replaceChildComponent(Widget oldComponent, + Widget newComponent) { + com.google.gwt.dom.client.Element parentElement = oldComponent + .getElement().getParentElement(); + int index = childWidgets.indexOf(oldComponent); + oldComponent.removeFromParent(); + + parentElement.appendChild(newComponent.getElement()); + childWidgets.insertElementAt(newComponent, index); + adopt(newComponent); + + } + + public boolean requestLayout(Set<Paintable> children) { + // row size should never change and system wouldn't event + // survive as this is a kind of fake paitable + return true; + } + + public void updateCaption(Paintable component, UIDL uidl) { + // NOP, not rendered + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // Should never be called, + // Component container interface faked here to get layouts + // render properly + } + } + } + + public void deselectAll() { + final Object[] keys = selectedRowKeys.toArray(); + for (int i = 0; i < keys.length; i++) { + final IScrollTableRow row = getRenderedRowByKey((String) keys[i]); + if (row != null && row.isSelected()) { + row.toggleSelection(); + } + } + // still ensure all selects are removed from (not necessary rendered) + selectedRowKeys.clear(); + + } + + @Override + public void setWidth(String width) { + if (this.width.equals(width)) { + return; + } + + this.width = width; + if (width != null && !"".equals(width)) { + super.setWidth(width); + int innerPixels = getOffsetWidth() - getBorderWidth(); + if (innerPixels < 0) { + innerPixels = 0; + } + setContentWidth(innerPixels); + + if (!rendering) { + // readjust undefined width columns + lazyAdjustColumnWidths.cancel(); + lazyAdjustColumnWidths.schedule(LAZY_COLUMN_ADJUST_TIMEOUT); + } + + } else { + super.setWidth(""); + } + + } + + private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300; + + private final Timer lazyAdjustColumnWidths = new Timer() { + /** + * Check for column widths, and available width, to see if we can fix + * column widths "optimally". Doing this lazily to avoid expensive + * calculation when resizing is not yet finished. + */ + @Override + public void run() { + + Iterator<Widget> headCells = tHead.iterator(); + int usedMinimumWidth = 0; + int totalExplicitColumnsWidths = 0; + float expandRatioDivider = 0; + int colIndex = 0; + while (headCells.hasNext()) { + final HeaderCell hCell = (HeaderCell) headCells.next(); + if (hCell.isDefinedWidth()) { + totalExplicitColumnsWidths += hCell.getWidth(); + usedMinimumWidth += hCell.getWidth(); + } else { + usedMinimumWidth += hCell.getNaturalColumnWidth(colIndex); + expandRatioDivider += hCell.getExpandRatio(); + } + colIndex++; + } + + int availW = tBody.getAvailableWidth(); + // Hey IE, are you really sure about this? + availW = tBody.getAvailableWidth(); + availW -= tBody.getCellExtraWidth() * visibleColOrder.length; + if (willHaveScrollbars()) { + availW -= Util.getNativeScrollbarSize(); + } + + int extraSpace = availW - usedMinimumWidth; + if (extraSpace < 0) { + extraSpace = 0; + } + + int totalUndefinedNaturaWidths = usedMinimumWidth + - totalExplicitColumnsWidths; + + // we have some space that can be divided optimally + HeaderCell hCell; + colIndex = 0; + headCells = tHead.iterator(); + while (headCells.hasNext()) { + hCell = (HeaderCell) headCells.next(); + if (!hCell.isDefinedWidth()) { + int w = hCell.getNaturalColumnWidth(colIndex); + int newSpace; + if (expandRatioDivider > 0) { + // divide excess space by expand ratios + newSpace = (int) (w + extraSpace + * hCell.getExpandRatio() / expandRatioDivider); + } else { + if (totalUndefinedNaturaWidths != 0) { + // divide relatively to natural column widths + newSpace = w + extraSpace * w + / totalUndefinedNaturaWidths; + } else { + newSpace = w; + } + } + setColWidth(colIndex, newSpace, false); + } + colIndex++; + } + Util.runWebkitOverflowAutoFix(bodyContainer.getElement()); + tBody.reLayoutComponents(); + } + }; + + /** + * helper to set pixel size of head and body part + * + * @param pixels + */ + private void setContentWidth(int pixels) { + tHead.setWidth(pixels + "px"); + bodyContainer.setWidth(pixels + "px"); + } + + private int borderWidth = -1; + + /** + * @return border left + border right + */ + private int getBorderWidth() { + if (borderWidth < 0) { + borderWidth = Util.measureHorizontalPaddingAndBorder(bodyContainer + .getElement(), 2); + if (borderWidth < 0) { + borderWidth = 0; + } + } + return borderWidth; + } + + /** + * Ensures scrollable area is properly sized. + */ + private void setContainerHeight() { + if (height != null && !"".equals(height)) { + int contentH = getOffsetHeight() - tHead.getOffsetHeight(); + contentH -= getContentAreaBorderHeight(); + if (contentH < 0) { + contentH = 0; + } + bodyContainer.setHeight(contentH + "px"); + } + } + + private int contentAreaBorderHeight = -1; + + /** + * @return border top + border bottom of the scrollable area of table + */ + private int getContentAreaBorderHeight() { + if (contentAreaBorderHeight < 0) { + DOM.setStyleAttribute(bodyContainer.getElement(), "overflow", + "hidden"); + contentAreaBorderHeight = bodyContainer.getOffsetHeight() + - bodyContainer.getElement().getPropertyInt("clientHeight"); + DOM.setStyleAttribute(bodyContainer.getElement(), "overflow", + "auto"); + } + return contentAreaBorderHeight; + } + + @Override + public void setHeight(String height) { + this.height = height; + super.setHeight(height); + setContainerHeight(); + } + + /* + * Overridden due Table might not survive of visibility change (scroll pos + * lost). Example ITabPanel just set contained components invisible and back + * when changing tabs. + */ + @Override + public void setVisible(boolean visible) { + if (isVisible() != visible) { + super.setVisible(visible); + if (initializedAndAttached) { + if (visible) { + DeferredCommand.addCommand(new Command() { + public void execute() { + bodyContainer.setScrollPosition(firstRowInViewPort + * tBody.getRowHeight()); + } + }); + } + } + } + } + + /** + * Helper function to build html snippet for column or row headers + * + * @param uidl + * possibly with values caption and icon + * @return html snippet containing possibly an icon + caption text + */ + private String buildCaptionHtmlSnippet(UIDL uidl) { + String s = uidl.getStringAttribute("caption"); + if (uidl.hasAttribute("icon")) { + s = "<img src=\"" + + client.translateToolkitUri(uidl + .getStringAttribute("icon")) + + "\" alt=\"icon\" class=\"i-icon\">" + s; + } + return s; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ISlider.java b/src/com/vaadin/terminal/gwt/client/ui/ISlider.java new file mode 100644 index 0000000000..0cd04f6e89 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ISlider.java @@ -0,0 +1,436 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+//
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+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 ISlider extends Widget implements Paintable, Field,
+ ContainerResizedListener {
+
+ public static final String CLASSNAME = "i-slider";
+
+ /**
+ * Minimum size (width or height, depending on orientation) of the slider
+ * base.
+ */
+ private static final int MIN_SIZE = 50;
+
+ ApplicationConnection client;
+
+ String id;
+
+ private boolean immediate;
+ private boolean disabled;
+ private boolean readonly;
+ private boolean scrollbarStyle;
+
+ private int handleSize;
+ private double min;
+ private double max;
+ private int resolution;
+ private Double value;
+ private boolean vertical;
+ private int size = -1;
+ private boolean arrows;
+
+ /* DOM element for slider's base */
+ private final Element base;
+ private final int BASE_BORDER_WIDTH = 1;
+
+ /* DOM element for slider's handle */
+ private final Element handle;
+
+ /* DOM element for decrement arrow */
+ private final Element smaller;
+
+ /* DOM element for increment arrow */
+ private final Element bigger;
+
+ /* Temporary dragging/animation variables */
+ private boolean dragging = false;
+
+ public ISlider() {
+ super();
+
+ setElement(DOM.createDiv());
+ base = DOM.createDiv();
+ handle = DOM.createDiv();
+ smaller = DOM.createDiv();
+ bigger = DOM.createDiv();
+
+ setStyleName(CLASSNAME);
+ DOM.setElementProperty(base, "className", CLASSNAME + "-base");
+ DOM.setElementProperty(handle, "className", CLASSNAME + "-handle");
+ DOM.setElementProperty(smaller, "className", CLASSNAME + "-smaller");
+ DOM.setElementProperty(bigger, "className", CLASSNAME + "-bigger");
+
+ DOM.appendChild(getElement(), bigger);
+ DOM.appendChild(getElement(), smaller);
+ DOM.appendChild(getElement(), base);
+ DOM.appendChild(base, handle);
+
+ // Hide initially
+ DOM.setStyleAttribute(smaller, "display", "none");
+ DOM.setStyleAttribute(bigger, "display", "none");
+ DOM.setStyleAttribute(handle, "visibility", "hidden");
+
+ DOM.sinkEvents(getElement(), Event.MOUSEEVENTS | Event.ONMOUSEWHEEL);
+ 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);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ this.client = client;
+ id = uidl.getId();
+
+ // Ensure correct implementation
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ immediate = uidl.getBooleanAttribute("immediate");
+ disabled = uidl.getBooleanAttribute("disabled");
+ readonly = uidl.getBooleanAttribute("readonly");
+
+ vertical = uidl.hasAttribute("vertical");
+ arrows = uidl.hasAttribute("arrows");
+
+ String style = "";
+ if (uidl.hasAttribute("style")) {
+ style = uidl.getStringAttribute("style");
+ }
+
+ scrollbarStyle = style.indexOf("scrollbar") > -1;
+
+ if (arrows) {
+ DOM.setStyleAttribute(smaller, "display", "block");
+ DOM.setStyleAttribute(bigger, "display", "block");
+ }
+
+ if (vertical) {
+ addStyleName(CLASSNAME + "-vertical");
+ } else {
+ removeStyleName(CLASSNAME + "-vertical");
+ }
+
+ min = uidl.getDoubleAttribute("min");
+ max = uidl.getDoubleAttribute("max");
+ resolution = uidl.getIntAttribute("resolution");
+ value = new Double(uidl.getDoubleVariable("value"));
+
+ handleSize = uidl.getIntAttribute("hsize");
+
+ buildBase();
+
+ if (!vertical) {
+ // Draw handle with a delay to allow base to gain maximum width
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ buildHandle();
+ setValue(value, false);
+ }
+ });
+ } else {
+ buildHandle();
+ setValue(value, false);
+ }
+ }
+
+ private void buildBase() {
+ final String styleAttribute = vertical ? "height" : "width";
+ final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
+
+ if (size == -1) {
+ final Element p = DOM.getParent(getElement());
+ if (DOM.getElementPropertyInt(p, domProperty) > 50) {
+ if (vertical) {
+ setHeight();
+ } else {
+ DOM.setStyleAttribute(base, styleAttribute, "");
+ }
+ } else {
+ // Set minimum size and adjust after all components have
+ // (supposedly) been drawn completely.
+ DOM.setStyleAttribute(base, styleAttribute, MIN_SIZE + "px");
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ final Element p = DOM.getParent(getElement());
+ if (DOM.getElementPropertyInt(p, domProperty) > (MIN_SIZE + 5)) {
+ if (vertical) {
+ setHeight();
+ } else {
+ DOM.setStyleAttribute(base, styleAttribute, "");
+ }
+ // Ensure correct position
+ setValue(value, false);
+ }
+ }
+ });
+ }
+ } else {
+ DOM.setStyleAttribute(base, styleAttribute, size + "px");
+ }
+
+ // TODO attach listeners for focusing and arrow keys
+ }
+
+ private void buildHandle() {
+ final String styleAttribute = vertical ? "height" : "width";
+ final String handleAttribute = vertical ? "marginTop" : "marginLeft";
+ final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
+
+ DOM.setStyleAttribute(handle, handleAttribute, "0");
+
+ if (scrollbarStyle) {
+ // Only stretch the handle if scrollbar style is set.
+ int s = (int) (Double.parseDouble(DOM.getElementProperty(base,
+ domProperty)) / 100 * handleSize);
+ if (handleSize == -1) {
+ final int baseS = Integer.parseInt(DOM.getElementProperty(base,
+ domProperty));
+ final double range = (max - min) * (resolution + 1) * 3;
+ s = (int) (baseS - range);
+ }
+ if (s < 3) {
+ s = 3;
+ }
+ DOM.setStyleAttribute(handle, styleAttribute, s + "px");
+ } else {
+ DOM.setStyleAttribute(handle, styleAttribute, "");
+ }
+
+ // Restore visibility
+ DOM.setStyleAttribute(handle, "visibility", "visible");
+
+ }
+
+ private void setValue(Double value, boolean updateToServer) {
+ if (value == null) {
+ return;
+ }
+
+ if (value.doubleValue() < min) {
+ value = new Double(min);
+ } else if (value.doubleValue() > max) {
+ value = new Double(max);
+ }
+
+ // Update handle position
+ final String styleAttribute = vertical ? "marginTop" : "marginLeft";
+ final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
+ final int handleSize = Integer.parseInt(DOM.getElementProperty(handle,
+ domProperty));
+ final int baseSize = Integer.parseInt(DOM.getElementProperty(base,
+ domProperty))
+ - (2 * BASE_BORDER_WIDTH);
+
+ final int range = baseSize - handleSize;
+ double v = value.doubleValue();
+ // Round value to resolution
+ if (resolution > 0) {
+ v = Math.round(v * Math.pow(10, resolution));
+ v = v / Math.pow(10, resolution);
+ } else {
+ v = Math.round(v);
+ }
+ final double valueRange = max - min;
+ double p = 0;
+ if (valueRange > 0) {
+ p = range * ((v - min) / valueRange);
+ }
+ if (p < 0) {
+ p = 0;
+ }
+ if (vertical) {
+ // IE6 rounding behaves a little unstable, reduce one pixel so the
+ // containing element (base) won't expand without limits
+ p = range - p - (BrowserInfo.get().isIE6() ? 1 : 0);
+ }
+ final double pos = p;
+
+ DOM.setStyleAttribute(handle, styleAttribute, (Math.round(pos)) + "px");
+
+ // TODO give more detailed info when dragging and do roundup
+ DOM.setElementAttribute(handle, "title", "" + v);
+
+ // Update value
+ this.value = new Double(v);
+
+ if (updateToServer) {
+ updateValueToServer();
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (disabled || readonly) {
+ return;
+ }
+ final Element targ = DOM.eventGetTarget(event);
+
+ if (DOM.eventGetType(event) == Event.ONMOUSEWHEEL) {
+ processMouseWheelEvent(event);
+ } else if (dragging || targ == handle) {
+ processHandleEvent(event);
+ } else if (targ == smaller) {
+ decreaseValue(true);
+ } else if (targ == bigger) {
+ increaseValue(true);
+ } else {
+ processBaseEvent(event);
+ }
+ }
+
+ private Timer scrollTimer;
+
+ private void processMouseWheelEvent(final Event event) {
+ final int dir = DOM.eventGetMouseWheelVelocityY(event);
+
+ if (dir < 0) {
+ increaseValue(false);
+ } else {
+ decreaseValue(false);
+ }
+
+ if (scrollTimer != null) {
+ scrollTimer.cancel();
+ }
+ scrollTimer = new Timer() {
+ @Override
+ public void run() {
+ updateValueToServer();
+ }
+ };
+ scrollTimer.schedule(100);
+
+ DOM.eventPreventDefault(event);
+ DOM.eventCancelBubble(event, true);
+ }
+
+ private void processHandleEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEDOWN:
+ if (!disabled && !readonly) {
+ dragging = true;
+ DOM.setCapture(getElement());
+ DOM.eventPreventDefault(event); // prevent selecting text
+ DOM.eventCancelBubble(event, true);
+ }
+ break;
+ case Event.ONMOUSEMOVE:
+ if (dragging) {
+ // DOM.setCapture(getElement());
+ setValueByEvent(event, false);
+ }
+ break;
+ case Event.ONMOUSEUP:
+ dragging = false;
+ DOM.releaseCapture(getElement());
+ setValueByEvent(event, true);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void processBaseEvent(Event event) {
+ if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
+ if (!disabled && !readonly && !dragging) {
+ setValueByEvent(event, true);
+ DOM.eventCancelBubble(event, true);
+ }
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN && dragging) {
+ dragging = false;
+ DOM.releaseCapture(getElement());
+ setValueByEvent(event, true);
+ }
+ }
+
+ private void decreaseValue(boolean updateToServer) {
+ setValue(new Double(value.doubleValue() - Math.pow(10, -resolution)),
+ updateToServer);
+ }
+
+ private void increaseValue(boolean updateToServer) {
+ setValue(new Double(value.doubleValue() + Math.pow(10, -resolution)),
+ updateToServer);
+ }
+
+ private void setValueByEvent(Event event, boolean updateToServer) {
+ double v = min; // Fallback to min
+
+ final int coord = vertical ? DOM.eventGetClientY(event) : DOM
+ .eventGetClientX(event);
+ final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
+
+ final double handleSize = Integer.parseInt(DOM.getElementProperty(
+ handle, domProperty));
+ final double baseSize = Integer.parseInt(DOM.getElementProperty(base,
+ domProperty));
+ final double baseOffset = vertical ? DOM.getAbsoluteTop(base)
+ - handleSize / 2 : DOM.getAbsoluteLeft(base) + handleSize / 2;
+
+ if (vertical) {
+ v = ((baseSize - (coord - baseOffset)) / (baseSize - handleSize))
+ * (max - min) + min;
+ } else {
+ v = ((coord - baseOffset) / (baseSize - handleSize)) * (max - min)
+ + min;
+ }
+
+ if (v < min) {
+ v = min;
+ } else if (v > max) {
+ v = max;
+ }
+
+ setValue(new Double(v), updateToServer);
+ }
+
+ public void iLayout() {
+ if (vertical) {
+ setHeight();
+ }
+ // Update handle position
+ setValue(value, false);
+ }
+
+ private void setHeight() {
+ if (size == -1) {
+ // Calculate decoration size
+ DOM.setStyleAttribute(base, "height", "0");
+ DOM.setStyleAttribute(base, "overflow", "hidden");
+ int h = DOM.getElementPropertyInt(getElement(), "offsetHeight");
+ if (h < MIN_SIZE) {
+ h = MIN_SIZE;
+ }
+ DOM.setStyleAttribute(base, "height", h + "px");
+ } else {
+ DOM.setStyleAttribute(base, "height", size + "px");
+ }
+ DOM.setStyleAttribute(base, "overflow", "");
+ }
+
+ private void updateValueToServer() {
+ client.updateVariable(id, "value", value.doubleValue(), immediate);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ISplitPanel.java b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanel.java new file mode 100644 index 0000000000..74ca6b5125 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanel.java @@ -0,0 +1,584 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.Set; + +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.RootPanel; +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.ContainerResizedListener; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderInformation; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class ISplitPanel extends ComplexPanel implements Container, + ContainerResizedListener { + public static final String CLASSNAME = "i-splitpanel"; + + public static final int ORIENTATION_HORIZONTAL = 0; + + public static final int ORIENTATION_VERTICAL = 1; + + private static final int MIN_SIZE = 30; + + private int orientation = ORIENTATION_HORIZONTAL; + + private Widget firstChild; + + private Widget secondChild; + + private final Element wrapper = DOM.createDiv(); + + private final Element firstContainer = DOM.createDiv(); + + private final Element secondContainer = DOM.createDiv(); + + private final Element splitter = DOM.createDiv(); + + private boolean resizing; + + private int origX; + + private int origY; + + private int origMouseX; + + private int origMouseY; + + private boolean locked = false; + + private String[] componentStyleNames; + + private Element draggingCurtain; + + private ApplicationConnection client; + + private String width = ""; + + private String height = ""; + + private RenderSpace firstRenderSpace = new RenderSpace(0, 0, true); + private RenderSpace secondRenderSpace = new RenderSpace(0, 0, true); + + RenderInformation renderInformation = new RenderInformation(); + + private String id; + + private boolean immediate; + + private boolean rendering = false; + + public ISplitPanel() { + this(ORIENTATION_HORIZONTAL); + } + + public ISplitPanel(int orientation) { + setElement(DOM.createDiv()); + switch (orientation) { + case ORIENTATION_HORIZONTAL: + setStyleName(CLASSNAME + "-horizontal"); + break; + case ORIENTATION_VERTICAL: + default: + setStyleName(CLASSNAME + "-vertical"); + break; + } + // size below will be overridden in update from uidl, initial size + // needed to keep IE alive + setWidth(MIN_SIZE + "px"); + setHeight(MIN_SIZE + "px"); + constructDom(); + setOrientation(orientation); + DOM.sinkEvents(splitter, (Event.MOUSEEVENTS)); + DOM.sinkEvents(getElement(), (Event.MOUSEEVENTS)); + } + + protected void constructDom() { + DOM.appendChild(splitter, DOM.createDiv()); // for styling + DOM.appendChild(getElement(), wrapper); + DOM.setStyleAttribute(wrapper, "position", "relative"); + DOM.setStyleAttribute(wrapper, "width", "100%"); + DOM.setStyleAttribute(wrapper, "height", "100%"); + + DOM.appendChild(wrapper, secondContainer); + DOM.appendChild(wrapper, firstContainer); + DOM.appendChild(wrapper, splitter); + + DOM.setStyleAttribute(splitter, "position", "absolute"); + DOM.setStyleAttribute(secondContainer, "position", "absolute"); + + DOM.setStyleAttribute(firstContainer, "overflow", "auto"); + DOM.setStyleAttribute(secondContainer, "overflow", "auto"); + + } + + private void setOrientation(int orientation) { + this.orientation = orientation; + if (orientation == ORIENTATION_HORIZONTAL) { + DOM.setStyleAttribute(splitter, "height", "100%"); + DOM.setStyleAttribute(splitter, "top", "0"); + DOM.setStyleAttribute(firstContainer, "height", "100%"); + DOM.setStyleAttribute(secondContainer, "height", "100%"); + } else { + DOM.setStyleAttribute(splitter, "width", "100%"); + DOM.setStyleAttribute(splitter, "left", "0"); + DOM.setStyleAttribute(firstContainer, "width", "100%"); + DOM.setStyleAttribute(secondContainer, "width", "100%"); + } + + DOM.setElementProperty(firstContainer, "className", CLASSNAME + + "-first-container"); + DOM.setElementProperty(secondContainer, "className", CLASSNAME + + "-second-container"); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + this.client = client; + id = uidl.getId(); + rendering = true; + + immediate = uidl.hasAttribute("immediate"); + + if (client.updateComponent(this, uidl, true)) { + rendering = false; + return; + } + + if (uidl.hasAttribute("style")) { + componentStyleNames = uidl.getStringAttribute("style").split(" "); + } else { + componentStyleNames = new String[0]; + } + + setLocked(uidl.getBooleanAttribute("locked")); + + setStylenames(); + + setSplitPosition(uidl.getStringAttribute("position")); + + final Paintable newFirstChild = client.getPaintable(uidl + .getChildUIDL(0)); + final Paintable newSecondChild = client.getPaintable(uidl + .getChildUIDL(1)); + if (firstChild != newFirstChild) { + if (firstChild != null) { + client.unregisterPaintable((Paintable) firstChild); + } + setFirstWidget((Widget) newFirstChild); + } + if (secondChild != newSecondChild) { + if (secondChild != null) { + client.unregisterPaintable((Paintable) secondChild); + } + setSecondWidget((Widget) newSecondChild); + } + newFirstChild.updateFromUIDL(uidl.getChildUIDL(0), client); + newSecondChild.updateFromUIDL(uidl.getChildUIDL(1), client); + + renderInformation.updateSize(getElement()); + + if (BrowserInfo.get().isIE7()) { + // Part III of IE7 hack + DeferredCommand.addCommand(new Command() { + public void execute() { + iLayout(); + } + }); + } + rendering = false; + + } + + private void setLocked(boolean newValue) { + if (locked != newValue) { + locked = newValue; + splitterSize = -1; + setStylenames(); + } + } + + private void setSplitPosition(String pos) { + if (orientation == ORIENTATION_HORIZONTAL) { + DOM.setStyleAttribute(splitter, "left", pos); + } else { + DOM.setStyleAttribute(splitter, "top", pos); + } + iLayout(); + client.runDescendentsLayout(this); + + } + + /* + * Calculates absolutely positioned container places/sizes (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.NeedsLayout#layout() + */ + public void iLayout() { + if (!isAttached()) { + return; + } + + renderInformation.updateSize(getElement()); + + int wholeSize; + int pixelPosition; + + switch (orientation) { + case ORIENTATION_HORIZONTAL: + wholeSize = DOM.getElementPropertyInt(wrapper, "clientWidth"); + pixelPosition = DOM.getElementPropertyInt(splitter, "offsetLeft"); + + // reposition splitter in case it is out of box + if (pixelPosition > 0 + && pixelPosition + getSplitterSize() > wholeSize) { + pixelPosition = wholeSize - getSplitterSize(); + if (pixelPosition < 0) { + pixelPosition = 0; + } + setSplitPosition(pixelPosition + "px"); + return; + } + + DOM + .setStyleAttribute(firstContainer, "width", pixelPosition + + "px"); + int secondContainerWidth = (wholeSize - pixelPosition - getSplitterSize()); + if (secondContainerWidth < 0) { + secondContainerWidth = 0; + } + DOM.setStyleAttribute(secondContainer, "width", + secondContainerWidth + "px"); + DOM.setStyleAttribute(secondContainer, "left", + (pixelPosition + getSplitterSize()) + "px"); + + int contentHeight = renderInformation.getRenderedSize().getHeight(); + firstRenderSpace.setHeight(contentHeight); + firstRenderSpace.setWidth(pixelPosition); + secondRenderSpace.setHeight(contentHeight); + secondRenderSpace.setWidth(secondContainerWidth); + + break; + case ORIENTATION_VERTICAL: + wholeSize = DOM.getElementPropertyInt(wrapper, "clientHeight"); + pixelPosition = DOM.getElementPropertyInt(splitter, "offsetTop"); + + // reposition splitter in case it is out of box + if (pixelPosition > 0 + && pixelPosition + getSplitterSize() > wholeSize) { + pixelPosition = wholeSize - getSplitterSize(); + if (pixelPosition < 0) { + pixelPosition = 0; + } + setSplitPosition(pixelPosition + "px"); + return; + } + + DOM.setStyleAttribute(firstContainer, "height", pixelPosition + + "px"); + int secondContainerHeight = (wholeSize - pixelPosition - getSplitterSize()); + if (secondContainerHeight < 0) { + secondContainerHeight = 0; + } + DOM.setStyleAttribute(secondContainer, "height", + secondContainerHeight + "px"); + DOM.setStyleAttribute(secondContainer, "top", + (pixelPosition + getSplitterSize()) + "px"); + + int contentWidth = renderInformation.getRenderedSize().getWidth(); + firstRenderSpace.setHeight(pixelPosition); + firstRenderSpace.setWidth(contentWidth); + secondRenderSpace.setHeight(secondContainerHeight); + secondRenderSpace.setWidth(contentWidth); + + break; + } + + // fixes scrollbars issues on webkit based browsers + Util.runWebkitOverflowAutoFix(secondContainer); + Util.runWebkitOverflowAutoFix(firstContainer); + + } + + private void setFirstWidget(Widget w) { + if (firstChild != null) { + firstChild.removeFromParent(); + } + super.add(w, firstContainer); + firstChild = w; + } + + private void setSecondWidget(Widget w) { + if (secondChild != null) { + secondChild.removeFromParent(); + } + super.add(w, secondContainer); + secondChild = w; + } + + @Override + public void onBrowserEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEMOVE: + if (resizing) { + onMouseMove(event); + } + break; + case Event.ONMOUSEDOWN: + onMouseDown(event); + break; + case Event.ONMOUSEUP: + if (resizing) { + onMouseUp(event); + } + break; + case Event.ONCLICK: + resizing = false; + break; + } + } + + public void onMouseDown(Event event) { + if (locked) { + return; + } + final Element trg = DOM.eventGetTarget(event); + if (trg == splitter || trg == DOM.getChild(splitter, 0)) { + resizing = true; + if (BrowserInfo.get().isGecko()) { + showDraggingCurtain(); + } + DOM.setCapture(getElement()); + origX = DOM.getElementPropertyInt(splitter, "offsetLeft"); + origY = DOM.getElementPropertyInt(splitter, "offsetTop"); + origMouseX = DOM.eventGetClientX(event); + origMouseY = DOM.eventGetClientY(event); + DOM.eventCancelBubble(event, true); + DOM.eventPreventDefault(event); + } + } + + public void onMouseMove(Event event) { + switch (orientation) { + case ORIENTATION_HORIZONTAL: + final int x = DOM.eventGetClientX(event); + onHorizontalMouseMove(x); + break; + case ORIENTATION_VERTICAL: + default: + final int y = DOM.eventGetClientY(event); + onVerticalMouseMove(y); + break; + } + iLayout(); + // TODO Check if this is needed + client.runDescendentsLayout(this); + + } + + private void onHorizontalMouseMove(int x) { + int newX = origX + x - origMouseX; + if (newX < 0) { + newX = 0; + } + if (newX + getSplitterSize() > getOffsetWidth()) { + newX = getOffsetWidth() - getSplitterSize(); + } + DOM.setStyleAttribute(splitter, "left", newX + "px"); + updateSplitPosition(newX); + } + + private void onVerticalMouseMove(int y) { + int newY = origY + y - origMouseY; + if (newY < 0) { + newY = 0; + } + + if (newY + getSplitterSize() > getOffsetHeight()) { + newY = getOffsetHeight() - getSplitterSize(); + } + DOM.setStyleAttribute(splitter, "top", newY + "px"); + updateSplitPosition(newY); + } + + public void onMouseUp(Event event) { + DOM.releaseCapture(getElement()); + if (BrowserInfo.get().isGecko()) { + hideDraggingCurtain(); + } + resizing = false; + onMouseMove(event); + } + + /** + * Used in FF to avoid losing mouse capture when pointer is moved on an + * iframe. + */ + private void showDraggingCurtain() { + if (draggingCurtain == null) { + draggingCurtain = DOM.createDiv(); + DOM.setStyleAttribute(draggingCurtain, "position", "absolute"); + DOM.setStyleAttribute(draggingCurtain, "top", "0px"); + DOM.setStyleAttribute(draggingCurtain, "left", "0px"); + DOM.setStyleAttribute(draggingCurtain, "width", "100%"); + DOM.setStyleAttribute(draggingCurtain, "height", "100%"); + DOM.setStyleAttribute(draggingCurtain, "zIndex", "" + + IToolkitOverlay.Z_INDEX); + DOM.appendChild(RootPanel.getBodyElement(), draggingCurtain); + } + } + + /** + * Hides dragging curtain + */ + private void hideDraggingCurtain() { + if (draggingCurtain != null) { + DOM.removeChild(RootPanel.getBodyElement(), draggingCurtain); + draggingCurtain = null; + } + } + + private int splitterSize = -1; + + private int getSplitterSize() { + if (splitterSize < 0) { + if (isAttached()) { + switch (orientation) { + case ORIENTATION_HORIZONTAL: + splitterSize = DOM.getElementPropertyInt(splitter, + "offsetWidth"); + break; + + default: + splitterSize = DOM.getElementPropertyInt(splitter, + "offsetHeight"); + break; + } + } + } + return splitterSize; + } + + @Override + public void setHeight(String height) { + if (this.height.equals(height)) { + return; + } + + this.height = height; + super.setHeight(height); + if (!rendering && client != null) { + iLayout(); + client.runDescendentsLayout(this); + } + } + + @Override + public void setWidth(String width) { + if (this.width.equals(width)) { + return; + } + + this.width = width; + super.setWidth(width); + if (!rendering && client != null) { + iLayout(); + client.runDescendentsLayout(this); + } + } + + public RenderSpace getAllocatedSpace(Widget child) { + if (child == firstChild) { + return firstRenderSpace; + } else if (child == secondChild) { + return secondRenderSpace; + } + + return null; + } + + public boolean hasChildComponent(Widget component) { + return (component != null && (component == firstChild || component == secondChild)); + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + if (oldComponent == firstChild) { + setFirstWidget(newComponent); + } else if (oldComponent == secondChild) { + setSecondWidget(newComponent); + } + } + + public boolean requestLayout(Set<Paintable> child) { + if (height != null && width != null) { + /* + * If the height and width has been specified the child components + * cannot make the size of the layout change + */ + + return true; + } + + if (renderInformation.updateSize(getElement())) { + return false; + } else { + return true; + } + + } + + public void updateCaption(Paintable component, UIDL uidl) { + // TODO Implement caption handling + } + + /** + * Updates the new split position back to server. + * + * @param pos + * The new position of the split handle. + */ + private void updateSplitPosition(int pos) { + // We always send pixel values to server + client.updateVariable(id, "position", pos, immediate); + } + + private void setStylenames() { + final String splitterSuffix = (orientation == ORIENTATION_HORIZONTAL ? "-hsplitter" + : "-vsplitter"); + final String firstContainerSuffix = "-first-container"; + final String secondContainerSuffix = "-second-container"; + String lockedSuffix = ""; + + String splitterStyle = CLASSNAME + splitterSuffix; + String firstStyle = CLASSNAME + firstContainerSuffix; + String secondStyle = CLASSNAME + secondContainerSuffix; + + if (locked) { + splitterStyle = CLASSNAME + splitterSuffix + "-locked"; + lockedSuffix = "-locked"; + } + for (int i = 0; i < componentStyleNames.length; i++) { + splitterStyle += " " + CLASSNAME + splitterSuffix + "-" + + componentStyleNames[i] + lockedSuffix; + firstStyle += " " + CLASSNAME + firstContainerSuffix + "-" + + componentStyleNames[i]; + secondStyle += " " + CLASSNAME + secondContainerSuffix + "-" + + componentStyleNames[i]; + } + DOM.setElementProperty(splitter, "className", splitterStyle); + DOM.setElementProperty(firstContainer, "className", firstStyle); + DOM.setElementProperty(secondContainer, "className", secondStyle); + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelHorizontal.java b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelHorizontal.java new file mode 100644 index 0000000000..140881484b --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelHorizontal.java @@ -0,0 +1,12 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +public class ISplitPanelHorizontal extends ISplitPanel { + + public ISplitPanelHorizontal() { + super(ISplitPanel.ORIENTATION_HORIZONTAL); + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelVertical.java b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelVertical.java new file mode 100644 index 0000000000..eff00e8a43 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelVertical.java @@ -0,0 +1,12 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +public class ISplitPanelVertical extends ISplitPanel { + + public ISplitPanelVertical() { + super(ISplitPanel.ORIENTATION_VERTICAL); + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITablePaging.java b/src/com/vaadin/terminal/gwt/client/ui/ITablePaging.java new file mode 100644 index 0000000000..224c549f3c --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITablePaging.java @@ -0,0 +1,439 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.Vector; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; + +/** + * TODO make this work (just an early prototype). We may want to have paging + * style table which will be much lighter than IScrollTable is. + */ +public class ITablePaging extends Composite implements Table, Paintable, + ClickListener { + + private final Grid tBody = new Grid(); + private final Button nextPage = new Button(">"); + private final Button prevPage = new Button("<"); + private final Button firstPage = new Button("<<"); + private final Button lastPage = new Button(">>"); + + private int pageLength = 15; + + private boolean rowHeaders = false; + + private ApplicationConnection client; + private String id; + + private boolean immediate = false; + + private int selectMode = Table.SELECT_MODE_NONE; + + private final Vector selectedRowKeys = new Vector(); + + private int totalRows; + + private final HashMap visibleColumns = new HashMap(); + + private int rows; + + private int firstRow; + private boolean sortAscending = true; + private final HorizontalPanel pager; + + public HashMap rowKeysToTableRows = new HashMap(); + + public ITablePaging() { + + tBody.setStyleName("itable-tbody"); + + final VerticalPanel panel = new VerticalPanel(); + + pager = new HorizontalPanel(); + pager.add(firstPage); + firstPage.addClickListener(this); + pager.add(prevPage); + prevPage.addClickListener(this); + pager.add(nextPage); + nextPage.addClickListener(this); + pager.add(lastPage); + lastPage.addClickListener(this); + + panel.add(pager); + panel.add(tBody); + + initWidget(panel); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (client.updateComponent(this, uidl, true)) { + return; + } + + this.client = client; + id = uidl.getStringAttribute("id"); + immediate = uidl.getBooleanAttribute("immediate"); + totalRows = uidl.getIntAttribute("totalrows"); + pageLength = uidl.getIntAttribute("pagelength"); + firstRow = uidl.getIntAttribute("firstrow"); + rows = uidl.getIntAttribute("rows"); + + if (uidl.hasAttribute("selectmode")) { + if (uidl.getStringAttribute("selectmode").equals("multi")) { + selectMode = Table.SELECT_MODE_MULTI; + } else { + selectMode = Table.SELECT_MODE_SINGLE; + } + + if (uidl.hasAttribute("selected")) { + final Set selectedKeys = uidl + .getStringArrayVariableAsSet("selected"); + selectedRowKeys.clear(); + for (final Iterator it = selectedKeys.iterator(); it.hasNext();) { + selectedRowKeys.add(it.next()); + } + } + } + + if (uidl.hasVariable("sortascending")) { + sortAscending = uidl.getBooleanVariable("sortascending"); + } + + if (uidl.hasAttribute("rowheaders")) { + rowHeaders = true; + } + + UIDL rowData = null; + UIDL visibleColumns = null; + for (final Iterator it = uidl.getChildIterator(); it.hasNext();) { + final UIDL c = (UIDL) it.next(); + if (c.getTag().equals("rows")) { + rowData = c; + } else if (c.getTag().equals("actions")) { + updateActionMap(c); + } else if (c.getTag().equals("visiblecolumns")) { + visibleColumns = c; + } + } + tBody.resize(rows + 1, uidl.getIntAttribute("cols") + + (rowHeaders ? 1 : 0)); + updateHeader(visibleColumns); + updateBody(rowData); + + updatePager(); + } + + private void updateHeader(UIDL c) { + final Iterator it = c.getChildIterator(); + visibleColumns.clear(); + int colIndex = (rowHeaders ? 1 : 0); + while (it.hasNext()) { + final UIDL col = (UIDL) it.next(); + final String cid = col.getStringAttribute("cid"); + if (!col.hasAttribute("collapsed")) { + tBody.setWidget(0, colIndex, new HeaderCell(cid, col + .getStringAttribute("caption"))); + + } + colIndex++; + } + } + + private void updateActionMap(UIDL c) { + // TODO Auto-generated method stub + + } + + /** + * Updates row data from uidl. UpdateFromUIDL delegates updating tBody to + * this method. + * + * Updates may be to different part of tBody, depending on update type. It + * can be initial row data, scroll up, scroll down... + * + * @param uidl + * which contains row data + */ + private void updateBody(UIDL uidl) { + final Iterator it = uidl.getChildIterator(); + + int curRowIndex = 1; + while (it.hasNext()) { + final UIDL rowUidl = (UIDL) it.next(); + final TableRow row = new TableRow(curRowIndex, String + .valueOf(rowUidl.getIntAttribute("key")), rowUidl + .hasAttribute("selected")); + int colIndex = 0; + if (rowHeaders) { + tBody.setWidget(curRowIndex, colIndex, new BodyCell(row, + rowUidl.getStringAttribute("caption"))); + colIndex++; + } + final Iterator cells = rowUidl.getChildIterator(); + while (cells.hasNext()) { + final Object cell = cells.next(); + if (cell instanceof String) { + tBody.setWidget(curRowIndex, colIndex, new BodyCell(row, + (String) cell)); + } else { + final Paintable cellContent = client + .getPaintable((UIDL) cell); + final BodyCell bodyCell = new BodyCell(row); + bodyCell.setWidget((Widget) cellContent); + tBody.setWidget(curRowIndex, colIndex, bodyCell); + } + colIndex++; + } + curRowIndex++; + } + } + + private void updatePager() { + if (pageLength == 0) { + pager.setVisible(false); + return; + } + if (isFirstPage()) { + firstPage.setEnabled(false); + prevPage.setEnabled(false); + } else { + firstPage.setEnabled(true); + prevPage.setEnabled(true); + } + if (hasNextPage()) { + nextPage.setEnabled(true); + lastPage.setEnabled(true); + } else { + nextPage.setEnabled(false); + lastPage.setEnabled(false); + + } + } + + private boolean hasNextPage() { + if (firstRow + rows + 1 > totalRows) { + return false; + } + return true; + } + + private boolean isFirstPage() { + if (firstRow == 0) { + return true; + } + return false; + } + + public void onClick(Widget sender) { + if (sender instanceof Button) { + if (sender == firstPage) { + client.updateVariable(id, "firstvisible", 0, true); + } else if (sender == nextPage) { + client.updateVariable(id, "firstvisible", + firstRow + pageLength, true); + } else if (sender == prevPage) { + int newFirst = firstRow - pageLength; + if (newFirst < 0) { + newFirst = 0; + } + client.updateVariable(id, "firstvisible", newFirst, true); + } else if (sender == lastPage) { + client.updateVariable(id, "firstvisible", totalRows + - pageLength, true); + } + } + if (sender instanceof HeaderCell) { + final HeaderCell hCell = (HeaderCell) sender; + client.updateVariable(id, "sortcolumn", hCell.getCid(), false); + client.updateVariable(id, "sortascending", (sortAscending ? false + : true), true); + } + } + + private class HeaderCell extends HTML { + + private String cid; + + public String getCid() { + return cid; + } + + public void setCid(String pid) { + cid = pid; + } + + HeaderCell(String pid, String caption) { + super(); + cid = pid; + addClickListener(ITablePaging.this); + setText(caption); + // TODO remove debug color + DOM.setStyleAttribute(getElement(), "color", "brown"); + DOM.setStyleAttribute(getElement(), "font-weight", "bold"); + } + } + + /** + * Abstraction of table cell content. In needs to know on which row it is in + * case of context click. + * + * @author mattitahvonen + */ + public class BodyCell extends SimplePanel { + private final TableRow row; + + public BodyCell(TableRow row) { + super(); + sinkEvents(Event.BUTTON_LEFT | Event.BUTTON_RIGHT); + this.row = row; + } + + public BodyCell(TableRow row2, String textContent) { + super(); + sinkEvents(Event.BUTTON_LEFT | Event.BUTTON_RIGHT); + row = row2; + setWidget(new Label(textContent)); + } + + @Override + public void onBrowserEvent(Event event) { + System.out.println("CEll event: " + event.toString()); + switch (DOM.eventGetType(event)) { + case Event.BUTTON_RIGHT: + row.showContextMenu(event); + Window.alert("context menu un-implemented"); + DOM.eventCancelBubble(event, true); + break; + case Event.BUTTON_LEFT: + if (selectMode > Table.SELECT_MODE_NONE) { + row.toggleSelected(); + } + break; + default: + break; + } + super.onBrowserEvent(event); + } + } + + private class TableRow { + + private final String key; + private final int rowIndex; + private boolean selected = false; + + public TableRow(int rowIndex, String rowKey, boolean selected) { + rowKeysToTableRows.put(rowKey, this); + this.rowIndex = rowIndex; + key = rowKey; + setSelected(selected); + } + + /** + * This method is used to set row status. Does not change value on + * server. + * + * @param selected + */ + public void setSelected(boolean sel) { + selected = sel; + if (selected) { + selectedRowKeys.add(key); + DOM.setStyleAttribute(tBody.getRowFormatter().getElement( + rowIndex), "background", "yellow"); + + } else { + selectedRowKeys.remove(key); + DOM.setStyleAttribute(tBody.getRowFormatter().getElement( + rowIndex), "background", "transparent"); + } + } + + public void setContextMenuOptions(HashMap options) { + + } + + /** + * Toggles rows select state. Also updates state to server according to + * tables immediate flag. + * + */ + public void toggleSelected() { + if (selected) { + setSelected(false); + } else { + if (selectMode == Table.SELECT_MODE_SINGLE) { + deselectAll(); + } + setSelected(true); + } + client.updateVariable(id, "selected", selectedRowKeys.toArray(), + immediate); + } + + /** + * Shows context menu for this row. + * + * @param event + * Event which triggered context menu. Correct place for + * context menu can be determined with it. + */ + public void showContextMenu(Event event) { + System.out.println("TODO: Show context menu"); + } + } + + public void deselectAll() { + final Object[] keys = selectedRowKeys.toArray(); + for (int i = 0; i < keys.length; i++) { + final TableRow tableRow = (TableRow) rowKeysToTableRows + .get(keys[i]); + if (tableRow != null) { + tableRow.setSelected(false); + } + } + // still ensure all selects are removed from + selectedRowKeys.clear(); + } + + public void add(Widget w) { + // TODO Auto-generated method stub + + } + + public void clear() { + // TODO Auto-generated method stub + + } + + public Iterator iterator() { + // TODO Auto-generated method stub + return null; + } + + public boolean remove(Widget w) { + // TODO Auto-generated method stub + return false; + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITabsheet.java b/src/com/vaadin/terminal/gwt/client/ui/ITabsheet.java new file mode 100644 index 0000000000..52f5a41062 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITabsheet.java @@ -0,0 +1,841 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.dom.client.Style; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.ComplexPanel; +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.ICaption; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.RenderInformation; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +public class ITabsheet extends ITabsheetBase { + + private class TabSheetCaption extends ICaption { + TabSheetCaption() { + super(null, client); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONLOAD) { + // icon onloads may change total width of tabsheet + if (isDynamicWidth()) { + updateDynamicWidth(); + } + updateTabScroller(); + } + } + + @Override + public void setWidth(String width) { + super.setWidth(width); + if (BrowserInfo.get().isIE7()) { + /* + * IE7 apparently has problems with calculating width for + * floated elements inside a DIV with padding. Set the width + * explicitly for the caption. + */ + fixTextWidth(); + } + } + + private void fixTextWidth() { + Element captionText = getTextElement(); + int captionWidth = Util.getRequiredWidth(captionText); + int scrollWidth = captionText.getScrollWidth(); + if (scrollWidth > captionWidth) { + captionWidth = scrollWidth; + } + captionText.getStyle().setPropertyPx("width", captionWidth); + } + + } + + class TabBar extends ComplexPanel implements ClickListener { + + private Element tr = DOM.createTR(); + + private Element spacerTd = DOM.createTD(); + + TabBar() { + Element el = DOM.createTable(); + Element tbody = DOM.createTBody(); + DOM.appendChild(el, tbody); + DOM.appendChild(tbody, tr); + setStyleName(spacerTd, CLASSNAME + "-spacertd"); + DOM.appendChild(tr, spacerTd); + DOM.appendChild(spacerTd, DOM.createDiv()); + setElement(el); + } + + protected Element getContainerElement() { + return tr; + } + + private Widget oldSelected; + + public int getTabCount() { + return getWidgetCount(); + } + + public void addTab(ICaption c) { + Element td = DOM.createTD(); + setStyleName(td, CLASSNAME + "-tabitemcell"); + + if (getWidgetCount() == 0) { + setStyleName(td, CLASSNAME + "-tabitemcell-first", true); + } + + Element div = DOM.createDiv(); + setStyleName(div, CLASSNAME + "-tabitem"); + DOM.appendChild(td, div); + DOM.insertBefore(tr, td, spacerTd); + c.addClickListener(this); + add(c, div); + } + + public void onClick(Widget sender) { + int index = getWidgetIndex(sender); + onTabSelected(index); + } + + public void selectTab(int index) { + Widget newSelected = getWidget(index); + Widget.setStyleName(DOM.getParent(newSelected.getElement()), + CLASSNAME + "-tabitem-selected", true); + if (oldSelected != null && oldSelected != newSelected) { + Widget.setStyleName(DOM.getParent(oldSelected.getElement()), + CLASSNAME + "-tabitem-selected", false); + } + oldSelected = newSelected; + } + + public void removeTab(int i) { + Widget w = getWidget(i); + if (w == null) { + return; + } + + Element caption = w.getElement(); + Element div = DOM.getParent(caption); + Element td = DOM.getParent(div); + Element tr = DOM.getParent(td); + remove(w); + + /* + * Widget is the Caption but we want to remove everything up to and + * including the parent TD + */ + + DOM.removeChild(tr, td); + + /* + * If this widget was selected we need to unmark it as the last + * selected + */ + if (w == oldSelected) { + oldSelected = null; + } + } + + @Override + public boolean remove(Widget w) { + ((ICaption) w).removeClickListener(this); + return super.remove(w); + } + + public TabSheetCaption getTab(int index) { + if (index >= getWidgetCount()) { + return null; + } + return (TabSheetCaption) getWidget(index); + } + + public void setVisible(int index, boolean visible) { + Element e = DOM.getParent(getTab(index).getElement()); + if (visible) { + DOM.setStyleAttribute(e, "display", ""); + } else { + DOM.setStyleAttribute(e, "display", "none"); + } + } + + public void updateCaptionSize(int index) { + ICaption c = getTab(index); + c.setWidth(c.getRequiredWidth() + "px"); + + } + + } + + public static final String CLASSNAME = "i-tabsheet"; + + public static final String TABS_CLASSNAME = "i-tabsheet-tabcontainer"; + public static final String SCROLLER_CLASSNAME = "i-tabsheet-scroller"; + private final Element tabs; // tabbar and 'scroller' container + private final Element scroller; // tab-scroller element + private final Element scrollerNext; // tab-scroller next button element + private final Element scrollerPrev; // tab-scroller prev button element + private int scrollerIndex = 0; + + private final TabBar tb = new TabBar(); + private final ITabsheetPanel tp = new ITabsheetPanel(); + private final Element contentNode, deco; + + private final HashMap<String, ICaption> captions = new HashMap<String, ICaption>(); + + private String height; + private String width; + + private boolean waitingForResponse; + + private RenderInformation renderInformation = new RenderInformation(); + + /** + * Previous visible widget is set invisible with CSS (not display: none, but + * visibility: hidden), to avoid flickering during render process. Normal + * visibility must be returned later when new widget is rendered. + */ + private Widget previousVisibleWidget; + + private boolean rendering = false; + + private void onTabSelected(final int tabIndex) { + if (disabled || waitingForResponse) { + return; + } + final Object tabKey = tabKeys.get(tabIndex); + if (disabledTabKeys.contains(tabKey)) { + return; + } + if (client != null && activeTabIndex != tabIndex) { + tb.selectTab(tabIndex); + addStyleDependentName("loading"); + // run updating variables in deferred command to bypass some FF + // optimization issues + DeferredCommand.addCommand(new Command() { + public void execute() { + previousVisibleWidget = tp.getWidget(tp.getVisibleWidget()); + DOM.setStyleAttribute(DOM.getParent(previousVisibleWidget + .getElement()), "visibility", "hidden"); + client.updateVariable(id, "selected", tabKeys.get(tabIndex) + .toString(), true); + } + }); + waitingForResponse = true; + } + } + + private boolean isDynamicWidth() { + return width == null || width.equals(""); + } + + private boolean isDynamicHeight() { + return height == null || height.equals(""); + } + + public ITabsheet() { + super(CLASSNAME); + + // Tab scrolling + DOM.setStyleAttribute(getElement(), "overflow", "hidden"); + tabs = DOM.createDiv(); + DOM.setElementProperty(tabs, "className", TABS_CLASSNAME); + scroller = DOM.createDiv(); + + DOM.setElementProperty(scroller, "className", SCROLLER_CLASSNAME); + scrollerPrev = DOM.createButton(); + DOM.setElementProperty(scrollerPrev, "className", SCROLLER_CLASSNAME + + "Prev"); + DOM.sinkEvents(scrollerPrev, Event.ONCLICK); + scrollerNext = DOM.createButton(); + DOM.setElementProperty(scrollerNext, "className", SCROLLER_CLASSNAME + + "Next"); + DOM.sinkEvents(scrollerNext, Event.ONCLICK); + DOM.appendChild(getElement(), tabs); + + // Tabs + tp.setStyleName(CLASSNAME + "-tabsheetpanel"); + contentNode = DOM.createDiv(); + + deco = DOM.createDiv(); + + addStyleDependentName("loading"); // Indicate initial progress + tb.setStyleName(CLASSNAME + "-tabs"); + DOM + .setElementProperty(contentNode, "className", CLASSNAME + + "-content"); + DOM.setElementProperty(deco, "className", CLASSNAME + "-deco"); + + add(tb, tabs); + DOM.appendChild(scroller, scrollerPrev); + DOM.appendChild(scroller, scrollerNext); + + DOM.appendChild(getElement(), contentNode); + add(tp, contentNode); + DOM.appendChild(getElement(), deco); + + DOM.appendChild(tabs, scroller); + + // TODO Use for Safari only. Fix annoying 1px first cell in TabBar. + // DOM.setStyleAttribute(DOM.getFirstChild(DOM.getFirstChild(DOM + // .getFirstChild(tb.getElement()))), "display", "none"); + + } + + @Override + public void onBrowserEvent(Event event) { + + // Tab scrolling + if (isScrolledTabs() && DOM.eventGetTarget(event) == scrollerPrev) { + if (scrollerIndex > 0) { + scrollerIndex--; + DOM.setStyleAttribute(DOM.getChild(DOM.getFirstChild(DOM + .getFirstChild(tb.getElement())), scrollerIndex), + "display", ""); + tb.updateCaptionSize(scrollerIndex); + updateTabScroller(); + } + } else if (isClippedTabs() && DOM.eventGetTarget(event) == scrollerNext) { + int tabs = tb.getTabCount(); + if (scrollerIndex + 1 <= tabs) { + DOM.setStyleAttribute(DOM.getChild(DOM.getFirstChild(DOM + .getFirstChild(tb.getElement())), scrollerIndex), + "display", "none"); + tb.updateCaptionSize(scrollerIndex); + scrollerIndex++; + updateTabScroller(); + } + } else { + super.onBrowserEvent(event); + } + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + rendering = true; + + super.updateFromUIDL(uidl, client); + if (cachedUpdate) { + return; + } + + // Add proper stylenames for all elements (easier to prevent unwanted + // style inheritance) + if (uidl.hasAttribute("style")) { + final String[] styles = uidl.getStringAttribute("style").split(" "); + final String contentBaseClass = CLASSNAME + "-content"; + String contentClass = contentBaseClass; + final String decoBaseClass = CLASSNAME + "-deco"; + String decoClass = decoBaseClass; + for (int i = 0; i < styles.length; i++) { + tb.addStyleDependentName(styles[i]); + contentClass += " " + contentBaseClass + "-" + styles[i]; + decoClass += " " + decoBaseClass + "-" + styles[i]; + } + DOM.setElementProperty(contentNode, "className", contentClass); + DOM.setElementProperty(deco, "className", decoClass); + } else { + tb.setStyleName(CLASSNAME + "-tabs"); + DOM.setElementProperty(contentNode, "className", CLASSNAME + + "-content"); + DOM.setElementProperty(deco, "className", CLASSNAME + "-deco"); + } + + if (uidl.hasAttribute("hidetabs")) { + tb.setVisible(false); + addStyleName(CLASSNAME + "-hidetabs"); + } else { + tb.setVisible(true); + removeStyleName(CLASSNAME + "-hidetabs"); + } + + // tabs; push or not + if (!isDynamicWidth()) { + // FIXME: This makes tab sheet tabs go to 1px width on every update + // and then back to original width + // update width later, in updateTabScroller(); + DOM.setStyleAttribute(tabs, "width", "1px"); + DOM.setStyleAttribute(tabs, "overflow", "hidden"); + } else { + showAllTabs(); + DOM.setStyleAttribute(tabs, "width", ""); + DOM.setStyleAttribute(tabs, "overflow", "visible"); + updateDynamicWidth(); + } + + if (!isDynamicHeight()) { + // Must update height after the styles have been set + updateContentNodeHeight(); + updateOpenTabSize(); + } + + iLayout(); + + // Re run relative size update to ensure optimal scrollbars + // TODO isolate to situation that visible tab has undefined height + try { + client.handleComponentRelativeSize(tp.getWidget(tp + .getVisibleWidget())); + } catch (Exception e) { + // Ignore, most likely empty tabsheet + } + + renderInformation.updateSize(getElement()); + + waitingForResponse = false; + rendering = false; + } + + private void updateDynamicWidth() { + // Find tab width + int tabsWidth = 0; + + int count = tb.getTabCount(); + for (int i = 0; i < count; i++) { + Element tabTd = tb.getTab(i).getElement().getParentElement().cast(); + tabsWidth += tabTd.getOffsetWidth(); + } + + // Find content width + Style style = tp.getElement().getStyle(); + String overflow = style.getProperty("overflow"); + style.setProperty("overflow", "hidden"); + style.setPropertyPx("width", tabsWidth); + Style wrapperstyle = tp.getWidget(tp.getVisibleWidget()).getElement() + .getParentElement().getStyle(); + wrapperstyle.setPropertyPx("width", tabsWidth); + // Get content width from actual widget + + int contentWidth = 0; + if (tp.getWidgetCount() > 0) { + contentWidth = tp.getWidget(tp.getVisibleWidget()).getOffsetWidth(); + } + style.setProperty("overflow", overflow); + + // Set widths to max(tabs,content) + if (tabsWidth < contentWidth) { + tabsWidth = contentWidth; + } + + int outerWidth = tabsWidth + getContentAreaBorderWidth(); + + tabs.getStyle().setPropertyPx("width", outerWidth); + style.setPropertyPx("width", tabsWidth); + wrapperstyle.setPropertyPx("width", tabsWidth); + + contentNode.getStyle().setPropertyPx("width", tabsWidth); + super.setWidth(outerWidth + "px"); + updateOpenTabSize(); + } + + @Override + protected void renderTab(final UIDL tabUidl, int index, boolean selected, + boolean hidden) { + TabSheetCaption c = tb.getTab(index); + if (c == null) { + c = new TabSheetCaption(); + tb.addTab(c); + } + c.updateCaption(tabUidl); + + tb.setVisible(index, !hidden); + + /* + * Force the width of the caption container so the content will not wrap + * and tabs won't be too narrow in certain browsers + */ + c.setWidth(c.getRequiredWidth() + "px"); + captions.put("" + index, c); + + UIDL tabContentUIDL = null; + Paintable tabContent = null; + if (tabUidl.getChildCount() > 0) { + tabContentUIDL = tabUidl.getChildUIDL(0); + tabContent = client.getPaintable(tabContentUIDL); + } + + if (tabContent != null) { + /* This is a tab with content information */ + + int oldIndex = tp.getWidgetIndex((Widget) tabContent); + if (oldIndex != -1 && oldIndex != index) { + /* + * The tab has previously been rendered in another position so + * we must move the cached content to correct position + */ + tp.insert((Widget) tabContent, index); + } + } else { + /* A tab whose content has not yet been loaded */ + + /* + * Make sure there is a corresponding empty tab in tp. The same + * operation as the moving above but for not-loaded tabs. + */ + if (index < tp.getWidgetCount()) { + Widget oldWidget = tp.getWidget(index); + if (!(oldWidget instanceof PlaceHolder)) { + tp.insert(new PlaceHolder(), index); + } + } + + } + + if (selected) { + renderContent(tabContentUIDL); + tb.selectTab(index); + } else { + if (tabContentUIDL != null) { + // updating a drawn child on hidden tab + if (tp.getWidgetIndex((Widget) tabContent) < 0) { + tp.insert((Widget) tabContent, index); + } + tabContent.updateFromUIDL(tabContentUIDL, client); + } else if (tp.getWidgetCount() <= index) { + tp.add(new PlaceHolder()); + } + } + } + + public class PlaceHolder extends ILabel { + public PlaceHolder() { + super(""); + } + } + + @Override + protected void selectTab(int index, final UIDL contentUidl) { + if (index != activeTabIndex) { + activeTabIndex = index; + tb.selectTab(activeTabIndex); + } + renderContent(contentUidl); + } + + private void renderContent(final UIDL contentUIDL) { + final Paintable content = client.getPaintable(contentUIDL); + if (tp.getWidgetCount() > activeTabIndex) { + Widget old = tp.getWidget(activeTabIndex); + if (old != content) { + tp.remove(activeTabIndex); + if (old instanceof Paintable) { + client.unregisterPaintable((Paintable) old); + } + tp.insert((Widget) content, activeTabIndex); + } + } else { + tp.add((Widget) content); + } + + tp.showWidget(activeTabIndex); + + ITabsheet.this.iLayout(); + (content).updateFromUIDL(contentUIDL, client); + /* + * The size of a cached, relative sized component must be updated to + * report correct size to updateOpenTabSize(). + */ + if (contentUIDL.getBooleanAttribute("cached")) { + client.handleComponentRelativeSize((Widget) content); + } + updateOpenTabSize(); + ITabsheet.this.removeStyleDependentName("loading"); + if (previousVisibleWidget != null) { + DOM.setStyleAttribute(previousVisibleWidget.getElement(), + "visibility", ""); + previousVisibleWidget = null; + } + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + this.height = height; + updateContentNodeHeight(); + + if (!rendering) { + updateOpenTabSize(); + iLayout(); + // TODO Check if this is needed + client.runDescendentsLayout(this); + } + } + + private void updateContentNodeHeight() { + if (height != null && !"".equals(height)) { + int contentHeight = getOffsetHeight(); + contentHeight -= DOM.getElementPropertyInt(deco, "offsetHeight"); + contentHeight -= tb.getOffsetHeight(); + if (contentHeight < 0) { + contentHeight = 0; + } + + // Set proper values for content element + DOM.setStyleAttribute(contentNode, "height", contentHeight + "px"); + renderSpace.setHeight(contentHeight); + } else { + DOM.setStyleAttribute(contentNode, "height", ""); + renderSpace.setHeight(0); + } + } + + @Override + public void setWidth(String width) { + if ((this.width == null && width.equals("")) + || (this.width != null && this.width.equals(width))) { + return; + } + + super.setWidth(width); + if (width.equals("")) { + width = null; + } + this.width = width; + if (width == null) { + renderSpace.setWidth(0); + contentNode.getStyle().setProperty("width", ""); + } else { + int contentWidth = getOffsetWidth() - getContentAreaBorderWidth(); + if (contentWidth < 0) { + contentWidth = 0; + } + contentNode.getStyle().setProperty("width", contentWidth + "px"); + renderSpace.setWidth(contentWidth); + } + + if (!rendering) { + if (isDynamicHeight()) { + Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, tp, + this); + } + + updateOpenTabSize(); + iLayout(); + // TODO Check if this is needed + client.runDescendentsLayout(this); + + } + + } + + public void iLayout() { + updateTabScroller(); + tp.runWebkitOverflowAutoFix(); + } + + /** + * Sets the size of the visible tab (component). As the tab is set to + * position: absolute (to work around a firefox flickering bug) we must keep + * this up-to-date by hand. + */ + private void updateOpenTabSize() { + /* + * The overflow=auto element must have a height specified, otherwise it + * will be just as high as the contents and no scrollbars will appear + */ + int height = -1; + int width = -1; + int minWidth = 0; + + if (!isDynamicHeight()) { + height = renderSpace.getHeight(); + } + if (!isDynamicWidth()) { + width = renderSpace.getWidth(); + } else { + /* + * If the tabbar is wider than the content we need to use the tabbar + * width as minimum width so scrollbars get placed correctly (at the + * right edge). + */ + minWidth = tb.getOffsetWidth() - getContentAreaBorderWidth(); + } + tp.fixVisibleTabSize(width, height, minWidth); + + } + + /** + * Layouts the tab-scroller elements, and applies styles. + */ + private void updateTabScroller() { + if (width != null) { + DOM.setStyleAttribute(tabs, "width", width); + } + if (scrollerIndex > tb.getTabCount()) { + scrollerIndex = 0; + } + boolean scrolled = isScrolledTabs(); + boolean clipped = isClippedTabs(); + if (tb.isVisible() && (scrolled || clipped)) { + DOM.setStyleAttribute(scroller, "display", ""); + DOM.setElementProperty(scrollerPrev, "className", + SCROLLER_CLASSNAME + (scrolled ? "Prev" : "Prev-disabled")); + DOM.setElementProperty(scrollerNext, "className", + SCROLLER_CLASSNAME + (clipped ? "Next" : "Next-disabled")); + } else { + DOM.setStyleAttribute(scroller, "display", "none"); + } + + if (BrowserInfo.get().isSafari()) { + // fix tab height for safari, bugs sometimes if tabs contain icons + String property = tabs.getStyle().getProperty("height"); + if (property == null || property.equals("")) { + tabs.getStyle().setPropertyPx("height", tb.getOffsetHeight()); + } + /* + * another hack for webkits. tabscroller sometimes drops without + * "shaking it" reproducable in + * com.vaadin.tests.components.tabsheet.TabSheetIcons + */ + final Style style = scroller.getStyle(); + style.setProperty("whiteSpace", "normal"); + DeferredCommand.addCommand(new Command() { + public void execute() { + style.setProperty("whiteSpace", ""); + } + }); + } + + } + + private void showAllTabs() { + scrollerIndex = 0; + Element tr = DOM.getFirstChild(DOM.getFirstChild(tb.getElement())); + for (int i = 0; i < tb.getTabCount(); i++) { + DOM.setStyleAttribute(DOM.getChild(tr, i), "display", ""); + } + } + + private boolean isScrolledTabs() { + return scrollerIndex > 0; + } + + private boolean isClippedTabs() { + return tb.getOffsetWidth() > getOffsetWidth(); + } + + @Override + protected void clearPaintables() { + + int i = tb.getTabCount(); + while (i > 0) { + tb.removeTab(--i); + } + tp.clear(); + + } + + @Override + protected Iterator getPaintableIterator() { + return tp.iterator(); + } + + public boolean hasChildComponent(Widget component) { + if (tp.getWidgetIndex(component) < 0) { + return false; + } else { + return true; + } + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + tp.replaceComponent(oldComponent, newComponent); + } + + public void updateCaption(Paintable component, UIDL uidl) { + /* Tabsheet does not render its children's captions */ + } + + public boolean requestLayout(Set<Paintable> child) { + if (!isDynamicHeight() && !isDynamicWidth()) { + /* + * If the height and width has been specified for this container the + * child components cannot make the size of the layout change + */ + + return true; + } + + updateOpenTabSize(); + + if (renderInformation.updateSize(getElement())) { + /* + * Size has changed so we let the child components know about the + * new size. + */ + iLayout(); + client.runDescendentsLayout(this); + + return false; + } else { + /* + * Size has not changed so we do not need to propagate the event + * further + */ + return true; + } + + } + + private int borderW = -1; + + private int getContentAreaBorderWidth() { + if (borderW < 0) { + borderW = Util.measureHorizontalBorder(contentNode); + } + return borderW; + } + + private RenderSpace renderSpace = new RenderSpace(0, 0, true); + + public RenderSpace getAllocatedSpace(Widget child) { + // All tabs have equal amount of space allocated + return renderSpace; + } + + @Override + protected int getTabCount() { + return tb.getWidgetCount(); + } + + @Override + protected Paintable getTab(int index) { + if (tp.getWidgetCount() > index) { + return (Paintable) tp.getWidget(index); + } + return null; + } + + @Override + protected void removeTab(int index) { + tb.removeTab(index); + /* + * This must be checked because renderTab automatically removes the + * active tab content when it changes + */ + if (tp.getWidgetCount() > index) { + tp.remove(index); + } + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITabsheetBase.java b/src/com/vaadin/terminal/gwt/client/ui/ITabsheetBase.java new file mode 100644 index 0000000000..081f5977aa --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITabsheetBase.java @@ -0,0 +1,144 @@ +package com.vaadin.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Container; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; + +abstract class ITabsheetBase extends ComplexPanel implements Container { + + String id; + ApplicationConnection client; + + protected final ArrayList tabKeys = new ArrayList(); + protected int activeTabIndex = 0; + protected boolean disabled; + protected boolean readonly; + protected Set disabledTabKeys = new HashSet(); + protected boolean cachedUpdate = false; + + public ITabsheetBase(String classname) { + setElement(DOM.createDiv()); + setStylePrimaryName(classname); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + this.client = client; + + // Ensure correct implementation + cachedUpdate = client.updateComponent(this, uidl, true); + if (cachedUpdate) { + return; + } + + // Update member references + id = uidl.getId(); + disabled = uidl.hasAttribute("disabled"); + + // Render content + final UIDL tabs = uidl.getChildUIDL(0); + + // Paintables in the TabSheet before update + ArrayList oldPaintables = new ArrayList(); + for (Iterator iterator = getPaintableIterator(); iterator.hasNext();) { + oldPaintables.add(iterator.next()); + } + + // Clear previous values + tabKeys.clear(); + disabledTabKeys.clear(); + + int index = 0; + for (final Iterator it = tabs.getChildIterator(); it.hasNext();) { + final UIDL tab = (UIDL) it.next(); + final String key = tab.getStringAttribute("key"); + final boolean selected = tab.getBooleanAttribute("selected"); + final boolean hidden = tab.getBooleanAttribute("hidden"); + + if (tab.getBooleanAttribute("disabled")) { + disabledTabKeys.add(key); + } + + tabKeys.add(key); + + if (selected) { + activeTabIndex = index; + } + renderTab(tab, index, selected, hidden); + index++; + } + + int tabCount = getTabCount(); + while (tabCount-- > index) { + removeTab(index); + } + + for (int i = 0; i < getTabCount(); i++) { + Paintable p = getTab(i); + oldPaintables.remove(p); + } + + // Perform unregister for any paintables removed during update + for (Iterator iterator = oldPaintables.iterator(); iterator.hasNext();) { + Object oldPaintable = iterator.next(); + if (oldPaintable instanceof Paintable) { + Widget w = (Widget) oldPaintable; + if (w.isAttached()) { + w.removeFromParent(); + } + client.unregisterPaintable((Paintable) oldPaintable); + } + } + + } + + /** + * @return a list of currently shown Paintables + */ + abstract protected Iterator getPaintableIterator(); + + /** + * Clears current tabs and contents + */ + abstract protected void clearPaintables(); + + /** + * Implement in extending classes. This method should render needed elements + * and set the visibility of the tab according to the 'selected' parameter. + */ + protected abstract void renderTab(final UIDL tabUidl, int index, + boolean selected, boolean hidden); + + /** + * Implement in extending classes. This method should render any previously + * non-cached content and set the activeTabIndex property to the specified + * index. + */ + protected abstract void selectTab(int index, final UIDL contentUidl); + + /** + * Implement in extending classes. This method should return the number of + * tabs currently rendered. + */ + protected abstract int getTabCount(); + + /** + * Implement in extending classes. This method should return the Paintable + * corresponding to the given index. + */ + protected abstract Paintable getTab(int index); + + /** + * Implement in extending classes. This method should remove the rendered + * tab with the specified index. + */ + protected abstract void removeTab(int index); +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITabsheetPanel.java b/src/com/vaadin/terminal/gwt/client/ui/ITabsheetPanel.java new file mode 100644 index 0000000000..776e06b17b --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITabsheetPanel.java @@ -0,0 +1,183 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.Util;
+
+/**
+ * A panel that displays all of its child widgets in a 'deck', where only one
+ * can be visible at a time. It is used by
+ * {@link com.vaadin.terminal.gwt.client.ui.ITabsheet}.
+ *
+ * This class has the same basic functionality as the GWT DeckPanel
+ * {@link com.google.gwt.user.client.ui.DeckPanel}, with the exception that it
+ * doesn't manipulate the child widgets' width and height attributes.
+ */
+public class ITabsheetPanel extends ComplexPanel {
+
+ private Widget visibleWidget;
+
+ /**
+ * Creates an empty tabsheet panel.
+ */
+ public ITabsheetPanel() {
+ setElement(DOM.createDiv());
+ }
+
+ /**
+ * Adds the specified widget to the deck.
+ *
+ * @param w
+ * the widget to be added
+ */
+ @Override
+ public void add(Widget w) {
+ Element el = createContainerElement();
+ DOM.appendChild(getElement(), el);
+ super.add(w, el);
+ }
+
+ private Element createContainerElement() {
+ Element el = DOM.createDiv();
+ DOM.setStyleAttribute(el, "position", "absolute");
+ DOM.setStyleAttribute(el, "overflow", "auto");
+ hide(el);
+ return el;
+ }
+
+ /**
+ * Gets the index of the currently-visible widget.
+ *
+ * @return the visible widget's index
+ */
+ public int getVisibleWidget() {
+ return getWidgetIndex(visibleWidget);
+ }
+
+ /**
+ * Inserts a widget before the specified index.
+ *
+ * @param w
+ * the widget to be inserted
+ * @param beforeIndex
+ * the index before which it will be inserted
+ * @throws IndexOutOfBoundsException
+ * if <code>beforeIndex</code> is out of range
+ */
+ public void insert(Widget w, int beforeIndex) {
+ Element el = createContainerElement();
+ DOM.insertChild(getElement(), el, beforeIndex);
+ super.insert(w, el, beforeIndex, false);
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ Element child = w.getElement();
+ Element parent = null;
+ if (child != null) {
+ parent = DOM.getParent(child);
+ }
+ final boolean removed = super.remove(w);
+ if (removed) {
+ if (visibleWidget == w) {
+ visibleWidget = null;
+ }
+ if (parent != null) {
+ DOM.removeChild(getElement(), parent);
+ }
+ }
+ return removed;
+ }
+
+ /**
+ * Shows the widget at the specified index. This causes the currently-
+ * visible widget to be hidden.
+ *
+ * @param index
+ * the index of the widget to be shown
+ */
+ public void showWidget(int index) {
+ checkIndexBoundsForAccess(index);
+ Widget newVisible = getWidget(index);
+ if (visibleWidget != newVisible) {
+ if (visibleWidget != null) {
+ hide(DOM.getParent(visibleWidget.getElement()));
+ }
+ visibleWidget = newVisible;
+ unHide(DOM.getParent(visibleWidget.getElement()));
+ }
+ }
+
+ private void hide(Element e) {
+ DOM.setStyleAttribute(e, "visibility", "hidden");
+ DOM.setStyleAttribute(e, "top", "-100000px");
+ DOM.setStyleAttribute(e, "left", "-100000px");
+ }
+
+ private void unHide(Element e) {
+ DOM.setStyleAttribute(e, "top", "0px");
+ DOM.setStyleAttribute(e, "left", "0px");
+ DOM.setStyleAttribute(e, "visibility", "");
+ }
+
+ public void fixVisibleTabSize(int width, int height, int minWidth) {
+ if (visibleWidget == null) {
+ return;
+ }
+
+ boolean dynamicHeight = false;
+
+ if (height < 0) {
+ height = visibleWidget.getOffsetHeight();
+ dynamicHeight = true;
+ }
+ if (width < 0) {
+ width = visibleWidget.getOffsetWidth();
+ }
+ if (width < minWidth) {
+ width = minWidth;
+ }
+
+ Element wrapperDiv = (Element) visibleWidget.getElement()
+ .getParentElement();
+
+ // width first
+ getElement().getStyle().setPropertyPx("width", width);
+ wrapperDiv.getStyle().setPropertyPx("width", width);
+
+ if (dynamicHeight) {
+ // height of widget might have changed due wrapping
+ height = visibleWidget.getOffsetHeight();
+ }
+ // i-tabsheet-tabsheetpanel height
+ getElement().getStyle().setPropertyPx("height", height);
+
+ // widget wrapper height
+ wrapperDiv.getStyle().setPropertyPx("height", height);
+ runWebkitOverflowAutoFix();
+ }
+
+ public void runWebkitOverflowAutoFix() {
+ if (visibleWidget != null) {
+ Util.runWebkitOverflowAutoFix(DOM.getParent(visibleWidget
+ .getElement()));
+ }
+
+ }
+
+ public void replaceComponent(Widget oldComponent, Widget newComponent) {
+ boolean isVisible = (visibleWidget == oldComponent);
+ int widgetIndex = getWidgetIndex(oldComponent);
+ remove(oldComponent);
+ insert(newComponent, widgetIndex);
+ if (isVisible) {
+ showWidget(widgetIndex);
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITextArea.java b/src/com/vaadin/terminal/gwt/client/ui/ITextArea.java new file mode 100644 index 0000000000..7a03f85542 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITextArea.java @@ -0,0 +1,72 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+/**
+ * This class represents a multiline textfield (textarea).
+ *
+ * TODO consider replacing this with a RichTextArea based implementation. IE
+ * does not support CSS height for textareas in Strict mode :-(
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+public class ITextArea extends ITextField {
+ public static final String CLASSNAME = "i-textarea";
+
+ public ITextArea() {
+ super(DOM.createTextArea());
+ setStyleName(CLASSNAME);
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Call parent renderer explicitly
+ super.updateFromUIDL(uidl, client);
+
+ if (uidl.hasAttribute("rows")) {
+ setRows(new Integer(uidl.getStringAttribute("rows")).intValue());
+ }
+
+ if (getMaxLength() >= 0) {
+ sinkEvents(Event.ONKEYPRESS);
+ }
+ }
+
+ public void setRows(int rows) {
+ setRows(getElement(), rows);
+ }
+
+ private native void setRows(Element e, int r)
+ /*-{
+ try {
+ if(e.tagName.toLowerCase() == "textarea")
+ e.rows = r;
+ } catch (e) {}
+ }-*/;
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (getMaxLength() >= 0 && event.getTypeInt() == Event.ONKEYPRESS) {
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ if (getText().length() > getMaxLength()) {
+ setText(getText().substring(0, getMaxLength()));
+ }
+ }
+ });
+ }
+ super.onBrowserEvent(event);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITextField.java b/src/com/vaadin/terminal/gwt/client/ui/ITextField.java new file mode 100644 index 0000000000..74cd1cf329 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITextField.java @@ -0,0 +1,270 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.ChangeListener; +import com.google.gwt.user.client.ui.FocusListener; +import com.google.gwt.user.client.ui.TextBoxBase; +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.ITooltip; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +/** + * This class represents a basic text input field with one row. + * + * @author IT Mill Ltd. + * + */ +public class ITextField extends TextBoxBase implements Paintable, Field, + ChangeListener, FocusListener { + + /** + * The input node CSS classname. + */ + public static final String CLASSNAME = "i-textfield"; + /** + * This CSS classname is added to the input node on hover. + */ + public static final String CLASSNAME_FOCUS = "focus"; + + protected String id; + + protected ApplicationConnection client; + + private String valueBeforeEdit = null; + + private boolean immediate = false; + private int extraHorizontalPixels = -1; + private int extraVerticalPixels = -1; + private int maxLength = -1; + + private static final String CLASSNAME_PROMPT = "prompt"; + private static final String ATTR_INPUTPROMPT = "prompt"; + private String inputPrompt = null; + private boolean prompting = false; + + public ITextField() { + this(DOM.createInputText()); + } + + protected ITextField(Element node) { + super(node); + if (BrowserInfo.get().isIE()) { + // Fixes IE margin problem (#2058) + DOM.setStyleAttribute(node, "marginTop", "-1px"); + DOM.setStyleAttribute(node, "marginBottom", "-1px"); + } + setStyleName(CLASSNAME); + addChangeListener(this); + addFocusListener(this); + sinkEvents(ITooltip.TOOLTIP_EVENTS); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (client != null) { + client.handleTooltipEvent(event, this); + } + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + this.client = client; + id = uidl.getId(); + + if (client.updateComponent(this, uidl, true)) { + return; + } + + if (uidl.getBooleanAttribute("readonly")) { + setReadOnly(true); + } else { + setReadOnly(false); + } + + inputPrompt = uidl.getStringAttribute(ATTR_INPUTPROMPT); + + setMaxLength(uidl.hasAttribute("maxLength") ? uidl + .getIntAttribute("maxLength") : -1); + + immediate = uidl.getBooleanAttribute("immediate"); + + if (uidl.hasAttribute("cols")) { + setColumns(new Integer(uidl.getStringAttribute("cols")).intValue()); + } + + String text = uidl.getStringVariable("text"); + prompting = inputPrompt != null && (text == null || text.equals("")); + if (prompting) { + setText(inputPrompt); + addStyleDependentName(CLASSNAME_PROMPT); + } else { + setText(text); + removeStyleDependentName(CLASSNAME_PROMPT); + } + valueBeforeEdit = uidl.getStringVariable("text"); + } + + private void setMaxLength(int newMaxLength) { + if (newMaxLength > 0) { + maxLength = newMaxLength; + if (getElement().getTagName().toLowerCase().equals("textarea")) { + // NOP no maxlength property for textarea + } else { + getElement().setPropertyInt("maxLength", maxLength); + } + } else if (maxLength != -1) { + if (getElement().getTagName().toLowerCase().equals("textarea")) { + // NOP no maxlength property for textarea + } else { + getElement().setAttribute("maxlength", ""); + } + maxLength = -1; + } + + } + + protected int getMaxLength() { + return maxLength; + } + + public void onChange(Widget sender) { + if (client != null && id != null) { + String newText = getText(); + if (!prompting && newText != null + && !newText.equals(valueBeforeEdit)) { + client.updateVariable(id, "text", getText(), immediate); + valueBeforeEdit = newText; + } + } + } + + private static ITextField focusedTextField; + + public static void flushChangesFromFocusedTextField() { + if (focusedTextField != null) { + focusedTextField.onChange(null); + } + } + + public void onFocus(Widget sender) { + addStyleDependentName(CLASSNAME_FOCUS); + if (prompting) { + setText(""); + removeStyleDependentName(CLASSNAME_PROMPT); + } + focusedTextField = this; + } + + public void onLostFocus(Widget sender) { + removeStyleDependentName(CLASSNAME_FOCUS); + focusedTextField = null; + String text = getText(); + prompting = inputPrompt != null && (text == null || "".equals(text)); + if (prompting) { + setText(inputPrompt); + addStyleDependentName(CLASSNAME_PROMPT); + } + onChange(sender); + } + + public void setColumns(int columns) { + setColumns(getElement(), columns); + } + + private native void setColumns(Element e, int c) + /*-{ + try { + switch(e.tagName.toLowerCase()) { + case "input": + //e.size = c; + e.style.width = c+"em"; + break; + case "textarea": + //e.cols = c; + e.style.width = c+"em"; + break; + default:; + } + } catch (e) {} + }-*/; + + /** + * @return space used by components paddings and borders + */ + private int getExtraHorizontalPixels() { + if (extraHorizontalPixels < 0) { + detectExtraSizes(); + } + return extraHorizontalPixels; + } + + /** + * @return space used by components paddings and borders + */ + private int getExtraVerticalPixels() { + if (extraVerticalPixels < 0) { + detectExtraSizes(); + } + return extraVerticalPixels; + } + + /** + * Detects space used by components paddings and borders. Used when + * relational size are used. + */ + private void detectExtraSizes() { + Element clone = Util.cloneNode(getElement(), false); + DOM.setElementAttribute(clone, "id", ""); + DOM.setStyleAttribute(clone, "visibility", "hidden"); + DOM.setStyleAttribute(clone, "position", "absolute"); + // due FF3 bug set size to 10px and later subtract it from extra pixels + DOM.setStyleAttribute(clone, "width", "10px"); + DOM.setStyleAttribute(clone, "height", "10px"); + DOM.appendChild(DOM.getParent(getElement()), clone); + extraHorizontalPixels = DOM.getElementPropertyInt(clone, "offsetWidth") - 10; + extraVerticalPixels = DOM.getElementPropertyInt(clone, "offsetHeight") - 10; + + DOM.removeChild(DOM.getParent(getElement()), clone); + } + + @Override + public void setHeight(String height) { + if (height.endsWith("px")) { + int h = Integer.parseInt(height.substring(0, height.length() - 2)); + h -= getExtraVerticalPixels(); + if (h < 0) { + h = 0; + } + + super.setHeight(h + "px"); + } else { + super.setHeight(height); + } + } + + @Override + public void setWidth(String width) { + if (width.endsWith("px")) { + int w = Integer.parseInt(width.substring(0, width.length() - 2)); + w -= getExtraHorizontalPixels(); + if (w < 0) { + w = 0; + } + + super.setWidth(w + "px"); + } else { + super.setWidth(width); + } + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITextualDate.java b/src/com/vaadin/terminal/gwt/client/ui/ITextualDate.java new file mode 100644 index 0000000000..311390cbcc --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITextualDate.java @@ -0,0 +1,301 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.ChangeListener;
+import com.google.gwt.user.client.ui.TextBox;
+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.ClientExceptionHandler;
+import com.vaadin.terminal.gwt.client.ContainerResizedListener;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.LocaleNotLoadedException;
+import com.vaadin.terminal.gwt.client.LocaleService;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class ITextualDate extends IDateField implements Paintable, Field,
+ ChangeListener, ContainerResizedListener, Focusable {
+
+ private static final String PARSE_ERROR_CLASSNAME = CLASSNAME
+ + "-parseerror";
+
+ private final TextBox text;
+
+ private String formatStr;
+
+ private String width;
+
+ private boolean needLayout;
+
+ protected int fieldExtraWidth = -1;
+
+ public ITextualDate() {
+ super();
+ text = new TextBox();
+ // use normal textfield styles as a basis
+ text.setStyleName(ITextField.CLASSNAME);
+ // add datefield spesific style name also
+ text.addStyleName(CLASSNAME + "-textfield");
+ text.addChangeListener(this);
+ add(text);
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ int origRes = currentResolution;
+ super.updateFromUIDL(uidl, client);
+ if (origRes != currentResolution) {
+ // force recreating format string
+ formatStr = null;
+ }
+ if (uidl.hasAttribute("format")) {
+ formatStr = uidl.getStringAttribute("format");
+ }
+
+ buildDate();
+ // not a FocusWidget -> needs own tabindex handling
+ if (uidl.hasAttribute("tabindex")) {
+ text.setTabIndex(uidl.getIntAttribute("tabindex"));
+ }
+ }
+
+ protected String getFormatString() {
+ if (formatStr == null) {
+ if (currentResolution == RESOLUTION_YEAR) {
+ formatStr = "yyyy"; // force full year
+ } else {
+
+ try {
+ String frmString = LocaleService
+ .getDateFormat(currentLocale);
+ frmString = cleanFormat(frmString);
+ String delim = LocaleService
+ .getClockDelimiter(currentLocale);
+
+ if (currentResolution >= RESOLUTION_HOUR) {
+ if (dts.isTwelveHourClock()) {
+ frmString += " hh";
+ } else {
+ frmString += " HH";
+ }
+ if (currentResolution >= RESOLUTION_MIN) {
+ frmString += ":mm";
+ if (currentResolution >= RESOLUTION_SEC) {
+ frmString += ":ss";
+ if (currentResolution >= RESOLUTION_MSEC) {
+ frmString += ".SSS";
+ }
+ }
+ }
+ if (dts.isTwelveHourClock()) {
+ frmString += " aaa";
+ }
+
+ }
+
+ formatStr = frmString;
+ } catch (LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ }
+ }
+ return formatStr;
+ }
+
+ /**
+ *
+ */
+ protected void buildDate() {
+ removeStyleName(PARSE_ERROR_CLASSNAME);
+ // Create the initial text for the textfield
+ String dateText;
+ if (date != null) {
+ dateText = DateTimeFormat.getFormat(getFormatString()).format(date);
+ } else {
+ dateText = "";
+ }
+
+ text.setText(dateText);
+ text.setEnabled(enabled && !readonly);
+
+ if (readonly) {
+ text.addStyleName("i-readonly");
+ } else {
+ text.removeStyleName("i-readonly");
+ }
+
+ }
+
+ public void onChange(Widget sender) {
+ if (sender == text) {
+ if (!text.getText().equals("")) {
+ try {
+ DateTimeFormat format = DateTimeFormat
+ .getFormat(getFormatString());
+ date = format.parse(text.getText());
+ long stamp = date.getTime();
+ if (stamp == 0) {
+ // If date parsing fails in firefox the stamp will be 0
+ date = null;
+ addStyleName(PARSE_ERROR_CLASSNAME);
+ } else {
+ // remove possibly added invalid value indication
+ removeStyleName(PARSE_ERROR_CLASSNAME);
+ }
+ } catch (final Exception e) {
+ ClientExceptionHandler.displayError(e.getMessage());
+
+ addStyleName(PARSE_ERROR_CLASSNAME);
+ // this is a hack that may eventually be removed
+ client.updateVariable(id, "lastInvalidDateString", text
+ .getText(), false);
+ date = null;
+ }
+ } else {
+ date = null;
+ // remove possibly added invalid value indication
+ removeStyleName(PARSE_ERROR_CLASSNAME);
+ }
+ // always send the date string
+ client.updateVariable(id, "dateString", text.getText(), false);
+
+ if (date != null) {
+ showingDate = new Date(date.getTime());
+ }
+
+ // Update variables
+ // (only the smallest defining resolution needs to be
+ // immediate)
+ client.updateVariable(id, "year",
+ date != null ? date.getYear() + 1900 : -1,
+ currentResolution == IDateField.RESOLUTION_YEAR
+ && immediate);
+ if (currentResolution >= IDateField.RESOLUTION_MONTH) {
+ client.updateVariable(id, "month", date != null ? date
+ .getMonth() + 1 : -1,
+ currentResolution == IDateField.RESOLUTION_MONTH
+ && immediate);
+ }
+ if (currentResolution >= IDateField.RESOLUTION_DAY) {
+ client.updateVariable(id, "day", date != null ? date.getDate()
+ : -1, currentResolution == IDateField.RESOLUTION_DAY
+ && immediate);
+ }
+ if (currentResolution >= IDateField.RESOLUTION_HOUR) {
+ client.updateVariable(id, "hour", date != null ? date
+ .getHours() : -1,
+ currentResolution == IDateField.RESOLUTION_HOUR
+ && immediate);
+ }
+ if (currentResolution >= IDateField.RESOLUTION_MIN) {
+ client.updateVariable(id, "min", date != null ? date
+ .getMinutes() : -1,
+ currentResolution == IDateField.RESOLUTION_MIN
+ && immediate);
+ }
+ if (currentResolution >= IDateField.RESOLUTION_SEC) {
+ client.updateVariable(id, "sec", date != null ? date
+ .getSeconds() : -1,
+ currentResolution == IDateField.RESOLUTION_SEC
+ && immediate);
+ }
+ if (currentResolution == IDateField.RESOLUTION_MSEC) {
+ client.updateVariable(id, "msec",
+ date != null ? getMilliseconds() : -1, immediate);
+ }
+
+ }
+ }
+
+ private String cleanFormat(String format) {
+ // Remove unnecessary d & M if resolution is too low
+ if (currentResolution < IDateField.RESOLUTION_DAY) {
+ format = format.replaceAll("d", "");
+ }
+ if (currentResolution < IDateField.RESOLUTION_MONTH) {
+ format = format.replaceAll("M", "");
+ }
+
+ // Remove unsupported patterns
+ // TODO support for 'G', era designator (used at least in Japan)
+ format = format.replaceAll("[GzZwWkK]", "");
+
+ // Remove extra delimiters ('/' and '.')
+ while (format.startsWith("/") || format.startsWith(".")
+ || format.startsWith("-")) {
+ format = format.substring(1);
+ }
+ while (format.endsWith("/") || format.endsWith(".")
+ || format.endsWith("-")) {
+ format = format.substring(0, format.length() - 1);
+ }
+
+ // Remove duplicate delimiters
+ format = format.replaceAll("//", "/");
+ format = format.replaceAll("\\.\\.", ".");
+ format = format.replaceAll("--", "-");
+
+ return format.trim();
+ }
+
+ @Override
+ public void setWidth(String newWidth) {
+ if (!"".equals(newWidth) && (width == null || !newWidth.equals(width))) {
+ if (BrowserInfo.get().isIE6()) {
+ // in IE6 cols ~ min-width
+ DOM.setElementProperty(text.getElement(), "size", "1");
+ }
+ needLayout = true;
+ width = newWidth;
+ super.setWidth(width);
+ iLayout();
+ if (newWidth.indexOf("%") < 0) {
+ needLayout = false;
+ }
+ } else {
+ if ("".equals(newWidth) && width != null && !"".equals(width)) {
+ if (BrowserInfo.get().isIE6()) {
+ // revert IE6 hack
+ DOM.setElementProperty(text.getElement(), "size", "");
+ }
+ super.setWidth("");
+ needLayout = true;
+ iLayout();
+ needLayout = false;
+ width = null;
+ }
+ }
+ }
+
+ /**
+ * Returns pixels in x-axis reserved for other than textfield content.
+ *
+ * @return extra width in pixels
+ */
+ protected int getFieldExtraWidth() {
+ if (fieldExtraWidth < 0) {
+ text.setWidth("0px");
+ fieldExtraWidth = text.getOffsetWidth();
+ }
+ return fieldExtraWidth;
+ }
+
+ public void iLayout() {
+ if (needLayout) {
+ text.setWidth((getOffsetWidth() - getFieldExtraWidth()) + "px");
+ }
+ }
+
+ public void focus() {
+ text.setFocus(true);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITime.java b/src/com/vaadin/terminal/gwt/client/ui/ITime.java new file mode 100644 index 0000000000..43c1f9f261 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITime.java @@ -0,0 +1,317 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+
+import com.google.gwt.user.client.ui.ChangeListener;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Widget;
+
+public class ITime extends FlowPanel implements ChangeListener {
+
+ private final IDateField datefield;
+
+ private ListBox hours;
+
+ private ListBox mins;
+
+ private ListBox sec;
+
+ private ListBox msec;
+
+ private ListBox ampm;
+
+ private int resolution = IDateField.RESOLUTION_HOUR;
+
+ private boolean readonly;
+
+ public ITime(IDateField parent) {
+ super();
+ datefield = parent;
+ setStyleName(IDateField.CLASSNAME + "-time");
+ }
+
+ private void buildTime(boolean redraw) {
+ final boolean thc = datefield.getDateTimeService().isTwelveHourClock();
+ if (redraw) {
+ clear();
+ final int numHours = thc ? 12 : 24;
+ hours = new ListBox();
+ hours.setStyleName(INativeSelect.CLASSNAME);
+ for (int i = 0; i < numHours; i++) {
+ hours.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ hours.addChangeListener(this);
+ if (thc) {
+ ampm = new ListBox();
+ ampm.setStyleName(INativeSelect.CLASSNAME);
+ final String[] ampmText = datefield.getDateTimeService()
+ .getAmPmStrings();
+ ampm.addItem(ampmText[0]);
+ ampm.addItem(ampmText[1]);
+ ampm.addChangeListener(this);
+ }
+
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {
+ mins = new ListBox();
+ mins.setStyleName(INativeSelect.CLASSNAME);
+ for (int i = 0; i < 60; i++) {
+ mins.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ mins.addChangeListener(this);
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {
+ sec = new ListBox();
+ sec.setStyleName(INativeSelect.CLASSNAME);
+ for (int i = 0; i < 60; i++) {
+ sec.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ sec.addChangeListener(this);
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {
+ msec = new ListBox();
+ msec.setStyleName(INativeSelect.CLASSNAME);
+ for (int i = 0; i < 1000; i++) {
+ if (i < 10) {
+ msec.addItem("00" + i);
+ } else if (i < 100) {
+ msec.addItem("0" + i);
+ } else {
+ msec.addItem("" + i);
+ }
+ }
+ msec.addChangeListener(this);
+ }
+
+ final String delimiter = datefield.getDateTimeService()
+ .getClockDelimeter();
+ final boolean ro = datefield.isReadonly();
+
+ if (ro) {
+ int h = 0;
+ if (datefield.getCurrentDate() != null) {
+ h = datefield.getCurrentDate().getHours();
+ }
+ if (thc) {
+ h -= h < 12 ? 0 : 12;
+ }
+ add(new ILabel(h < 10 ? "0" + h : "" + h));
+ } else {
+ add(hours);
+ }
+
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {
+ add(new ILabel(delimiter));
+ if (ro) {
+ final int m = mins.getSelectedIndex();
+ add(new ILabel(m < 10 ? "0" + m : "" + m));
+ } else {
+ add(mins);
+ }
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {
+ add(new ILabel(delimiter));
+ if (ro) {
+ final int s = sec.getSelectedIndex();
+ add(new ILabel(s < 10 ? "0" + s : "" + s));
+ } else {
+ add(sec);
+ }
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {
+ add(new ILabel("."));
+ if (ro) {
+ final int m = datefield.getMilliseconds();
+ final String ms = m < 100 ? "0" + m : "" + m;
+ add(new ILabel(m < 10 ? "0" + ms : ms));
+ } else {
+ add(msec);
+ }
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_HOUR) {
+ add(new ILabel(delimiter + "00")); // o'clock
+ }
+ if (thc) {
+ add(new ILabel(" "));
+ if (ro) {
+ add(new ILabel(ampm.getItemText(datefield.getCurrentDate()
+ .getHours() < 12 ? 0 : 1)));
+ } else {
+ add(ampm);
+ }
+ }
+
+ if (ro) {
+ return;
+ }
+ }
+
+ // Update times
+ Date cdate = datefield.getCurrentDate();
+ boolean selected = true;
+ if (cdate == null) {
+ cdate = new Date();
+ selected = false;
+ }
+ if (thc) {
+ int h = cdate.getHours();
+ ampm.setSelectedIndex(h < 12 ? 0 : 1);
+ h -= ampm.getSelectedIndex() * 12;
+ hours.setSelectedIndex(h);
+ } else {
+ hours.setSelectedIndex(cdate.getHours());
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {
+ mins.setSelectedIndex(cdate.getMinutes());
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {
+ sec.setSelectedIndex(cdate.getSeconds());
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {
+ if (selected) {
+ msec.setSelectedIndex(datefield.getMilliseconds());
+ } else {
+ msec.setSelectedIndex(0);
+ }
+ }
+ if (thc) {
+ ampm.setSelectedIndex(cdate.getHours() < 12 ? 0 : 1);
+ }
+
+ if (datefield.isReadonly() && !redraw) {
+ // Do complete redraw when in read-only status
+ clear();
+ final String delimiter = datefield.getDateTimeService()
+ .getClockDelimeter();
+
+ int h = cdate.getHours();
+ if (thc) {
+ h -= h < 12 ? 0 : 12;
+ }
+ add(new ILabel(h < 10 ? "0" + h : "" + h));
+
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {
+ add(new ILabel(delimiter));
+ final int m = mins.getSelectedIndex();
+ add(new ILabel(m < 10 ? "0" + m : "" + m));
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {
+ add(new ILabel(delimiter));
+ final int s = sec.getSelectedIndex();
+ add(new ILabel(s < 10 ? "0" + s : "" + s));
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {
+ add(new ILabel("."));
+ final int m = datefield.getMilliseconds();
+ final String ms = m < 100 ? "0" + m : "" + m;
+ add(new ILabel(m < 10 ? "0" + ms : ms));
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_HOUR) {
+ add(new ILabel(delimiter + "00")); // o'clock
+ }
+ if (thc) {
+ add(new ILabel(" "));
+ add(new ILabel(ampm.getItemText(cdate.getHours() < 12 ? 0 : 1)));
+ }
+ }
+
+ final boolean enabled = datefield.isEnabled();
+ hours.setEnabled(enabled);
+ if (mins != null) {
+ mins.setEnabled(enabled);
+ }
+ if (sec != null) {
+ sec.setEnabled(enabled);
+ }
+ if (msec != null) {
+ msec.setEnabled(enabled);
+ }
+ if (ampm != null) {
+ ampm.setEnabled(enabled);
+ }
+
+ }
+
+ public void updateTime(boolean redraw) {
+ buildTime(redraw || resolution != datefield.getCurrentResolution()
+ || readonly != datefield.isReadonly());
+ if (datefield instanceof ITextualDate) {
+ ((ITextualDate) datefield).buildDate();
+ }
+ resolution = datefield.getCurrentResolution();
+ readonly = datefield.isReadonly();
+ }
+
+ public void onChange(Widget sender) {
+ if (datefield.getCurrentDate() == null) {
+ // was null on server, need to set
+ Date now = datefield.getShowingDate();
+ if (now == null) {
+ now = new Date();
+ datefield.setShowingDate(now);
+ }
+ datefield.setCurrentDate(new Date(now.getTime()));
+
+ // Init variables with current time
+ datefield.getClient().updateVariable(datefield.getId(), "year",
+ now.getYear() + 1900, false);
+ datefield.getClient().updateVariable(datefield.getId(), "month",
+ now.getMonth() + 1, false);
+ datefield.getClient().updateVariable(datefield.getId(), "day",
+ now.getDate(), false);
+ datefield.getClient().updateVariable(datefield.getId(), "hour",
+ now.getHours(), false);
+ datefield.getClient().updateVariable(datefield.getId(), "min",
+ now.getMinutes(), false);
+ datefield.getClient().updateVariable(datefield.getId(), "sec",
+ now.getSeconds(), false);
+ datefield.getClient().updateVariable(datefield.getId(), "msec",
+ datefield.getMilliseconds(), false);
+ }
+ if (sender == hours) {
+ int h = hours.getSelectedIndex();
+ if (datefield.getDateTimeService().isTwelveHourClock()) {
+ h = h + ampm.getSelectedIndex() * 12;
+ }
+ datefield.getCurrentDate().setHours(h);
+ datefield.getShowingDate().setHours(h);
+ datefield.getClient().updateVariable(datefield.getId(), "hour", h,
+ datefield.isImmediate());
+ updateTime(false);
+ } else if (sender == mins) {
+ final int m = mins.getSelectedIndex();
+ datefield.getCurrentDate().setMinutes(m);
+ datefield.getShowingDate().setMinutes(m);
+ datefield.getClient().updateVariable(datefield.getId(), "min", m,
+ datefield.isImmediate());
+ updateTime(false);
+ } else if (sender == sec) {
+ final int s = sec.getSelectedIndex();
+ datefield.getCurrentDate().setSeconds(s);
+ datefield.getShowingDate().setSeconds(s);
+ datefield.getClient().updateVariable(datefield.getId(), "sec", s,
+ datefield.isImmediate());
+ updateTime(false);
+ } else if (sender == msec) {
+ final int ms = msec.getSelectedIndex();
+ datefield.setMilliseconds(ms);
+ datefield.setShowingMilliseconds(ms);
+ datefield.getClient().updateVariable(datefield.getId(), "msec", ms,
+ datefield.isImmediate());
+ updateTime(false);
+ } else if (sender == ampm) {
+ final int h = hours.getSelectedIndex() + ampm.getSelectedIndex()
+ * 12;
+ datefield.getCurrentDate().setHours(h);
+ datefield.getShowingDate().setHours(h);
+ datefield.getClient().updateVariable(datefield.getId(), "hour", h,
+ datefield.isImmediate());
+ updateTime(false);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IToolkitOverlay.java b/src/com/vaadin/terminal/gwt/client/ui/IToolkitOverlay.java new file mode 100644 index 0000000000..8399544e4f --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IToolkitOverlay.java @@ -0,0 +1,313 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.animation.client.Animation; +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.PopupListener; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.RootPanel; +import com.vaadin.terminal.gwt.client.BrowserInfo; + +/** + * In Toolkit UI this Overlay should always be used for all elements that + * temporary float over other components like context menus etc. This is to deal + * stacking order correctly with IWindow objects. + */ +public class IToolkitOverlay extends PopupPanel { + + /* + * The z-index value from where all overlays live. This can be overridden in + * any extending class. + */ + protected static int Z_INDEX = 20000; + + /* + * Shadow element style. If an extending class wishes to use a different + * style of shadow, it can use setShadowStyle(String) to give the shadow + * element a new style name. + */ + public static final String CLASSNAME_SHADOW = "i-shadow"; + + /* + * The shadow element for this overlay. + */ + private Element shadow; + + /** + * The HTML snippet that is used to render the actual shadow. In consists of + * nine different DIV-elements with the following class names: + * + * <pre> + * .i-shadow[-stylename] + * ---------------------------------------------- + * | .top-left | .top | .top-right | + * |---------------|-----------|----------------| + * | | | | + * | .left | .center | .right | + * | | | | + * |---------------|-----------|----------------| + * | .bottom-left | .bottom | .bottom-right | + * ---------------------------------------------- + * </pre> + * + * See default theme 'shadow.css' for implementation example. + */ + private static final String SHADOW_HTML = "<div class=\"top-left\"></div><div class=\"top\"></div><div class=\"top-right\"></div><div class=\"left\"></div><div class=\"center\"></div><div class=\"right\"></div><div class=\"bottom-left\"></div><div class=\"bottom\"></div><div class=\"bottom-right\"></div>"; + + public IToolkitOverlay() { + super(); + adjustZIndex(); + } + + public IToolkitOverlay(boolean autoHide) { + super(autoHide); + adjustZIndex(); + } + + public IToolkitOverlay(boolean autoHide, boolean modal) { + super(autoHide, modal); + adjustZIndex(); + } + + public IToolkitOverlay(boolean autoHide, boolean modal, boolean showShadow) { + super(autoHide, modal); + if (showShadow) { + shadow = DOM.createDiv(); + shadow.setClassName(CLASSNAME_SHADOW); + shadow.setInnerHTML(SHADOW_HTML); + DOM.setStyleAttribute(shadow, "position", "absolute"); + + addPopupListener(new PopupListener() { + public void onPopupClosed(PopupPanel sender, boolean autoClosed) { + if (shadow.getParentElement() != null) { + shadow.getParentElement().removeChild(shadow); + } + } + }); + } + adjustZIndex(); + } + + private void adjustZIndex() { + setZIndex(Z_INDEX); + } + + /** + * Set the z-index (visual stack position) for this overlay. + * + * @param zIndex + * The new z-index + */ + protected void setZIndex(int zIndex) { + DOM.setStyleAttribute(getElement(), "zIndex", "" + zIndex); + if (shadow != null) { + DOM.setStyleAttribute(shadow, "zIndex", "" + zIndex); + } + if (BrowserInfo.get().isIE6()) { + adjustIE6Frame(getElement(), zIndex - 1); + } + } + + /** + * Get the z-index (visual stack position) of this overlay. + * + * @return The z-index for this overlay. + */ + private int getZIndex() { + return Integer.parseInt(DOM.getStyleAttribute(getElement(), "zIndex")); + } + + @Override + public void setPopupPosition(int left, int top) { + super.setPopupPosition(left, top); + if (shadow != null) { + updateShadowSizeAndPosition(isAnimationEnabled() ? 0 : 1); + } + } + + @Override + public void show() { + super.show(); + if (shadow != null) { + if (isAnimationEnabled()) { + ShadowAnimation sa = new ShadowAnimation(); + sa.run(200); + } else { + updateShadowSizeAndPosition(1.0); + } + } + if (BrowserInfo.get().isIE6()) { + adjustIE6Frame(getElement(), getZIndex()); + } + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (shadow != null) { + shadow.getStyle().setProperty("visibility", + visible ? "visible" : "hidden"); + } + } + + /* + * Needed to position overlays on top of native SELECT elements in IE6. See + * bug #2004 + */ + private native void adjustIE6Frame(Element popup, int zindex) + /*-{ + // relies on PopupImplIE6 + if(popup.__frame) + popup.__frame.style.zIndex = zindex; + }-*/; + + @Override + public void setWidth(String width) { + super.setWidth(width); + if (shadow != null) { + updateShadowSizeAndPosition(1.0); + } + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + if (shadow != null) { + updateShadowSizeAndPosition(1.0); + } + } + + /** + * Sets the shadow style for this overlay. Will override any previous style + * for the shadow. The default style name is defined by CLASSNAME_SHADOW. + * The given style will be prefixed with CLASSNAME_SHADOW. + * + * @param style + * The new style name for the shadow element. Will be prefixed by + * CLASSNAME_SHADOW, e.g. style=='foobar' -> actual style + * name=='i-shadow-foobar'. + */ + protected void setShadowStyle(String style) { + if (shadow != null) { + shadow.setClassName(CLASSNAME_SHADOW + "-" + style); + } + } + + /* + * Extending classes should always call this method after they change the + * size of overlay without using normal 'setWidth(String)' and + * 'setHeight(String)' methods (if not calling super.setWidth/Height). + */ + protected void updateShadowSizeAndPosition() { + updateShadowSizeAndPosition(1.0); + } + + /** + * Recalculates proper position and dimensions for the shadow element. Can + * be used to animate the shadow, using the 'progress' parameter (used to + * animate the shadow in sync with GWT PopupPanel's default animation + * 'PopupPanel.AnimationType.CENTER'). + * + * @param progress + * A value between 0.0 and 1.0, indicating the progress of the + * animation (0=start, 1=end). + */ + private void updateShadowSizeAndPosition(final double progress) { + // Don't do anything if overlay element is not attached + if (!isAttached()) { + return; + } + // Calculate proper z-index + String zIndex = null; + try { + // Odd behaviour with Windows Hosted Mode forces us to use + // this redundant try/catch block (See dev.itmill.com #2011) + zIndex = DOM.getStyleAttribute(getElement(), "zIndex"); + } catch (Exception ignore) { + // Ignored, will cause no harm + } + if (zIndex == null) { + zIndex = "" + Z_INDEX; + } + // Calculate position and size + if (BrowserInfo.get().isIE()) { + // Shake IE + getOffsetHeight(); + getOffsetWidth(); + } + + int x = getAbsoluteLeft(); + int y = getAbsoluteTop(); + + /* This is needed for IE7 at least */ + // Account for the difference between absolute position and the + // body's positioning context. + x -= Document.get().getBodyOffsetLeft(); + y -= Document.get().getBodyOffsetTop(); + + int width = getOffsetWidth(); + int height = getOffsetHeight(); + + if (width < 0) { + width = 0; + } + if (height < 0) { + height = 0; + } + + // Animate the shadow size + x += (int) (width * (1.0 - progress) / 2.0); + y += (int) (height * (1.0 - progress) / 2.0); + width = (int) (width * progress); + height = (int) (height * progress); + + // Opera needs some shaking to get parts of the shadow showing + // properly + // (ticket #2704) + if (BrowserInfo.get().isOpera()) { + // Clear the height of all middle elements + DOM.getChild(shadow, 3).getStyle().setProperty("height", "auto"); + DOM.getChild(shadow, 4).getStyle().setProperty("height", "auto"); + DOM.getChild(shadow, 5).getStyle().setProperty("height", "auto"); + } + + // Update correct values + DOM.setStyleAttribute(shadow, "zIndex", zIndex); + DOM.setStyleAttribute(shadow, "width", width + "px"); + DOM.setStyleAttribute(shadow, "height", height + "px"); + DOM.setStyleAttribute(shadow, "top", y + "px"); + DOM.setStyleAttribute(shadow, "left", x + "px"); + DOM.setStyleAttribute(shadow, "display", progress < 0.9 ? "none" : ""); + + // Opera fix, part 2 (ticket #2704) + if (BrowserInfo.get().isOpera()) { + // We'll fix the height of all the middle elements + DOM.getChild(shadow, 3).getStyle().setPropertyPx("height", + DOM.getChild(shadow, 3).getOffsetHeight()); + DOM.getChild(shadow, 4).getStyle().setPropertyPx("height", + DOM.getChild(shadow, 4).getOffsetHeight()); + DOM.getChild(shadow, 5).getStyle().setPropertyPx("height", + DOM.getChild(shadow, 5).getOffsetHeight()); + } + + // Attach to dom if not there already + if (shadow.getParentElement() == null) { + RootPanel.get().getElement().insertBefore(shadow, getElement()); + } + + } + + protected class ShadowAnimation extends Animation { + @Override + protected void onUpdate(double progress) { + if (shadow != null) { + updateShadowSizeAndPosition(progress); + } + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITree.java b/src/com/vaadin/terminal/gwt/client/ui/ITree.java new file mode 100644 index 0000000000..b322293964 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITree.java @@ -0,0 +1,469 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.BrowserInfo; +import com.vaadin.terminal.gwt.client.MouseEventDetails; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +/** + * + */ +public class ITree extends FlowPanel implements Paintable { + + public static final String CLASSNAME = "i-tree"; + + private Set<String> selectedIds = new HashSet<String>(); + private ApplicationConnection client; + private String paintableId; + private boolean selectable; + private boolean isMultiselect; + + private final HashMap<String, TreeNode> keyToNode = new HashMap<String, TreeNode>(); + + /** + * This map contains captions and icon urls for actions like: * "33_c" -> + * "Edit" * "33_i" -> "http://dom.com/edit.png" + */ + private final HashMap<String, String> actionMap = new HashMap<String, String>(); + + private boolean immediate; + + private boolean isNullSelectionAllowed = true; + + private boolean disabled = false; + + private boolean readonly; + + private boolean emitClickEvents; + + private boolean rendering; + + public ITree() { + super(); + setStyleName(CLASSNAME); + } + + private void updateActionMap(UIDL c) { + final Iterator it = c.getChildIterator(); + while (it.hasNext()) { + final UIDL action = (UIDL) it.next(); + final String key = action.getStringAttribute("key"); + final String caption = action.getStringAttribute("caption"); + actionMap.put(key + "_c", caption); + if (action.hasAttribute("icon")) { + // TODO need some uri handling ?? + actionMap.put(key + "_i", client.translateToolkitUri(action + .getStringAttribute("icon"))); + } + } + + } + + public String getActionCaption(String actionKey) { + return actionMap.get(actionKey + "_c"); + } + + public String getActionIcon(String actionKey) { + return actionMap.get(actionKey + "_i"); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + // Ensure correct implementation and let container manage caption + if (client.updateComponent(this, uidl, true)) { + return; + } + + rendering = true; + + this.client = client; + + if (uidl.hasAttribute("partialUpdate")) { + handleUpdate(uidl); + rendering = false; + return; + } + + paintableId = uidl.getId(); + + immediate = uidl.hasAttribute("immediate"); + + disabled = uidl.getBooleanAttribute("disabled"); + readonly = uidl.getBooleanAttribute("readonly"); + emitClickEvents = uidl.getBooleanAttribute("listenClicks"); + + isNullSelectionAllowed = uidl.getBooleanAttribute("nullselect"); + + clear(); + for (final Iterator i = uidl.getChildIterator(); i.hasNext();) { + final UIDL childUidl = (UIDL) i.next(); + if ("actions".equals(childUidl.getTag())) { + updateActionMap(childUidl); + continue; + } + final TreeNode childTree = new TreeNode(); + this.add(childTree); + childTree.updateFromUIDL(childUidl, client); + } + final String selectMode = uidl.getStringAttribute("selectmode"); + selectable = !"none".equals(selectMode); + isMultiselect = "multi".equals(selectMode); + + selectedIds = uidl.getStringArrayVariableAsSet("selected"); + + rendering = false; + + } + + private void handleUpdate(UIDL uidl) { + final TreeNode rootNode = keyToNode.get(uidl + .getStringAttribute("rootKey")); + if (rootNode != null) { + if (!rootNode.getState()) { + // expanding node happened server side + rootNode.setState(true, false); + } + rootNode.renderChildNodes(uidl.getChildIterator()); + } + + if (uidl.hasVariable("selected")) { + // update selection in case selected nodes were not visible + selectedIds = uidl.getStringArrayVariableAsSet("selected"); + } + + } + + public void setSelected(TreeNode treeNode, boolean selected) { + if (selected) { + if (!isMultiselect) { + while (selectedIds.size() > 0) { + final String id = selectedIds.iterator().next(); + final TreeNode oldSelection = keyToNode.get(id); + if (oldSelection != null) { + // can be null if the node is not visible (parent + // collapsed) + oldSelection.setSelected(false); + } + selectedIds.remove(id); + } + } + treeNode.setSelected(true); + selectedIds.add(treeNode.key); + } else { + if (!isNullSelectionAllowed) { + if (!isMultiselect || selectedIds.size() == 1) { + return; + } + } + selectedIds.remove(treeNode.key); + treeNode.setSelected(false); + } + client.updateVariable(paintableId, "selected", selectedIds.toArray(), + immediate); + } + + public boolean isSelected(TreeNode treeNode) { + return selectedIds.contains(treeNode.key); + } + + protected class TreeNode extends SimplePanel implements ActionOwner { + + public static final String CLASSNAME = "i-tree-node"; + + String key; + + private String[] actionKeys = null; + + private boolean childrenLoaded; + + private Element nodeCaptionDiv; + + protected Element nodeCaptionSpan; + + private FlowPanel childNodeContainer; + + private boolean open; + + private Icon icon; + + private Element ie6compatnode; + + public TreeNode() { + constructDom(); + sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.ONMOUSEUP + | Event.ONCONTEXTMENU); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (disabled) { + return; + } + final int type = DOM.eventGetType(event); + final Element target = DOM.eventGetTarget(event); + if (emitClickEvents && target == nodeCaptionSpan + && (type == Event.ONDBLCLICK || type == Event.ONMOUSEUP)) { + fireClick(event); + } + if (type == Event.ONCLICK) { + if (getElement() == target || ie6compatnode == target) { + // state change + toggleState(); + } else if (!readonly && target == nodeCaptionSpan) { + // caption click = selection change && possible click event + toggleSelection(); + } + DOM.eventCancelBubble(event, true); + } else if (type == Event.ONCONTEXTMENU) { + showContextMenu(event); + } + } + + private void fireClick(Event evt) { + // non-immediate iff an immediate select event is going to happen + boolean imm = !immediate + || !selectable + || (!isNullSelectionAllowed && isSelected() && selectedIds + .size() == 1); + MouseEventDetails details = new MouseEventDetails(evt); + client.updateVariable(paintableId, "clickedKey", key, false); + client.updateVariable(paintableId, "clickEvent", + details.toString(), imm); + } + + private void toggleSelection() { + if (selectable) { + ITree.this.setSelected(this, !isSelected()); + } + } + + private void toggleState() { + setState(!getState(), true); + } + + protected void constructDom() { + // workaround for a very weird IE6 issue #1245 + ie6compatnode = DOM.createDiv(); + setStyleName(ie6compatnode, CLASSNAME + "-ie6compatnode"); + DOM.setInnerText(ie6compatnode, " "); + DOM.appendChild(getElement(), ie6compatnode); + + DOM.sinkEvents(ie6compatnode, Event.ONCLICK); + + nodeCaptionDiv = DOM.createDiv(); + DOM.setElementProperty(nodeCaptionDiv, "className", CLASSNAME + + "-caption"); + Element wrapper = DOM.createDiv(); + nodeCaptionSpan = DOM.createSpan(); + DOM.appendChild(getElement(), nodeCaptionDiv); + DOM.appendChild(nodeCaptionDiv, wrapper); + DOM.appendChild(wrapper, nodeCaptionSpan); + + childNodeContainer = new FlowPanel(); + childNodeContainer.setStylePrimaryName(CLASSNAME + "-children"); + setWidget(childNodeContainer); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + setText(uidl.getStringAttribute("caption")); + key = uidl.getStringAttribute("key"); + + keyToNode.put(key, this); + + if (uidl.hasAttribute("al")) { + actionKeys = uidl.getStringArrayAttribute("al"); + } + + if (uidl.getTag().equals("node")) { + if (uidl.getChildCount() == 0) { + childNodeContainer.setVisible(false); + } else { + renderChildNodes(uidl.getChildIterator()); + childrenLoaded = true; + } + } else { + addStyleName(CLASSNAME + "-leaf"); + } + addStyleName(CLASSNAME); + + if (uidl.getBooleanAttribute("expanded") && !getState()) { + setState(true, false); + } + + if (uidl.getBooleanAttribute("selected")) { + setSelected(true); + } + + if (uidl.hasAttribute("icon")) { + if (icon == null) { + icon = new Icon(client); + DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv), icon + .getElement(), nodeCaptionSpan); + } + icon.setUri(uidl.getStringAttribute("icon")); + } else { + if (icon != null) { + DOM.removeChild(DOM.getFirstChild(nodeCaptionDiv), icon + .getElement()); + icon = null; + } + } + + if (BrowserInfo.get().isIE6() && isAttached()) { + fixWidth(); + } + } + + private void setState(boolean state, boolean notifyServer) { + if (open == state) { + return; + } + if (state) { + if (!childrenLoaded && notifyServer) { + client.updateVariable(paintableId, "requestChildTree", + true, false); + } + if (notifyServer) { + client.updateVariable(paintableId, "expand", + new String[] { key }, true); + } + addStyleName(CLASSNAME + "-expanded"); + childNodeContainer.setVisible(true); + + } else { + removeStyleName(CLASSNAME + "-expanded"); + childNodeContainer.setVisible(false); + if (notifyServer) { + client.updateVariable(paintableId, "collapse", + new String[] { key }, true); + } + } + open = state; + + if (!rendering) { + Util.notifyParentOfSizeChange(ITree.this, false); + } + } + + private boolean getState() { + return open; + } + + private void setText(String text) { + DOM.setInnerText(nodeCaptionSpan, text); + } + + private void renderChildNodes(Iterator i) { + childNodeContainer.clear(); + childNodeContainer.setVisible(true); + while (i.hasNext()) { + final UIDL childUidl = (UIDL) i.next(); + // actions are in bit weird place, don't mix them with children, + // but current node's actions + if ("actions".equals(childUidl.getTag())) { + updateActionMap(childUidl); + continue; + } + final TreeNode childTree = new TreeNode(); + childNodeContainer.add(childTree); + childTree.updateFromUIDL(childUidl, client); + } + childrenLoaded = true; + } + + public boolean isChildrenLoaded() { + return childrenLoaded; + } + + public Action[] getActions() { + if (actionKeys == null) { + return new Action[] {}; + } + final Action[] actions = new Action[actionKeys.length]; + for (int i = 0; i < actions.length; i++) { + final String actionKey = actionKeys[i]; + final TreeAction a = new TreeAction(this, String.valueOf(key), + actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + actions[i] = a; + } + return actions; + } + + public ApplicationConnection getClient() { + return client; + } + + public String getPaintableId() { + return paintableId; + } + + /** + * Adds/removes IT Mill Toolkit specific style name. This method ought + * to be called only from ITree. + * + * @param selected + */ + protected void setSelected(boolean selected) { + // add style name to caption dom structure only, not to subtree + setStyleName(nodeCaptionDiv, "i-tree-node-selected", selected); + } + + protected boolean isSelected() { + return ITree.this.isSelected(this); + } + + public void showContextMenu(Event event) { + if (!readonly && !disabled) { + if (actionKeys != null) { + int left = event.getClientX(); + int top = event.getClientY(); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + client.getContextMenu().showAt(this, left, top); + } + event.cancelBubble(true); + event.preventDefault(); + } + } + + /* + * We need to fix the width of TreeNodes so that the float in + * ie6compatNode does not wrap (see ticket #1245) + */ + private void fixWidth() { + nodeCaptionDiv.getStyle().setProperty("styleFloat", "left"); + nodeCaptionDiv.getStyle().setProperty("display", "inline"); + nodeCaptionDiv.getStyle().setProperty("marginLeft", "0"); + final int captionWidth = ie6compatnode.getOffsetWidth() + + nodeCaptionDiv.getOffsetWidth(); + setWidth(captionWidth + "px"); + } + + @Override + public void onAttach() { + super.onAttach(); + if (BrowserInfo.get().isIE6()) { + fixWidth(); + } + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITwinColSelect.java b/src/com/vaadin/terminal/gwt/client/ui/ITwinColSelect.java new file mode 100644 index 0000000000..c29f520f8d --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ITwinColSelect.java @@ -0,0 +1,244 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+import java.util.Vector;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.FlowPanel;
+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.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class ITwinColSelect extends IOptionGroupBase {
+
+ private static final String CLASSNAME = "i-select-twincol";
+
+ private static final int VISIBLE_COUNT = 10;
+
+ private static final int DEFAULT_COLUMN_COUNT = 10;
+
+ private final ListBox options;
+
+ private final ListBox selections;
+
+ private final IButton add;
+
+ private final IButton remove;
+
+ private FlowPanel buttons;
+
+ private Panel panel;
+
+ private boolean widthSet = false;
+
+ public ITwinColSelect() {
+ super(CLASSNAME);
+ options = new ListBox();
+ options.addClickListener(this);
+ selections = new ListBox();
+ selections.addClickListener(this);
+ options.setVisibleItemCount(VISIBLE_COUNT);
+ selections.setVisibleItemCount(VISIBLE_COUNT);
+ options.setStyleName(CLASSNAME + "-options");
+ selections.setStyleName(CLASSNAME + "-selections");
+ buttons = new FlowPanel();
+ buttons.setStyleName(CLASSNAME + "-buttons");
+ add = new IButton();
+ add.setText(">>");
+ add.addClickListener(this);
+ remove = new IButton();
+ remove.setText("<<");
+ remove.addClickListener(this);
+ panel = ((Panel) optionsContainer);
+ panel.add(options);
+ buttons.add(add);
+ final HTML br = new HTML("<span/>");
+ br.setStyleName(CLASSNAME + "-deco");
+ buttons.add(br);
+ buttons.add(remove);
+ panel.add(buttons);
+ panel.add(selections);
+ }
+
+ @Override
+ protected void buildOptions(UIDL uidl) {
+ final boolean enabled = !isDisabled() && !isReadonly();
+ options.setMultipleSelect(isMultiselect());
+ selections.setMultipleSelect(isMultiselect());
+ options.setEnabled(enabled);
+ selections.setEnabled(enabled);
+ add.setEnabled(enabled);
+ remove.setEnabled(enabled);
+ options.clear();
+ selections.clear();
+ for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
+ final UIDL optionUidl = (UIDL) i.next();
+ if (optionUidl.hasAttribute("selected")) {
+ selections.addItem(optionUidl.getStringAttribute("caption"),
+ optionUidl.getStringAttribute("key"));
+ } else {
+ options.addItem(optionUidl.getStringAttribute("caption"),
+ optionUidl.getStringAttribute("key"));
+ }
+ }
+
+ int cols = -1;
+ if (getColumns() > 0) {
+ cols = getColumns();
+ } else if (!widthSet) {
+ cols = DEFAULT_COLUMN_COUNT;
+ }
+
+ if (cols >= 0) {
+ options.setWidth(cols + "em");
+ selections.setWidth(cols + "em");
+ buttons.setWidth("3.5em");
+ optionsContainer.setWidth((2 * cols + 4) + "em");
+ }
+ if (getRows() > 0) {
+ options.setVisibleItemCount(getRows());
+ selections.setVisibleItemCount(getRows());
+
+ }
+
+ }
+
+ @Override
+ protected Object[] getSelectedItems() {
+ final Vector selectedItemKeys = new Vector();
+ for (int i = 0; i < selections.getItemCount(); i++) {
+ selectedItemKeys.add(selections.getValue(i));
+ }
+ return selectedItemKeys.toArray();
+ }
+
+ private boolean[] getItemsToAdd() {
+ final boolean[] selectedIndexes = new boolean[options.getItemCount()];
+ for (int i = 0; i < options.getItemCount(); i++) {
+ if (options.isItemSelected(i)) {
+ selectedIndexes[i] = true;
+ } else {
+ selectedIndexes[i] = false;
+ }
+ }
+ return selectedIndexes;
+ }
+
+ private boolean[] getItemsToRemove() {
+ final boolean[] selectedIndexes = new boolean[selections.getItemCount()];
+ for (int i = 0; i < selections.getItemCount(); i++) {
+ if (selections.isItemSelected(i)) {
+ selectedIndexes[i] = true;
+ } else {
+ selectedIndexes[i] = false;
+ }
+ }
+ return selectedIndexes;
+ }
+
+ @Override
+ public void onClick(Widget sender) {
+ super.onClick(sender);
+ if (sender == 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(),
+ isImmediate());
+
+ } else if (sender == 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(),
+ isImmediate());
+ } else if (sender == options) {
+ // unselect all in other list, to avoid mistakes (i.e wrong button)
+ final int c = selections.getItemCount();
+ for (int i = 0; i < c; i++) {
+ selections.setItemSelected(i, false);
+ }
+ } else if (sender == selections) {
+ // unselect all in other list, to avoid mistakes (i.e wrong button)
+ final int c = options.getItemCount();
+ for (int i = 0; i < c; i++) {
+ options.setItemSelected(i, false);
+ }
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ if ("".equals(height)) {
+ options.setHeight("");
+ selections.setHeight("");
+ } else {
+ setFullHeightInternals();
+ }
+ }
+
+ private void setFullHeightInternals() {
+ options.setHeight("100%");
+ selections.setHeight("100%");
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ if (!"".equals(width) && width != null) {
+ setRelativeInternalWidths();
+ }
+ }
+
+ private void setRelativeInternalWidths() {
+ DOM.setStyleAttribute(getElement(), "position", "relative");
+ buttons.setWidth("15%");
+ options.setWidth("42%");
+ selections.setWidth("42%");
+ widthSet = true;
+ }
+
+ @Override
+ protected void setTabIndex(int tabIndex) {
+ options.setTabIndex(tabIndex);
+ selections.setTabIndex(tabIndex);
+ add.setTabIndex(tabIndex);
+ remove.setTabIndex(tabIndex);
+ }
+
+ public void focus() {
+ options.setFocus(true);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IUnknownComponent.java b/src/com/vaadin/terminal/gwt/client/ui/IUnknownComponent.java new file mode 100644 index 0000000000..5715ab02c8 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IUnknownComponent.java @@ -0,0 +1,40 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.Tree; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; + +public class IUnknownComponent extends Composite implements Paintable { + + com.google.gwt.user.client.ui.Label caption = new com.google.gwt.user.client.ui.Label();; + Tree uidlTree = new Tree(); + + public IUnknownComponent() { + final VerticalPanel panel = new VerticalPanel(); + panel.add(caption); + panel.add(uidlTree); + initWidget(panel); + setStyleName("itmill-unknown"); + caption.setStyleName("itmill-unknown-caption"); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (client.updateComponent(this, uidl, false)) { + return; + } + setCaption("Client faced an unknown component type. Unrendered UIDL:"); + uidlTree.clear(); + uidlTree.addItem(uidl.dir()); + } + + public void setCaption(String c) { + caption.setText(c); + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IUpload.java b/src/com/vaadin/terminal/gwt/client/ui/IUpload.java new file mode 100644 index 0000000000..a03beed3e2 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IUpload.java @@ -0,0 +1,152 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.FileUpload; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.FormHandler; +import com.google.gwt.user.client.ui.FormPanel; +import com.google.gwt.user.client.ui.FormSubmitCompleteEvent; +import com.google.gwt.user.client.ui.FormSubmitEvent; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; + +public class IUpload extends FormPanel implements Paintable, ClickListener, + FormHandler { + + public static final String CLASSNAME = "i-upload"; + + /** + * FileUpload component that opens native OS dialog to select file. + */ + FileUpload fu = new FileUpload(); + + Panel panel = new FlowPanel(); + + ApplicationConnection client; + + private String paintableId; + + /** + * Button that initiates uploading + */ + private final Button submitButton; + + /** + * When expecting big files, programmer may initiate some UI changes when + * uploading the file starts. Bit after submitting file we'll visit the + * server to check possible changes. + */ + private Timer t; + + /** + * some browsers tries to send form twice if submit is called in button + * click handler, some don't submit at all without it, so we need to track + * if form is already being submitted + */ + private boolean submitted = false; + + private boolean enabled = true; + + public IUpload() { + super(); + setEncoding(FormPanel.ENCODING_MULTIPART); + setMethod(FormPanel.METHOD_POST); + + setWidget(panel); + panel.add(fu); + submitButton = new Button(); + submitButton.addClickListener(this); + panel.add(submitButton); + + addFormHandler(this); + + setStyleName(CLASSNAME); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (client.updateComponent(this, uidl, true)) { + return; + } + this.client = client; + paintableId = uidl.getId(); + setAction(client.getAppUri()); + submitButton.setText(uidl.getStringAttribute("buttoncaption")); + fu.setName(paintableId + "_file"); + + if (uidl.hasAttribute("disabled") || uidl.hasAttribute("readonly")) { + disableUpload(); + } else if (uidl.getBooleanAttribute("state")) { + enableUploaod(); + } + + } + + public void onClick(Widget sender) { + submit(); + } + + public void onSubmit(FormSubmitEvent event) { + if (fu.getFilename().length() == 0 || submitted || !enabled) { + event.setCancelled(true); + ApplicationConnection + .getConsole() + .log( + "Submit cancelled (disabled, no file or already submitted)"); + return; + } + // flush possibly pending variable changes, so they will be handled + // before upload + client.sendPendingVariableChanges(); + + submitted = true; + ApplicationConnection.getConsole().log("Submitted form"); + + disableUpload(); + + /* + * Visit server a moment after upload has started to see possible + * changes from UploadStarted event. Will be cleared on complete. + */ + t = new Timer() { + @Override + public void run() { + client.sendPendingVariableChanges(); + } + }; + t.schedule(800); + } + + protected void disableUpload() { + submitButton.setEnabled(false); + fu.setVisible(false); + enabled = false; + } + + protected void enableUploaod() { + submitButton.setEnabled(true); + fu.setVisible(true); + enabled = true; + } + + public void onSubmitComplete(FormSubmitCompleteEvent event) { + if (client != null) { + if (t != null) { + t.cancel(); + } + ApplicationConnection.getConsole().log("Submit complete"); + client.sendPendingVariableChanges(); + } + submitted = false; + enableUploaod(); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IUriFragmentUtility.java b/src/com/vaadin/terminal/gwt/client/ui/IUriFragmentUtility.java new file mode 100644 index 0000000000..d2237c5f60 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IUriFragmentUtility.java @@ -0,0 +1,67 @@ +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.History; +import com.google.gwt.user.client.HistoryListener; +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.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; + +/** + * Client side implementation for UriFragmentUtility. Uses GWT's History object + * as an implementation. + * + */ +public class IUriFragmentUtility extends Widget implements Paintable, + HistoryListener { + + private String fragment; + private ApplicationConnection client; + private String paintableId; + private boolean immediate; + + public IUriFragmentUtility() { + setElement(Document.get().createDivElement()); + if (BrowserInfo.get().isIE6()) { + getElement().getStyle().setProperty("overflow", "hidden"); + getElement().getStyle().setProperty("height", "0"); + } + History.addHistoryListener(this); + History.fireCurrentHistoryState(); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (client.updateComponent(this, uidl, false)) { + return; + } + String uidlFragment = uidl.getStringVariable("fragment"); + immediate = uidl.getBooleanAttribute("immediate"); + if (this.client == null) { + // initial paint has some special logic + this.client = client; + paintableId = uidl.getId(); + if (!fragment.equals(uidlFragment)) { + // initial server side fragment (from link/bookmark/typed) does + // not equal the one on + // server, send initial fragment to server + History.fireCurrentHistoryState(); + } + } else { + if (uidlFragment != null && !uidlFragment.equals(fragment)) { + fragment = uidlFragment; + // normal fragment change from server, add new history item + History.newItem(uidlFragment, false); + } + } + } + + public void onHistoryChanged(String historyToken) { + fragment = historyToken; + if (client != null) { + client.updateVariable(paintableId, "fragment", fragment, immediate); + } + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IVerticalLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IVerticalLayout.java new file mode 100644 index 0000000000..9adeb8158e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IVerticalLayout.java @@ -0,0 +1,11 @@ +package com.vaadin.terminal.gwt.client.ui;
+
+public class IVerticalLayout extends IOrderedLayout {
+
+ public static final String CLASSNAME = "i-verticallayout";
+
+ public IVerticalLayout() {
+ super(CLASSNAME, ORIENTATION_VERTICAL);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IView.java b/src/com/vaadin/terminal/gwt/client/ui/IView.java new file mode 100644 index 0000000000..2d8ce9a986 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IView.java @@ -0,0 +1,575 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +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.WindowCloseListener; +import com.google.gwt.user.client.WindowResizeListener; +import com.google.gwt.user.client.ui.HasFocus; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.SimplePanel; +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.Paintable; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +/** + * + */ +public class IView extends SimplePanel implements Container, + WindowResizeListener, WindowCloseListener { + + private static final String CLASSNAME = "i-view"; + + private String theme; + + private Paintable layout; + + private final LinkedHashSet<IWindow> subWindows = new LinkedHashSet<IWindow>(); + + private String id; + + private ShortcutActionHandler actionHandler; + + /** stored width for IE resize optimization */ + private int width; + + /** stored height for IE resize optimization */ + private int height; + + private ApplicationConnection connection; + + /** + * We are postponing resize process with IE. IE bugs with scrollbars in some + * situations, that causes false onWindowResized calls. With Timer we will + * give IE some time to decide if it really wants to keep current size + * (scrollbars). + */ + private Timer resizeTimer; + + private int scrollTop; + + private int scrollLeft; + + private boolean rendering; + + private boolean scrollable; + + private boolean immediate; + + public IView(String elementId) { + super(); + setStyleName(CLASSNAME); + + DOM.sinkEvents(getElement(), Event.ONKEYDOWN | Event.ONSCROLL); + + // iview is focused when created so element needs tabIndex + // 1 due 0 is at the end of natural tabbing order + DOM.setElementProperty(getElement(), "tabIndex", "1"); + + RootPanel root = RootPanel.get(elementId); + root.add(this); + root.removeStyleName("i-app-loading"); + + BrowserInfo browser = BrowserInfo.get(); + + // set focus to iview element by default to listen possible keyboard + // shortcuts + if (browser.isOpera() || browser.isSafari() + && browser.getWebkitVersion() < 526) { + // old webkits don't support focusing div elements + Element fElem = DOM.createInputCheck(); + DOM.setStyleAttribute(fElem, "margin", "0"); + DOM.setStyleAttribute(fElem, "padding", "0"); + DOM.setStyleAttribute(fElem, "border", "0"); + DOM.setStyleAttribute(fElem, "outline", "0"); + DOM.setStyleAttribute(fElem, "width", "1px"); + DOM.setStyleAttribute(fElem, "height", "1px"); + DOM.setStyleAttribute(fElem, "position", "absolute"); + DOM.setStyleAttribute(fElem, "opacity", "0.1"); + DOM.appendChild(getElement(), fElem); + focus(fElem); + } else { + focus(getElement()); + } + + } + + private static native void focus(Element el) + /*-{ + try { + el.focus(); + } catch (e) { + + } + }-*/; + + public String getTheme() { + return theme; + } + + /** + * Used to reload host page on theme changes. + */ + private static native void reloadHostPage() + /*-{ + $wnd.location.reload(); + }-*/; + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + rendering = true; + + id = uidl.getId(); + boolean firstPaint = connection == null; + connection = client; + + immediate = uidl.hasAttribute("immediate"); + + String newTheme = uidl.getStringAttribute("theme"); + if (theme != null && !newTheme.equals(theme)) { + // Complete page refresh is needed due css can affect layout + // calculations etc + reloadHostPage(); + } else { + theme = newTheme; + } + if (uidl.hasAttribute("style")) { + addStyleName(uidl.getStringAttribute("style")); + } + + if (uidl.hasAttribute("name")) { + client.setWindowName(uidl.getStringAttribute("name")); + } + + com.google.gwt.user.client.Window.setTitle(uidl + .getStringAttribute("caption")); + + // Process children + int childIndex = 0; + + // Open URL:s + boolean isClosed = false; // was this window closed? + while (childIndex < uidl.getChildCount() + && "open".equals(uidl.getChildUIDL(childIndex).getTag())) { + final UIDL open = uidl.getChildUIDL(childIndex); + final String url = open.getStringAttribute("src"); + final String target = open.getStringAttribute("name"); + if (target == null) { + // This window is closing. Send close event before + // going to the new url + isClosed = true; + onWindowClosed(); + goTo(url); + } else { + String options; + if (open.hasAttribute("border")) { + if (open.getStringAttribute("border").equals("minimal")) { + options = "menubar=yes,location=no,status=no"; + } else { + options = "menubar=no,location=no,status=no"; + } + + } else { + options = "resizable=yes,menubar=yes,toolbar=yes,directories=yes,location=yes,scrollbars=yes,status=yes"; + } + + if (open.hasAttribute("width")) { + int w = open.getIntAttribute("width"); + options += ",width=" + w; + } + if (open.hasAttribute("height")) { + int h = open.getIntAttribute("height"); + options += ",height=" + h; + } + + Window.open(url, target, options); + } + childIndex++; + } + if (isClosed) { + // don't render the content + rendering = false; + return; + } + + // Draw this application level window + UIDL childUidl = uidl.getChildUIDL(childIndex); + final Paintable lo = client.getPaintable(childUidl); + + if (layout != null) { + if (layout != lo) { + // remove old + client.unregisterPaintable(layout); + // add new + setWidget((Widget) lo); + layout = lo; + } + } else { + setWidget((Widget) lo); + layout = lo; + } + + layout.updateFromUIDL(childUidl, client); + + // Update subwindows + final HashSet<IWindow> removedSubWindows = new HashSet<IWindow>( + subWindows); + + // Open new windows + while ((childUidl = uidl.getChildUIDL(childIndex++)) != null) { + if ("window".equals(childUidl.getTag())) { + final Paintable w = client.getPaintable(childUidl); + if (subWindows.contains(w)) { + removedSubWindows.remove(w); + } else { + subWindows.add((IWindow) w); + } + w.updateFromUIDL(childUidl, client); + } else if ("actions".equals(childUidl.getTag())) { + if (actionHandler == null) { + actionHandler = new ShortcutActionHandler(id, client); + } + actionHandler.updateActionMap(childUidl); + } else if (childUidl.getTag().equals("notifications")) { + for (final Iterator it = childUidl.getChildIterator(); it + .hasNext();) { + final UIDL notification = (UIDL) it.next(); + String html = ""; + if (notification.hasAttribute("icon")) { + final String parsedUri = client + .translateToolkitUri(notification + .getStringAttribute("icon")); + html += "<IMG src=\"" + parsedUri + "\" />"; + } + if (notification.hasAttribute("caption")) { + html += "<H1>" + + notification.getStringAttribute("caption") + + "</H1>"; + } + if (notification.hasAttribute("message")) { + html += "<p>" + + notification.getStringAttribute("message") + + "</p>"; + } + + final String style = notification.hasAttribute("style") ? notification + .getStringAttribute("style") + : null; + final int position = notification + .getIntAttribute("position"); + final int delay = notification.getIntAttribute("delay"); + new INotification(delay).show(html, position, style); + } + } + } + + // Close old windows + for (final Iterator<IWindow> rem = removedSubWindows.iterator(); rem + .hasNext();) { + final IWindow w = rem.next(); + client.unregisterPaintable(w); + subWindows.remove(w); + w.hide(); + } + + if (uidl.hasAttribute("focused")) { + final String focusPid = uidl.getStringAttribute("focused"); + // set focused component when render phase is finished + DeferredCommand.addCommand(new Command() { + public void execute() { + final Paintable toBeFocused = connection + .getPaintable(focusPid); + + /* + * Two types of Widgets can be focused, either implementing + * GWT HasFocus of a thinner Toolkit specific Focusable + * interface. + */ + if (toBeFocused instanceof HasFocus) { + final HasFocus toBeFocusedWidget = (HasFocus) toBeFocused; + toBeFocusedWidget.setFocus(true); + } else if (toBeFocused instanceof Focusable) { + ((Focusable) toBeFocused).focus(); + } else { + ApplicationConnection.getConsole().log( + "Could not focus component"); + } + } + }); + } + + // Add window listeners on first paint, to prevent premature + // variablechanges + if (firstPaint) { + Window.addWindowCloseListener(this); + Window.addWindowResizeListener(this); + } + + onWindowResized(Window.getClientWidth(), Window.getClientHeight()); + + if (BrowserInfo.get().isSafari()) { + Util.runWebkitOverflowAutoFix(getElement()); + } + + // finally set scroll position from UIDL + if (uidl.hasVariable("scrollTop")) { + scrollable = true; + scrollTop = uidl.getIntVariable("scrollTop"); + DOM.setElementPropertyInt(getElement(), "scrollTop", scrollTop); + scrollLeft = uidl.getIntVariable("scrollLeft"); + DOM.setElementPropertyInt(getElement(), "scrollLeft", scrollLeft); + } else { + scrollable = false; + } + + rendering = false; + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + int type = DOM.eventGetType(event); + if (type == Event.ONKEYDOWN && actionHandler != null) { + actionHandler.handleKeyboardEvent(event); + return; + } else if (scrollable && type == Event.ONSCROLL) { + updateScrollPosition(); + } + } + + /** + * Updates scroll position from DOM and saves variables to server. + */ + private void updateScrollPosition() { + int oldTop = scrollTop; + int oldLeft = scrollLeft; + scrollTop = DOM.getElementPropertyInt(getElement(), "scrollTop"); + scrollLeft = DOM.getElementPropertyInt(getElement(), "scrollLeft"); + if (connection != null && !rendering) { + if (oldTop != scrollTop) { + connection.updateVariable(id, "scrollTop", scrollTop, false); + } + if (oldLeft != scrollLeft) { + connection.updateVariable(id, "scrollLeft", scrollLeft, false); + } + } + } + + public void onWindowResized(int width, int height) { + if (BrowserInfo.get().isIE()) { + /* + * IE will give us some false resized events due bugs with + * scrollbars. Postponing layout phase to see if size was really + * changed. + */ + if (resizeTimer == null) { + resizeTimer = new Timer() { + @Override + public void run() { + boolean changed = false; + if (IView.this.width != getOffsetWidth()) { + IView.this.width = getOffsetWidth(); + changed = true; + ApplicationConnection.getConsole().log( + "window w" + IView.this.width); + } + if (IView.this.height != getOffsetHeight()) { + IView.this.height = getOffsetHeight(); + changed = true; + ApplicationConnection.getConsole().log( + "window h" + IView.this.height); + } + if (changed) { + ApplicationConnection + .getConsole() + .log( + "Running layout functions due window resize"); + connection.runDescendentsLayout(IView.this); + + sendClientResized(); + } + } + }; + } else { + resizeTimer.cancel(); + } + resizeTimer.schedule(200); + } else { + if (width == IView.this.width && height == IView.this.height) { + // No point in doing resize operations if window size has not + // changed + return; + } + + IView.this.width = Window.getClientWidth(); + IView.this.height = Window.getClientHeight(); + + ApplicationConnection.getConsole().log( + "Running layout functions due window resize"); + + connection.runDescendentsLayout(this); + Util.runWebkitOverflowAutoFix(getElement()); + + sendClientResized(); + } + + } + + /** + * Send new dimensions to the server. + */ + private void sendClientResized() { + connection.updateVariable(id, "height", height, false); + connection.updateVariable(id, "width", width, immediate); + } + + public native static void goTo(String url) + /*-{ + $wnd.location = url; + }-*/; + + public void onWindowClosed() { + // Change focus on this window in order to ensure that all state is + // collected from textfields + ITextField.flushChangesFromFocusedTextField(); + + // Send the closing state to server + connection.updateVariable(id, "close", true, false); + connection.sendPendingVariableChangesSync(); + } + + public String onWindowClosing() { + return null; + } + + private final RenderSpace myRenderSpace = new RenderSpace() { + private int excessHeight = -1; + private int excessWidth = -1; + + @Override + public int getHeight() { + return getElement().getOffsetHeight() - getExcessHeight(); + } + + private int getExcessHeight() { + if (excessHeight < 0) { + detectExcessSize(); + } + return excessHeight; + } + + private void detectExcessSize() { + // TODO define that iview cannot be themed and decorations should + // get to parent element, then get rid of this expensive and error + // prone function + final String overflow = getElement().getStyle().getProperty( + "overflow"); + getElement().getStyle().setProperty("overflow", "hidden"); + if (BrowserInfo.get().isIE() + && getElement().getPropertyInt("clientWidth") == 0) { + // can't detect possibly themed border/padding width in some + // situations (with some layout configurations), use empty div + // to measure width properly + DivElement div = Document.get().createDivElement(); + div.setInnerHTML(" "); + div.getStyle().setProperty("overflow", "hidden"); + div.getStyle().setProperty("height", "1px"); + getElement().appendChild(div); + excessWidth = getElement().getOffsetWidth() + - div.getOffsetWidth(); + getElement().removeChild(div); + } else { + excessWidth = getElement().getOffsetWidth() + - getElement().getPropertyInt("clientWidth"); + } + excessHeight = getElement().getOffsetHeight() + - getElement().getPropertyInt("clientHeight"); + + getElement().getStyle().setProperty("overflow", overflow); + } + + @Override + public int getWidth() { + return getElement().getOffsetWidth() - getExcessWidth(); + } + + private int getExcessWidth() { + if (excessWidth < 0) { + detectExcessSize(); + } + return excessWidth; + } + + @Override + public int getScrollbarSize() { + return Util.getNativeScrollbarSize(); + } + }; + + public RenderSpace getAllocatedSpace(Widget child) { + return myRenderSpace; + } + + public boolean hasChildComponent(Widget component) { + return (component != null && component == layout); + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + // TODO This is untested as no layouts require this + if (oldComponent != layout) { + return; + } + + setWidget(newComponent); + layout = (Paintable) newComponent; + } + + public boolean requestLayout(Set<Paintable> child) { + /* + * Can never propagate further and we do not want need to re-layout the + * layout which has caused this request. + */ + return true; + + } + + public void updateCaption(Paintable component, UIDL uidl) { + // NOP Subwindows never draw caption for their first child (layout) + } + + /** + * Return an iterator for current subwindows. This method is meant for + * testing purposes only. + * + * @return + */ + public ArrayList<IWindow> getSubWindowList() { + ArrayList<IWindow> windows = new ArrayList<IWindow>(subWindows.size()); + for (IWindow widget : subWindows) { + windows.add(widget); + } + return windows; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/IWindow.java b/src/com/vaadin/terminal/gwt/client/ui/IWindow.java new file mode 100644 index 0000000000..eaa1c99da1 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/IWindow.java @@ -0,0 +1,1001 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.Iterator; +import java.util.Set; +import java.util.Vector; + +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Frame; +import com.google.gwt.user.client.ui.HasWidgets; +import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.ScrollListener; +import com.google.gwt.user.client.ui.ScrollPanel; +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.Paintable; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; + +/** + * "Sub window" component. + * + * TODO update position / scroll position / size to client + * + * @author IT Mill Ltd + */ +public class IWindow extends IToolkitOverlay implements Container, + ScrollListener { + + private static final int MIN_HEIGHT = 100; + + private static final int MIN_WIDTH = 150; + + private static Vector<IWindow> windowOrder = new Vector<IWindow>(); + + public static final String CLASSNAME = "i-window"; + + /** + * Pixels used by inner borders and paddings horizontally (calculated only + * once) + */ + private int borderWidth = -1; + + /** + * Pixels used by inner borders and paddings vertically (calculated only + * once) + */ + private int borderHeight = -1; + + private static final int STACKING_OFFSET_PIXELS = 15; + + public static final int Z_INDEX = 10000; + + private Paintable layout; + + private Element contents; + + private Element header; + + private Element footer; + + private Element resizeBox; + + private final ScrollPanel contentPanel = new ScrollPanel(); + + private boolean dragging; + + private int startX; + + private int startY; + + private int origX; + + private int origY; + + private boolean resizing; + + private int origW; + + private int origH; + + private Element closeBox; + + protected ApplicationConnection client; + + private String id; + + ShortcutActionHandler shortcutHandler; + + /** Last known positionx read from UIDL or updated to application connection */ + private int uidlPositionX = -1; + + /** Last known positiony read from UIDL or updated to application connection */ + private int uidlPositionY = -1; + + private boolean vaadinModality = false; + + private boolean resizable = true; + + private Element modalityCurtain; + private Element draggingCurtain; + + private Element headerText; + + private boolean readonly; + + boolean dynamicWidth = false; + boolean dynamicHeight = false; + boolean layoutRelativeWidth = false; + boolean layoutRelativeHeight = false; + + // If centered (via UIDL), the window should stay in the centered -mode + // until a position is received from the server, or the user moves or + // resizes the window. + boolean centered = false; + + private RenderSpace renderSpace = new RenderSpace(MIN_WIDTH, MIN_HEIGHT, + true); + + private String width; + + private String height; + + private boolean immediate; + + public IWindow() { + super(false, false, true); // no autohide, not modal, shadow + // Different style of shadow for windows + setShadowStyle("window"); + + final int order = windowOrder.size(); + setWindowOrder(order); + windowOrder.add(this); + constructDOM(); + setPopupPosition(order * STACKING_OFFSET_PIXELS, order + * STACKING_OFFSET_PIXELS); + contentPanel.addScrollListener(this); + } + + private void bringToFront() { + int curIndex = windowOrder.indexOf(this); + if (curIndex + 1 < windowOrder.size()) { + windowOrder.remove(this); + windowOrder.add(this); + for (; curIndex < windowOrder.size(); curIndex++) { + windowOrder.get(curIndex).setWindowOrder(curIndex); + } + } + } + + /** + * Returns true if window is the topmost window + * + * @return + */ + private boolean isActive() { + return windowOrder.lastElement().equals(this); + } + + public void setWindowOrder(int order) { + setZIndex(order + Z_INDEX); + } + + @Override + protected void setZIndex(int zIndex) { + super.setZIndex(zIndex); + if (vaadinModality) { + DOM.setStyleAttribute(modalityCurtain, "zIndex", "" + zIndex); + } + } + + protected void constructDOM() { + setStyleName(CLASSNAME); + + header = DOM.createDiv(); + DOM.setElementProperty(header, "className", CLASSNAME + "-outerheader"); + headerText = DOM.createDiv(); + DOM.setElementProperty(headerText, "className", CLASSNAME + "-header"); + contents = DOM.createDiv(); + DOM.setElementProperty(contents, "className", CLASSNAME + "-contents"); + footer = DOM.createDiv(); + DOM.setElementProperty(footer, "className", CLASSNAME + "-footer"); + resizeBox = DOM.createDiv(); + DOM + .setElementProperty(resizeBox, "className", CLASSNAME + + "-resizebox"); + closeBox = DOM.createDiv(); + DOM.setElementProperty(closeBox, "className", CLASSNAME + "-closebox"); + DOM.appendChild(footer, resizeBox); + + DOM.sinkEvents(getElement(), Event.ONLOSECAPTURE); + DOM.sinkEvents(closeBox, Event.ONCLICK); + DOM.sinkEvents(contents, Event.ONCLICK); + + final Element wrapper = DOM.createDiv(); + DOM.setElementProperty(wrapper, "className", CLASSNAME + "-wrap"); + + final Element wrapper2 = DOM.createDiv(); + DOM.setElementProperty(wrapper2, "className", CLASSNAME + "-wrap2"); + + DOM.sinkEvents(wrapper, Event.ONKEYDOWN); + + DOM.appendChild(wrapper2, closeBox); + DOM.appendChild(wrapper2, header); + DOM.appendChild(header, headerText); + DOM.appendChild(wrapper2, contents); + DOM.appendChild(wrapper2, footer); + DOM.appendChild(wrapper, wrapper2); + DOM.appendChild(super.getContainerElement(), wrapper); + + sinkEvents(Event.MOUSEEVENTS); + + setWidget(contentPanel); + + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + id = uidl.getId(); + this.client = client; + + // Workaround needed for Testing Tools (GWT generates window DOM + // slightly different in different browsers). + DOM.setElementProperty(closeBox, "id", id + "_window_close"); + + if (uidl.hasAttribute("invisible")) { + hide(); + return; + } + + if (!uidl.hasAttribute("cached")) { + if (uidl.getBooleanAttribute("modal") != vaadinModality) { + setVaadinModality(!vaadinModality); + } + if (!isAttached()) { + show(); + } + } + + if (client.updateComponent(this, uidl, false)) { + return; + } + + immediate = uidl.hasAttribute("immediate"); + + if (uidl.getBooleanAttribute("resizable") != resizable) { + setResizable(!resizable); + } + + if (isReadOnly() != uidl.getBooleanAttribute("readonly")) { + setReadOnly(!isReadOnly()); + } + + // Initialize the position form UIDL + try { + final int positionx = uidl.getIntVariable("positionx"); + final int positiony = uidl.getIntVariable("positiony"); + if (positionx >= 0 && positiony >= 0) { + setPopupPosition(positionx, positiony); + } + } catch (final IllegalArgumentException e) { + // Silently ignored as positionx and positiony are not required + // parameters + } + + if (uidl.hasAttribute("caption")) { + setCaption(uidl.getStringAttribute("caption"), uidl + .getStringAttribute("icon")); + } + + boolean showingUrl = false; + int childIndex = 0; + UIDL childUidl = uidl.getChildUIDL(childIndex++); + while ("open".equals(childUidl.getTag())) { + // TODO multiple opens with the same target will in practice just + // open the last one - should we fix that somehow? + final String parsedUri = client.translateToolkitUri(childUidl + .getStringAttribute("src")); + if (!childUidl.hasAttribute("name")) { + final Frame frame = new Frame(); + DOM.setStyleAttribute(frame.getElement(), "width", "100%"); + DOM.setStyleAttribute(frame.getElement(), "height", "100%"); + DOM.setStyleAttribute(frame.getElement(), "border", "0px"); + frame.setUrl(parsedUri); + contentPanel.setWidget(frame); + showingUrl = true; + } else { + final String target = childUidl.getStringAttribute("name"); + Window.open(parsedUri, target, ""); + } + childUidl = uidl.getChildUIDL(childIndex++); + } + + final Paintable lo = client.getPaintable(childUidl); + if (layout != null) { + if (layout != lo) { + // remove old + client.unregisterPaintable(layout); + contentPanel.remove((Widget) layout); + // add new + if (!showingUrl) { + contentPanel.setWidget((Widget) lo); + } + layout = lo; + } + } else if (!showingUrl) { + contentPanel.setWidget((Widget) lo); + layout = lo; + } + + dynamicWidth = !uidl.hasAttribute("width"); + dynamicHeight = !uidl.hasAttribute("height"); + + layoutRelativeWidth = uidl.hasAttribute("layoutRelativeWidth"); + layoutRelativeHeight = uidl.hasAttribute("layoutRelativeHeight"); + + if (dynamicWidth && layoutRelativeWidth) { + /* + * Relative layout width, fix window width before rendering (width + * according to caption) + */ + setNaturalWidth(); + } + + layout.updateFromUIDL(childUidl, client); + if (!dynamicHeight && layoutRelativeWidth) { + /* + * Relative layout width, and fixed height. Must update the size to + * be able to take scrollbars into account (layout gets narrower + * space if it is higher than the window) -> only vertical scrollbar + */ + client.runDescendentsLayout(this); + } + + /* + * No explicit width is set and the layout does not have relative width + * so fix the size according to the layout. + */ + if (dynamicWidth && !layoutRelativeWidth) { + setNaturalWidth(); + } + + if (dynamicHeight && layoutRelativeHeight) { + // Prevent resizing until height has been fixed + resizable = false; + } + + // we may have actions and notifications + if (uidl.getChildCount() > 1) { + final int cnt = uidl.getChildCount(); + for (int i = 1; i < cnt; i++) { + childUidl = uidl.getChildUIDL(i); + if (childUidl.getTag().equals("actions")) { + if (shortcutHandler == null) { + shortcutHandler = new ShortcutActionHandler(id, client); + } + shortcutHandler.updateActionMap(childUidl); + } else if (childUidl.getTag().equals("notifications")) { + // TODO needed? move -> + for (final Iterator it = childUidl.getChildIterator(); it + .hasNext();) { + final UIDL notification = (UIDL) it.next(); + String html = ""; + if (notification.hasAttribute("icon")) { + final String parsedUri = client + .translateToolkitUri(notification + .getStringAttribute("icon")); + html += "<img src=\"" + parsedUri + "\" />"; + } + if (notification.hasAttribute("caption")) { + html += "<h1>" + + notification + .getStringAttribute("caption") + + "</h1>"; + } + if (notification.hasAttribute("message")) { + html += "<p>" + + notification + .getStringAttribute("message") + + "</p>"; + } + + final String style = notification.hasAttribute("style") ? notification + .getStringAttribute("style") + : null; + final int position = notification + .getIntAttribute("position"); + final int delay = notification.getIntAttribute("delay"); + new INotification(delay).show(html, position, style); + } + } + } + + } + + // setting scrollposition must happen after children is rendered + contentPanel.setScrollPosition(uidl.getIntVariable("scrollTop")); + contentPanel.setHorizontalScrollPosition(uidl + .getIntVariable("scrollLeft")); + + // Center this window on screen if requested + // This has to be here because we might not know the content size before + // everything is painted into the window + if (uidl.getBooleanAttribute("center")) { + // mark as centered - this is unset on move/resize + centered = true; + center(); + } else { + // don't try to center the window anymore + centered = false; + } + + updateShadowSizeAndPosition(); + + // ensure window is not larger than browser window + if (getOffsetWidth() > Window.getClientWidth()) { + setWidth(Window.getClientWidth() + "px"); + } + if (getOffsetHeight() > Window.getClientHeight()) { + setHeight(Window.getClientHeight() + "px"); + } + + if (dynamicHeight && layoutRelativeHeight) { + /* + * Window height is undefined, layout is 100% high so the layout + * should define the initial window height but on resize the layout + * should be as high as the window. We fix the height to deal with + * this. + */ + + int h = contents.getOffsetHeight() + getExtraHeight(); + int w = contents.getOffsetWidth(); + + client.updateVariable(id, "height", h, false); + client.updateVariable(id, "width", w, true); + } + + } + + private void setNaturalWidth() { + /* + * For some reason IE6 has title DIV set to width 100% which messes this + * up. Also IE6 has a 0 wide element so we use the container element. + */ + int naturalWidth; + if (BrowserInfo.get().isIE6()) { + String headerW = headerText.getStyle().getProperty("width"); + headerText.getStyle().setProperty("width", "auto"); + naturalWidth = getElement().getOffsetWidth(); + headerText.getStyle().setProperty("width", headerW); + } else { + // use max(layout width, window width) + // i.e layout content width or caption width + int lowidth = contentPanel.getElement().getScrollWidth() + + borderWidth; // layout does not know about border + int elwidth = getElement().getOffsetWidth(); + naturalWidth = (lowidth > elwidth ? lowidth : elwidth); + } + + setWidth(naturalWidth + "px"); + } + + private void setReadOnly(boolean readonly) { + this.readonly = readonly; + if (readonly) { + DOM.setStyleAttribute(closeBox, "display", "none"); + } else { + DOM.setStyleAttribute(closeBox, "display", ""); + } + } + + private boolean isReadOnly() { + return readonly; + } + + @Override + public void show() { + if (vaadinModality) { + showModalityCurtain(); + } + super.show(); + + setFF2CaretFixEnabled(true); + fixFF3OverflowBug(); + } + + /** Disable overflow auto with FF3 to fix #1837. */ + private void fixFF3OverflowBug() { + if (BrowserInfo.get().isFF3()) { + DeferredCommand.addCommand(new Command() { + public void execute() { + DOM.setStyleAttribute(getElement(), "overflow", ""); + } + }); + } + } + + /** + * Fix "missing cursor" browser bug workaround for FF2 in Windows and Linux. + * + * Calling this method has no effect on other browsers than the ones based + * on Gecko 1.8 + * + * @param enable + */ + private void setFF2CaretFixEnabled(boolean enable) { + if (BrowserInfo.get().isFF2()) { + if (enable) { + DeferredCommand.addCommand(new Command() { + public void execute() { + DOM.setStyleAttribute(getElement(), "overflow", "auto"); + } + }); + } else { + DOM.setStyleAttribute(getElement(), "overflow", ""); + } + } + } + + @Override + public void hide() { + if (vaadinModality) { + hideModalityCurtain(); + } + super.hide(); + } + + private void setVaadinModality(boolean modality) { + vaadinModality = modality; + if (vaadinModality) { + modalityCurtain = DOM.createDiv(); + DOM.setElementProperty(modalityCurtain, "className", CLASSNAME + + "-modalitycurtain"); + if (isAttached()) { + showModalityCurtain(); + bringToFront(); + } else { + DeferredCommand.addCommand(new Command() { + public void execute() { + // vaadinModality window must on top of others + bringToFront(); + } + }); + } + } else { + if (modalityCurtain != null) { + if (isAttached()) { + hideModalityCurtain(); + } + modalityCurtain = null; + } + } + } + + private void showModalityCurtain() { + if (BrowserInfo.get().isFF2()) { + DOM.setStyleAttribute(modalityCurtain, "height", DOM + .getElementPropertyInt(RootPanel.getBodyElement(), + "offsetHeight") + + "px"); + DOM.setStyleAttribute(modalityCurtain, "position", "absolute"); + } + DOM.setStyleAttribute(modalityCurtain, "zIndex", "" + + (windowOrder.indexOf(this) + Z_INDEX)); + DOM.appendChild(RootPanel.getBodyElement(), modalityCurtain); + } + + private void hideModalityCurtain() { + DOM.removeChild(RootPanel.getBodyElement(), modalityCurtain); + } + + /* + * Shows (or hides) an empty div on top of all other content; used when + * resizing or moving, so that iframes (etc) do not steal event. + */ + private void showDraggingCurtain(boolean show) { + if (show && draggingCurtain == null) { + + setFF2CaretFixEnabled(false); // makes FF2 slow + + draggingCurtain = DOM.createDiv(); + DOM.setStyleAttribute(draggingCurtain, "position", "absolute"); + DOM.setStyleAttribute(draggingCurtain, "top", "0px"); + DOM.setStyleAttribute(draggingCurtain, "left", "0px"); + DOM.setStyleAttribute(draggingCurtain, "width", "100%"); + DOM.setStyleAttribute(draggingCurtain, "height", "100%"); + DOM.setStyleAttribute(draggingCurtain, "zIndex", "" + + IToolkitOverlay.Z_INDEX); + + DOM.appendChild(RootPanel.getBodyElement(), draggingCurtain); + } else if (!show && draggingCurtain != null) { + + setFF2CaretFixEnabled(true); // makes FF2 slow + + DOM.removeChild(RootPanel.getBodyElement(), draggingCurtain); + draggingCurtain = null; + } + + } + + private void setResizable(boolean resizability) { + resizable = resizability; + if (resizability) { + DOM.setElementProperty(resizeBox, "className", CLASSNAME + + "-resizebox"); + } else { + DOM.setElementProperty(resizeBox, "className", CLASSNAME + + "-resizebox " + CLASSNAME + "-resizebox-disabled"); + } + } + + @Override + public void setPopupPosition(int left, int top) { + super.setPopupPosition(left, top); + if (left != uidlPositionX && client != null) { + client.updateVariable(id, "positionx", left, false); + uidlPositionX = left; + } + if (top != uidlPositionY && client != null) { + client.updateVariable(id, "positiony", top, false); + uidlPositionY = top; + } + } + + public void setCaption(String c) { + setCaption(c, null); + } + + public void setCaption(String c, String icon) { + String html = Util.escapeHTML(c); + if (icon != null) { + icon = client.translateToolkitUri(icon); + html = "<img src=\"" + icon + "\" class=\"i-icon\" />" + html; + } + DOM.setInnerHTML(headerText, html); + } + + @Override + protected Element getContainerElement() { + // in GWT 1.5 this method is used in PopupPanel constructor + if (contents == null) { + return super.getContainerElement(); + } + return contents; + } + + @Override + public void onBrowserEvent(final Event event) { + if (event != null) { + final int type = event.getTypeInt(); + + if (type == Event.ONKEYDOWN && shortcutHandler != null) { + shortcutHandler.handleKeyboardEvent(event); + return; + } + + final Element target = DOM.eventGetTarget(event); + + // Handle window caption tooltips + if (client != null && DOM.isOrHasChild(header, target)) { + client.handleTooltipEvent(event, this); + } + + if (resizing || resizeBox == target) { + onResizeEvent(event); + event.cancelBubble(true); + } else if (target == closeBox) { + if (type == Event.ONCLICK) { + onCloseClick(); + event.cancelBubble(true); + } + } else if (dragging || !DOM.isOrHasChild(contents, target)) { + onDragEvent(event); + event.cancelBubble(true); + } else if (type == Event.ONCLICK) { + // clicked inside window, ensure to be on top + if (!isActive()) { + bringToFront(); + } + } + } + } + + private void onCloseClick() { + client.updateVariable(id, "close", true, true); + } + + private void onResizeEvent(Event event) { + if (resizable) { + switch (event.getTypeInt()) { + case Event.ONMOUSEDOWN: + if (!isActive()) { + bringToFront(); + } + showDraggingCurtain(true); + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(resizeBox, "visibility", "hidden"); + } + resizing = true; + startX = event.getScreenX(); + startY = event.getScreenY(); + origW = getElement().getOffsetWidth(); + origH = getElement().getOffsetHeight(); + DOM.setCapture(getElement()); + event.preventDefault(); + break; + case Event.ONMOUSEUP: + showDraggingCurtain(false); + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(resizeBox, "visibility", ""); + } + resizing = false; + DOM.releaseCapture(getElement()); + setSize(event, true); + break; + case Event.ONLOSECAPTURE: + showDraggingCurtain(false); + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(resizeBox, "visibility", ""); + } + resizing = false; + case Event.ONMOUSEMOVE: + if (resizing) { + centered = false; + setSize(event, false); + event.preventDefault(); + } + break; + default: + event.preventDefault(); + break; + } + } + } + + private void setSize(Event event, boolean updateVariables) { + int w = event.getScreenX() - startX + origW; + if (w < MIN_WIDTH + borderWidth) { + w = MIN_WIDTH + borderWidth; + } + + int h = event.getScreenY() - startY + origH; + if (h < MIN_HEIGHT + getExtraHeight()) { + h = MIN_HEIGHT + getExtraHeight(); + } + + setWidth(w + "px"); + setHeight(h + "px"); + + if (updateVariables) { + // sending width back always as pixels, no need for unit + client.updateVariable(id, "width", w, false); + client.updateVariable(id, "height", h, immediate); + } + + // Update child widget dimensions + if (client != null) { + client.handleComponentRelativeSize((Widget) layout); + client.runDescendentsLayout((HasWidgets) layout); + } + + Util.runWebkitOverflowAutoFix(contentPanel.getElement()); + } + + @Override + /* + * Width is set to the out-most element (i-window). + * + * This function should never be called with percentage values (it will + * throw an exception) + */ + public void setWidth(String width) { + this.width = width; + if (!isAttached()) { + return; + } + if (width != null && !"".equals(width)) { + int pixelWidth; + // Convert non-pixel values to pixels + if (width.indexOf("px") < 0) { + DOM.setStyleAttribute(getElement(), "width", width); + pixelWidth = getElement().getOffsetWidth(); + width = pixelWidth + "px"; + } + if (BrowserInfo.get().isIE6()) { + getElement().getStyle().setProperty("overflow", "hidden"); + } + getElement().getStyle().setProperty("width", width); + + pixelWidth = getElement().getOffsetWidth() - borderWidth; + if (pixelWidth < MIN_WIDTH) { + pixelWidth = MIN_WIDTH; + int rootWidth = pixelWidth + borderWidth; + DOM.setStyleAttribute(getElement(), "width", rootWidth + "px"); + } + + renderSpace.setWidth(pixelWidth); + + // IE6 needs the actual inner content width on the content element, + // otherwise it won't wrap the content properly (no scrollbars + // appear, content flows out of window) + if (BrowserInfo.get().isIE6()) { + DOM.setStyleAttribute(contentPanel.getElement(), "width", + pixelWidth + "px"); + } + updateShadowSizeAndPosition(); + } + } + + @Override + /* + * Height is set to the out-most element (i-window). + * + * This function should never be called with percentage values (it will + * throw an exception) + */ + public void setHeight(String height) { + this.height = height; + if (!isAttached()) { + return; + } + if (height != null && !"".equals(height)) { + DOM.setStyleAttribute(getElement(), "height", height); + int pixels = getElement().getOffsetHeight() - getExtraHeight(); + if (pixels < MIN_HEIGHT) { + pixels = MIN_HEIGHT; + int rootHeight = pixels + getExtraHeight(); + DOM.setStyleAttribute(getElement(), "height", (rootHeight) + + "px"); + + } + renderSpace.setHeight(pixels); + height = pixels + "px"; + contentPanel.getElement().getStyle().setProperty("height", height); + updateShadowSizeAndPosition(); + + } + } + + private int extraH = 0; + + private int getExtraHeight() { + extraH = header.getOffsetHeight() + footer.getOffsetHeight(); + return extraH; + } + + private void onDragEvent(Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONMOUSEDOWN: + if (!isActive()) { + bringToFront(); + } + showDraggingCurtain(true); + dragging = true; + startX = DOM.eventGetScreenX(event); + startY = DOM.eventGetScreenY(event); + origX = DOM.getAbsoluteLeft(getElement()); + origY = DOM.getAbsoluteTop(getElement()); + DOM.setCapture(getElement()); + DOM.eventPreventDefault(event); + break; + case Event.ONMOUSEUP: + dragging = false; + showDraggingCurtain(false); + DOM.releaseCapture(getElement()); + break; + case Event.ONLOSECAPTURE: + showDraggingCurtain(false); + dragging = false; + break; + case Event.ONMOUSEMOVE: + if (dragging) { + centered = false; + final int x = DOM.eventGetScreenX(event) - startX + origX; + final int y = DOM.eventGetScreenY(event) - startY + origY; + setPopupPosition(x, y); + DOM.eventPreventDefault(event); + } + break; + default: + break; + } + } + + @Override + public boolean onEventPreview(Event event) { + if (dragging) { + onDragEvent(event); + return false; + } else if (resizing) { + onResizeEvent(event); + return false; + } else if (vaadinModality) { + // return false when modal and outside window + final Element target = event.getTarget().cast(); + if (!DOM.isOrHasChild(getElement(), target)) { + return false; + } + } + return true; + } + + public void onScroll(Widget widget, int scrollLeft, int scrollTop) { + client.updateVariable(id, "scrollTop", scrollTop, false); + client.updateVariable(id, "scrollLeft", scrollLeft, false); + } + + @Override + public void addStyleDependentName(String styleSuffix) { + // IWindow's getStyleElement() does not return the same element as + // getElement(), so we need to override this. + setStyleName(getElement(), getStylePrimaryName() + "-" + styleSuffix, + true); + } + + @Override + protected void onAttach() { + super.onAttach(); + + // Calculate space required by window borders, so we can accurately + // calculate space for content + + // IE (IE6 especially) requires some magic tricks to pull the border + // size correctly (remember that we want to accomodate for paddings as + // well) + if (BrowserInfo.get().isIE()) { + DOM.setStyleAttribute(contents, "width", "7000px"); + DOM.setStyleAttribute(contentPanel.getElement(), "width", "7000px"); + int contentWidth = DOM.getElementPropertyInt(contentPanel + .getElement(), "offsetWidth"); + contentWidth = DOM.getElementPropertyInt(contentPanel.getElement(), + "offsetWidth"); + final int windowWidth = DOM.getElementPropertyInt(getElement(), + "offsetWidth"); + DOM.setStyleAttribute(contentPanel.getElement(), "width", ""); + DOM.setStyleAttribute(contents, "width", ""); + + borderWidth = windowWidth - contentWidth; + } + + // Standards based browsers get away with it a little easier :) + else { + final int contentWidth = DOM.getElementPropertyInt(contentPanel + .getElement(), "offsetWidth"); + final int windowWidth = DOM.getElementPropertyInt(getElement(), + "offsetWidth"); + borderWidth = windowWidth - contentWidth; + } + + setWidth(width); + setHeight(height); + + } + + public RenderSpace getAllocatedSpace(Widget child) { + if (child == layout) { + return renderSpace; + } else { + // Exception ?? + return null; + } + } + + public boolean hasChildComponent(Widget component) { + if (component == layout) { + return true; + } else { + return false; + } + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + contentPanel.setWidget(newComponent); + } + + public boolean requestLayout(Set<Paintable> child) { + if (dynamicWidth && !layoutRelativeWidth) { + setNaturalWidth(); + } + if (centered) { + center(); + } + updateShadowSizeAndPosition(); + return true; + } + + public void updateCaption(Paintable component, UIDL uidl) { + // NOP, window has own caption, layout captio not rendered + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/Icon.java b/src/com/vaadin/terminal/gwt/client/ui/Icon.java new file mode 100644 index 0000000000..d17227955e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/Icon.java @@ -0,0 +1,44 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.UIObject; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +public class Icon extends UIObject { + private final ApplicationConnection client; + private String myUri; + + public Icon(ApplicationConnection client) { + setElement(DOM.createImg()); + DOM.setElementProperty(getElement(), "alt", ""); + setStyleName("i-icon"); + this.client = client; + client.addPngFix(getElement()); + } + + public Icon(ApplicationConnection client, String uidlUri) { + this(client); + setUri(uidlUri); + } + + public void setUri(String uidlUri) { + if (!uidlUri.equals(myUri)) { + /* + * Start sinking onload events, widgets responsibility to react. We + * must do this BEFORE we set src as IE fires the event immediately + * if the image is found in cache (#2592). + */ + sinkEvents(Event.ONLOAD); + + String uri = client.translateToolkitUri(uidlUri); + DOM.setElementProperty(getElement(), "src", uri); + myUri = uidlUri; + } + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/MenuBar.java b/src/com/vaadin/terminal/gwt/client/ui/MenuBar.java new file mode 100644 index 0000000000..a711864a37 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/MenuBar.java @@ -0,0 +1,514 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +/* + * Copyright 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +// COPIED HERE DUE package privates in GWT +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.PopupListener; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.Widget; + +/** + * A standard menu bar widget. A menu bar can contain any number of menu items, + * each of which can either fire a {@link com.google.gwt.user.client.Command} or + * open a cascaded menu bar. + * + * <p> + * <img class='gallery' src='MenuBar.png'/> + * </p> + * + * <h3>CSS Style Rules</h3> <ul class='css'> <li>.gwt-MenuBar { the menu bar + * itself }</li> <li>.gwt-MenuBar .gwt-MenuItem { menu items }</li> <li> + * .gwt-MenuBar .gwt-MenuItem-selected { selected menu items }</li> </ul> + * + * <p> + * <h3>Example</h3> + * {@example com.google.gwt.examples.MenuBarExample} + * </p> + * + * @deprecated + */ +@Deprecated +public class MenuBar extends Widget implements PopupListener { + + private final Element body; + private final ArrayList items = new ArrayList(); + private MenuBar parentMenu; + private PopupPanel popup; + private MenuItem selectedItem; + private MenuBar shownChildMenu; + private final boolean vertical; + private boolean autoOpen; + + /** + * Creates an empty horizontal menu bar. + */ + public MenuBar() { + this(false); + } + + /** + * Creates an empty menu bar. + * + * @param vertical + * <code>true</code> to orient the menu bar vertically + */ + public MenuBar(boolean vertical) { + super(); + + final Element table = DOM.createTable(); + body = DOM.createTBody(); + DOM.appendChild(table, body); + + if (!vertical) { + final Element tr = DOM.createTR(); + DOM.appendChild(body, tr); + } + + this.vertical = vertical; + + final Element outer = DOM.createDiv(); + DOM.appendChild(outer, table); + setElement(outer); + + sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT); + setStyleName("gwt-MenuBar"); + } + + /** + * Adds a menu item to the bar. + * + * @param item + * the item to be added + */ + public void addItem(MenuItem item) { + Element tr; + if (vertical) { + tr = DOM.createTR(); + DOM.appendChild(body, tr); + } else { + tr = DOM.getChild(body, 0); + } + + DOM.appendChild(tr, item.getElement()); + + item.setParentMenu(this); + item.setSelectionStyle(false); + items.add(item); + } + + /** + * Adds a menu item to the bar, that will fire the given command when it is + * selected. + * + * @param text + * the item's text + * @param asHTML + * <code>true</code> to treat the specified text as html + * @param cmd + * the command to be fired + * @return the {@link MenuItem} object created + */ + public MenuItem addItem(String text, boolean asHTML, Command cmd) { + final MenuItem item = new MenuItem(text, asHTML, cmd); + addItem(item); + return item; + } + + /** + * Adds a menu item to the bar, that will open the specified menu when it is + * selected. + * + * @param text + * the item's text + * @param asHTML + * <code>true</code> to treat the specified text as html + * @param popup + * the menu to be cascaded from it + * @return the {@link MenuItem} object created + */ + public MenuItem addItem(String text, boolean asHTML, MenuBar popup) { + final MenuItem item = new MenuItem(text, asHTML, popup); + addItem(item); + return item; + } + + /** + * Adds a menu item to the bar, that will fire the given command when it is + * selected. + * + * @param text + * the item's text + * @param cmd + * the command to be fired + * @return the {@link MenuItem} object created + */ + public MenuItem addItem(String text, Command cmd) { + final MenuItem item = new MenuItem(text, cmd); + addItem(item); + return item; + } + + /** + * Adds a menu item to the bar, that will open the specified menu when it is + * selected. + * + * @param text + * the item's text + * @param popup + * the menu to be cascaded from it + * @return the {@link MenuItem} object created + */ + public MenuItem addItem(String text, MenuBar popup) { + final MenuItem item = new MenuItem(text, popup); + addItem(item); + return item; + } + + /** + * Removes all menu items from this menu bar. + */ + public void clearItems() { + final Element container = getItemContainerElement(); + while (DOM.getChildCount(container) > 0) { + DOM.removeChild(container, DOM.getChild(container, 0)); + } + items.clear(); + } + + /** + * Gets whether this menu bar's child menus will open when the mouse is + * moved over it. + * + * @return <code>true</code> if child menus will auto-open + */ + public boolean getAutoOpen() { + return autoOpen; + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + + final MenuItem item = findItem(DOM.eventGetTarget(event)); + switch (DOM.eventGetType(event)) { + case Event.ONCLICK: { + // Fire an item's command when the user clicks on it. + if (item != null) { + doItemAction(item, true); + } + break; + } + + case Event.ONMOUSEOVER: { + if (item != null) { + itemOver(item); + } + break; + } + + case Event.ONMOUSEOUT: { + if (item != null) { + itemOver(null); + } + break; + } + } + } + + public void onPopupClosed(PopupPanel sender, boolean autoClosed) { + // If the menu popup was auto-closed, close all of its parents as well. + if (autoClosed) { + closeAllParents(); + } + + // When the menu popup closes, remember that no item is + // currently showing a popup menu. + onHide(); + shownChildMenu = null; + popup = null; + } + + /** + * Removes the specified menu item from the bar. + * + * @param item + * the item to be removed + */ + public void removeItem(MenuItem item) { + final int idx = items.indexOf(item); + if (idx == -1) { + return; + } + + final Element container = getItemContainerElement(); + DOM.removeChild(container, DOM.getChild(container, idx)); + items.remove(idx); + } + + /** + * Sets whether this menu bar's child menus will open when the mouse is + * moved over it. + * + * @param autoOpen + * <code>true</code> to cause child menus to auto-open + */ + public void setAutoOpen(boolean autoOpen) { + this.autoOpen = autoOpen; + } + + /** + * Returns a list containing the <code>MenuItem</code> objects in the menu + * bar. If there are no items in the menu bar, then an empty + * <code>List</code> object will be returned. + * + * @return a list containing the <code>MenuItem</code> objects in the menu + * bar + */ + protected List getItems() { + return items; + } + + /** + * Returns the <code>MenuItem</code> that is currently selected + * (highlighted) by the user. If none of the items in the menu are currently + * selected, then <code>null</code> will be returned. + * + * @return the <code>MenuItem</code> that is currently selected, or + * <code>null</code> if no items are currently selected + */ + protected MenuItem getSelectedItem() { + return selectedItem; + } + + @Override + protected void onDetach() { + // When the menu is detached, make sure to close all of its children. + if (popup != null) { + popup.hide(); + } + + super.onDetach(); + } + + /* + * Closes all parent menu popups. + */ + void closeAllParents() { + MenuBar curMenu = this; + while (curMenu != null) { + curMenu.close(); + + if ((curMenu.parentMenu == null) && (curMenu.selectedItem != null)) { + curMenu.selectedItem.setSelectionStyle(false); + curMenu.selectedItem = null; + } + + curMenu = curMenu.parentMenu; + } + } + + /* + * Performs the action associated with the given menu item. If the item has + * a popup associated with it, the popup will be shown. If it has a command + * associated with it, and 'fireCommand' is true, then the command will be + * fired. Popups associated with other items will be hidden. + * + * @param item the item whose popup is to be shown. @param fireCommand + * <code>true</code> if the item's command should be fired, + * <code>false</code> otherwise. + */ + void doItemAction(final MenuItem item, boolean fireCommand) { + // If the given item is already showing its menu, we're done. + if ((shownChildMenu != null) && (item.getSubMenu() == shownChildMenu)) { + return; + } + + // If another item is showing its menu, then hide it. + if (shownChildMenu != null) { + shownChildMenu.onHide(); + popup.hide(); + } + + // If the item has no popup, optionally fire its command. + if (item.getSubMenu() == null) { + if (fireCommand) { + // Close this menu and all of its parents. + closeAllParents(); + + // Fire the item's command. + final Command cmd = item.getCommand(); + if (cmd != null) { + DeferredCommand.addCommand(cmd); + } + } + return; + } + + // Ensure that the item is selected. + selectItem(item); + + // Create a new popup for this item, and position it next to + // the item (below if this is a horizontal menu bar, to the + // right if it's a vertical bar). + popup = new IToolkitOverlay(true) { + { + setWidget(item.getSubMenu()); + item.getSubMenu().onShow(); + } + + @Override + public boolean onEventPreview(Event event) { + // Hook the popup panel's event preview. We use this to keep it + // from + // auto-hiding when the parent menu is clicked. + switch (DOM.eventGetType(event)) { + case Event.ONCLICK: + // If the event target is part of the parent menu, suppress + // the + // event altogether. + final Element target = DOM.eventGetTarget(event); + final Element parentMenuElement = item.getParentMenu() + .getElement(); + if (DOM.isOrHasChild(parentMenuElement, target)) { + return false; + } + break; + } + + return super.onEventPreview(event); + } + }; + popup.addPopupListener(this); + + if (vertical) { + popup.setPopupPosition(item.getAbsoluteLeft() + + item.getOffsetWidth(), item.getAbsoluteTop()); + } else { + popup.setPopupPosition(item.getAbsoluteLeft(), item + .getAbsoluteTop() + + item.getOffsetHeight()); + } + + shownChildMenu = item.getSubMenu(); + item.getSubMenu().parentMenu = this; + + // Show the popup, ensuring that the menubar's event preview remains on + // top + // of the popup's. + popup.show(); + } + + void itemOver(MenuItem item) { + if (item == null) { + // Don't clear selection if the currently selected item's menu is + // showing. + if ((selectedItem != null) + && (shownChildMenu == selectedItem.getSubMenu())) { + return; + } + } + + // Style the item selected when the mouse enters. + selectItem(item); + + // If child menus are being shown, or this menu is itself + // a child menu, automatically show an item's child menu + // when the mouse enters. + if (item != null) { + if ((shownChildMenu != null) || (parentMenu != null) || autoOpen) { + doItemAction(item, false); + } + } + } + + void selectItem(MenuItem item) { + if (item == selectedItem) { + return; + } + + if (selectedItem != null) { + selectedItem.setSelectionStyle(false); + } + + if (item != null) { + item.setSelectionStyle(true); + } + + selectedItem = item; + } + + /** + * Closes this menu (if it is a popup). + */ + private void close() { + if (parentMenu != null) { + parentMenu.popup.hide(); + } + } + + private MenuItem findItem(Element hItem) { + for (int i = 0; i < items.size(); ++i) { + final MenuItem item = (MenuItem) items.get(i); + if (DOM.isOrHasChild(item.getElement(), hItem)) { + return item; + } + } + + return null; + } + + private Element getItemContainerElement() { + if (vertical) { + return body; + } else { + return DOM.getChild(body, 0); + } + } + + /* + * This method is called when a menu bar is hidden, so that it can hide any + * child popups that are currently being shown. + */ + private void onHide() { + if (shownChildMenu != null) { + shownChildMenu.onHide(); + popup.hide(); + } + } + + /* + * This method is called when a menu bar is shown. + */ + private void onShow() { + // Select the first item when a menu is shown. + if (items.size() > 0) { + selectItem((MenuItem) items.get(0)); + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/MenuItem.java b/src/com/vaadin/terminal/gwt/client/ui/MenuItem.java new file mode 100644 index 0000000000..7ef8b6dff6 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/MenuItem.java @@ -0,0 +1,189 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +/* + * Copyright 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +// COPIED HERE DUE package privates in GWT +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.HasHTML; +import com.google.gwt.user.client.ui.UIObject; + +/** + * A widget that can be placed in a + * {@link com.google.gwt.user.client.ui.MenuBar}. Menu items can either fire a + * {@link com.google.gwt.user.client.Command} when they are clicked, or open a + * cascading sub-menu. + * + * @deprecated + */ +@Deprecated +public class MenuItem extends UIObject implements HasHTML { + + private static final String DEPENDENT_STYLENAME_SELECTED_ITEM = "selected"; + + private Command command; + private MenuBar parentMenu, subMenu; + + /** + * Constructs a new menu item that fires a command when it is selected. + * + * @param text + * the item's text + * @param cmd + * the command to be fired when it is selected + */ + public MenuItem(String text, Command cmd) { + this(text, false); + setCommand(cmd); + } + + /** + * Constructs a new menu item that fires a command when it is selected. + * + * @param text + * the item's text + * @param asHTML + * <code>true</code> to treat the specified text as html + * @param cmd + * the command to be fired when it is selected + */ + public MenuItem(String text, boolean asHTML, Command cmd) { + this(text, asHTML); + setCommand(cmd); + } + + /** + * Constructs a new menu item that cascades to a sub-menu when it is + * selected. + * + * @param text + * the item's text + * @param subMenu + * the sub-menu to be displayed when it is selected + */ + public MenuItem(String text, MenuBar subMenu) { + this(text, false); + setSubMenu(subMenu); + } + + /** + * Constructs a new menu item that cascades to a sub-menu when it is + * selected. + * + * @param text + * the item's text + * @param asHTML + * <code>true</code> to treat the specified text as html + * @param subMenu + * the sub-menu to be displayed when it is selected + */ + public MenuItem(String text, boolean asHTML, MenuBar subMenu) { + this(text, asHTML); + setSubMenu(subMenu); + } + + MenuItem(String text, boolean asHTML) { + setElement(DOM.createTD()); + setSelectionStyle(false); + + if (asHTML) { + setHTML(text); + } else { + setText(text); + } + setStyleName("gwt-MenuItem"); + } + + /** + * Gets the command associated with this item. + * + * @return this item's command, or <code>null</code> if none exists + */ + public Command getCommand() { + return command; + } + + public String getHTML() { + return DOM.getInnerHTML(getElement()); + } + + /** + * Gets the menu that contains this item. + * + * @return the parent menu, or <code>null</code> if none exists. + */ + public MenuBar getParentMenu() { + return parentMenu; + } + + /** + * Gets the sub-menu associated with this item. + * + * @return this item's sub-menu, or <code>null</code> if none exists + */ + public MenuBar getSubMenu() { + return subMenu; + } + + public String getText() { + return DOM.getInnerText(getElement()); + } + + /** + * Sets the command associated with this item. + * + * @param cmd + * the command to be associated with this item + */ + public void setCommand(Command cmd) { + command = cmd; + } + + public void setHTML(String html) { + DOM.setInnerHTML(getElement(), html); + } + + /** + * Sets the sub-menu associated with this item. + * + * @param subMenu + * this item's new sub-menu + */ + public void setSubMenu(MenuBar subMenu) { + this.subMenu = subMenu; + } + + public void setText(String text) { + DOM.setInnerText(getElement(), text); + } + + void setParentMenu(MenuBar parentMenu) { + this.parentMenu = parentMenu; + } + + void setSelectionStyle(boolean selected) { + if (selected) { + addStyleDependentName(DEPENDENT_STYLENAME_SELECTED_ITEM); + } else { + removeStyleDependentName(DEPENDENT_STYLENAME_SELECTED_ITEM); + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java b/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java new file mode 100644 index 0000000000..3acdeab171 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java @@ -0,0 +1,178 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.KeyboardListener; +import com.google.gwt.user.client.ui.KeyboardListenerCollection; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.UIDL; + +/** + * A helper class to implement keyboard shorcut handling. Keeps a list of owners + * actions and fires actions to server. User class needs to delegate keyboard + * events to handleKeyboardEvents function. + * + * @author IT Mill ltd + */ +public class ShortcutActionHandler { + private final ArrayList actions = new ArrayList(); + private ApplicationConnection client; + private String paintableId; + + /** + * + * @param pid + * Paintable id + * @param c + * reference to application connections + */ + public ShortcutActionHandler(String pid, ApplicationConnection c) { + paintableId = pid; + client = c; + } + + /** + * Updates list of actions this handler listens to. + * + * @param c + * UIDL snippet containing actions + */ + public void updateActionMap(UIDL c) { + actions.clear(); + final Iterator it = c.getChildIterator(); + while (it.hasNext()) { + final UIDL action = (UIDL) it.next(); + + int[] modifiers = null; + if (action.hasAttribute("mk")) { + modifiers = action.getIntArrayAttribute("mk"); + } + + final ShortcutKeyCombination kc = new ShortcutKeyCombination(action + .getIntAttribute("kc"), modifiers); + final String key = action.getStringAttribute("key"); + final String caption = action.getStringAttribute("caption"); + actions.add(new ShortcutAction(key, kc, caption)); + } + } + + public void handleKeyboardEvent(final Event event) { + final int modifiers = KeyboardListenerCollection + .getKeyboardModifiers(event); + final char keyCode = (char) DOM.eventGetKeyCode(event); + final ShortcutKeyCombination kc = new ShortcutKeyCombination(keyCode, + modifiers); + final Iterator it = actions.iterator(); + while (it.hasNext()) { + final ShortcutAction a = (ShortcutAction) it.next(); + if (a.getShortcutCombination().equals(kc)) { + DOM.eventPreventDefault(event); + shakeTarget(DOM.eventGetTarget(event)); + DeferredCommand.addCommand(new Command() { + public void execute() { + client.updateVariable(paintableId, "action", + a.getKey(), true); + } + }); + break; + } + } + } + + public static native void shakeTarget(Element e) + /*-{ + if(e.blur) { + e.blur(); + e.focus(); + } + }-*/; + +} + +class ShortcutKeyCombination { + + public static final int SHIFT = 16; + public static final int CTRL = 17; + public static final int ALT = 18; + + char keyCode = 0; + private int modifiersMask; + + public ShortcutKeyCombination() { + } + + ShortcutKeyCombination(char kc, int modifierMask) { + keyCode = kc; + modifiersMask = modifierMask; + } + + ShortcutKeyCombination(int kc, int[] modifiers) { + keyCode = (char) kc; + keyCode = Character.toUpperCase(keyCode); + + modifiersMask = 0; + if (modifiers != null) { + for (int i = 0; i < modifiers.length; i++) { + switch (modifiers[i]) { + case ALT: + modifiersMask = modifiersMask + | KeyboardListener.MODIFIER_ALT; + break; + case CTRL: + modifiersMask = modifiersMask + | KeyboardListener.MODIFIER_CTRL; + break; + case SHIFT: + modifiersMask = modifiersMask + | KeyboardListener.MODIFIER_SHIFT; + break; + default: + break; + } + } + } + } + + public boolean equals(ShortcutKeyCombination other) { + if (keyCode == other.keyCode && modifiersMask == other.modifiersMask) { + return true; + } + return false; + } +} + +class ShortcutAction { + + private final ShortcutKeyCombination sc; + private final String caption; + private final String key; + + public ShortcutAction(String key, ShortcutKeyCombination sc, String caption) { + this.sc = sc; + this.key = key; + this.caption = caption; + } + + public ShortcutKeyCombination getShortcutCombination() { + return sc; + } + + public String getCaption() { + return caption; + } + + public String getKey() { + return key; + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java b/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java new file mode 100644 index 0000000000..ceec985f4f --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java @@ -0,0 +1,11 @@ +package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Element;
+
+public interface SubPartAware {
+
+ Element getSubPartElement(String subPart);
+
+ String getSubPartName(Element subElement);
+
+}
\ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/client/ui/Table.java b/src/com/vaadin/terminal/gwt/client/ui/Table.java new file mode 100644 index 0000000000..ac59bc0064 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/Table.java @@ -0,0 +1,15 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import com.google.gwt.user.client.ui.HasWidgets; +import com.vaadin.terminal.gwt.client.Paintable; + +public interface Table extends Paintable, HasWidgets { + final int SELECT_MODE_NONE = 0; + final int SELECT_MODE_SINGLE = 1; + final int SELECT_MODE_MULTI = 2; + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java b/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java new file mode 100644 index 0000000000..a77730f749 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java @@ -0,0 +1,56 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +/** + * This class is used for "row actions" in ITree and ITable + */ +public class TreeAction extends Action { + + String targetKey = ""; + String actionKey = ""; + + public TreeAction(ActionOwner owner) { + super(owner); + } + + public TreeAction(ActionOwner owner, String target, String action) { + this(owner); + targetKey = target; + actionKey = action; + } + + /** + * Sends message to server that this action has been fired. Messages are + * "standard" Toolkit messages whose value is comma separated pair of + * targetKey (row, treeNod ...) and actions id. + * + * Variablename is always "action". + * + * Actions are always sent immediatedly to server. + */ + @Override + public void execute() { + owner.getClient().updateVariable(owner.getPaintableId(), "action", + targetKey + "," + actionKey, true); + owner.getClient().getContextMenu().hide(); + } + + public String getActionKey() { + return actionKey; + } + + public void setActionKey(String actionKey) { + this.actionKey = actionKey; + } + + public String getTargetKey() { + return targetKey; + } + + public void setTargetKey(String targetKey) { + this.targetKey = targetKey; + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java b/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java new file mode 100644 index 0000000000..db53653e71 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java @@ -0,0 +1,31 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.ui.AbstractImagePrototype;
+
+public interface TreeImages extends com.google.gwt.user.client.ui.TreeImages {
+
+ /**
+ * An image indicating an open branch.
+ *
+ * @return a prototype of this image
+ * @gwt.resource
+ * com/itmill/toolkit/terminal/gwt/public/default/tree/img/expanded
+ * .png
+ */
+ AbstractImagePrototype treeOpen();
+
+ /**
+ * An image indicating a closed branch.
+ *
+ * @return a prototype of this image
+ * @gwt.resource
+ * com/itmill/toolkit/terminal/gwt/public/default/tree/img/collapsed
+ * .png
+ */
+ AbstractImagePrototype treeClosed();
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png Binary files differnew file mode 100644 index 0000000000..49b918ec0c --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png Binary files differnew file mode 100644 index 0000000000..9fd6635765 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png Binary files differnew file mode 100644 index 0000000000..7cd07369dc --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png Binary files differnew file mode 100644 index 0000000000..c2e1f49efe --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png Binary files differnew file mode 100644 index 0000000000..417c9aecfd --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png Binary files differnew file mode 100644 index 0000000000..2f1e461b0a --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png Binary files differnew file mode 100644 index 0000000000..63984cdee7 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png Binary files differnew file mode 100644 index 0000000000..1e730c072b --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png Binary files differnew file mode 100644 index 0000000000..34e47d1551 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png Binary files differnew file mode 100644 index 0000000000..99e3709acc --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png Binary files differnew file mode 100644 index 0000000000..be9a4cd8c5 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png Binary files differnew file mode 100644 index 0000000000..0b555ad1e7 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png Binary files differnew file mode 100644 index 0000000000..8ff42ed0f4 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/layout/CellBasedLayout.java b/src/com/vaadin/terminal/gwt/client/ui/layout/CellBasedLayout.java new file mode 100644 index 0000000000..5c2c2f0204 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/layout/CellBasedLayout.java @@ -0,0 +1,335 @@ +package com.vaadin.terminal.gwt.client.ui.layout; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style; +import com.google.gwt.user.client.ui.ComplexPanel; +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.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.ui.IMarginInfo; + +public abstract class CellBasedLayout extends ComplexPanel implements Container { + + protected Map<Widget, ChildComponentContainer> widgetToComponentContainer = new HashMap<Widget, ChildComponentContainer>(); + + protected ApplicationConnection client = null; + + protected DivElement root; + + public static final int ORIENTATION_VERTICAL = 0; + public static final int ORIENTATION_HORIZONTAL = 1; + + protected Margins activeMargins = new Margins(0, 0, 0, 0); + protected IMarginInfo activeMarginsInfo = new IMarginInfo(-1); + + protected boolean spacingEnabled = false; + protected final Spacing spacingFromCSS = new Spacing(12, 12); + protected final Spacing activeSpacing = new Spacing(0, 0); + + private boolean dynamicWidth; + + private boolean dynamicHeight; + + private final DivElement clearElement = Document.get().createDivElement(); + + private String lastStyleName = ""; + + private boolean marginsNeedsRecalculation = false; + + protected String STYLENAME_SPACING = ""; + protected String STYLENAME_MARGIN_TOP = ""; + protected String STYLENAME_MARGIN_RIGHT = ""; + protected String STYLENAME_MARGIN_BOTTOM = ""; + protected String STYLENAME_MARGIN_LEFT = ""; + + public static class Spacing { + + public int hSpacing = 0; + public int vSpacing = 0; + + public Spacing(int hSpacing, int vSpacing) { + this.hSpacing = hSpacing; + this.vSpacing = vSpacing; + } + + @Override + public String toString() { + return "Spacing [hSpacing=" + hSpacing + ",vSpacing=" + vSpacing + + "]"; + } + + } + + public CellBasedLayout() { + super(); + + setElement(Document.get().createDivElement()); + getElement().getStyle().setProperty("overflow", "hidden"); + if (BrowserInfo.get().isIE()) { + getElement().getStyle().setProperty("position", "relative"); + getElement().getStyle().setProperty("zoom", "1"); + } + + root = Document.get().createDivElement(); + root.getStyle().setProperty("overflow", "hidden"); + if (BrowserInfo.get().isIE()) { + root.getStyle().setProperty("position", "relative"); + } + + getElement().appendChild(root); + + Style style = clearElement.getStyle(); + style.setProperty("width", "0px"); + style.setProperty("height", "0px"); + style.setProperty("clear", "both"); + style.setProperty("overflow", "hidden"); + root.appendChild(clearElement); + + } + + public boolean hasChildComponent(Widget component) { + return widgetToComponentContainer.containsKey(component); + } + + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + this.client = client; + + // Only non-cached UIDL:s can introduce changes + if (uidl.getBooleanAttribute("cached")) { + return; + } + + /** + * Margin and spacind detection depends on classNames and must be set + * before setting size. Here just update the details from UIDL and from + * overridden setStyleName run actual margin detections. + */ + updateMarginAndSpacingInfo(uidl); + + /* + * This call should be made first. Ensure correct implementation, handle + * size etc. + */ + if (client.updateComponent(this, uidl, true)) { + return; + } + + handleDynamicDimensions(uidl); + + } + + @Override + public void setStyleName(String styleName) { + super.setStyleName(styleName); + + if (isAttached() && marginsNeedsRecalculation + || !lastStyleName.equals(styleName)) { + measureMarginsAndSpacing(); + lastStyleName = styleName; + marginsNeedsRecalculation = false; + } + + } + + private void handleDynamicDimensions(UIDL uidl) { + String w = uidl.hasAttribute("width") ? uidl + .getStringAttribute("width") : ""; + + String h = uidl.hasAttribute("height") ? uidl + .getStringAttribute("height") : ""; + + if (w.equals("")) { + dynamicWidth = true; + } else { + dynamicWidth = false; + } + + if (h.equals("")) { + dynamicHeight = true; + } else { + dynamicHeight = false; + } + + } + + protected void addOrMoveChild(ChildComponentContainer childComponent, + int position) { + if (childComponent.getParent() == this) { + if (getWidgetIndex(childComponent) != position) { + // Detach from old position child. + childComponent.removeFromParent(); + + // Logical attach. + getChildren().insert(childComponent, position); + + root.insertBefore(childComponent.getElement(), root + .getChildNodes().getItem(position)); + + adopt(childComponent); + } + } else { + widgetToComponentContainer.put(childComponent.getWidget(), + childComponent); + + // Logical attach. + getChildren().insert(childComponent, position); + + // avoid inserts (they are slower than appends) + boolean insert = true; + if (widgetToComponentContainer.size() == position) { + insert = false; + } + if (insert) { + root.insertBefore(childComponent.getElement(), root + .getChildNodes().getItem(position)); + } else { + root.insertBefore(childComponent.getElement(), clearElement); + } + // Adopt. + adopt(childComponent); + + } + + } + + protected ChildComponentContainer getComponentContainer(Widget child) { + return widgetToComponentContainer.get(child); + } + + protected boolean isDynamicWidth() { + return dynamicWidth; + } + + protected boolean isDynamicHeight() { + return dynamicHeight; + } + + private void updateMarginAndSpacingInfo(UIDL uidl) { + int bitMask = uidl.getIntAttribute("margins"); + if (activeMarginsInfo.getBitMask() != bitMask) { + activeMarginsInfo = new IMarginInfo(bitMask); + marginsNeedsRecalculation = true; + } + boolean spacing = uidl.getBooleanAttribute("spacing"); + if (spacing != spacingEnabled) { + marginsNeedsRecalculation = true; + spacingEnabled = spacing; + } + } + + protected boolean measureMarginsAndSpacing() { + if (!isAttached()) { + return false; + } + + DivElement measurement = Document.get().createDivElement(); + Style style = measurement.getStyle(); + style.setProperty("position", "absolute"); + style.setProperty("top", "0"); + style.setProperty("left", "0"); + style.setProperty("width", "0"); + style.setProperty("height", "0"); + style.setProperty("visibility", "hidden"); + style.setProperty("overflow", "hidden"); + root.appendChild(measurement); + + // Measure spacing (actually CSS padding) + measurement.setClassName(STYLENAME_SPACING + + (spacingEnabled ? "-on" : "-off")); + activeSpacing.vSpacing = measurement.getOffsetHeight(); + activeSpacing.hSpacing = measurement.getOffsetWidth(); + + DivElement measurement2 = Document.get().createDivElement(); + style = measurement2.getStyle(); + style.setProperty("width", "0px"); + style.setProperty("height", "0px"); + style.setProperty("visibility", "hidden"); + style.setProperty("overflow", "hidden"); + + measurement.appendChild(measurement2); + + String sn = getStylePrimaryName() + "-margin"; + + if (activeMarginsInfo.hasTop()) { + sn += " " + STYLENAME_MARGIN_TOP; + } + if (activeMarginsInfo.hasBottom()) { + sn += " " + STYLENAME_MARGIN_BOTTOM; + } + if (activeMarginsInfo.hasLeft()) { + sn += " " + STYLENAME_MARGIN_LEFT; + } + if (activeMarginsInfo.hasRight()) { + sn += " " + STYLENAME_MARGIN_RIGHT; + } + + // Measure top and left margins (actually CSS padding) + measurement.setClassName(sn); + + activeMargins.setMarginTop(measurement2.getOffsetTop()); + activeMargins.setMarginLeft(measurement2.getOffsetLeft()); + activeMargins.setMarginRight(measurement.getOffsetWidth() + - activeMargins.getMarginLeft()); + activeMargins.setMarginBottom(measurement.getOffsetHeight() + - activeMargins.getMarginTop()); + + // ApplicationConnection.getConsole().log("Margins: " + activeMargins); + // ApplicationConnection.getConsole().log("Spacing: " + activeSpacing); + // Util.alert("Margins: " + activeMargins); + root.removeChild(measurement); + + // apply margin + style = root.getStyle(); + style.setPropertyPx("marginLeft", activeMargins.getMarginLeft()); + style.setPropertyPx("marginRight", activeMargins.getMarginRight()); + style.setPropertyPx("marginTop", activeMargins.getMarginTop()); + style.setPropertyPx("marginBottom", activeMargins.getMarginBottom()); + + return true; + } + + protected ChildComponentContainer getFirstChildComponentContainer() { + int size = getChildren().size(); + if (size < 1) { + return null; + } + + return (ChildComponentContainer) getChildren().get(0); + } + + protected void removeChildrenAfter(int pos) { + // Remove all children after position "pos" but leave the clear element + // in place + + int toRemove = getChildren().size() - pos; + while (toRemove-- > 0) { + ChildComponentContainer child = (ChildComponentContainer) getChildren() + .get(pos); + widgetToComponentContainer.remove(child.getWidget()); + remove(child); + Paintable p = (Paintable) child.getWidget(); + client.unregisterPaintable(p); + } + + } + + public void replaceChildComponent(Widget oldComponent, Widget newComponent) { + ChildComponentContainer componentContainer = widgetToComponentContainer + .remove(oldComponent); + if (componentContainer == null) { + return; + } + + componentContainer.setWidget(newComponent); + client.unregisterPaintable((Paintable) oldComponent); + widgetToComponentContainer.put(newComponent, componentContainer); + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/layout/ChildComponentContainer.java b/src/com/vaadin/terminal/gwt/client/ui/layout/ChildComponentContainer.java new file mode 100644 index 0000000000..158138359d --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/layout/ChildComponentContainer.java @@ -0,0 +1,736 @@ +package com.vaadin.terminal.gwt.client.ui.layout; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.TableElement; +import com.google.gwt.user.client.ui.Panel; +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.ICaption; +import com.vaadin.terminal.gwt.client.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize; +import com.vaadin.terminal.gwt.client.RenderInformation.Size; +import com.vaadin.terminal.gwt.client.ui.AlignmentInfo; + +public class ChildComponentContainer extends Panel { + + /** + * Size of the container DIV excluding any margins and also excluding the + * expansion amount (containerExpansion) + */ + private Size contSize = new Size(0, 0); + + /** + * Size of the widget inside the container DIV + */ + private Size widgetSize = new Size(0, 0); + /** + * Size of the caption + */ + private int captionRequiredWidth = 0; + private int captionWidth = 0; + private int captionHeight = 0; + + /** + * Padding added to the container when it is larger than the component. + */ + private Size containerExpansion = new Size(0, 0); + + private float expandRatio; + + private int containerMarginLeft = 0; + private int containerMarginTop = 0; + + AlignmentInfo alignment = AlignmentInfo.TOP_LEFT; + + private int alignmentLeftOffsetForWidget = 0; + private int alignmentLeftOffsetForCaption = 0; + /** + * Top offset for implementing alignment. Top offset is set to the container + * DIV as it otherwise would have to be set to either the Caption or the + * Widget depending on whether there is a caption and where the caption is + * located. + */ + private int alignmentTopOffset = 0; + + // private Margins alignmentOffset = new Margins(0, 0, 0, 0); + private ICaption caption = null; + private DivElement containerDIV; + private DivElement widgetDIV; + private Widget widget; + private FloatSize relativeSize = null; + + public ChildComponentContainer(Widget widget, int orientation) { + super(); + + containerDIV = Document.get().createDivElement(); + + widgetDIV = Document.get().createDivElement(); + if (BrowserInfo.get().isFF2()) { + Style style = widgetDIV.getStyle(); + // FF2 chokes on some floats very easily. Measuring size escpecially + // becomes terribly slow + TableElement tableEl = Document.get().createTableElement(); + tableEl + .setInnerHTML("<tbody><tr><td><div></div></td></tr></tbody>"); + DivElement div = (DivElement) tableEl.getFirstChildElement() + .getFirstChildElement().getFirstChildElement() + .getFirstChildElement(); + tableEl.setCellPadding(0); + tableEl.setCellSpacing(0); + tableEl.setBorder(0); + div.getStyle().setProperty("padding", "0"); + + setElement(tableEl); + containerDIV = div; + } else { + setFloat(widgetDIV, "left"); + setElement(containerDIV); + containerDIV.getStyle().setProperty("height", "0"); + containerDIV.getStyle().setProperty("width", "0px"); + containerDIV.getStyle().setProperty("overflow", "hidden"); + } + + if (BrowserInfo.get().isIE()) { + /* + * IE requires position: relative on overflow:hidden elements if + * they should hide position:relative elements. Without this e.g. a + * 1000x1000 Panel inside an 500x500 OrderedLayout will not be + * clipped but fully shown. + */ + containerDIV.getStyle().setProperty("position", "relative"); + widgetDIV.getStyle().setProperty("position", "relative"); + } + + containerDIV.appendChild(widgetDIV); + + setOrientation(orientation); + + setWidget(widget); + + } + + public void setWidget(Widget w) { + // Validate + if (w == widget) { + return; + } + + // Detach new child. + if (w != null) { + w.removeFromParent(); + } + + // Remove old child. + if (widget != null) { + remove(widget); + } + + // Logical attach. + widget = w; + + if (w != null) { + // Physical attach. + widgetDIV.appendChild(widget.getElement()); + adopt(w); + } + } + + private static void setFloat(Element div, String floatString) { + if (BrowserInfo.get().isIE()) { + div.getStyle().setProperty("styleFloat", floatString); + // IE requires display:inline for margin-left to work together + // with float:left + if (floatString.equals("left")) { + div.getStyle().setProperty("display", "inline"); + } else { + div.getStyle().setProperty("display", "block"); + } + + } else { + div.getStyle().setProperty("cssFloat", floatString); + } + } + + public void setOrientation(int orientation) { + if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) { + setFloat(getElement(), "left"); + } else { + setFloat(getElement(), ""); + } + setHeight("0px"); + // setWidth("0px"); + contSize.setHeight(0); + contSize.setWidth(0); + containerMarginLeft = 0; + containerMarginTop = 0; + containerDIV.getStyle().setProperty("paddingLeft", "0"); + containerDIV.getStyle().setProperty("paddingTop", "0"); + + containerExpansion.setHeight(0); + containerExpansion.setWidth(0); + + // Clear old alignments + clearAlignments(); + + } + + public void renderChild(UIDL childUIDL, ApplicationConnection client, + int fixedWidth) { + /* + * Must remove width specification from container before rendering to + * allow components to grow in horizontal direction. + * + * For fixed width layouts we specify the width directly so that height + * is automatically calculated correctly (e.g. for Labels). + */ + /* + * This should no longer be needed (after #2563) as all components are + * such that they can be rendered inside a 0x0 DIV. + */ + // if (fixedWidth > 0) { + // setLimitedContainerWidth(fixedWidth); + // } else { + // setUnlimitedContainerWidth(); + // } + ((Paintable) widget).updateFromUIDL(childUIDL, client); + } + + public void setUnlimitedContainerWidth() { + setLimitedContainerWidth(1000000); + } + + public void setLimitedContainerWidth(int width) { + containerDIV.getStyle().setProperty("width", width + "px"); + } + + public void updateWidgetSize() { + /* + * Widget wrapper includes margin which the widget offsetWidth/Height + * does not include + */ + int w = Util.getRequiredWidth(widgetDIV); + int h = Util.getRequiredHeight(widgetDIV); + + widgetSize.setWidth(w); + widgetSize.setHeight(h); + + // ApplicationConnection.getConsole().log( + // Util.getSimpleName(widget) + " size is " + w + "," + h); + + } + + public void setMarginLeft(int marginLeft) { + containerMarginLeft = marginLeft; + containerDIV.getStyle().setPropertyPx("paddingLeft", marginLeft); + } + + public void setMarginTop(int marginTop) { + containerMarginTop = marginTop; + containerDIV.getStyle().setPropertyPx("paddingTop", + marginTop + alignmentTopOffset); + + updateContainerDOMSize(); + } + + public void updateAlignments(int parentWidth, int parentHeight) { + if (parentHeight == -1) { + parentHeight = contSize.getHeight(); + } + if (parentWidth == -1) { + parentWidth = contSize.getWidth(); + } + + alignmentTopOffset = calculateVerticalAlignmentTopOffset(parentHeight); + + calculateHorizontalAlignment(parentWidth); + + applyAlignments(); + + } + + private void applyAlignments() { + + // Update top margin to take alignment into account + setMarginTop(containerMarginTop); + + if (caption != null) { + caption.getElement().getStyle().setPropertyPx("marginLeft", + alignmentLeftOffsetForCaption); + } + widgetDIV.getStyle().setPropertyPx("marginLeft", + alignmentLeftOffsetForWidget); + } + + public int getCaptionRequiredWidth() { + if (caption == null) { + return 0; + } + + return captionRequiredWidth; + } + + public int getCaptionWidth() { + if (caption == null) { + return 0; + } + + return captionWidth; + } + + public int getCaptionHeight() { + if (caption == null) { + return 0; + } + + return captionHeight; + } + + public int getCaptionWidthAfterComponent() { + if (caption == null || !caption.shouldBePlacedAfterComponent()) { + return 0; + } + + return getCaptionWidth(); + } + + public int getCaptionHeightAboveComponent() { + if (caption == null || caption.shouldBePlacedAfterComponent()) { + return 0; + } + + return getCaptionHeight(); + } + + private int calculateVerticalAlignmentTopOffset(int emptySpace) { + if (alignment.isTop()) { + return 0; + } + + if (caption != null) { + if (caption.shouldBePlacedAfterComponent()) { + /* + * Take into account the rare case that the caption on the right + * side of the component AND is higher than the component + */ + emptySpace -= Math.max(widgetSize.getHeight(), caption + .getHeight()); + } else { + emptySpace -= widgetSize.getHeight(); + emptySpace -= getCaptionHeight(); + } + } else { + /* + * There is no caption and thus we do not need to take anything but + * the widget into account + */ + emptySpace -= widgetSize.getHeight(); + } + + int top = 0; + if (alignment.isVerticalCenter()) { + top = emptySpace / 2; + } else if (alignment.isBottom()) { + top = emptySpace; + } + + if (top < 0) { + top = 0; + } + return top; + } + + private void calculateHorizontalAlignment(int emptySpace) { + alignmentLeftOffsetForCaption = 0; + alignmentLeftOffsetForWidget = 0; + + if (alignment.isLeft()) { + return; + } + + int captionSpace = emptySpace; + int widgetSpace = emptySpace; + + if (caption != null) { + // There is a caption + if (caption.shouldBePlacedAfterComponent()) { + /* + * The caption is after component. In this case the caption + * needs no alignment. + */ + captionSpace = 0; + widgetSpace -= widgetSize.getWidth(); + widgetSpace -= getCaptionWidth(); + } else { + /* + * The caption is above the component. Caption and widget needs + * separate alignment offsets. + */ + widgetSpace -= widgetSize.getWidth(); + captionSpace -= getCaptionWidth(); + } + } else { + /* + * There is no caption and thus we do not need to take anything but + * the widget into account + */ + captionSpace = 0; + widgetSpace -= widgetSize.getWidth(); + } + + if (alignment.isHorizontalCenter()) { + alignmentLeftOffsetForCaption = captionSpace / 2; + alignmentLeftOffsetForWidget = widgetSpace / 2; + } else if (alignment.isRight()) { + alignmentLeftOffsetForCaption = captionSpace; + alignmentLeftOffsetForWidget = widgetSpace; + } + + if (alignmentLeftOffsetForCaption < 0) { + alignmentLeftOffsetForCaption = 0; + } + if (alignmentLeftOffsetForWidget < 0) { + alignmentLeftOffsetForWidget = 0; + } + + } + + public void setAlignment(AlignmentInfo alignmentInfo) { + alignment = alignmentInfo; + + } + + public Size getWidgetSize() { + return widgetSize; + } + + public void updateCaption(UIDL uidl, ApplicationConnection client) { + if (ICaption.isNeeded(uidl)) { + // We need a caption + + ICaption newCaption = caption; + + if (newCaption == null) { + newCaption = new ICaption((Paintable) widget, client); + // Set initial height to avoid Safari flicker + newCaption.setHeight("18px"); + // newCaption.setHeight(newCaption.getHeight()); // This might + // be better... ?? + } + + boolean positionChanged = newCaption.updateCaption(uidl); + + if (newCaption != caption || positionChanged) { + setCaption(newCaption); + } + + } else { + // Caption is not needed + if (caption != null) { + remove(caption); + } + + } + + updateCaptionSize(); + } + + public void updateCaptionSize() { + captionWidth = 0; + captionHeight = 0; + + if (caption != null) { + captionWidth = caption.getRenderedWidth(); + captionHeight = caption.getHeight(); + captionRequiredWidth = caption.getRequiredWidth(); + + /* + * ApplicationConnection.getConsole().log( + * "Caption rendered width: " + captionWidth + + * ", caption required width: " + captionRequiredWidth + + * ", caption height: " + captionHeight); + */ + } + + } + + private void setCaption(ICaption newCaption) { + // Validate + // if (newCaption == caption) { + // return; + // } + + // Detach new child. + if (newCaption != null) { + newCaption.removeFromParent(); + } + + // Remove old child. + if (caption != null && newCaption != caption) { + remove(caption); + } + + // Logical attach. + caption = newCaption; + + if (caption != null) { + // Physical attach. + if (caption.shouldBePlacedAfterComponent()) { + Util.setFloat(caption.getElement(), "left"); + containerDIV.appendChild(caption.getElement()); + } else { + Util.setFloat(caption.getElement(), ""); + containerDIV.insertBefore(caption.getElement(), widgetDIV); + } + + adopt(caption); + } + + } + + @Override + public boolean remove(Widget child) { + // Validate + if (child != caption && child != widget) { + return false; + } + + // Orphan + orphan(child); + + // Physical && Logical Detach + if (child == caption) { + containerDIV.removeChild(child.getElement()); + caption = null; + } else { + widgetDIV.removeChild(child.getElement()); + widget = null; + } + + return true; + } + + public Iterator<Widget> iterator() { + return new ChildComponentContainerIterator<Widget>(); + } + + public class ChildComponentContainerIterator<T> implements Iterator<Widget> { + private int id = 0; + + public boolean hasNext() { + return (id < size()); + } + + public Widget next() { + Widget w = get(id); + id++; + return w; + } + + private Widget get(int i) { + if (i == 0) { + if (widget != null) { + return widget; + } else if (caption != null) { + return caption; + } else { + throw new NoSuchElementException(); + } + } else if (i == 1) { + if (widget != null && caption != null) { + return caption; + } else { + throw new NoSuchElementException(); + } + } else { + throw new NoSuchElementException(); + } + } + + public void remove() { + int toRemove = id - 1; + if (toRemove == 0) { + if (widget != null) { + ChildComponentContainer.this.remove(widget); + } else if (caption != null) { + ChildComponentContainer.this.remove(caption); + } else { + throw new IllegalStateException(); + } + + } else if (toRemove == 1) { + if (widget != null && caption != null) { + ChildComponentContainer.this.remove(caption); + } else { + throw new IllegalStateException(); + } + } else { + throw new IllegalStateException(); + } + + id--; + } + } + + public int size() { + if (widget != null) { + if (caption != null) { + return 2; + } else { + return 1; + } + } else { + if (caption != null) { + return 1; + } else { + return 0; + } + } + } + + public Widget getWidget() { + return widget; + } + + /** + * Return true if the size of the widget has been specified in the selected + * orientation. + * + * @return + */ + public boolean widgetHasSizeSpecified(int orientation) { + String size; + if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) { + size = widget.getElement().getStyle().getProperty("width"); + } else { + size = widget.getElement().getStyle().getProperty("height"); + } + return (size != null && !size.equals("")); + } + + public boolean isComponentRelativeSized(int orientation) { + if (relativeSize == null) { + return false; + } + if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) { + return relativeSize.getWidth() >= 0; + } else { + return relativeSize.getHeight() >= 0; + } + } + + public void setRelativeSize(FloatSize relativeSize) { + this.relativeSize = relativeSize; + } + + public Size getContSize() { + return contSize; + } + + public void clearAlignments() { + alignmentLeftOffsetForCaption = 0; + alignmentLeftOffsetForWidget = 0; + alignmentTopOffset = 0; + applyAlignments(); + + } + + public void setExpandRatio(int expandRatio) { + this.expandRatio = (expandRatio / 1000.0f); + } + + public int expand(int orientation, int spaceForExpansion) { + int expansionAmount = (int) ((double) spaceForExpansion * expandRatio); + + if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) { + // HORIZONTAL + containerExpansion.setWidth(expansionAmount); + } else { + // VERTICAL + containerExpansion.setHeight(expansionAmount); + } + + return expansionAmount; + } + + public void expandExtra(int orientation, int extra) { + if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) { + // HORIZONTAL + containerExpansion.setWidth(containerExpansion.getWidth() + extra); + } else { + // VERTICAL + containerExpansion + .setHeight(containerExpansion.getHeight() + extra); + } + + } + + public void setContainerSize(int widgetAndCaptionWidth, + int widgetAndCaptionHeight) { + + int containerWidth = widgetAndCaptionWidth; + containerWidth += containerExpansion.getWidth(); + + int containerHeight = widgetAndCaptionHeight; + containerHeight += containerExpansion.getHeight(); + + // ApplicationConnection.getConsole().log( + // "Setting container size for " + Util.getSimpleName(widget) + // + " to " + containerWidth + "," + containerHeight); + + if (containerWidth < 0) { + ApplicationConnection.getConsole().log( + "containerWidth should never be negative: " + + containerWidth); + containerWidth = 0; + } + if (containerHeight < 0) { + ApplicationConnection.getConsole().log( + "containerHeight should never be negative: " + + containerHeight); + containerHeight = 0; + } + + contSize.setWidth(containerWidth); + contSize.setHeight(containerHeight); + + updateContainerDOMSize(); + } + + public void updateContainerDOMSize() { + int width = contSize.getWidth(); + int height = contSize.getHeight() - alignmentTopOffset; + if (width < 0) { + width = 0; + } + if (height < 0) { + height = 0; + } + + setWidth(width + "px"); + setHeight(height + "px"); + + // Also update caption max width + if (caption != null) { + if (caption.shouldBePlacedAfterComponent()) { + caption.setMaxWidth(captionWidth); + } else { + caption.setMaxWidth(width); + } + captionWidth = caption.getRenderedWidth(); + + // Remove initial height + caption.setHeight(""); + } + + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java b/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java new file mode 100644 index 0000000000..5010744ee5 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java @@ -0,0 +1,83 @@ +package com.vaadin.terminal.gwt.client.ui.layout;
+
+public class Margins {
+
+ private int marginTop;
+ private int marginBottom;
+ private int marginLeft;
+ private int marginRight;
+
+ private int horizontal = 0;
+ private int vertical = 0;
+
+ public Margins(int marginTop, int marginBottom, int marginLeft,
+ int marginRight) {
+ super();
+ this.marginTop = marginTop;
+ this.marginBottom = marginBottom;
+ this.marginLeft = marginLeft;
+ this.marginRight = marginRight;
+
+ updateHorizontal();
+ updateVertical();
+ }
+
+ public int getMarginTop() {
+ return marginTop;
+ }
+
+ public int getMarginBottom() {
+ return marginBottom;
+ }
+
+ public int getMarginLeft() {
+ return marginLeft;
+ }
+
+ public int getMarginRight() {
+ return marginRight;
+ }
+
+ public int getHorizontal() {
+ return horizontal;
+ }
+
+ public int getVertical() {
+ return vertical;
+ }
+
+ public void setMarginTop(int marginTop) {
+ this.marginTop = marginTop;
+ updateVertical();
+ }
+
+ public void setMarginBottom(int marginBottom) {
+ this.marginBottom = marginBottom;
+ updateVertical();
+ }
+
+ public void setMarginLeft(int marginLeft) {
+ this.marginLeft = marginLeft;
+ updateHorizontal();
+ }
+
+ public void setMarginRight(int marginRight) {
+ this.marginRight = marginRight;
+ updateHorizontal();
+ }
+
+ private void updateVertical() {
+ vertical = marginTop + marginBottom;
+ }
+
+ private void updateHorizontal() {
+ horizontal = marginLeft + marginRight;
+ }
+
+ @Override
+ public String toString() {
+ return "Margins [marginLeft=" + marginLeft + ",marginTop=" + marginTop
+ + ",marginRight=" + marginRight + ",marginBottom="
+ + marginBottom + "]";
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextArea.java b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextArea.java new file mode 100644 index 0000000000..b6a3c85238 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextArea.java @@ -0,0 +1,251 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui.richtextarea; + +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.ChangeListener; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.FocusListener; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.KeyboardListener; +import com.google.gwt.user.client.ui.RichTextArea; +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.Paintable; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.Util; +import com.vaadin.terminal.gwt.client.ui.Field; + +/** + * This class implements a basic client side rich text editor component. + * + * @author IT Mill Ltd. + * + */ +public class IRichTextArea extends Composite implements Paintable, Field, + ChangeListener, FocusListener, KeyboardListener { + + /** + * The input node CSS classname. + */ + public static final String CLASSNAME = "i-richtextarea"; + + protected String id; + + protected ApplicationConnection client; + + private boolean immediate = false; + + private RichTextArea rta = new RichTextArea(); + + private IRichTextToolbar formatter = new IRichTextToolbar(rta); + + private HTML html = new HTML(); + + private final FlowPanel fp = new FlowPanel(); + + private boolean enabled = true; + + private int extraHorizontalPixels = -1; + private int extraVerticalPixels = -1; + + private int maxLength = -1; + + private int toolbarNaturalWidth = 500; + + public IRichTextArea() { + fp.add(formatter); + + rta.setWidth("100%"); + rta.addFocusListener(this); + + fp.add(rta); + + initWidget(fp); + setStyleName(CLASSNAME); + + } + + public void setEnabled(boolean enabled) { + if (this.enabled != enabled) { + rta.setEnabled(enabled); + if (enabled) { + fp.remove(html); + fp.add(rta); + } else { + html.setHTML(rta.getHTML()); + fp.remove(rta); + fp.add(html); + } + + this.enabled = enabled; + } + } + + public void updateFromUIDL(final UIDL uidl, ApplicationConnection client) { + this.client = client; + id = uidl.getId(); + + if (uidl.hasVariable("text")) { + if (BrowserInfo.get().isIE()) { + // rta is rather buggy in IE (as pretty much everything is) + // it needs some "shaking" not to fall into uneditable state + // see #2374 + rta.getBasicFormatter().toggleBold(); + rta.getBasicFormatter().toggleBold(); + } + rta.setHTML(uidl.getStringVariable("text")); + + } + setEnabled(!uidl.getBooleanAttribute("disabled")); + + if (client.updateComponent(this, uidl, true)) { + return; + } + + immediate = uidl.getBooleanAttribute("immediate"); + int newMaxLength = uidl.hasAttribute("maxLength") ? uidl + .getIntAttribute("maxLength") : -1; + if (newMaxLength >= 0) { + if (maxLength == -1) { + rta.addKeyboardListener(this); + } + maxLength = newMaxLength; + } else if (maxLength != -1) { + getElement().setAttribute("maxlength", ""); + maxLength = -1; + rta.removeKeyboardListener(this); + } + } + + public void onChange(Widget sender) { + if (client != null && id != null) { + client.updateVariable(id, "text", rta.getText(), immediate); + } + } + + public void onFocus(Widget sender) { + + } + + public void onLostFocus(Widget sender) { + final String html = rta.getHTML(); + client.updateVariable(id, "text", html, immediate); + + } + + /** + * @return space used by components paddings and borders + */ + private int getExtraHorizontalPixels() { + if (extraHorizontalPixels < 0) { + detectExtraSizes(); + } + return extraHorizontalPixels; + } + + /** + * @return space used by components paddings and borders + */ + private int getExtraVerticalPixels() { + if (extraVerticalPixels < 0) { + detectExtraSizes(); + } + return extraVerticalPixels; + } + + /** + * Detects space used by components paddings and borders. + */ + private void detectExtraSizes() { + Element clone = Util.cloneNode(getElement(), false); + DOM.setElementAttribute(clone, "id", ""); + DOM.setStyleAttribute(clone, "visibility", "hidden"); + DOM.setStyleAttribute(clone, "position", "absolute"); + // due FF3 bug set size to 10px and later subtract it from extra pixels + DOM.setStyleAttribute(clone, "width", "10px"); + DOM.setStyleAttribute(clone, "height", "10px"); + DOM.appendChild(DOM.getParent(getElement()), clone); + extraHorizontalPixels = DOM.getElementPropertyInt(clone, "offsetWidth") - 10; + extraVerticalPixels = DOM.getElementPropertyInt(clone, "offsetHeight") - 10; + + DOM.removeChild(DOM.getParent(getElement()), clone); + } + + @Override + public void setHeight(String height) { + if (height.endsWith("px")) { + int h = Integer.parseInt(height.substring(0, height.length() - 2)); + h -= getExtraVerticalPixels(); + if (h < 0) { + h = 0; + } + + super.setHeight(h + "px"); + } else { + super.setHeight(height); + } + + if (height == null || height.equals("")) { + rta.setHeight(""); + } else { + int editorHeight = getOffsetHeight() - getExtraVerticalPixels() + - formatter.getOffsetHeight(); + rta.setHeight(editorHeight + "px"); + } + } + + @Override + public void setWidth(String width) { + if (width.endsWith("px")) { + int w = Integer.parseInt(width.substring(0, width.length() - 2)); + w -= getExtraHorizontalPixels(); + if (w < 0) { + w = 0; + } + + super.setWidth(w + "px"); + } else if (width.equals("")) { + /* + * IE cannot calculate the width of the 100% iframe correctly if + * there is no width specified for the parent. In this case we would + * use the toolbar but IE cannot calculate the width of that one + * correctly either in all cases. So we end up using a default width + * for a RichTextArea with no width definition in all browsers (for + * compatibility). + */ + + super.setWidth(toolbarNaturalWidth + "px"); + } else { + super.setWidth(width); + } + } + + public void onKeyDown(Widget sender, char keyCode, int modifiers) { + // NOP + } + + public void onKeyPress(Widget sender, char keyCode, int modifiers) { + if (maxLength >= 0) { + DeferredCommand.addCommand(new Command() { + public void execute() { + if (rta.getHTML().length() > maxLength) { + rta.setHTML(rta.getHTML().substring(0, maxLength)); + } + } + }); + } + } + + public void onKeyUp(Widget sender, char keyCode, int modifiers) { + // NOP + } + +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar$Strings.properties b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar$Strings.properties new file mode 100644 index 0000000000..363b704584 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar$Strings.properties @@ -0,0 +1,35 @@ +bold = Toggle Bold +createLink = Create Link +hr = Insert Horizontal Rule +indent = Indent Right +insertImage = Insert Image +italic = Toggle Italic +justifyCenter = Center +justifyLeft = Left Justify +justifyRight = Right Justify +ol = Insert Ordered List +outdent = Indent Left +removeFormat = Remove Formatting +removeLink = Remove Link +strikeThrough = Toggle Strikethrough +subscript = Toggle Subscript +superscript = Toggle Superscript +ul = Insert Unordered List +underline = Toggle Underline +color = Color +black = Black +white = White +red = Red +green = Green +yellow = Yellow +blue = Blue +font = Font +normal = Normal +size = Size +xxsmall = XX-Small +xsmall = X-Small +small = Small +medium = Medium +large = Large +xlarge = X-Large +xxlarge = XX-Large
\ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar.java b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar.java new file mode 100644 index 0000000000..433be77464 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar.java @@ -0,0 +1,474 @@ +/* + * Copyright 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.terminal.gwt.client.ui.richtextarea; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.i18n.client.Constants; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.AbstractImagePrototype; +import com.google.gwt.user.client.ui.ChangeListener; +import com.google.gwt.user.client.ui.ClickListener; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.ImageBundle; +import com.google.gwt.user.client.ui.KeyboardListener; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.PushButton; +import com.google.gwt.user.client.ui.RichTextArea; +import com.google.gwt.user.client.ui.ToggleButton; +import com.google.gwt.user.client.ui.Widget; + +/** + * A modified version of sample toolbar for use with {@link RichTextArea}. It + * provides a simple UI for all rich text formatting, dynamically displayed only + * for the available functionality. + */ +public class IRichTextToolbar extends Composite { + + /** + * This {@link ImageBundle} is used for all the button icons. Using an image + * bundle allows all of these images to be packed into a single image, which + * saves a lot of HTTP requests, drastically improving startup time. + */ + public interface Images extends ImageBundle { + + @ImageBundle.Resource("bold.gif") + AbstractImagePrototype bold(); + + @ImageBundle.Resource("createLink.gif") + AbstractImagePrototype createLink(); + + @ImageBundle.Resource("hr.gif") + AbstractImagePrototype hr(); + + @ImageBundle.Resource("indent.gif") + AbstractImagePrototype indent(); + + @ImageBundle.Resource("insertImage.gif") + AbstractImagePrototype insertImage(); + + @ImageBundle.Resource("italic.gif") + AbstractImagePrototype italic(); + + @ImageBundle.Resource("justifyCenter.gif") + AbstractImagePrototype justifyCenter(); + + @ImageBundle.Resource("justifyLeft.gif") + AbstractImagePrototype justifyLeft(); + + @ImageBundle.Resource("justifyRight.gif") + AbstractImagePrototype justifyRight(); + + @ImageBundle.Resource("ol.gif") + AbstractImagePrototype ol(); + + @ImageBundle.Resource("outdent.gif") + AbstractImagePrototype outdent(); + + @ImageBundle.Resource("removeFormat.gif") + AbstractImagePrototype removeFormat(); + + @ImageBundle.Resource("removeLink.gif") + AbstractImagePrototype removeLink(); + + @ImageBundle.Resource("strikeThrough.gif") + AbstractImagePrototype strikeThrough(); + + @ImageBundle.Resource("subscript.gif") + AbstractImagePrototype subscript(); + + @ImageBundle.Resource("superscript.gif") + AbstractImagePrototype superscript(); + + @ImageBundle.Resource("ul.gif") + AbstractImagePrototype ul(); + + @ImageBundle.Resource("underline.gif") + AbstractImagePrototype underline(); + } + + /** + * This {@link Constants} interface is used to make the toolbar's strings + * internationalizable. + */ + public interface Strings extends Constants { + + String black(); + + String blue(); + + String bold(); + + String color(); + + String createLink(); + + String font(); + + String green(); + + String hr(); + + String indent(); + + String insertImage(); + + String italic(); + + String justifyCenter(); + + String justifyLeft(); + + String justifyRight(); + + String large(); + + String medium(); + + String normal(); + + String ol(); + + String outdent(); + + String red(); + + String removeFormat(); + + String removeLink(); + + String size(); + + String small(); + + String strikeThrough(); + + String subscript(); + + String superscript(); + + String ul(); + + String underline(); + + String white(); + + String xlarge(); + + String xsmall(); + + String xxlarge(); + + String xxsmall(); + + String yellow(); + } + + /** + * We use an inner EventListener class to avoid exposing event methods on + * the RichTextToolbar itself. + */ + private class EventListener implements ClickListener, ChangeListener, + KeyboardListener { + + public void onChange(Widget sender) { + if (sender == backColors) { + basic.setBackColor(backColors.getValue(backColors + .getSelectedIndex())); + backColors.setSelectedIndex(0); + } else if (sender == foreColors) { + basic.setForeColor(foreColors.getValue(foreColors + .getSelectedIndex())); + foreColors.setSelectedIndex(0); + } else if (sender == fonts) { + basic.setFontName(fonts.getValue(fonts.getSelectedIndex())); + fonts.setSelectedIndex(0); + } else if (sender == fontSizes) { + basic.setFontSize(fontSizesConstants[fontSizes + .getSelectedIndex() - 1]); + fontSizes.setSelectedIndex(0); + } + } + + public void onClick(Widget sender) { + if (sender == bold) { + basic.toggleBold(); + } else if (sender == italic) { + basic.toggleItalic(); + } else if (sender == underline) { + basic.toggleUnderline(); + } else if (sender == subscript) { + basic.toggleSubscript(); + } else if (sender == superscript) { + basic.toggleSuperscript(); + } else if (sender == strikethrough) { + extended.toggleStrikethrough(); + } else if (sender == indent) { + extended.rightIndent(); + } else if (sender == outdent) { + extended.leftIndent(); + } else if (sender == justifyLeft) { + basic.setJustification(RichTextArea.Justification.LEFT); + } else if (sender == justifyCenter) { + basic.setJustification(RichTextArea.Justification.CENTER); + } else if (sender == justifyRight) { + basic.setJustification(RichTextArea.Justification.RIGHT); + } else if (sender == insertImage) { + final String url = Window.prompt("Enter an image URL:", + "http://"); + if (url != null) { + extended.insertImage(url); + } + } else if (sender == createLink) { + final String url = Window + .prompt("Enter a link URL:", "http://"); + if (url != null) { + extended.createLink(url); + } + } else if (sender == removeLink) { + extended.removeLink(); + } else if (sender == hr) { + extended.insertHorizontalRule(); + } else if (sender == ol) { + extended.insertOrderedList(); + } else if (sender == ul) { + extended.insertUnorderedList(); + } else if (sender == removeFormat) { + extended.removeFormat(); + } else if (sender == richText) { + // We use the RichTextArea's onKeyUp event to update the toolbar + // status. + // This will catch any cases where the user moves the cursur + // using the + // keyboard, or uses one of the browser's built-in keyboard + // shortcuts. + updateStatus(); + } + } + + public void onKeyDown(Widget sender, char keyCode, int modifiers) { + } + + public void onKeyPress(Widget sender, char keyCode, int modifiers) { + } + + public void onKeyUp(Widget sender, char keyCode, int modifiers) { + if (sender == richText) { + // We use the RichTextArea's onKeyUp event to update the toolbar + // status. + // This will catch any cases where the user moves the cursur + // using the + // keyboard, or uses one of the browser's built-in keyboard + // shortcuts. + updateStatus(); + } + } + } + + private static final RichTextArea.FontSize[] fontSizesConstants = new RichTextArea.FontSize[] { + RichTextArea.FontSize.XX_SMALL, RichTextArea.FontSize.X_SMALL, + RichTextArea.FontSize.SMALL, RichTextArea.FontSize.MEDIUM, + RichTextArea.FontSize.LARGE, RichTextArea.FontSize.X_LARGE, + RichTextArea.FontSize.XX_LARGE }; + + private final Images images = (Images) GWT.create(Images.class); + private final Strings strings = (Strings) GWT.create(Strings.class); + private final EventListener listener = new EventListener(); + + private final RichTextArea richText; + private final RichTextArea.BasicFormatter basic; + private final RichTextArea.ExtendedFormatter extended; + + private final FlowPanel outer = new FlowPanel(); + private final FlowPanel topPanel = new FlowPanel(); + private final FlowPanel bottomPanel = new FlowPanel(); + private ToggleButton bold; + private ToggleButton italic; + private ToggleButton underline; + private ToggleButton subscript; + private ToggleButton superscript; + private ToggleButton strikethrough; + private PushButton indent; + private PushButton outdent; + private PushButton justifyLeft; + private PushButton justifyCenter; + private PushButton justifyRight; + private PushButton hr; + private PushButton ol; + private PushButton ul; + private PushButton insertImage; + private PushButton createLink; + private PushButton removeLink; + private PushButton removeFormat; + + private ListBox backColors; + private ListBox foreColors; + private ListBox fonts; + private ListBox fontSizes; + + /** + * Creates a new toolbar that drives the given rich text area. + * + * @param richText + * the rich text area to be controlled + */ + public IRichTextToolbar(RichTextArea richText) { + this.richText = richText; + basic = richText.getBasicFormatter(); + extended = richText.getExtendedFormatter(); + + outer.add(topPanel); + outer.add(bottomPanel); + topPanel.setWidth("100%"); + topPanel.setHeight("20px"); + topPanel.getElement().getStyle().setProperty("overflow", "hidden"); + bottomPanel.setWidth("100%"); + + initWidget(outer); + setStyleName("gwt-RichTextToolbar"); + + if (basic != null) { + topPanel.add(bold = createToggleButton(images.bold(), strings + .bold())); + topPanel.add(italic = createToggleButton(images.italic(), strings + .italic())); + topPanel.add(underline = createToggleButton(images.underline(), + strings.underline())); + topPanel.add(subscript = createToggleButton(images.subscript(), + strings.subscript())); + topPanel.add(superscript = createToggleButton(images.superscript(), + strings.superscript())); + topPanel.add(justifyLeft = createPushButton(images.justifyLeft(), + strings.justifyLeft())); + topPanel.add(justifyCenter = createPushButton(images + .justifyCenter(), strings.justifyCenter())); + topPanel.add(justifyRight = createPushButton(images.justifyRight(), + strings.justifyRight())); + } + + if (extended != null) { + topPanel.add(strikethrough = createToggleButton(images + .strikeThrough(), strings.strikeThrough())); + topPanel.add(indent = createPushButton(images.indent(), strings + .indent())); + topPanel.add(outdent = createPushButton(images.outdent(), strings + .outdent())); + topPanel.add(hr = createPushButton(images.hr(), strings.hr())); + topPanel.add(ol = createPushButton(images.ol(), strings.ol())); + topPanel.add(ul = createPushButton(images.ul(), strings.ul())); + topPanel.add(insertImage = createPushButton(images.insertImage(), + strings.insertImage())); + topPanel.add(createLink = createPushButton(images.createLink(), + strings.createLink())); + topPanel.add(removeLink = createPushButton(images.removeLink(), + strings.removeLink())); + topPanel.add(removeFormat = createPushButton(images.removeFormat(), + strings.removeFormat())); + } + + if (basic != null) { + bottomPanel.add(backColors = createColorList("Background")); + bottomPanel.add(foreColors = createColorList("Foreground")); + bottomPanel.add(fonts = createFontList()); + bottomPanel.add(fontSizes = createFontSizes()); + + // We only use these listeners for updating status, so don't hook + // them up + // unless at least basic editing is supported. + richText.addKeyboardListener(listener); + richText.addClickListener(listener); + } + } + + private ListBox createColorList(String caption) { + final ListBox lb = new ListBox(); + lb.addChangeListener(listener); + lb.setVisibleItemCount(1); + + lb.addItem(caption); + lb.addItem(strings.white(), "white"); + lb.addItem(strings.black(), "black"); + lb.addItem(strings.red(), "red"); + lb.addItem(strings.green(), "green"); + lb.addItem(strings.yellow(), "yellow"); + lb.addItem(strings.blue(), "blue"); + return lb; + } + + private ListBox createFontList() { + final ListBox lb = new ListBox(); + lb.addChangeListener(listener); + lb.setVisibleItemCount(1); + + lb.addItem(strings.font(), ""); + lb.addItem(strings.normal(), ""); + lb.addItem("Times New Roman", "Times New Roman"); + lb.addItem("Arial", "Arial"); + lb.addItem("Courier New", "Courier New"); + lb.addItem("Georgia", "Georgia"); + lb.addItem("Trebuchet", "Trebuchet"); + lb.addItem("Verdana", "Verdana"); + return lb; + } + + private ListBox createFontSizes() { + final ListBox lb = new ListBox(); + lb.addChangeListener(listener); + lb.setVisibleItemCount(1); + + lb.addItem(strings.size()); + lb.addItem(strings.xxsmall()); + lb.addItem(strings.xsmall()); + lb.addItem(strings.small()); + lb.addItem(strings.medium()); + lb.addItem(strings.large()); + lb.addItem(strings.xlarge()); + lb.addItem(strings.xxlarge()); + return lb; + } + + private PushButton createPushButton(AbstractImagePrototype img, String tip) { + final PushButton pb = new PushButton(img.createImage()); + pb.addClickListener(listener); + pb.setTitle(tip); + return pb; + } + + private ToggleButton createToggleButton(AbstractImagePrototype img, + String tip) { + final ToggleButton tb = new ToggleButton(img.createImage()); + tb.addClickListener(listener); + tb.setTitle(tip); + return tb; + } + + /** + * Updates the status of all the stateful buttons. + */ + private void updateStatus() { + if (basic != null) { + bold.setDown(basic.isBold()); + italic.setDown(basic.isItalic()); + underline.setDown(basic.isUnderlined()); + subscript.setDown(basic.isSubscript()); + superscript.setDown(basic.isSuperscript()); + } + + if (extended != null) { + strikethrough.setDown(extended.isStrikethrough()); + } + } +} diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif Binary files differnew file mode 100644 index 0000000000..ddfc1cea2c --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif Binary files differnew file mode 100644 index 0000000000..249e5afc04 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif Binary files differnew file mode 100644 index 0000000000..3ab9e599f8 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif Binary files differnew file mode 100644 index 0000000000..c2f4c8cb21 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif Binary files differnew file mode 100644 index 0000000000..1629cabb78 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif Binary files differnew file mode 100644 index 0000000000..2bb89ef189 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png Binary files differnew file mode 100644 index 0000000000..80728186d8 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif Binary files differnew file mode 100644 index 0000000000..3fb1607e67 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif Binary files differnew file mode 100644 index 0000000000..8b837f0fd9 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif Binary files differnew file mode 100644 index 0000000000..db61c9a3de --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif Binary files differnew file mode 100644 index 0000000000..2b0a5a0a06 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif Binary files differnew file mode 100644 index 0000000000..7d22640af0 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif Binary files differnew file mode 100644 index 0000000000..3c0f350122 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif Binary files differnew file mode 100644 index 0000000000..99ee25836f --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif Binary files differnew file mode 100644 index 0000000000..833bb40667 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif Binary files differnew file mode 100644 index 0000000000..be8662411a --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif Binary files differnew file mode 100644 index 0000000000..a4339c059e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif Binary files differnew file mode 100644 index 0000000000..522ab4b29e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif Binary files differnew file mode 100644 index 0000000000..6b174c884e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif Binary files differnew file mode 100644 index 0000000000..04bba05b8b --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif Binary files differnew file mode 100644 index 0000000000..ac478eeb7e --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif Binary files differnew file mode 100644 index 0000000000..01380dbb83 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif Binary files differnew file mode 100644 index 0000000000..82bae11f59 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif diff --git a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java new file mode 100644 index 0000000000..87f966a65b --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java @@ -0,0 +1,1797 @@ +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.util.Collection; +import java.util.Date; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.xml.sax.SAXException; + +import com.itmill.toolkit.external.org.apache.commons.fileupload.servlet.ServletFileUpload; +import com.vaadin.Application; +import com.vaadin.Application.SystemMessages; +import com.vaadin.service.FileTypeResolver; +import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.ParameterHandler; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.ThemeResource; +import com.vaadin.terminal.URIHandler; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.ui.Window; + +/** + * Abstract implementation of the ApplicationServlet which handles all + * communication between the client and the server. + * + * It is possible to extend this class to provide own functionality but in most + * cases this is unnecessary. + * + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 6.0 + */ + +@SuppressWarnings("serial") +public abstract class AbstractApplicationServlet extends HttpServlet { + /** + * Version number of this release. For example "5.0.0". + */ + public static final String VERSION; + /** + * Major version number. For example 5 in 5.1.0. + */ + public static final int VERSION_MAJOR; + + /** + * Minor version number. For example 1 in 5.1.0. + */ + public static final int VERSION_MINOR; + + /** + * Builds number. For example 0-custom_tag in 5.0.0-custom_tag. + */ + public static final String VERSION_BUILD; + + /* Initialize version numbers from string replaced by build-script. */ + static { + if ("@VERSION@".equals("@" + "VERSION" + "@")) { + VERSION = "5.9.9-INTERNAL-NONVERSIONED-DEBUG-BUILD"; + } else { + VERSION = "@VERSION@"; + } + final String[] digits = VERSION.split("\\."); + VERSION_MAJOR = Integer.parseInt(digits[0]); + VERSION_MINOR = Integer.parseInt(digits[1]); + VERSION_BUILD = digits[2]; + } + + /** + * If the attribute is present in the request, a html fragment will be + * written instead of a whole page. + */ + public static final String REQUEST_FRAGMENT = ApplicationServlet.class + .getName() + + ".fragment"; + /** + * This request attribute forces widgetset used; e.g for portlets that can + * not have different widgetsets. + */ + public static final String REQUEST_WIDGETSET = ApplicationServlet.class + .getName() + + ".widgetset"; + /** + * This request attribute is used to add styles to the main element. E.g + * "height:500px" generates a style="height:500px" to the main element, + * useful from some embedding situations (e.g portlet include.) + */ + public static final String REQUEST_APPSTYLE = ApplicationServlet.class + .getName() + + ".style"; + + private Properties applicationProperties; + + private static final String NOT_PRODUCTION_MODE_INFO = "=================================================================\nIT Mill Toolkit is running in DEBUG MODE.\nAdd productionMode=true to web.xml to disable debug features.\nTo show debug window, add ?debug to your application URL.\n================================================================="; + + private boolean productionMode = false; + + private static final String URL_PARAMETER_RESTART_APPLICATION = "restartApplication"; + private static final String URL_PARAMETER_CLOSE_APPLICATION = "closeApplication"; + private static final String URL_PARAMETER_REPAINT_ALL = "repaintAll"; + protected static final String URL_PARAMETER_THEME = "theme"; + + private static final String SERVLET_PARAMETER_DEBUG = "Debug"; + private static final String SERVLET_PARAMETER_PRODUCTION_MODE = "productionMode"; + + // Configurable parameter names + private static final String PARAMETER_ITMILL_RESOURCES = "Resources"; + + private static final int DEFAULT_BUFFER_SIZE = 32 * 1024; + + private static final int MAX_BUFFER_SIZE = 64 * 1024; + + private static final String RESOURCE_URI = "/RES/"; + + private static final String AJAX_UIDL_URI = "/UIDL"; + + static final String THEME_DIRECTORY_PATH = "ITMILL/themes/"; + + private static final int DEFAULT_THEME_CACHETIME = 1000 * 60 * 60 * 24; + + static final String WIDGETSET_DIRECTORY_PATH = "ITMILL/widgetsets/"; + + // Name of the default widget set, used if not specified in web.xml + private static final String DEFAULT_WIDGETSET = "com.vaadin.terminal.gwt.DefaultWidgetSet"; + + // Widget set parameter name + private static final String PARAMETER_WIDGETSET = "widgetset"; + + private static final String ERROR_NO_WINDOW_FOUND = "Application did not give any window, did you remember to setMainWindow()?"; + + private String resourcePath = null; + + /** + * Called by the servlet container to indicate to a servlet that the servlet + * is being placed into service. + * + * @param servletConfig + * the object containing the servlet's configuration and + * initialization parameters + * @throws javax.servlet.ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + @Override + public void init(javax.servlet.ServletConfig servletConfig) + throws javax.servlet.ServletException { + super.init(servletConfig); + + // Stores the application parameters into Properties object + applicationProperties = new Properties(); + for (final Enumeration e = servletConfig.getInitParameterNames(); e + .hasMoreElements();) { + final String name = (String) e.nextElement(); + applicationProperties.setProperty(name, servletConfig + .getInitParameter(name)); + } + + // Overrides with server.xml parameters + final ServletContext context = servletConfig.getServletContext(); + for (final Enumeration e = context.getInitParameterNames(); e + .hasMoreElements();) { + final String name = (String) e.nextElement(); + applicationProperties.setProperty(name, context + .getInitParameter(name)); + } + + checkProductionMode(); + } + + private void checkProductionMode() { + // Check if the application is in production mode. + // We are in production mode if Debug=false or productionMode=true + if (getApplicationOrSystemProperty(SERVLET_PARAMETER_DEBUG, "true") + .equals("false")) { + // "Debug=true" is the old way and should no longer be used + productionMode = true; + } else if (getApplicationOrSystemProperty( + SERVLET_PARAMETER_PRODUCTION_MODE, "false").equals("true")) { + // "productionMode=true" is the real way to do it + productionMode = true; + } + + if (!productionMode) { + /* Print an information/warning message about running in debug mode */ + System.err.println(NOT_PRODUCTION_MODE_INFO); + } + + } + + /** + * Gets an application property value. + * + * @param parameterName + * the Name or the parameter. + * @return String value or null if not found + */ + protected String getApplicationProperty(String parameterName) { + + String val = applicationProperties.getProperty(parameterName); + if (val != null) { + return val; + } + + // Try lower case application properties for backward compatibility with + // 3.0.2 and earlier + val = applicationProperties.getProperty(parameterName.toLowerCase()); + + return val; + } + + /** + * Gets an system property value. + * + * @param parameterName + * the Name or the parameter. + * @return String value or null if not found + */ + protected String getSystemProperty(String parameterName) { + String val = null; + + String pkgName; + final Package pkg = getClass().getPackage(); + if (pkg != null) { + pkgName = pkg.getName(); + } else { + final String className = getClass().getName(); + pkgName = new String(className.toCharArray(), 0, className + .lastIndexOf('.')); + } + val = System.getProperty(pkgName + "." + parameterName); + if (val != null) { + return val; + } + + // Try lowercased system properties + val = System.getProperty(pkgName + "." + parameterName.toLowerCase()); + return val; + } + + /** + * Gets an application or system property value. + * + * @param parameterName + * the Name or the parameter. + * @param defaultValue + * the Default to be used. + * @return String value or default if not found + */ + private String getApplicationOrSystemProperty(String parameterName, + String defaultValue) { + + String val = null; + + // Try application properties + val = getApplicationProperty(parameterName); + if (val != null) { + return val; + } + + // Try system properties + val = getSystemProperty(parameterName); + if (val != null) { + return val; + } + + return defaultValue; + } + + /** + * Returns true if the servlet is running in production mode. Production + * mode disables all debug facilities. + * + * @return true if in production mode, false if in debug mode + */ + public boolean isProductionMode() { + return productionMode; + } + + /** + * Receives standard HTTP requests from the public service method and + * dispatches them. + * + * @param request + * the object that contains the request the client made of the + * servlet. + * @param response + * the object that contains the response the servlet returns to + * the client. + * @throws ServletException + * if an input or output error occurs while the servlet is + * handling the TRACE request. + * @throws IOException + * if the request for the TRACE cannot be handled. + */ + @Override + protected void service(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + + // check if we should serve static files (widgetsets, themes) + if (serveStaticResources(request, response)) { + return; + } + + Application application = null; + RequestType requestType = getRequestType(request); + + try { + // Find out which application this request is related to + application = findApplicationInstance(request, requestType); + if (application == null) { + return; + } + + /* + * Get or create a WebApplicationContext and an ApplicationManager + * for the session + */ + WebApplicationContext webApplicationContext = WebApplicationContext + .getApplicationContext(request.getSession()); + CommunicationManager applicationManager = webApplicationContext + .getApplicationManager(application, this); + + /* Update browser information from the request */ + webApplicationContext.getBrowser().updateBrowserProperties(request); + + /* + * Transaction starts. Call transaction listeners. Transaction end + * is called in the finally block below. + */ + webApplicationContext.startTransaction(application, request); + + // TODO Add screen height and width to the GWT client + + /* Handle the request */ + if (requestType == RequestType.FILE_UPLOAD) { + applicationManager.handleFileUpload(request, response); + return; + } else if (requestType == RequestType.UIDL) { + // Handles AJAX UIDL requests + applicationManager.handleUidlRequest(request, response, this); + return; + } + + // Removes application if it has stopped + if (!application.isRunning()) { + // FIXME How can this be reached? + endApplication(request, response, application); + return; + } + + // Finds the window within the application + Window window = getApplicationWindow(request, application); + if (window == null) { + throw new ServletException(ERROR_NO_WINDOW_FOUND); + } + + // Sets terminal type for the window, if not already set + if (window.getTerminal() == null) { + window.setTerminal(webApplicationContext.getBrowser()); + } + + // Handle parameters + final Map parameters = request.getParameterMap(); + if (window != null && parameters != null) { + window.handleParameters(parameters); + } + + /* + * Call the URI handlers and if this turns out to be a download + * request, send the file to the client + */ + if (handleURI(applicationManager, window, request, response)) { + return; + } + + String themeName = getThemeForWindow(request, window); + + // Handles theme resource requests + if (handleResourceRequest(request, response, themeName)) { + return; + } + + // Send initial AJAX page that kickstarts Toolkit application + writeAjaxPage(request, response, window, themeName, application); + + } catch (final SessionExpired e) { + // Session has expired, notify user + handleServiceSessionExpired(request, response); + } catch (final GeneralSecurityException e) { + handleServiceSecurityException(request, response); + } catch (final Throwable e) { + handleServiceException(request, response, application, e); + } finally { + // Notifies transaction end + if (application != null) { + ((WebApplicationContext) application.getContext()) + .endTransaction(application, request); + } + + // Work-around for GAE session problem. Explicitly touch session so + // it is re-serialized. + HttpSession session = request.getSession(false); + if (session != null) { + session.setAttribute("sessionUpdated", new Date().getTime()); + } + } + } + + protected ClassLoader getClassLoader() throws ServletException { + // Gets custom class loader + final String classLoaderName = getApplicationOrSystemProperty( + "ClassLoader", null); + ClassLoader classLoader; + if (classLoaderName == null) { + classLoader = getClass().getClassLoader(); + } else { + try { + final Class<?> classLoaderClass = getClass().getClassLoader() + .loadClass(classLoaderName); + final Constructor<?> c = classLoaderClass + .getConstructor(new Class[] { ClassLoader.class }); + classLoader = (ClassLoader) c + .newInstance(new Object[] { getClass().getClassLoader() }); + } catch (final Exception e) { + throw new ServletException( + "Could not find specified class loader: " + + classLoaderName, e); + } + } + return classLoader; + } + + /** + * Send notification to client's application. Used to notify client of + * critical errors and session expiration due to long inactivity. Server has + * no knowledge of what application client refers to. + * + * @param request + * the HTTP request instance. + * @param response + * the HTTP response to write to. + * @param caption + * for the notification + * @param message + * for the notification + * @param url + * url to load after message, null for current page + * @throws IOException + * if the writing failed due to input/output error. + */ + void criticalNotification(HttpServletRequest request, + HttpServletResponse response, String caption, String message, + String url) throws IOException { + + // clients JS app is still running, but server application either + // no longer exists or it might fail to perform reasonably. + // send a notification to client's application and link how + // to "restart" application. + + if (caption != null) { + caption = "\"" + caption + "\""; + } + if (message != null) { + message = "\"" + message + "\""; + } + if (url != null) { + url = "\"" + url + "\""; + } + + // Set the response type + response.setContentType("application/json; charset=UTF-8"); + final ServletOutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("for(;;);[{\"changes\":[], \"meta\" : {" + + "\"appError\": {" + "\"caption\":" + caption + "," + + "\"message\" : " + message + "," + "\"url\" : " + url + + "}}, \"resources\": {}, \"locales\":[]}]"); + outWriter.flush(); + outWriter.close(); + out.flush(); + } + + /** + * Returns the application instance to be used for the request. If an + * existing instance is not found a new one is created or null is returned + * to indicate that the application is not available. + * + * @param request + * @param requestType + * @return + * @throws MalformedURLException + * @throws SAXException + * @throws IllegalAccessException + * @throws InstantiationException + * @throws ServletException + * @throws SessionExpired + */ + private Application findApplicationInstance(HttpServletRequest request, + RequestType requestType) throws MalformedURLException, + SAXException, IllegalAccessException, InstantiationException, + ServletException, SessionExpired { + + final boolean restartApplication = (request + .getParameter(URL_PARAMETER_RESTART_APPLICATION) != null); + final boolean closeApplication = (request + .getParameter(URL_PARAMETER_CLOSE_APPLICATION) != null); + + Application application = getExistingApplication(request); + if (application != null) { + /* + * There is an existing application. We can use this as long as the + * user not specifically requested to close or restart it. + */ + if (restartApplication) { + closeApplication(application, request.getSession(false)); + return createApplication(request); + } else if (closeApplication) { + closeApplication(application, request.getSession(false)); + return null; + } else { + return application; + } + } + + // No existing application was found + if (requestType == RequestType.UIDL && isRepaintAll(request)) { + /* + * UIDL request contains valid repaintAll=1 event, the user probably + * wants to initiate a new application through a custom index.html + * without using writeAjaxPage. + */ + return createApplication(request); + + } else if (requestType == RequestType.OTHER) { + /* + * TODO Should *any* request really create a new application + * instance if none was found? + */ + return createApplication(request); + + } else { + /* The application was not found. Assume the session has expired. */ + throw new SessionExpired(); + } + + } + + /** + * Gets resource path using different implementations. Required to + * supporting different servlet container implementations (application + * servers). + * + * @param servletContext + * @param path + * the resource path. + * @return the resource path. + */ + protected static String getResourcePath(ServletContext servletContext, + String path) { + String resultPath = null; + resultPath = servletContext.getRealPath(path); + if (resultPath != null) { + return resultPath; + } else { + try { + final URL url = servletContext.getResource(path); + resultPath = url.getFile(); + } catch (final Exception e) { + // FIXME: Handle exception + e.printStackTrace(); + } + } + return resultPath; + } + + /** + * Handles the requested URI. An application can add handlers to do special + * processing, when a certain URI is requested. The handlers are invoked + * before any windows URIs are processed and if a DownloadStream is returned + * it is sent to the client. + * + * @param stream + * the download stream. + * + * @param request + * the HTTP request instance. + * @param response + * the HTTP response to write to. + * @throws IOException + * + * @see com.vaadin.terminal.URIHandler + */ + private void handleDownload(DownloadStream stream, + HttpServletRequest request, HttpServletResponse response) + throws IOException { + + if (stream.getParameter("Location") != null) { + response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + response.addHeader("Location", stream.getParameter("Location")); + return; + } + + // Download from given stream + final InputStream data = stream.getStream(); + if (data != null) { + + // Sets content type + response.setContentType(stream.getContentType()); + + // Sets cache headers + final long cacheTime = stream.getCacheTime(); + if (cacheTime <= 0) { + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + } else { + response.setHeader("Cache-Control", "max-age=" + cacheTime + / 1000); + response.setDateHeader("Expires", System.currentTimeMillis() + + cacheTime); + response.setHeader("Pragma", "cache"); // Required to apply + // caching in some + // Tomcats + } + + // Copy download stream parameters directly + // to HTTP headers. + final Iterator i = stream.getParameterNames(); + if (i != null) { + while (i.hasNext()) { + final String param = (String) i.next(); + response.setHeader(param, stream.getParameter(param)); + } + } + + // suggest local filename from DownloadStream if Content-Disposition + // not explicitly set + String contentDispositionValue = stream + .getParameter("Content-Disposition"); + if (contentDispositionValue == null) { + contentDispositionValue = "filename=\"" + stream.getFileName() + + "\""; + response.setHeader("Content-Disposition", + contentDispositionValue); + } + + int bufferSize = stream.getBufferSize(); + if (bufferSize <= 0 || bufferSize > MAX_BUFFER_SIZE) { + bufferSize = DEFAULT_BUFFER_SIZE; + } + final byte[] buffer = new byte[bufferSize]; + int bytesRead = 0; + + final OutputStream out = response.getOutputStream(); + + while ((bytesRead = data.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + out.flush(); + } + out.close(); + + } + + } + + /** + * Creates and starts a new application. This is not meant to be overridden. + * Override getNewApplication to create the application instance in a custom + * way. + * + * @param request + * @return + * @throws ServletException + * @throws MalformedURLException + */ + private Application createApplication(HttpServletRequest request) + throws ServletException, MalformedURLException { + Application newApplication = getNewApplication(request); + + // Create application + final URL applicationUrl = getApplicationUrl(request); + + // Initial locale comes from the request + Locale locale = request.getLocale(); + HttpSession session = request.getSession(); + + // Start the newly created application + startApplication(session, newApplication, applicationUrl, locale); + + return newApplication; + } + + private void handleServiceException(HttpServletRequest request, + HttpServletResponse response, Application application, Throwable e) + throws IOException, ServletException { + // if this was an UIDL request, response UIDL back to client + if (getRequestType(request) == RequestType.UIDL) { + Application.SystemMessages ci = getSystemMessages(); + criticalNotification(request, response, ci + .getInternalErrorCaption(), ci.getInternalErrorMessage(), + ci.getInternalErrorURL()); + if (application != null) { + application.getErrorHandler() + .terminalError(new RequestError(e)); + } else { + throw new ServletException(e); + } + } else { + // Re-throw other exceptions + throw new ServletException(e); + } + + } + + private String getThemeForWindow(HttpServletRequest request, Window window) { + // Finds theme name + String themeName = window.getTheme(); + if (request.getParameter(URL_PARAMETER_THEME) != null) { + themeName = request.getParameter(URL_PARAMETER_THEME); + } + + if (themeName == null) { + themeName = "default"; + } + + return themeName; + } + + /** + * Calls URI handlers for the request. If an URI handler returns a + * DownloadStream the stream is passed to the client for downloading. + * + * @param applicationManager + * @param window + * @param request + * @param response + * @return true if an DownloadStream was sent to the client, false otherwise + * @throws IOException + */ + private boolean handleURI(CommunicationManager applicationManager, + Window window, HttpServletRequest request, + HttpServletResponse response) throws IOException { + // Handles the URI + DownloadStream download = applicationManager.handleURI(window, request, + response); + + // A download request + if (download != null) { + // Client downloads an resource + handleDownload(download, request, response); + return true; + } + + return false; + } + + private void handleServiceSessionExpired(HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + + if (isOnUnloadRequest(request)) { + /* + * Request was an unload request (e.g. window close event) and the + * client expects no response if it fails. + */ + return; + } + + try { + Application.SystemMessages ci = getSystemMessages(); + if (getRequestType(request) != RequestType.UIDL) { + // 'plain' http req - e.g. browser reload; + // just go ahead redirect the browser + response.sendRedirect(ci.getSessionExpiredURL()); + } else { + // send uidl redirect + criticalNotification(request, response, ci + .getSessionExpiredCaption(), ci + .getSessionExpiredMessage(), ci.getSessionExpiredURL()); + /* + * Invalidate session (weird to have session if we're saying + * that it's expired, and worse: portal integration will fail + * since the session is not created by the portal. + */ + request.getSession().invalidate(); + } + } catch (SystemMessageException ee) { + throw new ServletException(ee); + } + + } + + private void handleServiceSecurityException(HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + if (isOnUnloadRequest(request)) { + /* + * Request was an unload request (e.g. window close event) and the + * client expects no response if it fails. + */ + return; + } + + // TODO handle differently? + // Invalid security key, show session expired message for now. + handleServiceSessionExpired(request, response); + } + + /** + * Creates a new application for the given request. + * + * @param request + * the HTTP request. + * @return A new Application instance. + * @throws ServletException + */ + protected abstract Application getNewApplication(HttpServletRequest request) + throws ServletException; + + /** + * Starts the application if it is not already running. Ensures the + * application is added to the WebApplicationContext. + * + * @param session + * @param application + * @param applicationUrl + * @param locale + * @throws ServletException + */ + private void startApplication(HttpSession session, Application application, + URL applicationUrl, Locale locale) throws ServletException { + if (application == null) { + throw new ServletException( + "Application is null and can't be started"); + } + + if (!application.isRunning()) { + final WebApplicationContext context = WebApplicationContext + .getApplicationContext(session); + // final URL applicationUrl = getApplicationUrl(request); + context.addApplication(application); + application.setLocale(locale); + application.start(applicationUrl, applicationProperties, context); + } + } + + /** + * Check if this is a request for a static resource and, if it is, serve the + * resource to the client. Returns true if a file was served and the request + * has been handled, false otherwise. + * + * @param request + * @param response + * @return + * @throws IOException + * @throws ServletException + */ + private boolean serveStaticResources(HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + + // FIXME What does 10 refer to? + String pathInfo = request.getPathInfo(); + if (pathInfo == null || pathInfo.length() <= 10) { + return false; + } + + if ((request.getContextPath() != null) + && (request.getRequestURI().startsWith("/ITMILL/"))) { + serveStaticResourcesInITMILL(request.getRequestURI(), response); + return true; + } else if (request.getRequestURI().startsWith( + request.getContextPath() + "/ITMILL/")) { + serveStaticResourcesInITMILL(request.getRequestURI().substring( + request.getContextPath().length()), response); + return true; + } + + return false; + } + + /** + * Serve resources from ITMILL directory. + * + * @param request + * @param response + * @throws IOException + * @throws ServletException + */ + private void serveStaticResourcesInITMILL(String filename, + HttpServletResponse response) throws IOException, ServletException { + + final ServletContext sc = getServletContext(); + InputStream is = sc.getResourceAsStream(filename); + if (is == null) { + // try if requested file is found from classloader + + // strip leading "/" otherwise stream from JAR wont work + filename = filename.substring(1); + is = getClassLoader().getResourceAsStream(filename); + + if (is == null) { + // cannot serve requested file + System.err + .println("Requested resource [" + + filename + + "] not found from filesystem or through class loader." + + " Add widgetset and/or theme JAR to your classpath or add files to WebContent/ITMILL folder."); + response.setStatus(404); + return; + } + } + final String mimetype = sc.getMimeType(filename); + if (mimetype != null) { + response.setContentType(mimetype); + } + final OutputStream os = response.getOutputStream(); + final byte buffer[] = new byte[20000]; + int bytes; + while ((bytes = is.read(buffer)) >= 0) { + os.write(buffer, 0, bytes); + } + } + + private enum RequestType { + FILE_UPLOAD, UIDL, OTHER; + } + + private RequestType getRequestType(HttpServletRequest request) { + if (isFileUploadRequest(request)) { + return RequestType.FILE_UPLOAD; + } else if (isUIDLRequest(request)) { + return RequestType.UIDL; + } + + return RequestType.OTHER; + } + + private boolean isUIDLRequest(HttpServletRequest request) { + String pathInfo = getRequestPathInfo(request); + + if (pathInfo == null) { + return false; + } + + String compare = AJAX_UIDL_URI; + + if (pathInfo.startsWith(compare + "/") || pathInfo.endsWith(compare)) { + return true; + } + + return false; + } + + private boolean isFileUploadRequest(HttpServletRequest request) { + return ServletFileUpload.isMultipartContent(request); + } + + private boolean isOnUnloadRequest(HttpServletRequest request) { + return request.getParameter(ApplicationConnection.PARAM_UNLOADBURST) != null; + } + + /** + * Get system messages from the current application class + * + * @return + */ + protected SystemMessages getSystemMessages() { + try { + Class appCls = getApplicationClass(); + Method m = appCls.getMethod("getSystemMessages", (Class[]) null); + return (Application.SystemMessages) m.invoke(null, (Object[]) null); + } catch (ClassNotFoundException e) { + // This should never happen + throw new SystemMessageException(e); + } catch (SecurityException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (NoSuchMethodException e) { + // This is completely ok and should be silently ignored + } catch (IllegalArgumentException e) { + // This should never happen + throw new SystemMessageException(e); + } catch (IllegalAccessException e) { + throw new SystemMessageException( + "Application.getSystemMessage() should be static public", e); + } catch (InvocationTargetException e) { + // This should never happen + throw new SystemMessageException(e); + } + return Application.getSystemMessages(); + } + + protected abstract Class getApplicationClass() + throws ClassNotFoundException; + + /** + * Resolve widgetset URL. Widgetset is not application specific. + * + * @param request + * @return + * @throws MalformedURLException + */ + String getWidgetsetLocation(HttpServletRequest request) + throws MalformedURLException { + URL applicationURL = getApplicationUrl(request); + + String applicationPath = applicationURL.getPath(); + String pathParts[] = applicationPath.split("\\/"); + + // if context is specified add it to widgetsetUrl + String ctxPath = request.getContextPath(); + if (ctxPath.length() == 0 + && request.getAttribute("javax.servlet.include.context_path") != null) { + // include request (e.g portlet), get context path from + // attribute + ctxPath = (String) request + .getAttribute("javax.servlet.include.context_path"); + } + + String widgetsetPath = ""; + if (pathParts.length > 1 + && pathParts[1].equals(ctxPath.replaceAll("\\/", ""))) { + widgetsetPath = "/" + pathParts[1]; + } + + return widgetsetPath; + + } + + /** + * + * @param request + * the HTTP request. + * @param response + * the HTTP response to write to. + * @param out + * @param unhandledParameters + * @param window + * @param terminalType + * @param theme + * @throws IOException + * if the writing failed due to input/output error. + * @throws MalformedURLException + * if the application is denied access the persistent data store + * represented by the given URL. + */ + private void writeAjaxPage(HttpServletRequest request, + HttpServletResponse response, Window window, String themeName, + Application application) throws IOException, MalformedURLException, + ServletException { + + // e.g portlets only want a html fragment + boolean fragment = (request.getAttribute(REQUEST_FRAGMENT) != null); + if (fragment) { + request.setAttribute(Application.class.getName(), application); + } + + final BufferedWriter page = new BufferedWriter(new OutputStreamWriter( + response.getOutputStream())); + String pathInfo = getRequestPathInfo(request); + if (pathInfo == null) { + pathInfo = "/"; + } + + String title = ((window.getCaption() == null) ? "IT Mill Toolkit 5" + : window.getCaption()); + + String widgetset = null; + // request widgetset takes precedence (e.g portlet include) + Object reqParam = request.getAttribute(REQUEST_WIDGETSET); + try { + widgetset = (String) reqParam; + } catch (Exception e) { + // FIXME: Handle exception + System.err.println("Warning: request param '" + REQUEST_WIDGETSET + + "' could not be used (is not a String)" + e); + } + + // TODO: Any reason this could not use + // getApplicationOrSystemProperty with DEFAULT_WIDGETSET as default + // value ? + if (widgetset == null) { + widgetset = getApplicationProperty(PARAMETER_WIDGETSET); + } + if (widgetset == null) { + widgetset = DEFAULT_WIDGETSET; + } + + /* Fetch relative url to application */ + // don't use server and port in uri. It may cause problems with some + // virtual server configurations which lose the server name + String appUrl = getApplicationUrl(request).getPath(); + if (appUrl.endsWith("/")) { + appUrl = appUrl.substring(0, appUrl.length() - 1); + } + + final String widgetsetPath = getWidgetsetLocation(request); + + final String staticFilePath = getApplicationOrSystemProperty( + PARAMETER_ITMILL_RESOURCES, widgetsetPath); + + // Default theme does not use theme URI + String themeUri = null; + if (themeName != null) { + // Using custom theme + themeUri = staticFilePath + "/" + THEME_DIRECTORY_PATH + themeName; + } + + if (!fragment) { + // Window renders are not cacheable + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + response.setContentType("text/html; charset=UTF-8"); + + // write html header + page.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD " + + "XHTML 1.0 Transitional//EN\" " + + "\"http://www.w3.org/TR/xhtml1/" + + "DTD/xhtml1-transitional.dtd\">\n"); + + page.write("<html xmlns=\"http://www.w3.org/1999/xhtml\"" + + ">\n<head>\n"); + page + .write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n"); + page + .write("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=7\" />\n"); + page.write("<style type=\"text/css\">" + + "html, body {height:100%;}</style>"); + + // Add favicon links + page + .write("<link rel=\"shortcut icon\" type=\"image/vnd.microsoft.icon\" href=\"" + + themeUri + "/favicon.ico\" />"); + page + .write("<link rel=\"icon\" type=\"image/vnd.microsoft.icon\" href=\"" + + themeUri + "/favicon.ico\" />"); + + page.write("<title>" + title + "</title>"); + + page + .write("\n</head>\n<body scroll=\"auto\" class=\"i-generated-body\">\n"); + } + + String appId = appUrl; + if ("".equals(appUrl)) { + appId = "ROOT"; + } + appId = appId.replaceAll("[^a-zA-Z0-9]", ""); + // Add hashCode to the end, so that it is still (sort of) predictable, + // but indicates that it should not be used in CSS and such: + int hashCode = appId.hashCode(); + if (hashCode < 0) { + hashCode = -hashCode; + } + appId = appId + "-" + hashCode; + + // Get system messages + Application.SystemMessages systemMessages = null; + try { + systemMessages = getSystemMessages(); + } catch (SystemMessageException e) { + // failing to get the system messages is always a problem + throw new ServletException("CommunicationError!", e); + } + + if (isGecko17(request)) { + // special start page for gecko 1.7 versions. Firefox 1.0 is not + // supported, but the hack is make it possible to use linux and + // hosted mode browser for debugging. Note that due this hack, + // debugging gwt code in portals with linux will be problematic if + // there are multiple toolkit portlets visible at the same time. + // TODO remove this when hosted mode on linux gets newer gecko + + page.write("<iframe tabIndex=\"-1\" id=\"__gwt_historyFrame\" " + + "style=\"width:0;height:0;border:0;overflow:" + + "hidden\" src=\"javascript:false\"></iframe>\n"); + page.write("<script language='javascript' src='" + staticFilePath + + "/" + WIDGETSET_DIRECTORY_PATH + widgetset + "/" + + widgetset + ".nocache.js?" + new Date().getTime() + + "'></script>\n"); + page.write("<script type=\"text/javascript\">\n"); + page.write("//<![CDATA[\n"); + page.write("if(!itmill || !itmill.toolkitConfigurations) {\n " + + "if(!itmill) { var itmill = {}} \n" + + "itmill.toolkitConfigurations = {};\n" + + "itmill.themesLoaded = {}};\n"); + + if (!isProductionMode()) { + page.write("itmill.debug = true;\n"); + } + + page.write("itmill.toolkitConfigurations[\"" + appId + "\"] = {"); + page.write("appUri:'" + appUrl + "', "); + page.write("pathInfo: '" + pathInfo + "', "); + if (window != application.getMainWindow()) { + page.write("windowName: '" + window.getName() + "', "); + } + page.write("themeUri:"); + page.write(themeUri != null ? "'" + themeUri + "'" : "null"); + page.write(", versionInfo : {toolkitVersion:\""); + page.write(VERSION); + page.write("\",applicationVersion:\""); + page.write(application.getVersion()); + page.write("\"},"); + if (systemMessages != null) { + // Write the CommunicationError -message to client + String caption = systemMessages.getCommunicationErrorCaption(); + if (caption != null) { + caption = "\"" + caption + "\""; + } + String message = systemMessages.getCommunicationErrorMessage(); + if (message != null) { + message = "\"" + message + "\""; + } + String url = systemMessages.getCommunicationErrorURL(); + if (url != null) { + url = "\"" + url + "\""; + } + page.write("\"comErrMsg\": {" + "\"caption\":" + caption + "," + + "\"message\" : " + message + "," + "\"url\" : " + url + + "}"); + } + page.write("};\n//]]>\n</script>\n"); + + if (themeName != null) { + // Custom theme's stylesheet, load only once, in different + // script + // tag to be dominate styles injected by widget + // set + page.write("<script type=\"text/javascript\">\n"); + page.write("//<![CDATA[\n"); + page.write("if(!itmill.themesLoaded['" + themeName + "']) {\n"); + page + .write("var stylesheet = document.createElement('link');\n"); + page.write("stylesheet.setAttribute('rel', 'stylesheet');\n"); + page.write("stylesheet.setAttribute('type', 'text/css');\n"); + page.write("stylesheet.setAttribute('href', '" + themeUri + + "/styles.css');\n"); + page + .write("document.getElementsByTagName('head')[0].appendChild(stylesheet);\n"); + page.write("itmill.themesLoaded['" + themeName + + "'] = true;\n}\n"); + page.write("//]]>\n</script>\n"); + } + + } else { + page.write("<script type=\"text/javascript\">\n"); + page.write("//<![CDATA[\n"); + page.write("if(!itmill || !itmill.toolkitConfigurations) {\n " + + "if(!itmill) { var itmill = {}} \n" + + "itmill.toolkitConfigurations = {};\n" + + "itmill.themesLoaded = {};\n"); + if (!isProductionMode()) { + page.write("itmill.debug = true;\n"); + } + page + .write("document.write('<iframe tabIndex=\"-1\" id=\"__gwt_historyFrame\" " + + "style=\"width:0;height:0;border:0;overflow:" + + "hidden\" src=\"javascript:false\"></iframe>');\n"); + page.write("document.write(\"<script language='javascript' src='" + + staticFilePath + "/" + WIDGETSET_DIRECTORY_PATH + + widgetset + "/" + widgetset + ".nocache.js?" + + new Date().getTime() + "'><\\/script>\");\n}\n"); + + page.write("itmill.toolkitConfigurations[\"" + appId + "\"] = {"); + page.write("appUri:'" + appUrl + "', "); + page.write("pathInfo: '" + pathInfo + "', "); + if (window != application.getMainWindow()) { + page.write("windowName: '" + window.getName() + "', "); + } + page.write("themeUri:"); + page.write(themeUri != null ? "'" + themeUri + "'" : "null"); + page.write(", versionInfo : {toolkitVersion:\""); + page.write(VERSION); + page.write("\",applicationVersion:\""); + page.write(application.getVersion()); + page.write("\"},"); + if (systemMessages != null) { + // Write the CommunicationError -message to client + String caption = systemMessages.getCommunicationErrorCaption(); + if (caption != null) { + caption = "\"" + caption + "\""; + } + String message = systemMessages.getCommunicationErrorMessage(); + if (message != null) { + message = "\"" + message + "\""; + } + String url = systemMessages.getCommunicationErrorURL(); + if (url != null) { + url = "\"" + url + "\""; + } + + page.write("\"comErrMsg\": {" + "\"caption\":" + caption + "," + + "\"message\" : " + message + "," + "\"url\" : " + url + + "}"); + } + page.write("};\n//]]>\n</script>\n"); + + if (themeName != null) { + // Custom theme's stylesheet, load only once, in different + // script + // tag to be dominate styles injected by widget + // set + page.write("<script type=\"text/javascript\">\n"); + page.write("//<![CDATA[\n"); + page.write("if(!itmill.themesLoaded['" + themeName + "']) {\n"); + page + .write("var stylesheet = document.createElement('link');\n"); + page.write("stylesheet.setAttribute('rel', 'stylesheet');\n"); + page.write("stylesheet.setAttribute('type', 'text/css');\n"); + page.write("stylesheet.setAttribute('href', '" + themeUri + + "/styles.css');\n"); + page + .write("document.getElementsByTagName('head')[0].appendChild(stylesheet);\n"); + page.write("itmill.themesLoaded['" + themeName + + "'] = true;\n}\n"); + page.write("//]]>\n</script>\n"); + } + } + + // Warn if the widgetset has not been loaded after 15 seconds on + // inactivity + page.write("<script type=\"text/javascript\">\n"); + page.write("//<![CDATA[\n"); + page.write("setTimeout('if (typeof " + widgetset.replace('.', '_') + + " == \"undefined\") {alert(\"Failed to load the widgetset: " + + staticFilePath + "/" + WIDGETSET_DIRECTORY_PATH + widgetset + + "/" + widgetset + ".nocache.js\")};',15000);\n" + + "//]]>\n</script>\n"); + + String style = null; + reqParam = request.getAttribute(REQUEST_APPSTYLE); + if (reqParam != null) { + style = "style=\"" + reqParam + "\""; + } + /*- Add classnames; + * .i-app + * .i-app-loading + * .i-app-<simpleName for app class> + * .i-theme-<themeName, remove non-alphanum> + */ + String appClass = "i-app-"; + try { + appClass += getApplicationClass().getSimpleName(); + } catch (ClassNotFoundException e) { + appClass += "unknown"; + + e.printStackTrace(); + } + String themeClass = ""; + if (themeName != null) { + themeClass = "i-theme-" + themeName.replaceAll("[^a-zA-Z0-9]", ""); + } + + page.write("<div id=\"" + appId + "\" class=\"i-app i-app-loading " + + themeClass + " " + appClass + "\" " + + (style != null ? style : "") + "></div>\n"); + + if (!fragment) { + page.write("<noscript>" + getNoScriptMessage() + "</noscript>"); + page.write("</body>\n</html>\n"); + } + page.close(); + + } + + /** + * Returns a message printed for browsers without scripting support or if + * browsers scripting support is disabled. + */ + protected String getNoScriptMessage() { + return "You have to enable javascript in your browser to use an application built with IT Mill Toolkit."; + } + + private boolean isGecko17(HttpServletRequest request) { + final WebBrowser browser = WebApplicationContext.getApplicationContext( + request.getSession()).getBrowser(); + if (browser != null && browser.getBrowserApplication() != null) { + if (browser.getBrowserApplication().indexOf("rv:1.7.") > 0 + && browser.getBrowserApplication().indexOf("Gecko") > 0) { + return true; + } + } + return false; + } + + /** + * Handles theme resource file requests. Resources supplied with the themes + * are provided by the WebAdapterServlet. + * + * @param request + * the HTTP request. + * @param response + * the HTTP response. + * @return boolean <code>true</code> if the request was handled and further + * processing should be suppressed, <code>false</code> otherwise. + * @throws ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + private boolean handleResourceRequest(HttpServletRequest request, + HttpServletResponse response, String themeName) + throws ServletException { + + // If the resource path is unassigned, initialize it + if (resourcePath == null) { + resourcePath = request.getContextPath() + request.getServletPath() + + RESOURCE_URI; + // WebSphere Application Server related fix + resourcePath = resourcePath.replaceAll("//", "/"); + } + + String resourceId = request.getPathInfo(); + + // Checks if this really is a resource request + if (resourceId == null || !resourceId.startsWith(RESOURCE_URI)) { + return false; + } + + // Checks the resource type + resourceId = resourceId.substring(RESOURCE_URI.length()); + InputStream data = null; + + // Gets theme resources + try { + data = getServletContext().getResourceAsStream( + THEME_DIRECTORY_PATH + themeName + "/" + resourceId); + } catch (final Exception e) { + // FIXME: Handle exception + e.printStackTrace(); + data = null; + } + + // Writes the response + try { + if (data != null) { + response.setContentType(FileTypeResolver + .getMIMEType(resourceId)); + + // Use default cache time for theme resources + response.setHeader("Cache-Control", "max-age=" + + DEFAULT_THEME_CACHETIME / 1000); + response.setDateHeader("Expires", System.currentTimeMillis() + + DEFAULT_THEME_CACHETIME); + response.setHeader("Pragma", "cache"); // Required to apply + // caching in some + // Tomcats + + // Writes the data to client + final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int bytesRead = 0; + final OutputStream out = response.getOutputStream(); + while ((bytesRead = data.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + } + out.close(); + data.close(); + } else { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } + + } catch (final java.io.IOException e) { + // FIXME: Handle exception + System.err.println("Resource transfer failed: " + + request.getRequestURI() + ". (" + e.getMessage() + ")"); + } + + return true; + } + + /** + * Gets the current application URL from request. + * + * @param request + * the HTTP request. + * @throws MalformedURLException + * if the application is denied access to the persistent data + * store represented by the given URL. + */ + URL getApplicationUrl(HttpServletRequest request) + throws MalformedURLException { + final URL reqURL = new URL( + (request.isSecure() ? "https://" : "http://") + + request.getServerName() + + ((request.isSecure() && request.getServerPort() == 443) + || (!request.isSecure() && request + .getServerPort() == 80) ? "" : ":" + + request.getServerPort()) + + request.getRequestURI()); + String servletPath = ""; + if (request.getAttribute("javax.servlet.include.servlet_path") != null) { + // this is an include request + servletPath = request.getAttribute( + "javax.servlet.include.context_path").toString() + + request + .getAttribute("javax.servlet.include.servlet_path"); + + } else { + servletPath = request.getContextPath() + request.getServletPath(); + } + + if (servletPath.length() == 0 + || servletPath.charAt(servletPath.length() - 1) != '/') { + servletPath = servletPath + "/"; + } + + URL u = new URL(reqURL, servletPath); + return u; + } + + /** + * Gets the existing application for given request. Looks for application + * instance for given request based on the requested URL. + * + * @param request + * the HTTP request. + * @return Application instance, or null if the URL does not map to valid + * application. + * @throws MalformedURLException + * if the application is denied access to the persistent data + * store represented by the given URL. + * @throws SAXException + * @throws IllegalAccessException + * @throws InstantiationException + */ + private Application getExistingApplication(HttpServletRequest request) + throws MalformedURLException, SAXException, IllegalAccessException, + InstantiationException { + + // Ensures that the session is still valid + final HttpSession session = request.getSession(true); + + WebApplicationContext context = WebApplicationContext + .getApplicationContext(session); + + // Gets application list for the session. + final Collection applications = context.getApplications(); + + // Search for the application (using the application URI) from the list + for (final Iterator i = applications.iterator(); i.hasNext();) { + final Application sessionApplication = (Application) i.next(); + final String sessionApplicationPath = sessionApplication.getURL() + .getPath(); + String requestApplicationPath = getApplicationUrl(request) + .getPath(); + + if (requestApplicationPath.equals(sessionApplicationPath)) { + // Found a running application + if (sessionApplication.isRunning()) { + return sessionApplication; + } + // Application has stopped, so remove it before creating a new + // application + WebApplicationContext.getApplicationContext(session) + .removeApplication(sessionApplication); + break; + } + } + + // Existing application not found + return null; + } + + /** + * Ends the application. + * + * @param request + * the HTTP request. + * @param response + * the HTTP response to write to. + * @param application + * the application to end. + * @throws IOException + * if the writing failed due to input/output error. + */ + private void endApplication(HttpServletRequest request, + HttpServletResponse response, Application application) + throws IOException { + + String logoutUrl = application.getLogoutURL(); + if (logoutUrl == null) { + logoutUrl = application.getURL().toString(); + } + + final HttpSession session = request.getSession(); + if (session != null) { + WebApplicationContext.getApplicationContext(session) + .removeApplication(application); + } + + response.sendRedirect(response.encodeRedirectURL(logoutUrl)); + } + + /** + * Gets the existing application or create a new one. Get a window within an + * application based on the requested URI. + * + * @param request + * the HTTP Request. + * @param application + * the Application to query for window. + * @return Window matching the given URI or null if not found. + * @throws ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + private Window getApplicationWindow(HttpServletRequest request, + Application application) throws ServletException { + + Window window = null; + + // Finds the window where the request is handled + String path = getRequestPathInfo(request); + + // Main window as the URI is empty + if (path == null || path.length() == 0 || path.equals("/") + || path.startsWith("/APP/")) { + window = application.getMainWindow(); + } else { + String windowName = null; + if (path.charAt(0) == '/') { + path = path.substring(1); + } + final int index = path.indexOf('/'); + if (index < 0) { + windowName = path; + path = ""; + } else { + windowName = path.substring(0, index); + path = path.substring(index + 1); + } + window = application.getWindow(windowName); + + if (window == null) { + // By default, we use main window + window = application.getMainWindow(); + } else if (!window.isVisible()) { + // Implicitly painting without actually invoking paint() + window.requestRepaintRequests(); + + // If the window is invisible send a blank page + return null; + } + } + + return window; + } + + String getRequestPathInfo(HttpServletRequest request) { + return request.getPathInfo(); + } + + /** + * Gets relative location of a theme resource. + * + * @param theme + * the Theme name. + * @param resource + * the Theme resource. + * @return External URI specifying the resource + */ + public String getResourceLocation(String theme, ThemeResource resource) { + + if (resourcePath == null) { + return resource.getResourceId(); + } + return resourcePath + theme + "/" + resource.getResourceId(); + } + + private boolean isRepaintAll(HttpServletRequest request) { + return (request.getParameter(URL_PARAMETER_REPAINT_ALL) != null) + && (request.getParameter(URL_PARAMETER_REPAINT_ALL).equals("1")); + } + + private void closeApplication(Application application, HttpSession session) { + if (application == null) { + return; + } + + application.close(); + if (session != null) { + WebApplicationContext context = WebApplicationContext + .getApplicationContext(session); + context.applicationToAjaxAppMgrMap.remove(application); + // FIXME: Move to WebApplicationContext + context.removeApplication(application); + } + } + + /** + * Implementation of ParameterHandler.ErrorEvent interface. + */ + public class ParameterHandlerErrorImpl implements + ParameterHandler.ErrorEvent, Serializable { + + private ParameterHandler owner; + + private Throwable throwable; + + /** + * Gets the contained throwable. + * + * @see com.vaadin.terminal.Terminal.ErrorEvent#getThrowable() + */ + public Throwable getThrowable() { + return throwable; + } + + /** + * Gets the source ParameterHandler. + * + * @see com.vaadin.terminal.ParameterHandler.ErrorEvent#getParameterHandler() + */ + public ParameterHandler getParameterHandler() { + return owner; + } + + } + + /** + * Implementation of URIHandler.ErrorEvent interface. + */ + public class URIHandlerErrorImpl implements URIHandler.ErrorEvent, + Serializable { + + private final URIHandler owner; + + private final Throwable throwable; + + /** + * + * @param owner + * @param throwable + */ + private URIHandlerErrorImpl(URIHandler owner, Throwable throwable) { + this.owner = owner; + this.throwable = throwable; + } + + /** + * Gets the contained throwable. + * + * @see com.vaadin.terminal.Terminal.ErrorEvent#getThrowable() + */ + public Throwable getThrowable() { + return throwable; + } + + /** + * Gets the source URIHandler. + * + * @see com.vaadin.terminal.URIHandler.ErrorEvent#getURIHandler() + */ + public URIHandler getURIHandler() { + return owner; + } + } + + public class RequestError implements Terminal.ErrorEvent, Serializable { + + private final Throwable throwable; + + public RequestError(Throwable throwable) { + this.throwable = throwable; + } + + public Throwable getThrowable() { + return throwable; + } + + } +} diff --git a/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java b/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java new file mode 100644 index 0000000000..25b1c387a2 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java @@ -0,0 +1,93 @@ +package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Serializable;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.ActionResponse;
+import javax.portlet.Portlet;
+import javax.portlet.PortletConfig;
+import javax.portlet.PortletException;
+import javax.portlet.PortletRequestDispatcher;
+import javax.portlet.PortletSession;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+
+import com.vaadin.Application;
+
+@SuppressWarnings("serial")
+public class ApplicationPortlet implements Portlet, Serializable {
+ // The application to show
+ protected String app = null;
+ // some applications might require forced height (and, more seldom, width)
+ protected String style = null; // e.g "height:500px;"
+ protected String widgetset = null;
+
+ public void destroy() {
+
+ }
+
+ public void init(PortletConfig config) throws PortletException {
+ app = config.getInitParameter("application");
+ if (app == null) {
+ app = "PortletDemo";
+ }
+ style = config.getInitParameter("style");
+ widgetset = config.getInitParameter("widgetset");
+ }
+
+ public void processAction(ActionRequest request, ActionResponse response)
+ throws PortletException, IOException {
+ PortletApplicationContext.dispatchRequest(this, request, response);
+ }
+
+ public void render(RenderRequest request, RenderResponse response)
+ throws PortletException, IOException {
+
+ // display the IT Mill Toolkit application
+ writeAjaxWindow(request, response);
+ }
+
+ protected void writeAjaxWindow(RenderRequest request,
+ RenderResponse response) throws IOException {
+
+ response.setContentType("text/html");
+ if (app != null) {
+ PortletSession sess = request.getPortletSession();
+ PortletApplicationContext ctx = PortletApplicationContext
+ .getApplicationContext(sess);
+
+ PortletRequestDispatcher dispatcher = sess.getPortletContext()
+ .getRequestDispatcher("/" + app);
+
+ try {
+ request.setAttribute(ApplicationServlet.REQUEST_FRAGMENT,
+ "true");
+ if (widgetset != null) {
+ request.setAttribute(ApplicationServlet.REQUEST_WIDGETSET,
+ widgetset);
+ }
+ if (style != null) {
+ request.setAttribute(ApplicationServlet.REQUEST_APPSTYLE,
+ style);
+ }
+ dispatcher.include(request, response);
+
+ } catch (PortletException e) {
+ PrintWriter out = response.getWriter();
+ out.print("<h1>Servlet include failed!</h1>");
+ out.print("<div>" + e + "</div>");
+ ctx.setPortletApplication(this, null);
+ return;
+ }
+
+ Application app = (Application) request
+ .getAttribute(Application.class.getName());
+ ctx.setPortletApplication(this, app);
+ ctx.firePortletRenderRequest(this, request, response);
+
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/server/ApplicationRunnerServlet.java b/src/com/vaadin/terminal/gwt/server/ApplicationRunnerServlet.java new file mode 100644 index 0000000000..774b866a44 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ApplicationRunnerServlet.java @@ -0,0 +1,173 @@ +package com.vaadin.terminal.gwt.server; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.Application; + +@SuppressWarnings("serial") +public class ApplicationRunnerServlet extends AbstractApplicationServlet { + + /** + * The name of the application class currently used. Only valid within one + * request. + */ + String applicationClassName = ""; + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + super.init(servletConfig); + } + + @Override + protected void service(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + applicationClassName = getApplicationRunnerApplicationClassName(request); + super.service(request, response); + applicationClassName = ""; + + } + + @Override + URL getApplicationUrl(HttpServletRequest request) + throws MalformedURLException { + URL url = super.getApplicationUrl(request); + + String path = url.toString(); + path += applicationClassName; + path += "/"; + + return new URL(path); + } + + @Override + protected Application getNewApplication(HttpServletRequest request) + throws ServletException { + + // Creates a new application instance + try { + Class<?> applicationClass = getClassLoader().loadClass( + applicationClassName); + final Application application = (Application) applicationClass + .newInstance(); + + return application; + } catch (final IllegalAccessException e) { + throw new ServletException(e); + } catch (final InstantiationException e) { + throw new ServletException(e); + } catch (final ClassNotFoundException e) { + throw new ServletException( + new InstantiationException( + "Failed to load application class: " + + applicationClassName)); + } + + } + + private String getApplicationRunnerApplicationClassName( + HttpServletRequest request) { + return getApplicationRunnerURIs(request).applicationClassname; + } + + private static class URIS { + String widgetsetPath; + String applicationURI; + String context; + String runner; + String applicationClassname; + + } + + /** + * Parses application runner URIs. + * + * If request URL is e.g. + * http://localhost:8080/itmill/run/com.vaadin.demo.Calc then + * <ul> + * <li>context=itmill</li> + * <li>Runner servlet=run</li> + * <li>Toolkit application=com.vaadin.demo.Calc</li> + * </ul> + * + * @param request + * @return string array containing widgetset URI, application URI and + * context, runner, application classname + */ + private static URIS getApplicationRunnerURIs(HttpServletRequest request) { + final String[] urlParts = request.getRequestURI().toString().split( + "\\/"); + String context = null; + String runner = null; + URIS uris = new URIS(); + String applicationClassname = null; + String contextPath = request.getContextPath(); + if (urlParts[1].equals(contextPath.replaceAll("\\/", ""))) { + // class name comes after web context and runner application + context = urlParts[1]; + runner = urlParts[2]; + if (urlParts.length == 3) { + throw new IllegalArgumentException("No application specified"); + } + applicationClassname = urlParts[3]; + + uris.widgetsetPath = "/" + context; + uris.applicationURI = "/" + context + "/" + runner + "/" + + applicationClassname; + uris.context = context; + uris.runner = runner; + uris.applicationClassname = applicationClassname; + } else { + // no context + context = ""; + runner = urlParts[1]; + if (urlParts.length == 2) { + throw new IllegalArgumentException("No application specified"); + } + applicationClassname = urlParts[2]; + + uris.widgetsetPath = "/"; + uris.applicationURI = "/" + runner + "/" + applicationClassname; + uris.context = context; + uris.runner = runner; + uris.applicationClassname = applicationClassname; + } + return uris; + } + + // @Override + @Override + protected Class getApplicationClass() throws ClassNotFoundException { + // TODO use getClassLoader() ? + return getClass().getClassLoader().loadClass(applicationClassName); + } + + @Override + String getRequestPathInfo(HttpServletRequest request) { + String path = request.getPathInfo(); + if (path == null) { + return null; + } + + path = path.substring(1 + applicationClassName.length()); + return path; + } + + @Override + String getWidgetsetLocation(HttpServletRequest request) { + URIS uris = getApplicationRunnerURIs(request); + String widgetsetPath = uris.widgetsetPath; + if (widgetsetPath.equals("/")) { + widgetsetPath = ""; + } + + return widgetsetPath; + } + +} diff --git a/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java new file mode 100644 index 0000000000..990c84bab0 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java @@ -0,0 +1,84 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import com.vaadin.Application; + +/** + * This servlet connects IT Mill Toolkit Application to Web. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ + +@SuppressWarnings("serial") +public class ApplicationServlet extends AbstractApplicationServlet { + + // Private fields + private Class<? extends Application> applicationClass; + + /** + * Called by the servlet container to indicate to a servlet that the servlet + * is being placed into service. + * + * @param servletConfig + * the object containing the servlet's configuration and + * initialization parameters + * @throws javax.servlet.ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + @SuppressWarnings("unchecked") + @Override + public void init(javax.servlet.ServletConfig servletConfig) + throws javax.servlet.ServletException { + super.init(servletConfig); + + // Loads the application class using the same class loader + // as the servlet itself + + // Gets the application class name + final String applicationClassName = servletConfig + .getInitParameter("application"); + if (applicationClassName == null) { + throw new ServletException( + "Application not specified in servlet parameters"); + } + + try { + applicationClass = (Class<? extends Application>) getClassLoader() + .loadClass(applicationClassName); + } catch (final ClassNotFoundException e) { + throw new ServletException("Failed to load application class: " + + applicationClassName); + } + } + + @Override + protected Application getNewApplication(HttpServletRequest request) + throws ServletException { + + // Creates a new application instance + try { + final Application application = getApplicationClass().newInstance(); + + return application; + } catch (final IllegalAccessException e) { + throw new ServletException("getNewApplication failed", e); + } catch (final InstantiationException e) { + throw new ServletException("getNewApplication failed", e); + } + } + + @Override + protected Class<? extends Application> getApplicationClass() { + return applicationClass; + } +}
\ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java b/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java new file mode 100644 index 0000000000..62d99a5f02 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java @@ -0,0 +1,35 @@ +package com.vaadin.terminal.gwt.server;
+
+import java.util.Map;
+
+import com.vaadin.ui.Component;
+import com.vaadin.ui.AbstractComponent.ComponentErrorEvent;
+
+@SuppressWarnings("serial")
+public class ChangeVariablesErrorEvent implements ComponentErrorEvent {
+
+ private Throwable throwable;
+ private Component component;
+
+ private Map variableChanges;
+
+ public ChangeVariablesErrorEvent(Component component, Throwable throwable,
+ Map variableChanges) {
+ this.component = component;
+ this.throwable = throwable;
+ this.variableChanges = variableChanges;
+ }
+
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ public Component getComponent() {
+ return component;
+ }
+
+ public Map getVariableChanges() {
+ return variableChanges;
+ }
+
+}
\ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/server/CommunicationManager.java b/src/com/vaadin/terminal/gwt/server/CommunicationManager.java new file mode 100644 index 0000000000..c43c857c1a --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/CommunicationManager.java @@ -0,0 +1,1497 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.BufferedWriter; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.itmill.toolkit.external.org.apache.commons.fileupload.FileItemIterator; +import com.itmill.toolkit.external.org.apache.commons.fileupload.FileItemStream; +import com.itmill.toolkit.external.org.apache.commons.fileupload.FileUploadException; +import com.itmill.toolkit.external.org.apache.commons.fileupload.servlet.ServletFileUpload; +import com.vaadin.Application; +import com.vaadin.Application.SystemMessages; +import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.Paintable; +import com.vaadin.terminal.URIHandler; +import com.vaadin.terminal.UploadStream; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.terminal.Paintable.RepaintRequestEvent; +import com.vaadin.terminal.Terminal.ErrorEvent; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout; +import com.vaadin.ui.AbstractField; +import com.vaadin.ui.Component; +import com.vaadin.ui.Upload; +import com.vaadin.ui.Window; + +/** + * Application manager processes changes and paints for single application + * instance. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public class CommunicationManager implements Paintable.RepaintRequestListener, + Serializable { + + private static String GET_PARAM_REPAINT_ALL = "repaintAll"; + + /* Variable records indexes */ + private static final int VAR_PID = 1; + private static final int VAR_NAME = 2; + private static final int VAR_TYPE = 3; + private static final int VAR_VALUE = 0; + + private static final String VAR_RECORD_SEPARATOR = "\u001e"; + + private static final String VAR_FIELD_SEPARATOR = "\u001f"; + + public static final String VAR_BURST_SEPARATOR = "\u001d"; + + public static final String VAR_ARRAYITEM_SEPARATOR = "\u001c"; + + private final HashSet<String> currentlyOpenWindowsInClient = new HashSet<String>(); + + private static final int MAX_BUFFER_SIZE = 64 * 1024; + + private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts"; + + private final ArrayList<Paintable> dirtyPaintabletSet = new ArrayList<Paintable>(); + + private final HashMap<Paintable, String> paintableIdMap = new HashMap<Paintable, String>(); + + private final HashMap<String, Paintable> idPaintableMap = new HashMap<String, Paintable>(); + + private int idSequence = 0; + + private final AbstractApplicationServlet applicationServlet; + + private final Application application; + + // Note that this is only accessed from synchronized block and + // thus should be thread-safe. + private String closingWindowName = null; + + private List<String> locales; + + private int pendingLocalesIndex; + + private int timeoutInterval = -1; + + public CommunicationManager(Application application, + AbstractApplicationServlet applicationServlet) { + this.application = application; + requireLocale(application.getLocale().toString()); + this.applicationServlet = applicationServlet; + } + + /** + * Handles file upload request submitted via Upload component. + * + * @param request + * @param response + * @throws IOException + * @throws FileUploadException + */ + public void handleFileUpload(HttpServletRequest request, + HttpServletResponse response) throws IOException, + FileUploadException { + // Create a new file upload handler + final ServletFileUpload upload = new ServletFileUpload(); + + final UploadProgressListener pl = new UploadProgressListener(); + + upload.setProgressListener(pl); + + // Parse the request + FileItemIterator iter; + + try { + iter = upload.getItemIterator(request); + /* + * ATM this loop is run only once as we are uploading one file per + * request. + */ + while (iter.hasNext()) { + final FileItemStream item = iter.next(); + final String name = item.getFieldName(); + final String filename = item.getName(); + final String mimeType = item.getContentType(); + final InputStream stream = item.openStream(); + if (item.isFormField()) { + // ignored, upload requests contains only files + } else { + final String pid = name.split("_")[0]; + final Upload uploadComponent = (Upload) idPaintableMap + .get(pid); + if (uploadComponent.isReadOnly()) { + throw new FileUploadException( + "Warning: ignored file upload because upload component is set as read-only"); + } + if (uploadComponent == null) { + throw new FileUploadException( + "Upload component not found"); + } + synchronized (application) { + // put upload component into receiving state + uploadComponent.startUpload(); + } + final UploadStream upstream = new UploadStream() { + + public String getContentName() { + return filename; + } + + public String getContentType() { + return mimeType; + } + + public InputStream getStream() { + return stream; + } + + public String getStreamName() { + return "stream"; + } + + }; + + // tell UploadProgressListener which component is receiving + // file + pl.setUpload(uploadComponent); + + uploadComponent.receiveUpload(upstream); + } + } + } catch (final FileUploadException e) { + throw e; + } + + // Send short response to acknowledge client that request was done + response.setContentType("text/html"); + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("<html><body>download handled</body></html>"); + outWriter.flush(); + out.close(); + } + + /** + * Handles UIDL request + * + * @param request + * @param response + * @throws IOException + * @throws ServletException + */ + public void handleUidlRequest(HttpServletRequest request, + HttpServletResponse response, + AbstractApplicationServlet applicationServlet) throws IOException, + ServletException, InvalidUIDLSecurityKeyException { + + // repaint requested or session has timed out and new one is created + boolean repaintAll = (request.getParameter(GET_PARAM_REPAINT_ALL) != null) + || request.getSession().isNew(); + boolean analyzeLayouts = false; + if (repaintAll) { + // analyzing can be done only with repaintAll + analyzeLayouts = (request.getParameter(GET_PARAM_ANALYZE_LAYOUTS) != null); + } + + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + + // The rest of the process is synchronized with the application + // in order to guarantee that no parallel variable handling is + // made + synchronized (application) { + + // Finds the window within the application + Window window = null; + if (application.isRunning()) { + window = getApplicationWindow(request, application, null); + // Returns if no window found + if (window == null) { + // This should not happen, no windows exists but + // application is still open. + System.err + .println("Warning, could not get window for application with request URI " + + request.getRequestURI()); + return; + } + } else { + // application has been closed + endApplication(request, response, application); + return; + } + + // Change all variables based on request parameters + if (!handleVariables(request, response, application, window)) { + + // var inconsistency; the client is probably out-of-sync + SystemMessages ci = null; + try { + Method m = application.getClass().getMethod( + "getSystemMessages", (Class[]) null); + ci = (Application.SystemMessages) m.invoke(null, + (Object[]) null); + } catch (Exception e2) { + // FIXME: Handle exception + // Not critical, but something is still wrong; print + // stacktrace + e2.printStackTrace(); + } + if (ci != null) { + String msg = ci.getOutOfSyncMessage(); + String cap = ci.getOutOfSyncCaption(); + if (msg != null || cap != null) { + applicationServlet.criticalNotification(request, + response, cap, msg, ci.getOutOfSyncURL()); + // will reload page after this + return; + } + } + // No message to show, let's just repaint all. + repaintAll = true; + + } + + paintAfterVariablechanges(request, response, applicationServlet, + repaintAll, outWriter, window, analyzeLayouts); + + // Mark this window to be open on client + currentlyOpenWindowsInClient.add(window.getName()); + if (closingWindowName != null) { + currentlyOpenWindowsInClient.remove(closingWindowName); + closingWindowName = null; + } + } + + out.flush(); + out.close(); + } + + private void paintAfterVariablechanges(HttpServletRequest request, + HttpServletResponse response, + AbstractApplicationServlet applicationServlet, boolean repaintAll, + final PrintWriter outWriter, Window window, boolean analyzeLayouts) + throws IOException, ServletException, PaintException { + + // If repaint is requested, clean all ids in this root window + if (repaintAll) { + for (final Iterator it = idPaintableMap.keySet().iterator(); it + .hasNext();) { + final Component c = (Component) idPaintableMap.get(it.next()); + if (isChildOf(window, c)) { + it.remove(); + paintableIdMap.remove(c); + } + } + } + + // Removes application if it has stopped during variable changes + if (!application.isRunning()) { + endApplication(request, response, application); + return; + } + + // Sets the response type + response.setContentType("application/json; charset=UTF-8"); + // some dirt to prevent cross site scripting + outWriter.print("for(;;);[{"); + + outWriter.print("\"changes\":["); + + ArrayList<Paintable> paintables = null; + + // If the browser-window has been closed - we do not need to paint it at + // all + if (!window.getName().equals(closingWindowName)) { + + List<InvalidLayout> invalidComponentRelativeSizes = null; + + // re-get window - may have been changed + Window newWindow = getApplicationWindow(request, application, + window); + if (newWindow != window) { + window = newWindow; + repaintAll = true; + } + + JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter, + !repaintAll); + + // Paints components + if (repaintAll) { + paintables = new ArrayList<Paintable>(); + paintables.add(window); + + // Reset sent locales + locales = null; + requireLocale(application.getLocale().toString()); + + } else { + // remove detached components from paintableIdMap so they + // can be GC'ed + for (Iterator it = paintableIdMap.keySet().iterator(); it + .hasNext();) { + Component p = (Component) it.next(); + if (p.getApplication() == null) { + idPaintableMap.remove(paintableIdMap.get(p)); + it.remove(); + dirtyPaintabletSet.remove(p); + p.removeListener(this); + } + } + paintables = getDirtyVisibleComponents(window); + } + if (paintables != null) { + + // We need to avoid painting children before parent. + // This is ensured by ordering list by depth in component + // tree + Collections.sort(paintables, new Comparator<Paintable>() { + public int compare(Paintable o1, Paintable o2) { + Component c1 = (Component) o1; + Component c2 = (Component) o2; + int d1 = 0; + while (c1.getParent() != null) { + d1++; + c1 = c1.getParent(); + } + int d2 = 0; + while (c2.getParent() != null) { + d2++; + c2 = c2.getParent(); + } + if (d1 < d2) { + return -1; + } + if (d1 > d2) { + return 1; + } + return 0; + } + }); + + for (final Iterator i = paintables.iterator(); i.hasNext();) { + final Paintable p = (Paintable) i.next(); + + // TODO CLEAN + if (p instanceof Window) { + final Window w = (Window) p; + if (w.getTerminal() == null) { + w.setTerminal(application.getMainWindow() + .getTerminal()); + } + } + /* + * This does not seem to happen in tk5, but remember this + * case: else if (p instanceof Component) { if (((Component) + * p).getParent() == null || ((Component) + * p).getApplication() == null) { // Component requested + * repaint, but is no // longer attached: skip + * paintablePainted(p); continue; } } + */ + + // TODO we may still get changes that have been + // rendered already (changes with only cached flag) + if (paintTarget.needsToBePainted(p)) { + paintTarget.startTag("change"); + paintTarget.addAttribute("format", "uidl"); + final String pid = getPaintableId(p); + paintTarget.addAttribute("pid", pid); + + p.paint(paintTarget); + + paintTarget.endTag("change"); + } + paintablePainted(p); + + if (analyzeLayouts) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes(((Window) p) + .getLayout(), null, null); + } + } + } + + paintTarget.close(); + outWriter.print("]"); // close changes + + outWriter.print(", \"meta\" : {"); + boolean metaOpen = false; + + if (repaintAll) { + metaOpen = true; + outWriter.write("\"repaintAll\":true"); + if (analyzeLayouts) { + outWriter.write(", \"invalidLayouts\":"); + outWriter.write("["); + if (invalidComponentRelativeSizes != null) { + boolean first = true; + for (InvalidLayout invalidLayout : invalidComponentRelativeSizes) { + if (!first) { + outWriter.write(","); + } else { + first = false; + } + invalidLayout.reportErrors(outWriter, this, + System.err); + } + } + outWriter.write("]"); + } + } + + SystemMessages ci = null; + try { + Method m = application.getClass().getMethod( + "getSystemMessages", (Class[]) null); + ci = (Application.SystemMessages) m.invoke(null, + (Object[]) null); + } catch (NoSuchMethodException e1) { + e1.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + + // meta instruction for client to enable auto-forward to + // sessionExpiredURL after timer expires. + if (ci != null && ci.getSessionExpiredMessage() == null + && ci.getSessionExpiredCaption() == null + && ci.isSessionExpiredNotificationEnabled()) { + int newTimeoutInterval = request.getSession() + .getMaxInactiveInterval(); + if (repaintAll || (timeoutInterval != newTimeoutInterval)) { + String escapedURL = ci.getSessionExpiredURL() == null ? "" + : ci.getSessionExpiredURL().replace("/", "\\/"); + if (metaOpen) { + outWriter.write(","); + } + outWriter.write("\"timedRedirect\":{\"interval\":" + + (newTimeoutInterval + 15) + ",\"url\":\"" + + escapedURL + "\"}"); + metaOpen = true; + } + timeoutInterval = newTimeoutInterval; + } + + outWriter.print("}, \"resources\" : {"); + + // Precache custom layouts + String themeName = window.getTheme(); + if (request.getParameter("theme") != null) { + themeName = request.getParameter("theme"); + } + if (themeName == null) { + themeName = "default"; + } + + // TODO We should only precache the layouts that are not + // cached already + int resourceIndex = 0; + for (final Iterator i = paintTarget.getPreCachedResources() + .iterator(); i.hasNext();) { + final String resource = (String) i.next(); + InputStream is = null; + try { + is = applicationServlet + .getServletContext() + .getResourceAsStream( + "/" + + ApplicationServlet.THEME_DIRECTORY_PATH + + themeName + "/" + resource); + } catch (final Exception e) { + // FIXME: Handle exception + e.printStackTrace(); + } + if (is != null) { + + outWriter.print((resourceIndex++ > 0 ? ", " : "") + "\"" + + resource + "\" : "); + final StringBuffer layout = new StringBuffer(); + + try { + final InputStreamReader r = new InputStreamReader(is, + "UTF-8"); + final char[] buffer = new char[20000]; + int charsRead = 0; + while ((charsRead = r.read(buffer)) > 0) { + layout.append(buffer, 0, charsRead); + } + r.close(); + } catch (final java.io.IOException e) { + // FIXME: Handle exception + System.err.println("Resource transfer failed: " + + request.getRequestURI() + ". (" + + e.getMessage() + ")"); + } + outWriter.print("\"" + + JsonPaintTarget.escapeJSON(layout.toString()) + + "\""); + } else { + // FIXME: Handle exception + System.err.println("CustomLayout " + "/" + + ApplicationServlet.THEME_DIRECTORY_PATH + + themeName + "/" + resource + " not found!"); + } + } + outWriter.print("}"); + + printLocaleDeclarations(outWriter); + + outWriter.print("}]"); + } + outWriter.flush(); + outWriter.close(); + + } + + /** + * If this method returns false, something was submitted that we did not + * expect; this is probably due to the client being out-of-sync and sending + * variable changes for non-existing pids + * + * @param request + * @param application2 + * @return true if successful, false if there was an inconsistency + * @throws IOException + */ + private boolean handleVariables(HttpServletRequest request, + HttpServletResponse response, Application application2, + Window window) throws IOException, InvalidUIDLSecurityKeyException { + boolean success = true; + + if (request.getContentLength() > 0) { + String changes = readRequest(request); + + // Manage bursts one by one + final String[] bursts = changes.split(VAR_BURST_SEPARATOR); + + // Security: double cookie submission pattern unless disabled by + // property + if (!"true".equals(application2 + .getProperty("disable-xsrf-protection"))) { + if (bursts.length == 1 && "init".equals(bursts[0])) { + // initial request, no variable changes: send key + String seckey = (String) request.getSession().getAttribute( + ApplicationConnection.UIDL_SECURITY_HEADER); + if (seckey == null) { + seckey = "" + (int) (Math.random() * 1000000); + } + /* + * Cookie c = new Cookie( + * ApplicationConnection.UIDL_SECURITY_COOKIE_NAME, uuid); + * response.addCookie(c); + */ + response.setHeader( + ApplicationConnection.UIDL_SECURITY_HEADER, seckey); + request.getSession().setAttribute( + ApplicationConnection.UIDL_SECURITY_HEADER, seckey); + return true; + } else { + // check the key + String sessId = (String) request.getSession().getAttribute( + ApplicationConnection.UIDL_SECURITY_HEADER); + if (sessId == null || !sessId.equals(bursts[0])) { + throw new InvalidUIDLSecurityKeyException( + "Security key mismatch"); + } + } + } + + for (int bi = 1; bi < bursts.length; bi++) { + + // extract variables to two dim string array + final String[] tmp = bursts[bi].split(VAR_RECORD_SEPARATOR); + final String[][] variableRecords = new String[tmp.length][4]; + for (int i = 0; i < tmp.length; i++) { + variableRecords[i] = tmp[i].split(VAR_FIELD_SEPARATOR); + } + + for (int i = 0; i < variableRecords.length; i++) { + String[] variable = variableRecords[i]; + String[] nextVariable = null; + if (i + 1 < variableRecords.length) { + nextVariable = variableRecords[i + 1]; + } + final VariableOwner owner = (VariableOwner) idPaintableMap + .get(variable[VAR_PID]); + if (owner != null && owner.isEnabled()) { + Map m; + if (nextVariable != null + && variable[VAR_PID] + .equals(nextVariable[VAR_PID])) { + // we have more than one value changes in row for + // one variable owner, collect em in HashMap + m = new HashMap(); + m.put(variable[VAR_NAME], convertVariableValue( + variable[VAR_TYPE].charAt(0), + variable[VAR_VALUE])); + } else { + // use optimized single value map + m = new SingleValueMap(variable[VAR_NAME], + convertVariableValue(variable[VAR_TYPE] + .charAt(0), variable[VAR_VALUE])); + } + + // collect following variable changes for this owner + while (nextVariable != null + && variable[VAR_PID] + .equals(nextVariable[VAR_PID])) { + i++; + variable = nextVariable; + if (i + 1 < variableRecords.length) { + nextVariable = variableRecords[i + 1]; + } else { + nextVariable = null; + } + m.put(variable[VAR_NAME], convertVariableValue( + variable[VAR_TYPE].charAt(0), + variable[VAR_VALUE])); + } + try { + owner.changeVariables(request, m); + + // Special-case of closing browser-level windows: + // track browser-windows currently open in client + if (owner instanceof Window + && ((Window) owner).getParent() == null) { + final Boolean close = (Boolean) m.get("close"); + if (close != null && close.booleanValue()) { + closingWindowName = ((Window) owner) + .getName(); + } + } + } catch (Exception e) { + handleChangeVariablesError(application2, + (Component) owner, e, m); + } + } else { + + // Handle special case where window-close is called + // after the window has been removed from the + // application or the application has closed + if ("close".equals(variable[VAR_NAME]) + && "true".equals(variable[VAR_VALUE])) { + // Silently ignore this + continue; + } + + // Ignore variable change + String msg = "Warning: Ignoring variable change for "; + if (owner != null) { + msg += "disabled component " + owner.getClass(); + String caption = ((Component) owner).getCaption(); + if (caption != null) { + msg += ", caption=" + caption; + } + } else { + msg += "non-existent component, VAR_PID=" + + variable[VAR_PID]; + success = false; + } + System.err.println(msg); + continue; + } + } + + // In case that there were multiple bursts, we know that this is + // a special synchronous case for closing window. Thus we are + // not interested in sending any UIDL changes back to client. + // Still we must clear component tree between bursts to ensure + // that no removed components are updated. The painting after + // the last burst is handled normally by the calling method. + if (bi < bursts.length - 1) { + + // We will be discarding all changes + final PrintWriter outWriter = new PrintWriter( + new CharArrayWriter()); + try { + paintAfterVariablechanges(request, response, + applicationServlet, true, outWriter, window, + false); + } catch (ServletException e) { + // We will ignore all servlet exceptions + } + } + + } + } + return success; + } + + /** + * Reads the request data from the HttpServletRequest and returns it + * converted to an UTF-8 string. + * + * @param request + * @return + * @throws IOException + */ + private static String readRequest(HttpServletRequest request) + throws IOException { + + int requestLength = request.getContentLength(); + + byte[] buffer = new byte[requestLength]; + ServletInputStream inputStream = request.getInputStream(); + + int bytesRemaining = requestLength; + while (bytesRemaining > 0) { + int bytesToRead = Math.min(bytesRemaining, MAX_BUFFER_SIZE); + int bytesRead = inputStream.read(buffer, requestLength + - bytesRemaining, bytesToRead); + if (bytesRead == -1) { + break; + } + + bytesRemaining -= bytesRead; + } + + String result = new String(buffer, "utf-8"); + + return result; + } + + public class ErrorHandlerErrorEvent implements ErrorEvent, Serializable { + private final Throwable throwable; + + public ErrorHandlerErrorEvent(Throwable throwable) { + this.throwable = throwable; + } + + public Throwable getThrowable() { + return throwable; + } + + } + + private void handleChangeVariablesError(Application application, + Component owner, Exception e, Map m) { + boolean handled = false; + ChangeVariablesErrorEvent errorEvent = new ChangeVariablesErrorEvent( + owner, e, m); + + if (owner instanceof AbstractField) { + try { + handled = ((AbstractField) owner).handleError(errorEvent); + } catch (Exception handlerException) { + /* + * If there is an error in the component error handler we pass + * the that error to the application error handler and continue + * processing the actual error + */ + application.getErrorHandler().terminalError( + new ErrorHandlerErrorEvent(handlerException)); + handled = false; + } + } + + if (!handled) { + application.getErrorHandler().terminalError(errorEvent); + } + + } + + private Object convertVariableValue(char variableType, String strValue) { + Object val = null; + switch (variableType) { + case 'a': + val = strValue.split(VAR_ARRAYITEM_SEPARATOR); + break; + case 's': + val = strValue; + break; + case 'i': + val = Integer.valueOf(strValue); + break; + case 'l': + val = Long.valueOf(strValue); + break; + case 'f': + val = Float.valueOf(strValue); + break; + case 'd': + val = Double.valueOf(strValue); + break; + case 'b': + val = Boolean.valueOf(strValue); + break; + case 'p': + val = idPaintableMap.get(strValue); + break; + } + + return val; + } + + private void printLocaleDeclarations(PrintWriter outWriter) { + /* + * ----------------------------- Sending Locale sensitive date + * ----------------------------- + */ + + // Send locale informations to client + outWriter.print(", \"locales\":["); + for (; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) { + + final Locale l = generateLocale(locales.get(pendingLocalesIndex)); + // Locale name + outWriter.print("{\"name\":\"" + l.toString() + "\","); + + /* + * Month names (both short and full) + */ + final DateFormatSymbols dfs = new DateFormatSymbols(l); + final String[] short_months = dfs.getShortMonths(); + final String[] months = dfs.getMonths(); + outWriter.print("\"smn\":[\"" + + // ShortMonthNames + short_months[0] + "\",\"" + short_months[1] + "\",\"" + + short_months[2] + "\",\"" + short_months[3] + "\",\"" + + short_months[4] + "\",\"" + short_months[5] + "\",\"" + + short_months[6] + "\",\"" + short_months[7] + "\",\"" + + short_months[8] + "\",\"" + short_months[9] + "\",\"" + + short_months[10] + "\",\"" + short_months[11] + "\"" + + "],"); + outWriter.print("\"mn\":[\"" + + // MonthNames + months[0] + "\",\"" + months[1] + "\",\"" + months[2] + + "\",\"" + months[3] + "\",\"" + months[4] + "\",\"" + + months[5] + "\",\"" + months[6] + "\",\"" + months[7] + + "\",\"" + months[8] + "\",\"" + months[9] + "\",\"" + + months[10] + "\",\"" + months[11] + "\"" + "],"); + + /* + * Weekday names (both short and full) + */ + final String[] short_days = dfs.getShortWeekdays(); + final String[] days = dfs.getWeekdays(); + outWriter.print("\"sdn\":[\"" + + // ShortDayNames + short_days[1] + "\",\"" + short_days[2] + "\",\"" + + short_days[3] + "\",\"" + short_days[4] + "\",\"" + + short_days[5] + "\",\"" + short_days[6] + "\",\"" + + short_days[7] + "\"" + "],"); + outWriter.print("\"dn\":[\"" + + // DayNames + days[1] + "\",\"" + days[2] + "\",\"" + days[3] + "\",\"" + + days[4] + "\",\"" + days[5] + "\",\"" + days[6] + "\",\"" + + days[7] + "\"" + "],"); + + /* + * First day of week (0 = sunday, 1 = monday) + */ + final Calendar cal = new GregorianCalendar(l); + outWriter.print("\"fdow\":" + (cal.getFirstDayOfWeek() - 1) + ","); + + /* + * Date formatting (MM/DD/YYYY etc.) + */ + + DateFormat dateFormat = DateFormat.getDateTimeInstance( + DateFormat.SHORT, DateFormat.SHORT, l); + if (!(dateFormat instanceof SimpleDateFormat)) { + System.err + .println("Unable to get default date pattern for locale " + + l.toString()); + dateFormat = new SimpleDateFormat(); + } + final String df = ((SimpleDateFormat) dateFormat).toPattern(); + + int timeStart = df.indexOf("H"); + if (timeStart < 0) { + timeStart = df.indexOf("h"); + } + final int ampm_first = df.indexOf("a"); + // E.g. in Korean locale AM/PM is before h:mm + // TODO should take that into consideration on client-side as well, + // now always h:mm a + if (ampm_first > 0 && ampm_first < timeStart) { + timeStart = ampm_first; + } + final String dateformat = df.substring(0, timeStart - 1); + + outWriter.print("\"df\":\"" + dateformat.trim() + "\","); + + /* + * Time formatting (24 or 12 hour clock and AM/PM suffixes) + */ + final String timeformat = df.substring(timeStart, df.length()); + /* + * Doesn't return second or milliseconds. + * + * We use timeformat to determine 12/24-hour clock + */ + final boolean twelve_hour_clock = timeformat.indexOf("a") > -1; + // TODO there are other possibilities as well, like 'h' in french + // (ignore them, too complicated) + final String hour_min_delimiter = timeformat.indexOf(".") > -1 ? "." + : ":"; + // outWriter.print("\"tf\":\"" + timeformat + "\","); + outWriter.print("\"thc\":" + twelve_hour_clock + ","); + outWriter.print("\"hmd\":\"" + hour_min_delimiter + "\""); + if (twelve_hour_clock) { + final String[] ampm = dfs.getAmPmStrings(); + outWriter.print(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1] + + "\"]"); + } + outWriter.print("}"); + if (pendingLocalesIndex < locales.size() - 1) { + outWriter.print(","); + } + } + outWriter.print("]"); // Close locales + } + + /** + * Gets the existing application or create a new one. Get a window within an + * application based on the requested URI. + * + * @param request + * the HTTP Request. + * @param application + * the Application to query for window. + * @param assumedWindow + * if the window has been already resolved once, this parameter + * must contain the window. + * @return Window mathing the given URI or null if not found. + * @throws ServletException + * if an exception has occurred that interferes with the + * servlet's normal operation. + */ + private Window getApplicationWindow(HttpServletRequest request, + Application application, Window assumedWindow) + throws ServletException { + + Window window = null; + + // If the client knows which window to use, use it if possible + String windowClientRequestedName = request.getParameter("windowName"); + if (assumedWindow != null + && application.getWindows().contains(assumedWindow)) { + windowClientRequestedName = assumedWindow.getName(); + } + if (windowClientRequestedName != null) { + window = application.getWindow(windowClientRequestedName); + if (window != null) { + return window; + } + } + + // If client does not know what window it wants + if (window == null) { + + // Get the path from URL + String path = applicationServlet.getRequestPathInfo(request); + path = path.substring("/UIDL".length()); + + // If the path is specified, create name from it + if (path != null && path.length() > 0 && !path.equals("/")) { + String windowUrlName = null; + if (path.charAt(0) == '/') { + path = path.substring(1); + } + final int index = path.indexOf('/'); + if (index < 0) { + windowUrlName = path; + path = ""; + } else { + windowUrlName = path.substring(0, index); + path = path.substring(index + 1); + } + + window = application.getWindow(windowUrlName); + } + } + + // By default, use mainwindow + if (window == null) { + window = application.getMainWindow(); + } + + // If the requested window is already open, resolve conflict + if (currentlyOpenWindowsInClient.contains(window.getName())) { + String newWindowName = window.getName(); + while (currentlyOpenWindowsInClient.contains(newWindowName)) { + newWindowName = window.getName() + "_" + + ((int) (Math.random() * 100000000)); + } + + window = application.getWindow(newWindowName); + + // If everything else fails, use main window even in case of + // conflicts + if (window == null) { + window = application.getMainWindow(); + } + } + + return window; + } + + /** + * Ends the Application. + * + * @param request + * the HTTP request instance. + * @param response + * the HTTP response to write to. + * @param application + * the Application to end. + * @throws IOException + * if the writing failed due to input/output error. + */ + private void endApplication(HttpServletRequest request, + HttpServletResponse response, Application application) + throws IOException { + + String logoutUrl = application.getLogoutURL(); + if (logoutUrl == null) { + logoutUrl = application.getURL().toString(); + } + // clients JS app is still running, send a special json file to tell + // client that application has quit and where to point browser now + // Set the response type + response.setContentType("application/json; charset=UTF-8"); + final ServletOutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("for(;;);[{"); + outWriter.print("\"redirect\":{"); + outWriter.write("\"url\":\"" + logoutUrl + "\"}}]"); + outWriter.flush(); + outWriter.close(); + out.flush(); + } + + /** + * Gets the Paintable Id. If Paintable has debug id set it will be used + * prefixed with "PID_S". Otherwise a sequenced ID is created. + * + * @param paintable + * @return the paintable Id. + */ + public String getPaintableId(Paintable paintable) { + + String id = paintableIdMap.get(paintable); + if (id == null) { + // use testing identifier as id if set + id = paintable.getDebugId(); + if (id == null) { + id = "PID" + Integer.toString(idSequence++); + } else { + id = "PID_S" + id; + } + Paintable old = idPaintableMap.put(id, paintable); + if (old != null && old != paintable) { + /* + * Two paintables have the same id. We still make sure the old + * one is a component which is still attached to the + * application. This is just a precaution and should not be + * absolutely necessary. + */ + + if (old instanceof Component + && ((Component) old).getApplication() != null) { + throw new IllegalStateException("Two paintables (" + + paintable.getClass().getSimpleName() + "," + + old.getClass().getSimpleName() + + ") have been assigned the same id: " + + paintable.getDebugId()); + } + } + paintableIdMap.put(paintable, id); + } + + return id; + } + + public boolean hasPaintableId(Paintable paintable) { + return paintableIdMap.containsKey(paintable); + } + + /** + * Returns dirty components which are in given window. Components in an + * invisible subtrees are omitted. + * + * @param w + * root window for which dirty components is to be fetched + * @return + */ + private ArrayList<Paintable> getDirtyVisibleComponents(Window w) { + final ArrayList<Paintable> resultset = new ArrayList<Paintable>( + dirtyPaintabletSet); + + // The following algorithm removes any components that would be painted + // as a direct descendant of other components from the dirty components + // list. The result is that each component should be painted exactly + // once and any unmodified components will be painted as "cached=true". + + for (final Iterator i = dirtyPaintabletSet.iterator(); i.hasNext();) { + final Paintable p = (Paintable) i.next(); + if (p instanceof Component) { + final Component component = (Component) p; + if (component.getApplication() == null) { + // component is detached after requestRepaint is called + resultset.remove(p); + i.remove(); + } else { + Window componentsRoot = component.getWindow(); + if (componentsRoot.getParent() != null) { + // this is a subwindow + componentsRoot = (Window) componentsRoot.getParent(); + } + if (componentsRoot != w) { + resultset.remove(p); + } else if (component.getParent() != null + && !component.getParent().isVisible()) { + /* + * Do not return components in an invisible subtree. + * + * Components that are invisible in visible subree, must + * be rendered (to let client know that they need to be + * hidden). + */ + resultset.remove(p); + } + } + } + } + + return resultset; + } + + /** + * @see com.vaadin.terminal.Paintable.RepaintRequestListener#repaintRequested(com.vaadin.terminal.Paintable.RepaintRequestEvent) + */ + public void repaintRequested(RepaintRequestEvent event) { + final Paintable p = event.getPaintable(); + if (!dirtyPaintabletSet.contains(p)) { + dirtyPaintabletSet.add(p); + } + } + + /** + * + * @param p + */ + private void paintablePainted(Paintable p) { + dirtyPaintabletSet.remove(p); + p.requestRepaintRequests(); + } + + private final class SingleValueMap implements Map<Object, Object>, + Serializable { + + private final String name; + + private final Object value; + + private SingleValueMap(String name, Object value) { + this.name = name; + this.value = value; + } + + public void clear() { + throw new UnsupportedOperationException(); + } + + public boolean containsKey(Object key) { + if (name == null) { + return key == null; + } + return name.equals(key); + } + + public boolean containsValue(Object v) { + if (value == null) { + return v == null; + } + return value.equals(v); + } + + public Set entrySet() { + final Set s = new HashSet(); + s.add(new Map.Entry() { + + public Object getKey() { + return name; + } + + public Object getValue() { + return value; + } + + public Object setValue(Object value) { + throw new UnsupportedOperationException(); + } + }); + return s; + } + + public Object get(Object key) { + if (!name.equals(key)) { + return null; + } + return value; + } + + public boolean isEmpty() { + return false; + } + + public Set keySet() { + final Set s = new HashSet(); + s.add(name); + return s; + } + + public Object put(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + public void putAll(Map t) { + throw new UnsupportedOperationException(); + } + + public Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + public int size() { + return 1; + } + + public Collection values() { + final LinkedList s = new LinkedList(); + s.add(value); + return s; + + } + } + + /** + * Implementation of URIHandler.ErrorEvent interface. + */ + public class URIHandlerErrorImpl implements URIHandler.ErrorEvent, + Serializable { + + private final URIHandler owner; + + private final Throwable throwable; + + /** + * + * @param owner + * @param throwable + */ + private URIHandlerErrorImpl(URIHandler owner, Throwable throwable) { + this.owner = owner; + this.throwable = throwable; + } + + /** + * @see com.vaadin.terminal.Terminal.ErrorEvent#getThrowable() + */ + public Throwable getThrowable() { + return throwable; + } + + /** + * @see com.vaadin.terminal.URIHandler.ErrorEvent#getURIHandler() + */ + public URIHandler getURIHandler() { + return owner; + } + } + + public void requireLocale(String value) { + if (locales == null) { + locales = new ArrayList<String>(); + locales.add(application.getLocale().toString()); + pendingLocalesIndex = 0; + } + if (!locales.contains(value)) { + locales.add(value); + } + } + + private Locale generateLocale(String value) { + final String[] temp = value.split("_"); + if (temp.length == 1) { + return new Locale(temp[0]); + } else if (temp.length == 2) { + return new Locale(temp[0], temp[1]); + } else { + return new Locale(temp[0], temp[1], temp[2]); + } + } + + /* + * Upload progress listener notifies upload component once when Jakarta + * FileUpload can determine content length. Used to detect files total size, + * uploads progress can be tracked inside upload. + */ + private class UploadProgressListener implements ProgressListener, + Serializable { + + Upload uploadComponent; + + boolean updated = false; + + public void setUpload(Upload u) { + uploadComponent = u; + } + + public void update(long bytesRead, long contentLength, int items) { + if (!updated && uploadComponent != null) { + uploadComponent.setUploadSize(contentLength); + updated = true; + } + } + } + + /** + * Helper method to test if a component contains another + * + * @param parent + * @param child + */ + private static boolean isChildOf(Component parent, Component child) { + Component p = child.getParent(); + while (p != null) { + if (parent == p) { + return true; + } + p = p.getParent(); + } + return false; + } + + private class InvalidUIDLSecurityKeyException extends + GeneralSecurityException { + + InvalidUIDLSecurityKeyException(String message) { + super(message); + } + + } + + /** + * Handles the requested URI. An application can add handlers to do special + * processing, when a certain URI is requested. The handlers are invoked + * before any windows URIs are processed and if a DownloadStream is returned + * it is sent to the client. + * + * @param application + * the Application owning the URI. + * @param request + * the HTTP request instance. + * @param response + * the HTTP response to write to. + * @return boolean <code>true</code> if the request was handled and further + * processing should be suppressed, <code>false</code> otherwise. + * @see com.vaadin.terminal.URIHandler + */ + DownloadStream handleURI(Window window, HttpServletRequest request, + HttpServletResponse response) { + + String uri = applicationServlet.getRequestPathInfo(request); + + // If no URI is available + if (uri == null) { + uri = ""; + } else { + // Removes the leading / + while (uri.startsWith("/") && uri.length() > 0) { + uri = uri.substring(1); + } + } + + // Handles the uri + try { + URL context = application.getURL(); + if (window == application.getMainWindow()) { + DownloadStream stream = null; + /* + * Application.handleURI run first. Handles possible + * ApplicationResources. + */ + stream = application.handleURI(context, uri); + if (stream == null) { + stream = window.handleURI(context, uri); + } + return stream; + } else { + // Resolve the prefix end inded + final int index = uri.indexOf('/'); + if (index > 0) { + String prefix = uri.substring(0, index); + URL windowContext; + windowContext = new URL(context, prefix + "/"); + final String windowUri = (uri.length() > prefix.length() + 1) ? uri + .substring(prefix.length() + 1) + : ""; + return window.handleURI(windowContext, windowUri); + } else { + return null; + } + } + + } catch (final Throwable t) { + application.getErrorHandler().terminalError( + new URIHandlerErrorImpl(application, t)); + return null; + } + } +} diff --git a/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java b/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java new file mode 100644 index 0000000000..045b9c180d --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java @@ -0,0 +1,678 @@ +package com.vaadin.terminal.gwt.server; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.Vector; + +import com.vaadin.terminal.Sizeable; +import com.vaadin.ui.AbstractOrderedLayout; +import com.vaadin.ui.Component; +import com.vaadin.ui.ComponentContainer; +import com.vaadin.ui.CustomComponent; +import com.vaadin.ui.Form; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.Layout; +import com.vaadin.ui.OrderedLayout; +import com.vaadin.ui.Panel; +import com.vaadin.ui.SplitPanel; +import com.vaadin.ui.TabSheet; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; +import com.vaadin.ui.GridLayout.Area; + +@SuppressWarnings("serial") +public class ComponentSizeValidator implements Serializable { + + private final static int LAYERS_SHOWN = 4; + + /** + * Recursively checks given component and its subtree for invalid layout + * setups. Prints errors to std err stream. + * + * @param component + * component to check + * @return set of first level errors found + */ + public static List<InvalidLayout> validateComponentRelativeSizes( + Component component, List<InvalidLayout> errors, + InvalidLayout parent) { + + boolean invalidHeight = !checkHeights(component); + boolean invalidWidth = !checkWidths(component); + + if (invalidHeight || invalidWidth) { + InvalidLayout error = new InvalidLayout(component, invalidHeight, + invalidWidth); + if (parent != null) { + parent.addError(error); + } else { + if (errors == null) { + errors = new LinkedList<InvalidLayout>(); + } + errors.add(error); + } + parent = error; + } + + if (component instanceof Panel) { + Panel panel = (Panel) component; + errors = validateComponentRelativeSizes(panel.getLayout(), errors, + parent); + } else if (component instanceof ComponentContainer) { + ComponentContainer lo = (ComponentContainer) component; + Iterator it = lo.getComponentIterator(); + while (it.hasNext()) { + errors = validateComponentRelativeSizes((Component) it.next(), + errors, parent); + } + } else if (component instanceof Form) { + Form form = (Form) component; + if (form.getLayout() != null) { + errors = validateComponentRelativeSizes(form.getLayout(), + errors, parent); + } + if (form.getFooter() != null) { + errors = validateComponentRelativeSizes(form.getFooter(), + errors, parent); + } + } + + return errors; + } + + private static void printServerError(String msg, + Stack<ComponentInfo> attributes, boolean widthError, + PrintStream errorStream) { + StringBuffer err = new StringBuffer(); + err.append("IT Mill Toolkit DEBUG\n"); + + StringBuilder indent = new StringBuilder(""); + ComponentInfo ci; + if (attributes != null) { + while (attributes.size() > LAYERS_SHOWN) { + attributes.pop(); + } + while (!attributes.empty()) { + ci = attributes.pop(); + showComponent(ci.component, ci.info, err, indent, widthError); + } + } + + err.append("Layout problem detected: "); + err.append(msg); + err.append("\n"); + err + .append("Relative sizes were replaced by undefined sizes, components may not render as expected.\n"); + errorStream.println(err); + + } + + public static boolean checkHeights(Component component) { + try { + if (!hasRelativeHeight(component)) { + return true; + } + if (component instanceof Window) { + return true; + } + if (component.getParent() == null) { + return true; + } + + return parentCanDefineHeight(component); + } catch (Exception e) { + e.printStackTrace(); + return true; + } + } + + public static boolean checkWidths(Component component) { + try { + if (!hasRelativeWidth(component)) { + return true; + } + if (component instanceof Window) { + return true; + } + if (component.getParent() == null) { + return true; + } + + return parentCanDefineWidth(component); + } catch (Exception e) { + e.printStackTrace(); + return true; + } + } + + public static class InvalidLayout implements Serializable { + + private Component component; + + private boolean invalidHeight; + private boolean invalidWidth; + + private Vector<InvalidLayout> subErrors = new Vector<InvalidLayout>(); + + public InvalidLayout(Component component, boolean height, boolean width) { + this.component = component; + invalidHeight = height; + invalidWidth = width; + } + + public void addError(InvalidLayout error) { + subErrors.add(error); + } + + @SuppressWarnings("deprecation") + public void reportErrors(PrintWriter clientJSON, + CommunicationManager communicationManager, + PrintStream serverErrorStream) { + clientJSON.write("{"); + + Component parent = component.getParent(); + String paintableId = communicationManager.getPaintableId(component); + + clientJSON.print("id:\"" + paintableId + "\""); + + if (invalidHeight) { + Stack<ComponentInfo> attributes = null; + String msg = ""; + // set proper error messages + if (parent instanceof AbstractOrderedLayout) { + AbstractOrderedLayout ol = (AbstractOrderedLayout) parent; + boolean vertical = false; + + if (ol instanceof OrderedLayout) { + if (((OrderedLayout) ol).getOrientation() == OrderedLayout.ORIENTATION_VERTICAL) { + vertical = true; + } + } else if (ol instanceof VerticalLayout) { + vertical = true; + } + + if (vertical) { + msg = "Component with relative height inside a VerticalLayout with no height defined."; + attributes = getHeightAttributes(component); + } else { + msg = "At least one of a HorizontalLayout's components must have non relative height if the height of the layout is not defined"; + attributes = getHeightAttributes(component); + } + } else if (parent instanceof GridLayout) { + msg = "At least one of the GridLayout's components in each row should have non relative height if the height of the layout is not defined."; + attributes = getHeightAttributes(component); + } else { + // default error for non sized parent issue + msg = "A component with relative height needs a parent with defined height."; + attributes = getHeightAttributes(component); + } + printServerError(msg, attributes, false, serverErrorStream); + clientJSON.print(",\"heightMsg\":\"" + msg + "\""); + } + if (invalidWidth) { + Stack<ComponentInfo> attributes = null; + String msg = ""; + if (parent instanceof AbstractOrderedLayout) { + AbstractOrderedLayout ol = (AbstractOrderedLayout) parent; + boolean horizontal = true; + + if (ol instanceof OrderedLayout) { + if (((OrderedLayout) ol).getOrientation() == OrderedLayout.ORIENTATION_VERTICAL) { + horizontal = false; + } + } else if (ol instanceof VerticalLayout) { + horizontal = false; + } + + if (horizontal) { + msg = "Component with relative width inside a HorizontalLayout with no width defined"; + attributes = getWidthAttributes(component); + } else { + msg = "At least one of a VerticalLayout's components must have non relative width if the width of the layout is not defined"; + attributes = getWidthAttributes(component); + } + } else if (parent instanceof GridLayout) { + msg = "At least one of the GridLayout's components in each column should have non relative width if the width of the layout is not defined."; + attributes = getWidthAttributes(component); + } else { + // default error for non sized parent issue + msg = "A component with relative width needs a parent with defined width."; + attributes = getWidthAttributes(component); + } + clientJSON.print(",\"widthMsg\":\"" + msg + "\""); + printServerError(msg, attributes, true, serverErrorStream); + } + if (subErrors.size() > 0) { + serverErrorStream.println("Sub errors >>"); + clientJSON.write(", \"subErrors\" : ["); + boolean first = true; + for (InvalidLayout subError : subErrors) { + if (!first) { + clientJSON.print(","); + } else { + first = false; + } + subError.reportErrors(clientJSON, communicationManager, + serverErrorStream); + } + clientJSON.write("]"); + serverErrorStream.println("<< Sub erros"); + } + clientJSON.write("}"); + } + } + + private static class ComponentInfo implements Serializable { + Component component; + String info; + + public ComponentInfo(Component component, String info) { + this.component = component; + this.info = info; + } + + } + + private static Stack<ComponentInfo> getHeightAttributes(Component component) { + Stack<ComponentInfo> attributes = new Stack<ComponentInfo>(); + attributes + .add(new ComponentInfo(component, getHeightString(component))); + Component parent = component.getParent(); + attributes.add(new ComponentInfo(parent, getHeightString(parent))); + + while ((parent = parent.getParent()) != null) { + attributes.add(new ComponentInfo(parent, getHeightString(parent))); + } + + return attributes; + } + + private static Stack<ComponentInfo> getWidthAttributes(Component component) { + Stack<ComponentInfo> attributes = new Stack<ComponentInfo>(); + attributes.add(new ComponentInfo(component, getWidthString(component))); + Component parent = component.getParent(); + attributes.add(new ComponentInfo(parent, getWidthString(parent))); + + while ((parent = parent.getParent()) != null) { + attributes.add(new ComponentInfo(parent, getWidthString(parent))); + } + + return attributes; + } + + private static String getWidthString(Component component) { + String width = "width: "; + if (hasRelativeWidth(component)) { + width += "RELATIVE, " + component.getWidth() + " %"; + } else if (component instanceof Window && component.getParent() == null) { + width += "MAIN WINDOW"; + } else if (component.getWidth() >= 0) { + width += "ABSOLUTE, " + component.getWidth() + " " + + Sizeable.UNIT_SYMBOLS[component.getWidthUnits()]; + } else { + width += "UNDEFINED"; + } + + return width; + } + + private static String getHeightString(Component component) { + String height = "height: "; + if (hasRelativeHeight(component)) { + height += "RELATIVE, " + component.getHeight() + " %"; + } else if (component instanceof Window && component.getParent() == null) { + height += "MAIN WINDOW"; + } else if (component.getHeight() > 0) { + height += "ABSOLUTE, " + component.getHeight() + " " + + Sizeable.UNIT_SYMBOLS[component.getHeightUnits()]; + } else { + height += "UNDEFINED"; + } + + return height; + } + + private static void showComponent(Component component, String attribute, + StringBuffer err, StringBuilder indent, boolean widthError) { + + FileLocation createLoc = creationLocations.get(component); + + FileLocation sizeLoc; + if (widthError) { + sizeLoc = widthLocations.get(component); + } else { + sizeLoc = heightLocations.get(component); + } + + err.append(indent); + indent.append(" "); + err.append("- "); + + err.append(component.getClass().getSimpleName()); + err.append("/").append(Integer.toHexString(component.hashCode())); + + if (component.getCaption() != null) { + err.append(" \""); + err.append(component.getCaption()); + err.append("\""); + } + + if (component.getDebugId() != null) { + err.append(" debugId: "); + err.append(component.getDebugId()); + } + + if (createLoc != null) { + err.append(", created at (" + createLoc.file + ":" + + createLoc.lineNumber + ")"); + + } + + if (attribute != null) { + err.append(" ("); + err.append(attribute); + if (sizeLoc != null) { + err.append(", set at (" + sizeLoc.file + ":" + + sizeLoc.lineNumber + ")"); + } + + err.append(")"); + } + err.append("\n"); + + } + + private static boolean hasNonRelativeHeightComponent( + AbstractOrderedLayout ol) { + Iterator it = ol.getComponentIterator(); + while (it.hasNext()) { + if (!hasRelativeHeight((Component) it.next())) { + return true; + } + } + return false; + } + + @SuppressWarnings("deprecation") + public static boolean parentCanDefineHeight(Component component) { + Component parent = component.getParent(); + if (parent == null) { + // main window, valid situation + return true; + } + if (parent.getHeight() < 0) { + // Undefined height + if (parent instanceof Window) { + Window w = (Window) parent; + if (w.getParent() == null) { + // main window is considered to have size + return true; + } + } + + if (parent instanceof AbstractOrderedLayout) { + boolean horizontal = true; + if (parent instanceof OrderedLayout) { + horizontal = ((OrderedLayout) parent).getOrientation() == OrderedLayout.ORIENTATION_HORIZONTAL; + } else if (parent instanceof VerticalLayout) { + horizontal = false; + } + if (horizontal + && hasNonRelativeHeightComponent((AbstractOrderedLayout) parent)) { + return true; + } else { + return false; + } + + } else if (parent instanceof GridLayout) { + GridLayout gl = (GridLayout) parent; + Area componentArea = gl.getComponentArea(component); + boolean rowHasHeight = false; + for (int row = componentArea.getRow1(); !rowHasHeight + && row <= componentArea.getRow2(); row++) { + for (int column = 0; !rowHasHeight + && column < gl.getColumns(); column++) { + Component c = gl.getComponent(column, row); + if (c != null) { + rowHasHeight = !hasRelativeHeight(c); + } + } + } + if (!rowHasHeight) { + return false; + } else { + // Other components define row height + return true; + } + } + + if (parent instanceof Panel || parent instanceof SplitPanel + || parent instanceof TabSheet + || parent instanceof CustomComponent) { + // height undefined, we know how how component works and no + // exceptions + // TODO horiz SplitPanel ?? + return false; + } else { + // We cannot generally know if undefined component can serve + // space for children (like CustomLayout or component built by + // third party) so we assume they can + return true; + } + + } else if (hasRelativeHeight(parent)) { + // Relative height + if (parent.getParent() != null) { + return parentCanDefineHeight(parent); + } else { + return true; + } + } else { + // Absolute height + return true; + } + } + + private static boolean hasRelativeHeight(Component component) { + return (component.getHeightUnits() == Sizeable.UNITS_PERCENTAGE && component + .getHeight() > 0); + } + + private static boolean hasNonRelativeWidthComponent(AbstractOrderedLayout ol) { + Iterator it = ol.getComponentIterator(); + while (it.hasNext()) { + if (!hasRelativeWidth((Component) it.next())) { + return true; + } + } + return false; + } + + private static boolean hasRelativeWidth(Component paintable) { + return paintable.getWidth() > 0 + && paintable.getWidthUnits() == Sizeable.UNITS_PERCENTAGE; + } + + @SuppressWarnings("deprecation") + public static boolean parentCanDefineWidth(Component component) { + Component parent = component.getParent(); + if (parent == null) { + // main window, valid situation + return true; + } + if (parent instanceof Window) { + Window w = (Window) parent; + if (w.getParent() == null) { + // main window is considered to have size + return true; + } + + } + + if (parent.getWidth() < 0) { + // Undefined width + + if (parent instanceof AbstractOrderedLayout) { + AbstractOrderedLayout ol = (AbstractOrderedLayout) parent; + boolean horizontal = true; + if (ol instanceof OrderedLayout) { + if (((OrderedLayout) ol).getOrientation() == OrderedLayout.ORIENTATION_VERTICAL) { + horizontal = false; + } + } else if (ol instanceof VerticalLayout) { + horizontal = false; + } + + if (!horizontal && hasNonRelativeWidthComponent(ol)) { + // valid situation, other components defined width + return true; + } else { + return false; + } + } else if (parent instanceof GridLayout) { + GridLayout gl = (GridLayout) parent; + Area componentArea = gl.getComponentArea(component); + boolean columnHasWidth = false; + for (int col = componentArea.getColumn1(); !columnHasWidth + && col <= componentArea.getColumn2(); col++) { + for (int row = 0; !columnHasWidth && row < gl.getRows(); row++) { + Component c = gl.getComponent(col, row); + if (c != null) { + columnHasWidth = !hasRelativeWidth(c); + } + } + } + if (!columnHasWidth) { + return false; + } else { + // Other components define column width + return true; + } + } else if (parent instanceof Form) { + /* + * If some other part of the form is not relative it determines + * the component width + */ + return hasNonRelativeWidthComponent((Form) parent); + } else if (parent instanceof SplitPanel + || parent instanceof TabSheet + || parent instanceof CustomComponent) { + // FIXME Could we use com.vaadin package name here and + // fail for all component containers? + // FIXME Actually this should be moved to containers so it can + // be implemented for custom containers + // TODO vertical splitpanel with another non relative component? + return false; + } else if (parent instanceof Window) { + // Sub window can define width based on caption + if (parent.getCaption() != null + && !parent.getCaption().equals("")) { + return true; + } else { + return false; + } + } else if (parent instanceof Panel) { + // TODO Panel should be able to define width based on caption + return false; + } else { + return true; + } + } else if (hasRelativeWidth(parent)) { + // Relative width + if (parent.getParent() == null) { + return true; + } + + return parentCanDefineWidth(parent); + } else { + return true; + } + + } + + private static boolean hasNonRelativeWidthComponent(Form form) { + Layout layout = form.getLayout(); + Layout footer = form.getFooter(); + + if (layout != null && !hasRelativeWidth(layout)) { + return true; + } + if (footer != null && !hasRelativeWidth(footer)) { + return true; + } + + return false; + } + + private static Map<Object, FileLocation> creationLocations = new HashMap<Object, FileLocation>(); + private static Map<Object, FileLocation> widthLocations = new HashMap<Object, FileLocation>(); + private static Map<Object, FileLocation> heightLocations = new HashMap<Object, FileLocation>(); + + public static class FileLocation implements Serializable { + public String method; + public String file; + public String className; + public String classNameSimple; + public int lineNumber; + + public FileLocation(StackTraceElement traceElement) { + file = traceElement.getFileName(); + className = traceElement.getClassName(); + classNameSimple = className + .substring(className.lastIndexOf('.') + 1); + lineNumber = traceElement.getLineNumber(); + method = traceElement.getMethodName(); + } + } + + public static void setCreationLocation(Object object) { + setLocation(creationLocations, object); + } + + public static void setWidthLocation(Object object) { + setLocation(widthLocations, object); + } + + public static void setHeightLocation(Object object) { + setLocation(heightLocations, object); + } + + private static void setLocation(Map<Object, FileLocation> map, Object object) { + StackTraceElement[] traceLines = Thread.currentThread().getStackTrace(); + for (StackTraceElement traceElement : traceLines) { + Class cls; + try { + String className = traceElement.getClassName(); + if (className.startsWith("java.") + || className.startsWith("sun.")) { + continue; + } + + cls = Class.forName(className); + if (cls == ComponentSizeValidator.class || cls == Thread.class) { + continue; + } + + if (Component.class.isAssignableFrom(cls) + && !CustomComponent.class.isAssignableFrom(cls)) { + continue; + } + FileLocation cl = new FileLocation(traceElement); + map.put(object, cl); + return; + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + } + +} diff --git a/src/com/vaadin/terminal/gwt/server/HttpUploadStream.java b/src/com/vaadin/terminal/gwt/server/HttpUploadStream.java new file mode 100644 index 0000000000..c9e3c77cad --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/HttpUploadStream.java @@ -0,0 +1,91 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.InputStream; + +/** + * AjaxAdapter implementation of the UploadStream interface. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public class HttpUploadStream implements + com.vaadin.terminal.UploadStream { + + /** + * Holds value of property variableName. + */ + private final String streamName; + + private final String contentName; + + private final String contentType; + + /** + * Holds value of property variableValue. + */ + private final InputStream stream; + + /** + * Creates a new instance of UploadStreamImpl. + * + * @param name + * the name of the stream. + * @param stream + * the input stream. + * @param contentName + * the name of the content. + * @param contentType + * the type of the content. + */ + public HttpUploadStream(String name, InputStream stream, + String contentName, String contentType) { + streamName = name; + this.stream = stream; + this.contentName = contentName; + this.contentType = contentType; + } + + /** + * Gets the name of the stream. + * + * @return the name of the stream. + */ + public String getStreamName() { + return streamName; + } + + /** + * Gets the input stream. + * + * @return the Input stream. + */ + public InputStream getStream() { + return stream; + } + + /** + * Gets the input stream content type. + * + * @return the content type of the input stream. + */ + public String getContentType() { + return contentType; + } + + /** + * Gets the stream content name. Stream content name usually differs from + * the actual stream name. It is used to identify the content of the stream. + * + * @return the Name of the stream content. + */ + public String getContentName() { + return contentName; + } +} diff --git a/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java b/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java new file mode 100644 index 0000000000..b81e65e429 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java @@ -0,0 +1,1108 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.PrintWriter; +import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Stack; +import java.util.Vector; + +import com.vaadin.Application; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.ExternalResource; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Paintable; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.ThemeResource; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.ui.Component; + +/** + * User Interface Description Language Target. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public class JsonPaintTarget implements PaintTarget { + + /* Document type declarations */ + + private final static String UIDL_ARG_NAME = "name"; + + private final Stack<String> mOpenTags; + + private final Stack<JsonTag> openJsonTags; + + private final PrintWriter uidlBuffer; + + private boolean closed = false; + + private final CommunicationManager manager; + + private int changes = 0; + + Set preCachedResources = new HashSet(); + + private boolean customLayoutArgumentsOpen = false; + + private JsonTag tag; + + private int errorsOpen; + + private boolean cacheEnabled = false; + + private Collection<Paintable> paintedComponents = new HashSet<Paintable>(); + + private Collection<Paintable> identifiersCreatedDueRefPaint; + + /** + * Creates a new XMLPrintWriter, without automatic line flushing. + * + * @param variableMap + * @param manager + * @param outWriter + * A character-output stream. + * @throws PaintException + * if the paint operation failed. + */ + public JsonPaintTarget(CommunicationManager manager, PrintWriter outWriter, + boolean cachingRequired) throws PaintException { + + this.manager = manager; + + // Sets the target for UIDL writing + uidlBuffer = outWriter; + + // Initialize tag-writing + mOpenTags = new Stack<String>(); + openJsonTags = new Stack<JsonTag>(); + cacheEnabled = cachingRequired; + } + + public void startTag(String tagName) throws PaintException { + startTag(tagName, false); + } + + /** + * Prints the element start tag. + * + * <pre> + * Todo: + * Checking of input values + * + * </pre> + * + * @param tagName + * the name of the start tag. + * @throws PaintException + * if the paint operation failed. + * + */ + public void startTag(String tagName, boolean isChildNode) + throws PaintException { + // In case of null data output nothing: + if (tagName == null) { + throw new NullPointerException(); + } + + // Ensures that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + if (tag != null) { + openJsonTags.push(tag); + } + // Checks tagName and attributes here + mOpenTags.push(tagName); + + tag = new JsonTag(tagName); + + customLayoutArgumentsOpen = "customlayout".equals(tagName); + + if ("error".equals(tagName)) { + errorsOpen++; + } + } + + /** + * Prints the element end tag. + * + * If the parent tag is closed before every child tag is closed an + * PaintException is raised. + * + * @param tag + * the name of the end tag. + * @throws Paintexception + * if the paint operation failed. + */ + public void endTag(String tagName) throws PaintException { + // In case of null data output nothing: + if (tagName == null) { + throw new NullPointerException(); + } + + // Ensure that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + if (openJsonTags.size() > 0) { + final JsonTag parent = openJsonTags.pop(); + + String lastTag = ""; + + lastTag = mOpenTags.pop(); + if (!tagName.equalsIgnoreCase(lastTag)) { + throw new PaintException("Invalid UIDL: wrong ending tag: '" + + tagName + "' expected: '" + lastTag + "'."); + } + + // simple hack which writes error uidl structure into attribute + if ("error".equals(lastTag)) { + if (errorsOpen == 1) { + parent.addAttribute("\"error\":[\"error\",{}" + + tag.getData() + "]"); + } else { + // sub error + parent.addData(tag.getJSON()); + } + errorsOpen--; + } else { + parent.addData(tag.getJSON()); + } + + tag = parent; + } else { + changes++; + uidlBuffer.print(((changes > 1) ? "," : "") + tag.getJSON()); + tag = null; + } + } + + /** + * Substitutes the XML sensitive characters with predefined XML entities. + * + * @param xml + * the String to be substituted. + * @return A new string instance where all occurrences of XML sensitive + * characters are substituted with entities. + */ + static public String escapeXML(String xml) { + if (xml == null || xml.length() <= 0) { + return ""; + } + return escapeXML(new StringBuffer(xml)).toString(); + } + + /** + * Substitutes the XML sensitive characters with predefined XML entities. + * + * @param xml + * the String to be substituted. + * @return A new StringBuffer instance where all occurrences of XML + * sensitive characters are substituted with entities. + * + */ + static public StringBuffer escapeXML(StringBuffer xml) { + if (xml == null || xml.length() <= 0) { + return new StringBuffer(""); + } + + final StringBuffer result = new StringBuffer(xml.length() * 2); + + for (int i = 0; i < xml.length(); i++) { + final char c = xml.charAt(i); + final String s = toXmlChar(c); + if (s != null) { + result.append(s); + } else { + result.append(c); + } + } + return result; + } + + static public String escapeJSON(String s) { + if (s == null) { + return ""; + } + final StringBuffer sb = new StringBuffer(); + for (int i = 0; i < s.length(); i++) { + final char ch = s.charAt(i); + switch (ch) { + case '"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '/': + sb.append("\\/"); + break; + default: + if (ch >= '\u0000' && ch <= '\u001F') { + final String ss = Integer.toHexString(ch); + sb.append("\\u"); + for (int k = 0; k < 4 - ss.length(); k++) { + sb.append('0'); + } + sb.append(ss.toUpperCase()); + } else { + sb.append(ch); + } + } + } + return sb.toString(); + } + + /** + * Substitutes a XML sensitive character with predefined XML entity. + * + * @param c + * the Character to be replaced with an entity. + * @return String of the entity or null if character is not to be replaced + * with an entity. + */ + private static String toXmlChar(char c) { + switch (c) { + case '&': + return "&"; // & => & + case '>': + return ">"; // > => > + case '<': + return "<"; // < => < + case '"': + return """; // " => " + case '\'': + return "'"; // ' => ' + default: + return null; + } + } + + /** + * Prints XML-escaped text. + * + * @param str + * @throws PaintException + * if the paint operation failed. + * + */ + public void addText(String str) throws PaintException { + tag.addData("\"" + escapeJSON(str) + "\""); + } + + /** + * Adds a boolean attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, boolean value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + (value ? "true" : "false")); + } + + /** + * Adds a resource attribute to component. Attributes must be added before + * any content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, Resource value) throws PaintException { + + if (value instanceof ExternalResource) { + addAttribute(name, ((ExternalResource) value).getURL()); + + } else if (value instanceof ApplicationResource) { + final ApplicationResource r = (ApplicationResource) value; + final Application a = r.getApplication(); + if (a == null) { + throw new PaintException( + "Application not specified for resorce " + + value.getClass().getName()); + } + String uri; + if (a.getURL() != null) { + uri = a.getURL().getPath(); + } else { + uri = ""; + } + if (uri.length() > 0 && uri.charAt(uri.length() - 1) != '/') { + uri += "/"; + } + uri += a.getRelativeLocation(r); + addAttribute(name, uri); + + } else if (value instanceof ThemeResource) { + final String uri = "theme://" + + ((ThemeResource) value).getResourceId(); + addAttribute(name, uri); + } else { + throw new PaintException("Ajax adapter does not " + + "support resources of type: " + + value.getClass().getName()); + } + + } + + /** + * Adds a integer attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, int value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + /** + * Adds a long attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, long value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + /** + * Adds a float attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, float value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + /** + * Adds a double attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the Attribute name. + * @param value + * the Attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, double value) throws PaintException { + tag.addAttribute("\"" + name + "\":" + String.valueOf(value)); + } + + /** + * Adds a string attribute to component. Atributes must be added before any + * content is written. + * + * @param name + * the String attribute name. + * @param value + * the String attribute value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addAttribute(String name, String value) throws PaintException { + // In case of null data output nothing: + if ((value == null) || (name == null)) { + throw new NullPointerException( + "Parameters must be non-null strings"); + } + + tag.addAttribute("\"" + name + "\": \"" + escapeJSON(value) + "\""); + + if (customLayoutArgumentsOpen && "template".equals(name)) { + getPreCachedResources().add("layouts/" + value + ".html"); + } + + if (name.equals("locale")) { + manager.requireLocale(value); + } + + } + + public void addAttribute(String name, Object[] values) { + // In case of null data output nothing: + if ((values == null) || (name == null)) { + throw new NullPointerException( + "Parameters must be non-null strings"); + } + final StringBuffer buf = new StringBuffer(); + buf.append("\"" + name + "\":["); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + buf.append(","); + } + buf.append("\""); + buf.append(escapeJSON(values[i].toString())); + buf.append("\""); + } + buf.append("]"); + tag.addAttribute(buf.toString()); + } + + /** + * Adds a string type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, String value) + throws PaintException { + tag.addVariable(new StringVariable(owner, name, escapeJSON(value))); + } + + /** + * Adds a int type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, int value) + throws PaintException { + tag.addVariable(new IntVariable(owner, name, value)); + } + + /** + * Adds a long type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, long value) + throws PaintException { + tag.addVariable(new LongVariable(owner, name, value)); + } + + /** + * Adds a float type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, float value) + throws PaintException { + tag.addVariable(new FloatVariable(owner, name, value)); + } + + /** + * Adds a double type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, double value) + throws PaintException { + tag.addVariable(new DoubleVariable(owner, name, value)); + } + + /** + * Adds a boolean type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, boolean value) + throws PaintException { + tag.addVariable(new BooleanVariable(owner, name, value)); + } + + /** + * Adds a string array type variable. + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * @param value + * the Variable initial value. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addVariable(VariableOwner owner, String name, String[] value) + throws PaintException { + tag.addVariable(new ArrayVariable(owner, name, value)); + } + + /** + * Adds a upload stream type variable. + * + * TODO not converted for JSON + * + * @param owner + * the Listener for variable changes. + * @param name + * the Variable name. + * + * @throws PaintException + * if the paint operation failed. + */ + public void addUploadStreamVariable(VariableOwner owner, String name) + throws PaintException { + startTag("uploadstream"); + addAttribute(UIDL_ARG_NAME, name); + endTag("uploadstream"); + } + + /** + * Prints the single text section. + * + * Prints full text section. The section data is escaped + * + * @param sectionTagName + * the name of the tag. + * @param sectionData + * the section data to be printed. + * @throws PaintException + * if the paint operation failed. + */ + public void addSection(String sectionTagName, String sectionData) + throws PaintException { + tag.addData("{\"" + sectionTagName + "\":\"" + escapeJSON(sectionData) + + "\"}"); + } + + /** + * Adds XML directly to UIDL. + * + * @param xml + * the Xml to be added. + * @throws PaintException + * if the paint operation failed. + */ + public void addUIDL(String xml) throws PaintException { + + // Ensure that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + // Make sure that the open start tag is closed before + // anything is written. + + // Escape and write what was given + if (xml != null) { + tag.addData("\"" + escapeJSON(xml) + "\""); + } + + } + + /** + * Adds XML section with namespace. + * + * @param sectionTagName + * the name of the tag. + * @param sectionData + * the section data. + * @param namespace + * the namespace to be added. + * @throws PaintException + * if the paint operation failed. + * + * @see com.vaadin.terminal.PaintTarget#addXMLSection(String, + * String, String) + */ + public void addXMLSection(String sectionTagName, String sectionData, + String namespace) throws PaintException { + + // Ensure that the target is open + if (closed) { + throw new PaintException( + "Attempted to write to a closed PaintTarget."); + } + + startTag(sectionTagName); + if (namespace != null) { + addAttribute("xmlns", namespace); + } + customLayoutArgumentsOpen = false; + + if (sectionData != null) { + tag.addData("\"" + escapeJSON(sectionData) + "\""); + } + endTag(sectionTagName); + } + + /** + * Gets the UIDL already printed to stream. Paint target must be closed + * before the <code>getUIDL</code> can be called. + * + * @return the UIDL. + */ + public String getUIDL() { + if (closed) { + return uidlBuffer.toString(); + } + throw new IllegalStateException( + "Tried to read UIDL from open PaintTarget"); + } + + /** + * Closes the paint target. Paint target must be closed before the + * <code>getUIDL</code> can be called. Subsequent attempts to write to paint + * target. If the target was already closed, call to this function is + * ignored. will generate an exception. + * + * @throws PaintException + * if the paint operation failed. + */ + public void close() throws PaintException { + if (tag != null) { + uidlBuffer.write(tag.getJSON()); + } + flush(); + closed = true; + } + + /** + * Method flush. + */ + private void flush() { + uidlBuffer.flush(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.PaintTarget#startTag(com.vaadin.terminal + * .Paintable, java.lang.String) + */ + public boolean startTag(Paintable paintable, String tagName) + throws PaintException { + startTag(tagName, true); + final boolean isPreviouslyPainted = manager.hasPaintableId(paintable) + && (identifiersCreatedDueRefPaint == null || !identifiersCreatedDueRefPaint + .contains(paintable)); + final String id = manager.getPaintableId(paintable); + paintable.addListener(manager); + addAttribute("id", id); + paintedComponents.add(paintable); + return cacheEnabled && isPreviouslyPainted; + } + + public void paintReference(Paintable paintable, String referenceName) + throws PaintException { + if (!manager.hasPaintableId(paintable)) { + if (identifiersCreatedDueRefPaint == null) { + identifiersCreatedDueRefPaint = new HashSet<Paintable>(); + } + identifiersCreatedDueRefPaint.add(paintable); + } + final String id = manager.getPaintableId(paintable); + addAttribute(referenceName, id); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.PaintTarget#addCharacterData(java.lang.String + * ) + */ + public void addCharacterData(String text) throws PaintException { + if (text != null) { + tag.addData(text); + } + } + + /** + * This is basically a container for UI components variables, that will be + * added at the end of JSON object. + * + * @author mattitahvonen + * + */ + class JsonTag implements Serializable { + boolean firstField = false; + + Vector variables = new Vector(); + + Vector children = new Vector(); + + Vector attr = new Vector(); + + StringBuffer data = new StringBuffer(); + + public boolean childrenArrayOpen = false; + + private boolean childNode = false; + + private boolean tagClosed = false; + + public JsonTag(String tagName) { + data.append("[\"" + tagName + "\""); + } + + private void closeTag() { + if (!tagClosed) { + data.append(attributesAsJsonObject()); + data.append(getData()); + // Writes the end (closing) tag + data.append("]"); + tagClosed = true; + } + } + + public String getJSON() { + if (!tagClosed) { + closeTag(); + } + return data.toString(); + } + + public void openChildrenArray() { + if (!childrenArrayOpen) { + // append("c : ["); + childrenArrayOpen = true; + // firstField = true; + } + } + + public void closeChildrenArray() { + // append("]"); + // firstField = false; + } + + public void setChildNode(boolean b) { + childNode = b; + } + + public boolean isChildNode() { + return childNode; + } + + public String startField() { + if (firstField) { + firstField = false; + return ""; + } else { + return ","; + } + } + + /** + * + * @param s + * json string, object or array + */ + public void addData(String s) { + children.add(s); + } + + public String getData() { + final StringBuffer buf = new StringBuffer(); + final Iterator it = children.iterator(); + while (it.hasNext()) { + buf.append(startField()); + buf.append(it.next()); + } + return buf.toString(); + } + + public void addAttribute(String jsonNode) { + attr.add(jsonNode); + } + + private String attributesAsJsonObject() { + final StringBuffer buf = new StringBuffer(); + buf.append(startField()); + buf.append("{"); + for (final Iterator iter = attr.iterator(); iter.hasNext();) { + final String element = (String) iter.next(); + buf.append(element); + if (iter.hasNext()) { + buf.append(","); + } + } + buf.append(tag.variablesAsJsonObject()); + buf.append("}"); + return buf.toString(); + } + + public void addVariable(Variable v) { + variables.add(v); + } + + private String variablesAsJsonObject() { + if (variables.size() == 0) { + return ""; + } + final StringBuffer buf = new StringBuffer(); + buf.append(startField()); + buf.append("\"v\":{"); + final Iterator iter = variables.iterator(); + while (iter.hasNext()) { + final Variable element = (Variable) iter.next(); + buf.append(element.getJsonPresentation()); + if (iter.hasNext()) { + buf.append(","); + } + } + buf.append("}"); + return buf.toString(); + } + + class TagCounter { + int count; + + public TagCounter() { + count = 0; + } + + public void increment() { + count++; + } + + public String postfix(String s) { + if (count > 0) { + return s + count; + } + return s; + } + } + } + + abstract class Variable implements Serializable { + + String name; + + public abstract String getJsonPresentation(); + } + + class BooleanVariable extends Variable implements Serializable { + boolean value; + + public BooleanVariable(VariableOwner owner, String name, boolean v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + (value == true ? "true" : "false"); + } + + } + + class StringVariable extends Variable implements Serializable { + String value; + + public StringVariable(VariableOwner owner, String name, String v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":\"" + value + "\""; + } + + } + + class IntVariable extends Variable implements Serializable { + int value; + + public IntVariable(VariableOwner owner, String name, int v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class LongVariable extends Variable implements Serializable { + long value; + + public LongVariable(VariableOwner owner, String name, long v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class FloatVariable extends Variable implements Serializable { + float value; + + public FloatVariable(VariableOwner owner, String name, float v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class DoubleVariable extends Variable implements Serializable { + double value; + + public DoubleVariable(VariableOwner owner, String name, double v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + return "\"" + name + "\":" + value; + } + } + + class ArrayVariable extends Variable implements Serializable { + String[] value; + + public ArrayVariable(VariableOwner owner, String name, String[] v) { + value = v; + this.name = name; + } + + @Override + public String getJsonPresentation() { + String pres = "\"" + name + "\":["; + for (int i = 0; i < value.length;) { + pres += "\"" + value[i] + "\""; + i++; + if (i < value.length) { + pres += ","; + } + } + pres += "]"; + return pres; + } + } + + public Set getPreCachedResources() { + return preCachedResources; + } + + public void setPreCachedResources(Set preCachedResources) { + throw new UnsupportedOperationException(); + } + + /** + * Method to check if paintable is already painted into this target. + * + * @param p + * @return true if is not yet painted into this target and is connected to + * app + */ + public boolean needsToBePainted(Paintable p) { + if (paintedComponents.contains(p)) { + return false; + } else if (((Component) p).getApplication() == null) { + return false; + } else { + return true; + } + } +} diff --git a/src/com/vaadin/terminal/gwt/server/PortletApplicationContext.java b/src/com/vaadin/terminal/gwt/server/PortletApplicationContext.java new file mode 100644 index 0000000000..87fe4104a8 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/PortletApplicationContext.java @@ -0,0 +1,290 @@ +/**
+ *
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.ActionResponse;
+import javax.portlet.Portlet;
+import javax.portlet.PortletSession;
+import javax.portlet.PortletURL;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+import javax.servlet.http.HttpSession;
+
+import com.vaadin.Application;
+
+/**
+ * @author marc
+ *
+ */
+@SuppressWarnings("serial")
+public class PortletApplicationContext extends WebApplicationContext implements
+ Serializable {
+
+ protected PortletSession portletSession;
+
+ protected Map portletListeners = new HashMap();
+
+ protected Map portletToApplication = new HashMap();
+
+ PortletApplicationContext() {
+
+ }
+
+ static public PortletApplicationContext getApplicationContext(
+ PortletSession session) {
+ WebApplicationContext cx = (WebApplicationContext) session
+ .getAttribute(WebApplicationContext.class.getName(),
+ PortletSession.APPLICATION_SCOPE);
+ if (cx == null) {
+ cx = new PortletApplicationContext();
+ }
+ if (!(cx instanceof PortletApplicationContext)) {
+ // TODO Should we even try this? And should we leave original as-is?
+ PortletApplicationContext pcx = new PortletApplicationContext();
+ pcx.applications.addAll(cx.applications);
+ cx.applications.clear();
+ pcx.browser = cx.browser;
+ cx.browser = null;
+ pcx.listeners = cx.listeners;
+ cx.listeners = null;
+ pcx.session = cx.session;
+ cx = pcx;
+ }
+ if (((PortletApplicationContext) cx).portletSession == null) {
+ ((PortletApplicationContext) cx).portletSession = session;
+ }
+ session.setAttribute(WebApplicationContext.class.getName(), cx,
+ PortletSession.APPLICATION_SCOPE);
+ return (PortletApplicationContext) cx;
+ }
+
+ static public WebApplicationContext getApplicationContext(
+ HttpSession session) {
+ WebApplicationContext cx = (WebApplicationContext) session
+ .getAttribute(WebApplicationContext.class.getName());
+ if (cx == null) {
+ cx = new PortletApplicationContext();
+ }
+ if (cx.session == null) {
+ cx.session = session;
+ }
+ session.setAttribute(WebApplicationContext.class.getName(), cx);
+ return cx;
+ }
+
+ @Override
+ protected void removeApplication(Application application) {
+ portletListeners.remove(application);
+ for (Iterator it = portletToApplication.keySet().iterator(); it
+ .hasNext();) {
+ Object key = it.next();
+ if (key == application) {
+ portletToApplication.remove(key);
+ }
+ }
+ super.removeApplication(application);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (portletSession == null) {
+ return super.equals(obj);
+ }
+ return portletSession.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ if (portletSession == null) {
+ return super.hashCode();
+ }
+ return portletSession.hashCode();
+ }
+
+ public void setPortletApplication(Portlet portlet, Application app) {
+ portletToApplication.put(portlet, app);
+ }
+
+ public Application getPortletApplication(Portlet portlet) {
+ return (Application) portletToApplication.get(portlet);
+ }
+
+ public PortletSession getPortletSession() {
+ return portletSession;
+ }
+
+ public void addPortletListener(Application app, PortletListener listener) {
+ Set l = (Set) portletListeners.get(app);
+ if (l == null) {
+ l = new LinkedHashSet();
+ portletListeners.put(app, l);
+ }
+ l.add(listener);
+ }
+
+ public void removePortletListener(Application app, PortletListener listener) {
+ Set l = (Set) portletListeners.get(app);
+ if (l != null) {
+ l.remove(listener);
+ }
+ }
+
+ public static void dispatchRequest(Portlet portlet, RenderRequest request,
+ RenderResponse response) {
+ PortletApplicationContext ctx = getApplicationContext(request
+ .getPortletSession());
+ if (ctx != null) {
+ ctx.firePortletRenderRequest(portlet, request, response);
+ }
+ }
+
+ public static void dispatchRequest(Portlet portlet, ActionRequest request,
+ ActionResponse response) {
+ PortletApplicationContext ctx = getApplicationContext(request
+ .getPortletSession());
+ if (ctx != null) {
+ ctx.firePortletActionRequest(portlet, request, response);
+ }
+ }
+
+ public void firePortletRenderRequest(Portlet portlet,
+ RenderRequest request, RenderResponse response) {
+ Application app = getPortletApplication(portlet);
+ Set listeners = (Set) portletListeners.get(app);
+ if (listeners != null) {
+ for (Iterator it = listeners.iterator(); it.hasNext();) {
+ PortletListener l = (PortletListener) it.next();
+ l.handleRenderRequest(request, new RestrictedRenderResponse(
+ response));
+ }
+ }
+ }
+
+ public void firePortletActionRequest(Portlet portlet,
+ ActionRequest request, ActionResponse response) {
+ Application app = getPortletApplication(portlet);
+ Set listeners = (Set) portletListeners.get(app);
+ if (listeners != null) {
+ for (Iterator it = listeners.iterator(); it.hasNext();) {
+ PortletListener l = (PortletListener) it.next();
+ l.handleActionRequest(request, response);
+ }
+ }
+ }
+
+ public interface PortletListener extends Serializable {
+ public void handleRenderRequest(RenderRequest request,
+ RenderResponse response);
+
+ public void handleActionRequest(ActionRequest request,
+ ActionResponse response);
+ }
+
+ private class RestrictedRenderResponse implements RenderResponse,
+ Serializable {
+
+ private RenderResponse response;
+
+ private RestrictedRenderResponse(RenderResponse response) {
+ this.response = response;
+ }
+
+ public void addProperty(String key, String value) {
+ response.addProperty(key, value);
+ }
+
+ public PortletURL createActionURL() {
+ return response.createActionURL();
+ }
+
+ public PortletURL createRenderURL() {
+ return response.createRenderURL();
+ }
+
+ public String encodeURL(String path) {
+ return response.encodeURL(path);
+ }
+
+ public void flushBuffer() throws IOException {
+ // NOP
+ // TODO throw?
+ }
+
+ public int getBufferSize() {
+ return response.getBufferSize();
+ }
+
+ public String getCharacterEncoding() {
+ return response.getCharacterEncoding();
+ }
+
+ public String getContentType() {
+ return response.getContentType();
+ }
+
+ public Locale getLocale() {
+ return response.getLocale();
+ }
+
+ public String getNamespace() {
+ return response.getNamespace();
+ }
+
+ public OutputStream getPortletOutputStream() throws IOException {
+ // write forbidden
+ return null;
+ }
+
+ public PrintWriter getWriter() throws IOException {
+ // write forbidden
+ return null;
+ }
+
+ public boolean isCommitted() {
+ return response.isCommitted();
+ }
+
+ public void reset() {
+ // NOP
+ // TODO throw?
+ }
+
+ public void resetBuffer() {
+ // NOP
+ // TODO throw?
+ }
+
+ public void setBufferSize(int size) {
+ // NOP
+ // TODO throw?
+ }
+
+ public void setContentType(String type) {
+ // NOP
+ // TODO throw?
+ }
+
+ public void setProperty(String key, String value) {
+ response.setProperty(key, value);
+ }
+
+ public void setTitle(String title) {
+ response.setTitle(title);
+ }
+
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/server/SessionExpired.java b/src/com/vaadin/terminal/gwt/server/SessionExpired.java new file mode 100644 index 0000000000..c2cf309837 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/SessionExpired.java @@ -0,0 +1,6 @@ +package com.vaadin.terminal.gwt.server; + +@SuppressWarnings("serial") +public class SessionExpired extends Exception { + +} diff --git a/src/com/vaadin/terminal/gwt/server/SystemMessageException.java b/src/com/vaadin/terminal/gwt/server/SystemMessageException.java new file mode 100644 index 0000000000..67731d5627 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/SystemMessageException.java @@ -0,0 +1,54 @@ +package com.vaadin.terminal.gwt.server;
+
+@SuppressWarnings("serial")
+public class SystemMessageException extends RuntimeException {
+
+ /**
+ * Cause of the method exception
+ */
+ private Throwable cause;
+
+ /**
+ * Constructs a new <code>SystemMessageException</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message.
+ */
+ public SystemMessageException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a new <code>SystemMessageException</code> with the specified
+ * detail message and cause.
+ *
+ * @param msg
+ * the detail message.
+ * @param cause
+ * the cause of the exception.
+ */
+ public SystemMessageException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+ /**
+ * Constructs a new <code>SystemMessageException</code> from another
+ * exception.
+ *
+ * @param cause
+ * the cause of the exception.
+ */
+ public SystemMessageException(Throwable cause) {
+ this.cause = cause;
+ }
+
+ /**
+ * @see java.lang.Throwable#getCause()
+ */
+ @Override
+ public Throwable getCause() {
+ return cause;
+ }
+
+}
\ No newline at end of file diff --git a/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java b/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java new file mode 100644 index 0000000000..0cc83f21f5 --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java @@ -0,0 +1,286 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.io.File; +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.StringWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; + +import com.vaadin.Application; +import com.vaadin.service.ApplicationContext; + +/** + * Web application context for the IT Mill Toolkit applications. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.1 + */ +@SuppressWarnings("serial") +public class WebApplicationContext implements ApplicationContext, + HttpSessionBindingListener, Serializable { + + protected List<TransactionListener> listeners; + + protected transient HttpSession session; + + protected final HashSet<Application> applications = new HashSet<Application>(); + + protected WebBrowser browser = new WebBrowser(); + + protected HashMap<Application, CommunicationManager> applicationToAjaxAppMgrMap = new HashMap<Application, CommunicationManager>(); + + /** + * Creates a new Web Application Context. + * + */ + WebApplicationContext() { + + } + + /** + * Gets the application context base directory. + * + * @see com.vaadin.service.ApplicationContext#getBaseDirectory() + */ + public File getBaseDirectory() { + final String realPath = ApplicationServlet.getResourcePath(session + .getServletContext(), "/"); + if (realPath == null) { + return null; + } + return new File(realPath); + } + + /** + * Gets the http-session application is running in. + * + * @return HttpSession this application context resides in. + */ + public HttpSession getHttpSession() { + return session; + } + + /** + * Gets the applications in this context. + * + * @see com.vaadin.service.ApplicationContext#getApplications() + */ + public Collection getApplications() { + return Collections.unmodifiableCollection(applications); + } + + /** + * Gets the application context for HttpSession. + * + * @param session + * the HTTP session. + * @return the application context for HttpSession. + */ + static public WebApplicationContext getApplicationContext( + HttpSession session) { + WebApplicationContext cx = (WebApplicationContext) session + .getAttribute(WebApplicationContext.class.getName()); + if (cx == null) { + cx = new WebApplicationContext(); + session.setAttribute(WebApplicationContext.class.getName(), cx); + } + if (cx.session == null) { + cx.session = session; + } + return cx; + } + + @Override + public boolean equals(Object obj) { + if (session == null) { + return false; + } + + return session.equals(obj); + } + + @Override + public int hashCode() { + if (session == null) { + return -1; + } + return session.hashCode(); + } + + /** + * Adds the transaction listener to this context. + * + * @see com.vaadin.service.ApplicationContext#addTransactionListener(com.vaadin.service.ApplicationContext.TransactionListener) + */ + public void addTransactionListener(TransactionListener listener) { + if (listeners == null) { + listeners = new LinkedList<TransactionListener>(); + } + listeners.add(listener); + } + + /** + * Removes the transaction listener from this context. + * + * @see com.vaadin.service.ApplicationContext#removeTransactionListener(com.vaadin.service.ApplicationContext.TransactionListener) + */ + public void removeTransactionListener(TransactionListener listener) { + if (listeners != null) { + listeners.remove(listener); + } + + } + + /** + * Notifies the transaction start. + * + * @param application + * @param request + * the HTTP request. + */ + protected void startTransaction(Application application, + HttpServletRequest request) { + if (listeners == null) { + return; + } + for (final Iterator i = listeners.iterator(); i.hasNext();) { + ((ApplicationContext.TransactionListener) i.next()) + .transactionStart(application, request); + } + } + + /** + * Notifies the transaction end. + * + * @param application + * @param request + * the HTTP request. + */ + protected void endTransaction(Application application, + HttpServletRequest request) { + if (listeners == null) { + return; + } + + LinkedList<Exception> exceptions = null; + for (final Iterator i = listeners.iterator(); i.hasNext();) { + try { + ((ApplicationContext.TransactionListener) i.next()) + .transactionEnd(application, request); + } catch (final RuntimeException t) { + if (exceptions == null) { + exceptions = new LinkedList<Exception>(); + } + exceptions.add(t); + } + } + + // If any runtime exceptions occurred, throw a combined exception + if (exceptions != null) { + final StringBuffer msg = new StringBuffer(); + for (final Iterator i = exceptions.iterator(); i.hasNext();) { + final RuntimeException e = (RuntimeException) i.next(); + if (msg.length() == 0) { + msg.append("\n\n--------------------------\n\n"); + } + msg.append(e.getMessage() + "\n"); + final StringWriter trace = new StringWriter(); + e.printStackTrace(new PrintWriter(trace, true)); + msg.append(trace.toString()); + } + throw new RuntimeException(msg.toString()); + } + } + + protected void removeApplication(Application application) { + applications.remove(application); + } + + protected void addApplication(Application application) { + applications.add(application); + } + + /** + * @see javax.servlet.http.HttpSessionBindingListener#valueBound(HttpSessionBindingEvent) + */ + public void valueBound(HttpSessionBindingEvent arg0) { + // We are not interested in bindings + } + + /** + * @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(HttpSessionBindingEvent) + */ + public void valueUnbound(HttpSessionBindingEvent event) { + // If we are going to be unbound from the session, the session must be + // closing + try { + while (!applications.isEmpty()) { + final Application app = applications.iterator().next(); + app.close(); + applicationToAjaxAppMgrMap.remove(app); + removeApplication(app); + } + } catch (Exception e) { + // This should never happen but is possible with rare + // configurations (e.g. robustness tests). If you have one + // thread doing HTTP socket write and another thread trying to + // remove same application here. Possible if you got e.g. session + // lifetime 1 min but socket write may take longer than 1 min. + // FIXME: Handle exception + System.err.println("Could not remove application, leaking memory."); + e.printStackTrace(); + } + } + + /** + * Get the web browser associated with this application context. + * + * Because application context is related to the http session and server + * maintains one session per browser-instance, each context has exactly one + * web browser associated with it. + * + * @return + */ + public WebBrowser getBrowser() { + return browser; + } + + /** + * Gets communication manager for an application. + * + * If this application has not been running before, a new manager is + * created. + * + * @param application + * @return CommunicationManager + */ + protected CommunicationManager getApplicationManager( + Application application, AbstractApplicationServlet servlet) { + CommunicationManager mgr = applicationToAjaxAppMgrMap.get(application); + + if (mgr == null) { + // Creates new manager + mgr = new CommunicationManager(application, servlet); + applicationToAjaxAppMgrMap.put(application, mgr); + } + return mgr; + } + +} diff --git a/src/com/vaadin/terminal/gwt/server/WebBrowser.java b/src/com/vaadin/terminal/gwt/server/WebBrowser.java new file mode 100644 index 0000000000..2ec726fc6a --- /dev/null +++ b/src/com/vaadin/terminal/gwt/server/WebBrowser.java @@ -0,0 +1,98 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.server; + +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import com.vaadin.terminal.Terminal; + +@SuppressWarnings("serial") +public class WebBrowser implements Terminal { + + private int screenHeight = 0; + private int screenWidth = 0; + private String browserApplication = null; + private Locale locale; + private String address; + private boolean secureConnection; + + /** + * There is no default-theme for this terminal type. + * + * @return Allways returns null. + */ + public String getDefaultTheme() { + return null; + } + + /** + * Get the height of the users display in pixels. + * + */ + public int getScreenHeight() { + return screenHeight; + } + + /** + * Get the width of the users display in pixels. + * + */ + public int getScreenWidth() { + return screenWidth; + } + + /** + * Get the browser user-agent string. + * + * @return + */ + public String getBrowserApplication() { + return browserApplication; + } + + void updateBrowserProperties(HttpServletRequest request) { + locale = request.getLocale(); + address = request.getRemoteAddr(); + secureConnection = request.isSecure(); + + final String agent = request.getHeader("user-agent"); + if (agent != null) { + browserApplication = agent; + } + + final String sw = request.getParameter("screenWidth"); + final String sh = request.getParameter("screenHeight"); + if (sw != null && sh != null) { + try { + screenHeight = Integer.parseInt(sh); + screenWidth = Integer.parseInt(sw); + } catch (final NumberFormatException e) { + screenHeight = screenWidth = 0; + } + } + } + + /** + * Get the IP-address of the web browser. + * + * @return IP-address in 1.12.123.123 -format + */ + public String getAddress() { + return address; + } + + /** Get the default locate of the browser. */ + public Locale getLocale() { + return locale; + } + + /** Is the connection made using HTTPS? */ + public boolean isSecureConnection() { + return secureConnection; + } + +} diff --git a/src/com/vaadin/terminal/package.html b/src/com/vaadin/terminal/package.html new file mode 100644 index 0000000000..83514a0de5 --- /dev/null +++ b/src/com/vaadin/terminal/package.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> + +</head> + +<body bgcolor="white"> + +<!-- Package summary here --> + +<p>Provides classes and interfaces that wrap the terminal-side functionalities +for the server-side application. (FIXME: This could be a little more descriptive and wordy.)</p> + +<h2>Package Specification</h2> + +<!-- Package spec here --> + +<!-- Put @see and @since tags down here. --> + +</body> +</html> |