10.1. Creating a New Component

This section describes the general process of creating a new component. The sample class shown here is derived from SoXtRenderArea . The second half of this chapter, beginning with Section 10.2, “Creating a New Viewer”, describes creating a more specialized component, a viewer.

There are no special macros for creating new component classes. Creating a new component requires these steps:

  1. Select a name for the new component class and determine what class it is derived from (see the section called “Overview” for a discussion of deriving new viewers).

  2. Define a constructor for the new class. If you want other programmers to be able to derive classes from your new class, you need to define two constructors for the class, a public constructor and a protected one (see the section called “Defining the Constructor”). If no one will be deriving classes from your new class, you can simply define a public constructor.

  3. Implement show() and hide() methods for your component (optional step). The base class, SoXtComponent , takes care of showing and hiding your new component. But if your component needs to show other components when it is shown, or hide other components when it is hidden, you need to implement these two methods for your new class (see the section called “The show() and hide() Methods”).

  4. Implement the visibility-changed callback function (optional step). This function is called when the component changes state between visible and invisible (see the section called “Visibility-Changed Callback Function”).

See the SoXtComponent.h file for additional methods you may choose to implement. Possibilities include the following:

If you are sure that no one will need to derive classes from your new component, you can simply implement one public constructor. This constructor needs to do the following:

If you want to be sure that programmers can derive new classes from your class, you need to provide a protected constructor in addition to the public constructor. Here's why. The widget tree is built when the component is constructed. If you derive a component subclass, the parent class constructor is called before the constructor of the subclass. This means that the parent class widget is built before the subclass widget. The problem arises if you want the component subclass to provide a container widget for the parent class. The Xt Library requires that a parent widget be supplied when a child widget is created and provides no way to reparent a widget. A little fancy footwork is required for the subclass to provide the parent widget, and that maneuver is provided by the protected constructor.

In Inventor, every SoXtComponent class has two constructors: a public constructor and a protected constructor. The protected constructor has one additional parameter, buildNow, which is a Boolean value that specifies whether to build the widget tree now or later:

protected:
	 SoXtComponent(
		Widget parent,
         		const char *name,
         		SbBool buildInsideParent,
         		SbBool buildNow);

If you use the protected constructor and specify FALSE for buildNow, you can have explicit control over which widgets are built and in what order. For example, your new class may want to build a container widget such as a Motif-style form or bulletin board before it lets the parent class build its widget. In this case, your new class can call its buildWidget() method first and then later it can call the buildWidget() method of its parent class. In Inventor, the SoXtFullViewer class uses this technique. It builds a form widget with user interface trim and then has the parent class, SoXtRenderArea , later build its widget inside this form.

In Inventor, and in Examples 10-1 and 10-2, the basic constructor tasks are put into the constructorCommon() method, which is called by both constructors. Although this is a useful technique, it is not required. The constructorCommon() method is where the actual building of this widget occurs. This method checks the buildNow flag and builds the widget.

Let's analyze the constructorCommon() code in Example 10.2, “ SceneTumble.c++ in a bit more detail. After setting up sensors, a camera, and a light for the component, the following calls are made:

addVisibilityChangeCallback(visibilityChangeCB, this);
setClassName("SceneTumble");
setTitle("Tumble");

// If we do not build now, the subclass will build when ready
if (buildNow) {
   Widget w = buildWidget(getParentWidget());
   setBaseWidget(w);
}

The visibility-changed callback is described in the section called “Visibility-Changed Callback Function”. The setClassName() method sets the name of the class for X resource lookup, which occurs while the widget is being built. The setTitle() method sets the title used in the shell window if this is a top-level component. Although not shown here, you can also call setIconTitle() to set the title used when the component is iconified.

The constructor then checks the buildNow flag. If this flag is TRUE, it builds the widget tree. Also, note that the buildWidget() method uses getParentWidget() to obtain the parent widget, which is not necessarily the parent widget passed in to the constructor. (The parent passed in to the constructor could be NULL, or the buildInsideParent parameter could be FALSE.)

Next, the constructor calls the setBaseWidget() method, which letsSoXtComponent know what the root of the widget tree is. This widget is used for layout, and by the show() and hide() methods.

The base class SoXtComponent will show and hide your new component automatically. However, if your component needs to show or hide other components, you must implement your own show() and hide() methods. In Inventor, if the material editor and color editor are on the screen and the program tells the material editor to hide itself, the material editor needs to tell the color editor to hide itself as well. Similarly, when a viewer hides itself, it also hides the preference sheet if it is visible.

