Saturday, December 17, 2005



To the left is a class diagram that illustrates the relationships between the abstract widget interface classes, and the concrete widget implementation classes (only Button is illustrated in this document to keep things simple). Let's zoom in and take a closer look at the components that make up this class diagram.

At the top of the following figure, we have two classes, Element and Widget. Element represents a DOM entity in the layout engine. As attributes, Elements maintain a parent, a list of child Elements, an associated Document, and a type. In addition, the Element class provides setters and getters allowing the layout engine to access these attributes. Element might have easily had been named Object; the abstract class Object in the Gtk+ class hierarchy is roughly analogous to Element, for example.



Inheriting from Element is Widget. A Widget, also represented in the class diagram above, is a base class that represents all things that are related to widgets. Widgets can be shown, hidden, drawn, and created. They can be containers of other widgets (examples include boxes, grids, and so forth), or they can be controls that manage no children, but represent some functionality (menu items, text fields, and buttons, to name but a few). Widgets have some geometry (an x, y position, and a width and height), and several of the interfaces defined by Widget are used by the layout to retrieve the desired geometry of the widget (GetGeometryRequest()) and tell the widget what its final geometry is once it has been computed by the layout engine (much of this was described in an earlier post to this blog).

So, as mentioned, widgets can be containers. Or they can be controls. Button widgets, which we are currently focusing on, are obviously controls, because they do not manage children, and represent some user interface object that a user directly interacts with. But for a moment, take a look at the following Figure, which illustrates the Widget->Container->Box and Widget->Control inheritance class diagram.



As you can see, the Container class abstracts things that Widgets managing a set of children must provide. This includes the ability to compute a layout given some region allocated it by a parent (ComputeLayout()), as well as iterate its children and command them to be created, drawn, shown, and hidden.

Returning to the portion of the class diagram that involves buttons, the next Figure shows the relationship between a Control, the abstract Button class, and the abstract ButtonImpl class.

It's important to note that ButtonImpl does not inherit from Button, rather, a Button manages an instance of a ButtonImpl (in Design Pattern terminology, one calls this relationship between classes a bridge (see GoF, around page 151)). Implementing this as a bridge allows the ButtonImpl hierarchy to exist independently of Buttons; should radical changes in how a button is implemented becomes necessary, I can make those changes without impacting the Widget->Control->Button class hierarchy, and vice versa. Bridges also play nicely with the factory concept, allowing me to isolate what a factory provides in an independent class hierarchy. The hook, once again, is in the Button class, which maintains the link between abstraction and implementation, calling whatever ButtonImpl-derived class the widget factory decided to provide.

The final Figure of this post wraps up our look at the class hierarchy, focusing on the classes GtkWidgetImpl, CocoaWidgetImpl, GtkButtonImpl, and CocoaButtonImpl.

