Chapter 10. Creating a Component

This chapter describes how to create your own Inventor Xt component. Before creating a new Xt component, be sure to read Chapters 10 and 16 in The Inventor Mentor.

The first part of this chapter offers an overview of the steps required to create a new component. The second part of the chapter describes the additional steps required to create a new viewer, which is a specific type of component. The chapter examples show creating two new component classes:

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 “Creating a New Viewer”, describes creating a more specialized component, a viewer.

Overview

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 “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 “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 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 “Visibility-Changed Callback Function”).

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

windowCloseAction()  


specifies what happens when the user closes the window using the Window Manager (for example, you could implement this method to exit the program or delete the component)

openHelpCard() 

opens a help card for the component

Defining the Constructor

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:

  • Build the widget (or the widget tree if your component is made up of more than one widget)

  • Call setBaseWidget() so that the SoXtComponent base class methods—such as show() and hide()—work properly

Defining Public and Protected Constructors

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 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 “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 lets SoXtComponent know what the root of the widget tree is. This widget is used for layout, and by the show() and hide() methods.

The buildWidget() Method

In Inventor, because we want to allow subclasses to have explicit control over building the widget tree, we implement a separate buildWidget() method. If you are providing only the public constructor, you can simply build the widget in the constructor and do not need to create a separate buildWidget() method. This method, called by the constructor of your new class (or by subclasses of your new class), builds the widget hierarchy and returns its topmost widget.

If your widget supports X resources, be sure to call registerWidget() immediately after you create the topmost container widget and before you build the rest of the widget hierarchy. This method associates the Motif-style widget with the Inventor component to which it belongs. When you create other widgets in the hierarchy, Inventor uses the class name of the component instead of the widget name during resource lookup. For example, the base widget of a render area is a Motif-style bulletin board. Once you have called registerWidget(), you can set the background color resource directly on the render area without affecting other bulletin board widgets in your hierarchy.

To define and retrieve your own resources, see the Open Inventor C++ Reference Manual on SoXtResource. For more information on X resources, see the Xlib Programming Manual by Adrian Nye (O'Reilly & Associates, 1990).

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.

Visibility-Changed Callback Function

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 for an illustration of using visibilityChangeCB().

Creating a Simple Component

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 shows the include file for the SceneTumble class. Example 10-2 shows the source code for this class.

Example 10-1. SceneTumble.h


#include <Inventor/Xt/SoXtRenderArea.h>

class SoPerspectiveCamera;
class SoRotation;
class SoSeparator;
class SoTimerSensor;

class SceneTumble : public SoXtRenderArea {

public:
   
   // Constructor for public consumption
   SceneTumble(
     Widget parent = NULL,
     const char *name = NULL, 
     SbBool buildInsideParent = TRUE);
   ~SceneTumble();

   virtual void setSceneGraph(SoNode *newScene);
   virtual SoNode *getSceneGraph();

   void setTumbling(SbBool onOff);
   SbBool isTumbling() const;
   
   protected:
   
   // Constructor subclasses can call if they don't want the
   // widget built right away (i.e. the subclass wants to create
   // a container widget first.)
   SceneTumble(
     Widget parent,
     const char *name, 
     SbBool buildInsideParent, 
     SbBool buildNow);

   Widget buildWidget(Widget parent);

   void doTumbleAnimation();

   void setSpeed(int s) { speed = s; }
   int getSpeed() const { return speed; }

   Widget speedSlider;
   
private:

   SoNode * userScene;
      SoPerspectiveCamera *camera;
   SoRotation *rotx;
   SoRotation *roty;
   SoRotation *rotz;
   SoSeparator *root;
   int speed;
   SoTimerSensor *animationSensor;
   
   void constructorCommon(SbBool buildNow);
   static void visibilityChangeCB(void *userData,
                                  SbBool visible);
   static void animationSensorCB(void *userData, SoSensor *);
   static void speedCB(Widget, XtPointer, XtPointer);
}; 

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);
}

Creating a New Viewer

Viewers are subclassed from SoXtRenderArea and can be thought of as smart rendering windows that respond to events and modify the camera. You can use one of the following classes as a base class for your own viewer:

  • SoXtViewer

  • SoXtFullViewer

  • SoXtConstrainedViewer

SoXtViewer, the lowest base class for viewers, adds the notion of a camera to an SoXtRenderArea. The camera through which the scene is viewed is either found in the scene or created automatically by the viewer. SoXtFullViewer adds a decoration trim around the rendering area, which includes thumbwheels, a zoom slider, and push buttons. This class also creates a pop-up menu and a preference sheet with generic viewer functionality built into them. SoXtConstrainedViewer, the last base class, is used for viewers that have the notion of a world up-direction. These viewers constrain the camera orientation to prevent the user from being upside down relative to the given world up-direction (which defaults to +y).

