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:
Leave the name of the App class be App, so that the class name will be SEAtomShakerApp
:
Click the Generate button:
Your SAMSON Extension has been generated, click Finish:
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:
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:
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:
The default interface contains a label with a developer tip. Delete the label and add a QPushButton
.
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:
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):
Finally, we connect the released
signal of the push button to this new slot (onShakeAtoms
):
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):
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:
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::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 aSBAtom
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:
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:
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:
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:
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:
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:
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:
We need to modify the shakeAtoms
function declaration in the SEAtomShakerApp.hpp file as follows:
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.
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!