Event Handling

Overview

The HEventManager class receives and dispatches events from one object to another. By registering objects derived from a specific HEventListener class with the EventManager, it is possible to route one event (which is essentially a function call) to multiple recipients. This is useful in any situation that requires events to be received by more than one object and/or in which the potential recipients of an event are not known in advance.

Creating a New Listener Type

MVO already uses a number of predefined EventListeners for mouse input, animation events and screen updates. However, it is easy to create a new Listener type.

Here is an example:

class MVO_API HUpdateListener : public HEventListener
{
        public:
                // A set of macros are used to define the functions for the various listener types
                // HLISTENER_SETUP_FUNCTIONS() needs to be called with the name of the class and sets
                // up the correct constructor, etc.
                HLISTENER_SETUP_FUNCTIONS_STRING(HUpdateListener)

                // Each function of the Event Listener needs to be declared via a macro which are defined in HEventListener.h
                // The first macro parameter is the class name, the second parameter is the function name
                // the following parameters are the type of the parameter and it's name (in this case for two parameters)
                HLISTENER_FUNCTION_2(HUpdateListener, UpdateEvent, bool, antialias, bool, forceUpdate)
};

Event Registration

Each object that is interested in receiving events (in this case Update related) needs to subclass from this Event Listener and must implement at least the functions that it is interested in:

class MVO_API HBaseView : public HUpdateListener
{
        .
        .
        int UpdateEvent(bool antialias = false, bool forecUpdate = false);
        .
        .
}


int HBaseView::UpdateEvent(bool antialias, bool forceUpdate)
{
        Update(antialias, forceUpdate);

        // the function can choose to consume the event in which case it will not be propagated to the next
        // recipient or pass it on
        return HLISTENER_PASS_EVENT;
}

The object must also register itself with the EventManager it wants to receive the events from. This will be usually be the EventManager associated with the active model. To register itself with the EventManager, the function RegisterHandler is called with a pointer to the object that needs to be registered, the type of event that this object should be registered for (one object can derive from multiple event listeners) and a constant indicating the priority with which this object receives this event type. A higher priority means that this object will “see” the event before another object with a lower priority.

m_pModel->GetEventManager()->RegisterHandler(this, HUpdateListener::GetType(), HLISTENER_PRIORITY_NORMAL);

Creating a New Event

To create an event from anywhere inside the application, the HLISTENER_EVENT macro should be used. The first parameter is the event listener that this event belongs to, the second parameter is a pointer to the Event Manager and the third parameter is the function call itself:

HLISTENER_EVENT(HUpdateListener, m_pHView->GetEventManager(), UpdateEvent())

When executed, the code behind this macro will inject an event into the event queue.

HEventInfo

Mouse clicks and key presses are some of the most common user events. In HOOPS/MVO, the HMouseListener class defines a set of interfaces that handles these events. The methods specified in HMouseListener all take one parameter, an HEventInfo object. The HEventInfo class encapsulates information like mouse pointer position and various keyboard states and is platform independent.

When your application receives a mouse or keyboard event from the system, the raw data is likely to be in a platform specific format. In your method for handling the event, you will need to repackage this information into an HEventInfo object before passing it on. For example, to process a mouse event, create an instance of HEventInfo and initialize it using HEventInfo::SetPoint, which requires the following parameters:

  • Type of event

  • x and y locations in pixel space where the event occurred

  • :Flags associated with the event

The following sample code shows how you might handle a left mouse button double click event:

void MyHoopsView::OnLButtonDblClk(UINT nFlags, CPoint point)
{
        if (m_pHView)
        {
                HEventInfo      event(m_pHView);
                event.SetPoint(HE_LButtonDblClk, point.x, point.y, MapFlags(nFlags));
                HLISTENER_EVENT(HMouseListener, m_pHView->GetEventManager(), OnLButtonDblClk(event))
        }
}

MapFlags is a utility function that maps the UI-specific event-state flags to platform-independent HOOPS/MVO flags. It is part of both the HOOPS/MFC and HOOPS/QT integrations. Please refer to CHoopsView::MapFlags and HQWidget::MapFlags for more details.