The SoXtViewer class provides basic viewer functionality. This base class provides methods for changing the camera, including setCamera(), getCamera(), viewAll(), saveHomePosition(), and resetToHome-
Position()
. SoXtViewer also adds a headlight, as well as drawing styles, buffering types, and autoclipping. In addition, SoXtViewer adds support for seek, copy, and paste, enabling subclasses to redefine how a seek or a paste is performed.

Overview

If you create a viewer that is a subclass of SoXtViewer, you perform the following steps. Examples 10-3 and 10-4 illustrate each step.

  1. Construct the viewer (required step for all new viewers). See “Defining the Constructor”.

  2. Implement the event-processing routines (required step for all new viewers). These routines include processEvent() and processCommonEvents(), which in turn calls translateCamera() and switchMode(). See “Defining the Event-Processing Routines”.

  3. Implement the seek function (optional step). See “Implementing the Seek Function”.

  4. Modify the cursor to be used for feedback (optional step). See “Using the Cursor for Feedback”.

    If you create a viewer that is subclassed from SoXtFullViewer, you can perform the following steps, in addition to steps 1 through 4. Only step 5 is required; the other steps are optional.

  5. Modify how the trim decoration is used (required step). See “Using the SoXtFullViewer Trim Decoration”.

  6. Add push buttons (optional step). See “Adding Push Buttons”.

  7. Change the preference sheet (optional step). See “Changing the Preference Sheet”.

  8. Change the pop-up menu (optional step). See “Changing the Pop-up Menu”.

  9. Change the trim decoration (optional step). See “Changing the Decoration Layout”.

    If you create a viewer that is subclassed from SoXtConstrainedViewer,
    you can perform the following step, in addition to steps 1 through 9. See “Creating a Constrained Viewer” for more information on creating a viewer subclassed from SoXtConstrainedViewer.

  10. Define constraints for the viewer.

Examples 10-3 and 10-4 show how to create a simple viewer derived from SoXtFullViewer, similar to the SoXtPlaneViewer. The left mouse button is used to translate the camera in the viewer plane and to seek objects. This new viewer redefines the decoration thumbwheels to translate the camera. It also defines mouse cursors to reflect the viewer's current state (viewing, seeking, or picking).

Defining the Constructor

The constructor for the viewer takes an

SoXtViewer::Type

parameter, which specifies whether the viewer is of type BROWSER (the default) or



EDITOR. This argument specifies the camera creation policy of the viewer. For more information on component constructors, see “Defining the Constructor”.

Defining the Event-Processing Routines

Any new viewer must implement the processEvent() routine, defined in SoXtRenderArea, to send events directly to the scene graph. When viewing is turned on, the new viewer uses those events to manipulate the camera.

The base-class routine processCommonEvents(), defined in SoXtViewer, is first called when an event is received. This routine is used to handle a set of events that should be common across all viewers. These events are as follows:

  • Pressing the Escape key (toggles a viewer between Viewing and Picking mode)

  • Pressing the Home key (resets the viewer to Home Position)

  • Pressing the S key (toggles Seek mode on and off)

  • Pressing any of the four arrow keys (moves the camera left, right, up, or down)

The viewer calls the base class interactiveCountInc() and interactiveCountDec() methods when the left mouse button is pressed and released. These methods enable the viewer base class to call the user interactive start and finish callbacks and are also used to change the drawing styles and buffering types when interactive styles are chosen (for example, move wireframe).

The code to translate the camera using the new mouse position is called within the processEvent() routine (see the translateCamera() method in Example 10-4).

The switchMode() routine called within processEvent() switches between viewer modes. It also sets the correct cursor on the window (see “Using the Cursor for Feedback”).

In addition to showing how the camera can be translated given the mouse events received, the processEvent() routine also shows how seek functionality is supported in the viewer. This topic is explained in detail in the following section.

Implementing the Seek Function

The seek function moves the camera to the picked point (when detail seek is ON) or to the picked object (when detail seek is OFF). The seek functionality for viewers is provided by the base class SoXtViewer. The following public methods can be called to set seek parameters:

setDetailSeek() 

specifies whether to orient the camera toward the picked point (detail seek is ON), or toward the center of the object's bounding box (detail seek is OFF). The default is ON.

isDetailSeek()  

returns whether detail seek is ON.

setSeekTime() 

sets the time for a seek to animate the new camera location. The default time is 2 seconds.

getSeekTime() 

returns the seek time.

Subclasses can then simply call seekToPoint(), passing the mouse location, and the base class SoXtViewer performs the seek animation. By default, detail seek is ON, and the base class changes the camera to be the focal distance away from the picked point and aligned to the point normal. When detail seek is OFF, the camera centers itself on the object's bounding box and keeps its orientation the same (the picked point has no real importance; only the picked object is used in this case).

