Writing a plug-in for XBMC with its own interface: part I - theory and simplest example

  • Tutorial

Introduction


This is part I of a series of articles on writing plugins for XBMC with its own interface. It will cover basic information about creating a plug-in interface and provide a simple example.
In Part II, I plan to give a little more theory and a slightly more complex example.
In Part III, I will demonstrate the micro-framework I wrote that simplifies the layout of the interface.

In my first article, “ Detailed Anatomy of a Simple XBMC Plugin, ” I tried to describe the structure of the plug-in for XBMC in as much detail as possible. In addition, I tried to tell how the plugin-content sources and plug-in scripts differ. For those who have forgotten, let me remind you that content source plugins form multilevel lists (directories) of content using the addDirectoryItem () function. That is, we sequentially "feed" the functions of the list items, and all the rest - displaying the list, handling events when selecting list items, etc. - is engaged in XBMC. In turn, script plugins are deprived of such a privilege, and in them all the tasks mentioned are borne by the developer. I also remind you that, like source plug-ins, scripts can be displayed in the “Video”, “Music” and / or “Photo” sections if they perform some tasks with the specified content, or in the “Programs” section if These are general purpose script plugins. In addition, script plugins can also download subtitles, work as background services, etc. The plugin function is determined by the tags in the file Addon.xml containing all the information about our plugin.

Unfortunately, no ingenious ideas have visited me, so the examples in these articles are somewhat abstract and theoretical in nature. The use of this information for writing something useful I leave you.

To understand these articles, a good knowledge of the principles of OOP and writing GUI applications in Python (hereinafter - Python) is highly desirable.

general information


The xbmcgui module , which is part of the XBMC Python API, is responsible for working with the interface . This module contains general container classes, as well as classes responsible for different interface elements. Unfortunately, the part of the API responsible for the interface is less convenient than the traditional Python libraries of the GUI, and the documentation is also full of inaccuracies. I recommend using the stub modules that I wrote about in my previous article. In addition to the convenience of development, they also contain more relevant information: both on the set of classes, methods and functions, and on their use (in dockstring).

Plugin Interface Components


To create the plugin interface, the xbmcgui module provides us with various components:
- container classes (Window class and its descendants);
- widgets, or in XBMC terminology, controls that are responsible for different interface elements (Control descendant classes);
- dialogs used to display information and interact with the user (methods of the Dialog class and others).

Consider the first 2 points in more detail.

WindowXML and WindowXMLDialog Classes


As the names suggest, these container classes use an XML skeleton interface skeleton that has a structure similar to that of XBMC skins. That is, in fact, the interface based on these classes has its own mini-skin. I don’t want to delve into the jungle of skin-writing (which I admit to know little), so I won’t dwell on these classes.

Window and WindowDialog Classes


The Window and WindowDialog classes are the parent containers that host other interface elements - controls. To place controls, these classes provide a coordinate grid starting from the upper left corner of the screen. By default, the resolution of the visible area of ​​the grid is 1280 x 720 pixels. This resolution can be changed, but personally I don’t see much point in this, so the examples will use the standard resolution of the visible area. It is not for nothing that I used the term “visible area”. The coordinates of the controls can have any values, including negative ones, but only what falls into the visible area will be visible on the screen. This can be used, for example, to temporarily hide some kind of control by setting coordinates for it that obviously go beyond the visible area (for example, -5000, -5000).
Attention: do not confuse the resolution of the visible area of ​​the coordinate grid and the screen resolution. The visible area has the same resolution at any actual screen resolution, so that the interface elements always have the same scale regardless of the actual resolution.

The difference between the Window and WindowDialog classes is that the Window class has a black opaque background and can hide under a video playback or music visualization window. WindowDialog, in turn, has a transparent background, and its controls are always displayed on top of other elements of the XBMC interface. Thus, if your plug-in will play video and play music, then it is better to use the Window class for it, and in other cases the choice of the class depends on the personal preferences and tasks of the plug-in.

To handle interface events, both classes have the onAction () and onControl () methods. The first intercepts keyboard commands, and the second - activated controls. Thus, in order to ensure interaction with the user, these methods must be defined in the child class that implements the interface of your plug-in, having written the appropriate event handlers in them.
Attention! By default, the onAction method “catches” keyboard commands corresponding to the BACKSPACE and ESC keys, which call the close method (exiting the plugin). Therefore, if this method is overridden in a child class, you must register at least the plug-in exit command (s). Otherwise, you can exit the plugin only by forcibly stopping (“killing”) the XBMC, which can be a problem, say, in embedded systems (OpenELEC, etc.).

Controls


Now we will get acquainted in more detail with controls. I will not tell about all the controls, but only about those that I personally studied, but I think they will be enough for most tasks. Controls are the interface elements (widgets) of the plugin. However, unlike widgets in general-purpose GUI frameworks (Tkinter, PyQt), controls are kind of “skeletons” of interface elements. For visual design controls need graphic files with pictures (textures).

