Saturday, March 11, 2006

This week, I continued work towards supporting DOM interfaces from JavaScript, adding support for getting and setting the value of controls (at this point, the only content-based controls I support are text and static text). The ability for user interface code to initialize, set, and retrieve the value of controls (e.g., text fields, radio buttons) is a requirement of any UI toolkit, and trixul is no exception. The prototypical case would be a program displaying a form to the user; the program must be able to load the form with any previously entered values. To do this, it must iterate each user interface object, and set its value. Similarly, when the user submits the form, the user interface code must iterate the controls in the form, obtain their values, and and then write the values somewhere (so that they can be acted upon by the application, or perhaps be persisted until the next time the user loads the data back into the form for editing).

In a previous post, I described how DOM interfaces could be used to support function calls that can be used from JavaScript to enable or disable a control, for example:

var button = document.getElementById("myButton");
if (button && someCondition)
button.enable(); // enable the button is someCondition is true
else if (button)
button.disable();
else
dump("Unable to locate the button 'myButton'\n");


and I also described how I implemented this functionality by exposing JS objects that mapped to concrete control/widget implementations in the layout engine.

Implementing support for setting and getting the value controls follows a very similar strategy. What we want to support is code like the following, which gets and sets the value of a text field:

var text = document.getElementById("myText");
if (text) {
value = text.getValue();
dump("myText value is " + value + "\n");
// set value to the string "Hello";
text.setValue("Hello");
} else
dump("Unable to locate the text field 'myText'\n");

When it comes to the implementation of the above, about the only difference between getValue/setValue and enable/disable is these functions either return a value (getvalue()) or require an argument (setValue()), meaning that getValue() must return a value of the appropriate type, and setValue() must correctly interpret the argument that it has been passed. In addition, both must interact with the concrete widget to retrieve its value, or set its value, respectively.

When document.getElementById() is called, the JavaScript object returned is lazily instantiated; that is, the layout code will not create this object until JavaScript code attempts to retrieve it. The lifetime of the JavaScript object is determined by the scope of the JavaScript variable that stores its value, and once the scope is left, the JavaScript object becomes a candidate for being garbage collected. Multiple requests for a JavaScript object pertaining to a given DOM element will result in multiple JavaScript objects being created and returned, but this is ok. Why? The JavaScript object is nothing more than an interface that allows JavaScript code to make calls on the layout object; any state is maintained by the layout object, not by the JavaScript objects that represent it.

In fact, the JavaScript object is the same object that is used to call enable() and disable(), all I needed to do was add entries to Element's JSFunctionSpec vector:

static JSFunctionSpec element_functions[] = {
{"enable", DoEnable, 1},
{"disable", DoDisable, 1},
{"setValue", DoSetValue, 1},
{"getValue", DoGetValue, 1},
{0}
};

and implement the functions DoSetValue() and DoGetValue().

The most interesting part of DoSetValue() and DoGetValue() is how they support the argument (DoSetValue()) and return value (DoGetValue()). When I call DoSetValue() against a JavaScript object that represents a text field, the argument needs to be interpreted as a string. If I am making the call against a radio button, it might be an integer or a boolean value to indicate its state. A progress bar value might be an integer in a specific range. Similarly, if I return the value of a progress bar, text field, or radio button, the types need to be appropriate for the control.

Let's take a look at DoSetValue() and its helper function to see how this works. Here is the code for DoSetValue():


JS_STATIC_DLL_CALLBACK(JSBool)
DoSetValue(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
JSBool ret = JS_FALSE;

if (!argc)
return JS_TRUE;

Element *element = Element::FindElementByObj(obj);
if (element) {
Control *control = dynamic_cast<Control *>(element);
if (control &&
element->SetValueHelper(cx, control, argv[0]) == PR_SUCCESS)
ret = JS_TRUE;
}

return ret;
}

The function prototype is the same is it is for the callbacks supporting disable() and enable(). argv[0] will contain a JavaScript object that represents the value we wish to set the control to. This value, along with the control that maps to the object passed in, and the JavaScript context, are passed to the helper function SetValueHelper(). The job of SetValueHelper() is simple; it needs to convert the value passed in via argv[0] to a native C++ type, and then invoke a function in the concrete widget implementation that can interact with the platform-specific widget to change its value. Here is the body of SetValueHelper():

PRStatus
Element::SetValueHelper(JSContext *cx, Control *control, jsval val)
{
if (!control)
return PR_FAILURE;

PRStatus ret = PR_FAILURE;

ContentType type = control->GetContentType();
switch (type) {
case ContentTypeString:
JSString *str;
str = JS_ValueToString(cx, val);
if (!str)
return ret;

char *bytes = JS_GetStringBytes(str);
if (bytes)
bytes = strdup(bytes);
if (bytes) {
XPVariant v;
v.SetValue(bytes);
if (control->SetValue(v) == PR_SUCCESS)
ret = PR_SUCCESS;;
free(bytes);
}
break;
default:
break;
}
return ret;
}

Let's look at this code carefully. The first important line of code:

ContentType type = control->GetContentType();