Note that all ASCII characters are handled. However, only certain special keys are detected in a platform-independent way. These keys are listed below:

  • Ctrl

  • Alt

  • Shift

  • Up arrow

  • Down arrow

  • Left arrow

  • Right arrow

Other keys may be detected, however, the integer value of those key presses is not guaranteed to be equivalent across all platforms. If you require the use of unsupported keys, we recommend that you set your own value for the key when building the event, then test for that value when processing the event in your operator. Example:

int myKeyValue = 282; // "282" will be the custom key value
event.SetKey(HE_KeyDown, myKeyValue, 1, 0); // setting the custom value

HLISTENER_EVENT(HMouseListener, m_pHView->GetEventManager(), OnKeyDown(event)); // inject the event

3D Mouse and Joystick Events

In addition to traditional events like mouse movement and key presses, HOOPS/MVO also provides a predefined event listener for feedback from 3D devices such as 3D mice and joysticks via the HJoyStickListener class.

To utilize HJoyStickListener’s capabilities, a class must inherit from this event listener. During the initialization of the class, you need to register with the Event Manager to receive notice of the joystick events. The class must also implement HJoyStickListener’s two main methods: OnJoyStickRotation, OnJoyStickTranslation, and OnJoyStickButton.

The HJoyStickListener::OnJoyStickRotation is called whenever a rotation event occurs with the 3D mouse or joystick. When this method is called, an HEventInfo object is passed as a parameter. You can use the HEventInfo::GetJoystickRotation method to access the current rotation of x, y and z axes. Note that this information is raw data from the device, we recommend that you invest some time in fine tuning how your application wishes to interpret this data.

When a translation event occurs with the joystick or 3D mouse, the OnJoyStickTranslation method is called and an HEventInfo object is passed. Using the HEventInfo::GetJoystickTranslation method, you can access the current translation vector. As with the rotation event, the information for the translation vector is also raw data from the device. We suggest that you determine how you want to interpret this information such that it represents the best experience for your user.

If a button is pressed, the OnJoyStickButton method is called. In the HEventInfo object, you can determine which 3D mouse button was pressed by calling HEventInfo::GetFlags(). HOOPS supports up to eight mouse buttons from MVO_JOYSTICK_BUTTON_1 to MVO_JOYSTICK_BUTTON_8.

The following sample code shows how a class would access and utilize the events provided by the HJoyStickListener event listener on the Microsoft Windows platform. Example:

#include "HEventInfo.h"
#include "HEventListener.h"

class MyJoyStickHandler : public HJoyStickListener
{
public:
        MyJoyStickHandler(HBaseView* view);

        //The method that receives joystick rotation events
        int OnJoyStickRotation( HEventInfo &hevent);

        //The method that receives joystick translation events
        int OnJoyStickTranslation( HEventInfo &hevent);

