com.vaadin.ui.CustomLayout Maven / Gradle / Ivy
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* license.
*/
package com.vaadin.ui;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.jsoup.nodes.Element;
import com.vaadin.server.JsonPaintTarget;
import com.vaadin.server.PaintException;
import com.vaadin.server.PaintTarget;
import com.vaadin.shared.ui.customlayout.CustomLayoutState;
import com.vaadin.ui.declarative.DesignContext;
/**
*
* A container component with freely designed layout and style. The layout
* consists of items with textually represented locations. Each item contains
* one sub-component, which can be any Vaadin component, such as a layout. The
* adapter and theme are responsible for rendering the layout with a given style
* by placing the items in the defined locations.
*
*
*
* The placement of the locations is not fixed - different themes can define the
* locations in a way that is suitable for them. One typical example would be to
* create visual design for a web site as a custom layout: the visual design
* would define locations for "menu", "body", and "title", for example. The
* layout would then be implemented as an HTML template for each theme.
*
*
*
* A location is identified with the attribute "data-location" or "location"
* which has the location name as its value.
*
*
*
* The default theme handles the styles that are not defined by drawing the
* subcomponents just as in OrderedLayout.
*
*
* @author Vaadin Ltd.
* @since 3.0
*/
@SuppressWarnings("serial")
public class CustomLayout extends AbstractLayout implements LegacyComponent {
private static final int BUFFER_SIZE = 10000;
/**
* Custom layout slots containing the components.
*/
private final Map slots = new HashMap<>();
/**
* Default constructor only used by subclasses. Subclasses are responsible
* for setting the appropriate fields. Either
* {@link #setTemplateName(String)}, that makes layout fetch the template
* from theme, or {@link #setTemplateContents(String)}.
*
* @since 7.5.0
*/
public CustomLayout() {
setWidth(100, Unit.PERCENTAGE);
}
/**
* Constructs a custom layout with the template given in the stream.
*
* @param templateStream
* Stream containing template data. Must be using UTF-8 encoding.
* To use a String as a template use for instance new
* ByteArrayInputStream("<template>".getBytes()).
* @throws IOException
*/
public CustomLayout(InputStream templateStream) throws IOException {
this();
initTemplateContentsFromInputStream(templateStream);
}
/**
* Constructor for custom layout with given template name. Template file is
* fetched from "<theme>/layouts/<templateName>".
*/
public CustomLayout(String template) {
this();
setTemplateName(template);
}
protected void initTemplateContentsFromInputStream(
InputStream templateStream) throws IOException {
BufferedReader reader = new BufferedReader(
new InputStreamReader(templateStream, UTF_8));
StringBuilder builder = new StringBuilder(BUFFER_SIZE);
try {
char[] cbuf = new char[BUFFER_SIZE];
int nRead;
while ((nRead = reader.read(cbuf, 0, BUFFER_SIZE)) > 0) {
builder.append(cbuf, 0, nRead);
}
} finally {
reader.close();
}
setTemplateContents(builder.toString());
}
@Override
protected CustomLayoutState getState() {
return (CustomLayoutState) super.getState();
}
@Override
protected CustomLayoutState getState(boolean markAsDirty) {
return (CustomLayoutState) super.getState(markAsDirty);
}
/**
* Adds the component into this container to given location. If the location
* is already populated, the old component is removed.
*
* @param c
* the component to be added.
* @param location
* the location of the component.
*/
public void addComponent(Component c, String location) {
final Component old = slots.get(location);
if (old != null) {
removeComponent(old);
}
slots.put(location, c);
getState().childLocations.put(c, location);
super.addComponent(c);
}
/**
* Adds the component into this container. The component is added without
* specifying the location (empty string is then used as location). Only one
* component can be added to the default "" location and adding more
* components into that location overwrites the old components.
*
* @param c
* the component to be added.
*/
@Override
public void addComponent(Component c) {
this.addComponent(c, "");
}
/**
* Removes the component from this container.
*
* @param c
* the component to be removed.
*/
@Override
public void removeComponent(Component c) {
if (c == null) {
return;
}
slots.values().remove(c);
getState().childLocations.remove(c);
super.removeComponent(c);
}
/**
* Removes the component from this container from given location.
*
* @param location
* the Location identifier of the component.
*/
public void removeComponent(String location) {
this.removeComponent(slots.get(location));
}
/**
* Gets the component container iterator for going trough all the components
* in the container.
*
* @return the Iterator of the components inside the container.
*/
@Override
public Iterator iterator() {
return Collections.unmodifiableCollection(slots.values()).iterator();
}
/**
* Gets the number of contained components. Consistent with the iterator
* returned by {@link #getComponentIterator()}.
*
* @return the number of contained components
*/
@Override
public int getComponentCount() {
return slots.values().size();
}
/**
* Gets the child-component by its location.
*
* @param location
* the name of the location where the requested component
* resides.
* @return the Component in the given location or null if not found.
*/
public Component getComponent(String location) {
return slots.get(location);
}
/* Documented in superclass */
@Override
public void replaceComponent(Component oldComponent,
Component newComponent) {
// Gets the locations
String oldLocation = null;
String newLocation = null;
for (final String location : slots.keySet()) {
final Component component = slots.get(location);
if (component == oldComponent) {
oldLocation = location;
}
if (component == newComponent) {
newLocation = location;
}
}
if (oldLocation == null) {
addComponent(newComponent);
} else if (newLocation == null) {
removeComponent(oldLocation);
addComponent(newComponent, oldLocation);
} else {
slots.put(newLocation, oldComponent);
slots.put(oldLocation, newComponent);
getState().childLocations.put(newComponent, oldLocation);
getState().childLocations.put(oldComponent, newLocation);
}
}
/** Get the name of the template. */
public String getTemplateName() {
return getState(false).templateName;
}
/** Get the contents of the template. */
public String getTemplateContents() {
return getState(false).templateContents;
}
/**
* Set the name of the template used to draw custom layout.
*
* With GWT-adapter, the template with name 'templatename' is loaded from
* VAADIN/themes/themename/layouts/templatename.html. If the theme has not
* been set (with Application.setTheme()), themename is 'default'.
*
* @param templateName
*/
public void setTemplateName(String templateName) {
getState().templateName = templateName;
getState().templateContents = null;
}
/**
* Set the contents of the template used to draw the custom layout.
*
* Note: setTemplateContents can be applied only before CustomLayout
* instance has been attached.
*
* @param templateContents
*/
public void setTemplateContents(String templateContents) {
getState().templateContents = templateContents;
getState().templateName = null;
}
@Override
public void changeVariables(Object source, Map variables) {
// Nothing to see here
}
@Override
public void paintContent(PaintTarget target) throws PaintException {
// Workaround to make the CommunicationManager read the template file
// and send it to the client
String templateName = getState(false).templateName;
if (templateName != null && !templateName.isEmpty()) {
Set