Fixes event handling for CustomButton and subclasses:
- Mouse works like for a normal button; capture+focus on down click, click fires on release, button reacts to mouse enter/leave while held
- Keyboard support works like IE buttons; space bar "holds" the button down with click on key up; enter is a click on keypress

Review by: bruce


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1169 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/CustomButton.java b/user/src/com/google/gwt/user/client/ui/CustomButton.java
index acda760..adfeb4e 100644
--- a/user/src/com/google/gwt/user/client/ui/CustomButton.java
+++ b/user/src/com/google/gwt/user/client/ui/CustomButton.java
@@ -304,6 +304,16 @@
   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;
+
+  /**
    * Constructor for <code>CustomButton</code>.
    * 
    * @param upImage image for the default (up) face of the button
@@ -505,14 +515,86 @@
 
     int type = DOM.eventGetType(event);
     switch (type) {
+      case Event.ONMOUSEDOWN:
+        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()) {
+            onClick();
+          }
+        }
+        break;
+      case Event.ONMOUSEMOVE:
+        if (isCapturing) {
+          // Prevent dragging (on other browsers);
+          DOM.eventPreventDefault(event);
+        }
+        break;
       case Event.ONMOUSEOUT:
-        setHovering(false);
+        if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) {
+          if (isCapturing) {
+            onClickCancel();
+          }
+          setHovering(false);
+        }
         break;
       case Event.ONMOUSEOVER:
-        setHovering(true);
+        if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) {
+          setHovering(true);
+          if (isCapturing) {
+            onClickStart();
+          }
+        }
+        break;
+      case Event.ONCLICK:
+        // we handle clicks ourselves
+        return;
+      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.
+    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;
+    }
   }
 
   public void setAccessKey(char key) {
@@ -529,6 +611,9 @@
     if (isEnabled() != enabled) {
       toggleDisabled();
       super.setEnabled(enabled);
+      if (!enabled) {
+        cleanupCaptureState();
+      }
     }
   }
 
@@ -581,6 +666,42 @@
   }
 
   /**
+   * 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() {
+    fireClickListeners();
+  }
+
+  /**
+   * 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() {
+  }
+
+  protected void onDetach() {
+    super.onDetach();
+    cleanupCaptureState();
+  }
+
+  /**
    * Sets whether this button is down.
    * 
    * @param down <code>true</code> to press the button, <code>false</code>
@@ -612,9 +733,7 @@
      * Implementation note: Package protected so we can use it when testing the
      * button.
      */
-
     finishSetup();
-
     return curFace;
   }
 