There are a few things that are important to notice in the above class diagram. First of all, GtkButtonImpl and CocoaButtonImpl implement the same interface (inheriting that interface from WidgetImpl, which abstracts all of the things a concrete widget must be, and from ButtonImpl, which isolates the interfaces that are specific to a button and don't belong in a WidgetImpl class, namely SetLabel()).

Second, and perhaps more suprisingly, both GtkButtonImpl and CocoaButtonImpl also inherit from two mixin classes - GtkWidgetImpl and CocoaWidgetImpl - respectively. What, exactly, is the purpose of these two classes? Well, GtkWidgetImpl and CocoaWidgetImpl implement abstractions that are specific to each platform, but which are common for all concrete widgets that are created on that platform. Be they buttons, menus, text fields, images, or whatever you might envision supporting in the toolkit. Originally, these classes did not exist in my design, but I found that I was duplicating code in all of the CocoaWidgetImpl-derived classes, that this code being duplicated was not portable enough to implement in WidgetImpl, and so I decided to create mixin classes that contained factorizations of the duplicated code. In Gtk+, for example, all widgets must be parented by an instance of GtkFixed, which is the immediate child of GtkWindow in the instance hierarchy and gives the concrete widgets (controls like GtkButton) a place to add themselves and allows for the layout engine to control their positions as it computes the layout of a window. Hence the need for an interface named SetFixedParent(), which allows the widget to store a pointer to the GtkFixed widget instance by querying the abstract WidgetImpl object that is passed as an argument for its concrete implementation pointer. Here is the code that does that:

PRStatus GtkWidgetImpl::SetFixedParent(WidgetImpl *top)
{
if (top) {
GtkWidgetImpl *widgetImpl = dynamic_cast(top);

if (widgetImpl) {
GtkWidget *w = widgetImpl->GetImpl();
if (w) {
m_fixedParent = w;
return PR_SUCCESS;
}
}
}
return PR_FAILURE;
}

In the case of Cocoa, we are required to implement a completely different strategy for parenting a widget. Essentially, this is done by obtaining the NSView pointer associated with the parent widget, and telling the NSButton that is being instantiated, within its Create() function, what this view parent is:

m_view = (NSView *) m_button;

WidgetImpl *parentImpl = GetParent();

if (parentImpl) {

NSView *parentView = dynamic_cast(parentImpl)->GetView();

// add the button to the parent view

if (parentView) {
[parentView addSubview: m_view];
return PR_SUCCESS;
}
}


To support this, CocoaWidgetImpl implements a pair of functions, GetView() and SetView(), which allow an instance of class inheriting CocoaWidgetImpl to store and retrieve this view pointer, as shown in the above code.

As another example of the factorizations present in CocoaWidgetImpl and GtkWidgetImpl, compare the two functions CocoaWidgetImpl::SetGeometry() and GtkWidgetImpl::SetGeometry(). Again, remember that these factorizations exist to support all concrete widgets implemented for a specific platform. First up, CocoaWidgetImpl::SetGeometry():

PRStatus CocoaWidgetImpl::SetGeometry(const int &x, const int &y,
const int &width, const int &height, const char &mask)
{
if (m_view) {
NSRect graphicsRect;
if (mask == GEOM_ALL)
graphicsRect = NSMakeRect(x, y, width, height);
else {
graphicsRect = [m_view frame];
if (mask & GEOM_X)
graphicsRect.origin.x = x;
if (mask & GEOM_Y)
graphicsRect.origin.y = y;
if (mask & GEOM_WIDTH)
graphicsRect.size.width = width;
if (mask & GEOM_HEIGHT)
graphicsRect.size.height = height;
}

[m_view setFrame: graphicsRect];
return PR_SUCCESS;
}
return PR_FAILURE;
}

The above function essentially takes the geometry (x, y, width, height) passed to it and, based on the mask specified by the mask argument, invokes Objective-C code to modify the geometry of the NSView inherited by the (NSButton) widget.

Now let's look at the GtkWidgetImpl implementation of the same function:

PRStatus GtkWidgetImpl::SetGeometryImpl(const int &x, const int &y,
const int &width, const int &height, const char &mask)
{
if (m_widget) {
// get the current values

GtkArg arg;

if (mask & GEOM_X) {
arg.name = "GtkWidget::x";
arg.type = GTK_TYPE_INT;
GTK_VALUE_INT(arg) = x;
gtk_object_arg_set(GTK_OBJECT(m_widget), &arg, NULL);
}

if (mask & GEOM_Y) {
arg.name = "GtkWidget::y";
arg.type = GTK_TYPE_INT;
GTK_VALUE_INT(arg) = y;
gtk_object_arg_set(GTK_OBJECT(m_widget), &arg, NULL);
}

if (mask & GEOM_WIDTH) {
arg.name = "GtkWidget::width";
arg.type = GTK_TYPE_INT;
GTK_VALUE_INT(arg) = width;
gtk_object_arg_set(GTK_OBJECT(m_widget), &arg, NULL);
}

if (mask & GEOM_HEIGHT) {
arg.name = "GtkWidget::height";
arg.type = GTK_TYPE_INT;
GTK_VALUE_INT(arg) = height;
gtk_object_arg_set(GTK_OBJECT(m_widget), &arg, NULL);
}

return PR_SUCCESS;
}

return PR_FAILURE;
}

While the structure of both of these implementations is the same, notice that the mechanisms used are very specific to Gtk+ (namely, C calls to gtk_object_arg_set() for each geometry attribute that is to be modified). Such is the nature of the code found at this level; this is code which is best designed and implemented by platform-savvy developers who are given the basic API via the abstract classes (WidgetImpl, ButtonImpl) to work with, and who are free to create helper classes like CocoaWidgetImpl to do those things that are necessary for only that particular platform.