13 December 2012

Tutorial 3: Specifying GUI (part 2)

In this tutorial we build upon Tutorial 2 to create a more complex graphical user interface using the Tinia framework.

Familiarity with basic OpenGL and C++ with object orientation is assumed.

The program created in this tutorial will run both as a desktop program and as a server/client program.



The program consists of three files: The Job class definition and two main files. One main file will be created for the desktop program, and one for the web program. We will only highlight changes from Tutorial 2, so it's a good idea to brush up on that tutorial before reading this one.

User input through Tinia

With the exception of layout and spacing widgets (e.g. HorizontalLayout and HorizontalExpandingSpace) most GUI widgets in Tinia pass user information to the exposed model.

A TextInput for instance, takes the text the user has entered in and hands it over to the exposed model. The exposed model is then free deny the text (if for example the element is an integer the model will only accept text that is convertible to integers). If the exposed model accepts the new value, the relevant components will be notified. Specifically, an update to an element in the exposed model will trigger a redraw of the OpenGL canvas.

Making our triangle resizeable

We want to make our triangle from the previous example resizeable. To be precise, we want to define three scalars  $s_1,s_2,s_3\in [0,10]$, and define the three corners of our triangle to be

\[v_1=\begin{pmatrix}0\\0\\0\end{pmatrix}−s_1\begin{pmatrix}1\\0\\0\end{pmatrix}\]

\[v_2=\begin{pmatrix}1\\0\\0\end{pmatrix}+s_2\begin{pmatrix}1\\0\\0\end{pmatrix}\]

\[v_3=\begin{pmatrix}1\\1\\0\end{pmatrix}+s_2\begin{pmatrix}1\\0\\0\end{pmatrix}+s_3\begin{pmatrix}0\\1\\0\end{pmatrix}.\]

The two corners of the boundingbox will then be

\[\begin{pmatrix}−s_1\\0\\0\end{pmatrix},\begin{pmatrix}1+s_2\\1+s_3\\0\end{pmatrix}\}.\]

Constrained elements in ExposedModel

A constrained element in ExposedModel is an element with upper and lower bounds. For our triangle example, we need to add s1,s2,s3 as constrained elements via the addConstrainedElement method. The first argument to this method is the key, the second is the current value, the third is the minimum allowed value for the element, the fourth is the maximum allowed value for the element.
m_model->addConstrainedElement("s1", 0, 0, 10);
m_model->addConstrainedElement("s2", 0, 0, 10);
m_model->addConstrainedElement("s3", 0, 0, 10);
Notice that we add our elements as int, and the compiler is able to deduce the type automatically as an int.

Listeners to ExposedModel

If we allow the user to resize the triangle, we need to update our boundingbox whenever the user updates either s1, s2 or s3. To do this, we want to add a simple listener to the ExposedModel. A listener here is just a subclass of tinia::model::StateListener with the method stateElementModified implemented.

Our listener class is this simple class:

