/* * 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.cell.client; import static com.google.gwt.dom.client.BrowserEvents.CLICK; import static com.google.gwt.dom.client.BrowserEvents.KEYDOWN; import static com.google.gwt.dom.client.BrowserEvents.MOUSEDOWN; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CommonResources; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.CssResource.ImportedWithPrefix; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.resources.client.ImageResource.ImageOptions; import com.google.gwt.resources.client.ImageResource.RepeatStyle; import com.google.gwt.safehtml.client.SafeHtmlTemplates; 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.text.shared.SafeHtmlRenderer; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Event.NativePreviewEvent; import com.google.gwt.user.client.Event.NativePreviewHandler; import com.google.gwt.user.client.ui.AbstractImagePrototype; import com.google.gwt.user.client.ui.HasEnabled; /** * Base class for button Cells. * * @param <C> the type that this Cell represents */ public class ButtonCellBase<C> extends AbstractCell<C> implements IsCollapsible, HasEnabled { /** * The appearance used to render this Cell. * * @param <C> the type that this Cell represents */ public interface Appearance<C> { /** * Called when the user pushes the button down. * * @param parent the parent Element */ void onPush(Element parent); /** * Called when the user releases the button from being pushed. * * @param parent the parent Element */ void onUnpush(Element parent); /** * Render the button and its contents. * * @param cell the cell that is being rendered * @param context the {@link Context} of the cell * @param value the value that generated the content * @param sb the {@link SafeHtmlBuilder} to render into */ void render(ButtonCellBase<C> cell, Context context, C value, SafeHtmlBuilder sb); /** * Explicitly focus/unfocus this cell. * * @param parent the parent element * @param focused whether this cell should take focus or release it */ void setFocus(Element parent, boolean focused); } /** * The decoration applied to the button. * * <dl> * <dt>DEFAULT</dt> * <dd>A general button used in any context.</dd> * <dt>PRIMARY</dt> * <dd>A primary button that stands out against other buttons.</dd> * <dt>NEGATIVE</dt> * <dd>A button that results in a negative action, such as delete or cancel</dd> * </dl> */ public static enum Decoration { DEFAULT, PRIMARY, NEGATIVE; } /** * The default implementation of the {@link Appearance}. * * @param <C> the type that this Cell represents */ public static class DefaultAppearance<C> implements Appearance<C> { /** * The resources used by this appearance. */ public interface Resources extends ClientBundle { /** * The background image applied to the button. */ @ImageOptions(repeatStyle = RepeatStyle.Horizontal, flipRtl = true) ImageResource buttonCellBaseBackground(); @Source(Style.DEFAULT_CSS) Style buttonCellBaseStyle(); } /** * The Styles used by this appearance. */ @ImportedWithPrefix("gwt-ButtonCellBase") public interface Style extends CssResource { String DEFAULT_CSS = "com/google/gwt/cell/client/ButtonCellBase.css"; /** * Applied to the button. */ String buttonCellBase(); /** * Applied to the button when it has a collapsed left side. */ String buttonCellBaseCollapseLeft(); /** * Applied to the button when it has a collapsed right side. */ String buttonCellBaseCollapseRight(); /** * Applied to default buttons. */ String buttonCellBaseDefault(); /** * Applied to negative buttons. */ String buttonCellBaseNegative(); /** * Applied to primary buttons. */ String buttonCellBasePrimary(); /** * Applied to the button when being pushed. */ String buttonCellBasePushing(); } /** * The templates used by this appearance. */ interface Template extends SafeHtmlTemplates { /** * Positions the icon next to the text. * * NOTE: zoom:0 is a workaround for an IE7 bug where the button contents * wrap even when they do not need to. */ @SafeHtmlTemplates.Template("<div class=\"{0}\"" + " style=\"position:relative;padding-{1}:{2}px;zoom:0;\">{3}{4}</div>") SafeHtml iconContentLayout(String classes, String iconDirection, int iconWidth, SafeHtml icon, SafeHtml cellContents); /** * The wrapper around the icon that aligns it vertically with the text. */ @SafeHtmlTemplates.Template("<div style=\"position:absolute;{0}:0px;top:50%;line-height:0px;" + "margin-top:-{1}px;\">{2}</div>") SafeHtml iconWrapper(String direction, int halfHeight, SafeHtml image); } private static final int DEFAULT_ICON_PADDING = 3; private static Resources defaultResources; private static Template template; private static Resources getDefaultResources() { if (defaultResources == null) { defaultResources = GWT.create(Resources.class); } return defaultResources; } private final String iconDirection = LocaleInfo.getCurrentLocale().isRTL() ? "right" : "left"; private SafeHtml iconSafeHtml = SafeHtmlUtils.EMPTY_SAFE_HTML; private ImageResource lastIcon; private final SafeHtmlRenderer<C> renderer; private final Style style; /** * Construct a new {@link DefaultAppearance} using the default styles. * * @param renderer the {@link SafeHtmlRenderer} used to render the contents */ public DefaultAppearance(SafeHtmlRenderer<C> renderer) { this(renderer, getDefaultResources()); } /** * Construct a new {@link DefaultAppearance} using the specified resources. * * @param renderer the {@link SafeHtmlRenderer} used to render the contents * @param resources the resources and styles to apply to the button */ public DefaultAppearance(SafeHtmlRenderer<C> renderer, Resources resources) { this.renderer = renderer; this.style = resources.buttonCellBaseStyle(); this.style.ensureInjected(); if (template == null) { template = GWT.create(Template.class); } } /** * Return the {@link SafeHtmlRenderer} used by this Appearance to render the * contents of the button. * * @return a {@link SafeHtmlRenderer} instance */ public SafeHtmlRenderer<C> getRenderer() { return renderer; } public void onPush(Element parent) { parent.getFirstChildElement().addClassName(style.buttonCellBasePushing()); } public void onUnpush(Element parent) { parent.getFirstChildElement().removeClassName(style.buttonCellBasePushing()); } public void render(ButtonCellBase<C> cell, Context context, C value, SafeHtmlBuilder sb) { // Determine the classes from the state of the button. SafeHtmlBuilder classes = new SafeHtmlBuilder(); classes.appendEscaped(style.buttonCellBase()); Decoration decoration = cell.getDecoration(); if (decoration == Decoration.PRIMARY) { classes.appendEscaped(" " + style.buttonCellBasePrimary()); } else if (decoration == Decoration.NEGATIVE) { classes.appendEscaped(" " + style.buttonCellBaseNegative()); } else { classes.appendEscaped(" " + style.buttonCellBaseDefault()); } if (cell.isCollapseLeft()) { classes.appendEscaped(" " + style.buttonCellBaseCollapseLeft()); } if (cell.isCollapseRight()) { classes.appendEscaped(" " + style.buttonCellBaseCollapseRight()); } // Update the cached HTML string used for the icon if the icon changes. ImageResource icon = cell.getIcon(); if (icon != lastIcon) { if (icon == null) { iconSafeHtml = SafeHtmlUtils.EMPTY_SAFE_HTML; } else { AbstractImagePrototype proto = AbstractImagePrototype.create(icon); SafeHtml iconOnly = SafeHtmlUtils.fromTrustedString(proto.getHTML()); int halfHeight = (int) Math.round(icon.getHeight() / 2.0); iconSafeHtml = template.iconWrapper(iconDirection, halfHeight, iconOnly); } } // Figure out the attributes. char accessKey = cell.getAccessKey(); StringBuilder attributes = new StringBuilder(); if (!cell.isEnabled()) { attributes.append("disabled=disabled "); } if (accessKey != 0) { attributes.append("accessKey=\"").append(SafeHtmlUtils.htmlEscape("" + accessKey)); attributes.append("\" "); } // Create the button. int iconWidth = (icon == null) ? 0 : icon.getWidth(); SafeHtml safeValue = renderer.render(value); SafeHtml content = template.iconContentLayout(CommonResources.getInlineBlockStyle(), iconDirection, iconWidth + DEFAULT_ICON_PADDING, iconSafeHtml, safeValue); int tabIndex = cell.getTabIndex(); StringBuilder openTag = new StringBuilder(); openTag.append("<button type=\"button\""); openTag.append(" class=\"" + classes.toSafeHtml().asString() + "\""); openTag.append(" tabindex=\"" + tabIndex + "\" "); openTag.append(attributes.toString()).append(">"); sb.appendHtmlConstant(openTag.toString()); sb.append(content); sb.appendHtmlConstant("</button>"); } public void setFocus(Element parent, boolean focused) { Element focusable = parent.getFirstChildElement().cast(); if (focused) { focusable.focus(); } else { focusable.blur(); } } } /** * The {@link NativePreviewHandler} used to unpush a button onmouseup, even if * the event doesn't occur over the button. */ private class UnpushHandler implements NativePreviewHandler { private final Element parent; private final HandlerRegistration reg; public UnpushHandler(Element parent) { this.parent = parent; this.reg = Event.addNativePreviewHandler(this); } public void onPreviewNativeEvent(NativePreviewEvent event) { if (BrowserEvents.MOUSEUP.equals(event.getNativeEvent().getType())) { // Unregister self. reg.removeHandler(); // Unpush the element. appearance.onUnpush(parent); } } } private char accessKey; private final Appearance<C> appearance; private Decoration decoration = Decoration.DEFAULT; private ImageResource icon; private boolean isCollapsedLeft; private boolean isCollapsedRight; private boolean isEnabled = true; /** * The tab index applied to the buttons. If the button is used in a list of * table, a tab index of -1 should be used so it doesn't interrupt the tab * sequence. */ private int tabIndex = -1; /** * Construct a new {@link ButtonCellBase} using the specified * {@link Appearance} to render the contents. * * @param appearance the appearance of the cell */ public ButtonCellBase(Appearance<C> appearance) { super(CLICK, KEYDOWN, MOUSEDOWN); this.appearance = appearance; } /** * Get the access key. * * @return the access key, or 0 if one is not defined */ public char getAccessKey() { return accessKey; } /** * Get the decoration style of the button. */ public Decoration getDecoration() { return decoration; } /** * Get the icon displayed next to the button text. * * @return the icon resource */ public ImageResource getIcon() { return icon; } /** * Return the tab index that is given to all rendered cells. * * @return the tab index */ public int getTabIndex() { return tabIndex; } public boolean isCollapseLeft() { return this.isCollapsedLeft; } public boolean isCollapseRight() { return this.isCollapsedRight; } public boolean isEnabled() { return isEnabled; } @Override public void onBrowserEvent(Context context, Element parent, C value, NativeEvent event, ValueUpdater<C> valueUpdater) { // Ignore all events if disabled. if (!isEnabled()) { return; } // Let AbstractCell handle the enter key. super.onBrowserEvent(context, parent, value, event, valueUpdater); // Ignore events that occur outside of the button element. Element target = event.getEventTarget().cast(); if (!parent.getFirstChildElement().isOrHasChild(target)) { return; } String eventType = event.getType(); if (CLICK.equals(eventType)) { // Click the button. onEnterKeyDown(context, parent, value, event, valueUpdater); } else if (MOUSEDOWN.equals(eventType)) { // Push. appearance.onPush(parent); /* * Add a preview handler to unpush the button onmouseup or onblur, even if * the event doesn't occur over the button. */ new UnpushHandler(parent); // We will be handling all user visual feedback manually. Prevent browser // doing default handling (i.e. double-click highlights button text). event.preventDefault(); } } @Override public void render(Context context, C value, SafeHtmlBuilder sb) { appearance.render(this, context, value, sb); } /** * Sets the cell's 'access key'. This key is used (in conjunction with a * browser-specific modifier key) to automatically focus the cell. * * <p> * The change takes effect the next time the Cell is rendered. * * @param key the cell's access key */ public void setAccessKey(char key) { this.accessKey = key; } /** * {@inheritDoc} * * <p> * The change takes effect the next time the Cell is rendered. */ public void setCollapseLeft(boolean isCollapsed) { this.isCollapsedLeft = isCollapsed; } /** * {@inheritDoc} * * <p> * The change takes effect the next time the Cell is rendered. */ public void setCollapseRight(boolean isCollapsed) { this.isCollapsedRight = isCollapsed; } /** * Set the {@link Decoration} of the button. * * <p> * This change takes effect the next time the cell is rendered. * * @param decoration the button decoration */ public void setDecoration(Decoration decoration) { this.decoration = decoration; } /** * {@inheritDoc} * * <p> * The change takes effect the next time the Cell is rendered. */ public void setEnabled(boolean isEnabled) { this.isEnabled = isEnabled; } /** * Explicitly focus/unfocus this cell. Only one UI component can have focus at * a time, and the component that does will receive all keyboard events. * * @param parent the parent element * @param focused whether this cell should take focus or release it */ public void setFocus(Element parent, boolean focused) { appearance.setFocus(parent, focused); } /** * Set the icon to display next to the button text. * * @param icon the icon resource, or null not to show an icon */ public void setIcon(ImageResource icon) { this.icon = icon; } /** * Set the tab index to apply to the button. By default, the tab index is set * to -1 so that the button does not interrupt the tab chain when in a table * or list. The change takes effect the next time the Cell is rendered. * * @param tabIndex the tab index */ public void setTabIndex(int tabIndex) { this.tabIndex = tabIndex; } @Override protected void onEnterKeyDown(Context context, Element parent, C value, NativeEvent event, ValueUpdater<C> valueUpdater) { if (valueUpdater != null) { valueUpdater.update(value); } } }