Using addVisibilityChangeCallback(), your new class can register a callback function with SoXtComponent that is called when its visibility changes. A component can be shown, hidden, or iconified; whenever it changes state between visible and invisible (hidden or iconified), the visibility-changed callback function is invoked. This callback is useful, for example, if your component contains a scene with animation. When your component is hidden or iconified, it can stop the animation. Another example of using this callback is the render area, which detaches its redraw sensor when it is hidden or iconified.

See Example 10.2, “ SceneTumble.c++ for an illustration of using visibilityChangeCB().

The following examples show the header file and source code for a simple component derived from SoXtRenderArea . This component animates a camera to rotate the scene. It uses a visibility-changed callback function to stop the tumbling when the component is not visible. A slider at the bottom of the window controls the speed of tumbling.

Example 10.1, “ SceneTumble.h ” shows the include file for the SceneTumble class. Example 10.2, “ SceneTumble.c++ shows the source code for this class.


Example 10.2.  SceneTumble.c++

#include <Xm/Form.h>
#include <Xm/Scale.h>
#include <Inventor/Xt/SoXtResource.h>
#include <Inventor/nodes/SoDirectionalLight.h>
#include <Inventor/nodes/SoPerspectiveCamera.h>
#include <Inventor/nodes/SoRotation.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/sensors/SoTimerSensor.h>
#include "SceneTumble.h"

#define MIN_SPEED 0
#define MAX_SPEED 100

// Speed factor is a small angle
#define SPEED_FACTOR (M_PI/3600.0)

// Public constructor
SceneTumble::SceneTumble(
   Widget parent,
   const char *name, 
   SbBool buildInsideParent)
   : SoXtRenderArea(parent, name, buildInsideParent, FALSE,
                    FALSE)
{
   // Passing TRUE means build the component right now
   constructorCommon(TRUE);
}

// Protected constructor for subclasses to call
SceneTumble::SceneTumble(
   Widget parent,
   const char *name, 
   SbBool buildInsideParent, 
   SbBool buildNow)
   : SoXtRenderArea(parent, name, buildInsideParent, FALSE,
                    FALSE)
{
   // Subclass tells us whether to build now
   constructorCommon(buildNow);
}

// Actual work done at construction time
void
SceneTumble::constructorCommon(SbBool buildNow)
{
   speed = MAX_SPEED/2;
   
   animationSensor = 
        new SoTimerSensor(SceneTumble::animationSensorCB, this);
   animationSensor->setInterval(1/60.0); // 60 frames per second

   userScene = NULL;
   root = new SoSeparator;
   camera = new SoPerspectiveCamera;
   rotx = new SoRotation;
   roty = new SoRotation;
   rotz = new SoRotation;
   
   root->addChild(camera);
   root->addChild(new SoDirectionalLight);
   root->addChild(rotx);
   root->addChild(roty);
   root->addChild(rotz);
   root->ref();
   
   addVisibilityChangeCallback(visibilityChangeCB, this);
   setClassName("SceneTumble");
   setTitle("Tumble");

   // If we do not build now, the subclass will build when ready
   if (buildNow) {
      Widget w = buildWidget(getParentWidget());
      setBaseWidget(w);
   }
}

// Destructor
SceneTumble::~SceneTumble()
{
   root->unref();
   delete animationSensor;
}

// Set the scene graph to tumble. We add this scene graph
// to our local graph so that we can rotate our own camera
// to create the tumbling effect. Our local scene graph
// root is passed to the render area for rendering.
void
SceneTumble::setSceneGraph(SoNode *newScene)
{
   // Replace the existing scene with this one
   if (userScene != NULL)
      root->replaceChild(userScene, newScene);
   else 
      root->addChild(newScene);
   userScene = newScene;
   
   // Make certain the scene is in view
   camera->viewAll(root, getViewportRegion(), 2.0);
   
   // Render area will handle redraws for us
   SoXtRenderArea::setSceneGraph(root);
}

// Return the user's scene graph, not our local graph
SoNode *
SceneTumble::getSceneGraph()
{
   return userScene;
}

