-
Notifications
You must be signed in to change notification settings - Fork 0
Heaven (Introduction)
Heaven is a tool library enabling you to write modularized software with consistent graphical user interface. It glues all the required match sticks together for you almost transparently behind your back.
Basically, Heaven consists of 2 utilities:
-
An action framework
An application typically has one menu bar and several tool bars. In an monolithic application there's nothing wrong with that. In a modularized application, however, this might lead to hard to implement Lego.
Imagine a word processor application. There will probably be File, Edit, View and Tools menus. But now you navigate your edit cursor into a table. Suddenly there is a requirement to insert a Table menu, say between the View- and Tools-Menu. Heaven will allow you to do that, with exactly one additional line of code.
Heaven defines a so called "Heaven Interface Definition" (HID) language.
The syntax of a HID file is strongly oriented towards C++ but is actually kind of declarative. HID files are read and compiled by the so called Heaven Interface Compiler (hic). Hic produces a C++-Header- and a C++-Source-File. Inside it, a class is generated which will setup all required
QAction
,QMenuBar
,QMenu
,QToolBar
etc. objects for you. This is of code. In past development projects I have often seen such code scattered around in several places.Back in 2008 I wrote a completely XML based solution of this concept. As I must sadly admit today, I gave copyright on that to a four letter company (for charity). But I shall not regret it: I've got a lot of lessons learnt from that implementation, which are now all incorporated into this brand new implementation.
-
An MDI framework
... Frankly, this part is more experimental than anything else ...
A Heaven Interface Definition is declarative approach for creating Qt Widget based menu and action classes.
A simple HID might look like:
Ui MainWindowActions {
Action Quit {
Text "&Quit";
_ConnectTo onQuit();
};
Menu MainFile {
Text "&File";
Action Quit;
};
MenuBar MainBar {
Menu MainFile;
};
};
It will:
- Create a user interface named
MainWindowActions
. - Inside this user interface it will create:
- the action
Quit
- the menu
MainFile
containing the actionQuit
- the menu bar
MainBar
containing the menuMainFile
- the action
- The
Quit
action'striggered()
signal will be connected to the owner'sonQuit()
slot.
In the above example, Ui
, Action
, Menu
and MenuBar
are keywords. Blocks
are enclosed in curly braces and terminated with a semicolon.
Each keyword denotes either the creation of a new object or a reference to an existing object, depending soley on the existance of that object. So, what the above example does, is:
- Create a Ui object named MainWindowActions.
- Create an Action object named Quit owned by the MainWindowActions Ui object.
- Set the property Text to "&Quit" => Puting a string value inside quotes means that we're dealing with a translatable text.
- Set the property _ConnectTo to onQuit(). => Omitting the quotes means that the text won't be translated.
- Create a Menu object named MainFile owned by the MainWindowActions Ui object.
- Set the property Text to "&File"
- Add a reference to the Quit object.
- Create a MenuBar object named MainBar owned by the MainWindowActions Ui object.
- Add a reference to the MainFile object.
Any created object is owned by the Ui object surrounding it. Therefore, the above example could as well be rewritten as:
Ui MainWindowActions {
MenuBar MainBar {
Menu MainFile {
Text "&File";
Action Quit {
Text "&Quit";
_ConnectTo onQuit();
};
};
};
};
Which is literally the same. However, it is a good practice NOT to nest object creation.
As mentioned initially, a HID file has to be compiled into (further compilable) C++ code. This mechanism is very similar to Qt's form generation.
Passing the above snippet through the interface compiler (hic) will produce a class looking like this:
class MainWindowActions {
public:
Heaven::Action* actQuit;
Heaven::Menu* menuMainFile;
Heaven::MenuBar* mbMainBar;
void setupActions( QObject* parent );
};
Invoking setupActions()
will fill all the members of that class with valid
object pointers. The created objects all inherit QObject
and are reparented
to the given parent
. That means they will be destroyed automatically with
their parent.
This class can be used in the same ways as a uic
generated form is used.
The most simple solution is to use multiple inheritance, second best would
be aggregation. Here we will use multiple inheritance.
class MainWindow : public QMainWindow, private MainWindowActions {
public:
MainWindow();
};
MainWindow::MainWindow() {
setupActions( this );
setMenuBar( mbMainBar->menuBarFor( this ) );
}
In most desktop environemnts, the menu bar usually belongs to the main window of application. Note that none of the objects stored inside the generated class are plain Qt types. They are rather Heaven specific types.
In Heaven any of these interaction objects can create as many GUI objects as is necessary. Heaven will synchronize them as is required.
If you wanted to create a second window using the same menu bar, you could simply write:
void MainWindow::createSecondaryWindow()
{
QWidget* w = new MySecondaryWindow();
w->setMenuBar( mbMainBar->menuBarFor( w ) );
}
Here, Heaven will create a QMenuBar
object that is suitable to be used with the
newly created QWidget
.
While this might not be very intersting for menu bars, it becomes intersting as soon as you think for: submenus which might appear as part of a menu in the menu bar and at the same time might appear as part of a context menu. Heaven will handle this behind the scenes for you.
Most of the above is straight forward - Just another way to express the creation of interaction objects. Now, let's see what Heaven can do for us to allow for more modularity of our code.
Let's create a file menu:
Ui MainWindowActions {
/* Define our actions */
Action FileNew { Text "&New"; _ConnectTo onFileNew(); };
Action FileOpen { Text "&Open"; _ConnectTo onFileOpen(); };
Action FileQuit { Text "&Quit"; _ConnectTo onFileQuit(); };
/* Define the File->Export menu */
Menu FileExport {
Text "&Export;
MergePlace FileExportPlace;
};
/* Define the File menu */
Menu MainFile {
Text "&File";
Action FileNew;
Action FileOpen;
Separator;
Menu FileExport;
Separator;
Action FileQuit;
};
// Define our menu bar
MenuBar MainBar {
Menu MainFile;
};
};
Compared to the previous example, this one contains 4 new elements:
-
HID files can be amended by using C++ style comments. Either
/* multi line */
or single line comments:// like this
. -
The Separator keyword inserts a horizontal line into the menu. Strictly speaking, this is just another (unnamed) object, which is implicitly generated and referenced here.
-
In the menu object
MainFile
we refer to another menu object. This will simply create a submenu. -
The menu object
FileExport
uses the keyword MergePlace with an object name assigned. This actually creates a so called merge place object. We don't need to set any properties on that object, because merge place objects don't have any. Thus, we don't need to open up a block for it (Like we used for i.e. action objects).
Usually, Heaven tries to be smart and most of the time succeeds in trying to be. If we were to show the menu bar from the above example, Heaven will determine that the FileExport menu has no content and will implicitly remove it from the MainFile menu object where we reference it. It will further see that there are 2 Separtors in a row, meaning: Of of them will implicitly removed.
Now, let's assume we want to create a PDF Export plugin to that application. The usual approach would be
- to determine the correct position in the file menu.
- see if another plugin has already created an "export menu" - and if not, create one.
- now that we can be sure there is a export menu, append our "PDF" action to that menu
We would repeat that for whatever other places like context-menus we wanted to show the action... This is much dumb code.
The Heaven approach is a bit different: We simply create another HID:
Ui FileExportPdfPluginActions {
Action FileExportPDF {
Text "&PDF...";
};
Container MergeToMainMenu {
Action FileExportPDF;
};
};
Here, we have yet another new keyword Container, which creates objects of type "Container".
A container object is simply an aggregation of object references without any special meaning or appearance. But we can use those containers from our C++ code:
class FileExportPdfPlugin : public Plugin, private FileExportPdfPluginActions {
public:
FileExportPdfPlugin();
};
FileExportPdfPlugin::FileExportPdfPlugin() {
setupActions( this );
acMergeToMainMenu->mergeInto( "FileExportPlace" );
};
Most of this code is the same as we wrote for the MainWindow. The only difference here is that we use the container and tell it where it belongs.
Heaven will take the contents of the MergeToMainMenu container and substitute all occurences of
merge place objects named FileExportPlace
with the container's content.
Now, our menu "FileExport" can no longer be considered empty. It will be added where it is referenced. In our example that would be the "MainFile" menu object. Following that, the two separator object references are no longer side by side. Heaven will now place one of that separators before and one after the "FileExport" menu.