Bug 123836 - Implement indeterminate property on checkboxes and radio buttons - r=roc,jst sr=roc
authorMichael Ventnor <ventnor.bugzilla@gmail.com>
Thu, 22 Jan 2009 13:07:44 +1300
changeset 24022 41d6bf675366ffec5578ca76db6f296ddc6c04af
parent 24021 53a4746eb1bac470f323da7d1c0bf41ee6ccb33e
child 24023 64ff884c9127239cdc0a2ff39bbe20d2a3743e40
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersroc, jst, roc
bugs123836
milestone1.9.2a1pre
Bug 123836 - Implement indeterminate property on checkboxes and radio buttons - r=roc,jst sr=roc
content/html/content/src/nsHTMLInputElement.cpp
dom/public/idl/html/nsIDOMNSHTMLInputElement.idl
layout/forms/nsGfxCheckboxControlFrame.cpp
layout/forms/nsGfxCheckboxControlFrame.h
layout/reftests/forms/indeterminate-checked-notref.html
layout/reftests/forms/indeterminate-checked.html
layout/reftests/forms/indeterminate-unchecked-notref.html
layout/reftests/forms/indeterminate-unchecked.html
layout/reftests/forms/reftest.list
widget/src/gtk2/gtk2drawing.c
widget/src/gtk2/gtkdrawing.h
widget/src/gtk2/nsNativeThemeGTK.cpp
--- a/content/html/content/src/nsHTMLInputElement.cpp
+++ b/content/html/content/src/nsHTMLInputElement.cpp
@@ -124,29 +124,32 @@ static NS_DEFINE_CID(kXULControllersCID,
 #define BF_VALUE_CHANGED 2
 #define BF_CHECKED_CHANGED 3
 #define BF_CHECKED 4
 #define BF_HANDLING_SELECT_EVENT 5
 #define BF_SHOULD_INIT_CHECKED 6
 #define BF_PARSER_CREATING 7
 #define BF_IN_INTERNAL_ACTIVATE 8
 #define BF_CHECKED_IS_TOGGLED 9
+#define BF_INDETERMINATE 10
 
 #define GET_BOOLBIT(bitfield, field) (((bitfield) & (0x01 << (field))) \
                                         ? PR_TRUE : PR_FALSE)
 #define SET_BOOLBIT(bitfield, field, b) ((b) \
                                         ? ((bitfield) |=  (0x01 << (field))) \
                                         : ((bitfield) &= ~(0x01 << (field))))
 
 // First bits are needed for the control type.
 #define NS_OUTER_ACTIVATE_EVENT   (1 << 9)
 #define NS_ORIGINAL_CHECKED_VALUE (1 << 10)
 #define NS_NO_CONTENT_DISPATCH    (1 << 11)
+#define NS_ORIGINAL_INDETERMINATE_VALUE (1 << 12)
 #define NS_CONTROL_TYPE(bits)  ((bits) & ~( \