// Build the widget - create a form widget, and place
// in it a render area and a scale slider to control
// the speed.
Widget
SceneTumble::buildWidget(Widget parent)
{
   Arg args[8];
   int n;

   // Create a form widget as the container.
   Widget form = XtCreateWidget(getWidgetName(),
                                xmFormWidgetClass, 
                                parent, NULL, 0);
   
   // Register the widget, so we can get resources
   registerWidget(form);
   
   // Get our starting speed from the resource.
   // Resource file should say:
   //    *SceneTumble*speed: <int between 0 and 100>
   short s;
   SoXtResource xr(form);
   if (xr.getResource("speed", "Speed", s)) {
      if (s > MAX_SPEED)
         speed = MAX_SPEED;
      else if (s < MIN_SPEED)
         speed = MIN_SPEED;
      else 
         speed = s;
   }
     
   // Create render area
   Widget raWidget = SoXtRenderArea::buildWidget(form);

   // Create slider to control speed
   n = 0;
   XtSetArg(args[n], XmNminimum, MIN_SPEED); n++;
   XtSetArg(args[n], XmNmaximum, MAX_SPEED); n++;
   XtSetArg(args[n], XmNvalue, speed); n++;
   XtSetArg(args[n], XmNorientation, XmHORIZONTAL); n++;
   speedSlider =
     XtCreateWidget("Speed", xmScaleWidgetClass, form, args, n);

   // Callbacks on the slider
   XtAddCallback(speedSlider, XmNdragCallback,
                 SceneTumble::speedCB, this);
   XtAddCallback(speedSlider, XmNvalueChangedCallback,
                 SceneTumble::speedCB, this);
   
   // Layout 
   n = 0;
   XtSetArg(args[n], XmNtopAttachment, XmNONE); n++;
   XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
   XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
   XtSetArg(args[n], XmNbottomAttachment, XmATTACH_FORM); n++;
   XtSetValues(speedSlider, args, n);
   
   n = 0;
   XtSetArg(args[n], XmNtopAttachment, XmATTACH_FORM); n++;
   XtSetArg(args[n], XmNleftAttachment, XmATTACH_FORM); n++;
   XtSetArg(args[n], XmNrightAttachment, XmATTACH_FORM); n++;
   XtSetArg(args[n], XmNbottomAttachment,XmATTACH_WIDGET); n++;
   XtSetArg(args[n], XmNbottomWidget, speedSlider); n++;
   XtSetValues(raWidget, args, n);
   
   // Make the widgets visible
   XtManageChild(speedSlider);
   XtManageChild(raWidget);
   
   return form;
}

// Do the tumble animation. This entails updating our three
// rotation nodes, one each for the x,y,and z axes.
void
SceneTumble::doTumbleAnimation()
{ 
   SbRotation r;
   float angle;
   
   // Rotate about three axes in three speeds
   angle = speed * SPEED_FACTOR;
   r = rotx->rotation.getValue() * SbRotation(SbVec3f(1, 0, 0),
                                              angle);
   rotx->rotation.setValue(r);

   angle = speed * SPEED_FACTOR * 1.5;
   r = roty->rotation.getValue() * SbRotation(SbVec3f(0, 1, 0),
                                              angle);
   roty->rotation.setValue(r);

   angle = speed * SPEED_FACTOR * 2.0;
   r = rotz->rotation.getValue() * SbRotation(SbVec3f(0, 0, 1),
                                              angle);
   rotz->rotation.setValue(r);
}

// Turn tumbling on and off. We simply schedule or unschedule
// the animation sensor.
void
SceneTumble::setTumbling(SbBool onOff)
{ 
   if (onOff) 
     animationSensor->schedule();
   else 
      animationSensor->unschedule();
}

// Return whether we are tumbling.
SbBool
SceneTumble::isTumbling() const
{
   return animationSensor->isScheduled();
}

// This is called when the render area visibility changes
// because it is shown, hidden, or iconified. If the 
// component is not visible, we turn off the tumble animation.
void
SceneTumble::visibilityChangeCB(void *userData, SbBool visible)
{
   // Set tumbling on when the component is visible,
   // and set it off when the component is not visible.
   SceneTumble *tumbler = (SceneTumble *) userData;   
   tumbler->setTumbling(visible);
}

// Animation sensor callback keeps the tumbling going.
void
SceneTumble::animationSensorCB(void *userData, SoSensor *)
{ 
   ((SceneTumble *) userData)->doTumbleAnimation();
}

// This is invoked when the speed slider changes value.
// We use the value of the slider to change the tumble speed.
void
SceneTumble::speedCB(Widget, XtPointer userData, 
                     XtPointer clientData)
{
   SceneTumble *tumbler = (SceneTumble *) userData;
   XmScaleCallbackStruct *data = (XmScaleCallbackStruct *)
                                 clientData;
   tumbler->setSpeed(data->value);
}