Writing a plug-in for XBMC with its own interface: Part II - dialogs and decorations

  • Tutorial

Introduction


This is part II of a series of articles on writing plugins for XBMC with its own interface. Part I describes general information about creating plug-ins for XBMC with its own interface and provides a simple example. In this part I will give some more general information - I will talk about dialogs, and also consider slightly more complex examples, which will show the use of pictures to decorate the interface, as well as the creation of simple interactive elements.

So, let's move on to the dialogs.

Dialogues


Unlike controls, dialogs do not require a parent container class and can be called directly - by creating an instance of the corresponding class and then calling the desired method. The second difference is that the dialogs automatically use the necessary textures from the current skin, that is, we do not need to worry about their visual design. Functionally, the XBMC UI dialogs resemble the tkFileDialog / tkMessageBox modal dialogs in Tkinter or the QDialog successor dialogs in PyQt.

Most but two of the dialogs are methods of the xbmcgui.Dialog class. The methods are static, that is, they can be called without creating an instance of the corresponding class.

Dialogs xbmcgui.Dialog


Next, we will discuss dialogs implemented using methods of the xbmcgui.Dialog class.

browse

Dialog for selecting files and folders. It has many parameters that allow you to configure the selection mode: file, folder, multiple selection, etc.
Returns a string containing the path to a file or folder, or a tuple of lines with paths depending on the selection mode.
Not quite obvious point: the 3rd call parameter (s_shares) is one of the tags of the file \ userdata \ sources.xml: video, music, pictures or files. The first 3 options are used to access the sources of video, music and photos, respectively, and the files option opens access to the disks / folders added to the file manager. If the file manager is empty, access to the entire file system is opened.
Example:
dialog = xbmcgui.Dialog()
path = dialog.browse(1, 'Select file', 'video', '.avi|.mkv|.mp4')


browseMultiple

A subset of the previous method. Implements multiple choice. Returns a tuple of rows with paths.

browseSingle

Also a subset of browse to select a single file / folder. Returns the path as a string.

input

Input dialog with virtual keyboard. It has different modes: entering text, numbers, dates, times, etc. Returns the entered data as a string.

numeric

A subset of the previous dialog that allows you to enter numerical information. Also returns the entered value as a string.

notification

Displays a pop-up notification in the XBMC interface. The position of the notification depends on the current skin. In Confluence, this is the lower right corner.
Note: this method was added in 13.0 Gotham. In previous versions, use alternative methods for displaying pop-up messages, since there are several methods.

yesno

Dialog with Yes and No buttons. Returns True when selecting “Yes” and False in other cases.

ok

Dialog with the OK button. Returns True when you click OK and False if the dialog was closed in any other way (for example, by pressing ESC).

select

List of text strings for selecting one element from a certain set. With a large number of lines, the list scrolls. Returns the number of the selected line, starting with 0, or -1 if the dialog was closed by pressing ESC (or another button that implements the cancellation of the action).

The following are two dialogs that are not Dialog methods.

DialogProgress


The DialogProgress class implements a dialog box with a progress bar showing the progress of a certain process.

DialogProgressBG


"Background" window with a progress bar. It is usually displayed in the upper right corner of the interface.

As we can see, dialogs complement the capabilities of controls. Combining different controls and dialogs, you can implement a fairly complex interaction with the user.

The most up-to-date brief reference on dialogs and other elements of the XBMC plug-in interface can be found here .

Add decorations and interactive elements.


Next, we’ll look at slightly more complex examples than the simple “Hello world!” in the previous part. To decorate the interface, we need pictures and textures. As mentioned in Part I, in the source code of the skins you can find a fairly wide range of pictures and textures. The textures for the examples below are taken from the sources of the Confluence skin , and I found a picture of a dancing banana on the Internet.

Window Class Example

We start, of course, with the addon.xml file.
Addon.xml content
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.test.buttons"
   name="Buttons 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">Buttons test script</summary>
    <description lang="en">My buttons test script.</description>
  </extension>
</addon>



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

# Имортируем модули стандартной библиотеки.
import os, sys
# Импортируем модули XBMC.
import xbmcgui, xbmcaddon

# Создаем экземпляр класса Addon для доступа к параметрам плагина.
# Если имя плагина в xbmcaddon.Addon() явно не указано,
# мы получаем параметры текущего плагина.
_addon = xbmcaddon.Addon()
# Получаем путь к плагину.
_addon_path = _addon.getAddonInfo('path').decode(sys.getfilesystemencoding())

# Коды клавиатурных команд.
ACTION_PREVIOUS_MENU = 10 # Esc
ACTION_NAV_BACK = 92 # Backspace

# Параметр выравнивания текста.
ALIGN_CENTER = 6

# Указываем пути к картинкам и текстурам:
# фон;
background_img = os.path.join(_addon_path, 'images', 'SKINDEFAULT.jpg')
# текстура кнопки без фокуса;
button_nf_img = os.path.join(_addon_path, 'images', 'KeyboardKeyNF.png')
# текстура кнопки в фокусе;
button_fo_img = os.path.join(_addon_path, 'images', 'KeyboardKey.png')
# танцующий банан чисто для прикола :-).
banana_img = os.path.join(_addon_path, 'images', 'banana.gif')