Our simple viewer example uses the seek functionality defined in the base class. If a viewer needs to redefine how seek is performed (SoXtFlyViewer and SoXtWalkViewer redefine it), the viewer can redefine the interpolateSeekAnimation() routine, which changes the camera.

The following protected variables are defined in SoXtViewer to help you redefine interpolateSeekAnimation() for a new viewer:

seekPoint, seekNormal  


point and normal to seek

oldCamOrientation, newCamOrientation  


old and new camera orientation

oldCamPosition, newCamPosition  


old and new camera position

seekDistance 

seek distance (either a percentage or an absolute value)

seekDistAsPercentage  


whether the seek distance is a percentage or an absolute value

computeSeekVariables  


whether the final camera seek values have been computed. This flag is set to FALSE when interpolateSeekAnimation() is first called on a new seek animation.

Using the Cursor for Feedback

It is often desirable to have a viewer change the cursor to reflect the viewer's state. The file SoXtCursors.h defines a set of X bitmaps that can be used for defining cursors. Some of these bitmaps were created for specific viewers, whereas others are generic enough to be reused across viewers. Most viewers have a different cursor during viewing and nonviewing modes. A generic viewer cursor is supplied in the file SoXtCursors.h.

To have a different cursor when viewing is on, the viewer needs to redefine the setViewing() method to set the correct cursor on the window. Similarly, if the viewer supports the seek functionality, it also needs to redefine the setSeekMode() method to change the cursor. Example 10-4 shows how to change the cursor.

The defineCursors() routine, also shown in Example 10-4, needs to be called only once to create the X cursor.

X cursors can be defined only when the widget is actually mapped onto the screen. It is thus a good idea to define the cursors whenever the first event is received in the processEvent() routine, since an event guarantees that the window is mapped onto the screen.

Cursors should be defined on the render-area window, not on the window found in the X event structure. This is because the actual window events are received from changes when the viewer switches between single- and double-buffering. The render-area window, however, stays constant, so the cursor is correctly specified.

The following sections deal with more advanced features of viewers, such as the trim decoration around the render area and the viewer pop-up menu.

Example 10-3 shows the header file for the new simple viewer class.

Example 10-3. SimpleViewer.h


#include <Inventor/SbLinear.h>
#include <Inventor/Xt/viewers/SoXtFullViewer.h>

class simpleViewer : public SoXtFullViewer {
 public:
   // Constructor/destructor
   simpleViewer(
      Widget parent = NULL,
      const char *name = NULL, 
      SbBool buildInsideParent = TRUE, 
      SoXtFullViewer::BuildFlag flag = BUILD_ALL, 
      SoXtViewer::Type type = BROWSER);
   ~simpleViewer();
   
   // Redefine this to also change the cursor (viewerCursor)
   virtual void        setViewing(SbBool onOrOff);
   
 protected:
   // Redefine this to process the events
   virtual void        processEvent(XAnyEvent *anyevent);
   
   // Redefine this to also change the cursor (seekCursor)
   virtual void        setSeekMode(SbBool onOrOff);
   
   // Define these thumbwheels to translate in the viewer plane
   virtual void        bottomWheelMotion(float newVal);
   virtual void        leftWheelMotion(float newVal);
   virtual void        rightWheelMotion(float newVal);
   virtual void        bottomWheelStart();
   virtual void        leftWheelStart();
   
   // Redefine this to customize the preference sheet
   virtual void        createPrefSheet();
   
   // Define this to bring up the viewer help card
   virtual void        openViewerHelpCard();
   
 private:
   // Viewer state variables
   int             mode;
   SbBool          createdCursors;
   Cursor          vwrCursor, seekCursor;
   SbVec2s         locator; // mouse position
   
   // Camera translation variables
   SbVec3f         locator3D;
   SbPlane         focalplane;
   float           transXspeed, transYspeed;
   
   void            switchMode(int newMode);
   void            defineCursors();
   void            translateCamera();
   void            computeTranslateValues();
};

Example 10-4. SimpleViewer.c++


#include <math.h>

#include <X11/Intrinsic.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>

#include <Inventor/nodes/SoOrthographicCamera.h>
#include <Inventor/nodes/SoPerspectiveCamera.h>
#include <Inventor/Xt/SoXtCursors.h>
#include "simpleViewer.h"

enum ViewerModes {
   IDLE_MODE, 
   TRANS_MODE, 
   SEEK_MODE, 
};

