Loading...
Searching...
No Matches
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 https://github.com/1A-OneAngstrom/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:

SAMSON Extension generator: naming an Extension

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

SAMSON Extension generator: add the app class

Click the Generate button:

SAMSON Extension generator: generate an Extension

Your SAMSON Extension has been generated, click Finish:

SAMSON Extension generator: the Extension 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 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 the App menu:

An icon of an app in the menu

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:

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 Extension generator is this:

A default GUI

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

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:

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

Add a slot

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

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 Extension generator pro tip: add slots here to interact with your app
void onShakeAtoms();
private:
Ui::SEAtomShakerAppGUIClass ui;
};
This class is the base class for App's GUI.
Definition: SBGApp.hpp:15

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();
};
#define SB_CLASS
Macro that is added inside the class declaration for Introspection, etc.
Definition: SBCClass.hpp:241
This class is the base class for apps.
Definition: SBDApp.hpp:18

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:

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);
}
}
#define SB_FOR(VARIABLE, CONTAINER)
A macro to easily write for loops involving containers.
Definition: SBCContainerFor.hpp:83
static void setStatusMessage(const QString &message, int time=0)
Shows a message in the status bar.
Definition: SAMSON.cpp:1647
static SBDDocument * getActiveDocument()
Returns a pointer to SAMSON's active document.
Definition: SAMSON.cpp:738
unsigned int size() const
Returns the number of indexed objects.
Definition: SBCContainerIndexer.hpp:481
This node predicate compares the node type with a given type.
Definition: SBDDataGraphNode.hpp:512
This class is the base class to describe a node in the data graph.
Definition: SBDDataGraphNode.hpp:33
@ Atom
Atom.
Definition: SBDDataGraphNode.hpp:67
This class describes a node indexer.
Definition: SBDDataGraphNodeIndexer.hpp:21
virtual void getNodes(SBNodeIndexer &nodeIndexer, SBNode::Type nodeType, bool selectedNodesOnly=false, const SBNodePredicate &visitPredicate=SBDDataGraphNode::All(), bool includeDependencies=false) const override
Collects nodes into nodeIndexer, based on a nodeType, a selection status and a visitPredicate,...
Definition: SBDDocumentFolder.cpp:804
SBDQuantityType< SBDQuantityUnitType< SBUnitSystemSI, -10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 > > angstrom
The angstrom type.
Definition: SBDQuantity.hpp:81
Quantity v[3]
The components of the physical vector.
Definition: SBDTypePhysicalVector3.hpp:768
This class describes an atom in a structural model.
Definition: SBMStructuralModelNodeAtom.hpp:34
SBPosition3 const & getPosition() const
Returns the atom's position.
Definition: SBMStructuralModelNodeAtom.cpp:330
void setPosition(SBPosition3 const &newPosition)
Sets the atom's position if the atom is not fixed.
Definition: SBMStructuralModelNodeAtom.cpp:356

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

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);
}
}
This class implements a random number generator.
Definition: SBDTypeRandom.hpp:17
double randDouble1()
Generates a random number on [0,1]-real-interval.
Definition: SBDTypeRandom.cpp:95

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());
static SBCTime getTime()
Returns SAMSON's internal time.
Definition: SAMSON.cpp:713

After recompiling, the app now randomly shakes atoms:

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
}
static void endHolding()
Ends holding.
Definition: SAMSON.cpp:2141
static void beginHolding(const std::string &name)
Begins holding.
Definition: SAMSON.cpp:2138

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:

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 node predicate returns true for selected nodes.
Definition: SBDDataGraphNode.hpp:548

This gives more control to the user:

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:

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()));
}
This template class defines physical quantity types.
Definition: SBDQuantityType.hpp:43

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.

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!