| /* |
| * Copyright 2011 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.google.gwt.user.cellview.client; |
| |
| import com.google.gwt.cell.client.Cell.Context; |
| import com.google.gwt.dom.builder.shared.DivBuilder; |
| import com.google.gwt.dom.builder.shared.ElementBuilderBase; |
| import com.google.gwt.dom.builder.shared.HtmlBuilderFactory; |
| import com.google.gwt.dom.builder.shared.HtmlTableSectionBuilder; |
| import com.google.gwt.dom.builder.shared.StylesBuilder; |
| import com.google.gwt.dom.builder.shared.TableRowBuilder; |
| import com.google.gwt.dom.builder.shared.TableSectionBuilder; |
| import com.google.gwt.dom.client.Document; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.dom.client.Style.Position; |
| import com.google.gwt.dom.client.Style.Unit; |
| import com.google.gwt.dom.client.TableRowElement; |
| import com.google.gwt.i18n.client.LocaleInfo; |
| import com.google.gwt.resources.client.ImageResource; |
| import com.google.gwt.safehtml.shared.SafeHtml; |
| import com.google.gwt.safehtml.shared.SafeHtmlBuilder; |
| import com.google.gwt.safehtml.shared.SafeHtmlUtils; |
| import com.google.gwt.user.client.ui.AbstractImagePrototype; |
| |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| /** |
| * Default implementation of {@link HeaderBuilder} that renders columns. |
| * |
| * @param <T> the data type of the table |
| */ |
| public abstract class AbstractHeaderOrFooterBuilder<T> implements HeaderBuilder<T>, |
| FooterBuilder<T> { |
| |
| /** |
| * A map that provides O(1) access to a value given the key, or to the key |
| * given the value. |
| */ |
| private static class TwoWayHashMap<K, V> { |
| private final Map<K, V> keyToValue = new HashMap<K, V>(); |
| private final Map<V, K> valueToKey = new HashMap<V, K>(); |
| |
| void clear() { |
| keyToValue.clear(); |
| valueToKey.clear(); |
| } |
| |
| K getKey(V value) { |
| return valueToKey.get(value); |
| } |
| |
| V getValue(K key) { |
| return keyToValue.get(key); |
| } |
| |
| void put(K key, V value) { |
| keyToValue.put(key, value); |
| valueToKey.put(value, key); |
| } |
| } |
| |
| /** |
| * The attribute used to indicate that an element contains a Column. |
| */ |
| private static final String COLUMN_ATTRIBUTE = "__gwt_column"; |
| |
| /** |
| * The attribute used to indicate that an element contains a header. |
| */ |
| private static final String HEADER_ATTRIBUTE = "__gwt_header"; |
| |
| /** |
| * The attribute used to specify the row index of a TR element in the header. |
| */ |
| private static final String ROW_ATTRIBUTE = "__gwt_header_row"; |
| |
| private static final int ICON_PADDING = 6; |
| |
| private final boolean isFooter; |
| private boolean isSortIconStartOfLine = true; |
| private final int sortAscIconHalfHeight; |
| private SafeHtml sortAscIconHtml; |
| private final int sortAscIconWidth; |
| private final int sortDescIconHalfHeight; |
| private SafeHtml sortDescIconHtml; |
| private final int sortDescIconWidth; |
| private final AbstractCellTable<T> table; |
| private int rowIndex; |
| |
| // The following fields are reset on every build. |
| private HtmlTableSectionBuilder section; |
| private final Map<String, Column<T, ?>> idToColumnMap = new HashMap<String, Column<T, ?>>(); |
| private final TwoWayHashMap<String, Header<?>> idToHeaderMap = |
| new TwoWayHashMap<String, Header<?>>(); |
| |
| /** |
| * Create a new DefaultHeaderBuilder for the header of footer section. |
| * |
| * @param table the table being built |
| * @param isFooter true if building the footer, false if the header |
| */ |
| public AbstractHeaderOrFooterBuilder(AbstractCellTable<T> table, boolean isFooter) { |
| this.isFooter = isFooter; |
| this.table = table; |
| |
| /* |
| * Cache the height and width of the sort icons. We do not cache the |
| * rendered image source so the compiler can optimize it out if the user |
| * overrides renderHeader and does not use the sort icon. |
| */ |
| ImageResource asc = table.getResources().sortAscending(); |
| ImageResource desc = table.getResources().sortDescending(); |
| if (asc != null) { |
| sortAscIconWidth = asc.getWidth() + ICON_PADDING; |
| sortAscIconHalfHeight = (int) Math.round(asc.getHeight() / 2.0); |
| } else { |
| sortAscIconWidth = 0; |
| sortAscIconHalfHeight = 0; |
| } |
| if (desc != null) { |
| sortDescIconWidth = desc.getWidth() + ICON_PADDING; |
| sortDescIconHalfHeight = (int) Math.round(desc.getHeight() / 2.0); |
| } else { |
| sortDescIconWidth = 0; |
| sortDescIconHalfHeight = 0; |
| } |
| } |
| |
| @Override |
| public final TableSectionBuilder buildFooter() { |
| if (!isFooter) { |
| throw new UnsupportedOperationException( |
| "Cannot build footer because this builder is designated to build a header"); |
| } |
| return buildHeaderOrFooter(); |
| } |
| |
| @Override |
| public final TableSectionBuilder buildHeader() { |
| if (isFooter) { |
| throw new UnsupportedOperationException( |
| "Cannot build header because this builder is designated to build a footer"); |
| } |
| return buildHeaderOrFooter(); |
| } |
| |
| @Override |
| public Column<T, ?> getColumn(Element elem) { |
| String cellId = getColumnId(elem); |
| return (cellId == null) ? null : idToColumnMap.get(cellId); |
| } |
| |
| @Override |
| public Header<?> getHeader(Element elem) { |
| String headerId = getHeaderId(elem); |
| return (headerId == null) ? null : idToHeaderMap.getValue(headerId); |
| } |
| |
| @Override |
| public int getRowIndex(TableRowElement row) { |
| return Integer.parseInt(row.getAttribute(ROW_ATTRIBUTE)); |
| } |
| |
| /** |
| * Check if this builder is building a header or footer table. |
| * |
| * @return true if a footer, false if a header |
| */ |
| public boolean isBuildingFooter() { |
| return isFooter; |
| } |
| |
| @Override |
| public boolean isColumn(Element elem) { |
| return getColumnId(elem) != null; |
| } |
| |
| @Override |
| public boolean isHeader(Element elem) { |
| return getHeaderId(elem) != null; |
| } |
| |
| /** |
| * Check if the icon is located at the start or end of the line. The start of |
| * the line refers to the left side in LTR mode and the right side in RTL |
| * mode. The default location is the start of the line. |
| */ |
| public boolean isSortIconStartOfLine() { |
| return isSortIconStartOfLine; |
| } |
| |
| /** |
| * Set the position of the sort icon to the start or end of the line. The |
| * start of the line refers to the left side in LTR mode and the right side in |
| * RTL mode. The default location is the start of the line. |
| */ |
| public void setSortIconStartOfLine(boolean isStartOfLine) { |
| this.isSortIconStartOfLine = isStartOfLine; |
| } |
| |
| /** |
| * Implementation that builds the header or footer using the convenience |
| * methods in this class. |
| * |
| * @return true if the header contains content, false if empty |
| */ |
| protected abstract boolean buildHeaderOrFooterImpl(); |
| |
| /** |
| * Enables column-specific event handling for the specified element. If a |
| * column is sortable, then clicking on the element or a child of the element |
| * will trigger a sort event. |
| * |
| * @param builder the builder to associate with the column. The builder should |
| * be a child element of a row returned by {@link #startRow} and must |
| * be in a state where an attribute can be added. |
| * @param column the column to associate |
| */ |
| protected final void enableColumnHandlers(ElementBuilderBase<?> builder, Column<T, ?> column) { |
| String columnId = "column-" + Document.get().createUniqueId(); |
| idToColumnMap.put(columnId, column); |
| builder.attribute(COLUMN_ATTRIBUTE, columnId); |
| } |
| |
| /** |
| * Get the header or footer at the specified index. |
| * |
| * @param index the column index of the header |
| * @return the header or footer, depending on the value of isFooter |
| */ |
| protected final Header<?> getHeader(int index) { |
| return isFooter ? getTable().getFooter(index) : getTable().getHeader(index); |
| } |
| |
| protected AbstractCellTable<T> getTable() { |
| return table; |
| } |
| |
| /** |
| * Renders a given Header into a given ElementBuilderBase. This method ensures |
| * that the CellTable widget will handle events events originating in the |
| * Header. |
| * |
| * @param <H> the data type of the header |
| * @param out the {@link ElementBuilderBase} to render into. The builder |
| * should be a child element of a row returned by {@link #startRow} |
| * and must be in a state that allows both attributes and elements to |
| * be added |
| * @param context the {@link Context} of the header being rendered |
| * @param header the {@link Header} to render |
| */ |
| protected final <H> void renderHeader(ElementBuilderBase<?> out, Context context, Header<H> header) { |
| // Generate a unique ID for the header. |
| String headerId = idToHeaderMap.getKey(header); |
| if (headerId == null) { |
| headerId = "header-" + Document.get().createUniqueId(); |
| idToHeaderMap.put(headerId, header); |
| } |
| out.attribute(HEADER_ATTRIBUTE, headerId); |
| |
| // Render the cell into the builder. |
| SafeHtmlBuilder sb = new SafeHtmlBuilder(); |
| header.render(context, sb); |
| out.html(sb.toSafeHtml()); |
| } |
| |
| /** |
| * Render a header, including a sort icon if the column is sortable and |
| * sorted. |
| * |
| * @param out the builder to render into |
| * @param header the header to render |
| * @param context the context of the header |
| * @param isSorted true if the column is sorted |
| * @param isSortAscending indicated the sort order, if sorted |
| */ |
| protected final void renderSortableHeader(ElementBuilderBase<?> out, Context context, |
| Header<?> header, boolean isSorted, boolean isSortAscending) { |
| ElementBuilderBase<?> headerContainer = out; |
| |
| // Wrap the header in a sort icon if sorted. |
| isSorted = isSorted && !isFooter; |
| if (isSorted) { |
| // Determine the position of the sort icon. |
| boolean posRight = |
| LocaleInfo.getCurrentLocale().isRTL() ? isSortIconStartOfLine : !isSortIconStartOfLine; |
| |
| // Create an outer container to hold the icon and the header. |
| int iconWidth = isSortAscending ? sortAscIconWidth : sortDescIconWidth; |
| int halfHeight = isSortAscending ? sortAscIconHalfHeight : sortDescIconHalfHeight; |
| DivBuilder outerDiv = out.startDiv(); |
| StylesBuilder style = |
| outerDiv.style().position(Position.RELATIVE).trustedProperty("zoom", "1"); |
| if (posRight) { |
| style.paddingRight(iconWidth, Unit.PX); |
| } else { |
| style.paddingLeft(iconWidth, Unit.PX); |
| } |
| style.endStyle(); |
| |
| // Add the icon. |
| DivBuilder imageHolder = outerDiv.startDiv(); |
| style = |
| outerDiv.style().position(Position.ABSOLUTE).top(50.0, Unit.PCT).lineHeight(0.0, Unit.PX) |
| .marginTop(-halfHeight, Unit.PX); |
| if (posRight) { |
| style.right(0, Unit.PX); |
| } else { |
| style.left(0, Unit.PX); |
| } |
| |
| style.endStyle(); |
| imageHolder.html(getSortIcon(isSortAscending)); |
| imageHolder.endDiv(); |
| |
| // Create the header wrapper. |
| headerContainer = outerDiv.startDiv(); |
| } |
| |
| // Build the header. |
| renderHeader(headerContainer, context, header); |
| |
| // Close the elements used for the sort icon. |
| if (isSorted) { |
| headerContainer.endDiv(); // headerContainer. |
| headerContainer.endDiv(); // outerDiv |
| } |
| } |
| |
| /** |
| * Add a header (or footer) row to the table, below any rows previously added. |
| * |
| * @return the row to add |
| */ |
| protected final TableRowBuilder startRow() { |
| // End any dangling rows. |
| while (section.getDepth() > 1) { |
| section.end(); |
| } |
| |
| // Verify the depth. |
| if (section.getDepth() < 1) { |
| throw new IllegalStateException( |
| "Cannot start a row. Did you call TableRowBuilder.end() too many times?"); |
| } |
| |
| // Start the next row. |
| TableRowBuilder row = section.startTR(); |
| row.attribute(ROW_ATTRIBUTE, rowIndex); |
| rowIndex++; |
| return row; |
| } |
| |
| private TableSectionBuilder buildHeaderOrFooter() { |
| // Reset the state of the header. |
| section = |
| isFooter ? HtmlBuilderFactory.get().createTFootBuilder() : HtmlBuilderFactory.get() |
| .createTHeadBuilder(); |
| idToHeaderMap.clear(); |
| idToColumnMap.clear(); |
| rowIndex = 0; |
| |
| // Build the header. |
| if (!buildHeaderOrFooterImpl()) { |
| // The header is empty. |
| return null; |
| } |
| |
| // End dangling elements. |
| while (section.getDepth() > 0) { |
| section.end(); |
| } |
| |
| // Return the section. |
| return section; |
| } |
| |
| /** |
| * Check if an element is the parent of a rendered header. |
| * |
| * @param elem the element to check |
| * @return the id if a header parent, null if not |
| */ |
| private String getColumnId(Element elem) { |
| return getElementAttribute(elem, COLUMN_ATTRIBUTE); |
| } |
| |
| private String getElementAttribute(Element elem, String attribute) { |
| if (elem == null) { |
| return null; |
| } |
| String value = elem.getAttribute(attribute); |
| return (value == null) || (value.length() == 0) ? null : value; |
| } |
| |
| /** |
| * Check if an element is the parent of a rendered header. |
| * |
| * @param elem the element to check |
| * @return the id if a header parent, null if not |
| */ |
| private String getHeaderId(Element elem) { |
| return getElementAttribute(elem, HEADER_ATTRIBUTE); |
| } |
| |
| /** |
| * Get the HTML representation of the sort icon. These are loaded lazily so |
| * the compiler has a chance to strip this method, and the icon source code, |
| * if the user overrides renderHeader. |
| * |
| * @param isAscending true for the ascending icon, false for descending |
| * @return the rendered HTML |
| */ |
| private SafeHtml getSortIcon(boolean isAscending) { |
| if (isAscending) { |
| if (sortAscIconHtml == null) { |
| AbstractImagePrototype proto = |
| AbstractImagePrototype.create(table.getResources().sortAscending()); |
| sortAscIconHtml = SafeHtmlUtils.fromTrustedString(proto.getHTML()); |
| } |
| return sortAscIconHtml; |
| } else { |
| if (sortDescIconHtml == null) { |
| AbstractImagePrototype proto = |
| AbstractImagePrototype.create(table.getResources().sortDescending()); |
| sortDescIconHtml = SafeHtmlUtils.fromTrustedString(proto.getHTML()); |
| } |
| return sortDescIconHtml; |
| } |
| } |
| } |