// Constructor for the viewer
simpleViewer::simpleViewer(
   Widget parent,
   const char *name, 
   SbBool buildInsideParent, 
   SoXtFullViewer::BuildFlag b, 
   SoXtViewer::Type t)
      : SoXtFullViewer(
         parent,
         name, 
         buildInsideParent, 
         b, 
         t, 
         TRUE) // Tell base class to build (since we don't add
               // anything)
{
   // Init local vars
   mode = IDLE_MODE;
   createdCursors = FALSE;
   setSize(SbVec2s(520, 360)); //def size
   
   // assign decoration titles
   setPopupMenuString("Simple Viewer");
   setBottomWheelString("transX");
   setLeftWheelString("transY");
   setRightWheelString("Dolly");
   setPrefSheetString("Simple Viewer Preference Sheet");
   setTitle("Simple Viewer");
}

simpleViewer::~simpleViewer()
{
}

// Call the base class and set the correct cursor 
// on the window
void
simpleViewer::setViewing(SbBool flag)
{
   if (flag == viewingFlag || camera == NULL) {
      viewingFlag = flag;
      return;
   }
   
   // Call the base class
   SoXtFullViewer::setViewing(flag);
   
   // Set the right cursor
   Widget w = getRenderAreaWidget();
   if (w != NULL && XtWindow(w) != NULL) {
      if (isViewing()) {
         if (! createdCursors)
            defineCursors();
         XDefineCursor(XtDisplay(w), XtWindow(w), vwrCursor);
      }
      else
         XUndefineCursor(XtDisplay(w), XtWindow(w));
   }
}

// Process the given event to change the camera
void
simpleViewer::processEvent(XAnyEvent *xe)
{
   // Let the base class handle the common set of events
   if (processCommonEvents(xe))
      return;
   
   // Check if cursors need to be defined (they can only
   // be defined after the window has been mapped. 
   // Receiving events guarantees that the window has 
   // been mapped.
   if (! createdCursors) {
      defineCursors();
      Widget w = getRenderAreaWidget();
      XDefineCursor(XtDisplay(w), XtWindow(w), vwrCursor);
   }
   
   XButtonEvent    *be;
   XMotionEvent    *me;
   SbVec2s windowSize = getGlxSize();
   
   switch (xe->type) {
      case ButtonPress:
         be = (XButtonEvent *) xe;
         locator[0] = be->x;
         locator[1] = windowSize[1] - be->y;
         if (be->button == Button1) {
            switch (mode) {
               case IDLE_MODE: 
                  interactiveCountInc();
                  switchMode(TRANS_MODE);
                  break;
               case SEEK_MODE:
                  seekToPoint(locator);
                  break;
            }
         }
         break;
         
      case ButtonRelease:
         be = (XButtonEvent *) xe;
         if (be->button == Button1 && mode == TRANS_MODE) {
            switchMode(IDLE_MODE);
            interactiveCountDec();
         }
         break;
         
      case MotionNotify:
         me = (XMotionEvent *) xe;
         locator[0] = me->x;
         locator[1] = windowSize[1] - me->y;
         if (mode == TRANS_MODE)
            translateCamera();
         break;
   }
}

// Switches to the specified viewer mode. The correct
// cursor is also set on the window.
void
simpleViewer::switchMode(int newMode)
{
   // needed to define new cursors
   Widget w = getRenderAreaWidget();
   Display *display = XtDisplay(w);
   Window window = XtWindow(w);
   if (! createdCursors)
      defineCursors();
   
   // Switch to new viewer mode
   mode = newMode;
   switch (mode) {
      case IDLE_MODE:
         if (window != 0)
            XDefineCursor(display, window, vwrCursor);
         break;
         
      case TRANS_MODE:
         {
            // Figure out the focal plane
            SbMatrix mx;
            mx = camera->orientation.getValue();
            SbVec3f forward(-mx[2][0], -mx[2][1], -mx[2][2]);
            SbVec3f fp = camera->position.getValue() + 
               forward * camera->focalDistance.getValue();
            focalplane = SbPlane(forward, fp);
            
            // Map mouse position onto the viewing plane
            SbVec2s windowSize = getGlxSize();
            SbLine line;
            SbViewVolume cameraVolume = camera->getViewVolume();
            cameraVolume.projectPointToLine(
                  SbVec2f( locator[0] / float(windowSize[0]), 
                  locator[1] / float(windowSize[1])), line);
            focalplane.intersect(line, locator3D);
         }
         if (window != 0)
            XDefineCursor(display, window, vwrCursor);
         break;
      
      case SEEK_MODE:
         if (window != 0)
            XDefineCursor(display, window, seekCursor);
         break;
   }
}

// Call the base class and set the correct cursor 
// on the window.
void
simpleViewer::setSeekMode(SbBool flag)
{
   if (! isViewing())
      return;
   
   // Call the base class
   SoXtFullViewer::setSeekMode(flag);
   
   // Switch to the right mode
   switchMode(isSeekMode() ? SEEK_MODE : IDLE_MODE);
}

