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:
A component derived from SoXtRenderArea called SceneTumble
A viewer called simpleViewer
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.
There are no special macros for creating new component classes. Creating a new component requires these steps:
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).
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.
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”).
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:
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 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.
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 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 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 shows the include file for the SceneTumble class. Example 10-2 shows the source code for this class.
#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); }; |
#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); } |
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, 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.
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.
Construct the viewer (required step for all new viewers). See “Defining the Constructor”.
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”.
Implement the seek function (optional step). See “Implementing the Seek Function”.
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.
Modify how the trim decoration is used (required step). See “Using the SoXtFullViewer Trim Decoration”.
Add push buttons (optional step). See “Adding Push Buttons”.
Change the preference sheet (optional step). See “Changing the Preference Sheet”.
Change the pop-up menu (optional step). See “Changing the Pop-up Menu”.
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.
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).
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”.
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:
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.
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:
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:
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.
#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(); }; |
#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(); } |
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 |
| |
rightWheelStr, bottomWheelStr, leftWheelStr |
| |
rightWheelVal, bottomWheelVal, leftWheelVal |
| |
rightWheelLabel, bottomWheelLabel, leftWheelLabel |
|
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.
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.
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.
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.
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.
// 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.
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(); |
| |
virtual void tiltCamera(float deltaAngle); |
|
For convenience, SoXtConstrainedViewer defines the decoration thumbwheels, which can also be redefined by subclasses. These are defined as follows:
virtual void bottomWheelMotion(float newVal); |
| |
virtual void leftWheelMotion(float newVal); |
| |
virtual void rightWheelMotion(float newVal); |
|