In the previous sections we have seen a basic example of KTutorial covering the main concepts. This section explores some of these concepts in greater depth and introduces others not shown before.
KTutorial provides some default WaitFor
subclasses to model common conditions to wait for:
WaitForEvent
: a simple condition that waits for an event to be received in some object. Note that the wait ends when the event of the specified type is received, no further checks are made. For example, if you wait for a MousePressed event, the wait will end whenever a mouse button is pressed (on the specified object). If you want to end it when the left button is pressed but not when the right button is pressed you have to subclass WaitForEvent
to implement a finer grained behavior.
WaitForSignal
: a simple condition that waits for a signal to be emitted by some object. Like WaitForEvent
, it just waits for the signal to be emitted. No argument value is checked. You have to provide your own WaitFor
to do the checks (or make them, when possible, in the slot called when the condition is met).
WaitForProperty
: a simple condition that waits for a property to have certain value. To wait for a property, the property must have a notify signal. However, no matter if it has a notify signal or not, a property can be used to check its value when another condition is met. For example, instead of calling a slot to check if a widget is visible once some signal is emitted, the WaitForSignal
can be composed with a WaitForProperty
for the visible property of the widget, even when the visible property does not have a notify signal.
WaitForWindow
: a simple condition that waits for certain window (or dialog) to be shown. Although it can be used with windows, modal dialogs and modeless dialogs, it was created mainly to deal with modal dialogs. Due to Qt's internals, waiting for a signal that is connected to a slot that shows a modal dialog does not work (the signal is not delivered to the WaitForSignal
until the dialog is closed). Waiting for the modal dialog to be shown must be used instead in that case.
WaitForStepActivation
: a simple condition that waits for its step to be activated. Its purpose is to create preconditions for a step. For example, if some item of a combo box is not selected when a step is activated, instead of showing that step the tutorial can change to another one that tells the user to select that item.
WaitForAnd
: a composed condition that waits for all its child conditions to be met.
WaitForOr
: a composed condition that waits for any of its child conditions to be met.
WaitForNot
: a special composed condition to be used only as a child of a WaitForAnd
. Its purpose is negate the condition it contains, so you can wait for one or more conditions unless other conditions were already met.
Please refer to the API documentation, the examples in KTutorial test app and the tutorials in KTutorial editor for further information.
Sometimes, as we have seen, the default WaitFor
subclasses provided by the library may not be enough. For example, to wait for left mouse button to be pressed, ignoring the other mouse buttons. In cases like that, you can extend WaitFor
or any of its subclasses to provide the desired behavior.
Custom WaitFor
subclasses can be made only in C++. It doesn't mean, however, that they can't be used from scripts. You can register your custom WaitFor
subclasses (before calling setup
method in KTutorial
class) as long as you provide a constructor with no arguments.
Example 3.14. Registering a custom WaitFor
subclass
... #include <ktutorial/KTutorial.h> #include "CustomWaitFor.h" ... using ktutorial::KTutorial; ... ApplicationMainWindow::ApplicationMainWindow(QWidget* parent): KXmlGuiWindow(parent) { ... KTutorial::self()->registerWaitForMetaObject(CustomWaitFor::staticMetaObject); KTutorial::self()->setup(this); KTutorial::self()->registerTutorial(new TutorialDerivedClass()); ... setupGUI(); }
Once registered, objects from the custom WaitFor
subclass can be created in scripts like objects from any default WaitFor
subclass included in the library.
Example 3.15. Creating an object of a custom WaitFor
subclass in a script
... waitForSignal = ktutorial.newWaitFor("WaitForSignal"); waitForCustomCondition = ktutorial.newWaitFor("CustomWaitFor"); ...
In the previous tutorial examples everything was initialized when the tutorial was created. In the C++ tutorial, steps and their reactions were created and initialized in the tutorial constructor. In the JavaScript tutorial, they were created and initialized in the main script body. But, what happens if we need, for example, to wait for a signal emitted by an object that did not exist when the tutorial was created?
For example, suppose that some step has to wait until a dialog is accepted. It is very unlikely that the dialog exists when the tutorials are created, that is, when KTutorial is set up. If the object doesn't exist, findObject
can't return it, so the step ends waiting for a signal emitted by a null object, the waiting never ends, and the tutorial never changes to another step.
The methods setup
and tearDown
in Tutorial
and Step
classes are the solution. They are executed when a tutorial is started or finished, or when a step is activated or deactivated (the tutorial changes to that step, or changes from that step to another one).
In C++ tutorials, setup
and tearDown
methods must be redefined in subclasses.
Example 3.16. Custom step setup in C++ tutorial
class CustomStep: public Step { ... virtual void setup() { QWidget* widget = KTutorial::self()->findObject<QWidget*>("widgetFoo"); addWaitFor(new WaitForEvent(widget, QEvent::Close), "otherStepId"); } ...
In scripted tutorials, a function to be connected to the setup and tearDown signals of tutorial or step objects must be done.
Example 3.17. Custom step setup in scripted tutorial
... function customStepSetup(step) { widget = ktutorial.findObject("widgetFoo"); waitForClose = ktutorial.newWaitFor("WaitForEvent"); waitForClose.setEvent(widget, "Close"); step.addWaitFor(waitForClose, "otherStepId"); } connect(customStep, "setup(QObject*)", this, "customStepSetup(QObject*)"); ...
The most common use for step setup methods is creating and initializing the reactions of that step that depend on "dynamic" objects (objects that may or may not be created depending on the user actions, like dialogs). However, there is no harm in creating every reaction of a step in its setup method. In fact, KTutorial editor generated code uses that approach to be on the safe side.
Also note that reactions (WaitFors and Options) created in step setup methods are automatically removed and deleted in step tearDown, so you don't have to worry about cleaning up them.
An example of the use of setup and tearDown methods can be seen in UsingKTutorial
tutorial class, under ktutorial-library/src/tutorials
directory.
When the tutorial changes to clearText step, it creates and shows a QTextEdit
to the user. It is done in ClearTextStep
class, a Step
derived class with a custom setup method.
Other steps wait for signals sent by the QTextEdit
, so the WaitForSignal
for these steps must be created and initialized after the QTextEdit
was created. So other Step
derived classes with custom setup methods are made for that. Also note that, as already said, the WaitForSignal
created in setup methods are automatically removed and deleted in their tearDown counterparts, so it doesn't have to be done explicitly.
Finally, there is a tearDown
method in the tutorial class. It is executed when the tutorial finishes, and ensures that the QTextEdit
is hidden and deleted, just in case the user closed the tutorial before closing the window with the QTextEdit
.
Most of the time, the name of the objects, when set, will be unique. However, it is not always the case. Think, for example, in the Ok button
or something like that, and inside its dialog it will be unique; in the whole application, on the other hand, it probably will not. So, if you try to find the object called Ok button
and there are several dialogs opened at that moment you can't be sure that you will end getting the desired one.
In cases like this one the way to identify an object using its name is also including its ancestor names. That is, instead of calling findObject
with the parameter Ok button
, call it with Configuration dialog/Ok button
. Note that the ancestor does not need to be the direct parent; any ancestor will work. And also note that as many ancestor names as desired can be included. For example, Configuration dialog/Button bar/Ok button
.
It is advisable that at least one of the ancestors has a unique name. However, it is not a must. Using ancestors with a name not unique is also valid if the full path itself is unique. For example, if there are several objects called Chart
and several objects called Dialog
, but only one Chart
object is descendant of a Dialog
object, then Dialog/Chart
is a valid name to look for.
Anyway, sometimes an object may not have a unique name. For example, think in two nested dialogs (a parent dialog and a child dialog) with an Ok button
each. If the Ok button
in the parent dialog is its direct child, its name would be Parent dialog/Ok button
. But that name will represent too the Ok button
of the child dialog, as the parent dialog is its ancestor too. So, when a name is not unique, a set of ambiguity solving rules is applied when findind the object it represents. Please refer to the API documentation of findObject(QString)
method in KTutorial
class for further information.
Tutorials usually refer to widgets when explaining the user what has to be done: show the item list in the combobox, click this or that button, write something in a text editor, etc. However, a user may not know what a combobox is, or may not be sure which button in a toolbar push, or may just want a confirmation that the tutorial expects him to write where he is going to write.
For cases like those ones, KTutorial provides a way to create links to widgets in the step text. When the link is clicked, the referenced widget will be highlighted. The highlighting will stop when the link is clicked again, the widget gets the focus or the link to another widget is clicked.
The links are like standard HTML links, but using a special URI:
widget:
theObjectNameOfTheWidget
. So, for example, to link to a button which object name is addButton
in some text you will write something like
Click on <a href="widget:addButton">the button with the plus sign</a> to create a new entry
or, if KDE semantic markup is used,
Click on <link url="widget:addButton">the button with the plus sign</link> to create a new entry
.
KTutorial provides a default KDE user interface ready to be used in your application, but it also offers you the possibility to use your own user interface. Instead of initializing KTutorial through the setup(KXmlGuiWindow*)
method in KTutorial
class, you have to initialize it using the setup(KTutorialCustomization*)
method, passing it your own implementation of KTutorialCustomization
interface.
That class has to set up the user interface of KTutorial, which is composed of two main elements: a user interface for the tutorial manager, and a user interface for the tutorials. The tutorial manager UI is responsible for showing the available tutorials to the user so he can decide which one to start, while the tutorial UI is responsible for showing an specific tutorial to the user.
Please refer to the API documentation of KTutorialCustomization
interface and the code itself of DefaultKdeCustomization
class for further information.
Note that the default tutorial, Using the tutorials , is highly coupled to the default KDE user interface of KTutorial, so it is registered only when that default UI is used. If you customize KTutorial you should also provide a tutorial explaining how to use the tutorials with your user interface.
Although the bridge made by Kross between Qt Meta-Object System and scripts is pretty nice, sometimes it is not enough. Sometimes you may need a feature that can't be accessed from properties or slots. For example, if you need to get a QTextCursor
in a QTextEdit
to know the currently selected text. Or if you need to define a class in the scripting language to be used as an event filter (which needs a QObject
derived class).
What happens in these cases? Are we condemned to a scripting world stuck to Qt Meta-Object System? Not at all. Fortunately, being just a bridge Kross allows us to use the scripting language bindings if they are available. So we can use PyQt, PyKDE, or the Smoke based bindings (QtRuby/Korundum, JSmoke...) like we would do in a normal script :D
“Can you show us an example, please, please?” Yes I can. And so I will do ;)
PyQt4 are the bindings of Qt4 for Python. It makes possible to use all Qt classes from Python code, and even defining Python classes that inherit from Qt classes. Impressed? You should. But there is still more. Go on reading to find out what else can PyQt4 do for you.
The following example will show, step by step, how to install an event filter in a C++ QTextEdit
that emits a signal when the mouse left button is pressed. The event filter will be defined in Python code and its Python signal will be connected in C++ code. Cool, isn't it? :)
To install an event filter in the QTextEdit
viewport, installEventFilter
method must be called. But this method isn't an slot, and neither an invokable method, so it can't be accessed directly by Kross. We need to use the PyQt version of QTextEdit
class.
But the text edit to install the filter on it already exists... and is a C++ object! Don't panic. Here enters PyQt4: we can get an existing C++ Qt object and wrap it in a Python PyQt object.
Example 3.18. Wrapping a Qt object as a PyQt object
textEdit = ktutorial.findObject("textEdit") import sip from PyQt4 import QtCore, Qt textEditPyQt = sip.wrapinstance(textEdit.__toPointer__(), Qt.QTextEdit)
Ok, now we could call installEventFilter
method on textEditPyQt
. But it expects an object of a class inheriting QObject
. How can that be done?
Thanks to PyQt4, a Python class can be defined as inheriting from QObject
, like you would do to inherit from a pure Python class. Then, create an object of that class and you are done.
So, after wrapping the QTextEdit
, we define the filter class and install an object of that class.
Example 3.19. Creating a Python class that inherits from a Qt class
class MouseFilter(Qt.QObject): def eventFilter(self, object, event): return False mouseFilter = MouseFilter() textAreaPyQt.viewport().installEventFilter(mouseFilter)
Yes, our mouse filter is the dumbest event filter ever created. Don't worry, we'll fix that now. As it was explained, the event filter must emit a signal when the mouse left button is pressed. The tricky part is this: the signals will be defined in the PyQt class, but they will be connected in C++ code. The C++ code knows nothing about Python, nor it needs to. It just wants to connect a signal from a QObject
derived class. So the Python class must have its corresponding QMetaObject
, and that QMetaObject
must know about the signal.
Rejoice, for PyQt is able to do exactly that since version 4.5. You define the Python class and its signal, and then you sit back and let PyQt do the magic. If you want further information on this subject, please see the chapter New-style Signal and Slot Support in PyQt4 reference guide. As far as I know, the old signal and slot support didn't add the signal to the QMetaObject
of the Python class, so it couldn't be connected from C++ code.
So we change our previous MouseFilter
class to the one in the next example.
Example 3.20. New-style signal and slot support in PyQt 4.5
class MouseFilter(Qt.QObject): mousePressed = QtCore.pyqtSignal() def eventFilter(self, object, event): if event.type() == QtCore.QEvent.MouseButtonPress: if event.button() == QtCore.Qt.LeftButton: self.mousePressed.emit() return False
But... what happens if the PyQt installed version is lower than 4.5? An ugly and strange error message (for a common user) is thrown in the console logs. “There isn't a nicer way to inform the user?” I hear you ask. “Yes, it is” I answer you.
Although still not the nicest way to inform the user (in the future I'll take care of error management in KTutorial, but it is not done yet), we can change the strange message to a friendlier one. To do this, we check if the PyQt version is lower than 4.5 and, in that case, we raise an exception with a clearer message. If no PyQt is installed at all, the system will raise an exception telling that it can't find PyQt module when it is imported.
So we add this before the MouseFilter
class, but after importing QtCore and Qt.
Example 3.21. Checking whether the needed PyQt4 version is available or not
import string version = string.split(QtCore.PYQT_VERSION_STR, ".") if map(int, version) < [4, 5]: raise ImportWarning('PyQt version is lower than 4.5, aborting')
Finally, the time to connect the Python signal in C++ code has come. In this case, it will be done setting the signal to wait for in the WaitForSignal
object. The method setSignal
receives the object that emits the signal and the name of the signal to connect to. Even being an object from a Python class, it is gracefully converted by Kross and PyQt to a QObject
pointer, as the Python class inherits from QObject
.
Internally, the method just adds a "2" in front of the signal name (as that is what connect
expects) and calls connect
with the object emitting the signal, the signal name prefixed with "2", the receiver object and its slot. Yes, exactly the same that would be needed to connect a C++ signal without using SIGNAL macro. As already said, this is so easy because PyQt >= 4.5 adds the signal to the QMetaObject
of the Python classes that inherit from QObject
.
In the example below, and to push even further the limits of Kross and PyQt, when the signal is emitted, it is handled by the C++ code which, in turn, calls a Python function!
Sadly, at the time of this writing, there is an open bug in Kross that makes the application crash in this scenario. Hopefully by the time when you are reading this, it will be already fixed ;) For further information, refer to krosspython: crash when PyQt signal is connected to C++ method that calls a function in the script.
Example 3.22. Connecting the Python signals in C++ code
waitForMousePressed = ktutorial.newWaitFor("WaitForSignal") waitForMousePressed.setSignal(mouseFilter, "mousePressed()") mousePressStep.addWaitFor(waitForMousePressed, self, "mousePress()") def mousePress(): # Code to be executed when the signal is emitted