class Tutorial3Listener : public tinia::model::StateListener {

First we need to get a hold of the Exposed model, which we receive in the constructor
Tutorial3Listener(std::shared_ptr<tinia::model::ExposedModel> model)
        : m_model(model)
    {

Then in the constructor of Tutorial3Listener we add ourselves as a listener to the relevant elements using the addStateListener method

m_model->addStateListener("s1", this);
m_model->addStateListener("s2", this);
m_model->addStateListener("s3", this);

Once we've added ourselves as a listener, we must also ensure that we remove ourselves upon deletion of the listener, hence we need the following destructor in the listener

    ~Tutorial3Listener()
    {
        m_model->removeStateListener("s1", this);
        m_model->removeStateListener("s2", this);
        m_model->removeStateListener("s3", this);
    }

Finally we write the stateElementModified method. This method firsts gets the three scalars, then create the new boundingbox as a string and lastly updates the boundingbox in the model.

void stateElementModified(tinia::model::StateElement *stateElement) {
        // Get the three values:
        int s1 = m_model->getElementValue<int>("s1");
        int s2 = m_model->getElementValue<int>("s2");
        int s3 = m_model->getElementValue<int>("s3");
        m_model->updateElement("boundingbox",
                               tinia::model::makeBoundingBoxString(1 - s1, 0,      0,
                                                                1 + s2, 1 + s3, 0));
}


We store our listener in the Tutorial3Job class in the variable

std::unique_ptr<Tutorial3Listener> m_listener;

In the constructor of Tutorial3Job we instantiate the listener

m_listener.reset(new Tutorial3Listener(m_model));

Sliders and labels

We want to modify s1, s2 and s3 through a slider. To use a slider in Tinia, simply add a new instance of HorizontalSlider to the GUI layout. The HorizontalSlider accepts the key of the element as the first parameter to the constructor.

    auto slider1 = new tinia::model::gui::HorizontalSlider("s1");
    auto slider2 = new tinia::model::gui::HorizontalSlider("s2");
    auto slider3 = new tinia::model::gui::HorizontalSlider("s3");

To the left of each slider we want a label saying either "Left corner", "Right corner" or "Upper corner". The Label lets us add labels to each element, but we need to add annotations to each element for it to display anything more useful than "s1", "s2" or "s3", hence we use the method addAnnotation in the model

    m_model->addAnnotation("s1", "Left corner");
    m_model->addAnnotation("s2", "Right corner");
    m_model->addAnnotation("s3", "Upper corner");

Then we create the three labels

    auto label1 = new tinia::model::gui::Label("s1");
    auto label2 = new tinia::model::gui::Label("s2");
    auto label3 = new tinia::model::gui::Label("s3");

Finally we create a Grid layout with size 3 times 3 to hold our six widgets plus 3 HorizontalExpandingSpaces spacers, then add this grid to the main layout:

    auto grid = new tinia::model::gui::Grid(3, 3);
    grid->setChild(0, 0, label1);
    grid->setChild(0, 1, slider1);
    grid->setChild(0, 2, new tinia::model::gui::HorizontalExpandingSpace());
    grid->setChild(1, 0, label2);
    grid->setChild(1, 1, slider2);
    grid->setChild(0, 2, new tinia::model::gui::HorizontalExpandingSpace());
    grid->setChild(2, 0, label3);
    grid->setChild(2, 1, slider3);
    grid->setChild(0, 2, new tinia::model::gui::HorizontalExpandingSpace());
    layout->addChild(grid);

Modification to the render loop

We only need to make small modifications to the render loop. First we need to get the scalars

    int s1 = m_model->getElementValue<int>("s1");
    int s2 = m_model->getElementValue<int>("s2");
    int s3 = m_model->getElementValue<int>("s3");

then we utilize the scalars while drawing the triangle

    glBegin(GL_TRIANGLES);
    glColor3f(1, 0, 0);
    glVertex2f(0 - s1, 0);
    glVertex2f(1 + s2, 0);
    glVertex2f(1 + s2, 1 + s3);
    glEnd();

Running the desktop program

Starting the program should show something similar to this:



Running the web program

If you've successfully installed Tinia you should be able to run the web program as tutorial3_web through the mod_trell web interface.

The program should look something like this:



The full Job file

All changes in this tutorial have been done in the Job file of the tutorial:

/* Copyright STIFTELSEN SINTEF 2012
 *
 * This file is part of the Tinia Framework.
 *
 * The Tinia Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * The Tinia Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with the Tinia Framework.  If not, see <http://www.gnu.org/licenses/>.
 */
#pragma once
#include <tinia/tinia.hpp>
#include <GL/glew.h>
namespace tinia { namespace tutorial {
class Tutorial3Listener : public tinia::model::StateListener {
public:
    // We need the model to update the boundingbox
    Tutorial3Listener(std::shared_ptr<tinia::model::ExposedModel> model)
        : m_model(model)
    {
        m_model->addStateListener("s1", this);
        m_model->addStateListener("s2", this);
        m_model->addStateListener("s3", this);
    }
    ~Tutorial3Listener()
    {
        m_model->removeStateListener("s1", this);
        m_model->removeStateListener("s2", this);
        m_model->removeStateListener("s3", this);
    }
    void stateElementModified(tinia::model::StateElement *stateElement) {
        // Get the three values:
        int s1 = m_model->getElementValue<int>("s1");
        int s2 = m_model->getElementValue<int>("s2");
        int s3 = m_model->getElementValue<int>("s3");
        m_model->updateElement("boundingbox",
                               tinia::model::makeBoundingBoxString(1 - s1, 0,      0,
                                                                1 + s2, 1 + s3, 0));
    }
private:
    std::shared_ptr<tinia::model::ExposedModel> m_model;
};
class Tutorial3Job : public tinia::jobcontroller::OpenGLJob {
public:
    Tutorial3Job();
    bool renderFrame( const std::string &session,
                      const std::string &key,
                      unsigned int fbo,
                      const size_t width,
                      const size_t height );
private:
    std::unique_ptr<Tutorial3Listener> m_listener;
};
Tutorial3Job::Tutorial3Job()
{
    m_model->addElement( "myViewer", tinia::model::Viewer() );
    m_model->addElement("boundingbox", "0 0 0 1 1 1");
    m_model->addConstrainedElement("s1", 0, 0, 10);
    m_model->addConstrainedElement("s2", 0, 0, 10);
    m_model->addConstrainedElement("s3", 0, 0, 10);
    m_model->addAnnotation("s1", "Left corner");
    m_model->addAnnotation("s2", "Right corner");
    m_model->addAnnotation("s3", "Upper corner");
    auto label1 = new tinia::model::gui::Label("s1");
    auto label2 = new tinia::model::gui::Label("s2");
    auto label3 = new tinia::model::gui::Label("s3");
    auto layout = new tinia::model::gui::VerticalLayout();
    auto canvas = new tinia::model::gui::Canvas("myViewer");
    canvas->boundingBoxKey("boundingbox");
    layout->addChild(canvas);
    auto slider1 = new tinia::model::gui::HorizontalSlider("s1");
    auto slider2 = new tinia::model::gui::HorizontalSlider("s2");
    auto slider3 = new tinia::model::gui::HorizontalSlider("s3");
    auto grid = new tinia::model::gui::Grid(3, 3);
    grid->setChild(0, 0, label1);
    grid->setChild(0, 1, slider1);
    grid->setChild(0, 2, new tinia::model::gui::HorizontalExpandingSpace());
    grid->setChild(1, 0, label2);
    grid->setChild(1, 1, slider2);
    grid->setChild(0, 2, new tinia::model::gui::HorizontalExpandingSpace());
    grid->setChild(2, 0, label3);
    grid->setChild(2, 1, slider3);
    grid->setChild(0, 2, new tinia::model::gui::HorizontalExpandingSpace());
    layout->addChild(grid);
    m_model->setGUILayout(layout, tinia::model::gui::ALL);
    m_listener.reset(new Tutorial3Listener(m_model));
}
bool Tutorial3Job::renderFrame( const std::string &session,
                                const std::string &key,
                                unsigned int fbo,
                                const size_t width,
                                const size_t height )
{
    tinia::model::Viewer viewer;
    m_model->getElementValue("myViewer", viewer);
    glMatrixMode(GL_MODELVIEW);
    glLoadMatrixf(viewer.modelviewMatrix.data());
    glMatrixMode(GL_PROJECTION);
    glLoadMatrixf(viewer.projectionMatrix.data());
    int s1 = m_model->getElementValue<int>("s1");
    int s2 = m_model->getElementValue<int>("s2");
    int s3 = m_model->getElementValue<int>("s3");
    glClearColor(0, 0, 0 ,0 );
    glClear(GL_COLOR_BUFFER_BIT);
    glViewport(0, 0, width, height);
    glBegin(GL_TRIANGLES);
    glColor3f(1, 0, 0);
    glVertex2f(0 - s1, 0);
    glVertex2f(1 + s2, 0);
    glVertex2f(1 + s2, 1 + s3);
    glEnd();
    return true;
}
} // of tutorial
} // of tinia

No comments:

Post a Comment