class MyAddon(xbmcgui.Window):

    def __init__(self):        
        # Устанавливаем фоновую картинку.
        background = xbmcgui.ControlImage(1, 1, 1280, 720, background_img)
        self.addControl(background)
        # Размещаем картинку с бананом.
        banana_picture = xbmcgui.ControlImage(500, 200, 256, 256, banana_img)
        self.addControl(banana_picture)
        # Создаем интерактивные контролы (кнопки).
        self.set_controls()
        # Настраиваем навигацию между контролами.
        self.set_navigation()

    def set_controls(self):
        # Кнопка "Привет".
        self.privet_btn = xbmcgui.ControlButton(500, 500, 110, 40, u'Привет…', focusTexture=button_fo_img,
                                                        noFocusTexture=button_nf_img, alignment=ALIGN_CENTER)
        self.addControl(self.privet_btn)
        # Кнопка "Выход".
        self.exit_btn = xbmcgui.ControlButton(650, 500, 110, 40, u'Выход', focusTexture=button_fo_img,
                                                        noFocusTexture=button_nf_img, alignment=ALIGN_CENTER)
        self.addControl(self.exit_btn)

    def set_navigation(self):
        # Назначаем соседние контролы для кнопки "Привет".
        self.privet_btn.controlRight(self.exit_btn)
        self.privet_btn.controlLeft(self.exit_btn)
        # Назначаем соседние контролы для кнопки "Выход".
        self.exit_btn.controlRight(self.privet_btn)
        self.exit_btn.controlLeft(self.privet_btn)
        # Устанавливаем первоначальный фокус на кнопку "Привет".
        self.setFocus(self.privet_btn)

    def onAction(self, action):
        # Обрабатываем нажатия кнопок для выхода из плагина.
        if action == ACTION_NAV_BACK or action == ACTION_PREVIOUS_MENU:
            self.close()

    def onControl(self, control):
        # Обрабатываем активированные контролы.
        # Если активирована кнопка "Привет"...
        if control == self.privet_btn:
            # ...выводим диалоговое окно с надписью и кнопкой ОК при помощи статического метода .Dialog().ok().
            xbmcgui.Dialog().ok(u'Привет, мир!', u'Я рад тебя видеть :-)')
        # Если активирована кнопка "Выход", выходим из плагина.
        elif control == self.exit_btn:
            self.close()

if __name__ == '__main__':
    addon = MyAddon()
    addon.doModal()
    del addon




Further line-by-line parsing. To display line numbers, use an editor with such a function, for example Notepad ++.
I miss the obvious or previously described points.

12, 14: create an instance of the Addon class to access the plugin parameters. In this case, we need the path to the plugin to access the image files.

21: a numeric constant that determines the alignment of text in some controls, including the button.

25–31: determine the paths to the image files. All pictures are stored in the subfolder \ images of the plugin folder.
Note: if you specify a file name without a full path, XBMC will search for a picture in the resources of the current skin. However, this should not be abused, since the names of image files in different skins most often do not match.

39–43: create two controls with pictures and display them on the screen.

49–55: create 2 buttons. The texture files and the alignment of the text on the button are specified explicitly.

57–65: creating navigation rules. In this case, we assign each of the neighbor buttons to the right and left. Since there are only 2 buttons, each of the buttons is a neighbor to each other, i.e., when you press the left or right arrow keys, the focus will alternately move between our two buttons.
67: for the navigation to work, set the focus to one of the buttons. If this is not done, the controls can only be activated with the mouse.

67–70: the onAction method is the same as in the previous example. I repeat, if you decide to override this method in your class, be sure to write the command (s) for exiting the plugin!

72-80: we process controls. The onControl method receives, as a second parameter, an instance of the activated control, that is, the control that we clicked on or on which we pressed ENTER when it was in focus.
77: when you click the "Hello ..." button, a simple dialog box appears with the "OK" button.
79, 80: The “Exit” button is an alternative way to exit the plugin, in addition to the ESC and BACKSPACE keys.

If everything is done right, we should see
here is a picture



Pay attention to 2 points:
- firstly, animation is supported in the gif file (as in png);
- secondly, controls created later (a picture with a banana, buttons) are displayed on top of the background that was created first.

If you select / click the "Hello" button, it will open
dialog box with text.



A complete plugin with an example can be downloaded from here .

An example based on the WindowDialog class

In the previous example, we inherited from the Window class. As I said in Part I (and as you can see in the very first example), the Window container class has a black opaque background, so in the previous example we placed a texture with a background in the background, on top of which we placed other controls. Now let's try to slightly modify our previous example. Firstly, we will inherit from the WindowDialog class, and secondly, we will select a texture with partial transparency as the background, which will occupy only part of the screen. Let me remind you that the WindowDialog class has a transparent background, i.e., as a result, we should get the window of our plugin on top of the XBMC interface. Let's check it in practice.