// Redefine this routine to customize the preference sheet
void
simpleViewer::createPrefSheet()
{
   // Create the preference sheet shell and form widget
   Widget shell, form;
   createPrefSheetShellAndForm(shell, form);
   
   // Create most of the default parts
   Widget widgetList[10];
   int num = 0;
   widgetList[num++] = createSeekPrefSheetGuts(form);
   widgetList[num++] = createZoomPrefSheetGuts(form);
   widgetList[num++] = createClippingPrefSheetGuts(form);
   
   layoutPartsAndMapPrefSheet(widgetList, num, form, shell);
}

// Bring up the viewer help card (called by "?" push button)
void
simpleViewer::openViewerHelpCard()
{
   // Tell the component to open the file for us
   openHelpCard("simpleViewer.help");
}

// Translate the camera right/left (called by thumbwheel).
void
simpleViewer::bottomWheelMotion(float newVal)
{
   if (camera == NULL)
      return;
   
   // Get camera right vector and translate by wheel 
   // delta rotation
   SbMatrix mx;
   mx = camera->orientation.getValue();
   SbVec3f rightVector(mx[0][0], mx[0][1], mx[0][2]);
   float dist = transXspeed * (bottomWheelVal - newVal);
   camera->position = camera->position.getValue() + 
      dist * rightVector;
   
   bottomWheelVal = newVal;
}

// Translate the camera up/down (called by thumbwheel).
void
simpleViewer::leftWheelMotion(float newVal)
{
   if (camera == NULL)
      return;
   
   // Get camera up vector and translate by wheel 
   // delta rotation
   SbMatrix mx;
   mx = camera->orientation.getValue();
   SbVec3f upVector(mx[1][0], mx[1][1], mx[1][2]);
   float dist = transYspeed * (leftWheelVal - newVal);
   camera->position = camera->position.getValue() + 
      dist * upVector;
   
   leftWheelVal = newVal;
}

// Moves the camera closer/further away from the plane 
// of interest, which is defined by the viewing normal 
// and the camera focalDistance field value.
void
simpleViewer::rightWheelMotion(float newVal)
{
   if (camera == NULL)
      return;
   
   // Shorten/grow the focal distance given the wheel rotation
   float focalDistance = camera->focalDistance.getValue();
   float newFocalDist = focalDistance / 
                     pow(2.0, newVal - rightWheelVal);
   
   // Finally, reposition the camera
   SbMatrix mx;
   mx = camera->orientation.getValue();
   SbVec3f forward(-mx[2][0], -mx[2][1], -mx[2][2]);
   camera->position = camera->position.getValue() + 
                  (focalDistance - newFocalDist) * forward;
   camera->focalDistance = newFocalDist;
   
   rightWheelVal = newVal;
}

// This routine is used to define cursors, which can 
// only be called after the window has been realized.
void
simpleViewer::defineCursors()
{
   XColor foreground;
   Pixmap source;
   Display *display = getDisplay();
   Drawable d = DefaultRootWindow(display);
   
   // Set a red color
   foreground.red = 65535;
   foreground.green = foreground.blue = 0;
   
   // View plane translate cursor
   source = XCreateBitmapFromData(display, d, 
            so_xt_flat_hand_bits, so_xt_flat_hand_width,
            so_xt_flat_hand_height);
   vwrCursor = XCreatePixmapCursor(display, source, source, 
               &foreground, &foreground, so_xt_flat_hand_x_hot, 
               so_xt_flat_hand_y_hot);
   XFreePixmap(display, source);
   
   // Seek cursor
   source = XCreateBitmapFromData(display, d,
            so_xt_target_bits, so_xt_target_width,
            so_xt_target_height);
   seekCursor = XCreatePixmapCursor(display, source, source, 
                &foreground, &foreground, so_xt_target_x_hot,
                so_xt_target_y_hot);
   XFreePixmap(display, source);
   
   createdCursors = TRUE;
}

// Moves the camera into the plane defined by the camera 
// forward vector and the focal point (using the camera
// focalDistance field) to follow the new mouse location.
void
simpleViewer::translateCamera()
{
   if (camera == NULL)
      return;
   
   SbVec2s windowSize = getGlxSize();
   SbVec2f newLocator(locator[0] / float(windowSize[0]), 
      locator[1] / float(windowSize[1]));
   
   // Map new mouse location into the camera focal plane
   SbLine          line;
   SbVec3f         newLocator3D;
   SbViewVolume cameraVolume = camera->getViewVolume();
   cameraVolume.projectPointToLine(newLocator, line);
   focalplane.intersect(line, newLocator3D);
   
   // Move the camera by the delta 3D position amount
   camera->position = camera->position.getValue() + 
      (locator3D - newLocator3D);
   
   // You would think we would have to set locator3D to
   // newLocator3D here.  But we don't, because moving 
   // the camera essentially makes locator3D equal to 
   // newLocator3D in the transformed space, and we will 
   // project the next newLocator3D in this transformed space.
}

