Web Analytics Made Easy - Statcounter
Skip to content

Programming new Apps#

In this tutorial, you will learn how to program an App that shakes atoms in a document when the user presses a button.

The source code for an Extension created in this tutorial can be found at SAMSON-Developer-Tutorials.

Generating an Extension#

First, use the SAMSON Extension Generator to create a new SAMSON Extension containing one app class. We refer to the SAMSON Extension Generator tutorial for a reminder on how to generate a new SAMSON Extension.

Launch SAMSON and invoke the SAMSON Extension Generator. Select a folder where you want to generate a SAMSON Extension.

Call the SAMSON Extension AtomShaker:

Extension Generator: Set the extension name

Leave the name of the App class be App, so that the class name will be SEAtomShakerApp:

Extension Generator: Add an app class

Click the Generate button:

Extension Generator: Generate

Your SAMSON Extension has been generated, click Finish:

Extension Generator: Generated files

If everything went well, you should now have a workable project with two main classes:

  • SEAtomShakerApp: the main class, which implements the functionality of the app;
  • SEAtomShakerAppGUI: the main GUI, which implements the user interface of the app.

Each class has its own pair of hpp (header) and cpp (code) files. The SEAtomShakerAppGUI class also goes with the ui file for GUI.

Now, try building the SAMSON Extension.

Once the SAMSON Extension is build and if you start SAMSON (e.g. the debug version provided with the SDK), you should see your app appear in Home > Apps:

App in the Apps menue

Let's now programm the App.

What's in a name?#

The first thing we're going to do is change the name of the app from "SEAtomShakerApp" to "Atom Shaker". To achieve this, modify the getName function in the SEAtomShakerGUI class, in the SEAtomShakerGUI.cpp file:

QString SEAtomShakerAppGUI::getName() const { 

    // SAMSON Extension generator pro tip: this string will be the GUI title. 
    // Modify this function to have a user-friendly description of your app inside SAMSON

    return "Atom shaker"; 

}

Notice the comment in the function which was suggesting you do just that. In general, you can search for such comments in the code written by the SAMSON Extension Generator to get development hints.

Once you compile and run again, the new app name is visible:

App name

Note how the apps are sorted alphabetically.

If you would like to modify the icon, you should modify the file SEAtomShakerAppIcon.png in the resources/icons folder.

Adding GUI elements#

Let's now add a button to our user interface. Open the SEAtomShakerAppGUI.ui file. Depending on your environment, opening this file should start Qt Designer or Qt Creator. The default user interface created by the SAMSON Extension Generator is this:

Default GUI

The default interface contains a label with a developer tip. Delete the label and add a QPushButton.

Add pushbutton

It is also a good habit to give meaningful names to widgets, especially later when you create more complex interfaces, so let's get into this habit right now:

Rename pushbutton

Signals and slots#

We need to make our app react when the user releases the push button. To do this, we are going to add a slot called onShakeAtoms to our interface (a slot is a function called when a widget emits a signal):

Add slot

Finally, we connect the released signal of the push button to this new slot (onShakeAtoms):

Connect signal to slot

We're done with this interface for now. We can save it and quit the interface editor.

Shaking atoms#

Even for simple apps, it is preferable to separate the user interface from the core functionality of the app. We are thus going to add to the GUI class the slot we promised to the interface editor. First, we declare the new slot in the SEAtomShakerAppGUI.hpp file:

class SEAtomShakerAppGUI : public SBGApp {

    // ...

public slots:

    // SAMSON Extension generator pro tip: add slots here to interact with your app

    void                          onShakeAtoms();

private:

    Ui::SEAtomShakerAppGUIClass   ui;

};

Then, we add the definition of this function in the SEAtomShakerAppGUI.cpp file (for example at the end of the file):

void SEAtomShakerAppGUI::onShakeAtoms() {

    getApp()->shakeAtoms();

}

Note how this function simply calls the shakeAtoms function of the app class, to delegate the actual work to the app. Again, this may seem superfluous for such a simple app, but it's a useful habit to get into.

Now, in the SEAtomShakerApp.hpp, we add the declaration of this shakeAtoms function:

class SEAtomShakerApp : public SBDApp {

    SB_CLASS

public:

    //...

    void                           shakeAtoms();

};

and we are now ready to add the function definition.

Our app is going to use several functionalities of the SAMSON SDK. Thus, we include the SAMSON header towards the beginning of the SEAtomShakerApp.cpp file:

#include "SAMSON.hpp"

