Project

General

Profile

3. Skindesigner Plugin Interface

A common problem when using skins is that plugins using their own OSD presentation don't match that of the skin. To compensate for this, the plugins author may write additional code within the plugin to simulate different skin appearances. On top of the extra coding and work required for each skin style the author wants to include, users only benefit if their own skin preferences are supported by the plugin. Simply put, this method is too limiting and inefficient. To fully integrate with any skins visual style, a better and more reasonable method is needed. This is where the Skindesigner Plugin Interface comes in.

The Skindesigner Plugin Interface provides an API for plugin developers to use, allowing for an easy way to present the plugin in any skins visual style. Using the API, plugin developers simply define tokens with output data to be displayed. Skindesigner takes over from there using toe skins own XML templates to place the output data accordingly. Once a plugin supports the API, no further work is required by the author and all visual styles available to Skindesigner are now available to the plugin as well.

Some of the topics covered in the following chapters assume the reader understands Skindesigners general functionality and design. If you haven't done so yet, reading the previous Skindesigner chapters contained in this wiki is strongly recommended before continuing. Without doing so certain elements of the Skindesigner Plugin Interface may be confusing or unclear.

 

   3.1    The "basic" and the "advanced" Plugin Interface
   3.2    Preparing the plugin - "basic" or "advanced"?
   3.3    The basic Interface in detail
   3.4    The advanced Interface in detail

 


3.1 The "basic" and the "advanced" Plugin Interface

A VDR Plugin in general can display his screen output in two different ways: either the plugin uses the "standard VDR menu style" with menu lists and text pages, or it draws his content completely free on the OSD. A plugin with screen output always has to include a

cOsdObject *cPlugin::MainMenuAction(void);

function (derived from VDRs cPlugin class to generate output inside the OSD). The cOsdObject return value determines the way a plugin displays its information in the OSD.

If a cOsdMenu object (which is a child class of cOsdObject) is returned by the plugin, the plugin intends to use only the VDR internal menu structure with menu lists and text pages. The plugin is then displayed as a submenu of the VDR main menu. The output then looks similar to the VDR "schedules" menu list, or the "detailed EPG view" that's displayed after selecting an element of the schedules list.

If a pure cOsdObject object is returned by the plugin, the plugin has access to the "raw OSD" and has full control how the output should look. The plugin then has responsible for drawing the menu structures and displaying the output on its own.

