/* * Copyright 2008 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.client.ui; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.safehtml.client.HasSafeHtml; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; /** * CustomButton is a base button class with built in support for a set number * of button faces. * * Each face has its own style modifier. For example, the state for down and * hovering is assigned the CSS modifier <i>down-hovering</i>. So, if the * button's overall style name is <i>gwt-PushButton</i> then when showing the * <code>down-hovering</code> face, the button's style is <i> * gwt-PushButton-down-hovering</i>. The overall style name can be used to * change the style of the button irrespective of the current face. * * <p> * Each button face can be assigned is own image, text, or html contents. If no * content is defined for a face, then the face will use the contents of another * face. For example, if <code>down-hovering</code> does not have defined * contents, it will use the contents defined by the <code>down</code> face. * </p> * * <p> * The supported faces are defined below: * </p> * <p> * <table border="4"> * <tr> * * <td><b>CSS style name</b></td> * <td><b>Getter method</b></td> * <td><b>description of face</b></td> * <td><b>defaults to contents of face</b></td> * </tr> * * <tr> * <td>up</td> * <td> {@link #getUpFace()}</td> * <td>face shown when button is up</td> * <td>none</td> * </tr> * * <tr> * <td>down</td> * <td> {@link #getDownFace()}</td> * <td>face shown when button is down</td> * <td>up</td> * </tr> * * <tr> * <td>up-hovering</td> * <td> {@link #getUpHoveringFace()}</td> * <td>face shown when button is up and hovering</td> * <td>up</td> * </tr> * * <tr> * <td>up-disabled</td> * <td> {@link #getUpDisabledFace()}</td> * <td>face shown when button is up and disabled</td> * <td>up</td> * </tr> * * <tr> * <td>down-hovering</td> * <td> {@link #getDownHoveringFace()}</td> * <td>face shown when button is down and hovering</td> * <td>down</td> * </tr> * * <tr> * <td>down-disabled</td> * <td> {@link #getDownDisabledFace()}</td> * <td>face shown when button is down and disabled</td> * <td>down</td> * </tr> * </table> * </p> * * <h3>Use in UiBinder Templates</h3> * * When working with CustomButton subclasses in * {@link com.google.gwt.uibinder.client.UiBinder UiBinder} templates, you * can set text and assign ImageResources for their various faces via * child elements: * <p> * <dl> * <dt><g:upFace> * <dt><g:downFace> * <dt><g:upHoveringFace> * <dt><g:downHoveringFace> * <dt><g:upDisabledFace> * <dt><g:downDisabledFace> * </dl> * * Each face element can take an optional <code>image</code> attribute * and an html body. For example:<pre> * <ui:image field='downButton'/> <!-- define an {@link com.google.gwt.resources.client.ImageResource ImageResource} --> * * <g:PushButton ui:field='pushButton' enabled='true'> * <g:upFace> * <b>click me</b> * </gwt:upFace> * <g:upHoveringFace> * <b>Click ME!</b> * </gwt:upHoveringFace> * * <g:downFace image='{downButton}'/> * <g:downHoveringFace image='{downButton}'/> * </g:PushButton> * </pre> */ public abstract class CustomButton extends ButtonBase { /** * Represents a button's face. Each face is associated with its own style * modifier and, optionally, its own contents html, text, or image. */ public abstract class Face implements HasHTML, HasSafeHtml { private static final String STYLENAME_HTML_FACE = "html-face"; private final Face delegateTo; private Element face; /** * Constructor for <code>Face</code>. Creates a new face that delegates to * the supplied face. * * @param delegateTo default content provider */ private Face(Face delegateTo) { this.delegateTo = delegateTo; } /** * Gets the face's contents as html. * * @return face's contents as html * */ public String getHTML() { return DOM.getInnerHTML(getFace()); } /** * Gets the face's contents as text. * * @return face's contents as text * */ public String getText() { return DOM.getInnerText(getFace()); } /** * Set the face's contents as html. * * @param html html to set as face's contents html */ public void setHTML(SafeHtml html) { setHTML(html.asString()); } /** * Set the face's contents as html. * * @param html html to set as face's contents html * */ public void setHTML(String html) { face = DOM.createDiv(); UIObject.setStyleName(face, STYLENAME_HTML_FACE, true); DOM.setInnerHTML(face, html); updateButtonFace(); } /** * Set the face's contents as an image. * * @param image image to set as face contents */ public final void setImage(Image image) { face = image.getElement(); updateButtonFace(); } /** * Sets the face's contents as text. * * @param text text to set as face's contents */ public final void setText(String text) { face = DOM.createDiv(); UIObject.setStyleName(face, STYLENAME_HTML_FACE, true); DOM.setInnerText(face, text); updateButtonFace(); } @Override public final String toString() { return this.getName(); } /** * Gets the ID associated with this face. This will be a bitwise and of all * of the attributes that comprise this face. */ abstract int getFaceID(); /** * Get the name of the face. This property is also used as a modifier on the * <code>CustomButton</code> style. <p/> For instance, if the * <code>CustomButton</code> style is "gwt-PushButton" and the face name is * "up", then the CSS class name will be "gwt-PushButton-up". * * @return the face's name */ abstract String getName(); /** * Gets the contents associated with this face. */ private Element getFace() { if (face == null) { if (delegateTo == null) { // provide a default face as none was supplied. face = DOM.createDiv(); return face; } else { return delegateTo.getFace(); } } else { return face; } } private void updateButtonFace() { if (curFace != null && curFace.getFace() == this.getFace()) { setCurrentFaceElement(face); } } } private static final String STYLENAME_DEFAULT = "gwt-CustomButton"; /** * Pressed Attribute bit. */ private static final int DOWN_ATTRIBUTE = 1; /** * Hovering Attribute bit. */ private static final int HOVERING_ATTRIBUTE = 2; /** * Disabled Attribute bit. */ private static final int DISABLED_ATTRIBUTE = 4; /** * ID for up face. */ private static final int UP = 0; /** * ID for down face. */ private static final int DOWN = DOWN_ATTRIBUTE; /** * ID for upHovering face. */ private static final int UP_HOVERING = HOVERING_ATTRIBUTE; /** * ID for downHovering face. */ private static final int DOWN_HOVERING = DOWN_ATTRIBUTE | HOVERING_ATTRIBUTE; /** * ID for upDisabled face. */ private static final int UP_DISABLED = DISABLED_ATTRIBUTE; /** * ID for downDisabled face. */ private static final int DOWN_DISABLED = DOWN | DISABLED_ATTRIBUTE; /** * The button's current face element. */ private Element curFaceElement; /** * The button's current face. */ private Face curFace; /** * Face for up. */ private Face up; /** * Face for down. */ private Face down; /** * Face for downHover. */ private Face downHovering; /** * Face for upHover. */ private Face upHovering; /** * Face for upDisabled. */ private Face upDisabled; /** * Face for downDisabled. */ private Face downDisabled; /** * If <code>true</code>, this widget is capturing with the mouse held down. */ private boolean isCapturing; /** * If <code>true</code>, this widget has focus with the space bar down. */ private boolean isFocusing; /** * Used to decide whether to allow clicks to propagate up to the superclass * or container elements. */ private boolean allowClick; /** * Constructor for <code>CustomButton</code>. * * @param upImage image for the default (up) face of the button */ public CustomButton(Image upImage) { this(); getUpFace().setImage(upImage); } /** * Constructor for <code>CustomButton</code>. * * @param upImage image for the default (up) face of the button * @param handler the click handler */ public CustomButton(Image upImage, ClickHandler handler) { this(upImage); addClickHandler(handler); } /** * Constructor for <code>CustomButton</code>. * * @param upImage image for the default (up) face of the button * @param listener the click listener * @deprecated Use {@link #CustomButton(Image, ClickHandler)} instead */ @Deprecated public CustomButton(Image upImage, ClickListener listener) { this(upImage); addClickListener(listener); } /** * Constructor for <code>CustomButton</code>. * * @param upImage image for the default (up) face of the button * @param downImage image for the down face of the button */ public CustomButton(Image upImage, Image downImage) { this(upImage); getDownFace().setImage(downImage); } /** * Constructor for <code>CustomButton</code>. * * @param upImage image for the default (up) face of the button * @param downImage image for the down face of the button * @param handler clickListener */ public CustomButton(Image upImage, Image downImage, ClickHandler handler) { this(upImage, handler); getDownFace().setImage(downImage); } /** * Constructor for <code>CustomButton</code>. * * @param upImage image for the default (up) face of the button * @param downImage image for the down face of the button * @param listener clickListener * @deprecated Use {@link #CustomButton(Image, Image, ClickHandler)} instead */ @Deprecated public CustomButton(Image upImage, Image downImage, ClickListener listener) { this(upImage, listener); getDownFace().setImage(downImage); } /** * Constructor for <code>CustomButton</code>. * * @param upText the text for the default (up) face of the button */ public CustomButton(String upText) { this(); getUpFace().setText(upText); } /** * Constructor for <code>CustomButton</code>. * * @param upText the text for the default (up) face of the button * @param handler the click handler */ public CustomButton(String upText, ClickHandler handler) { this(upText); addClickHandler(handler); } /** * Constructor for <code>CustomButton</code>. * * @param upText the text for the default (up) face of the button * @param listener the click listener * @deprecated Use {@link #CustomButton(String, ClickListener)} instead */ @Deprecated public CustomButton(String upText, ClickListener listener) { this(upText); addClickListener(listener); } /** * Constructor for <code>CustomButton</code>. * * @param upText the text for the default (up) face of the button * @param downText the text for the down face of the button */ public CustomButton(String upText, String downText) { this(upText); getDownFace().setText(downText); } /** * Constructor for <code>CustomButton</code>. * * @param upText the text for the default (up) face of the button * @param downText the text for the down face of the button * @param handler the click handler */ public CustomButton(String upText, String downText, ClickHandler handler) { this(upText, downText); addClickHandler(handler); } /** * Constructor for <code>CustomButton</code>. * * @param upText the text for the default (up) face of the button * @param downText the text for the down face of the button * @param listener the click listener * @deprecated Use {@link #CustomButton(String, String, ClickHandler)} instead */ @Deprecated public CustomButton(String upText, String downText, ClickListener listener) { this(upText, downText); addClickListener(listener); } /** * Constructor for <code>CustomButton</code>. */ protected CustomButton() { // Use FocusPanel.impl rather than FocusWidget because only FocusPanel.impl // works across browsers to create a focusable element. super(FocusPanel.impl.createFocusable()); sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.FOCUSEVENTS | Event.KEYEVENTS); setUpFace(createFace(null, "up", UP)); setStyleName(STYLENAME_DEFAULT); // Add a11y role "button" Accessibility.setRole(getElement(), Accessibility.ROLE_BUTTON); } /** * Gets the downDisabled face of the button. * * @return the downDisabled face */ public final Face getDownDisabledFace() { if (downDisabled == null) { setDownDisabledFace(createFace(getDownFace(), "down-disabled", DOWN_DISABLED)); } return downDisabled; } /** * Gets the down face of the button. * * @return the down face */ public final Face getDownFace() { if (down == null) { setDownFace(createFace(getUpFace(), "down", DOWN)); } return down; } /** * Gets the downHovering face of the button. * * @return the downHovering face */ public final Face getDownHoveringFace() { if (downHovering == null) { setDownHoveringFace(createFace(getDownFace(), "down-hovering", DOWN_HOVERING)); } return downHovering; } /** * Gets the current face's html. * * @return current face's html */ @Override public String getHTML() { return getCurrentFace().getHTML(); } @Override public int getTabIndex() { return FocusPanel.impl.getTabIndex(getElement()); } /** * Gets the current face's text. * * @return current face's text */ @Override public String getText() { return getCurrentFace().getText(); } /** * Gets the upDisabled face of the button. * * @return the upDisabled face */ public final Face getUpDisabledFace() { if (upDisabled == null) { setUpDisabledFace(createFace(getUpFace(), "up-disabled", UP_DISABLED)); } return upDisabled; } /** * Gets the up face of the button. * * @return the up face */ public final Face getUpFace() { return up; } /** * Gets the upHovering face of the button. * * @return the upHovering face */ public final Face getUpHoveringFace() { if (upHovering == null) { setUpHoveringFace(createFace(getUpFace(), "up-hovering", UP_HOVERING)); } return upHovering; } @Override public void onBrowserEvent(Event event) { // Should not act on button if disabled. if (isEnabled() == false) { // This can happen when events are bubbled up from non-disabled children return; } int type = DOM.eventGetType(event); switch (type) { case Event.ONCLICK: // If clicks are currently disallowed, keep it from bubbling or being // passed to the superclass. if (!allowClick) { event.stopPropagation(); return; } break; case Event.ONMOUSEDOWN: if (event.getButton() == Event.BUTTON_LEFT) { setFocus(true); onClickStart(); DOM.setCapture(getElement()); isCapturing = true; // Prevent dragging (on some browsers); DOM.eventPreventDefault(event); } break; case Event.ONMOUSEUP: if (isCapturing) { isCapturing = false; DOM.releaseCapture(getElement()); if (isHovering() && event.getButton() == Event.BUTTON_LEFT) { onClick(); } } break; case Event.ONMOUSEMOVE: if (isCapturing) { // Prevent dragging (on other browsers); DOM.eventPreventDefault(event); } break; case Event.ONMOUSEOUT: Element to = DOM.eventGetToElement(event); if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event)) && (to == null || !DOM.isOrHasChild(getElement(), to))) { if (isCapturing) { onClickCancel(); } setHovering(false); } break; case Event.ONMOUSEOVER: if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) { setHovering(true); if (isCapturing) { onClickStart(); } } break; case Event.ONBLUR: if (isFocusing) { isFocusing = false; onClickCancel(); } break; case Event.ONLOSECAPTURE: if (isCapturing) { isCapturing = false; onClickCancel(); } break; } super.onBrowserEvent(event); // Synthesize clicks based on keyboard events AFTER the normal key handling. if ((event.getTypeInt() & Event.KEYEVENTS) != 0) { char keyCode = (char) DOM.eventGetKeyCode(event); switch (type) { case Event.ONKEYDOWN: if (keyCode == ' ') { isFocusing = true; onClickStart(); } break; case Event.ONKEYUP: if (isFocusing && keyCode == ' ') { isFocusing = false; onClick(); } break; case Event.ONKEYPRESS: if (keyCode == '\n' || keyCode == '\r') { onClickStart(); onClick(); } break; } } } @Override public void setAccessKey(char key) { FocusPanel.impl.setAccessKey(getElement(), key); } /** * Sets whether this button is enabled. * * @param enabled <code>true</code> to enable the button, <code>false</code> * to disable it */ @Override public final void setEnabled(boolean enabled) { if (isEnabled() != enabled) { toggleDisabled(); super.setEnabled(enabled); if (!enabled) { cleanupCaptureState(); Accessibility.removeState(getElement(), Accessibility.STATE_PRESSED); } else { setAriaPressed(getCurrentFace()); } } } @Override public void setFocus(boolean focused) { if (focused) { FocusPanel.impl.focus(getElement()); } else { FocusPanel.impl.blur(getElement()); } } @Override public void setHTML(SafeHtml html) { setHTML(html.asString()); } /** * Sets the current face's html. * * @param html html to set */ @Override public void setHTML(String html) { getCurrentFace().setHTML(html); } @Override public void setTabIndex(int index) { FocusPanel.impl.setTabIndex(getElement(), index); } /** * Sets the current face's text. * * @param text text to set */ @Override public void setText(String text) { getCurrentFace().setText(text); } /** * Is this button down? * * @return <code>true</code> if the button is down */ protected boolean isDown() { return (DOWN_ATTRIBUTE & getCurrentFace().getFaceID()) > 0; } /** * Overridden on attach to ensure that a button face has been chosen before * the button is displayed. */ @Override protected void onAttach() { finishSetup(); super.onAttach(); } /** * Called when the user finishes clicking on this button. The default behavior * is to fire the click event to listeners. Subclasses that override * {@link #onClickStart()} should override this method to restore the normal * widget display. */ protected void onClick() { // Allow the click we're about to synthesize to pass through to the // superclass and containing elements. Element.dispatchEvent() is // synchronous, so we simply set and clear the flag within this method. allowClick = true; // Mouse coordinates are not always available (e.g., when the click is // caused by a keyboard event). NativeEvent evt = Document.get().createClickEvent(1, 0, 0, 0, 0, false, false, false, false); getElement().dispatchEvent(evt); allowClick = false; } /** * Called when the user aborts a click in progress; for example, by dragging * the mouse outside of the button before releasing the mouse button. * Subclasses that override {@link #onClickStart()} should override this * method to restore the normal widget display. */ protected void onClickCancel() { } /** * Called when the user begins to click on this button. Subclasses may * override this method to display the start of the click visually; such * subclasses should also override {@link #onClick()} and * {@link #onClickCancel()} to restore normal visual state. Each * <code>onClickStart</code> will eventually be followed by either * <code>onClick</code> or <code>onClickCancel</code>, depending on whether * the click is completed. */ protected void onClickStart() { } @Override protected void onDetach() { super.onDetach(); cleanupCaptureState(); setHovering(false); } /** * Sets whether this button is down. * * @param down <code>true</code> to press the button, <code>false</code> * otherwise */ protected void setDown(boolean down) { if (down != isDown()) { toggleDown(); } } /** * Common setup between constructors. */ void finishSetup() { if (curFace == null) { setCurrentFace(getUpFace()); } } void fireClickListeners(@SuppressWarnings("unused") Event nativeEvent) { // TODO(ecc) Once event triggering is committed, should fire a native click event instead. fireEvent(new ClickEvent() { }); } /** * Gets the current face of the button. * * @return the current face */ Face getCurrentFace() { /* * Implementation note: Package protected so we can use it when testing the * button. */ finishSetup(); return curFace; } /** * Is the mouse hovering over this button? * * @return <code>true</code> if the mouse is hovering */ final boolean isHovering() { return (HOVERING_ATTRIBUTE & getCurrentFace().getFaceID()) > 0; } void setCurrentFace(Face newFace) { /* * Implementation note: default access for testing. */ if (curFace != newFace) { if (curFace != null) { removeStyleDependentName(curFace.getName()); } curFace = newFace; setCurrentFaceElement(newFace.getFace()); addStyleDependentName(curFace.getName()); if (isEnabled()) { setAriaPressed(newFace); } } } /** * Sets whether this button is hovering. * * @param hovering is this button hovering? */ final void setHovering(boolean hovering) { if (hovering != isHovering()) { toggleHover(); } } /** * Toggle the up/down attribute. */ void toggleDown() { int newFaceID = getCurrentFace().getFaceID() ^ DOWN_ATTRIBUTE; setCurrentFace(newFaceID); } /** * Resets internal state if this button can no longer service events. This can * occur when the widget becomes detached or disabled. */ private void cleanupCaptureState() { if (isCapturing || isFocusing) { DOM.releaseCapture(getElement()); isCapturing = false; isFocusing = false; onClickCancel(); } } private Face createFace(Face delegateTo, final String name, final int faceID) { return new Face(delegateTo) { @Override public String getName() { return name; } @Override int getFaceID() { return faceID; } }; } private Face getFaceFromID(int id) { switch (id) { case DOWN: return getDownFace(); case UP: return getUpFace(); case DOWN_HOVERING: return getDownHoveringFace(); case UP_HOVERING: return getUpHoveringFace(); case UP_DISABLED: return getUpDisabledFace(); case DOWN_DISABLED: return getDownDisabledFace(); default: throw new IllegalStateException(id + " is not a known face id."); } } private void setAriaPressed(Face newFace) { boolean pressed = (newFace.getFaceID() & DOWN_ATTRIBUTE) == 1; Accessibility.setState(getElement(), Accessibility.STATE_PRESSED, pressed ? "true" : "false"); } /** * Sets the current face based on the faceID. * * @param faceID sets the new face of the button */ private void setCurrentFace(int faceID) { Face newFace = getFaceFromID(faceID); setCurrentFace(newFace); } private void setCurrentFaceElement(Element newFaceElement) { if (curFaceElement != newFaceElement) { if (curFaceElement != null) { DOM.removeChild(getElement(), curFaceElement); } curFaceElement = newFaceElement; DOM.appendChild(getElement(), curFaceElement); } } /** * Sets the downDisabled face of the button. * * @param downDisabled downDisabled face */ private void setDownDisabledFace(Face downDisabled) { this.downDisabled = downDisabled; } /** * Sets the down face of the button. * * @param down the down face */ private void setDownFace(Face down) { this.down = down; } /** * Sets the downHovering face of the button. * * @param downHovering hoverDown face */ private void setDownHoveringFace(Face downHovering) { this.downHovering = downHovering; } /** * Sets the upDisabled face of the button. * * @param upDisabled upDisabled face */ private void setUpDisabledFace(Face upDisabled) { this.upDisabled = upDisabled; } /** * Sets the up face of the button. * * @param up up face */ private void setUpFace(Face up) { this.up = up; } /** * Sets the upHovering face of the button. * * @param upHovering upHovering face */ private void setUpHoveringFace(Face upHovering) { this.upHovering = upHovering; } /** * Toggle the disabled attribute. */ private void toggleDisabled() { // Toggle disabled. int newFaceID = getCurrentFace().getFaceID() ^ DISABLED_ATTRIBUTE; // Remove hovering. newFaceID &= ~HOVERING_ATTRIBUTE; // Sets the current face. setCurrentFace(newFaceID); } /** * Toggle the hovering attribute. */ private void toggleHover() { // Toggle hovering. int newFaceID = getCurrentFace().getFaceID() ^ HOVERING_ATTRIBUTE; // Remove disabled. newFaceID &= ~DISABLED_ATTRIBUTE; setCurrentFace(newFaceID); } }