Thursday, December 15, 2005

The port from Cocoa to Gtk+ was pretty straightforward; as I mentioned in a previous post, I was able to pull it off in 10 or so hours of work.

The first step in performing the port was to create a directory to hold the Gtk+ specific sources. This was a simple matter of making a copy of the cocoa sources, i.e.,:

$ cd layout
$ cp -r cocoa gtk

The include files that were copied largely remained the same. This was to be expected, and reflects the fact that the code located in these directories provides concrete implementations of abstract classes defined by the layout engine, and as such, must adhere to interfaces defined by the abstract classes. One example is the widget factory code, which provides interfaces on all platforms for creating widgets (buttons, windows, text fields). Let's start by looking at WidgetFactory's definition:

#if !defined(__WIDGETFACTORY__H__)
#define __WIDGETFACTORY__H__

// abstract class for widget creation factory. For each widget
// we can create, there is a MakeFoo function that returns a
// FooImpl. In each platform widget directory, there is a class
// that inherits from WidgetFactory. Its MakeFoo function
// instantiates a concrete class, e.g., GtkFooImpl, that inherits
// from FooImpl, and represents Foo on that platform.

// The layout Foo abstract class has a reference to an object that
// belongs to the class FooImpl, which it creates in its constructor.
// Calls to the Foo class are delegated to that object. Likewise,
// the Foo class will be a listener to events that are defined in
// FooImpl and are implemented by the concrete class that inherits
// from FooImpl.

#include "textimpl.h"
#include "statictextimpl.h"
#include "buttonimpl.h"
#include "windowimpl.h"
#include "boximpl.h"
#include "spacerimpl.h"
#include "appimpl.h"
#include "prtypes.h"

class WidgetFactory {
public:
virtual ~WidgetFactory() {};
virtual AppImpl* MakeApp() = 0;
virtual ButtonImpl* MakeButton() = 0;
virtual StaticTextImpl* MakeStaticText() = 0;
virtual TextImpl* MakeText() = 0;
virtual WindowImpl* MakeWindow() = 0;
virtual BoxImpl* MakeBox() = 0;
virtual SpacerImpl* MakeSpacer() = 0;
};

WidgetFactory *GetWidgetFactory();

#endif

Notice that all of the functions in WidgetFactory (except for the CTOR and DTOR) are declared pure virtual, forcing implementation by the concrete classes that inherit from WidgetFactory. The only exception to this is the function GetWidgetFactory(), which will be discussed below.

To gain an understanding of the role of factories in this implementation, let's now look at gtkfactory.h, which provides the concrete, platform-specific implementation of WidgetFactory for Gtk+:
 

#if !defined(__GTK_FACTORY__H__)
#define __GTK_FACTORY__H__

#include "../widgetfactory.h"
#include "../textimpl.h"
#include "../statictextimpl.h"
#include "../buttonimpl.h"
#include "../boximpl.h"
#include "../spacerimpl.h"
#include "../windowimpl.h"
#include "../appimpl.h"
#include "prtypes.h"

class GtkFactory : public WidgetFactory
{
public:
GtkFactory();
virtual ~GtkFactory();
virtual TextImpl *MakeText();
virtual StaticTextImpl *MakeStaticText();
virtual ButtonImpl *MakeButton();
virtual BoxImpl *MakeBox();
virtual SpacerImpl *MakeSpacer();
virtual WindowImpl *MakeWindow();
virtual AppImpl *MakeApp();
};

#endif


As you can see, Gtk+ is a no-frills implementation of the WidgetFactory interface. The Cocoa implementation is mostly identical except for the names of the class, the CTOR, and the DTOR (e.g., class CocoaFactory, CocoaFactory(), and ~CocoaFactory(), respectively).

Next, let's take a look at the layout code that instantiates the concrete factory classes GtkWidgetFactory and CocoaWidgetFactory:

class WidgetFactory;

#if defined(HAVE_MACOS)
#include "cocoa/cocoafactory.h"
#endif
#if defined(HAVE_GTK)
#include "gtk/gtkfactory.h"
#endif

WidgetFactory *GetWidgetFactory()
{
static WidgetFactory *widgetFactory = 0;

if (!widgetFactory)
#if defined(HAVE_MACOS)
widgetFactory = new CocoaFactory;
#endif
#if defined(HAVE_GTK)
widgetFactory = new GtkFactory;
#endif
return widgetFactory;
}

