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 Element created in this tutorial can be found at https://github.com/1A-OneAngstrom/SAMSON-Developer-Tutorials.

Generating an Element

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

Launch SAMSON and invoke the SAMSON Element generator. Select a folder where you want to generate a SAMSON Element.

Call the SAMSON Element AtomShaker:

SEG-AtomShaker-Name.png
SAMSON Element generator: naming an Element

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

SEG-AtomShaker-AddAppClass.png
SAMSON Element generator: add the app class

Click the Generate button:

SEG-AtomShaker-Generate.png
SAMSON Element generator: generate an Element

Your SAMSON Element has been generated, click Finish:

SEG-AtomShaker-Finish.png
SAMSON Element generator: the Element has been generated

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 Element.

Once the SAMSON Element is build and if you start SAMSON (e.g. the debug version provided with the SDK), you should see your app appear in the App menu:

AtomShaker-AppMenu.png
An icon of an app in the menu

and the app toolbar, with a default SAMSON icon:

AtomShaker-AppToolbar.png
An icon of an app in the toolbar

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 Element 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 Element Generator to get development hints.

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

AtomShaker-NewAppName.png
An icon of an app with a new 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 Element generator is this:

AtomShaker-DefaultGUI.png
A default GUI

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

AtomShaker-pushButton.gif
Add a button

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:

AtomShaker-pushButtonShake.gif
Rename the button

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):

AtomShaker-AddSlot.gif
Add a slot

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

AtomShaker-ConnectSignalToSlot.png
Connect a signal to a 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 Element 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 {
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::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 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:

The unit 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 Elements 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:

AtomShaker-TranslateAtoms.gif
Translating 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::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:

AtomShaker-ShakeAtoms.gif
Shaking 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::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
}

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:

AtomShaker-ShakeAtomsUndoable.gif
Shaking 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 predicate when we collect atoms in the shakeAtoms function:

// collect all selected atoms in the active document

This gives more control to the user:

AtomShaker-ShakeSelectedAtoms.gif
Shaking selected atoms only

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:

AtomShaker-doublespinbox.png
Add a QDoubleSpinBox

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::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
}

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.

AtomShaker-WithMaxDistance.gif
Shaking atoms with max 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!