        //The method that receives joystick button events
        int OnJoyStickButton(HEventInfo &hevent){

};

MyJoyStickHandler::MyJoyStickHandler(HBaseView* view )
{
        //Registering for the HJoyStickListener Events
        view->GetEventManager()->RegisterHandler((HJoyStickListener *)this, HJoyStickListener::GetType(), HLISTENER_PRIORITY_NORMAL);
}

int MyJoyStickHandler::OnJoyStickRotation( HEventInfo &hevent)
{
        //Getting the rotation information
        HPoint rotate = hevent.GetJoystickRotation();

        HC_Open_Segment_By_Key(hevent.GetView()->GetSceneKey());
                //Applying the x, y and z axes rotation information to pan and rotate the camera
                HC_Pan_Camera(-rotate.z /2048.0, rotate.x / 2048.0);
                HC_Roll_Camera(-rotate.y / 2048.0);
                hevent.GetView()->Update();
        HC_Close_Segment();
        return 0;
}

int MyJoyStickHandler::OnJoyStickTranslation( HEventInfo &hevent)
{
        //Getting the translation vector
        HPoint xlate = hevent.GetJoystickTranslation();
        float center[3];
        float radius, scene_scale;

        HC_Open_Segment_By_Key(hevent.GetView()->GetSceneKey());
                HC_Show_Bounding_Sphere(center, &radius);
                scene_scale = radius / 81456.38;
                //Applying the translation vector to move the camera
                HC_Dolly_Camera(xlate.x / 2.5 * scene_scale, xlate.y / 2.5 * scene_scale, xlate.z / 2.5* scene_scale);
                hevent.GetView()->Update();
        HC_Close_Segment();
        return 0;
}

int MyJoyStickHandler::OnJoyStickButton(HEventInfo &hevent){
        TCHAR temp[MVO_BUFFER_SIZE];
        if (hevent.GetFlags() == MVO_JOYSTICK_BUTTON_1){
                _stprintf(temp, _T("3D Mouse Button 1"));
                AfxMessageBox(temp);
        }
        else if(hevent.GetFlags() == MVO_JOYSTICK_BUTTON_2){
                _stprintf(temp, _T("3D Mouse Button 2"));
                AfxMessageBox(temp);
        }
        else if(hevent.GetFlags() == MVO_JOYSTICK_BUTTON_3){
                _stprintf(temp, _T("3D Mouse Button 3"));
                AfxMessageBox(temp);
        }
        else if(hevent.GetFlags() == MVO_JOYSTICK_BUTTON_4){
                _stprintf(temp, _T("3D Mouse Button 4"));
                AfxMessageBox(temp);
        }
        else if(hevent.GetFlags() == MVO_JOYSTICK_BUTTON_5){
                _stprintf(temp, _T("3D Mouse Button 5"));
                AfxMessageBox(temp);
        }
        else if(hevent.GetFlags() == MVO_JOYSTICK_BUTTON_6){
                _stprintf(temp, _T("3D Mouse Button 6"));
                AfxMessageBox(temp);
        }
        else if(hevent.GetFlags() == MVO_JOYSTICK_BUTTON_7){
                _stprintf(temp, _T("3D Mouse Button 7"));
                AfxMessageBox(temp);
        }
        else if(hevent.GetFlags() == MVO_JOYSTICK_BUTTON_8){
                _stprintf(temp, _T("3D Mouse Button 8"));
                AfxMessageBox(temp);
        }
        return 0;
}

Since HOOPS/MVO is platform independent, your application must implement code to capture joystick events from the system and dispatch these events to HOOPS/MVO. For example, if your application runs on Microsoft Windows, a convenient method for managing 3D mouse/joystick events is via the DirectInput API. The sample code below is from the MFC project found in <hoops>/Dev_Tools/hoops_mfc/source/hoops_mfcu_mgk_dinput_[version].vcxproj. It shows how you can implement an integration between DirectInput and HOOPS/MVO for 3D mouse/joystick events with the following steps:

  1. Initialize the connection with DirectInput.

  2. Poll for 3D mouse/joystick information in your application’s timer function.

  3. Get 3D mouse/joystick states from DirectInput.

  4. Package the 3D mouse/joystick states into an instance of HEventInfo.

