Eagle Mode
About
Videos
Screenshots
Documentation
Change Log
License
System Requirements
Install and Start
General User Guide
emFileMan User Guide
emFileMan Customization
Advanced Configuration
C++ API Tutorial
C++ API Reference
Make System
Download
Project Philosophy
Future Plans
Forum
Contact
Hosted at:
Get Eagle Mode at SourceForge.net. Fast, secure and Free Open Source software downloads

 

 

Copyright © 2008-2024 Oliver Hamann. All rights reserved.

All trademarks are the property of their respective owners.

Eagle Mode -
C++ API Tutorial

Copyright © 2010-2011,2014-2016,2022 Oliver Hamann. Homepage: http://eaglemode.sourceforge.net/

Contents

1 Introduction
2 Standalone Hello World example
3 Exceptions, error handling and logging
4 Painting graphics and text
5 Keyboard and mouse input
6 Simple animation through an engine
7 Signals and timers
8 Panel tree expansion
9 The toolkit panels
10 Models and contexts
11 Plugin interface

1 Introduction

The Eagle Mode C++ API can be used to create both, plugin applications and standalone applications. A plugin application could show in the virtual cosmos and/or in the file manager, while a standalone application shows as an own window on the desktop.

This tutorial mainly consists of programming examples which can also be seen as design patterns for the respective topics. Originally, most of the examples are standalone applications because this makes it easier to compile and run them. Only the very last chapter brings a complete plugin example in order to explain the plugin interface. The examples can be found in the directory $EM_DIR/doc/examples/CppApiExamples (replace $EM_DIR by the Eagle Mode directory). There is also a Perl script that makes it very easy to run the standalone examples. It compiles automatically and executes a temporary binary. Here are the commands to run the Hello World example:

cd $EM_DIR/doc/examples/CppApiExamples
perl run-example.pl HelloWorldExample.cpp

The examples can also be found inline in this document. But except for the first and the last example, the beginnings and ends of the example sources are left out, because those fragments are almost always the same.

In order to make first programming experiments as easy as possible, this tutorial only covers the most important topics without explaining all the API classes and functions in detail. The examples should give you a good intuition of how the API works. For more details, you may have a look at the Reference Guide. That is an index to HTML-converted header files which are containing extensive comments as the reference documentation, sometimes also with tiny examples and short tutorials. If you find something is not described well enough, please do not hesitate to write to the API Support Forum.

The real header files are in $EM_DIR/include/emCore and the library to be linked is $EM_DIR/lib/libemCore.so or on Windows $EM_DIR/lib/emCore.lib. Thus, the API is made of a sub-project named emCore.

The writing of "makefiles" is described in another document: Make System.

Before continuing, you should have read the General User Guide in order to know what is meant with the terms Window, View, Panel, Focus and so on.

2 Standalone Hello World example

Let us start with a Hello World example. Below is the source code of a small standalone application which shows a window. Therein, the user can zoom and scroll a single black panel with a "Hello World" inscription. Panels are in the Eagle Mode API what widgets are in classic APIs. It is just that they are zoomable.

The base class for panels is emPanel. In the example source below, you can find the class MyPanel derived from emPanel. It overloads the constructor and two methods: GetTitle and Paint. The result of GetTitle is a character string which is shown in the title bar of the window when the panel is active or focused. The example has only one panel and therefore that panel is always the active one. The Paint method is overloaded in order to fill the background of the panel with black and to write "Hello World!" in red letters on it. More about painting is described in a later chapter.

The example source also defines the main function of the program. It does this through the macro MAIN_OR_WINMAIN_HERE. That macro expands to an operating system dependent main function (WinMain on Windows, main elsewhere), which simply forwards to wrapped_main. Therein, the example first calls emInitLocale() in order to enable UTF-8 encoding if possible (the declaration of that function is here). Then the example creates an emGUIFramework and prepares it to terminate the program automatically when it has no more windows. Afterwards the example creates an emWindow and prepares it to delete itself when the close button is clicked. Finally an instance of the example panel class MyPanel is created in that window. The Layout call just has effect on the height/width ratio of the panel, because it is a root panel (more about Layout in a later chapter). At the end, the main function calls the Run method of emGUIFramework in order to serve the user interface until termination.

What you should learn from the example is how to bring up a panel in a standalone application, because many further examples just show how to program panels.

HelloWorldExample.cpp
#include <emCore/emGUIFramework.h>
#include <emCore/emPanel.h>


class MyPanel : public emPanel {
public:
        MyPanel(ParentArg parent, const emString & name);
        virtual emString GetTitle() const;
protected:
        virtual void Paint(const emPainter & painter, emColor canvasColor) const;
};

MyPanel::MyPanel(ParentArg parent, const emString & name)
        : emPanel(parent,name)
{
}

emString MyPanel::GetTitle() const
{
        return "Hello World Example";
}

void MyPanel::Paint(const emPainter & painter, emColor canvasColor) const
{
        painter.Clear(emColor::BLACK);
        painter.PaintTextBoxed(0,0,1,GetHeight(),"Hello World!",.01,emColor::RED);
}


MAIN_OR_WINMAIN_HERE

static int wrapped_main(int argc, char * argv[])
{
        emInitLocale();

        emGUIFramework framework;
        framework.EnableAutoTermination();

        emWindow * window=new emWindow(framework.GetRootContext());
        window->SetWindowFlags(emWindow::WF_AUTO_DELETE);

        emPanel * panel=new MyPanel(window,"root");
        panel->Layout(0.0,0.0,4.0,3.0);

        return framework.Run();
}

3 Exceptions, error handling and logging

Before continuing with more fun, some words about error handling:

Every method and function of the API which could fail, fails by throwing an exception. The exception is always an emException which contains a human-readable error message. In addition, the name of the method or function contains the word "Try" at the beginning, in order to make clear that it could fail and throw an exception. Examples are: emTryOpenLib, emColor::TryParse.

If you can't continue the program after a failure, then you may call emFatalError(format, ...) in order to report the error and exit the program. Its arguments are like with printf. For more about this function and about logging, please read this section.

4 Painting graphics and text

The Hello World example already did a little bit of painting. This chapter brings a more complex paint example, but first some explanations:

In order to paint a panel, you have to overload the Paint method of emPanel. It is called automatically whenever the panel has to be painted again, for example after a change of the visual transformation. In addition, you can call the InvalidatePainting() method of emPanel, if you want Paint to be called again for whatever reason.

The Paint method has two arguments: a painter and a canvas color. The painter (class emPainter) has lots of methods for painting with transformation and clipping. It is prepared for painting in the coordinate system of the panel. This means, the origin is in the upper-left corner of the panel, the X-axis points right, and the Y-axis points down. The unit is panel widths, which means that a panel has always a width of 1.0 in its own coordinate system.