-  NS_OUTER_ACTIVATE_EVENT | NS_ORIGINAL_CHECKED_VALUE | NS_NO_CONTENT_DISPATCH))
+  NS_OUTER_ACTIVATE_EVENT | NS_ORIGINAL_CHECKED_VALUE | NS_NO_CONTENT_DISPATCH | \
+  NS_ORIGINAL_INDETERMINATE_VALUE))
 
 static const char kWhitespace[] = "\n\r\t\b";
 
 #define NS_INPUT_ELEMENT_STATE_IID                 \
 { /* dc3b3d14-23e2-4479-b513-7b369343e3a0 */       \
   0xdc3b3d14,                                      \
   0x23e2,                                          \
   0x4479,                                          \
@@ -749,16 +752,36 @@ nsHTMLInputElement::GetDefaultValue(nsAS
 
 NS_IMETHODIMP
 nsHTMLInputElement::SetDefaultValue(const nsAString& aValue)
 {
   return SetAttrHelper(nsGkAtoms::value, aValue);
 }
 
 NS_IMETHODIMP
+nsHTMLInputElement::GetIndeterminate(PRBool* aValue)
+{
+  *aValue = GET_BOOLBIT(mBitField, BF_INDETERMINATE);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsHTMLInputElement::SetIndeterminate(PRBool aValue)
+{
+  SET_BOOLBIT(mBitField, BF_INDETERMINATE, aValue);
+
+  // Repaint the frame
+  nsIFrame* frame = GetPrimaryFrame();
+  if (frame)
+    frame->InvalidateOverflowRect();
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsHTMLInputElement::GetSize(PRUint32* aValue)
 {
   const nsAttrValue* attrVal = mAttrsAndChildren.GetAttr(nsGkAtoms::size);
   if (attrVal && attrVal->Type() == nsAttrValue::eInteger) {
     *aValue = attrVal->GetIntegerValue();
   }
   else {
     *aValue = 0;
@@ -1524,16 +1547,22 @@ nsHTMLInputElement::PreHandleEvent(nsEve
   PRBool originalCheckedValue = PR_FALSE;
 
   if (outerActivateEvent) {
     SET_BOOLBIT(mBitField, BF_CHECKED_IS_TOGGLED, PR_FALSE);
 
     switch(mType) {
       case NS_FORM_INPUT_CHECKBOX:
         {
+          if (GET_BOOLBIT(mBitField, BF_INDETERMINATE)) {
+            // indeterminate is always set to FALSE when the checkbox is toggled
+            SET_BOOLBIT(mBitField, BF_INDETERMINATE, PR_FALSE);
+            aVisitor.mItemFlags |= NS_ORIGINAL_INDETERMINATE_VALUE;
+          }
+
           GetChecked(&originalCheckedValue);
           DoSetChecked(!originalCheckedValue);
           SET_BOOLBIT(mBitField, BF_CHECKED_IS_TOGGLED, PR_TRUE);
         }
         break;
 
       case NS_FORM_INPUT_RADIO:
         {
@@ -1674,16 +1703,19 @@ nsHTMLInputElement::PostHandleEvent(nsEv
       if (selectedRadioButton) {
         selectedRadioButton->SetChecked(PR_TRUE);
         // If this one is no longer a radio button we must reset it back to
         // false to cancel the action.  See how the web of hack grows?
         if (mType != NS_FORM_INPUT_RADIO) {
           DoSetChecked(PR_FALSE);
         }
       } else if (oldType == NS_FORM_INPUT_CHECKBOX) {
+        PRBool originalIndeterminateValue =
+          !!(aVisitor.mItemFlags & NS_ORIGINAL_INDETERMINATE_VALUE);
+        SET_BOOLBIT(mBitField, BF_INDETERMINATE, originalIndeterminateValue);
         DoSetChecked(originalCheckedValue);
       }
     } else {
       FireOnChange();
 #ifdef ACCESSIBILITY
       // Fire an event to notify accessibility
       if (mType == NS_FORM_INPUT_CHECKBOX) {
         FireEventForAccessibility(this, aVisitor.mPresContext,
--- a/dom/public/idl/html/nsIDOMNSHTMLInputElement.idl
+++ b/dom/public/idl/html/nsIDOMNSHTMLInputElement.idl
@@ -37,24 +37,26 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include "domstubs.idl"
 
 interface nsIControllers;
 interface nsIDOMFileList;
 
-[scriptable, uuid(df3dc133-d77a-482f-8364-8e40df978a33)]
+[scriptable, uuid(71e9ecc0-f2d0-422c-8601-430e7f17fa47)]
 interface nsIDOMNSHTMLInputElement : nsISupports
 {
 	readonly attribute nsIControllers   controllers;
 	
 	readonly attribute long             textLength;
 	
            attribute long             selectionStart;
            attribute long             selectionEnd;
 
 	readonly attribute nsIDOMFileList   files;
 
+           attribute boolean          indeterminate;
+
 	/* convenience */
   void                      setSelectionRange(in long selectionStart,
                                               in long selectionEnd);
 };
--- a/layout/forms/nsGfxCheckboxControlFrame.cpp
+++ b/layout/forms/nsGfxCheckboxControlFrame.cpp
@@ -41,16 +41,17 @@
 #include "nsCSSRendering.h"
 #ifdef ACCESSIBILITY
 #include "nsIAccessibilityService.h"
 #endif
 #include "nsIServiceManager.h"
 #include "nsIDOMHTMLInputElement.h"
 #include "nsDisplayList.h"
 #include "nsCSSAnonBoxes.h"
+#include "nsIDOMNSHTMLInputElement.h"
 
 static void
 PaintCheckMark(nsIRenderingContext& aRenderingContext,
                const nsRect& aRect)
 {
   // Points come from the coordinates on a 7X7 unit box centered at 0,0
   const PRInt32 checkPolygonX[] = { -3, -1,  3,  3, -1, -3 };
   const PRInt32 checkPolygonY[] = { -1,  1, -3, -1,  3,  1 };
@@ -69,16 +70,28 @@ PaintCheckMark(nsIRenderingContext& aRen
     paintPolygon[polyIndex] = paintCenter +
                               nsPoint(checkPolygonX[polyIndex] * paintScale,
                                       checkPolygonY[polyIndex] * paintScale);
   }
 
   aRenderingContext.FillPolygon(paintPolygon, checkNumPoints);
 }
 
+static void
+PaintIndeterminateMark(nsIRenderingContext& aRenderingContext,
+                       const nsRect& aRect)
+{
+  // Drawing a thin horizontal line in the middle of the rect.
+  nsRect fillRect = aRect;
+  fillRect.height /= 4;
+  fillRect.y += (aRect.height - fillRect.height) / 2;
+
+  aRenderingContext.FillRect(fillRect);
+}
+
 //------------------------------------------------------------
 nsIFrame*
 NS_NewGfxCheckboxControlFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
 {
   return new (aPresShell) nsGfxCheckboxControlFrame(aContext);
 }
 
 
@@ -204,30 +217,33 @@ nsGfxCheckboxControlFrame::PaintCheckBox
   // REVIEW: moved the mAppearance test out so we avoid constructing
   // a display item if it's not needed
   nsRect checkRect(aPt, mRect.Size());
   checkRect.Deflate(GetUsedBorderAndPadding());
 
   const nsStyleColor* color = GetStyleColor();
   aRenderingContext.SetColor(color->mColor);
 
-  PaintCheckMark(aRenderingContext, checkRect);
+  if (IsIndeterminate())
+    PaintIndeterminateMark(aRenderingContext, checkRect);
+  else
+    PaintCheckMark(aRenderingContext, checkRect);
 }
 
 //------------------------------------------------------------
 NS_IMETHODIMP
 nsGfxCheckboxControlFrame::BuildDisplayList(nsDisplayListBuilder*   aBuilder,
                                             const nsRect&           aDirtyRect,
                                             const nsDisplayListSet& aLists)
 {
   nsresult rv = nsFormControlFrame::BuildDisplayList(aBuilder, aDirtyRect, aLists);
   NS_ENSURE_SUCCESS(rv, rv);
   
   // Get current checked state through content model.
-  if (!GetCheckboxState() || !IsVisibleForPainting(aBuilder))
+  if ((!IsChecked() && !IsIndeterminate()) || !IsVisibleForPainting(aBuilder))
     return NS_OK;   // we're not checked or not visible, nothing to paint.
     
   if (IsThemed())
     return NS_OK; // No need to paint the checkmark. The theme will do it.
 
   // Paint the checkmark
   if (mCheckButtonFaceStyle) {
     // This code actually works now; not sure how useful it'll be
@@ -266,15 +282,24 @@ nsGfxCheckboxControlFrame::PaintCheckBox
                                         *myBorder, PR_FALSE);
   nsCSSRendering::PaintBorder(PresContext(), aRenderingContext, this,
                               aDirtyRect, rect, *myBorder,
                               mCheckButtonFaceStyle);
 }
 
 //------------------------------------------------------------
 PRBool
-nsGfxCheckboxControlFrame::GetCheckboxState ( )
+nsGfxCheckboxControlFrame::IsChecked()
 {
   nsCOMPtr<nsIDOMHTMLInputElement> elem(do_QueryInterface(mContent));
   PRBool retval = PR_FALSE;
   elem->GetChecked(&retval);
   return retval;
 }
+
+PRBool
+nsGfxCheckboxControlFrame::IsIndeterminate()
+{
+  nsCOMPtr<nsIDOMNSHTMLInputElement> elem(do_QueryInterface(mContent));
+  PRBool retval = PR_FALSE;
+  elem->GetIndeterminate(&retval);
+  return retval;
+}
--- a/layout/forms/nsGfxCheckboxControlFrame.h
+++ b/layout/forms/nsGfxCheckboxControlFrame.h
@@ -86,15 +86,16 @@ public:
   void PaintCheckBox(nsIRenderingContext& aRenderingContext,
                      nsPoint aPt, const nsRect& aDirtyRect);
 
   void PaintCheckBoxFromStyle(nsIRenderingContext& aRenderingContext,
                               nsPoint aPt, const nsRect& aDirtyRect);
 
 protected:
 
-  PRBool GetCheckboxState();
+  PRBool IsChecked();
+  PRBool IsIndeterminate();
 
   nsRefPtr<nsStyleContext> mCheckButtonFaceStyle;
 };
 
 #endif
 
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/indeterminate-checked-notref.html
@@ -0,0 +1,1 @@
+<input type="checkbox" checked style="-moz-appearance: none;">
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/indeterminate-checked.html
@@ -0,0 +1,1 @@
+<input type="checkbox" id="s" checked style="-moz-appearance: none;"><script>document.getElementById("s").indeterminate = true;</script>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/indeterminate-unchecked-notref.html
@@ -0,0 +1,1 @@
+<input type="checkbox" style="-moz-appearance: none;">
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/indeterminate-unchecked.html
@@ -0,0 +1,1 @@
+<input type="checkbox" id="s" style="-moz-appearance: none;"><script>document.getElementById("s").indeterminate = true;</script>
--- a/layout/reftests/forms/reftest.list
+++ b/layout/reftests/forms/reftest.list
@@ -1,7 +1,9 @@
 == checkbox-label-dynamic.html checkbox-label-dynamic-ref.html
 == checkbox-radio-stretched.html checkbox-radio-stretched-ref.html # bug 464589
 == input-file-width-clip-1.html input-file-width-clip-ref.html # bug 409587
 == input-text-size-1.html input-text-size-1-ref.html
 == input-text-size-2.html input-text-size-2-ref.html
 == radio-label-dynamic.html radio-label-dynamic-ref.html
 == out-of-bounds-selectedindex.html out-of-bounds-selectedindex-ref.html # bug 471741
+!= indeterminate-checked.html indeterminate-checked-notref.html
+!= indeterminate-unchecked.html indeterminate-unchecked-notref.html
--- a/widget/src/gtk2/gtk2drawing.c
+++ b/widget/src/gtk2/gtk2drawing.c
@@ -959,18 +959,18 @@ moz_gtk_button_get_inner_border(GtkWidge
         *inner_border = default_inner_border;
 
     return MOZ_GTK_SUCCESS;
 }
 
 static gint
 moz_gtk_toggle_paint(GdkDrawable* drawable, GdkRectangle* rect,
                      GdkRectangle* cliprect, GtkWidgetState* state,
-                     gboolean selected, gboolean isradio,
-                     GtkTextDirection direction)
+                     gboolean selected, gboolean inconsistent,
+                     gboolean isradio, GtkTextDirection direction)
 {
     GtkStateType state_type = ConvertGtkState(state);
     GtkShadowType shadow_type = (selected)?GTK_SHADOW_IN:GTK_SHADOW_OUT;
     gint indicator_size, indicator_spacing;
     gint x, y, width, height;
     gint focus_x, focus_y, focus_width, focus_height;
     GtkWidget *w;
     GtkStyle *style;
@@ -1012,16 +1012,27 @@ moz_gtk_toggle_paint(GdkDrawable* drawab
                          width, height);
         if (state->focused) {
             gtk_paint_focus(style, drawable, GTK_STATE_ACTIVE, cliprect,
                             gRadiobuttonWidget, "radiobutton", focus_x, focus_y,
                             focus_width, focus_height);
         }
     }
     else {
+       /*
+        * 'indeterminate' type on checkboxes. In GTK, the shadow type
+        * must also be changed for the state to be drawn.
+        */
+        if (inconsistent) {
+            gtk_toggle_button_set_inconsistent(GTK_TOGGLE_BUTTON(gCheckboxWidget), TRUE);
+            shadow_type = GTK_SHADOW_ETCHED_IN;
+        } else {
+            gtk_toggle_button_set_inconsistent(GTK_TOGGLE_BUTTON(gCheckboxWidget), FALSE);
+        }
+
         gtk_paint_check(style, drawable, state_type, shadow_type, cliprect, 
                         gCheckboxWidget, "checkbutton", x, y, width, height);
         if (state->focused) {
             gtk_paint_focus(style, drawable, GTK_STATE_ACTIVE, cliprect,
                             gCheckboxWidget, "checkbutton", focus_x, focus_y,
                             focus_width, focus_height);
         }
     }
@@ -3037,17 +3048,18 @@ moz_gtk_widget_paint(GtkThemeWidgetType 
         ensure_button_widget();
         return moz_gtk_button_paint(drawable, rect, cliprect, state,
                                     (GtkReliefStyle) flags, gButtonWidget,
                                     direction);
         break;
     case MOZ_GTK_CHECKBUTTON:
     case MOZ_GTK_RADIOBUTTON:
         return moz_gtk_toggle_paint(drawable, rect, cliprect, state,
-                                    (gboolean) flags,
+                                    !!(flags & MOZ_GTK_WIDGET_CHECKED),
+                                    !!(flags & MOZ_GTK_WIDGET_INCONSISTENT),
                                     (widget == MOZ_GTK_RADIOBUTTON),
                                     direction);
         break;
     case MOZ_GTK_SCROLLBAR_BUTTON:
         return moz_gtk_scrollbar_button_paint(drawable, rect, cliprect, state,
                                               (GtkScrollbarButtonFlags) flags,
                                               direction);
         break;
--- a/widget/src/gtk2/gtkdrawing.h
+++ b/widget/src/gtk2/gtkdrawing.h
@@ -105,16 +105,20 @@ typedef enum {
 /* function type for moz_gtk_enable_style_props */
 typedef gint (*style_prop_t)(GtkStyle*, const gchar*, gint);
 
 /*** result/error codes ***/
 #define MOZ_GTK_SUCCESS 0
 #define MOZ_GTK_UNKNOWN_WIDGET -1
 #define MOZ_GTK_UNSAFE_THEME -2
 
+/*** checkbox/radio flags ***/
+#define MOZ_GTK_WIDGET_CHECKED 1
+#define MOZ_GTK_WIDGET_INCONSISTENT (1 << 1)
+
 /*** widget type constants ***/
 typedef enum {
   /* Paints a GtkButton. flags is a GtkReliefStyle. */
   MOZ_GTK_BUTTON,
   /* Paints a GtkCheckButton. flags is a boolean, 1=checked, 0=not checked. */
   MOZ_GTK_CHECKBUTTON,
   /* Paints a GtkRadioButton. flags is a boolean, 1=checked, 0=not checked. */
   MOZ_GTK_RADIOBUTTON,
--- a/widget/src/gtk2/nsNativeThemeGTK.cpp
+++ b/widget/src/gtk2/nsNativeThemeGTK.cpp
@@ -54,16 +54,17 @@
 #include "nsINameSpaceManager.h"
 #include "nsILookAndFeel.h"
 #include "nsIDeviceContext.h"
 #include "nsGfxCIID.h"
 #include "nsTransform2D.h"
 #include "nsIMenuFrame.h"
 #include "prlink.h"
 #include "nsIDOMHTMLInputElement.h"
+#include "nsIDOMNSHTMLInputElement.h"
 #include "nsWidgetAtoms.h"
 
 #include <gdk/gdkprivate.h>
 #include <gtk/gtk.h>
 
 #include "gfxContext.h"
 #include "gfxPlatformGtk.h"
 #include "gfxGdkNativeRenderer.h"
@@ -207,20 +208,30 @@ nsNativeThemeGTK::GetGtkWidgetAndState(P
                       aWidgetType == NS_THEME_CHECKBOX_LABEL) ? nsWidgetAtoms::checked
                                                               : nsWidgetAtoms::selected;
             }
             *aWidgetFlags = CheckBooleanAttr(aFrame, atom);
           }
         } else {
           if (aWidgetFlags) {
             nsCOMPtr<nsIDOMHTMLInputElement> inputElt(do_QueryInterface(content));
+            *aWidgetFlags = 0;
             if (inputElt) {
               PRBool isHTMLChecked;
               inputElt->GetChecked(&isHTMLChecked);
-              *aWidgetFlags = isHTMLChecked;
+              if (isHTMLChecked)
+                *aWidgetFlags |= MOZ_GTK_WIDGET_CHECKED;
+            }
+
+            nsCOMPtr<nsIDOMNSHTMLInputElement> inputEltNS(do_QueryInterface(content));
+            if (inputEltNS) {
+              PRBool isIndeterminate;
+              inputEltNS->GetIndeterminate(&isIndeterminate);
+              if (isIndeterminate)
+                *aWidgetFlags |= MOZ_GTK_WIDGET_INCONSISTENT;
             }
           }
         }
       } else if (aWidgetType == NS_THEME_TOOLBAR_BUTTON_DROPDOWN ||
                  aWidgetType == NS_THEME_TREEVIEW_HEADER_SORTARROW) {
         stateFrame = aFrame->GetParent();
       }
 
@@ -229,17 +240,17 @@ nsNativeThemeGTK::GetGtkWidgetAndState(P
       aState->disabled = (IsDisabled(aFrame) || IsReadOnly(aFrame));
       aState->active  = (eventState & NS_EVENT_STATE_ACTIVE) == NS_EVENT_STATE_ACTIVE;
       aState->focused = (eventState & NS_EVENT_STATE_FOCUS) == NS_EVENT_STATE_FOCUS;
       aState->inHover = (eventState & NS_EVENT_STATE_HOVER) == NS_EVENT_STATE_HOVER;
       aState->isDefault = IsDefaultButton(aFrame);
       aState->canDefault = FALSE; // XXX fix me
       aState->depressed = FALSE;
 
-      if (aFrame && aFrame->GetContent()->IsNodeOfType(nsINode::eXUL)) {
+      if (aFrame->GetContent()->IsNodeOfType(nsINode::eXUL)) {
         // For these widget types, some element (either a child or parent)
         // actually has element focus, so we check the focused attribute
         // to see whether to draw in the focused state.
         if (aWidgetType == NS_THEME_TEXTFIELD ||
             aWidgetType == NS_THEME_TEXTFIELD_MULTILINE ||
             aWidgetType == NS_THEME_DROPDOWN_TEXTFIELD ||
             aWidgetType == NS_THEME_SPINNER_TEXTFIELD ||
             aWidgetType == NS_THEME_RADIO_CONTAINER ||