Skip to main content

General Purpose Interactor

Development of a General-Purpose Interactor

 

Through this tutorial we are going to walk through developing a tulip interactor, including the use of the chain of responsibility used by interactors, and developing a simple interactor component.

 

This interactor will allow the user to perform the most common tasks without having to switch interactors every two clicks.

 

This interactor will be able to perform the following:

  • Add Nodes

  • Add Edges

  • Zoom and Pan

  • Select Nodes and Edges

  • Edit the selection (resize, move)

 

The Boilerplate

We are going to develop a multi-staged interactor, composed of many different parts.

So the first thing we need to do is allow these different components to get the input events, in order for them to be able to do anything.

Tulip provides a base class for such interactors : InteractorChainOfResponsibility.

As the names suggests, it is a part of an implementation of the Chain of Responsibility pattern, but is also an interactor.

 

class GeneralPurposeInteractor  : public InteractorChainOfResponsibility {
  public:
    /**
     * The constructor's sole purpose is to initialize the help and icon.
     * Actual construction is done in the construct method.
     **/
    GeneralPurposeInteractor():InteractorChainOfResponsibility(":/i_open.png","general purpose"){
      setPriority(1);
      setConfigurationWidgetText(QString("<h3>general purpose interactor</h3>"));
    }

    /**
     * Tells which views this interactor can be used with.
     * Returns true if and only if this interactor is compatible with the view whose name is passed as param.
     **/
    virtual bool isCompatible(const std::string &viewName) {
      return (viewName=="Node Link Diagram view");
    }

    /**
     * Actually construct the InteractorChainOfResponsibility.
     * This consists in pushing the components.
     * The order in which the components are added matters, as events accepted by a component 
     * will not be passed further down the chain.
     * The first element of the chain is the last one we add to it (pushed on top).
     **/
    void construct(){ 
    }
};

 

 

Here we have a straightforward implementation of this class, only compatible with the "Node Link Diagram view" Tulip view. It could be compatible with lots of other views, for instance the Histogram View, but for now we will just use it on the main Tulip view and see how it goes.

You might have noticed I used the “i_open.png” icon for this interactor. This is because I needed an icon to set it apart from the others interactors. Also I cannot draw an icon, for the life of me, that looks like anything.


 

The Easy part : Re-using Tulip components

Most of this interactor is so straightforward, it's barely worth a Tutorial.

What we want to do is be able to Zoom and pan, right ?

Well, let's add the Zoom and Pan interactor component to our newly created interactor.

void construct(){
      pushInteractorComponent(new MouseNKeysNavigator);
}

 

 

There, it's done. This interactor now performs zooming on mouse wheel and panning when left-clicking and moving the mouse.

Now for the other features :

void construct(){
      pushInteractorComponent(new MouseNKeysNavigator);
      pushInteractorComponent(new MouseSelectionEditor);
      pushInteractorComponent(new MouseSelector(Qt::LeftButton, Qt::ShiftModifier));
      pushInteractorComponent(new MouseNodeBuilder(QEvent::MouseButtonDblClick));
    }


 

That was easy.

To be honest I had to commit 2 small patches to Tulip to make this work this easily, the first one allowing the MouseNodeBuilder to use any QEvent to add nodes, the second telling the MouseSelector to disable some additional comportment if the modifier key is Shift.

If you ever find yourself in such a position, don't hesitate and ask on the Tulip forums whether patching is the best solution, and if so we will be happy to integrate your patch :)

The hard part : adding edges

The only reason this is hard is because I wanted to do some real code in this tutorial.

I wanted also to have an easier interaction that the current edge adding one, requiring less click and allowing the user to add some edges faster.

The idea I had was to simply draw a line, and connect the nodes this line connects. Imagine the also interactor, but when your mouse hovers over a node, it links it with the last node you hovered on.

Ok, so how do we do this ? First we need to create a new class, an InteractorComponent that will be pushed alongside of his little friends.


 

class CustomMouseEdgeBuilder : public InteractorComponent {
  public:
    CustomMouseEdgeBuilder() :_isDragging(false), _currentNode(UINT_MAX), _mainWidget(NULL), _camera(NULL) {}
    ~CustomMouseEdgeBuilder() {}
    
    bool eventFilter(QObject*, QEvent* e) {
    }
    
    virtual bool draw(GlMainWidget* ) {
    }
    
    InteractorComponent *clone() { return new CustomMouseEdgeBuilder(); }
    
    virtual void setView(View* view) {
    }
};


 

Here we have the base of what we need.

As I said I want it to look like the lasso interaction, so a quick look at this interactor's sources show me how it's done, and a copy/paste later, I have some members : a bool indicating whether I'm already drawing, the camera used to draw, the GlMainWidget used by the view, and a vector of Coord that stores the points the mouse went by to draw the polygin.

To this I will add the last node that was hovered on, and here we are :