The canvas color (class emColor) is the color of the background, or it is any non-opaque color if the background is unknown or not plain. The idea is to give the canvas color to the paint method calls in emPainter (as the equal-named argument). In many cases, the paint methods are faster and more exact if they get a known (opaque) canvas color. In the example below you can see that the first paint operations fills the background with white color by calling the Clear method. Therefore the canvas color is changed to white one line after in order to use it in the other calls. The last two paint operations do not get the canvas color, because they overlap with other objects.

Whenever you overload the Paint method, you should also consider to overload the IsOpaque method. It tells whether the panel altogether paints opaque or not. If yes, the parent panel is automatically not painted in certain situations.

PaintExample.cpp (essential part)

class MyPanel : public emPanel {
public:
        MyPanel(ParentArg parent, const emString & name);
protected:
        virtual bool IsOpaque() const;
        virtual void Paint(const emPainter & painter, emColor canvasColor) const;
private:
        emImage EagleIcon;
};

MyPanel::MyPanel(ParentArg parent, const emString & name)
        : emPanel(parent,name)
{
        EagleIcon=emGetInsResImage(GetRootContext(),"icons","eaglemode.tga");
}

bool MyPanel::IsOpaque() const
{
        return true;
}

void MyPanel::Paint(const emPainter & painter, emColor canvasColor) const
{
        painter.Clear(emColor::WHITE,canvasColor);
        canvasColor=emColor::WHITE;

        painter.PaintImage(0.05,0.05,0.3,0.3,EagleIcon,255,canvasColor);

        painter.PaintRect(0.4,0.1,0.2,0.2,emColor::GREEN,canvasColor);

        painter.PaintRectOutline(0.69,0.09,0.22,0.22,0.01,0x0080C0FF,canvasColor);
        painter.PaintTextBoxed(
                0.7,0.1,0.2,0.2,
                "Centered text\nin\nthe bottom-right\nof a box",
                0.017,0x0080C0FF,canvasColor,
                EM_ALIGN_BOTTOM_RIGHT,EM_ALIGN_CENTER
        );

        static const double poly1[]={
                0.1, 0.4,
                0.05, 0.7,
                0.3, 0.5
        };
        painter.PaintPolygon(poly1,3,emColor(255,128,0),canvasColor);

        painter.PaintLine(
                0.3,0.4,0.5,0.7,0.05,
                emRoundedStroke(emColor(255,0,0,128)),
                emStrokeEnd::CAP,emStrokeEnd::CAP,
                canvasColor
        );

        painter.PaintEllipse(0.5,0.4,0.3,0.3,0x33CC88FF,canvasColor);
        painter.PaintEllipse(0.6,0.4,0.3,0.3,0xFF55AAFF);
        painter.PaintEllipse(0.55,0.35,0.3,0.3,0xFFFF3388);
}

5 Keyboard and mouse input

If you want to process keyboard and mouse input, then you have to overload the Input method of emPanel. It is called whenever there is a keyboard or mouse event like a button press, or when there is a change of any key or button state, or when the mouse is moved. Therefore the Input method has two major arguments: an emInputEvent and an emInputState. Besides, it also gets the current mouse position (mx, my) in the coordinate system of the panel, because emInputState provides the mouse position only in pixel coordinates.

Keyboard events are given to the panel which has the keyboard focus, and mouse events are given to the panel in which the mouse pointer is. In addition, the events may also be given to ancestors of those panels (e.g. a parent panel or the window). To avoid that, a panel can eat an event by calling the Eat method of emInputEvent. That is the reason why the event argument is non-const.

When you overload the Input method, you should not forget to call the base class implementation. emPanel itself eats certain keyboard and mouse events in order to change the focus. Therefore you should not eat those events before calling the base class, but you may use them to do additional things.

The example below demonstrates how to process various input events by overloading the Input method. Note that there are no events for the release of a button or key. So if you want to get noticed about a release, you have to track the input state. This is also demonstrated in the example.

In addition, you should notice that emInputEvent describes the event in two ways: the key (which could also be a mouse button), and a character string that the key produces. Normally you should ask for the key, but sometimes you may want to ask for a character which has no key on the keyboard. An example would be the dollar sign which is produced by Shift+4 on English keyboards.

InputExample.cpp (essential part)

class MyPanel : public emPanel {
public:
        MyPanel(ParentArg parent, const emString & name);
protected:
        virtual void Input(emInputEvent & event, const emInputState & state,
                           double mx, double my);
private:
        bool XKeyDown;
        bool ButtonDown;
        double LastMX, LastMY;
};

MyPanel::MyPanel(ParentArg parent, const emString & name)
        : emPanel(parent,name)
{
        XKeyDown=false;
        ButtonDown=false;
}

void MyPanel::Input(
        emInputEvent & event, const emInputState & state, double mx, double my
)
{
        if (event.IsKey(EM_KEY_E) && state.IsNoMod()) {
                emLog("The E key was pressed without any modifier keys.");
                event.Eat();
        }

        if (event.IsKey(EM_KEY_G) && state.IsShiftAltMod()) {
                emLog("The Shift+Alt+G key combination was pressed.");
                event.Eat();
        }

        emInputHotkey hotkey(EM_KEY_CTRL, EM_KEY_V);
        if (hotkey.Match(event,state)) {
                emLog("The %s key combination was pressed.", hotkey.GetString().Get());
                event.Eat();
        }

        if (event.GetChars()=="$") {
                emLog("A key combination that prints the dollar sign was pressed.");
                event.Eat();
        }

        if (event.IsKey(EM_KEY_X)) {
                emLog("The X key was pressed.");
                XKeyDown=true;
                event.Eat();
        }
        if (XKeyDown && !state.Get(EM_KEY_X)) {
                emLog("The X key was released.");
                XKeyDown=false;
        }

        if (event.IsKey(EM_KEY_LEFT_BUTTON)) {
                emLog("The left mouse button was pressed at (%g, %g).", mx, my);
                ButtonDown=true;
                LastMX=mx;
                LastMY=my;
                // We do not eat the event here so that emPanel::Input still
                // sets the focus on this panel by the button event. Otherwise
                // we should call Focus().
        }
        if (ButtonDown && (LastMX!=mx || LastMY!=mx)) {
                emLog("The mouse was dragged to (%g, %g).", mx, my);
                LastMX=mx;
                LastMY=my;
        }
        if (ButtonDown && !state.Get(EM_KEY_LEFT_BUTTON)) {
                emLog("The left mouse button was released.");
                ButtonDown=false;
        }

        // Call the base class. It changes the focus by certain input events.
        emPanel::Input(event,state,mx,my);
}

6 Simple animation through an engine

The API provides three concepts or classes which solve the problem of sharing the CPU: emThread, emProcess and emEngine. As you might expect from the names, the first two are just about classic threads and processes. But the third one is somehow usual, and it is used very widely in Eagle Mode, as it is the super class of all high-level classes. Therefore, this and the next chapter are about emEngine.

