We have started seeing how tutorials can be written in C++. Now we will see how to write tutorials in a script language.
KTutorial uses Kross to give support to scripting languages. Kross is a scripting framework that acts as a bridge between C++ and scripts. It doesn't implement interpreters or bindings for the scripting languages, but offers access to the already existing ones. If you are curious, I wrote the results of my experiments checking what can and what can't be done with Kross in Some notes about Kross.
In short, KTutorial exposes a C++ API to the scripts, and from the scripts you just use that API. The API is made of Qt objects, and you can access to their properties, slots and methods marked as invokable. Basic types like int, double, strings and so on are also supported through QVariant
.
The example tutorial will be the same shown in the C++ example, that is, it will just ask the user if he prefers to write one text or another and then it will wait for the user to clean the text using the Clean action. You should read before the C++ example, as only the script specific matters will be explained, but not the KTutorial concepts themselves.
This example will be written in JavaScript. The reason is that JavaScript backend is always installed in Kross, as it is part of kdelibs. However, Python and Ruby backends must be installed explicitly, as they are part of KDE Bindings. So JavaScript tutorials will be always recognized, unlike Python and Ruby tutorials that may not be understood by Kross.
The tutorial will be written in a JavaScript file, for example, SimpleTutorial.js
. The scripted tutorials must be explicitly installed somewhere for KTutorial to use them. KTutorial looks for tutorials in the tutorials
directory of the application data directory. So there is where the scripted tutorials must be installed, which can be done easily in your CMakeLists.txt as shown below.
Example 3.9. How to install the scripts in CMakeLists.txt
install(FILES SimpleTutorial.js DESTINATION ${DATA_INSTALL_DIR}/theNameOfYourApplication/tutorials)
In the C++ tutorial we created a class deriving from Tutorial
class. In scripted tutorials we don't need to create a custom class, it is created implicitly for us. If you are looking to the script through the C++ glass, think about the script like the constructor of our C++ Tutorial
derived class (although supporting even functions and classes defined on it).
The tutorial object is exposed in the script in a variable called tutorial
(nobody expected it, neither the Spanish Inquisition). As not every Kross backend supports WrapperInterface
, tutorialInformationAsObject
method, which returns a QObject*, has to be used instead of the more natural tutorialInformation
method, which returns a TutorialInformation*. Further information about this subject near the end of Some notes about Kross.
To internationalize strings, Kross provides a module called
kdetranslation
that offers the typical i18n
, i18nc
... functions as methods of the module object (although they receive a list of values instead of a value for each string argument, as it will be shown later). Note that in other script languages, like in Python, you would need to import Kross before using it. The nice thing is that as it uses KDE i18n system, you don't have to do anything special for it to work, just enclose strings in double quotes and extract the messages from script files like you would do from C++ files (that is, just add -o -name '*.js'
to
find
in your Messages.sh
file). Further information about this subject in Internationalization in Kross scripts.
Example 3.10. Creating a TutorialInformation (script)
t = Kross.module("kdetranslation"); tutorial.tutorialInformationAsObject().setName(t.i18n("Clear the text area")); tutorial.tutorialInformationAsObject().setDescription(t.i18n("This tutorial \ shows how to clean the text area with ease")); //New things will be added here
As already said, KTutorial exposes a C++ API to be used from the scripts, getting and setting the properties and calling slots and invokable methods. However, Kross can't create objects of a C++ class. To build the tutorial we need to create steps, options and so on. So, how do we get new objects?
There is a special object, of scripting::ScriptingModule
C++ class and exposed in the variable ktutorial
, that acts as a factory for new objects (and also to find objects by name).
There is also another difference from C++ tutorials when adding an Option
or a WaitFor
. In C++, we specified an object and the slot to call when the condition was satisfied. We still can in a script, but there is also another possibility, more appropriate for scripts. Using the same syntax, we can call a function of the script when the condition is satisfied, refering to the script as self
and specifying the name of the function as a string.
Finally, take into account that variables in JavaScript (and other script languages) are defined when used for first time and are deleted when their scope ends. So when you assign a variable inside a function that variable must have been defined in the global scope before the function is called, or just a local variable will be created and destroyed inside the function.
Example 3.11. Using options in a step (script)
//Added after the creation of TutorialInformation writeText = false; //Step start startStep = ktutorial.newStep("start"); startStep.setText(t.i18n("In this tutorial you will learn how to clear text.\n\ To do this, first something must be written in the text area. What do you prefer \ to write?")); startStep.addOption(ktutorial.newOption(t.i18n("Some text")), self, "textSelected()"); function textSelected() { writeText = true; tutorial.nextStep("writeText"); } startStep.addOption(ktutorial.newOption(t.i18n("Some numbers")), self, "numbersSelected()"); function numbersSelected() { writeText = false; tutorial.nextStep("writeNumbers"); } tutorial.addStep(startStep); //New things will be added here
If you didn't understand what I meant when I said that string arguments were passed as a list instead of a value for each argument, you can see it here. The C++ i18n
function receives the string to be internationalized/localized as its first argument. And then, if that string contained any argument to be replaced on it, each string argument was added as a different parameter of the function. When it is accessed through Kross, all the string arguments are grouped in a list of values instead (one or more comma separated expressions wrapped by [ and ]). It works the same way with the other i18n family functions.
In this example we can also see how ktutorial
object is used to find objects by their name.
Let's talk now about WaitFor
objects. Unlike Step
or Option
, there are several types of WaitFor
objects. Adding one method to ScriptingModule
for each WaitFor
subclass provided in the library would clutter ScriptingModule
API. However, KTutorial supports custom WaitFor
subclasses to be added by an application if needed, so there is no way for ScriptingModule
to provide a method for those subclasses.
So instead of one method for each WaitFor
type, there is only one which receives an string with the desired class name. Once the object is returned, its initialization can be ended as needed. For example, in the case of WaitForSignal
, there is an invokable method to set the emitter object and the signal to wait for.
Finally also note that toPlainText
is a normal method in QTextEdit
, so it can't be used through Kross. However, plainText is a Qt property, so we can read and (if allowed) write to it without problems. The returned QString
is automatically converted to a JavaScript string.
Example 3.12. Using WaitFor in a Step (script)
//Added after the creation of start step //Step write text writeTextStep = ktutorial.newStep("writeText"); writeTextStep.setText(t.i18n("Write \"%1\" (without quotes) in the text area", [t.i18n("Hello world")])); textArea = ktutorial.findObject("textArea"); waitForTextChanged = ktutorial.newWaitFor("WaitForSignal"); waitForTextChanged.setSignal(textArea, "textChanged()"); writeTextStep.addWaitFor(waitForTextChanged, self, "textWritten()"); tutorial.addStep(writeTextStep); //Step write numbers writeNumbersStep = ktutorial.newStep("writeNumbers"); writeNumbersStep.setText(t.i18n("Write \"%1\" (without quotes) in the text area", [t.i18n("4 8 15 16 23 42")])); writeNumbersStep.addWaitFor(waitForTextChanged, self, "textWritten()"); function textWritten() { if ((!writeText && textArea.plainText == t.i18n("4 8 15 16 23 42")) || (writeText && textArea.plainText == t.i18n("Hello world"))) { tutorial.nextStep("clearText"); } } tutorial.addStep(writeNumbersStep); //New things will be added here
And now, just add the two pending steps in the tutorial: one to wait for the text to be cleared, and the end one.
Example 3.13. Finishing the tutorial (script)
//Added after adding writeNumbersStep //Step clear text clearTextStep = ktutorial.newStep("clearText"); clearTextStep.setText(t.i18n("Now, you must trigger the Clear action.\nTo do \ this, you can click in File=>Clear, or in the toolbar button called Clear")); clearAction = ktutorial.findObject("clear"); waitForTriggered = ktutorial.newWaitFor("WaitForSignal"); waitForTriggered.setSignal(clearAction, "triggered(bool)"); clearTextStep.addWaitFor(waitForTriggered, "end"); tutorial.addStep(clearTextStep); //Step end endStep = ktutorial.newStep("end"); endStep.setText(t.i18n("And that's all.\nNow you can close the tutorial.")); tutorial.addStep(endStep);