Those who are good at photojack graphic programs, they can draw textures on their own, and the rest of the plugin descriptors can take the necessary texture files in the XBMC skin resources. Unfortunately, in the finished skins, the textures are packed in a special format, so we need the source code for the skin. Sources of the standard XBMC skin, Confluence, including images, can be found here .
Note: some controls (for example, a button) can automatically use the necessary textures of the current skin, but you can’t rely on this behavior. Firstly, not all controls can do this, and secondly, when switching to another skin, the appearance of the plugin may be violated. Therefore, it is better to specify texture files for controls explicitly.

As you can guess from the above, when creating plugins with their own interface, you can use 2 approaches: simple, in which the appearance of the plugin will have the same appearance in any skin, and complex, in which texture files for widgets will be selected depending on the current skin, so that the plugin interface is styled in the same style as the skin. It’s clear that you can’t grasp the immensity, so with the second approach you will have to limit yourself to a few popular skins, and use the default layout for the rest.

The following is a brief description of the main interface controls.

Controllabel

Simple inscription with a transparent background. Texture does not use. This is a complete analogue of the Label classes in Tkinter or QLabel in PyQt. Text alignment is specified by one of the following numeric constants:
ALIGN_LEFT = 0
ALIGN_RIGHT = 1
ALIGN_CENTER_X = 2
ALIGN_CENTER_Y = 4
ALIGN_CENTER = 6
ALIGN_TRUNCATED = 8
ALIGN_JUSTIFY = 10


Just in case, let me remind you that in a real plug-in, text strings for this and other controls displaying text information are best obtained from language files using the Addon.getLocalizedString () method, so that the interface of your plug-in matches the current XBMC language settings.

ControlFadeLabel

An analogue of ControlLabel with the only difference being that a line that is too long will automatically scroll.

ControlTextBox

A test window that may contain long text with automatic line breaks. There is no scrolling, and text that does not fit in the text box is trimmed.

ControlImage

Picture. The main image file formats are supported (jpg, png, gif). The png and gif files support transparency and animation (if any). By default, the picture fits into the designated place with a distortion of the proportions, but the way of fitting can be set. Unfortunately, the fit option does not work the next time you change the picture.

Controlbutton

Button. The button click handler is registered in the onControl () method of the container class.

ControlRadioButton and ControlCheckMark

Radio button and checkbox (“checkmark”). Functionally completely similar and differ only in appearance. Used as a two-state switch. Require texture.

Controledit

A field for entering text. When selected, an on-screen keyboard opens, allowing you to enter the desired text. If you specify the parameter isPassword = True, the entered text will be replaced by asterisks.

Controllist

List. You can add simple text strings to the list, but in fact, each item in the list is an instance of the ListItem class. Let me remind you that lists of content source plugins consist of the same elements. However, unlike source plugins, the functionality of list items in ControlList is limited. In fact, only text labels and small icons (thumbnailImage) are supported. All the rest of the logic, for example, the action when choosing a list item, you need to implement yourself. Control requires texture to style the list.

If all list items do not fit in the space provided, the list scrolls.

Controlslider

Slider with a scale. Serves for smooth adjustment of any parameter. Requires explicit textures.

Examples of controls designed using Confluence skin textures can be seen in the screenshot below.

Basic controls of the plugin interface



ControlButton, ControlRadioButton, ControlEdit, ControlList and ControlSlider are interactive elements that change their appearance when they are selected. Theoretically, you can choose any control, for example, ControlLabel, but there will be no visual feedback.

To navigate the plugin interface using the arrow keys on the keyboard or remote control, each control needs to assign neighbors on which the focus will be moved when the corresponding arrow key is pressed. To do this, use the setNavigation methods, as well as controlUp, controlDown, controlLeft and controlRight inherited from Control.
In addition, for the navigation to work, during the initial display of the interface, do not forget to set the focus on one of the controls.

Controls are added using the addControl method of the container class. At the same time, controls added later are displayed on top of those that were added earlier. For example, we can first display a picture that will serve as a background, and place all other controls on top of it.