  5. Dispatch the event using the HLISTENER_EVENT macro.

Example code:

/* Initializes the DirectInput variables.*/
HRESULT CHoopsView::InitDI8( void )
{
        HRESULT hr;
        g_nDevices = 0;

        // Register with the DirectInput subsystem and get a pointer to a IDirectInput interface we can use.
        // Create a DInput object
        if( FAILED( hr = DirectInput8Create( GetModuleHandle(NULL), DIRECTINPUT_VERSION,
                                                                                 IID_IDirectInput8, (VOID**)&(dev_handle->g_pDI), NULL ) ) )
        {
                m_bUsingDI8Joystick = false;
                return hr;
        }

        // Look for all 3Dx HID devices.
        if( FAILED( hr = dev_handle->g_pDI->EnumDevices( DIDEVTYPE_HID,
                                                                                 EnumDevicesCB,
                                                                                 this, DIEDFL_ATTACHEDONLY ) ) ){
                m_bUsingDI8Joystick = false;
                return hr;
        }

        // Make sure we got an HID device
        if( g_nDevices == 0 )
        {
                m_bUsingDI8Joystick = false;
                return S_OK;
        }

        /* Set the data format to "simple joystick" - a predefined data format
         * A data format specifies which controls on a device we are interested in,
         * and how they should be reported. This tells DInput that we will be
         * passing a DIJOYSTATE structure to IDirectInputDevice::GetDeviceState().
         * This is a fine format to use for the 3Dx Devoce as it has 6 DOF and upto
         * 32 buttons.
         */
        if( FAILED( hr = g_pDevice->pUsbHidDevice->SetDataFormat( &c_dfDIJoystick ) ) ){
                m_bUsingDI8Joystick = false;
                return hr;
        }

        /* Set the cooperative level to let DInput know how this device should
         * interact with the system and with other DInput applications.
         * You probably don't want the dialog box to be the target of the events.*/
        if( FAILED( hr = g_pDevice->pUsbHidDevice->SetCooperativeLevel( AfxGetApp()->m_pMainWnd->GetSafeHwnd(),
                           DISCL_EXCLUSIVE | DISCL_FOREGROUND    // normal use:  get events when have focus
                         //DISCL_NONEXCLUSIVE | DISCL_BACKGROUND /* get events all the time */
                         ) ) ){
                m_bUsingDI8Joystick = false;
                return hr;
        }

        /* Determine how many axis the UsbHidDevice has (so we don't error out setting
         * properties for unavailable axis)*/
        g_pDevice->diDevCaps.dwSize = sizeof(DIDEVCAPS);
        if ( FAILED( hr = g_pDevice->pUsbHidDevice->GetCapabilities(&g_pDevice->diDevCaps) ) ){
                m_bUsingDI8Joystick = false;
                return hr;
        }

        /* Enumerate the axes of the UsbHidDevice and set the range of each axis and
         * make sure the axes are set to ABSOLUTE mode. */
        if ( FAILED( hr = g_pDevice->pUsbHidDevice->EnumObjects( EnumAxesCB,
                (VOID*)g_pDevice, DIDFT_AXIS ) ) ){
                m_bUsingDI8Joystick = false;
                return hr;
        }
        return S_OK;
}

/*Callback function for enumerating the axes on a UsbHidDevice*/
BOOL CALLBACK EnumAxesCB( const DIDEVICEOBJECTINSTANCE* pdidoi, VOID* pContext )
{
        DIDeviceInfo *pDIDeviceInfo = (DIDeviceInfo *)pContext;


        /*
         * Recent 3Dx USB device descriptors indicate that the device axes return relative data
         * even though the device is really an absolute device.
         * HID ignores this, but apparently DI uses it.
         * Changing the axis handling to return RELATIVE values gives us back what we want (displacement
         * values rather than accumulated values).  If older USB devices were to be supported, this handling
         * would have to be changed to DIPROPAXISMODE_ABS.
         *
         * In this mode, DI sets the notification event even though the values haven't changed.
         */
        DIPROPDWORD dipdw;
        dipdw.diph.dwSize       = sizeof(DIPROPDWORD);
        dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER);
        dipdw.diph.dwHow        = DIPH_DEVICE;
        dipdw.diph.dwObj        = 0; // set for whole device not an axis (really only needs to be done once)
        dipdw.dwData            = DIPROPAXISMODE_REL;

        if( FAILED( pDIDeviceInfo->pUsbHidDevice->SetProperty( DIPROP_AXISMODE, &dipdw.diph ) ) )
                return DIENUM_STOP;

        /* Setting up the buffered input for the mouse buttons*/
        dipdw.dwData            = 10;

        if( FAILED( pDIDeviceInfo->pUsbHidDevice->SetProperty( DIPROP_BUFFERSIZE, &dipdw.diph ) ) )
                return DIENUM_STOP;

        return DIENUM_CONTINUE;
}


/*The timer function that polls the for joystick events*/
void CHoopsView::OnTimer(UINT_PTR nIDEvent)
{
        if (m_pHView)
        {
                HEventInfo      event(m_pHView);
                HBaseOperator *op = m_pHView->GetCurrentOperator();

                if(m_bUsingDI8Joystick)
                        this->UpdateDI8State(0);

                if (op)
                {
                        event.SetTimer(HE_Timer, nIDEvent);
                        op->OnTimer(event);
                }
                else
                        CView::OnTimer(nIDEvent);
        }
}

