labplot  2.8.2
About: LabPlot is an application for plotting and analysis of 2D and 3D functions and data. It is a complete rewrite of LabPlot1 and lacks in the first release a lot of features available in the predecessor. On the other hand, the GUI and the usability is more superior.
  Fossies Dox: labplot-2.8.2.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

The Aspect Framework

The notion of what we call Aspects pervades and unifies the internal representation of SciDAVis / LabPLot projects. Basically, every line in the project explorer corresponds to an Aspect, although Aspects which the user normally does not need to know about are hidden by default. Also, note that Aspects are internal in the sense that they do not interact directly with the user. The latter task is done by views. Most Aspects have a primary view with which they have a 1:1 relationship; specifically, if its primary view is closed, an Aspect gets removed. However, Aspects also can have multiple views on them or no views at all. Aspects organize themselves into trees, usually with a Project at the top. They can also be used standalone, although in that case some features (especially undo) do not work.

The Aspect framework provides the following features:

  • Utilities and guidelines which make implementation of undo-aware changes relatively painless.
  • Organization of object trees with multi-level undo for all changes and powerful features like object hiding and querying trees based on the object's class and hidden status.
  • XML (de-)serialization of (partial) trees
  • Common attributes like name or comment, also with full undo support.
  • Easy access to global settings via global() and setGlobal().

Every Aspect is internally divided into three layers: private data, undo commands and public API. Additionally, two more layers are built on top of Aspects.

Layer 0 (private data)

Every Aspect class has an internal class called Private, which manages its persistent data. The fact that it is internal implies that it cannot inherit from QObject (or any of its descendants); usually, Private will not inherit from any class. Note that since most classes in the inheritance chain of an Aspect class have their own private classes, Aspect objects in general have multiple private objects associated to them. Every private object knows its owning Aspect and can use its inherited public API (layer 2) to talk to the other private objects of the same Aspect object. The technique of introducing a separate class which hides the content of an object is known variously as "Pimpl idiom", "Cheshire Cat", "D-Pointer" etc.; one of the main advantages is that it makes it easier to change the implementation of a class without breaking binary compatibility (i.e., plugins don't need to be recompiled). Also, in the context of this framework, the indirection involved in accessing layer 0 from layer 2 (the API class) serves as a reminder that changes to the data must be routed through layer 1 (i.e. instantiating a command).

Since encapsulation of an Aspect is already provided by layer 2, private classes can be pretty promiscuous with their member variables; responsibility for maintaining a coherent state is shared with layers 1 and 2 (this is necessary in cases where a single action needs to change data contained in multiple private objects). In addition to the member variables, private classes usually provide functionality common to multiple undo commands (layer 1). These methods must only operate on the internal data, since write calls into layer 2 usually cause undo commands to be pushed onto the undo stack (which must not happen within another undo command). Purely internal Aspects which are not part of the hierarchy of the Aspect in question (e.g., temporary filters or columns) count as internal data and can of course be modified.

Layer 1 (undo commands)

Every undo command instance is associated with exactly one private object and accesses only data and methods of this object. For common cases, generic undo commands (like PropertyChangeCommand or SignallingUndoCommand) can be used; more involved cases require undo commands written specifically for one Aspect class (say, MyAspect). The latter follow the naming convention MyAspect*Cmd and are gathered into source files myaspectcommands.cpp and myaspectcommands.h in order to reflect their association. Read access to other private objects of the same Aspect is done via the Aspect's inherited public API. If write access to multiple private objects is necessary, separate undo commands are created and instantiated within a command macro (compare e.g. AbstractColumn::removeRows()).

Undo commands are also responsible for triggering signals that are part of the public API of an Aspect (i.e. change notifications which allow views and other Aspects to update themselves if necessary). Usually this can be accomplished by using AbstractAspect::exec(QUndoCommand*,const char*,const char*,QGenericArgument,QGenericArgument,QGenericArgument,QGenericArgument) or SignallingUndoCommand.

Undo commands specific to one Aspect class will usually rely on implementation details of its private class; they even may require a call protocol, that is, make assumptions about the context of their employment. Since undo commands are internal to an Aspect and often only used in one place, this is not a big problem. Still, it makes sense to document such assumptions to ease future changes to the code.

Layer 2 (public API)

The public API of an Aspect consists exclusively of methods which a) are undo safe, b) have no side effects (orthogonality) and c) don't require call protocols (they are atomic). Basically, the API will provide direct read access to the data in Private (layer 0) and translate write access into instantiations of appropriate undo commands (layer 1). Actions that require changes to multiple Private objects are a bit more subtle, since they require multiple undo commands to be instantiated and packaged into a macro; possibly with some simple logic to connect them. As opposed to layers 0 and 1, layer 2 may also use the public API of other Aspects in the hierarchy without restrictions.

