View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.internal.component;
21  
22  import org.apache.myfaces.tobago.apt.annotation.Preliminary;
23  import org.apache.myfaces.tobago.component.Attributes;
24  import org.apache.myfaces.tobago.component.LabelLayout;
25  import org.apache.myfaces.tobago.component.RendererTypes;
26  import org.apache.myfaces.tobago.component.Tags;
27  import org.apache.myfaces.tobago.internal.util.StyleRenderUtils;
28  import org.apache.myfaces.tobago.layout.GridSpan;
29  import org.apache.myfaces.tobago.layout.MeasureList;
30  import org.apache.myfaces.tobago.util.ComponentUtils;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  import javax.faces.component.UIComponent;
35  import javax.faces.component.UIPanel;
36  import javax.faces.context.FacesContext;
37  import javax.faces.event.AbortProcessingException;
38  import javax.faces.event.ComponentSystemEvent;
39  import javax.faces.event.ComponentSystemEventListener;
40  import javax.faces.event.ListenerFor;
41  import javax.faces.event.PreRenderComponentEvent;
42  import java.lang.invoke.MethodHandles;
43  import java.util.List;
44  import java.util.Map;
45  
46  /**
47   * <p>
48   * A grid layout manager.
49   * </p>
50   * <p>
51   * {@link org.apache.myfaces.tobago.internal.taglib.component.GridLayoutTagDeclaration}
52   * </p>
53   */
54  @Preliminary
55  @ListenerFor(systemEventClass = PreRenderComponentEvent.class)
56  public abstract class AbstractUIGridLayout extends AbstractUILayoutBase implements ComponentSystemEventListener {
57  
58    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
59  
60    public static final String COMPONENT_FAMILY = "org.apache.myfaces.tobago.GridLayout";
61  
62    private static final int STEP = 5;
63  
64    protected static final UIComponent SPAN = new UIPanel();
65  
66    /**
67     * Initialize the grid and remove the current width and height values from the component, recursively.
68     */
69    @Override
70    public void processEvent(final ComponentSystemEvent event) throws AbortProcessingException {
71  
72      super.processEvent(event);
73  
74      if (!isRendered()) {
75        return;
76      }
77  
78      if (event instanceof PreRenderComponentEvent) {
79  
80        layout(
81            MeasureList.parse(getColumns()).getSize(),
82            MeasureList.parse(getRows()).getSize(),
83            ComponentUtils.findLayoutChildren(this));
84  
85      }
86    }
87  
88    public abstract String getRows();
89  
90    public abstract void setRows(String rows);
91  
92    public abstract String getColumns();
93  
94    public abstract void setColumns(String columns);
95  
96    protected UIComponent[][] layout(
97        final int columnsCount, final int initialRowsCount, final List<UIComponent> components) {
98      assert columnsCount > 0;
99      assert initialRowsCount > 0;
100 
101     final FacesContext facesContext = FacesContext.getCurrentInstance();
102     UIComponent[][] cells = new UIComponent[initialRowsCount][columnsCount];
103 
104     // #1 put all components with "gridRow" and "gridColumn" set into the grid cells
105     for (final UIComponent component : components) {
106       final Map<String, Object> attributes = component.getAttributes();
107       final Integer gridColumn = (Integer) attributes.get(Attributes.gridColumn.getName());
108       final Integer gridRow = (Integer) attributes.get(Attributes.gridRow.getName());
109       if (gridColumn != null && gridRow != null) {
110         if (gridColumn > columnsCount) {
111           // ignore wrong columns
112           LOG.warn("gridColumn {} > columnsCount {} in component '{}'!", gridColumn, columnsCount,
113               component.getClientId(facesContext));
114         } else {
115           if (gridRow > cells.length) {
116             if (LOG.isDebugEnabled()) {
117               LOG.debug("expanding, because gridRow {} > rowCount {} in component '{}'!", gridRow, cells.length,
118                   component.getClientId(facesContext));
119             }
120             // ensure enough rows
121             cells = expand(cells, gridRow);
122           }
123           cells = set(cells, gridColumn - 1, gridRow - 1, component);
124         }
125       } else if (gridColumn != null) {
126         LOG.warn("gridColumn is set to {}, but gridRow not in component '{}'!", gridColumn,
127             component.getClientId(facesContext));
128       } else if (gridRow != null) {
129         LOG.warn("gridRow is set to {}, but gridColumn not in component '{}'!", gridRow,
130             component.getClientId(facesContext));
131       }
132     }
133 
134     // #2 distribute the rest of the components to the free grid cells
135     int j = 0;
136     int i = 0;
137     for (final UIComponent component : components) {
138       final Map<String, Object> attributes = component.getAttributes();
139       final Integer gridColumn = (Integer) attributes.get(Attributes.gridColumn.getName());
140       final Integer gridRow = (Integer) attributes.get(Attributes.gridRow.getName());
141       // find a component without a position
142       // if only one value is defined, treat as undefined
143       if (gridColumn == null || gridRow == null) {
144         // find next free cell
145         while (cells[j][i] != null) {
146           i++;
147           if (i >= columnsCount) {
148             i = 0;
149             j++;
150           }
151           if (j >= cells.length) {
152             cells = expand(cells, j + STEP);
153           }
154         }
155         cells = set(cells, i, j, component);
156       }
157     }
158 
159     // #3 create UIStyle children. TODO: There might be a better way...
160     for (final UIComponent component : components) {
161       final Map<String, Object> attributes = component.getAttributes();
162 
163       final int gridColumn = (Integer) attributes.get(Attributes.gridColumn.getName());
164       final Integer columnSpan = (Integer) attributes.get(Attributes.columnSpan.getName());
165       final int gridRow = (Integer) attributes.get(Attributes.gridRow.getName());
166       final Integer rowSpan = (Integer) attributes.get(Attributes.rowSpan.getName());
167       final boolean labeledLeft = LabelLayout.isGridLeft(component);
168       final boolean labeledRight = LabelLayout.isGridRight(component);
169       final boolean labeledHorizontal = labeledLeft || labeledRight;
170       final boolean labeledTop = LabelLayout.isGridTop(component);
171       final boolean labeledBottom = LabelLayout.isGridBottom(component);
172       final boolean labeledVertical = labeledTop || labeledBottom;
173       final boolean labeled = labeledHorizontal || labeledVertical;
174 
175       // field style
176 
177       AbstractUIStyle fieldStyle = ComponentUtils.findChild(component, AbstractUIStyle.class);
178       if (fieldStyle == null) {
179         fieldStyle = (AbstractUIStyle) ComponentUtils.createComponent(
180             facesContext, Tags.style.componentType(), RendererTypes.Style, null);
181         component.getChildren().add(fieldStyle);
182       }
183       // Style must be transient to avoid creating a new instance of GridSpan while restore state
184       // https://issues.apache.org/jira/browse/TOBAGO-1909
185       fieldStyle.setTransient(true);
186 
187       fieldStyle.setGridColumn(
188           GridSpan.valueOf(labeledLeft ? gridColumn + 1 : gridColumn, labeledHorizontal ? columnSpan - 1 : columnSpan));
189       fieldStyle.setGridRow(
190           GridSpan.valueOf(labeledTop ? gridRow + 1 : gridRow, labeledVertical ? rowSpan - 1 : rowSpan));
191 
192       // label style
193 
194       if (labeled) {
195         final AbstractUIStyle labelStyle = (AbstractUIStyle) ComponentUtils.createComponent(
196             facesContext, Tags.style.componentType(), RendererTypes.Style, null);
197         component.getChildren().add(labelStyle);
198         labelStyle.setTransient(true);
199         labelStyle.setSelector(StyleRenderUtils.encodeIdSelector(
200             component.getClientId(facesContext) + ComponentUtils.SUB_SEPARATOR + "label"));
201 
202         labelStyle.setGridColumn(GridSpan.valueOf(labeledRight ? gridColumn + columnSpan - 1 : gridColumn, null));
203         labelStyle.setGridRow(GridSpan.valueOf(labeledBottom ? gridRow + rowSpan - 1 : gridRow, null));
204       }
205 
206     }
207 
208     return cells;
209   }
210 
211   /**
212    * Set the component to the cells grid.
213    * Also mark the span-area as occupied.
214    *
215    * @param initialCells The cells area
216    * @param column       Zero based column position
217    * @param row          Zero based row position
218    * @param component    Component to set
219    * @return The result cells, possibly the initial array or a new array
220    */
221   private UIComponent[][] set(
222       final UIComponent[][] initialCells, final Integer column, final Integer row, final UIComponent component) {
223 
224     UIComponent[][] cells = initialCells;
225 
226     final Map<String, Object> attributes = component.getAttributes();
227     int rowSpan = ComponentUtils.getIntAttribute(component, Attributes.rowSpan,
228         LabelLayout.isGridTop(component) || LabelLayout.isGridBottom(component) ? 2 : 1);
229     ComponentUtils.setAttribute(component, Attributes.rowSpan, rowSpan);
230     int columnSpan = ComponentUtils.getIntAttribute(component, Attributes.columnSpan,
231         LabelLayout.isGridLeft(component) || LabelLayout.isGridRight(component) ? 2 : 1);
232     ComponentUtils.setAttribute(component, Attributes.columnSpan, columnSpan);
233 
234     // the span area
235     for (int j = row; j < rowSpan + row; j++) {
236       for (int i = column; i < columnSpan + column; i++) {
237         if (i >= cells[0].length) {
238           LOG.warn("column {} + columnSpan {} - 1 >= columnsCount {} in component '{}'!",
239               column + 1, columnSpan, cells[0].length, component.getClientId(FacesContext.getCurrentInstance()));
240           break;
241         }
242         if (j >= cells.length) {
243           cells = expand(cells, j + STEP);
244         }
245         if (j == row && i == column) { // position
246           cells[j][i] = component;
247           attributes.put(Attributes.gridRow.getName(), j + 1);
248           attributes.put(Attributes.gridColumn.getName(), i + 1);
249         } else {
250           cells[j][i] = SPAN;
251         }
252       }
253     }
254 
255     return cells;
256   }
257 
258   /**
259    * @deprecated since 4.3.0, please use {@link #expand(UIComponent[][], int)}
260    */
261   @Deprecated
262   protected UIComponent[][] expand(final UIComponent[][] cells, final Integer minRows, final int step) {
263     return expand(cells, minRows);
264   }
265 
266   protected UIComponent[][] expand(final UIComponent[][] cells, final int rows) {
267     final int columns = cells[0].length;
268 
269     final UIComponent[][] result = new UIComponent[rows][columns];
270     for (int j = 0; j < cells.length; j++) {
271       System.arraycopy(cells[j], 0, result[j], 0, columns);
272     }
273     return result;
274   }
275 
276 }