/* Get the input device's state.  Dispatches an JoystickButton event if any of the button flags are set.*/
HRESULT CHoopsView::UpdateDI8State( unsigned long devIndex )
{
        HRESULT     hr;
        DIJOYSTATE  js;           // DInput UsbHidSpaceball state
        DIDeviceInfo dinfo = *g_pDevice;

        if( NULL == dinfo.pUsbHidDevice )
                return S_OK;

        /*
         * Poll the device to read the current state.  This should not be necessary
         * but is advised by DI documentation and should not take a significant amount
         * of time.  (This could be where we get the double vector events.)
         */
        hr = dinfo.pUsbHidDevice->Poll();
        if( FAILED(hr) )
        {
                //OutputDebugString("Poll failed\n");

                // DInput is telling us that the input stream has been
                // interrupted. We aren't tracking any state between polls, so
                // we don't have any special reset that needs to be done. We
                // just re-acquire and try again.
                hr = dinfo.pUsbHidDevice->Acquire();
                while( hr == DIERR_INPUTLOST )
                        hr = dinfo.pUsbHidDevice->Acquire();

                // hr may be DIERR_OTHERAPPHASPRIO or other errors.  This
                // may occur when the app is minimized or in the process of
                // switching, so just try again later
                return S_OK;
        }

        // Get the input's device state
        if( FAILED( hr = dinfo.pUsbHidDevice->GetDeviceState( sizeof(DIJOYSTATE), &js ) ) )
        {
                //OutputDebugString("GetDeviceState failed\n");
                return hr; // The device should have been acquired during the Poll()
        }

        // Display UsbHidDevice state to dialog
        HandleAxesState( this, js );

        DIDEVICEOBJECTDATA rgdod[10];
        DWORD dwItems = 10;
        hr = dinfo.pUsbHidDevice->GetDeviceData(
                sizeof(DIDEVICEOBJECTDATA),
                rgdod,
                &dwItems,
                0);
        if (SUCCEEDED(hr)) {
                // dwItems = Number of elements read (could be zero).
                if (hr == DI_BUFFEROVERFLOW) {
                // Buffer had overflowed.
                }
                for(int i = 0; i < (int)dwItems; i++){
                        HEventInfo      button1_event(this->m_pHView);
                        button1_event.SetType(HE_JoyStickButton);

                        switch(rgdod[i].dwOfs){
                                case DIJOFS_BUTTON0:
                                        button1_event.SetFlags(MVO_JOYSTICK_BUTTON_1);
                                        break;
                                case DIJOFS_BUTTON1:
                                        button1_event.SetFlags(MVO_JOYSTICK_BUTTON_2);
                                        break;
                                case DIJOFS_BUTTON2:
                                        button1_event.SetFlags(MVO_JOYSTICK_BUTTON_3);
                                        break;
                                case DIJOFS_BUTTON3:
                                        button1_event.SetFlags(MVO_JOYSTICK_BUTTON_4);
                                        break;
                                case DIJOFS_BUTTON4:
                                        button1_event.SetFlags(MVO_JOYSTICK_BUTTON_5);
                                        break;
                                case DIJOFS_BUTTON5:
                                        button1_event.SetFlags(MVO_JOYSTICK_BUTTON_6);
                                        break;
                                case DIJOFS_BUTTON6:
                                        button1_event.SetFlags(MVO_JOYSTICK_BUTTON_7);
                                        break;
                                case DIJOFS_BUTTON7:
                                        button1_event.SetFlags(MVO_JOYSTICK_BUTTON_8);
                                        break;
                        }

                        if(button1_event.GetFlags() != 0)
                                //Dispatching a button event
                                HLISTENER_EVENT(HJoyStickListener, this->m_pHView->GetEventManager(), OnJoyStickButton(button1_event))
                }
        }

        return S_OK;
}


/* This method reads the joystick state and generates the HOOPS/MVO Joystick Rotate
   and Translate events.*/
void HandleAxesState(CHoopsView* view, DIJOYSTATE js )