// Called by the bottom and left thumbwheels to compute 
// the translation factors (how fast should we translate 
// given a wheel rotation).
void
simpleViewer::computeTranslateValues()
{
   if (camera == NULL)
      return;
   
   float height;
   
   if (camera->isOfType( 
      SoPerspectiveCamera::getClassTypeId())) {
      float angle = ((SoPerspectiveCamera *) 
                    camera)->heightAngle.getValue() / 2;
      float dist = camera->focalDistance.getValue();
      height = dist * ftan(angle);
   }
   else if (camera->isOfType( 
      SoOrthographicCamera::getClassTypeId()))
      height = ((SoOrthographicCamera *) 
               camera)->height.getValue() / 2;
   
   // Given the size of the viewing plane, figure out 
   // the up/down and right/left speeds for the thumb wheels.
   transYspeed = height / 2;
   transXspeed = transYspeed * camera->aspectRatio.getValue();
}

// Thumbwheels start callbacks
void
simpleViewer::bottomWheelStart()
{
   computeTranslateValues();
   
   // call parent class
   SoXtFullViewer::bottomWheelStart();
}

void
simpleViewer::leftWheelStart()
{
   computeTranslateValues();
   
   // call parent class
   SoXtFullViewer::leftWheelStart();
}

Using the SoXtFullViewer Trim Decoration

SoXtFullViewer is used as the base class for most viewers. This abstract class adds a decoration around the render area, a pop-up menu with viewer functions, and a preference sheet that can be used to customize a specific viewer. The decoration around the render area includes thumbwheels that duplicate direct viewing manipulation, a slider to change the camera zooming factor, and viewer/application push buttons. By default, the base class creates push buttons for viewing, home, set home, view all, and seek. Subclasses can easily add viewer-specific push buttons, as well as change the look of the decoration and the preference sheet. The creation of the decoration and preference sheet is accomplished by many small routines, so subclasses can redefine as much or as little as necessary.

SoXtFullViewer provides three thumbwheels around the render area. By default, these thumbwheels do nothing in the SoXtFullViewer base class and should therefore be implemented by each subclass. The subclass should implement functions so that the bottom and left thumbwheels duplicate the right-left and up-down functionality of the mouse during direct viewing. The right thumbwheel is used to dolly the camera (move forward and backward).

The simple viewer example defines the thumbwheel functionality by redefining methods from the base class as shown in Example 10-4.

For convenience, when you are defining thumbwheel functionality and redefining the decoration layout, the base class SoXtFullViewer provides the following thumbwheel variables. These variables include thumbwheel widgets, previous values (helpful for obtaining incremental rotation), and labels:

rightWheel, bottomWheel, leftWheel  


thumbwheel widget variables

rightWheelStr, bottomWheelStr, leftWheelStr  


string label for each thumbwheel

rightWheelVal, bottomWheelVal, leftWheelVal  


previous value of each thumbwheel

rightWheelLabel, bottomWheelLabel, leftWheelLabel  


widget label for each thumbwheel

When a viewer is derived from SoXtFullViewer, it should set the correct labels on the thumbwheels, the pop-up menu, the preference sheet, and the window title. This needs to be done only once and therefore should be done in the constructor. Example 10-4 shows the code to fully support the SoXtFullViewer base class.

Adding Push Buttons

By default, the base class SoXtFullViewer creates a list of push buttons (XmPushButton widgets with pixmaps). The method buildViewer-
Buttons()
, which subclasses do not need to redefine, uses a list of push buttons to construct all the buttons within a form widget. The button's form widget is then laid out within the right-trim form widget. Subclasses can easily add or remove any number of buttons from the existing list of buttons by redefining the createViewerButtons() method and appending, inserting, and removing from the SbPList of buttons.

Our simple viewer example does not add any new viewer buttons, but here is some sample code that adds a push button to the existing list.

void
SoXtExaminerViewer::createViewerButtons(Widget parent)
{
   // Get the default buttons
   SoXtFullViewer::createViewerButtons(parent);
    
   // Allocate our buttons - this simple case doesn't
   // set the XmNlabelType to be a pixmap, just a simple letter.
   Arg args[2];
   int n = 0;
   XtSetArg(args[n], XmNshadowThickness, 2); n++;
   XtSetArg(args[n], XmNhighlightThickness, 0); n++;
   Widget button = XmCreatePushButtonGadget(parent, "P", args,
                                            n);
   XtAddCallback(button, XmNactivateCallback,
                 (XtCallbackProc)
                 SoXtExaminerViewer::pushButtonCB, 
                 (XtPointer) this);
    
   // Add this button to the list to have it laid out by the
   // parent class (removing a widget from the list will
   // prevent the corresponding push button from being laid
   // out and managed; therefore it will not show up in the
   // decoration).
   viewerButtonWidgets->append(button);
}