Note: almost all controls have methods that allow you to change their properties (text, image, etc.) during the execution of the plugin. So, always change the property of control (for example, the inscription ControlLabel with setLabel ('Some Text') after how this control was added to the container class using the addControl method. When you try to change the properties of an “unbound” control, nothing will happen in the best case, and in the worst, all kinds of interesting glitches of the XBMC interface are possible.

Now let's move on to practical examples. Let's start, of course, with the classic “Hello, World!”, Or in the Russian version of “Hello, world!”.

Plugin “Hello world!”


As I said in my first article, for each plugin you need the required utility file addon.xml.
Addon.xml content
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.test"
   name="Test script"
   version="0.0.1"
   provider-name="Roman_V_M">
  <requires>
    <import addon="xbmc.python" version="2.0"/>
  </requires>
  <extension point="xbmc.python.script" library="default.py">
    <provides>executable</provides>
  </extension>
  <extension point="xbmc.addon.metadata">
    <platform>all</platform>
    <summary lang="en">Test script</summary>
    <description lang="en">My test script.</description>
  </extension>
</addon>



Lines
<extension point="xbmc.python.script" library="default.py">
    <provides>executable</provides>
  </extension>

They tell us (and XBMC) that this is a software plug-in script that will be available in the "Programs" section.

Now directly the code of our plugin:
Contents of default.py
# -*- coding: utf-8 -*-
# Licence: GPL v.3 http://www.gnu.org/licenses/gpl.html

# Импортируем нужный модуль
import xbmcgui

# Коды клавиатурных действий
ACTION_PREVIOUS_MENU = 10 # По умолчанию - ESC
ACTION_NAV_BACK = 92 # По умолчанию - Backspace


# Главный класс-контейнер
class MyAddon(xbmcgui.Window):

    def __init__(self):
        # Создаем текстовую надпись.
        label = xbmcgui.ControlLabel(550, 300, 200, 50, u'Привет, мир!')
        # Добавляем наддпись в контейнер
        self.addControl(label)

    def onAction(self, action):
        # Если нажали ESC или Backspace...
        if action == ACTION_NAV_BACK or action == ACTION_PREVIOUS_MENU:
            # ...закрываем плагин.
            self.close()


if __name__ == '__main__':
    # Создаем экземпляр класса-контейнера.
    addon = MyAddon()
    # Выводим контейнер на экран.
    addon.doModal()
    # По завершении удаляем экземпляр.
    del addon




Further a brief analysis, since the main points are already indicated in the comments to the code. To display line numbers, use a text editor with the appropriate function, for example Notepad ++.

Lines 8, 9: here we set the constants corresponding to the numeric codes of the keyboard events used by the XBMC. A complete list of keyboard events can be found in the XBMC source code . The correspondence between keyboard events and the keys of the keyboard, remote control or other control is set in the keyboard.xml configuration file

13-25: we describe the class that implements our interface. In this case, the class inherits from Window. Our interface contains a single control - ControlLabel with the corresponding inscription. Please note that the first 4 parameters of the control are its coordinates and dimensions in width and height in pixels of the coordinate grid. These parameters are required. The coordinates and dimensions can subsequently be changed if necessary. If we want to create a control, but postpone its display, we can specify fictitious coordinates and / or sizes, and set real parameters after the control is displayed.
The addControl () method of the parent class is responsible for adding controls to the container.

In the onAction method, we intercept keyboard events corresponding to the ESC and Backspace keys in order to be able to exit our plugin. Once again, if onAction method overridden in a derived class, the plugin must be sure to have a code to get out of it, without resorting to radical measures, such as Task Manager.
Note: despite comparing with integers, the action parameter is actually an instance of the Action utility class.

32: the doModal () method displays the interface we created on the screen. This method is similar to mainloop () in Tkinter.Tk or exec_ () in QDialog. The interface will be displayed until the close () method is called.
In addition to doModal (), you can use show (). In this case, the interface will be displayed while the plugin is running or until close () is called. If you use show (), the organization of the event loop rests entirely with the programmer.

34: why remove the class instance at the end of the job I honestly don’t know. I saw this in an example in the official WiKi.

If everything is done right, we should see
Here's a picture.



Agree, not a very attractive sight. As stated above, the Window class has a black opaque background. Therefore, in the following example, we will try to slightly decorate our interface, as well as add some interactive elements.

For the sake of curiosity, try to inherit from WindowDialog. With this option, the label will appear on top of the XBMC interface.

Ready plugin “Hello world!” can be downloaded from here . Let me remind you that you can install the plug-in from the menu System> Add-ons> Install from ZIP File. All new plugins are installed in the \ addons folder of the user settings folder. On Windows, this is usually% AppData% \ XBMC, on Linux, $ HOME / .xbmc.

Conclusion


In part I of the article devoted to writing plug-ins for XBMC with its own interface, the basic elements of the plug-in interface were considered, as well as a simple example was given.
In the next part, I plan to talk about dialogs, as well as give a slightly more complex example, demonstrating the use of pictures to decorate the interface, as well as creating interactive elements.

Those who want to run a little ahead can download this plugin that demonstrates the controls described above. It was from him that a screenshot was taken with examples of controls above. The plugin uses the micro-framework I wrote, which I plan to talk about in Part III, but experienced nutritionists, I think, can figure it out easily.

P. S. Сделал несколько мелких исправлений и добавлений.
P.P.S. Исправил информацию о методе onClose.

Continuation


We are writing a plug-in for XBMC with its own interface: Part II - dialogs and decorations .