The code above supports the creation of an application singleton that provides the concrete implemenation of the abstract WidgetFactory interface used by the layout engine to create buttons and other widgets. The #ifdef's in the code are a reasonable way to ensure that the correct platform implementation is returned by GetWidgetFactory(). Had I named the concrete classes the same, however, these #ifdefs probably would not be needed. Whenever the concrete classes differ in some way in the class definition, then the above pattern would be more appropriate.

So, given a factory for generating platform-specific widgets, how does one use it? Let's focus on the creation of a button widget. Say that we have some XML markup that includes a <button> tag. When the layout engine parses this XML, it creates a button object, as in the following code taken from layout's parser code:

static void
StartElement(void *userData, const char *name, const char **atts)
{
Document *document = (Document *) userData;
Element *element = NULL;
Script *script = NULL;
int i;

if (!PL_strcasecmp(name, "button")) {
element = new Button();
element->SetType(TYPE_BUTTON);
}

The Button class implements the generic, platform independent functionality that is needed by the layout engine, and maintains a pointer to the platform-specific class that was provided by the widget factory, calling it as needed from its member functions:

class Button : public Control, public ButtonPressObserver
{
public:
Button();
~Button();
PRStatus Create();

...

private:
ButtonImpl *m_button;
};

The Button object that is created exists in a DOM maintained by the layout engine. At some point, when the document that was parsed is displayed (e.g., the application loads or opens a window containing the DOM), the DOM iterated, and the Create() function for each element in the DOM is called by the layout engine. Create() in Button is actually the implementation of an interface that is declared in the abstract class Widget, from which Button directly inherits via the Control base class. In any event, Button's Create() is called. It's implementation accesses the concrete implementation of Create() using a pointer to the abstract widget implementation (class ButtonImpl), as follows:

PRStatus Button::Create()
{
PRStatus status;

status = m_button->Create();
if (status == PR_SUCCESS) {
status = SetLabel(GetLabel());
}
return status;
}

Now here is where it gets a little tricky. m_button is an instance of ButtonImpl, which inherits from WidgetImpl. WidgetImpl declares Create() as pure virtual:

class WidgetImpl
{
public:
WidgetImpl() : m_parent(NULL) {};
virtual ~WidgetImpl() {};
virtual PRStatus Create() = 0;

...

}

The connection to the concrete classes implemented for Cocoa and Gtk+ is made via the classes that are created by the widget factory. Here again is GtkButton's definition:

#if !defined(__GTKBUTTONIMPL__H__)
#define __GTKBUTTONIMPL__H__

#include "buttonimpl.h"
#include

#include "gtkwidgetimpl.h"

#include "commandhandler.h"

class GtkButtonImpl : public ButtonImpl, public GtkWidgetImpl, public CommandHandler
{
public:
GtkButtonImpl();
virtual ~GtkButtonImpl();
virtual PRStatus Create();
virtual PRStatus Show();
virtual PRStatus Hide();
virtual PRStatus SetLabel(const string& label);
virtual PRStatus HandleCommand();
virtual PRStatus SetGeometry(const int &x, const int &y,
const int &width, const int &height, const char &mask) {
return SetGeometryImpl(x, y, width, height, mask); };
virtual PRStatus GetGeometryRequest(int &x, int &y,
int &width, int &height) {
return GetGeometryRequestImpl(x, y, width, height); };
private:
};
#endif

Notice how GtkButtonImpl includes the header buttonimpl.h, and inherits ButtonImpl. This is the hook we are looking for that ties ButtonImpl to GtkButtonImpl; we needed to call a class that implements ButtonImpl, and GtkButtonImpl fills the need.

One final detail needs to be described. That is, how the factory itself is invoked in order to initialize the m_button member of the abstract Button class. This is done from within the
Button CTOR, as follows:

Button::Button()
{
WidgetFactory *factory = GetWidgetFactory();

if (factory)
m_button = factory->MakeButton();
}

Simply put, an instance of Button gets a pointer to the singleton WidgetFactory instance, and calls its MakeButton() function, and assigns the result to the m_button member variable. Once this has been done, any calls made via m_button will find their way into the concrete class (GtkButtonImpl, or CocoaButtonImpl).

Abstract class Button's remaining interfaces (e.g., SetLabel()) use a pattern that is similar to Create(). That is, they check to make sure m_button is not null, and then invoke the concrete implementation. For example, a call to Button's SetLabel() function made by the layout engine is handled as follows:

PRStatus Button::SetLabel(string& label)
{
if (m_button)
m_button->SetLabel(label);
return PR_SUCCESS;
}

Now that you (hopefully) understand the relationship between the layout engines abstract widget classes, the platform-specific concrete classes, and the factory that is used to tie the two together, let's take a closer look at some of concrete code that is associated with Button's implementation. As you might expect, the interfaces are the same, but the implementations are vastly different. We'll start first with the header files that define the concrete classes, and then look more closely at the Create() function. Here is the header for Cocoa's CocoaButtonImpl class:

#if !defined(__COCOABUTTONIMPL__H__)
#define __COCOABUTTONIMPL__H__

#include "../buttonimpl.h"
#import

#include "cocoawidgetimpl.h"

#include "commandhandler.h"

class CocoaButtonImpl : public ButtonImpl, public CocoaWidgetImpl, public CommandHandler
{
public:
CocoaButtonImpl();
virtual ~CocoaButtonImpl();
virtual PRStatus Create();
virtual PRStatus Show();
virtual PRStatus Hide();
virtual PRStatus SetLabel(const string& label);
virtual PRStatus HandleCommand();
virtual PRStatus GetGeometryRequest(int &x, int &y, int &width, int &height);
virtual PRStatus SetGeometry(const int &x, const int &y,
const int &width, const int &height, const char &mask);
private:
NSButton *m_button;
};
#endif

And for Gtk+:

#if !defined(__GTKBUTTONIMPL__H__)
#define __GTKBUTTONIMPL__H__

#include "../buttonimpl.h"
#include

#include "gtkwidgetimpl.h"

#include "commandhandler.h"

class GtkButtonImpl : public ButtonImpl, public GtkWidgetImpl, public CommandHandler
{
public:
GtkButtonImpl();
virtual ~GtkButtonImpl();
virtual PRStatus Create();
virtual PRStatus Show();
virtual PRStatus Hide();
virtual PRStatus SetLabel(const string& label);
virtual PRStatus HandleCommand();
virtual PRStatus SetGeometry(const int &x, const int &y,
const int &width, const int &height, const char &mask) {
return SetGeometryImpl(x, y, width, height, mask); };
virtual PRStatus GetGeometryRequest(int &x, int &y,
int &width, int &height) {
return GetGeometryRequestImpl(x, y, width, height); };
private:
};
#endif

Notice, other than I decided to implement the bodies of a couple of the member functions directly in the Gtk+ header, that these two concrete implementations are essentially the same, except for the names assigned to the class (and CTOR, etc.). This should be no suprise, since these classes are intended to implement abstract interfaces.

Now let's take a look at Create(), starting first with Gtk+:

PRStatus GtkButtonImpl::Create()
{
m_widget = gtk_button_new();
if (m_widget) {

if (!m_fixedParent) {
WidgetImpl *top = GetRootWidget();
if (top) {
SetFixedParent(top);
}
}

if (m_fixedParent) {
gtk_fixed_put(GTK_FIXED(m_fixedParent), m_widget, 0, 0);
gtk_signal_connect(GTK_OBJECT(m_widget), "clicked",
GTK_SIGNAL_FUNC(HandleCommandThunk), this);
return PR_SUCCESS;
}
}
return PR_FAILURE;
}

And now for Cocoa:

PRStatus CocoaButtonImpl::Create()
{
NSRect graphicsRect = NSMakeRect(1.0, 1.0, 1.0, 1.0);
ButtonAction *action;

action = [ButtonAction alloc];

[action setHandler: this];
// create the button

m_button = [[NSButton alloc] initWithFrame:graphicsRect];

if (m_button) {

[m_button setBezelStyle: NSRoundedBezelStyle];
[m_button setTarget:action];
[m_button setAction:@selector(onClick:)];
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;
}
}
}

return PR_FAILURE;
}

Other than the function prototype (i.e., arguments and return value), there are huge differences evident in the implementation of these two classes. Most obvious is the fact that the Gtk+ code is written in C++, while the Cocoa code is written in Objective-C++. Beyond that, the other major differences are in how the Cocoa button is parented (made a subview of the view maintained by the button's parent in the DOM) and how Gtk+ is parented (made a child of an instance of the GtkFixed widget, which is in turn parented by the toplevel window within which the button exists). The point is that beyond the requirement that the interface to and return value from Create() adhere to the conventions forced upon it by layout, there are absolutely no rules telling you what you can or cannot do when it comes to implementing that interface; you are free to do whatever is needed.