{
        // Knowing the peculiar characteristics of the USB/HID/DI implementation, combine all 6 DOFs
        CombineDataStreams(view, js );

        HEventInfo      translate_event(view->m_pHView);
        translate_event.SetType(HE_JoyStickTranslate);
        translate_event.SetWorldPos(HE_JoyStickTranslate, HPoint(-js.lX, -js.lZ, -js.lY), 0);
        HLISTENER_EVENT(HJoyStickListener, view->m_pHView->GetEventManager(), OnJoyStickTranslation(translate_event))

        HEventInfo      rotate_event(view->m_pHView);
        rotate_event.SetType(HE_JoyStickRotate);
        rotate_event.SetWorldPos(HE_JoyStickRotate, HPoint(js.lRx, js.lRy, js.lRz), 0);
        HLISTENER_EVENT(HJoyStickListener, view->m_pHView->GetEventManager(), OnJoyStickRotation(rotate_event))

}

For a more comprehensive look at the implementation, please see the HOOPS/MFC project found in <hoops>/Dev_Tools/hoops_mfc/source/hoops_mfcu_mgk_[version].vcxproj. The DirectInput integration is enabled with the DIRECT_INPUT_SUPPORT flag.

Timer Events

The HTManager object in MVO provides a centralized timing service for MVO and allows the developer to register operators which will receive time-based function callbacks. Using timer-based operators allows developers to create operators which are interruptable and can be forced to occur over a user-determined span of time. Timer-based events are used extensively for animations and playback sequences.

To best demonstrate how to create a timer-based operator, we will review the HTCObjectRotate operator in MVO. This was added to MVO as a reference implementation. It allows you to select an object and get it to rotate about its centroid for 10 seconds and then stop. At any point, selecting on the object again will stop it rotating. Since the operator is timer based you can continue to use other operators to navigate about the scene or select any of the timer objects to stop before the 10-second rotation has completed.

For any timer-based operators you must derive from the HTClient class. Since we are implementing an operator, we will also inherit the HBaseOperator class. Note, timer-based events do not have to be operators, for example the smooth camera transition mechanism in MVO is also based on a timer event.

Let’s look at our HTCObjectRotate class:

class MVO_API HTCObjectRotate : public HBaseOperator, public HTClient
{
public:

        HTCObjectRotate (HBaseView* view, int DoRepeat=0, int DoCapture=1);
        ~HTCObjectRotate();

        // OnLButtonDown is where we do the work of determining which segment to rotate
        int OnLButtonDown (HEventInfo &event);

        // Tick is the function which receives the callbacks at the user defined frequency
        bool Tick( float request_time, float actual_time );

        // Sets a flag indicating whether we should delete *this when the client is UnRegistered
        void SetDeleteOnUnRegister() { m_delete_on_unregister = true; };

        // UnRegister unregisters the timer client from HTManager and deletes *this if flag set
        void UnRegister ();

        // OnSignal is called by the HBaseView destructor, allows the Client to do any necessary
        // cleanup in the event of a unforseen window shutdown
        bool OnSignal( int signal );
};

The first thing you need to do is register your timer client with HTManager, the timer manager object in MVO. This work is done in the HTCObjectRotate::OnLButtonDown. When a LButtonEvent occurs this operator either registers or removes a timer client from HTManager:

int HTCObjectRotate::OnLButtonDown(HEventInfo &event)
{
        HC_KEY key;
        HTCObjectRotate *op;

        // this never occurs
        if(m_state == HTCOR_Animating)
        {
                UnRegister();
                return HOP_OK;
        }

        // returns true if an object is selected and in that case
        // returns the key to that object
        if(!GetSelection(event, &key))
                return HOP_OK;

        // determines if the object is already in a timer event by querying
        // a user value on the object and if it is then it is removed
        // from the HTManager.
        if(SearchAndDestroyHTCOR(key))
                return HOP_OK;

        // this is for dealing with a case in MVO where we're dealing with
        // cutting planes.  You can do work here for any special cases you
        // may have in your scene graph
        while(SpecialKey(key))
                key = HC_KShow_Owner_By_Key(key);


        // If all is fine from above let's create our Timer client
        op = new HTCObjectRotate(*this);

        // set it so it gets deleted when the event finishes
        op->SetDeleteOnUnRegister();

        // and then call HTCObject::Animate which does all the work of
        // registering the client with HTManager
        op->Animate(key);

        return HOP_OK;
}