Look at the file SbPList.h for a description of methods available on the SoXtFullViewer::viewerButtonWidgets protected variable.


Note: The viewer default push buttons all have a 24-by-24-pixel size, and the decoration trim is designed with that in mind. It is therefore recommended that you create pixmaps of this size for viewer or application push buttons.


Changing the Preference Sheet

Preference sheets allow the user to customize the behavior of a viewer. A default preference sheet is created by the SoXtFullViewer class. Subclasses typically make changes to the default preference sheet to give the user control over viewer-specific parameters. Like the decoration in SoXtFullViewer, the preference sheet is made up of many small building blocks to make it easier for subclasses to redefine it. The following protected methods are used to build the different parts of the preference sheet:

void setPrefSheetString(const char *name);

virtual void createPrefSheet();

void createPrefSheetShellAndForm(Widget &shell, Widget &form);

void createDefaultPrefSheetParts(Widget widgetList[ ], int &num,
Widget form);

void layoutPartsAndMapPrefSheet(Widget widgetList[ ], int num,
Widget form, Widget shell);

Widget createSeekPrefSheetGuts(Widget parent);

Widget createSeekDistPrefSheetGuts(Widget parent);

Widget createZoomPrefSheetGuts(Widget parent);

Widget createClippingPrefSheetGuts(Widget parent);

Widget createStereoPrefSheetGuts(Widget parent);

To change only the preference-sheet title, use the setPrefSheetString() method. Use the createPrefSheet() method to redefine the preference sheet for a subclass, as follows:

//	 This creates the preference sheet in a separate window. It
// calls other routines to create the actual content of the 
// sheet.
void
SoXtFullViewer::createPrefSheet()
{
	   // Create the preference sheet shell and form widget
	   Widget shell, form;
	   createPrefSheetShellAndForm(shell, form);
	
   	// Create all of the default parts
	   Widget widgetList[10];
   	int num = 0;
   	createDefaultPrefSheetParts(widgetList, num, form);
	
	   layoutPartsAndMapPrefSheet(widgetList, num, form, shell);
}

// 	This simply creates the default parts of the pref sheet.
void
SoXtFullViewer::createDefaultPrefSheetParts(
                     Widget widgetList[], int &num, Widget form)
{
	   widgetList[num++] = createSeekPrefSheetGuts(form);
	   widgetList[num++] = createSeekDistPrefSheetGuts(form);
	   widgetList[num++] = createZoomPrefSheetGuts(form);
	   widgetList[num++] = createClippingPrefSheetGuts(form);
	   widgetList[num++] = createStereoPrefSheetGuts(form);
}

When a subclass creates its own preference sheet, it only needs to redefine the createPrefSheet() routine and write it like the base class routine. The simple viewer example redefines the preference sheet to omit some of the default parts. Example 10-4 shows the createPrefSheet() method for simpleViewer.

Subclasses can easily add new items to the preference sheet by adding them to the widget list that is passed to the layoutPartsAndMapPrefSheet() method, just like the default parts. The custom items should all be built within a form widget that is automatically laid out and managed within the layoutPartsAndMapPrefSheet() method. The layout is from top to bottom in the shell widget.

The preference-sheet widget and all of its child widgets are destroyed when the preference-sheet window is closed by the user. This behavior is intended, since the preference sheet is only a temporary window, and we don't want to carry the unwanted widget around when it is no longer needed.

Changing the Pop-up Menu

The SoXtFullViewer pop-up menu, which includes a rich set of viewer functions, can be changed in subclasses by redefining any of the following pop-up menu build routines:

void setPopupMenuString(const char *name);

virtual void buildPopupMenu();

Widget buildFunctionsSubmenu(Widget popup);

Widget buildDrawStyleSubmenu(Widget popup);

To change the pop-up menu title, use the setPopupMenuString() method. To change the pop-up menu, subclasses can redefine the buildPopupMenu() method. Subclasses can also append new entries to the pop-up menu by directly adding a Motif-compliant xmToggleButton or xmPushButton to the pop-up menu widget.

Changing the Decoration Layout

On rare occasions, you may want to change the decoration surrounding the rendering area. The SoXtWalkViewer class, for example, adds an extra thumbwheel and label in the left-hand trim. To simplify the redefining of the decoration, the base class SoXtFullViewer constructs the decoration in many small and manageable steps. The following functions are used to create the decoration and can be redefined by subclasses at any level:

Widget buildWidget(Widget parent);

virtual void buildDecoration(Widget parent);

