blob: 17f7145e91f8bce1dd19e76093cf9476d210c2d5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.myfaces.tobago.internal.component;
import org.apache.myfaces.tobago.apt.annotation.Preliminary;
import org.apache.myfaces.tobago.component.Attributes;
import org.apache.myfaces.tobago.component.LabelLayout;
import org.apache.myfaces.tobago.component.RendererTypes;
import org.apache.myfaces.tobago.component.Tags;
import org.apache.myfaces.tobago.internal.util.StyleRenderUtils;
import org.apache.myfaces.tobago.layout.GridSpan;
import org.apache.myfaces.tobago.layout.MeasureList;
import org.apache.myfaces.tobago.util.ComponentUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.faces.component.UIComponent;
import javax.faces.component.UIPanel;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ComponentSystemEvent;
import javax.faces.event.ComponentSystemEventListener;
import javax.faces.event.ListenerFor;
import javax.faces.event.PreRenderComponentEvent;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
/**
* <p>
* A grid layout manager.
* </p>
* <p>
* {@link org.apache.myfaces.tobago.internal.taglib.component.GridLayoutTagDeclaration}
* </p>
*/
@Preliminary
@ListenerFor(systemEventClass = PreRenderComponentEvent.class)
public abstract class AbstractUIGridLayout extends AbstractUILayoutBase implements ComponentSystemEventListener {
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String COMPONENT_FAMILY = "org.apache.myfaces.tobago.GridLayout";
private static final int STEP = 5;
protected static final UIComponent SPAN = new UIPanel();
/**
* Initialize the grid and remove the current width and height values from the component, recursively.
*/
@Override
public void processEvent(final ComponentSystemEvent event) throws AbortProcessingException {
super.processEvent(event);
if (!isRendered()) {
return;
}
if (event instanceof PreRenderComponentEvent) {
layout(
MeasureList.parse(getColumns()).getSize(),
MeasureList.parse(getRows()).getSize(),
ComponentUtils.findLayoutChildren(this));
}
}
public abstract String getRows();
public abstract void setRows(String rows);
public abstract String getColumns();
public abstract void setColumns(String columns);
protected UIComponent[][] layout(
final int columnsCount, final int initialRowsCount, final List<UIComponent> components) {
assert columnsCount > 0;
assert initialRowsCount > 0;
final FacesContext facesContext = FacesContext.getCurrentInstance();
UIComponent[][] cells = new UIComponent[initialRowsCount][columnsCount];
// #1 put all components with "gridRow" and "gridColumn" set into the grid cells
for (final UIComponent component : components) {
final Map<String, Object> attributes = component.getAttributes();
final Integer gridColumn = (Integer) attributes.get(Attributes.gridColumn.getName());
final Integer gridRow = (Integer) attributes.get(Attributes.gridRow.getName());
if (gridColumn != null && gridRow != null) {
if (gridColumn > columnsCount) {
// ignore wrong columns
LOG.warn("gridColumn {} > columnsCount {} in component '{}'!", gridColumn, columnsCount,
component.getClientId(facesContext));
} else {
if (gridRow > cells.length) {
if (LOG.isDebugEnabled()) {
LOG.debug("expanding, because gridRow {} > rowCount {} in component '{}'!", gridRow, cells.length,
component.getClientId(facesContext));
}
// ensure enough rows
cells = expand(cells, gridRow);
}
cells = set(cells, gridColumn - 1, gridRow - 1, component);
}
} else if (gridColumn != null) {
LOG.warn("gridColumn is set to {}, but gridRow not in component '{}'!", gridColumn,
component.getClientId(facesContext));
} else if (gridRow != null) {
LOG.warn("gridRow is set to {}, but gridColumn not in component '{}'!", gridRow,
component.getClientId(facesContext));
}
}
// #2 distribute the rest of the components to the free grid cells
int j = 0;
int i = 0;
for (final UIComponent component : components) {
final Map<String, Object> attributes = component.getAttributes();
final Integer gridColumn = (Integer) attributes.get(Attributes.gridColumn.getName());
final Integer gridRow = (Integer) attributes.get(Attributes.gridRow.getName());
// find a component without a position
// if only one value is defined, treat as undefined
if (gridColumn == null || gridRow == null) {
// find next free cell
while (cells[j][i] != null) {
i++;
if (i >= columnsCount) {
i = 0;
j++;
}
if (j >= cells.length) {
cells = expand(cells, j + STEP);
}
}
cells = set(cells, i, j, component);
}
}
// #3 create UIStyle children. TODO: There might be a better way...
for (final UIComponent component : components) {
final Map<String, Object> attributes = component.getAttributes();
final int gridColumn = (Integer) attributes.get(Attributes.gridColumn.getName());
final Integer columnSpan = (Integer) attributes.get(Attributes.columnSpan.getName());
final int gridRow = (Integer) attributes.get(Attributes.gridRow.getName());
final Integer rowSpan = (Integer) attributes.get(Attributes.rowSpan.getName());
final boolean labeledLeft = LabelLayout.isGridLeft(component);
final boolean labeledRight = LabelLayout.isGridRight(component);
final boolean labeledHorizontal = labeledLeft || labeledRight;
final boolean labeledTop = LabelLayout.isGridTop(component);
final boolean labeledBottom = LabelLayout.isGridBottom(component);
final boolean labeledVertical = labeledTop || labeledBottom;
final boolean labeled = labeledHorizontal || labeledVertical;
// field style
AbstractUIStyle fieldStyle = ComponentUtils.findChild(component, AbstractUIStyle.class);
if (fieldStyle == null) {
fieldStyle = (AbstractUIStyle) ComponentUtils.createComponent(
facesContext, Tags.style.componentType(), RendererTypes.Style, null);
component.getChildren().add(fieldStyle);
}
// Style must be transient to avoid creating a new instance of GridSpan while restore state
// https://issues.apache.org/jira/browse/TOBAGO-1909
fieldStyle.setTransient(true);
fieldStyle.setGridColumn(
GridSpan.valueOf(labeledLeft ? gridColumn + 1 : gridColumn, labeledHorizontal ? columnSpan - 1 : columnSpan));
fieldStyle.setGridRow(
GridSpan.valueOf(labeledTop ? gridRow + 1 : gridRow, labeledVertical ? rowSpan - 1 : rowSpan));
// label style
if (labeled) {
final AbstractUIStyle labelStyle = (AbstractUIStyle) ComponentUtils.createComponent(
facesContext, Tags.style.componentType(), RendererTypes.Style, null);
component.getChildren().add(labelStyle);
labelStyle.setTransient(true);
labelStyle.setSelector(StyleRenderUtils.encodeIdSelector(
component.getClientId(facesContext) + ComponentUtils.SUB_SEPARATOR + "label"));
labelStyle.setGridColumn(GridSpan.valueOf(labeledRight ? gridColumn + columnSpan - 1 : gridColumn, null));
labelStyle.setGridRow(GridSpan.valueOf(labeledBottom ? gridRow + rowSpan - 1 : gridRow, null));
}
}
return cells;
}
/**
* Set the component to the cells grid.
* Also mark the span-area as occupied.
*
* @param initialCells The cells area
* @param column Zero based column position
* @param row Zero based row position
* @param component Component to set
* @return The result cells, possibly the initial array or a new array
*/
private UIComponent[][] set(
final UIComponent[][] initialCells, final Integer column, final Integer row, final UIComponent component) {
UIComponent[][] cells = initialCells;
final Map<String, Object> attributes = component.getAttributes();
int rowSpan = ComponentUtils.getIntAttribute(component, Attributes.rowSpan,
LabelLayout.isGridTop(component) || LabelLayout.isGridBottom(component) ? 2 : 1);
ComponentUtils.setAttribute(component, Attributes.rowSpan, rowSpan);
int columnSpan = ComponentUtils.getIntAttribute(component, Attributes.columnSpan,
LabelLayout.isGridLeft(component) || LabelLayout.isGridRight(component) ? 2 : 1);
ComponentUtils.setAttribute(component, Attributes.columnSpan, columnSpan);
// the span area
for (int j = row; j < rowSpan + row; j++) {
for (int i = column; i < columnSpan + column; i++) {
if (i >= cells[0].length) {
LOG.warn("column {} + columnSpan {} - 1 >= columnsCount {} in component '{}'!",
column + 1, columnSpan, cells[0].length, component.getClientId(FacesContext.getCurrentInstance()));
break;
}
if (j >= cells.length) {
cells = expand(cells, j + STEP);
}
if (j == row && i == column) { // position
cells[j][i] = component;
attributes.put(Attributes.gridRow.getName(), j + 1);
attributes.put(Attributes.gridColumn.getName(), i + 1);
} else {
cells[j][i] = SPAN;
}
}
}
return cells;
}
/**
* @deprecated since 4.3.0, please use {@link #expand(UIComponent[][], int)}
*/
@Deprecated
protected UIComponent[][] expand(final UIComponent[][] cells, final Integer minRows, final int step) {
return expand(cells, minRows);
}
protected UIComponent[][] expand(final UIComponent[][] cells, final int rows) {
final int columns = cells[0].length;
final UIComponent[][] result = new UIComponent[rows][columns];
for (int j = 0; j < cells.length; j++) {
System.arraycopy(cells[j], 0, result[j], 0, columns);
}
return result;
}
}