HTC_ObjectRotate::SearchAndDestroy simply queries the object to see if a user option is set on it and if it is then it UnRegisters it, effectively stopping the object being animated. Note, we store a pointer to a segment’s timer event in the HOOPS database and use the existence of this to determine whether or not the object is currently in a timer event and if so we then use that pointer to unregister it. This is custom to this specific operator and may not be the right approach for your own timer client. You may not need to determine whether or not an object is already in a timer event or you may want to store this information in a separate data structure. We put this information in the HOOPS database for convenience.

Now let’s look at HTCObjectRotate::Animate. Before the code below, we moved the selected object into a unique segment and set a user option on it so that HTCObjectRotate::SearchAndDestroyHTCOR can later determine if it is already in a timer event. You should review the style enums section in the Reference Manual for details on the different style types. Note, you are not guaranteed to receive at your requested rate. However the timer manager will, depending on your style setting, let you know if you have been notified at the requested rate:

void HTCObjectRotate::Animate(HC_KEY key)
{
        .
        .

        // Sets the time interval between calls to our implementation of HTClient::Tick().
        // The value is in seconds, so a setting of 0.01 means that the timer manager
        // will try to call HTCObjectRotate::Tick 10 times a second
        SetInterval(0.01f);

        // Setting a periodic style enables us to ensure our rotational
        // position is based solely on time
        SetStyle(HTCS_PeriodicSkip);

        // tick will be called 0.01 seconds after the client has been
        // registered with the Manager
        SetNextRequest(m_start_time + 0.01f);

        // Here we register a function with the HBaseView object which will get called if
        // the view object is  destroyed, this will allow us to shutdown the timer event correctly
        m_cookie = m_pView->SetSignalNotify(HSignalDelete, HTCObjectRotate::signal_callback, this);

        // Finally we register the client with the manager
        HDB::GetHTManager()->RegisterClient(this);

        m_pView->Update();
}

Now that we have registered our client with the timer manager and have set a periodic style, our implementation of HTClient::Tick will be called with the time at whichit should have been called and also the actual time that the function is being called. This information allows us to implement logic which ensures that the operation occurs in the pre-defined time. In this case, we check if the elapsed time is greater than the duration (which is set as 10 seconds in the constructor). If so, then we unregister the client. Otherwise, we set a modelling matrix on the segment based on the elapsed time:

bool HTCObjectRotate::Tick( float request_time, float actual_time )
{
        HPoint axis;
        float halfway, elapsed, angle;

        if( request_time - m_start_time > m_duration )
        {
                HC_Open_Segment_By_Key(m_segkey);
                  HC_UnSet_Modelling_Matrix();
                  if( m_original_matrix_valid )
                           HC_Append_Modelling_Matrix( m_original_matrix );

                HC_Close_Segment();

                UnRegister();
                return true;
        }

        // calculate the angle by which we would like to rotate
        elapsed = actual_time - m_start_time;
        halfway = m_start_time + m_duration/2.0f;

        angle = elapsed / m_duration;

        if( actual_time < halfway )
                angle *= angle * 2.0f;
        else
        {
                angle = 1.0f - angle;
                angle *= angle;
                angle = 1.0f - 2.0f * angle;
        }
        m_angle = angle * 3.0f * 360.0f;

        HC_Open_Segment_By_Key(m_segkey);
          axis.x = 1;
          axis.y = 1;
          axis.z = 1;
          HC_UnSet_Modelling_Matrix();
          RotateSegmentLocally ( m_segkey, &axis, &m_centroid, m_angle );
          if( m_original_matrix_valid )
                HC_Append_Modelling_Matrix( m_original_matrix );

        HC_Close_Segment();

        m_pView->Update();

        return true;

} //end function Tick

Finally let’s look at HTCObjectRotate::UnRegister. It simply tells the manager to remove it from the list of objects receiving timer events and then unsets any user values that may have been previously set on the scene graph. It also unregisters it from the list of objects that should be notified if the HBaseView object is destroyed. If the flag is set, it also deletes the object:

void HTCObjectRotate::UnRegister()
{
HDB::GetHTManager()->UnRegisterClient( this );

m_state = HTCOR_Resting;

if( m_pView != NULL )
{
HC_Open_Segment_By_Key( m_segkey );
HC_UnSet_One_User_Index( HTC_OBJECT_ROTATE_MAGIC );
HC_Delete_Segment( "htcor_temp" );
HC_Close_Segment();

m_pView->UnSetSignalNotify( m_cookie );
m_pView->Update();
}

m_segkey = -1;
m_key = -1;

if( m_delete_on_unregister )
delete this;
}

