Cornerstone includes an event-passing framework that is used to deliver information on object activity.
Events provide a convenient way for controlling mutual interaction between multiple objects. For example, in the case of widgets, events can be taps, the beginning of interaction or the end of interaction, to name a few. The purpose of the event passing mechanism is to allow easy triggering of actions based on events. A typical use-case for events would be buttons, where one wants to get information about touch-down, and touch-up, and trigger an action based on that information.
The event system in Cornerstone is based on class Valuable::Node. The class contains all member functions needed to use the event system. It keeps track of event listeners and senders and removes them when they are deleted. The MultiWidgets::Widget class inherits Node.
A single event consists of a message id (string of a type QByteArray) and message data (a binary blob of a type Radiant::BinaryData). The use of character strings enables easy naming and identification of messages, as the "address space" for strings is practically infinite. The message data is carried in a binary buffer, that has straightforward read/write functions for storing and parsing basic data types.
To be able to receive events, one needs to register the listening object to the sender of the event. This can be achieved by calling eventAddListener function of the sender object and supplying it with the pointer to the receiving object:
After calling this function, receiverPtr will receive message with id messageId every time when senderPtr sends an event with id eventId.
It is possible to register arbitrary functions as event listeners. This is done using the Valuable::Node::eventAddListener(const QByteArray &, ListenerFuncVoid, ListenerType)
To register a simple callback when a widget is tapped on can be done as follows:
In the above example, the TextWidget could be replaced with any widget.
One thing to keep in mind when working with event listeners and lambdas is to avoid cyclic dependencies. If you capture widget pointers inside a lambda, it is easy to create a cyclic dependency that prevents the widgets from being destroyed. It can happen when two objects stored as smart pointers refer to each other. When the copy of smart pointer is captured inside lambda, the reference counter is incremented by one. The target of smart pointer will not be destroyed until the lambda function containing the smart pointer is destroyed.
When capturing widget pointers inside lambda functions, one should use MultiWidgets::WidgetWeakPtr to break the cycles. Weak pointers do not increase the reference counters and thus allow deletion of their target objects when other references to the target are deleted. An example of MultiWidgets::WidgetWeakPtr is shown below. All widgets inside MultiWidgets have corresponding weak pointer type that work same way.
Events are processed in a virtual function eventProcess():
It is possible to override this function and write custom code to handle events. One should first try to process the message, and after that call the eventProcess of the parent class if the message is not handled locally. To process a message, one would typically check the message id string against expected event ids, and once the match is found proceed to parse the data in the binary data holder. In many cases the message data can be ignored, as simply knowing that an event occurred can be the relevant information. For example eventProcesss could be implemented like follows:
The binary data argument contains possible event content. This could be for example a string describing an URL to open. The use of binary data might look a bit raw, but it allows conversions between data types within the blob parser, so that the receiver can for example process integer and floating-point parameters with the same code. The sender of the event can be queried by calling sender-function.
Events can be sent using the function eventSend of Node-class. The following code gives an example of sending an event:
The system will then send out an event "statistics" to all registered receivers with data defined. There are lots of built-in events in Cornerstone. For example everytime when interaction begins with an widget, it sends an event. The exhaustive lists can be found from the documentation of each subclass of Valuable::Node.
Cornerstone supports management of the events in a form of eventAddOut- and eventAddIn-functions. By calling these functions, objects can register to be able to send or receive events with the given id. Warnings are generated at the runtime if objects are listening or sending undeclared events.
Below are comparisons to some other event frameworks.
wxWindows (and many other systems) uses integers as the event identifiers. For successful event passing the event sender and receiver need to share the identifiers, which shared globally. This approach has the advantage of being extremely fast (due to the speed of integer comparisons), but it makes programming quite difficult as one needs to maintain and share the integer values. Avoiding collisions between identifiers can be difficult if the code for the application is coming from multiple sources. Likewise maintaining shared identifiers is difficult
Java Swing uses interface classes to implement listeners. Thus a class listening to various events, needs to implement a series of interfaces to match the event types. The system has the advantage of allowing one to pass customized events to each listener. However, the creation of new event types requires meticulous work in writing the interfaces, and then implementing the receiver functions. Originally Cornerstone used the interface approach with keyboard event handling. The old feature is still present, but in addition to it, the keyboard events are also available with the "standard" event processing system.
Qt uses signal/slot mechanism to connect events (aka signals) to slots (aka callback functions). The system has also been implemented in the Boost C++ libraries. The system has been successful in overcoming practically all the problems of the previous event passing methods, while offering an intuitive programming approach. A C++ purist might argue that Qt extends C++ so heavily that it is not proper C++ any more. A practically oriented person would accept the signal/slot mechanism as a bug fix, that implements the "missing" event passing mechanism in C++.
We have tried to include the good features of the signal/slot model into our event framework. Using strings as event idenfitiers (which is the basic structure behind Qt's signal/slot implementation) offers a lot of freedom. The CornerStone event mechanism does not rely on a QApplication (or any other marshalling object) to run for the event processing to work. The event renaming system also has an advantage over the signal/slot mechanism by allowing parameters to be embedded in the event identifier. This has been a small nuisance in Qt, since connecting several senders (for example a row of push-buttons) required either implementation of several slots in the receiver or the use of intermediate mapping objects that disambiguate the events coming from the senders. In the signal/slot approach the event sender and and listener must have exactly the same arguments (identical function signature), which may lead to situations where one needs to implement multiple slots, to receive practically identical signals from multiple sources. In practice this is seldom an issue if the data types in the signals have been selected in a clever way.
As a (some-what partial) conclusion it can be said that signal/slot mechanism offers a slightly easier message processing environment, while the Cornerstone event system has higher flexibility in regard to processing events in more complex situations, or using the event system without the need for relying on the QApplication or similar event marshaller.