As always, addon.xml is the main file of the plugin.
Addon.xml content
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.test.buttons.alt"
   name="Buttons test script - Dialog"
   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">Buttons test script</summary>
    <description lang="en">My buttons test script.</description>
  </extension>
</addon>



Next is the code itself.
default.py
# -*- coding: utf-8 -*-
# Licence: GPL v.3 http://www.gnu.org/licenses/gpl.html

# Имортируем модули стандартной библиотеки.
import os, sys
# Импортируем модули XBMC.
import xbmcgui, xbmcaddon

# Создаем экземпляр класса Addon для доступа к параметрам плагина.
# Если имя плагина в xbmcaddon.Addon() явно не указано,
# мы получаем параметры текущего плагина.
_addon = xbmcaddon.Addon()
# Получаем путь к плагину.
_addon_path = _addon.getAddonInfo('path').decode(sys.getfilesystemencoding())

# Коды клавиатурных комманд.
ACTION_PREVIOUS_MENU = 10 # Esc
ACTION_NAV_BACK = 92 # Backspace

# Параметр выравнивания текста.
ALIGN_CENTER = 6

# Указываем пути к картинкам и текстурам:
# фон;
background_img = os.path.join(_addon_path, 'images', 'ContentPanel.png')
# текстура кнопки без фокуса;
button_nf_img = os.path.join(_addon_path, 'images', 'KeyboardKeyNF.png')
# текстура кнопки в фокусе;
button_fo_img = os.path.join(_addon_path, 'images', 'KeyboardKey.png')
# танцующий банан чисто для прикола :-).
banana_img = os.path.join(_addon_path, 'images', 'banana.gif')

class MyAddon(xbmcgui.WindowDialog):

    def __init__(self):        
        # Устанавливаем фоновую картинку.
        background = xbmcgui.ControlImage(370, 100, 500, 500, background_img)
        self.addControl(background)
        # Размещаем картинку с бананом.
        banana_picture = xbmcgui.ControlImage(500, 200, 256, 256, banana_img)
        self.addControl(banana_picture)
        # Создаем интерактивные контролы (кнопки).
        self.set_controls()
        # Настраиваем навигацию между контролами.
        self.set_navigation()

    def set_controls(self):
        # Кнопка "Привет".
        self.privet_btn = xbmcgui.ControlButton(500, 500, 110, 40, u'Привет…', focusTexture=button_fo_img,
                                                        noFocusTexture=button_nf_img, alignment=ALIGN_CENTER)
        self.addControl(self.privet_btn)
        # Кнопка "Выход".
        self.exit_btn = xbmcgui.ControlButton(650, 500, 110, 40, u'Выход', focusTexture=button_fo_img,
                                                        noFocusTexture=button_nf_img, alignment=ALIGN_CENTER)
        self.addControl(self.exit_btn)

    def set_navigation(self):
        # Назначаем соседние контролы для кнопки "Привет".
        self.privet_btn.controlRight(self.exit_btn)
        self.privet_btn.controlLeft(self.exit_btn)
        # Назначаем соседние контролы для кнопки "Выход".
        self.exit_btn.controlRight(self.privet_btn)
        self.exit_btn.controlLeft(self.privet_btn)
        # Устанавливаем первоначальный фокус на кнопку "Привет".
        self.setFocus(self.privet_btn)

    def onAction(self, action):
        # Обрабатываем нажатия кнопок для выхода из плагина.
        if action == ACTION_NAV_BACK or action == ACTION_PREVIOUS_MENU:
            self.close()

    def onControl(self, control):
        # Обрабатываем активированные контролы.
        # Если активирована кнопка "Привет"...
        if control == self.privet_btn:
            # ...выводим диалоговое окно с надписью и кнопкой ОК при помощи статического метода xbmcgui.Dialog().ok().
            xbmcgui.Dialog().ok(u'Привет, мир!', u'Я рад тебя видеть :-)')
        # Если активирована кнопка "Выход", выходим из плагина.
        elif control == self.exit_btn:
            self.close()

if __name__ == '__main__':
    addon = MyAddon()
    addon.doModal()
    del addon



Compared to the previous example, only 3 lines have changed.
25: another background texture.
33: inherit from WindowDialog.
37: Other coordinates and sizes of the background image.

Check the result.


Now our plugin does not occupy the entire screen, but modestly fits in a window. This option is suitable for plugins with a small number of controls that will not play video or music, because, I recall, when inheriting from WindowDialog, all controls always remain on top of the video or music visualization.

The entire plugin from this example can be downloaded from here .

Conclusion


In Part II of the article series about creating plugins for XBMC with my own interface, I talked about dialogs, and also gave two relatively simple examples of plugins that use textures to decorate the interface and have simple interactive elements.
In Part III, I plan to touch on a little bit about the interaction of plug-ins with XBMC through different APIs, and also demonstrate the micro-framework I wrote that simplifies the creation of the XBMC plug-in interface.

Sources of information


Brief documentation the Python the API of XBMC .
HOW-TO: Write Python Scripts for XBMC .

Previous Articles


Detailed anatomy of a simple plugin for XBMC .
We are writing a plug-in for XBMC with its own interface: Part I - theory and the simplest example .