Skindesigner offers a solution for both of these approaches with a "basic" and "advanced" Plugin Interface. A reference implementation of the Basic Interface can be found in the "Weatherforecast" Plugin (http://projects.vdr-developer.org/projects/plg-weatherforecast). A reference implementation of the Advanced Interface can be found in the "TVGuideNG" Plugin (http://projects.vdr-developer.org/projects/plg-tvguideng). You can review the code in these plugins to see working examples of the Skindesigner Plugin Interface and how its used.

 


3.2 Preparing the plugin - "basic" or "advanced"?

The Skindesigner Plugin Interface is available as a shared library libSkindesignerApi. This library is automatically installed when installing Skindesigner. To use the library, just add the following code to your Plugin Makefile:

ifdef LCLBLD

SKINDESIGNER_PATH = $(shell pwd)/../skindesigner
LIBSKINDESIGNERAPI_PATH = $(SKINDESIGNER_PATH)/libskindesignerapi
# make sure the lib and pkg-config file exists
DUMMY:=$(shell $(MAKE) -C $(LIBSKINDESIGNERAPI_PATH) LCLBLD=$(LCLBLD))
INCLUDES += -I$(SKINDESIGNER_PATH)
LIBS += -L$(LIBSKINDESIGNERAPI_PATH) $(shell pkg-config --libs $(LIBSKINDESIGNERAPI_PATH)/libskindesignerapi.pc)
DEFINES += -DLIBSKINDESIGNERAPIVERSION='"$(shell pkg-config --modversion $(LIBSKINDESIGNERAPI_PATH)/libskindesignerapi.pc)"'

else

INCLUDES += $(shell pkg-config --cflags libskindesignerapi)
LIBS += $(shell pkg-config --libs libskindesignerapi)
DEFINES += -DLIBSKINDESIGNERAPIVERSION='"$(shell pkg-config --modversion libskindesignerapi)"'

endif

and adapt the $(SOFILE) target so that the LIBS variable is also considered when building the objects:

$(SOFILE): $(OBJS)
    $(CXX) $(CXXFLAGS) $(LDFLAGS) -shared $(OBJS) $(LIBS) -o $@

In the classes which use the interface, the following statements must be used:

#include <libskindesignerapi/skindesignerapi.h>
#include <libskindesignerapi/skindesignerosdbase.h>

To use the Basic Interface you'll need to return a cSkindesignerOsdMenu object which derives from cOsdMenu as the base class in your cPlugin::MainMenuAction function. Returning a cSkindesignerOsdObject object (which derives from cOsdObject) will use the Advanced Interface.

 


3.3 The basic Interface in detail

Once the preparations in chapter 3.2 are complete, you'll need to define and register all the submenus and the tokens which should be available for each submenu you intend to use with Skindesigner. This is done using the Skindesigner "RegisterPlugin" Service and a cPluginStructure object. Here's an example call from the Weatherforecast cPlugin::Start() function:

skindesignerapi::cPluginStructure plugStruct = new skindesignerapi::cPluginStructure();
plugStruct.name = "weatherforecast";
plugStruct.libskindesignerAPIVersion = LIBSKINDESIGNERAPIVERSION;

skindesignerapi::cTokenContainer *tkMenuRoot = new skindesignerapi::cTokenContainer();
tkMenuRoot->DefineIntToken("{current}", (int)eRootMenuIT::current);
tkMenuRoot->DefineIntToken("{iscurrent}", (int)eRootMenuIT::iscurrent);
...
tkMenuRoot->DefineStringToken("{menuitemtext}", (int)eRootMenuST::menuitemtext);
tkMenuRoot->DefineStringToken("{city}", (int)eRootMenuST::city);
...
tkMenuRoot->DefineLoopToken("{hourly[num]}", (int)eForecastHourlyLT::num);
tkMenuRoot->DefineLoopToken("{hourly[timestamp]}", (int)eForecastHourlyLT::timestamp);
tkMenuRoot->DefineLoopToken("{hourly[temperature]}", (int)eForecastHourlyLT::temperature);
...
plugStruct->RegisterMenu((int)eMenus::root, skindesignerapi::mtList, "weatherforecast.xml", tkMenuRoot);

skindesignerapi::cTokenContainer *tkDetailCurrent = new skindesignerapi::cTokenContainer();
tkDetailCurrent ->DefineIntToken("{precipitationprobability}", (int)eDetailCurrentIT::precipitationprobability);
tkDetailCurrent->DefineIntToken("{humidity}", (int)eDetailCurrentIT::humidity);
...
plugStruct->RegisterMenu((int)eMenus::detailCurrent, skindesignerapi::mtText, "weatherforecastdetailcurrent.xml", tkDetailCurrent);
...
if (!skindesignerapi::SkindesignerAPI::RegisterPlugin(plugStruct)) {
    esyslog("weatherforecast: skindesigner not available");
}

First a cPluginStructure Object is created. The attributes "name" and "libskindesignerAPIVersion" have to be set to inform skindesigner which plugin uses the API. The cPluginStructure object has further to be used to tell skindesigner how the plugin menu structure is layed out using the function

void RegisterMenu(int key, int type, string tpl, cTokenContainer *tk);

"key" has to be an unique integer value, "type" has to be each "skindesignerapi::mtList" or "skindesignerapi::mtText". For convenience I recommend to use enums to define the unique keys. These numerical keys have to be used later to set information for and display this menu. tpl is the name of the XML templated which represents the menu and last but not least the cTokenContainer object has to be used to define the integer, string and loop tokens used in this menu. As you see in the example, the cTokenContainer provides the functions

void DefineStringToken  (string name, int index);
void DefineIntToken     (string name, int index);
void DefineLoopToken    (string name, int index);

string and integer tokens can be defined just straight forward defining the name (in curly braces) and an unique index for the token. Here again appropriate enums should be used to define the index values. Loop tokens can be used to display 2dimensional arrays in a menu. For defining loop tokens the array name has to be used as shown in the example. Here the array "hourly" is defined, in this array the values "num" and "timestamp" and "temperature" are available. The "AddXXXToken()" functions which have to be used to set a value for a defined token expect exactly the here defined index values to identify the tokens. In the XML templates themselves the names of the tokens can be used to display a value of a token.

Once the definition of all your plugin menus is done in the explained way, the basic interface can be used. Keep in mind that the interface only extends existing VDR functionality, it does not replace it. If the plugin is used with a non-Skindesigner skin or the selected Skindesigner skin does not provide appropriate templates for your plugin, the VDR `default` behavior is automatically used as a fallback. If that happens the plugin is displayed using the standard VDR menu lists and detailed views.

As previously mentioned, the basic OSD class returned in the MainMenuAction function has to derive from cSkindesignerOsdMenu, which itself derives from cOsdMenu. This class provides the following constructor:

cSkindesignerOsdMenu(skindesignerapi::cPluginStructure *plugStruct, const char *Title, int c0 = 0, int c1 = 0, int c2 = 0, int c3 = 0, int c4 = 0);

    Constructor, passes Title and c0 ... c4 to cOsdMenu for "default" menu display

To display a menu, first cOsdItems have to be created. For that the class "cSkindesignerOsdItem" which derives from cOsdItem has to be used. For reference, heres how the Weatherforecast plugin creates its first (static) main menu entry:
//set menu to display, the template which is registered for this menu will be used
SetPluginMenu((int)eMenus::root, skindesignerapi::mtList);
//delete previous output
Clear();
//set title of menu for default display
SetTitle(tr("Weather Forecast"));
//fetch token container to fill
skindesignerapi::cTokenContainer *tkRoot = GetTokenContainer((int)eMenus::root);
//create an menu item
skindesignerapi::cSkindesignerOsdItem *currentWeather = new skindesignerapi::cSkindesignerOsdItem(tkRoot);
//set text for default display (no skindesigner or template available)
string itemLabelCurrent = tr("Current Weather");
currentWeather->SetText(itemLabelCurrent.c_str());
//fill tokens, to identify tokens use indexes as defined with DefineXXXToken()
currentWeather->AddStringToken((int)eRootMenuST::menuitemtext, itemLabelCurrent.c_str());
currentWeather->AddIntToken((int)eRootMenuIT::iscurrent, 1);
currentWeather->AddIntToken((int)eRootMenuIT::ishourly, 0);
...

In case a fallback is triggered, the text for the default item presentation is set with the "SetText(string text)" function. The three functions
    
void AddStringToken(int index, const char *value);
void AddIntToken(int index, int value);
void AddLoopToken(int loopIndex, int row, int index, const char *value);

can be used to add tokens to the menu item. These tokens can then be used in the templates, both in the "list view" and "current view" to display more detailed information about the currently selected menu item.

To set tokens which have already been set for an existing menu item, they must first be cleared using:

void cSkindesignerOsdMenu::ClearTokens(void);

After all needed menu items are created and added to the menu with
void cOsdMenu::Add(cOsdItem *item);

, it can be displayed with the Display function:
void cSkindesignerOsdMenu::Display(void);

The template (see for example: http://projects.vdr-developer.org/git/vdr-plugin-skindesigner.git/tree/skins/metrixhd/xmlfiles/plug-weatherforecast-weatherforecast.xml)
then uses these tokens to display the appropriate list. The root xml tag has to be <menuplugin>, possible attributes are identical to the other VDR submenus (such as displaymenumain or displaymenuschedules). Since the displayed menus with the basic Interface are submenus of VDRs menu, the viewelements <background>, <header>, <datetime>, <colorbuttons> and <scrollbar> can be used. For the menu lists <menuitems> with <listelement> and <currentelement> have to be implemented. See chapter 1.5 for more information.

Detailed views can be created in a similar way. First, the menu has to be set with "SetPluginMenu". Then, the "SetText" function from cOsdMenu has to be used to set the text for the default display. Integer, String and toop Tokens can be set with the identical functions from cSkindesignerOsdMenu. Additionally the following callback functions can be used which are called when the according keys on the remote are pressed to navigate in the (scrolling) text area:

void TextKeyLeft(void);
void TextKeyRight(void);
void TextKeyUp(void);
void TextKeyDown(void);

For more details look at the classes in the weatherosd.h/weatherosd.c from the Weatherforecast plugin source code.

To display information all features of templates described in the first two chapters of this Wiki can be used.

All of the functionality available to Skindesigner templates (described in chapter 2) is also available to plugins through the Skindesigner Plugin Interface. The skins included with Skindesigner (metrixHD, and estuary4vdr) each provide templates for the Weatherforecast plugin. Looking at them may give you a better idea of how to use templates from your plugin with the Skindesigner Plugin Interface.

 


3.4 The advanced Interface in detail

Contrary to the Basic Interface, the Advanced Interface does not provide a fallback display in case no Skindesigner skin is used or the selected Skindesigner skin does not provide templates for the plugin. Since the Advanced Interface is used to display the provided information completely free on the OSD, a "default behavior" is not possible. If default behavior is desired it will be up to the plugin author to support it within the plugin itself. Otherwise, plugin will only work with Skindesigner skins providing templates for your Plugin.

As already mentioned, the TVGuideNG Plugin uses the Advanced Interface. Since the standalone "TVGuide" plugin already exists, and to keep it simple, TVGuideNG is written without a default implementation on purpose.

If you want to implement a default display for when Skindesigner is not available or the selected Skindesigner skin does not provide appropriate templates, refer to chapter 3.4.2.

3.4.1 Registering a Plugin

To use the Advanced Interface, the base OSD class returned by MainMenuAction() has to derive from cSkindesignerOsdObject. As with the Basic Interface, the plugin has to register its menus via the RegisterPlugin Service. See https://projects.vdr-developer.org/projects/plg-tvguideng/repository/revisions/master/entry/tvguideng.c#L120 as an example how tvguideng defines it's menus and used tokens.

The Advanced Interface provides "views" which can be set with RegisterPlugin::SetView(int viewId, string template). The actual template in the
skin has to be named as "plug-<pluginname>-<templatename>.xml". In our example the template for the root view has to be named as "plug-tvguideng-root.xml".

Three different elements can be defined inside each view:
  • ViewElement: ViewElements are static OSD elements identically to "normal" Skindesigner ViewElements. Each defined ViewElement needs an associated definition in the Template used for this view. Only ViewElements registered at skindesigner are allowed to be used as ViewElement in the according Template.
  • ViewTab: ViewTabs can be used identically as in Skindesigner "detailed view" Templates. There is no need to register ViewTabs for a view, since all in one view defined individual view tabs are handeled indentically (see "plug-tvguideng-detail.xml" as an example).
  • ViewGrid: ViewGrids are a new element type which are available only for Plugins which use the advanced Plugin Interface. In ViewGrids an arbitrary number of Elements can be defined in the Plugin and can be placed or moved in an area defined for this ViewGrid. With that it is possible to build any kind of dynamic display, for instance the TVGuide "EPG grid". Also arbitrary horizontal or vertical menus can be done with this ViewGrid Element.

To register views, subviews, viewElements and viewGrids, the following functions are used:

void skindesignerapi::RegisterRootView(string templateName);
void skindesignerapi::RegisterSubView(int subView, string templateName);
void skindesignerapi::RegisterViewElement(int view, int viewElement, string name, cTokenContainer *tk);
void skindesignerapi::RegisterViewGrid(int view, int viewGrid, string name, cTokenContainer *tk);

SubViews are dedicated menus which are called from the parent view. Closing the sub view returns the display to the parent view. ViewElements and ViewGrids must also be registered for subviews.

3.4.2 Implementing the "MainMenuAction" Function

The function:

virtual cOsdObject *cPlugin::MainMenuAction(void)

has to be used to decide which output the plugin should generate. As done in the TVGuideNG plugin, you can just return an appropriate cSkindesignerOsdObject object. If this object cannot access the Skindesigner plugin via the interface, the output is empty. Alternatively you can handle this case directly in the cSkindesignerOsdObject object by using the "regular" OSD methods (create an OSD and draw on it with "DrawText()", DrawRectangle(), ...) to create an default display, or you can return another object (for instance a different cSkindesignerOsdMenu object) to display only list menus in this case.

You should consider the information to be displayed to decide the best way to do that. Functionality for doing so should be provided by your plugin.

3.4.3 Templates for Views, Definition of ViewElements and ViewGrids

Templates for views have to start with the "displayplugin" Tag:

<displayplugin x="0" y="0" width="100%" height="100%" fadetime="0" scaletvx="70%" scaletvy="0" scaletvwidth="30%" scaletvheight="20%">
   ...
</displayplugin>

As usual, each placement inside a pluginview is relative to the rectangle (x, y, width, height) defined in <displayplugin>. Subviews work the same way, the positioning in the <displayplugin> tag of the subview is relative to the definition in the main view. If the root view is visible in the background, it may be hidden by using the "hideroot" attribute in the root view which should be hided:
<displayplugin x="0" y="0" width="..." height="..." ... hideroot="true">

PluginView and ViewElement's are defined in the following way:
<viewelement name="background_hor">
    <area x="0" y="0" width="100%" height="20%" layer="1">
        <drawimage imagetype="skinpart" path="tvguideheader" x="0" y="0" width="100%" height="100%"/>
    </area>
    ...
</viewelement>

The "name" attribute has to be the name defined in the RegisterPlugin::SetView() function.

A ViewGrid can be defined as following:

<grid name="schedules_ver" x="8%" y="35%" width="92%" height="55%">
    <area layer="1">
        <drawimage condition="{color}++not{current}" imagetype="skinpart" path="tvguide_grid_bright_ver" x="0" y="0" width="100%" height="100%"/>
        <drawimage condition="not{color}++not{current}" imagetype="skinpart" path="tvguide_grid_dark_ver" x="0" y="0" width="100%" height="100%"/>
        <drawimage condition="{current}" imagetype="skinpart" path="tvguide_grid_active_ver" x="0" y="0" width="100%" height="100%"/>
    </area>
    ...
</grid>

The rectangle (x, y, width, height) defines the area in which the grid elements can be placed. Notice that there is (as mandatory for areas in ViewElements) no positioning of the areas in the template. The number of elements in the grid and the positioning of the grids is done dynamically inside the plugin code.

3.4.4 Working with the advanced Plugin Interface

First, inside the base OSD class returned by MainMenuAction, the function

bool cSkindesignerOsdObject::SkindesignerAvailable();

can be used to determinate if skindesigner is available. If it returns false, the Skindesigner plugin is not available. After that an cOsdObject for the root view and every defined subview can be requested:
skindesignerapi::cOsdView *GetOsdView(int subViewId = -1);

If no subViewId is provided, the root view will be returned. If the requested view is not available, NULL is returned.

If you intend to implement a default behavior in your plugin in case Skindesigner is not available, you should check if Skindesigner and all necessary views are available before returning the cSkindesignerOsdObject Object in MainMenuAction().

The cOsdObject class provides functions to work with ViewElements, ViewGrids and ViewTabs:

skindesignerapi::cViewElement *cOsdView::GetViewElement(int viewElementID);
skindesignerapi::cViewGrid *cOsdView::GetViewGrid(int viewGridID);
skindesignerapi::cViewTab *cOsdView::GetViewTabs(void);

The following additional functions are available:
void cOsdView::Deactivate(bool hide);
    Has to be called for a view before displaying a subview. If "hide" is set to true, the main view is invisible as long as the subview is displayed.
    This behaviour can also be gegerated with the "hideroot" attribute of the subviews <displayplugin> tag.

void cOsdView::Activate(void);
    Has to be called after a subview is closed to display the main view again.    

void cOsdView::Display(void);
    Displays the view.

The three classes, cViewElement, cViewGrid and cViewTab, which all derive from cOsdElement, provide the following API:
class cOsdElement

    void AddLoopToken(string loopName, map<string, string> &tokens);
    void AddStringToken(string key, string value);
    void AddIntToken(string key, int value);
    void ClearTokens(void);
    bool ChannelLogoExists(string channelId);
    string GetEpgImagePath(void);

class cViewElement

    void Clear(void);
    void Display(void);

class cViewGrid

    void SetGrid(long gridID, double x, double y, double width, double height);
    void SetCurrent(long gridID, bool current);
    void MoveGrid(long gridID, double x, double y, double width, double height);
    void Delete(long gridID);
    void Clear(void);
    void Display(void);

class cViewTab

    void Init(void);
    void Left(void);
    void Right(void);
    void Up(void);
    void Down(void);
    void Display(void);

Here's an example for displaying a ViewElement in a view:
//init interface
bool skinDesignerAvailable = SkindesignerAvailable();
//get root view
skindesignerapi::cOsdView *rootView = GetOsdView();
//get viewelement "header" from the root view
skindesignerapi::cViewElement *header = rootView->GetViewElement(headerId);
//clearing is not necessary since the viewelement is created newly,
//but if this ViewElement was already displayed before, you'd like
//to clear it and also clear the tokens set before
header->Clear();
header->ClearTokens();
//now add some tokens to the ViewElement
header->AddStringToken(stringTokenIndex, "I'm a header");
header->AddIntToken(intTokenIndex, 5);
//now Display the ViewElement
header->Display();

....

//after all output is generated, with cOsdView::Display() the output is 
flushed to the OSD 
rootView->Display();

Working with a ViewGrid is similar (assuming the interface Init is already done and the rootView is already created):
skindesignerapi::cViewGrid *grid = rootView->GetViewGrid(gridId);
grid->ClearTokens();
grid->AddIntToken(index_id, id1);
grid->AddStringToken(index_name, "gridname1");
...
grid->SetGrid(id2, x2, y2, width2, height2);
grid->ClearTokens();
grid->AddIntToken("id", id2);
grid->AddStringToken("name", "gridname2");
...
grid->SetGrid(id2, x2, y2, width2, height2);
...
grid->SetCurrent(id2, true);
...
grid->Display();
...
rootView->Display();

In this example two new grids are set, the 2nd grid is set to "current". In the template, the token "{current}" is always available. With SetCurrent(id, false) an active grid can be set to not-current.

If a grid is already displayed, but the position of the grid has to be changed, this can be done with the "MoveGrid(int id, int x, int y, int width, int height)" function. If a grid has to be removed, use the "Delete(int id)" function to remove the grid from being displayed.

With grids, an arbitrary dynamic output can be generated. I recommend organizing all your possible grids in dedicated objects and stored in a structure like a list. With that it's easy to generate unique id's (for example, using a static counter) and to remember the positions and states of the different grid elements. By implementing
appropriate Key Listeners in "virtual eOSState ProcessKey(eKeys Key)", which is derived from cOsdObject, the dynamic behavior of your plugin can be dictated in detail.