We can now add the function definition at the end of the SEAtomShakerApp.cpp file:

void SEAtomShakerApp::shakeAtoms() {

    SBNodeIndexer nodeIndexer;
    // collect all atoms in the active document
    SAMSON::getActiveDocument()->getNodes(nodeIndexer,
                                        SBNode::IsType(SBNode::Atom));

    SAMSON::setStatusMessage(
        QString("We found " + QString::number(nodeIndexer.size()) + QString(" atom(s).")), 0);

    SB_FOR(SBNode* node, nodeIndexer) {

        SBAtom* currentAtom = static_cast<SBAtom*>(node);
        SBPosition3 position = currentAtom->getPosition();

        position.v[0] += SBQuantity::angstrom(1.0);

        currentAtom->setPosition(position);

    }

}

Here, first, we collect all atoms in the active document, and store pointers to them in a node indexer nodeIndexer. The SBNode::IsType(SBNode::Atom) is a node predicate (i.e. an object that contains a function returning true or false when it is applied to a node). When passed to the getNodes function, it ensures that only atoms are added to the node indexer.

We set the status message in SAMSON saying how many atoms were found.

Then, the for loop (with the SB_FOR macro) goes over all indexed atoms to translate them by one angstrom in the x direction. Two things should be emphasized:

  • All nodes in the document derive (directly or indirectly) from the SBNode class. The node indexer stores pointers to nodes, and the type of the node variable is thus SBNode*. In order to use functionalities of atoms (such as the getPosition and setPosition function), we need to cast node to a pointer to a SBAtom object.
  • All physical quantities (lengths, energies, etc.) are strongly typed in SAMSON: they are not mere floating-point values, but have associated units. We thus need to specify the unit of the displacement, for that we use SBQuantity::angstrom.

The units mechanism integrated in SAMSON ensures that developers from different backgrounds may work with units relevant to them, while ensuring physical correctness and preserving integration between SAMSON Extensions of various origins. For a test, try to replace angstrom(1.0) by e.g. electronVolt(1.0), and the code will not compile, whereas picometer(100.0) will compile and give the same result as angstrom(1.0). Please, refer to the Units for more information.

We may now compile and run SAMSON, and the app translates atoms:

Translate atoms

Finally, in order to shake atoms in random directions, we include the SBRandom.hpp header that provides random number generators in the beginning of the SEAtomShakerApp.cpp file:

#include "SBRandom.hpp"

and we modify the shakeAtoms function to use a random generator to perturb atoms positions:

void SEAtomShakerApp::shakeAtoms() {

    SBNodeIndexer nodeIndexer;
    // collect all atoms in the active document
    SAMSON::getActiveDocument()->getNodes(nodeIndexer,
                                          SBNode::IsType(SBNode::Atom));

    SAMSON::setStatusMessage(
        QString("We found " + QString::number(nodeIndexer.size()) + QString(" atom(s).")), 0);

    // create a random generator with fixed seed
    static SBRandom randomGenerator;

    SB_FOR(SBNode* node, nodeIndexer) {

        SBAtom* currentAtom = static_cast<SBAtom*>(node);
        SBPosition3 position = currentAtom->getPosition();

        position.v[0] += SBQuantity::angstrom(randomGenerator.randDouble1() - 0.5);
        position.v[1] += SBQuantity::angstrom(randomGenerator.randDouble1() - 0.5);
        position.v[2] += SBQuantity::angstrom(randomGenerator.randDouble1() - 0.5);

        currentAtom->setPosition(position);

    }

}

Note the use of static for the randomGenerator, to force the random generator to be initialized only once (else, the same random perturbation would be applied each time we shake atoms). To create a random generator with custom seed you can change it to

// create a random generator with custom seed based on the time
SBRandom randomGenerator(SAMSON::getTime());

After recompiling, the app now randomly shakes atoms:

Shake atoms

Making it safe: undo & redo#

What if users shake too much and want to undo the perturbation? In SAMSON, many functions are undoable, and we just need to turn on and off the holding mechanism which stores incremental modifications to the document:

void SEAtomShakerApp::shakeAtoms() {

    SBNodeIndexer nodeIndexer;
    // collect all atoms in the active document
    SAMSON::getActiveDocument()->getNodes(nodeIndexer,
                                          SBNode::IsType(SBNode::Atom));

    SAMSON::setStatusMessage(
        QString("We found " + QString::number(nodeIndexer.size()) + QString(" atom(s).")), 0);

    // create a random generator with fixed seed
    static SBRandom randomGenerator;

    // turn the holding mechanism on
    SAMSON::beginHolding("Shake atoms");

    SB_FOR(SBNode* node, nodeIndexer) {

        SBAtom* currentAtom = static_cast<SBAtom*>(node);
        SBPosition3 position = currentAtom->getPosition();

        position.v[0] += SBQuantity::angstrom(randomGenerator.randDouble1() - 0.5);
        position.v[1] += SBQuantity::angstrom(randomGenerator.randDouble1() - 0.5);
        position.v[2] += SBQuantity::angstrom(randomGenerator.randDouble1() - 0.5);

        currentAtom->setPosition(position);

    }

    // turn the holding mechanism off
    SAMSON::endHolding();

}

The SAMSON::beginHolding function turns the holding mechanism on. In the history window, the user will see a "Shake atoms" command that can be undone. The SAMSON::endHolding function turns the holding mechanism off. If the user undoes the command, all undoable functions called between beginHolding and endHolding (in this case, the setPosition calls) will be undone:

Shake atoms, undoable operation

A select few#

The user might want to perturb the positions of selected atoms only. To achieve this, we simply form a more complex node predicate when we collect atoms in the shakeAtoms function:

// collect all selected atoms in the active document
SAMSON::getActiveDocument()->getNodes(nodeIndexer,
                                      SBNode::IsType(SBNode::Atom) && SBNode::IsSelected());

This gives more control to the user:

Shake selected atoms

Shaking magnitude#

The user might want to setup the maximal shaking distance. To achieve this, we add a QDoubleSpinBox to the SEAtomShakerAppGUI.ui, rename the added QDoubleSpinBox as doubleSpinBoxMaximumDistance, and set the QDoubleSpinBox parameters as shown below:

Add doublespinbox

This spin box will define the maximum shaking distance in angstrom for each atom.

We modify the onShakeAtoms function in the SEAtomShakerAppGUI.cpp file:

void SEAtomShakerAppGUI::onShakeAtoms() {

    getApp()->shakeAtoms(SBQuantity::angstrom(ui.doubleSpinBoxMaximumDistance->value()));

}

Here, we send the maximum distance from the GUI to the shakeAtoms function of the app class.

Open the SEAtomShakerApp.hpp file and include the following header:

#include "SBQuantity.hpp"

We need to modify the shakeAtoms function declaration in the SEAtomShakerApp.hpp file as follows:

void    shakeAtoms(const SBQuantity::length& distance);

Note that here we are using SBQuantity::length and in the GUI class we are sending SBQuantity::angstrom. This is possible thanks to SAMSON's units.

Now, we modify the shakeAtoms function in the SEAtomShakerApp.cpp file:

void SEAtomShakerApp::shakeAtoms(const SBQuantity::length& distance) {

    SBNodeIndexer nodeIndexer;
    // collect all selected atoms in the active document
    SAMSON::getActiveDocument()->getNodes(nodeIndexer,
                                          SBNode::IsType(SBNode::Atom) && SBNode::IsSelected());

    SAMSON::setStatusMessage(
        QString("We found " + QString::number(nodeIndexer.size()) + QString(" atom(s).")), 0);

    // create a random generator with fixed seed
    static SBRandom randomGenerator;

    // turn the holding mechanism on
    SAMSON::beginHolding("Shake atoms");

    SB_FOR(SBNode* node, nodeIndexer) {

        SBAtom* currentAtom = static_cast<SBAtom*>(node);
        SBPosition3 position = currentAtom->getPosition();

        position.v[0] += distance * (randomGenerator.randDouble1() - 0.5);
        position.v[1] += distance * (randomGenerator.randDouble1() - 0.5);
        position.v[2] += distance * (randomGenerator.randDouble1() - 0.5);

        currentAtom->setPosition(position);

    }

    // turn the holding mechanism off
    SAMSON::endHolding();

}

Here, we change atoms' positions by a user defined maximum distance multiplied by a random number in the interval (-0.5, 0.5).

Now, users can specify the maximum shaking distance.

Shake with max shaking distance

Done#

Congratulations! You coded a SAMSON app that shakes selected atoms when the user presses a push button, and gives users the possibility to undo and redo their actions. You organized your code to cleanly separate the user interface from the core functionality of the app, and you touched interface design and widgets, signals and slots, nodes and node indexers, predicates, units, random generators, the holding mechanism, and selections. Good job!