Sunday, February 26, 2006

This weekend I tackled the issue of supporting the enabling and disabling of controls (e.g., menu items, buttons, text fields) from user code, once again, starting my work on Cocoa, and migrating it over to Gtk+ and Windows .NET. Most of the work was highly cross platform, but some of it (the menuitem code on Cocoa) was very non-portable (but, as usual, very powerful).

In this entry, I'll describe the architecture of the overall solution. Notes on platform-specific issues that arose during the implementation will be described later in the week (notably, the implementation of enable and disable for Cocoa NSMenuItems under MacOS X).

Typically, applications will enable and disable certain controls in the user interface based upon the state of the application. If there have been no changes to a document, for example, a text editor will have its Save and Save As... menu items disabled. If no text is selected in a text field that has the input focus, then Cut and Copy menu items will be disabled. If a user has not filled out all of the fields in a form, then perhaps a button labeled "Submit" will be disabled. This sort of functionality implies the following support: JavaScript (or less desirably, C++ component code) needs to be able to enable and disable controls, and it also needs to be able to query controls for their values (and, as a corollary, JavaScript code should also be able to set the value of a control).

In the W3 DOM Level 1 Specification , the standard way to find an object is to search for it by name or ID. The result of this call is a DOM element, which can then by interacted with by the JavaScript code, e.g., by setting attributes on the element, or by calling functions that are implemented by the element. Here is how such code might look like:

var findText = document.getElementById("findText");

The above code, taken from legacy Mozilla's JavaScript, shows how getElementById(), a W3 DOM standard function, can be used to find the DOM element with the id "findText". The result is stored in the variable findText, and then code like the following:

var findTextValue = findText.value;
dump("The value of the text field 'findText' is " + findTextValue + "\n");

can be called to retrieve the value associated with the text field referenced by findText, and print out its value.

Assigning an id to an element is easily done by supplying an id="value" attribute in markup. For example, the following markup, also taken from Mozilla, illustrates the XUL used to define the textbox retrieved by calling getElementById() in the JavaScript code presented above, assigning the the id "findText":

<textbox id="findText" type="timed" timeout="500" oncommand="doFind();"/>

In my design, I adopted the above paradigm based on identifying objects in markup with an id attribute, and finding objects in JavaScript by calling my own implementation of getElementById().

To do this, one needs to expose a JavaScript object that implements getElementById(). Since the goal is to find an element within an object, the natural choice was to extend layout's Document class to manage a "document" JavaScript object for each Document instance created. Each window displayed by the application has a Document instance representing it in the layout engine, and the "document" JavaScript object gives us access to that Document instance. And, all of the elements that we wish to locate are children of the document (elements are scoped to a particular instance of a document just like controls are scoped to a particular window in more traditional GUI toolkits), thus the Document class is well-suited to locating any item that we are interested in finding.

When the Document class is instantiated by the layout engine, it will call a function named CreateJSObject() to create a JavaScript document object for that instance of Document, like this:

document = new Document();
if (document)
document->CreateJSObject();

The resulting JavaScript object is stored as a member variable in the Document instance, and, importantly, it is also stored in a static map that binds the JavaScript object to the Document instance (why this is done is explained later). Let's look at the code for CreateJSObject():

static JSFunctionSpec document_functions[] = {
{"getElementById", DoGetElementById, 1},
{0}
};


PRStatus
Document::CreateJSObject()
{
JSEngine *js = JSEngine::GetJSEngine();

if (!js)
return PR_FAILURE;

if (m_jsObj)
return PR_SUCCESS;

JSContext *ctx = js->GetContext();
JSObject *obj = js->GetGlobalObject();

JSClass document_class = {
"document", JSCLASS_NEW_RESOLVE,
JS_PropertyStub, JS_PropertyStub,
JS_PropertyStub, JS_PropertyStub,
JS_EnumerateStub, JS_ResolveStub,
JS_ConvertStub, JS_FinalizeStub
};

m_jsObj = JS_DefineObject(ctx, obj, "document", &document_class, NULL, 0);

if (m_jsObj) {
JS_DefineFunctions(ctx, m_jsObj, document_functions);
m_objectMap.insert(pair<JSObject *, Document *>(m_jsObj, this));
return PR_SUCCESS;
}
return PR_FAILURE;
}

JSObject *
Document::GetJSObject()
{
return m_jsObj;
}

map <JSObject *, Document *> Document::m_objectMap;

In short, the above code creates a JavaScript object named "document" that implements a function named "getElementById", which when invoked, invokes a standard JavaScript callback named DoGetElementById() which we must implement. It also adds the a JavaScript object, Document * pair to the map so that, given a JavaScript object, we can find the corresponding document. This will be needed so that, when we get called by the JavaScript engine in DoGetElementById(), we can find the Document instance that corresponds to the call.

Next up is the code for DoGetElementById():