virtual Widget buildLeftTrim(Widget parent);

virtual Widget buildBottomTrim(Widget parent);

virtual Widget buildRightTrim(Widget parent);

virtual Widget buildZoomSlider(Widget parent);

virtual Widget buildViewerButtons(Widget parent);

virtual Widget buildAppButtons(Widget parent);

void setBottomWheelString(const char *name);

void setLeftWheelString(const char *name);

void setRightWheelString(const char *name);

Example 10-5 contains pseudocode that shows how the decoration is built in the base class SoXtFullViewer. Only important parts of the code are given to illustrate how the decoration is built.

Example 10-5. Building the Viewer Decoration in SoXtFullViewer


// Build the render area and the decoration trim within the
// given parent widget.
Widget
SoXtFullViewer::buildWidget(Widget parent)
{
   // Create a form to hold everything together
   mgrWidget = XtCreateWidget(getWidgetName(),
                              xmFormWidgetClass, parent, 
                              args, n);
    
   // Build the render area and the decoration
   raWidget = SoXtRenderArea::buildWidget(mgrWidget);
   if (decorationFlag)
      	buildDecoration(mgrWidget);
    
   // Lay out and manage the render area and decoration
   ...
    
   return mgrWidget;
}

// Build the viewer decoration (left, right and bottom trim)
void
SoXtFullViewer::buildDecoration(Widget parent)
{
   // Build the trim sides
   leftTrimForm = buildLeftTrim(parent);
   bottomTrimForm = buildBottomTrim(parent);
   rightTrimForm = buildRightTrim(parent);
    
   // Lay out the trims but let the buildWidget() manage them
   ...
}

// Build the left trim decoration
Widget
SoXtFullViewer::buildLeftTrim(Widget parent)
{
   // Create a form to hold all the parts
   Widget form = XtCreateWidget("LeftTrimForm",
                                 xmFormWidgetClass, parent,
                                 NULL, 0);
    
   // Create all the parts
   buildLeftWheel(form);
   Widget butForm = buildAppButtons(form);
   
   // Lay out and manage the parts
   ...
    
   return form;
}

SoXtWalkViewer redefines only the buildLeftTrim() routine in order to build the default parts as well as the extra thumbwheel and label. The viewer then simply returns a form containing its new left trim to the buildDecoration() routine, and everything works as before. Only the new trim has to be modified.

Creating a Constrained Viewer

The SoXtConstrainedViewer base class adds the notion of a world up-direction, with methods like setUpDirection() and getUpDirection(). New viewers that require the notion of an up-direction should be derived from this base class. With the notion of a world up-direction (which defaults to +y), a viewer can constrain the camera to prevent the user from looking upside down. This constraint is currently used in SoXtFlyViewer and SoXtWalkViewer.

SoXtConstrainedViewer redefines some of the routines, such as saveHomePosition() and resetToHomePosition(), to save and restore the original camera up-direction. This base class redefines the paste() and setCamera() methods to guarantee that the original camera up-direction is preserved whenever new camera values are given. SoXtConstrainedViewer also provides some convenience routines to allow the user to interactively specify the world up-direction (the findUpDirection() method) and constrain the camera to the current up-direction (the checkForCameraUpConstrain() method).

The world up-direction can be changed with the setUpDirection() method and can also be changed interactively by the user while viewing a model using the findUpDirection() method, defined in SoXtConstrainedViewer.

The base class SoXtConstrainedViewer also provides a convenient way to check that the current camera values are consistent with the up-direction and to tilt the camera up or down while constraining to +/- 90 degrees from the eye-level plane. This prevents the camera from ever looking upside down. Those protected methods are as follows:

void checkForCameraUpConstrain(); 


checks the camera orientation and makes sure that the current right vector and ideal right vector (cross between the view vector and world up-direction) are the same and corrects it if they are not the same. This method keeps the up-direction valid.

virtual void tiltCamera(float deltaAngle); 


tilts the camera, restraining it to within 180 degrees from the up-direction. A positive angle tilts the camera up.

For convenience, SoXtConstrainedViewer defines the decoration thumbwheels, which can also be redefined by subclasses. These are defined as follows:

virtual void bottomWheelMotion(float newVal); 


rotates the camera around the world up-direction

virtual void leftWheelMotion(float newVal); 


tilts the camera up and down, constraining it to within 180 degrees of the world up-direction

virtual void rightWheelMotion(float newVal); 


moves the camera forward and backward


Note: A viewer that is constrained to a world up-direction should always rotate the camera around that world up-direction when rotating left and right. It is important to rotate around the world up-direction—as opposed to the current camera up-value, which is not the same if the camera is tilted up or down—to prevent rolling. This rolling disturbs the camera alignment and eventually causes the camera to look upside down.