private:
    bool _isDragging;
    tlp::node _currentNode;
    GlMainWidget* _mainWidget;
    Camera* _camera;
    std::vector<Coord> _polygon;


 

We retrieve the GlMainWidget and the camera in the setView method :

virtual void setView(View* view) {
      GlMainView *glView = dynamic_cast<GlMainView *>(view);
      if (glView) {
        _mainWidget = glView->getGlMainWidget();
        _camera = _mainWidget->getScene()->getLayer("Main")->getCamera();
      }
    }


 

When the interactor is drawn, we check the vector of coords is not empty, and if it's not we draw a new polygon:

virtual bool draw(GlMainWidget* ) {
      if (_polygon.size() > 0) {
        Camera camera2D(_camera->getScene(), false);

        glEnable(GL_BLEND);
        glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        camera2D.initGl();
        GlComplexPolygon complexPolygon(_polygon, Color(0,0,0,0), Color(0,255,0));
        complexPolygon.draw(0,0);
      }
      return true;
    }

 

 

All that is left is to use the events and this will be done.

Tulip interactors uses Qt's eventfilter method to get events and act upon them.

This method returns true if the event was consumed and should not be passed to other listeners, and false if the event should be passed along.

So we begin by initializing a boolean to hold our return value to false, because if none of our conditions are met, we want the vent to be passed along

bool eventFilter(QObject*, QEvent* e) {
      bool result = false;
      //Once we have checked the event is a Mouse event (the only kind that interests us), we can go on.
      QMouseEvent * qMouseEv = (QMouseEvent *) e;
      if(qMouseEv == NULL)
        return result;
      //But if the event is a mouse button release, we clear all our stuff and store we're not dragging anymore.
      if (e->type() == QEvent::MouseButtonPress) {
        if (qMouseEv->button() == Qt::LeftButton && qMouseEv->modifiers() == Qt::CTRL) {
          _isDragging = true;
          result= true;
        }
      }
      //But if the event is a mouse button release, we clear all our stuff and store we're not dragging anymore.
      else if(e->type() == QEvent::MouseButtonRelease) {
        _isDragging = false;
        _polygon.clear();
        _currentNode = node(UINT_MAX);
        _mainWidget->redraw();
        result= true;
      }
      //but if we move the mouse, interesting stuff happens
      else if(_isDragging && e->type() == QEvent::MouseMove) {
        //we get the current coordinates of the mouse
        Coord currentPointerScreenCoord = Coord(qMouseEv->x(), _mainWidget->height() - qMouseEv->y());
        //add those to the polygon we are drawing
        _polygon.push_back(currentPointerScreenCoord);
        
        edge tmpEdge;
        node tmpNode;
        ElementType type;
        //we try to see if there's something to select under the mouse
        bool result = _mainWidget->doSelect(qMouseEv->x(),qMouseEv->y(),type,tmpNode,tmpEdge);
        //and if there's a node under the mouse, and it's not the last selected node
        if(result && type == NODE && _currentNode != tmpNode) {
          //if the last node we hovered on is valid, let's create and edge :)
          if(_currentNode.isValid()) {
            Graph* graph = _mainWidget->getScene()->getGlGraphComposite()->getInputData()->getGraph();
            //but as we don't really see what we're doing with this interactor, let's just create one edge between two said nodes
            if(!graph->existEdge(_currentNode, tmpNode, false).isValid()) {
              graph->addEdge(_currentNode, tmpNode);
            }
          }
          //in any case this node is now the last node we hovered on
          _currentNode = tmpNode;
        }
        
        _mainWidget->redraw();
        result = true;
      }
      
      return result;
    }

 

 

OK, this is not easy to read as I couldn't find any syntax highlighting for HTML, but copy/paste it into any text editor worthy of this name and you'll find this much more easy to read.


 

Anyways, we created the last component in the interactor we tried to build !

Now, let's finalize all this stuff !


 

void construct(){
      pushInteractorComponent(new MouseNKeysNavigator);
      pushInteractorComponent(new MouseSelectionEditor);
      pushInteractorComponent(new CustomMouseEdgeBuilder());
      pushInteractorComponent(new MouseSelector(Qt::LeftButton, Qt::ShiftModifier));
      pushInteractorComponent(new MouseNodeBuilder(QEvent::MouseButtonDblClick));
    }
INTERACTORPLUGIN(GeneralPurposeInteractor, "GeneralPurposeInteractor", "Tulip Team", "24/08/2010", "General purpose interactor : zoom and pan, add nodes, add edges.", "1.0");

Team", "08/10/2010", "General purpose interactor : zoom and pan, add nodes, add edges, select and edit selection", "1.0");

 


 

This interactor has some drawbacks :

  • We cannot use it to create more than one edge between two nodes

  • We cannot modify the selection (add one node/edge to the selection)

But it is designed to fit most cases, not all of them, so it should be OK.


 

If you find this Tutorial to be incomplete, have questions, or find a mistake please do tell !