com.google.gwt.user.cellview.client.AbstractHeaderOrFooterBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.user.cellview.client.AbstractHeaderOrFooterBuilder.java

Source

/*
 * 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;
        }
    }
}