In the simplest case, an Aspect's private object MyAspect::Private has a public member variable (say, QString m_tag) which doesn't have to be coordinated with anything else. In that case, no custom undo command needs to be written and MyAspect could use a setter method like this:

01 void MyAspect::setTag(const QString &value) {
02 if (value == m_my_private->m_tag) return; // don't create superfluous commands
03 exec(new PropertyChangeCommand<QString>(tr("%1: change tag").arg(name()),
04 &m_my_private->m_tag, value));
05 }
Generic undo command changing a single variable.

Line 2 is a short check which is supposed to prevent creation of command instances that do not actually change anything. Lines 3 and 4 instantiate a PropertyChangeCommand (which changes a single variable) and hand it to AbstractAspect::exec(). The PropertyChangeCommand receives a short user-visible description, a pointer to the variable to be changed and the new value of the variable. It takes care of creating a backup of the current value and switching between the old and the new value on undo/redo. AbstractAspect::exec() makes sure that the command is executed at least once and, if the MyAspect instance is part of a Project, saves the command for later undo/redo.

Part of the public API are also signals sent to views and other Aspects in order to notify them of changes in the data. While this is the responsibility of layer 2, convenience methods are provided so that also in this case, no custom undo command needs to be written. If MyAspect wishes to send the signals "void tagAboutToChange(const MyAspect*)" and "void tagChanged(const MyAspect*)" before and after its tag property changes, this can be accomplished by adding a single line to the above example:

01 void MyAspect::setTag(const QString &value) {
02 if (value == m_my_private->m_tag) return; // don't create superfluous commands
03 exec(new PropertyChangeCommand<QString>(tr("%1: change tag").arg(name()),
04 &m_my_private->m_tag, value),
05 "tagAboutToChange", "tagChanged", Q_ARG(const MyAspect*,this));
06 }

This calls an overloaded version of exec() which creates a command macro and two instances of SignallingUndoCommand behind the scenes.

Layer 3 (Qt models)

Between an Aspect and one of its views, classes derived from QAbstractItemModel may be used to translate between the Aspect's public API (layer 2) and Qt's Model/View framework. Every view (layer 4) can own one or more such translation objects. Examples include AspectTreeModel and SpreadsheetModel.

This layer is only needed in order to use existing views of Qt's Model/View framework; and as such it is optional.

Layer 4 (views and scripts)

Views use layers 2 and 3 of an Aspect in order to build the GUI and process user input. An Aspect and its primary view know about each other and call each other's API. View-related actions in the project explorer (like "maximize") always act on the primary view. The primary view class of an Aspect class MyAspect is called MyAspectView. Most users will not notice a difference or have to make a distinction between an Aspect and its primary view, whereas secondary views are represented as distinct entities (typically modeless dialogs) to the user. Showing and hiding of the primary view is done via the public API of the Aspect so as to be available via the Aspect's context menu.

Views can access layer 2 APIs of other Aspects, although this is expected to be rare. A view receives signals only from its associated Aspect. Should a view need to react on changes to multiple Aspects, then one Aspect has to gather signals from the others and relay them to the view.

Because views and scripts make changes to the underlying data only via the public API, one does not have to care about undo or synchronisation between different views or scripts and views on this layer. Views just have to make sure that they update themselves upon the appropriate change signals from their Aspect. Also, only the API of layer 2 has to be wrapped for access from within scripting languages.

The details of handling primary views is dependent on the kind of Aspect. For example, AbstractPart expects its implementations to provide a view() method returning a QWidget. To the outside world, it provides the method mdiSubWindow(), which wraps the result of view() into an MDI window and makes sure that this window gets closed when the Aspect is removed and vice versa.

Base Classes for Aspects

AbstractAspect

The base class of all Aspects is AbstractAspect. It implements undo-aware adding and removing of child aspects, basic properties like name and creation time and provides methods for handling undo commands. Upon changes to the Aspect tree or to the properties, signals are emitted. It also allows implementations to specify a context menu and an icon for use by the project explorer.

AbstractPart

AbstractPart is the base class of all Parts, which are somewhat analogous to KDE's KParts. As a first approximation, a Part is everything that gets displayed in its own MDI subwindow, although in principle Parts could be displayed differently, e.g. in their own main windows.

AbstractColumn

AbstractColumn provides a generic interface for data columns. Its purpose is to abstract away the differences between stored and computed data, allowing filters and graphs to transparently handle all kinds of data sources.

AbstractFilter

AbstractFilter is the base class for all analysis operations. It is modelled on integrated circuits, which is to say, a filter is basically a black box with input and output ports. Input ports accept their data in the form of AbstractColumn instances; output ports provide their data in the same form. Typically filters generate their data lazily; that is, only when asked for data on an output port they will start reading from their inputs. This is not true for all filters, however; StatisticsFilter being a noteworthy exception. Notification of changes to the input data is propagated in the form of signals.