queries the control object asking it what kind of content it manages. ContentType is an enum defined in the layout engine; each class deriving from Control, in its constructor, will call a function that registers its type (the setter and getter are implemented by the Control base class, which maintains the value in a private member variable. Text fields will register the type ContentTypeString.

We then enter a switch statement based on this type. The code for handling ContentTypeText uses the JavaScript API function JS_ValueToString() to retrieve the JSObject value we were passed to a JSString, and we then call another JS API function, GetStringBytes(), to get the characters as a C string:

switch (type) {
case ContentTypeString:
JSString *str;
str = JS_ValueToString(cx, val);
if (!str)
return ret;

char *bytes = JS_GetStringBytes(str);
if (bytes)
bytes = strdup(bytes);

JS_ValueToString() is one of a handful of JavaScript API functions that can convert a JSValue to a type (e.g., JS_ValueToInt32() converts a JSValue to an integer). In a sense, a JSValue is a variant object, meaning a single object that can be used to represent values of varying types. JS_ValueToString() et al. allow application code to convert the value stored in the JSObject to whatever type is needed (assuming that the conversion makes sense).

Now that the value of the JSObject is in hand, we only need to call the control to change the value of the widget. There are two ways to accomplish this. One would be to overload a setter function in Control for each possible type, e.g.:

PRStatus SetValue(const string &value);
PRStatus SetValue(const int &value);
PRStatus SetValue(const float &value);
...

Or, we could be a bit more formal about it and and use a variant. I decided to do just that, and I invented a simple variant type, XPVariant. It implements the same basic strategy as mentioned above (overloaded setters and getters), but encapsulates this and the representation of the value into a class that can be used outside of the scope of the problem at hand. The data is stored as a member variable in a union:

private:
XPVarType m_type;
union {
char vChar;
PRUint8 vUint8;
PRInt8 vInt8;
PRUint16 vUint16;
PRInt16 vInt16;
PRUint32 vUint32;
PRInt32 vInt32;
#ifdef HAVE_LONG_LONG
PRUint64 vUint64;
PRInt64 vInt64;
#endif
PRFloat64 vFloat64;
PRBool vBool;
} m_val;
string m_vString;
};

The types of the variables are mostly one-to-one with the types that are defined by NSPR. Strings are presented outside of the union as STL strings, however.

The setters and getters are overloads of basically the same interface. Here are a few of them:

PRStatus SetValue(const PRUint8 &val);
PRStatus GetValue(PRUint8 &val);
PRStatus SetValue(const PRInt8 &val);
PRStatus GetValue(PRInt8 &val);
PRStatus SetValue(const PRUint16 &val);
PRStatus GetValue(PRUint16 &val);

The implementation of SetValue() and GetValue() for PRUint8 simply assign or read the appropriate value in the union:

PRStatus XPVariant::SetValue(const PRUint8 &val)
{
m_val.vUint8 = val;
m_type = XPVarTypeUint8;

return PR_SUCCESS;
}

PRStatus XPVariant::GetValue(PRUint8 &val)
{
if (m_type == XPVarTypeUint8) {
val = m_val.vUint8;
return PR_SUCCESS;
} else
return PR_FAILURE;
}

By storing the type (e.g., XPVarTypeUint8) in the object, we can make queries on the object to determine the type it is storing via a member function name GetType(). More sophistication can be achieved by implementing converters that, like those associated with JSObject, that can be used to convert from one to type to another. However, I don't see the need for implementing converters in this project, at this point.

Continuing the discussion of SetValueHelper(), here is the code that uses the variant type to invoke the Control object interface to set the value:

if (bytes) {
XPVariant v;
v.SetValue(bytes); // automatically makes the variant a string
if (control->SetValue(v) == PR_SUCCESS)
ret = PR_SUCCESS;;
free(bytes);
}
break;

Control::SetValue() then extracts the value from the variant, and calls the concrete implementation, which for Cocoa, is the following function, SetValue(), implemented in CocoaTextImpl:

PRStatus
CocoaTextImpl::SetValue(const string &v)
{
if (m_text) {
NSString *nsvalue = [NSString stringWithCString: v.c_str()];
if (nsvalue)
[m_text setString: nsvalue];
}
return PR_SUCCESS;
}


Getting values is about as simple. As before, the JavaScript function calls us, this time in DoGetValue():

JS_STATIC_DLL_CALLBACK(JSBool)
DoGetValue(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
JSBool ret = JS_FALSE;

Element *element = Element::FindElementByObj(obj);
if (element) {
Control *control = dynamic_cast(element);
if (control && element->GetValueHelper(cx, control, rval) == PR_SUCCESS)
ret = JS_TRUE;
}
return ret;
}

Once the control is located, GetValueHelper() is called. It's job is to call the control, and return the value of the control in its rval argument:

PRStatus
Element::GetValueHelper(JSContext *cx, Control *control, jsval *rval)
{
if (!control)
return PR_FAILURE;

PRStatus ret = PR_FAILURE;

ContentType type = control->GetContentType();
switch (type) {
case ContentTypeString:
{
XPVariant v;
if (control->GetValue(v) == PR_SUCCESS) {
string val;
if (v.GetValue(val) == PR_SUCCESS) {
JSString *str = ::JS_NewStringCopyN(cx, val.c_str(), val.size() + 1);
*rval = STRING_TO_JSVAL(str);
ret = PR_SUCCESS;
}
}
break;
}
default:
break;
}
return ret;
}

The job of Control::GetValue() is to interact with the concrete widget to determine its current value, and place this in the XPVariant it is passed. Then, GetValueHelper() can convert the STL string returned in the case of a text field to a JSvalue, storing the result in rval. The JavaScript engine will then ensure that JavaScript code can assign the return value to a JavaScript variable.

For completeness sake, below is the implementation of CocoaTextImpl::GetValue(), which retrieves the value of a text field and stores it in the passed STL string (Control, which calls this function, will take the result and assign it to the variant before returning):

PRStatus
CocoaTextImpl::GetValue(string &v)
{
if (m_text) {
NSString *value = [m_text string];
if (value) {
v = [value cStringUsingEncoding: NSUTF8StringEncoding];
return PR_SUCCESS;
}
}
return PR_FAILURE;
}

0 Comments:

Post a Comment

<< Home