@@ -660,6 +779,19 @@
     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) {
 
diff --git a/user/src/com/google/gwt/user/client/ui/PushButton.java b/user/src/com/google/gwt/user/client/ui/PushButton.java
index 66a8a80..239bee0 100644
--- a/user/src/com/google/gwt/user/client/ui/PushButton.java
+++ b/user/src/com/google/gwt/user/client/ui/PushButton.java
@@ -16,9 +16,6 @@
 
 package com.google.gwt.user.client.ui;
 
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-
 /**
  * A normal push button with custom styling.
  * 
@@ -39,8 +36,6 @@
 
   private static final String STYLENAME_DEFAULT = "gwt-PushButton";
 
-  private boolean waitingForMouseUp = false;
-
   {
     setStyleName(STYLENAME_DEFAULT);
   }
@@ -62,6 +57,17 @@
   }
 
   /**
+   * Constructor for <code>PushButton</code>. The supplied image is used to
+   * construct the default face of the button.
+   * 
+   * @param upImage image for the default (up) face of the button
+   * @param listener the click listener
+   */
+  public PushButton(Image upImage, ClickListener listener) {
+    super(upImage, listener);
+  }
+
+  /**
    * Constructor for <code>PushButton</code>.
    * 
    * @param upImage image for the default(up) face of the button
@@ -79,18 +85,7 @@
    * @param listener clickListener
    */
   public PushButton(Image upImage, Image downImage, ClickListener listener) {
-    super(upImage, listener);
-  }
-
-  /**
-   * Constructor for <code>PushButton</code>. The supplied image is used to
-   * construct the default face of the button.
-   * 
-   * @param upImage image for the default (up) face of the button
-   * @param listener the click listener
-   */
-  public PushButton(Image upImage, ClickListener listener) {
-    super(upImage, listener);
+    super(upImage, downImage, listener);
   }
 
   /**
@@ -134,37 +129,17 @@
   public PushButton(String upText, String downText, ClickListener listener) {
     super(upText, downText, listener);
   }
-  public void onBrowserEvent(Event event) {
-    // Should not act on button if the button is disabled. This can happen
-    // because an event is bubbled up from a non-disabled interior component.
-    if (isEnabled() == false) {
-      return;
-    }
-    int type = DOM.eventGetType(event);
-    switch (type) {
-      case Event.ONMOUSEDOWN:
-        waitingForMouseUp = true;
-        setDown(true);
-        break;
-      case Event.ONCLICK:
-        // Must synthesize click events because when we have two separate face
-        // elements for up/down, no click events are generated.
-        return;
-      case Event.ONMOUSEUP:
-        if (waitingForMouseUp) {
-          fireClickListeners();
-        }
-        waitingForMouseUp = false;
-        setDown(false);
-        break;
-      case Event.ONMOUSEOUT:
-        setDown(false);
-        break;
-      case Event.ONMOUSEOVER:
-        if (waitingForMouseUp) {
-          setDown(true);
-        }
-    }
-    super.onBrowserEvent(event);
+
+  protected void onClick() {
+    setDown(false);
+    super.onClick();
+  }
+  
+  protected void onClickCancel() {
+    setDown(false);
+  }
+
+  protected void onClickStart() {
+    setDown(true);
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/ToggleButton.java b/user/src/com/google/gwt/user/client/ui/ToggleButton.java
index c86a2d2..a4b6551 100644
--- a/user/src/com/google/gwt/user/client/ui/ToggleButton.java
+++ b/user/src/com/google/gwt/user/client/ui/ToggleButton.java
@@ -16,9 +16,6 @@
 
 package com.google.gwt.user.client.ui;
 
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Event;
-
 /**
  * A <code>ToggleButton</code> is a stylish stateful button which allows the
  * user to toggle between <code>up</code> and <code>down</code> states.
@@ -61,6 +58,17 @@
   }
 
   /**
+   * Constructor for <code>ToggleButton</code>. The supplied image is used to
+   * construct the default face of the button.
+   * 
+   * @param upImage image for the default (up) face of the button
+   * @param listener the click listener
+   */
+  public ToggleButton(Image upImage, ClickListener listener) {
+    super(upImage, listener);
+  }
+
+  /**
    * Constructor for <code>ToggleButton</code>.
    * 
    * @param upImage image for the default(up) face of the button
@@ -82,17 +90,6 @@
   }
 
   /**
-   * Constructor for <code>ToggleButton</code>. The supplied image is used to
-   * construct the default face of the button.
-   * 
-   * @param upImage image for the default (up) face of the button
-   * @param listener the click listener
-   */
-  public ToggleButton(Image upImage, ClickListener listener) {
-    super(upImage, listener);
-  }
-
-  /**
    * Constructor for <code>ToggleButton</code>. The supplied text is used to
    * construct the default face of the button.
    * 
@@ -124,23 +121,17 @@
   }
 
   public boolean isDown() {
+    // Changes access to public.
     return super.isDown();
   }
-
-  public void onBrowserEvent(Event event) {
-    if (isEnabled() == false) {
-      return;
-    }
-    int type = DOM.eventGetType(event);
-    switch (type) {
-      case Event.ONCLICK:
-        toggleDown();
-        break;
-    }
-    super.onBrowserEvent(event);
+  
+  public void setDown(boolean down) {
+    // Changes access to public.
+    super.setDown(down);
   }
-
-  public void setDown(boolean isDown) {
-    super.setDown(isDown);
+  
+  protected void onClick() {
+    toggleDown();
+    super.onClick();
   }
 }