Touch Events

HOOPS/MVO provides multi-touch support, which can be utilized under supported HOOPS platforms that offer touch interfaces. (For example, iOS)

HBaseView provides a method called HBaseView::GetTouchManager. This returns the HTouchManager object, which allows for persistent handling of touches across multiple interaction events. HTouchManager::AddTouch() inserts new touches in the HTouchManager list, or updates an existing touch instance. HTouchManager::GetTouches() returns a list of touch instances. Each touch should have a unique identifier which allows HTouchManager to track touches across events.

For example, let’s say four ‘touch down’ events come in from the OS. The application code would call HTouchManager::AddTouch() four times. Then, consistent wtih the listener concepts covered above, HLISTENER_EVENT would get called with the appropriate touch event. Let’s look at the application/GUI-level code from the HOOPS iOS demo, which handles the various touch events and dispatches them using HLISTENER_EVENT.

The following is a utility method called AddTouch which conversts the GUI ‘pixel space’ coordinate to ‘window space’ (which is the coorindate system required by all the HOOPS/3DGS selection functions), and then calls HTouchManager::AddTouch to add the touch to the list of touches:

-(void)AddTouch: (UITouch *)touch withEvent:(HEventInfo *)event
{
        CGPoint location = [touch locationInView:self];

        HPoint pixelpos(location.x, location.y, 0);
        HPoint windowpos;

        HC_Open_Segment_By_Key(m_pHoopsView->GetSceneKey());
        {
                HC_Compute_Coordinates(".", "local pixels",&pixelpos, "local window", &windowpos);
        }
        HC_Close_Segment();

        event->SetWindowPos(HE_LButtonDown,&windowpos,MVO_LBUTTON);

        m_pHoopsView->GetTouchManager()->AddTouch(windowpos.x, windowpos.y,0.0f,[touch tapCount],(MVO_POINTER_SIZED_UINT)touch);
}

The following is an implementation of a GUI-specific touchesBegan method. Note the various cases:

  • if there is a single touch (i.e. a single finger touches the screen) with a double-tap, it will perform a selection

  • if there are 3 touches (i.e. three fingers touch the screen) it will do a FitWorld (aka ‘zoom to extents’).

  • otherwise, it will pass all the touch events onto any objects/operators that are ‘listening’ to the touch events, and the events are dispatched via HBaseOperator::OnTouchesDown:

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)evt
    {
    
      NSSet *allTouches = [evt allTouches];
      HEventInfo event(m_pHoopsView);
    
      m_pHoopsView->GetTouchManager()->ClearTouchActivity();
      if ([allTouches count] == 3)
      {
              m_pHoopsView->FitWorld();
              m_pHoopsView->Update();
              return;
      }
    
      if ([allTouches count] == 1)
      {
              UITouch *touch1 = [[allTouches allObjects] objectAtIndex:0];
              if ([touch1 tapCount] == 2)
              {
                      [self selectFromTouch:touch1];
                      return;
              }
      }
    
      for (int i=0;i<[allTouches count];i++)
      {
              UITouch *touch1 = [[allTouches allObjects] objectAtIndex:i];
              [self AddTouch:touch1 withEvent:&event];
      }
    
      HLISTENER_EVENT(HMouseListener, m_pHoopsView->GetEventManager(), OnTouchesDown(event));
    }
    

In addition to the various touch events being properly handled/dispatched in the GUI-specific event handler methods, each touch-enabled MVO HBaseOperator class needs to implement the HBaseOperator::OnTouchesDown, HBaseOperator::OnTouchesMove, and HBaseOperator::OnTouchesUp as require by the operator’s specific goals. HOOPS/MVO already includes touch-capable variants of the standard camera operators:

  • HOpCameraManipulateTouch

  • HOpCameraPanTouch

  • HOpCameraZoomTouch

  • HOpCameraRollTouch

Each of these MVO operator classes is provided in source code form. This gives you a reference of how you would access the touch information coming in via the HEventInfo class, if case you wish to create custom touch-capable operators derived from HBaseOperator.