com.google.gwt.user.client.ui.CustomButton Maven / Gradle / Ivy
/*
* 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.aria.client.PressedValue;
import com.google.gwt.aria.client.Roles;
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 down-hovering. So, if the
* button's overall style name is gwt-PushButton then when showing the
* down-hovering
face, the button's style is
* gwt-PushButton-down-hovering. The overall style name can be used to
* change the style of the button irrespective of the current face.
*
*
* 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 down-hovering
does not have defined
* contents, it will use the contents defined by the down
face.
*
*
*
* The supported faces are defined below:
*
*
*
*
*
* CSS style name
* Getter method
* description of face
* defaults to contents of face
*
*
*
* up
* {@link #getUpFace()}
* face shown when button is up
* none
*
*
*
* down
* {@link #getDownFace()}
* face shown when button is down
* up
*
*
*
* up-hovering
* {@link #getUpHoveringFace()}
* face shown when button is up and hovering
* up
*
*
*
* up-disabled
* {@link #getUpDisabledFace()}
* face shown when button is up and disabled
* up
*
*
*
* down-hovering
* {@link #getDownHoveringFace()}
* face shown when button is down and hovering
* down
*
*
*
* down-disabled
* {@link #getDownDisabledFace()}
* face shown when button is down and disabled
* down
*
*
*
*
* Use in UiBinder Templates
*
* 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:
*
*
* - <g:upFace>
*
- <g:downFace>
*
- <g:upHoveringFace>
*
- <g:downHoveringFace>
*
- <g:upDisabledFace>
*
- <g:downDisabledFace>
*
*
* Each face element can take an optional image
attribute
* and an html body. For example:
* <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>
*
*/
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 Face
. 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
*
*/
@Override
public String getHTML() {
return DOM.getInnerHTML(getFace());
}
/**
* Gets the face's contents as text.
*
* @return face's contents as text
*
*/
@Override
public String getText() {
return DOM.getInnerText(getFace());
}
/**
* Set the face's contents as html.
*
* @param html html to set as face's contents html
*/
@Override
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
*
*/
@Override
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
*/
@Override
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
* CustomButton
style. For instance, if the
* CustomButton
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 true
, this widget is capturing with the mouse held down.
*/
private boolean isCapturing;
/**
* If true
, 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 CustomButton
.
*
* @param upImage image for the default (up) face of the button
*/
public CustomButton(Image upImage) {
this();
getUpFace().setImage(upImage);
}
/**
* Constructor for CustomButton
.
*
* @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 CustomButton
.
*
* @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 CustomButton
.
*
* @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 CustomButton
.
*
* @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 CustomButton
.
*
* @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 CustomButton
.
*
* @param upText the text for the default (up) face of the button
*/
public CustomButton(String upText) {
this();
getUpFace().setText(upText);
}
/**
* Constructor for CustomButton
.
*
* @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 CustomButton
.
*
* @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 CustomButton
.
*
* @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 CustomButton
.
*
* @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 CustomButton
.
*
* @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 CustomButton
.
*/
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"
Roles.getButtonRole().set(getElement());
}
/**
* 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 true
to enable the button, false
* to disable it
*/
@Override
public final void setEnabled(boolean enabled) {
if (isEnabled() != enabled) {
toggleDisabled();
super.setEnabled(enabled);
if (!enabled) {
cleanupCaptureState();
Roles.getButtonRole().removeAriaPressedState(getElement());
} 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 true
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
* onClickStart
will eventually be followed by either
* onClick
or onClickCancel
, 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 true
to press the button, false
* 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 true
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;
Roles.getButtonRole().setAriaPressedState(getElement(), PressedValue.of(pressed));
}
/**
* 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);
}
}