All high-level classes are subclasses of emEngine.

emEngine has two possible uses: polling and signal exchange. Of course, polling is quite despised because it wastes CPU cycles, but signal exchange requires a little bit more programming effort. With emEngine, it is possible to quickly program a first solution with polling, and to improve that solution later by conversion into signal exchange, without any expensive restructuring.

In order to perform polling, you just need to do three things:

  • Overload the method Cycle() of emEngine and implement the polling code in it. Do not forget to call the base class implementation too, if you do not know it is empty.
  • Wake up the engine once at the beginning by calling WakeUp(), e.g. in the constructor.
  • Always return true form Cycle.
This way, Cycle is called up to about one hundred times per second, depending on the CPU load.

The following example code demonstrates the overloading of Cycle in an emPanel, in order to show a simple animation in form of a decimal counter which increases on every cycle.

SimpleAnimationExample.cpp (essential part)

class MyPanel : public emPanel {
public:
        MyPanel(ParentArg parent, const emString & name);
protected:
        virtual bool Cycle();
        virtual void Paint(const emPainter & painter, emColor canvasColor) const;
private:
        unsigned Counter;
};

MyPanel::MyPanel(ParentArg parent, const emString & name)
        : emPanel(parent,name)
{
        Counter=0;
        WakeUp();
}

bool MyPanel::Cycle()
{
        emPanel::Cycle();
        Counter++;
        InvalidatePainting();
        return true;
}

void MyPanel::Paint(const emPainter & painter, emColor canvasColor) const
{
        emString str=emString::Format("%u",Counter);
        painter.PaintTextBoxed(0,0,1,GetHeight(),str,.41,0xFFFF80FF);
}

7 Signals and timers

As already said, the better use of emEngine is to exchange signals. The idea is to wake up an engine by a signal whenever there is something useful to do for it. Moreover, it is possible to find out which signal has woken up an engine, and thereby an engine can receive any number of different signals in order to do different things. A signal can be received by multiple engines, and the sender does not need to know anything about the receivers.

Here is how you can send a signal:

  • Create an object of class emSignal at the beginning and make sure receivers can get a const-reference or -pointer to it.
  • Call the method Signal(signal) in any engine, with your signal object as the argument, whenever you actually want to send the signal.