JS_STATIC_DLL_CALLBACK(JSBool)
DoGetElementById(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
// find object to Document *mapping

JSBool ret = JS_FALSE;
*rval = NULL;

Document *document = Document::FindDocumentByObj(obj);
if (document) {
JSString *str;

str = JS_ValueToString(cx, argv[0]);
if (!str)
return JS_FALSE;

char *bytes = JS_GetStringBytes(str);
bytes = strdup(bytes);
if (bytes) {
Element *element = document->GetElementById(NULL, bytes);
if (element) {
if (element->CreateJSObject() == PR_SUCCESS) {
JSObject *jsobj = element->GetJSObject();
if (jsobj) {
*rval = OBJECT_TO_JSVAL(jsobj);
ret = JS_TRUE;
}
}
}
free(bytes);
}
}

return ret;
}

Understanding the above code is key to understanding how we locate and return a JavaScript object representing the Document the user currently is interacting with, so let's look at this line by line.

The line:

Document *document = Document::FindDocumentByObj(obj);

looks up the passed-in JSObject * argument, obj, in the JSObject * to Document *map. obj represents the document JavaScript object we created for the Document, and when the user references "document" from JavaScript, like this:

document.getElementById(...);

it is this JavaScript object (document) that will be passed into DoGetElementById() by the JavaScript engine, and can be used to find the Document object in the layout engine.

The first argument to DoGetElementById() is a string which contains the id we need to lookup. With that id, we then call a helper function in Document, aptly named GetElementById(), which will iterate the entire document looking for the first element which has an id attribute with the value we seek. Once that element is found, we need to retrieve a JavaScript object from that element to return. Here is the code that gets the id argument, and calls on the document object to see if it can find the corresponding element. Remember that an Element in the layout engine represents some markup like a menu item, a button, or a text field.
 
char *bytes = JS_GetStringBytes(str);
bytes = strdup(bytes);
if (bytes) {
Element *element = document->GetElementById(NULL, bytes);

Here is the (recursive) implementation of Document::GetElementById(). It eventually returns an Element *, or NULL if no such element exits in the document's DOM:

Element *Document::GetElementById(Element *root, const string& id)
{
if (!root)
root = m_root;

// if still NULL, bail

if (!root)
return NULL;

AnAttribute *attr;

if (((attr = root->GetAttributeByName("id")) &&
!PL_strcasecmp(id.c_str(), attr->GetValue().c_str())))
{
return root;
}

// if we get here, this node isn't it -- so check children

list<Element *>::iterator itr;
Element *node;

for (itr = root->m_children.begin(); itr != root->m_children.end(); ++itr) {
if ((node = GetElementById(*itr, id)))
return node;
}

return NULL;
}

So, with an Element in hand, we still need to give a JSObject back to the JavaScript engine. The way to do this is, naturally enough, to ask the Element for a JSObject. It is pointless to create JSObjects for elements that the JavaScript code never will interact with -- we can lazily instantiate an object some point after we have located the Element in the above code, and store the object as a member variable of the Element for later use if the same instance of a Document requests it. So, returning back to the code in DoGetElementById() (found in Document.cpp), can see how it calls on Element to create a JSObject corresponding to the element, which it then returns back to the JavaScript engine, along with a return value of JS_TRUE to indicate that the element was found in the document, and that we were able to create (or read from cache) a JSObject to represent it.

Element *element = document->GetElementById(NULL, bytes);
if (element) {
if (element->CreateJSObject() == PR_SUCCESS) {
JSObject *jsobj = element->GetJSObject();
if (jsobj) {
*rval = OBJECT_TO_JSVAL(jsobj);
ret = JS_TRUE;
}
}
}
...
return ret;


The JavaScript object created and returned by an Element has a similar evolution to that of the object create and returned by a Document. It is added to a map so that it can be located from within the context of a JavaScript callback. The only real changes are that it has a different name "element" and implements different functions. Two of them, enable() and disable(), provide the implementations for enabling and disabling the native widget (button, menu item, text field) that the element wraps. These two functions, enable and disable, are very similar. The callbacks that implement them asks Element to search its map to find the corresponding Element * from the passed in JSObject, just like was done when JS callbacks were invoked on Document. If found, we simply involve Element's Disable() or Enable() function, which in turn calls Widget's Disable() or Enable() function, which calls (if we are taking about Button widgets) Button's Disable() or Enable() function, which, finally, invokes the platform implementation to do the actual disabling or enabling, as it were. Here is the source for the start of this chain, Element's DoEnable() function (DoDisable() is similar, and not shown):

JS_STATIC_DLL_CALLBACK(JSBool)
DoEnable(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
{
JSBool ret = JS_FALSE;
Element *element = Element::FindElementByObj(obj);
if (element) {
Widget *widget = dynamic_cast<Widget *>(element);
if (widget) {
if (widget->Enable() == PR_SUCCESS)
ret = JS_TRUE;
}
}

return ret;
}

Element::FindElementByObj() looks up the Element in the static JSObj to Element map. If found, we dynamically cast the element to Widget and then invoke its Enable() function.

Additional functions can be added to the Element JS Object for functionality that will be needed by the application, such as setting and getting the value of a control, or changing its label, in a very straightforward manner now that the above architecture is in place. I plan to do exactly that in the coming week.

0 Comments:

Post a Comment

<< Home