And here is how you can receive a signal in an engine:
  • Make sure the engine is woken up whenever the signal is sent. This can be done by calling AddWakeUpSignal(signal) on the engine, e.g. in the constructor.
  • Overload the Cycle method of emEngine, in order to ask for the signal by calling IsSignaled(signal). Also, do not forget to call the base class implementation of Cycle too, if you do not know it is empty.
  • Return false form Cycle, except the base class implementation returned true. (false means that the engine is not woken up regularly on next time slice, i.e. that you don't want polling.)
The following programming example demonstrates all that. It has the class MyButton, which is a panel that sends a signal whenever the user clicks with the mouse on it. And it has the class MyPanel which receives that signal in order to increase a shown counter on every button click. In addition, MyPanel also demonstrates the use of the class emTimer with another counter. emTimer can generate periodic and non-periodic timer signals.

Besides, this is the first example with more than one panel: MyPanel creates and lays out a MyButton as its child panel.

SignalExample.cpp (essential part)

//================================== MyButton ==================================

class MyButton : public emPanel {
public:
        MyButton(ParentArg parent, const emString & name);
        const emSignal & GetClickSignal() const;
protected:
        virtual void Input(emInputEvent & event, const emInputState & state,
                           double mx, double my);
        virtual void Paint(const emPainter & painter, emColor canvasColor) const;
private:
        emSignal ClickSignal;
};

MyButton::MyButton(ParentArg parent, const emString & name)
        : emPanel(parent,name)
{
}

const emSignal & MyButton::GetClickSignal() const
{
        return ClickSignal;
}

void MyButton::Input(
        emInputEvent & event, const emInputState & state, double mx, double my
)
{
        if (event.IsLeftButton()) Signal(ClickSignal);
        emPanel::Input(event,state,mx,my);
}

void MyButton::Paint(const emPainter & painter, emColor canvasColor) const
{
        painter.Clear(0xC0C0C0FF,canvasColor);
        painter.PaintTextBoxed(0,0,1,GetHeight(),"Click Me",1,emColor::GREEN);
}


//================================== MyPanel ===================================

class MyPanel : public emPanel {
public:
        MyPanel(ParentArg parent, const emString & name);
protected:
        virtual bool Cycle();
        virtual void Paint(const emPainter & painter, emColor canvasColor) const;
private:
        unsigned Counter1, Counter2;
        MyButton * Button;
        emTimer Timer;
};

MyPanel::MyPanel(ParentArg parent, const emString & name)
        : emPanel(parent,name),
        Timer(GetScheduler())
{
        Counter1=0;
        Counter2=0;
        Button=new MyButton(this,"button");
        Button->Layout(0.1,0.1,0.8,0.1);
        Timer.Start(1000,true);
        AddWakeUpSignal(Button->GetClickSignal());
        AddWakeUpSignal(Timer.GetSignal());
}

bool MyPanel::Cycle()
{
        if (IsSignaled(Button->GetClickSignal())) {
                Counter1++;
                InvalidatePainting();
        }
        if (IsSignaled(Timer.GetSignal())) {
                Counter2++;
                InvalidatePainting();
        }
        return emPanel::Cycle();
}

void MyPanel::Paint(const emPainter & painter, emColor canvasColor) const
{
        emString str=emString::Format(
                "Button Signals: %u\nTimer Signals: %u",Counter1,Counter2
        );
        painter.PaintTextBoxed(0,0.3,1,GetHeight()-0.3,str,.1,0xFFFF80FF);
}

8 Panel tree expansion

A real application may have much more than one or two panels. A panel can have child panels, which also can have child panels and so on. After all, you could have a large tree of panels with extreme depth, or even with infinite depth - virtually.

In order to identify the children of a panel, each panel has a name which must be unique across its sisters. The name can be any character string and is usually given as the second argument to the constructor (first argument is the parent). The names of the panels on a path in the tree are making up the panel identification within the window or view. Mainly, these identifications are used by the bookmarks implementation of Eagle Mode.

Panels should usually be created with the new operator, but you do not need to care about the deletion, because the parent destructor does that for you.

An important point is that the panels of a large tree should be created only when they are needed. Otherwise your application may waste to much memory and CPU time if it shows thousands or even millions of panels. Goal is to create and destroy the panels dynamically so that there are not much more than those panels which are actually visible or which have visible descendants, and which are not too small on screen. There are two ways to solve that: manually and automatically.

The manual way would mean that you have to program all the dynamics yourself, but it gives you a little bit more freedom. If you ever want to go that way, you could start by reading the comments on the methods GetViewCondition and Notice of emPanel. But for now, lets continue with the easier way:

The automatic way of panel tree expansion and -shrinking is really easy. All you have to do is to overload the method AutoExpand of emPanel in order to create all your child panels in it with the new operator. There is also the counterpart method AutoShrink, but it is seldom to be overloaded, because its default implementation deletes exactly all the child panels which are created in AutoExpand. You suspect it: AutoExpand and AutoShrink are called automatically when it is a good idea to create or destroy the child panels depending on the zoom and scroll state of the view.

Another important point is to layout your child panels, which means to position and size them within the coordinate system of the parent panel. You can do this by calling Layout(x,y,w,h,canvasColor) on the child panel. In addition to the position and size, that method also gets the canvas color for the whole child panel. You should know about the canvas color since reading the paint chapter, as well as about the own coordinate system of a panel.

After a panel has been constructed, it is laid out outside the parent panel by default. This means the child panel is not shown by default, because child panels are always clipped by the rectangle of the parent. So you really have to call the Layout method if you want your child panel to be shown. One possibility is to do that directly after creating the panel, maybe in you implementation of AutoExpand. This is absolutely okay. But sometimes you may want to program a more dynamic layout which depends on the layout of the parent, say its height/width-ratio. Then it is a good idea to implement your Layout calls in an overloaded version of the method LayoutChildren of emPanel. That method is called automatically after new child panels have been created, and after the parent layout has changed.

One last word on the Layout method: The root panel only needs the height/width-ratio, and therefore it ignores everything else from the Layout call.

The following programming example demonstrates the overloading of AutoExpand and LayoutChildren. It simply shows a (virtually) infinite tree of black and white panels, where each panel has four children. Remember that the children are deleted automatically by the default implementation of AutoShrink. Therefore, the example uses the smart pointer class emCrossPtr to refer to the child panels. Such a pointer sets itself automatically to NULL when the object is deleted. And so we can safely test for existence of the child panels in the implementation of LayoutChildren. Another way would be to keep no pointers at all and to ask for the child panels by name whenever needed, using the GetChild method of emPanel. But that would be a little bit slower.

TreeExpansionExample.cpp (essential part)

class MyPanel : public emPanel {
public:
        MyPanel(ParentArg parent, const emString & name, emColor bgcolor=0xFFFFFFFF);
protected:
        virtual bool IsOpaque() const;
        virtual void Paint(const emPainter & painter, emColor canvasColor) const;
        virtual void AutoExpand();
        virtual void LayoutChildren();
private:
        emColor BackgroundColor;
        emCrossPtr<MyPanel> Child[4];
};

MyPanel::MyPanel(ParentArg parent, const emString & name, emColor bgcolor)
        : emPanel(parent,name)
{
        BackgroundColor=bgcolor;
}

bool MyPanel::IsOpaque() const
{
        return BackgroundColor.IsOpaque();
}

void MyPanel::Paint(const emPainter & painter, emColor canvasColor) const
{
        painter.Clear(BackgroundColor,canvasColor);
}

void MyPanel::AutoExpand()
{
        for (int i=0; i<4; i++) {
                Child[i]=new MyPanel(
                        this,
                        emString::Format("%d",i),
                        BackgroundColor.Get()^0xFFFFFF00
                );
        }
}

void MyPanel::LayoutChildren()
{
        for (int i=0; i<4; i++) {
                if (Child[i]) {
                        Child[i]->Layout(
                                0.1+(i&1)*0.5,
                                (0.1+(i&2)*0.25)*GetHeight(),
                                0.3,
                                0.3*GetHeight(),
                                BackgroundColor
                        );
                }
        }
}

9 The toolkit panels

There is a set of toolkit classes for frequently needed user interface components like buttons, text fields and so on. Most of them are panel classes, but not all. Here is a class hierarchy diagram of all the toolkit classes:

Class chart of the toolkit classes.

As you see, the class emBorder is the super class of all the other toolkit panels. It has lots of common properties: the look, the border, the label (caption + description + icon), the how-to text, and the auxiliary area. The look is expressed as an instance of emLook, which allows to configure the colors. There is a simple derivative of emBorder which shows the label as the content instead of in the border: emLabel.

emLinearLayout, emRasterLayout, and emPackLayout act as container panels which are able to layout their child panels automatically by different algorithms. They are borderless and unfocusable by default and so they are meant for solving layout problems only. But the derivatives emLinearGroup, emRasterGroup, and emPackGroup are focusable and show a group border, so that they can easily be used for grouping things visually together. A complete different container is emTunnel which just shows a "tunnel" with one custom child panel at the end. And another one is emSplitter which allows to have two custom child panels separated by a draggable bar, either horizontally or vertically.

The classes emButton, emCheckButton, emRadioButton, emCheckBox, and emRadioBox may not need much explanation. A button receives mouse clicks to trigger something, a check button receives mouse clicks to switch something on or off, and a radio button is similar but the idea is to have always exactly one radio button on in a group. Boxes are like buttons, but they look different.

Finally there are panel classes for data fields. An emTextField can be used for viewing or editing a plain text, either single- or multi-line. An emScalarField can be used for viewing or editing a scalar value or number, or a one-of-many choice with individual texts. emColorField is for viewing or editing colors.

emDialog is not a panel but an emWindow. You can use it to create a typical popup dialog with OK and Cancel buttons, or with custom buttons. It also provides a function for creating a simple popup dialog which just shows a message. Example:

emDialog::ShowMessage(GetViewContext(),"Message","Hello World!");
Note that each window requires a parent context (contexts are explained in a later chapter). That could also be a window, or a view of a window, or the root context. In the example above we set the parent context from GetViewContext(), which is a method of emPanel.

The following programming example demonstrates the use of some toolkit panels. Best is to have it running while trying to understand the code. (Please refer to the first chapter if you do not yet know how to run the examples.)

ToolkitExample.cpp (essential part)

class MyPanel : public emRasterGroup {
public:
        MyPanel(ParentArg parent, const emString & name);
protected:
        virtual bool Cycle();
private:
        static void ScalarField2_TextOfValue(
                char * buf, int bufSize, emInt64 value, emUInt64 markInterval,
                void * context
        );
        emTextField * Message;
        emButton * Button;
        emCheckButton * CheckButton;
        emRadioButton::RasterGroup * RadioGroup;
        emTextField * TextField;
        emScalarField * ScalarField1;
        emScalarField * ScalarField2;
};

MyPanel::MyPanel(ParentArg parent, const emString & name)
        : emRasterGroup(parent,name,"Toolkit Example")
{
        Message=new emTextField(this,"msg","Message");
        Message->SetDescription("This text field shows messages about your input.");

        Button=new emButton(this,"b","Button");
        AddWakeUpSignal(Button->GetClickSignal());

        CheckButton=new emCheckButton(this,"cb","Check Button");
        AddWakeUpSignal(CheckButton->GetCheckSignal());

        RadioGroup=new emRadioButton::RasterGroup(this,"rg","Radio Group");
        RadioGroup->SetBorderScaling(4.0);
        new emRadioBox(RadioGroup,"rb1","Radio Box 1");
        new emRadioBox(RadioGroup,"rb2","Radio Box 2");
        new emRadioBox(RadioGroup,"rb3","Radio Box 3");
        AddWakeUpSignal(RadioGroup->GetCheckSignal());

        TextField=new emTextField(this,"tf","Text Field");
        TextField->SetEditable();
        TextField->SetMultiLineMode();
        AddWakeUpSignal(TextField->GetTextSignal());

        ScalarField1=new emScalarField(this,"sf1","Scalar Field 1");
        ScalarField1->SetEditable();
        ScalarField1->SetMinMaxValues(0,100);
        ScalarField1->SetScaleMarkIntervals(50,10,5,1,0);
        AddWakeUpSignal(ScalarField1->GetValueSignal());

        ScalarField2=new emScalarField(this,"sf2","Scalar Field 2");
        ScalarField2->SetEditable();
        ScalarField2->SetMinMaxValues(-1,1);
        ScalarField2->SetTextOfValueFunc(ScalarField2_TextOfValue);
        AddWakeUpSignal(ScalarField2->GetValueSignal());
}

bool MyPanel::Cycle()
{
        if (IsSignaled(Button->GetClickSignal())) {
                Message->SetText("Button clicked");
        }
        if (IsSignaled(CheckButton->GetCheckSignal())) {
                Message->SetText(emString::Format(
                        "Check Button switched %s",
                        CheckButton->IsChecked() ? "on" : "off"
                ));
        }
        if (IsSignaled(RadioGroup->GetCheckSignal())) {
                Message->SetText(emString::Format(
                        "Radio Box %d set",
                        RadioGroup->GetCheckIndex() + 1
                ));
        }
        if (IsSignaled(TextField->GetTextSignal())) {
                Message->SetText(emString::Format(
                        "Text Field changed to \"%s\"",
                        TextField->GetText().Get()
                ));
        }
        if (IsSignaled(ScalarField1->GetValueSignal())) {
                Message->SetText(emString::Format(
                        "Scalar Field 1 changed to %d",
                        (int)ScalarField1->GetValue()
                ));
        }
        if (IsSignaled(ScalarField2->GetValueSignal())) {
                Message->SetText(emString::Format(
                        "Scalar Field 2 changed to %d",
                        (int)ScalarField2->GetValue()
                ));
        }
        return emRasterGroup::Cycle();
}

void MyPanel::ScalarField2_TextOfValue(
        char * buf, int bufSize, emInt64 value, emUInt64 markInterval, void * context
)
{
        if (value<0) strcpy(buf,"left");
        else if (value==0) strcpy(buf,"middle");
        else strcpy(buf,"right");
}

10 Models and contexts

Now after telling you so much about programming the front end of an application with emPanel and its derivatives, it is time to introduce the concepts for programming the things that work behind the scenes.

Usually, an application manages some data which is shown or edited through the panels. There are some reasons why it could be a bad idea to hold the data directly in the panels. The most important reason is that there could be multiple panels which show the same data. Remember that Eagle Mode supports to work with multiple windows, where every window shows the same virtual cosmos. This way, every panel of the virtual cosmos can exist multiple times, but they should not duplicate the data. Though, sometimes you may have some kind of data which shall be duplicated for every window, but which shall still be shared by multiple panels within a window (example: view settings like the directory sorting in the file manager).

Another reason to program the data outside the panels could be caching, because panels can be created and destroyed quite often through zooming and scrolling by the user. In addition, the data could lie outside the application, maybe in a database or on a remote computer. Then it is required to have an internal data interface which can be used by multiple panels. Finally, we need a good concept for binding the panels with their data at run-time, in order to keep the framework/plugin structure as simple as possible.

All those problems of sharing, caching, interfacing and late binding can be solved quite easily with the data class emModel, its container class emContext, and its referrer class emRef. That is what this chapter is about.

The idea is to program the data or data interface in a derivative of emModel. Either you could fill it with public member variables which make up the data, or you could define nice methods in order to create an abstract data type. You have all freedom there. But there is one important peculiarity of emModel: Constructors and destructors should always be kept protected. Instead of public constructors and destructors, a model class always has a public static method named Acquire, which is called if one wants to get a reference to a model. Acquire searches for an already existing model by an identification, and if does not find one, it creates and registers a new model.

The identification of a model consists of three things:

  • The final class of the model (not any base class!).
  • The context of the model (instance of emContext).
  • The name of the model (any character string).
In order to hand the whole identification to the Acquire call, the declaration of Acquire typically looks like in the following example:

  class MyModel : public emModel {
  public:
      static emRef<MyModel> Acquire(
          emContext & context, const emString & name, bool common=true
      );
      ...

The additional argument common allows to create a non-common model which is not shared. But that is needed very seldom, and therefore many derivatives do not have that argument. Some derivatives also suppress the name argument, and they may require a certain context class.

Because the implementations of Acquire are so similar, there are two macros in emModel which can help there: EM_IMPL_ACQUIRE and EM_IMPL_ACQUIRE_COMMON. Therewith a typical implementation looks like this:

  static emRef<MyModel> MyModel::Acquire(
      emContext & context, const emString & name, bool common
  )
  {
      EM_IMPL_ACQUIRE(MyModel,context,name,common)
  }

After you have programmed your data in a model and defined an Acquire method, you must decide in which emContext your model should live. If you want the model to be shared by all panels, it would have to be the instance of emRootContext. This is the context for things that could also be seen as "global variables", and it can be get by calling GetRootContext() in all high-level classes. Another possible context would be the view or the window in which your panel lives (emView and emWindow are derivatives of emContext). You can get them by calling GetView() or GetWindow() in emPanel.

Finally you may want your model to have a cache effect. Therefore you can simply call the method SetMinCommonLifetime(seconds) of emModel. It tells how long the model shall be hold by the context when there are no referrers. Normally a model is deleted as soon as the last reference (of class emRef) is destructed.

That is also the reason why you should normally not use any other pointer or reference type than emRef for models. But if you want a weak reference, you can use emCrossPtr.

The API also provides some useful derivatives of emModel. One is emFileModel, which is a powerful base class for file interfaces. Another is the template class emVarModel, which allows to acquire and use a one-variable model without programming a model class. Below is a diagram that shows the class hierarchies of emContext and emModel, and it completes the hierarchy of emPanel (when seeing it together with the chart from the toolkit chapter).

Class chart of contexts, models and panels.

Now to the programming example at the end of this chapter. When it is running, it shows a window with a group panel which has a button and a scalar field. The scalar field allows to view and edit an integer variable, which lies in a model in the root context. The button allows to create additional windows which show the same things. The important effect is that if the user sets the scalar field of one window to another value, the scalar fields of the other windows follow that change automatically so that all scalar field always show the same value.

The code of the example consists of two classes: a model class (MyModel) and a panel class (MyPanel). The model class simply contains an integer as the data. It defines a nice interface with a getter and setter, and with a signal for tracking changes. The panel refers to that model through an emRef<MyModel>, and it creates or finds the model in the root context by calling MyModel::Acquire(GetRootContext(),"test"). Quiz: What happens if you replace GetRootContext() by GetView() in that call? (Answer: each window gets its own MyModel and so the scalar field values are no longer the same).

Note that the example could be shortened by sparing the definition of MyModel and using emVarSigModel instead, but only as long as the data is just a single variable.

ModelExample.cpp (essential part)

//================================== MyModel ===================================

class MyModel : public emModel {
public:
        static emRef<MyModel> Acquire(
                emContext & context, const emString & name, bool common=true
        );
        int GetData() const;
        void SetData(int data);
        const emSignal & GetChangeSignal() const;
protected:
        MyModel(emContext & context, const emString & name);
private:
        int Data;
        emSignal ChangeSignal;
};

emRef<MyModel> MyModel::Acquire(
        emContext & context, const emString & name, bool common
)
{
        EM_IMPL_ACQUIRE(MyModel,context,name,common)
}

int MyModel::GetData() const
{
        return Data;
}

void MyModel::SetData(int data)
{
        if (Data!=data) {
                Data=data;
                Signal(ChangeSignal);
        }
}

const emSignal & MyModel::GetChangeSignal() const
{
        return ChangeSignal;
}

MyModel::MyModel(emContext & context, const emString & name)
        : emModel(context,name)
{
        Data=0;
}


//================================== MyPanel ===================================

class MyPanel : public emLinearGroup {
public:
        MyPanel(ParentArg parent, const emString & name);
protected:
        virtual bool Cycle();
private:
        emRef<MyModel> Model;
        emButton * BtNewWin;
        emScalarField * SField;
};

MyPanel::MyPanel(ParentArg parent, const emString & name)
        : emLinearGroup(parent,name,"Model Example")
{
        Model=MyModel::Acquire(GetRootContext(),"test");
        AddWakeUpSignal(Model->GetChangeSignal());

        BtNewWin=new emButton(this,"bnw","New Window");
        AddWakeUpSignal(BtNewWin->GetClickSignal());

        SField=new emScalarField(this,"sf","Data");
        SField->SetEditable();
        SField->SetValue(Model->GetData());
        AddWakeUpSignal(SField->GetValueSignal());

        SetSpace(0.2,0.8,0.2,0.8);
}

bool MyPanel::Cycle()
{
        if (IsSignaled(BtNewWin->GetClickSignal())) {
                emWindow * window=new emWindow(GetRootContext());
                window->SetWindowFlags(emWindow::WF_AUTO_DELETE);
                window->SetViewFlags(emView::VF_ROOT_SAME_TALLNESS);
                new MyPanel(window,"root");
        }
        if (IsSignaled(Model->GetChangeSignal())) {
                SField->SetValue(Model->GetData());
        }
        if (IsSignaled(SField->GetValueSignal())) {
                Model->SetData((int)SField->GetValue());
        }
        return emLinearGroup::Cycle();
}

11 Plugin interface

This chapter finally explains the plugin interface and brings a complete example for that. The name of the interface is emFpPlugin. That is a shortcut for "Eagle Mode File Panel Plugin", and it shall say that a plugin of this type creates a panel which acts as a user interface for viewing or editing a file. Each plugin is assigned to a file name ending, and so Eagle Mode knows which plugin to take for which file.

Thus, before programming an emFpPlugin, you have to thing about what type of file you want to interface. A picture viewer would interface picture files, and a video player would interface video files - that is easy. But often you may have to invent an individual file type for the plugin. For example, a game could interface a special save game file in order to make the game state persistent. However, if you do not need any file to be interfaced by the plugin, you would have to invent at least a unique file name ending and create an empty file with it.

The next step would be to program the plugin itself. It must be a shared link library in the $EM_DIR/lib directory, and it must provide a function of type emFpPluginFunc. That function is called by Eagle Mode when a new panel shall be created by the plugin. The function must have C linkage, not C++. Therefore the function has to be defined in an extern "C" block. The function must have a unique name and it must have exactly the arguments described in emFpPluginFunc. Please check that carefully, because the interface is not type-safe there.

Furthermore, a small configuration file must be written. It tells at least about the library and the file name ending of interfaced files. The configuration file format is described in the emFpPlugin chapter of the Advanced Configuration document. Please read there. Within the program, such a configuration is represented by an emFpPlugin.

Finally, if the plugin shall be shown in the virtual cosmos, a cosmos item must be written. This is described in the chapter about virtual cosmos configuration in the Advanced Configuration document.

Now to the plugin example:

The plugin example is a small vector drawing program, in which the user can draw just lines. The drawing is shown in the virtual cosmos, and the lines are drawn by dragging with the left mouse button. There is also a control panel with a little help text, a color field for setting the color of the next line to be drawn, and a button in a tunnel for clearing the drawing. The drawing is automatically stored in a file in order to keep all changes persistent. The plugin and the file format is given the name PlEx (shortcut for Plugin Example).

The source files of the plugin can be found in $EM_DIR/doc/examples/CppApiExamples/PluginExample, but they are also shown more below in this document. First, here comes an overview of all the files:

File Description
src/PlEx/PlEx.cpp C++ source file.
makers/PlEx.maker.pm Maker script that compiles and links the source.
etc/emCore/FpPlugins/PlEx.emFpPlugin Plugin configuration file.
etc/emMain/VcItems/PlEx1.emVcItem Virtual cosmos item configuration.
etc/emMain/VcItemFiles/PlEx1.PlEx Virtual cosmos item file.
etcw/emCore/FpPlugins/PlEx.emFpPlugin
etcw/emMain/VcItems/PlEx1.emVcItem
etcw/emMain/VcItemFiles/PlEx1.PlEx
These files are identical with the above in etc, because our example is 100% portable (etc is for UNIX/Linux, etcw is for Windows).

The Eagle Mode project is made so that plugin installation does not need to modify any existing files. It just requires to add files. So if you want to install the plugin example and play with it, simply copy the plugin source files with their relative paths into the directory tree of the Eagle Mode source package, and build, install, and run Eagle Mode as usual. You should find the drawing in the virtual cosmos then.

The rest of this chapter describes and shows all the plugin source files in detail. The description always comes before the file. First, we have the C++ source file. It has four sections and it uses some API elements not yet described. Therefore it requires some more words:

PlExFileModel

The first class in the C++ source file is PlExFileModel. It defines a file format for the drawing and provides an interface for loading, holding and saving the file. It is derived from the classes emRecFileModel and emStructRec by multiple inheritance. This requires a few explanations:

"Rec" is a shortcut for "recordable data type" or just "record". The header file emRec.h contains lots of classes which can be used to define data structures that can easily be saved and loaded to and from files (it could be seen as a replacement for XML). For details, please read the comments of the base class emRec first.

emStructRec is an emRec that stands for a structured data type (like a C struct). PlExFileModel is derived from it in order to give it two members: an emColorRec for the current color, and an emTArrayRec for the array of lines. And for the elements of that array, PlExFileModel defines the member class LineRec as another emStructRec.

emFileModel is an abstract emModel that can be used to interface files. It does not define the file format, but it provides everything else: loading states, automatic loading, parallelism, priorities, memory limits.

emRecFileModel is simply an emFileModel that uses an emRec as the file format definition. One just has to give it the emRec by calling its method PostConstruct(rec), and the file model is complete. PlExFileModel gives itself by calling PostConstruct(*this) because - you remember - it is also a derivative of emStructRec.

PlExControlPanel

The class PlExControlPanel is for the control panel. It uses the toolkit classes to show a help text, a color field, and a clear button. It connects the color field and the clear button to an PlExFileModel through listening to signals of both ends and reacting accordingly.

PlExFilePanel

A drawing itself is shown and edited through an instance of PlExFilePanel. That class uses many concepts already explained in this tutorial, but there is also something new:

PlExFilePanel is derived from emFilePanel, because the counterpart, PlExFileModel, is indirectly derived from emFileModel. An emFilePanel solves all the problems of managing an emFileModelClient, showing the progress of loading the file, displaying error messages, and so on. Therefore the implementation of PlExFilePanel only has to care about situations where the file model is in a fine loaded state. This can be asked with the IsVFSGood() method of emFilePanel. A signal for that state can be get with the GetVirFileStateSignal() method of the same class. See that the implementation of PlExFilePanel calls IsVFSGood() at the beginnings of Input, IsOpaque, Paint and CreateControlPanel. If the result is false, it simply forwards the calls to the original implementation of emFilePanel without doing anything else.

Another new point is the overloading of the CreateControlPanel method of emPanel. It is called by Eagle Mode when the panel gets active in order to create and return a control panel for it. Returning NULL means to have no control panel. A change of the results can be indicated by calling InvalidateControlPanel() so that the old control panel is deleted and that CreateControlPanel is called again. Therefore PlExFilePanel calls InvalidateControlPanel() on every change of IsVFSGood(). It does that in the implementation of Cycle().

emPlExFpPluginFunc

The last section of the C++ source file defines the function emPlExFpPluginFunc which matches the emFpPluginFunc function type. It simply acquires a PlExFileModel in the root context for a given file path and creates an PlExFilePanel on that model.

src/PlEx/PlEx.cpp
#include <emCore/emRecFileModel.h>
#include <emCore/emFilePanel.h>
#include <emCore/emToolkit.h>
#include <emCore/emFpPlugin.h>


//=============================== PlExFileModel ================================

class PlExFileModel : public emRecFileModel, public emStructRec
{
public:

        static emRef<PlExFileModel> Acquire(
                emContext & context, const emString & name, bool common=true
        );

        virtual const char * GetFormatName() const;

        class LineRec : public emStructRec {
        public:
                LineRec();
                emDoubleRec X1,Y1,X2,Y2;
                emDoubleRec Thickness;
                emColorRec Color;
        };

        emColorRec CurrentColor;
        emTArrayRec<LineRec> Lines;

protected:

        PlExFileModel(emContext & context, const emString & name);
};


emRef<PlExFileModel> PlExFileModel::Acquire(
        emContext & context, const emString & name, bool common
)
{
        EM_IMPL_ACQUIRE(PlExFileModel,context,name,common)
}

const char * PlExFileModel::GetFormatName() const
{
        return "PlEx";
}

PlExFileModel::LineRec::LineRec()
        : emStructRec(),
        X1(this,"X1"),
        Y1(this,"Y1"),
        X2(this,"X2"),
        Y2(this,"Y2"),
        Thickness(this,"Thickness"),
        Color(this,"Color")
{
}

PlExFileModel::PlExFileModel(emContext & context, const emString & name)
        : emRecFileModel(context,name), emStructRec(),
        CurrentColor(this,"CurrentColor"),
        Lines(this,"Lines")
{
        PostConstruct(*this);
}


//============================== PlExControlPanel ==============================

class PlExControlPanel : public emRasterGroup {
public:
        PlExControlPanel(ParentArg parent, const emString & name,
                         emRef<PlExFileModel> model);
protected:
        virtual bool Cycle();
private:
        emRef<PlExFileModel> Model;
        emColorField * ColorField;
        emButton * ClearButton;
};

PlExControlPanel::PlExControlPanel(
        ParentArg parent, const emString & name, emRef<PlExFileModel> model
)
        : emRasterGroup(parent,name,"PlEx")
{
        Model=model;
        SetPrefChildTallness(0.3);
        new emLabel(
                new emLinearGroup(this,"about","About"),
                "about",
                "This is PlEx - a plugin example.\n"
                "It allows to draw lines with the\n"
                "left mouse button.\n"
        );
        ColorField=new emColorField(this,"color","Color");
        ColorField->SetColor(Model->CurrentColor);
        ColorField->SetEditable();
        ClearButton=new emButton(
                new emTunnel(this,"tunnel","Clear"),
                "clear","Clear"
        );
        AddWakeUpSignal(ColorField->GetColorSignal());
        AddWakeUpSignal(ClearButton->GetClickSignal());
        AddWakeUpSignal(Model->GetChangeSignal());
}

bool PlExControlPanel::Cycle()
{
        if (IsSignaled(ColorField->GetColorSignal())) {
                Model->CurrentColor.Set(ColorField->GetColor());
                Model->Save(true);
        }
        if (IsSignaled(ClearButton->GetClickSignal())) {
                Model->Lines.SetCount(0);
                Model->Save(true);
        }
        if (IsSignaled(Model->GetChangeSignal())) {
                ColorField->SetColor(Model->CurrentColor);
        }
        return emRasterGroup::Cycle();
}


//=============================== PlExFilePanel ================================

class PlExFilePanel : public emFilePanel {
public:
        PlExFilePanel(ParentArg parent, const emString & name,
                      emRef<PlExFileModel> model);
protected:
        virtual bool Cycle();
        virtual void Input(emInputEvent & event, const emInputState & state,
                           double mx, double my);
        virtual bool IsOpaque() const;
        virtual void Paint(const emPainter & painter, emColor canvasColor) const;
        virtual emPanel * CreateControlPanel(ParentArg parent,
                                             const emString & name);
private:
        emRef<PlExFileModel> Model;
        bool Drawing;
        double X1,Y1,X2,Y2;
        double Thickness;
        bool ControlPanelShown;
};

PlExFilePanel::PlExFilePanel(
        ParentArg parent, const emString & name,
        emRef<PlExFileModel> model
)
        : emFilePanel(parent,name,model)
{
        Model=model;
        Drawing=false;
        ControlPanelShown=false;
        AddWakeUpSignal(GetVirFileStateSignal());
        AddWakeUpSignal(Model->GetChangeSignal());
}

bool PlExFilePanel::Cycle()
{
        if (IsSignaled(GetVirFileStateSignal())) {
                if (ControlPanelShown!=IsVFSGood()) {
                        ControlPanelShown=IsVFSGood();
                        InvalidateControlPanel();
                }
        }
        if (IsSignaled(Model->GetChangeSignal())) {
                InvalidatePainting();
        }
        return emFilePanel::Cycle();
}

void PlExFilePanel::Input(
        emInputEvent & event, const emInputState & state, double mx, double my
)
{
        if (IsVFSGood()) {
                if (event.IsKey(EM_KEY_LEFT_BUTTON)) {
                        Drawing=true;
                        X1=X2=mx;
                        Y1=Y2=my;
                        Thickness=5.0/(GetViewedWidth()+GetViewedHeight());
                        InvalidatePainting();
                }
                if (Drawing && (X2!=mx || Y2!=mx)) {
                        X2=mx;
                        Y2=my;
                        InvalidatePainting();
                }
                if (Drawing && !state.Get(EM_KEY_LEFT_BUTTON)) {
                        Drawing=false;
                        int i=Model->Lines.GetCount();
                        Model->Lines.SetCount(i+1);
                        Model->Lines[i].X1=X1;
                        Model->Lines[i].Y1=Y1;
                        Model->Lines[i].X2=X2;
                        Model->Lines[i].Y2=Y2;
                        Model->Lines[i].Thickness=Thickness;
                        Model->Lines[i].Color=Model->CurrentColor.Get();
                        Model->Save(true);
                        InvalidatePainting();
                }
        }
        emFilePanel::Input(event,state,mx,my);
}

bool PlExFilePanel::IsOpaque() const
{
        if (IsVFSGood()) return true;
        else return emFilePanel::IsOpaque();
}

void PlExFilePanel::Paint(const emPainter & painter, emColor canvasColor) const
{
        if (IsVFSGood()) {
                painter.Clear(emColor::WHITE,canvasColor);
                for (int i=0; i<Model->Lines.GetCount(); i++) {
                        painter.PaintLine(
                                Model->Lines[i].X1,
                                Model->Lines[i].Y1,
                                Model->Lines[i].X2,
                                Model->Lines[i].Y2,
                                Model->Lines[i].Thickness,
                                emRoundedStroke(Model->Lines[i].Color),
                                emStrokeEnd::CAP,emStrokeEnd::CAP
                        );
                }
                if (Drawing) {
                        painter.PaintLine(
                                X1,Y1,X2,Y2,Thickness,
                                emRoundedStroke(Model->CurrentColor),
                                emStrokeEnd::CAP,emStrokeEnd::CAP
                        );
                }
        }
        else {
                emFilePanel::Paint(painter,canvasColor);
        }
}

emPanel * PlExFilePanel::CreateControlPanel(
        ParentArg parent, const emString & name
)
{
        if (IsVFSGood()) {
                return new PlExControlPanel(parent,name,Model);
        }
        else {
                return emFilePanel::CreateControlPanel(parent,name);
        }
}


//============================== PlExFpPluginFunc ==============================

extern "C" {
        emPanel * PlExFpPluginFunc(
                emPanel::ParentArg parent, const emString & name,
                const emString & path, emFpPlugin * plugin,
                emString * errorBuf
        )
        {
                return new PlExFilePanel(
                        parent,
                        name,
                        PlExFileModel::Acquire(parent.GetRootContext(),path)
                );
        }
}

The next file we need, is the maker script. It is called by the make system to compile and link the source in order to create the shared library. For details about this, please read the Make System document.

makers/PlEx.maker.pm
package PlEx;

use strict;
use warnings;

sub GetDependencies
{
	return ('emCore');
}

sub IsEssential
{
	return 0;
}

sub GetFileHandlingRules
{
	return ();
}

sub GetExtraBuildOptions
{
	return ();
}

sub Build
{
	shift;
	my %options=@_;

	system(
		@{$options{'unicc_call'}},
		"--math",
		"--rtti",
		"--exceptions",
		"--bin-dir"       , "bin",
		"--lib-dir"       , "lib",
		"--obj-dir"       , "obj",
		"--inc-search-dir", "include",
		"--link"          , "emCore",
		"--type"          , "dynlib",
		"--name"          , "PlEx",
		"src/PlEx/PlEx.cpp"
	)==0 or return 0;

	return 1;
}

Furthermore, here comes the plugin configuration file. It assigns the plugin to the file name ending .PlEx. The plugin configuration file format is described in the emFpPlugin chapter of the Advanced Configuration document.

etc[w]/emCore/FpPlugins/PlEx.emFpPlugin
#%rec:emFpPlugin%#

FileTypes = { ".PlEx" }
Priority = 1.0
Library = "PlEx"
Function = "PlExFpPluginFunc"

Next is the cosmos item configuration file. It tells where and how the plugin shall appear in the virtual cosmos, and which file to interface. Note the last line: CopyToUser = true. It says that the file shall be copied into a configuration directory owned by the user, because in the original place, the user, and thus the plugin, may not have permission to modify it. For details of the cosmos item file format see the chapter about virtual cosmos configuration in the Advanced Configuration document.

etc[w]/emMain/VcItems/PlEx1.emVcItem
#%rec:emVirtualCosmosItem%#

Title = "Plugin Example (PlEx)"
PosX  = 0.3
PosY  = 0.28
Width = 0.05
ContentTallness = 0.7
BorderScaling = 2.0
BackgroundColor = "#fff"
BorderColor = "#c0a"
TitleColor = "#ff0"
FileName = "PlEx1.PlEx"
CopyToUser = true

Last but not least, here comes the file which contains the initial drawing (a single blue line). On first start, it is copied automatically into a user directory, where it is read and modified by the plugin. The file has an emRec format as defined by the PlExFileModel class. Only the first line is required (i.e. #%rec:PlEx%#), which tells about the file format. Everything else is optional and has defaults.

etc[w]/emMain/VcItemFiles/PlEx1.PlEx
#%rec:PlEx%#

CurrentColor = {0 0 0}
Lines = {
	{
		X1 = 0.1
		Y1 = 0.6
		X2 = 0.9
		Y2 = 0.1
		Thickness = 0.02
		Color = {0 0 255}
	}
}