Awesome Kivy Revelations

We are using kivy for the GUI of the knit editor. It can be used to create Android, Windows, Linux, Mac and IOS apps. You can divide UI-design and code by using the kv language. In this blog post, I want to share some revelations I had when using kivy. This includes showing you how you can update translations automatically when the configuration changes and subtracting values. This awaits you:

Automatic Update of Translated Text in the Kivy UI

In projects like Django, there is the “_” function that allows translations of text. The implementation usually look calls a “gettext” function of some sort, which takes a string and returns its translation as a string. What we have in the kniteditor, is an observable translation, with the same interface:

def _(string):
    """Translate a string using the current language.
    :param str string: the string to translate
    :return: the translated string
    :rtype: str
    """
    return _locales.gettext(string)
_ = ObservableTranslation(_)

The difference is that the observable translation can be used like the “_” function but has additional methods that allow the UI to register and be notified when the language changes. When the language is changed, the global “gettext” is replaced by the “gettext” in the new language and, inthe last line, the observers are notified about the change.

def change_language_to(new_language):
    """Change the language to a language from the translations folder.

    :param str new_language: the language code of the new language
    """
    global _locales, _current_language
    _locales = gettext.translation(DOMAIN, _locale_dir,
                                   languages=[new_language])
    _current_language = new_language
    _.language_changed()

To see what this does, we can look at the whole implementation. I would like to give the whole picture, first, as it clarifies the context and discuss them below.

"""Observable Translations for kivy that change when the language changes.
The functionality of this module is highly inspired by
`kivy-gettext-example <https://github.com/tito/kivy-gettext-example>`.
"""
from kivy.lang import Observable


class ObservableTranslation(Observable):

    """This class allows kivy translations to be updated with the language."""

    def __init__(self, translate):
        """Create a new translation object with a translation function.
        :param translate: a callable that translates the text. 
          Even when the language is changed,
          it returns the text for the current language.
        """
        super().__init__()
        self._translate = translate
        self._observers = []

    def __call__(self, text):
        """Call this object to translate text.
        :param str text: the text to translate
        :return: the text translated to the current language
        """
        return self._translate(text)

    def fbind(self, name, func, args, **kwargs):
        """Add an observer. This is used by kivy."""
        self._observers.append((name, func, args, kwargs))

    def funbind(self, name, func, args, **kwargs):
        """Remove an observer. This is used by kivy."""
        key = (name, func, args, kwargs)
        if key in self._observers:
            self._observers.remove(key)

    def language_changed(self):
        """Update all the kv rules attached to this text."""
        for name, func, args, kwargs in self._observers:
            func(args, None, None)

__all__ = ["ObservableTranslation"]

The constructor takes the “_” function. When the object is called like a function, the “__call__” method is invoked, translating like “_”. If we only have these two methods, the observable translation works just like the “_” function.

“fbind” and “funbind” are the methods that are called when a translation is used in the kv language. They add and remove the observes from the list of observes.

“language_changed” walks through the observers and tells everyone of then to update. With this, you already have a updated translations when “change_language_to” is called.

Here is how you create a button in the kv language that uses the translation function:

#:import _ kniteditor.localization._
Root:
    Button:
        text: _("Load")

Updating the Translations From the Configuration

Ultimately, you want the translations to be changed, when the built-in kivy configuration changed. In the video, seconds, you have seen what it can look like.

LANGUAGE_SECTION = "language"
LANGUAGE_CODE = "current"
LANGUAGES = ["English", "Deutsch"]

class EditorWindow(App):

    """The editor window."""

    def build_config(self, config):
        """Build the configuration.

        :param kivy.config.ConfigParser config: the configuration parser
        .. seealso:: `Application Configuration
         <https://kivy.org/docs/api-kivy.app.html#application-configuration>`__
        """
        config.setdefaults(LANGUAGE_SECTION, {
            LANGUAGE_CODE: LANGUAGES[0]
        })

    def build(self):
        """Build the application."""
        self.update_language_from_config()

    def update_language_from_config(self):
        """Set the current language from the configuration.
        """
        config_language = self.config.get(LANGUAGE_SECTION, LANGUAGE_CODE)
        change_language_to(config_language)

    def build_settings(self, settings):
        """Create the applications settings dialog.

        :param  kivy.uix.settings.Settings settings: the settings 
          for this app
        .. seealso:: `Create a settings panel
          <https://kivy.org/docs/api-kivy.app.html#create-a-settings-panel>`__,
          :meth:`kivy.uix.settings.Settings.add_json_panel`,
          :mod:`kivy.uix.settings`
        """
        settings.add_json_panel(_('KnitEditor'), self.config,
                                data=self.settings_specification)

    @property
    def settings_specification(self):
        """The settings specification as JSON string.

        :rtype: str
        :return: a JSON string
        """
        settings = [
            {"type": "title",
             "title": _("KnitEditor")},

            {"type": "options",
             "title": _("Language"),
             "desc": _("Choose your language"),
             "section": LANGUAGE_SECTION,
             "key": LANGUAGE_CODE,
             "options": LANGUAGES},
        ]
        return json.dumps(settings)

    def on_config_change(self, config, section, key, value):
        """The configuration was changed.

        :param kivy.config.ConfigParser config: the configuration that was
          changed
        :param str section: the section that was changed
        :param str key: the key in the section that was changed
        :param value: the value this key was changed to
        """
        if section == LANGUAGE_SECTION and key == LANGUAGE_CODE:
            change_language_to(new_language)

“build_config” is calls when the application starts to fill the empty configuration with useful values.

“build” is called when the application creates its window, at this point in time, the translation is updated.

“build_settings” is called when the F1 key is pressed or the Android settings menu is activated. “settings_sepcification” is a JSON string used by kivy to build the settings.

“on_config_change” is called, when the configuration is updated. Through e.g. the settings dialog.

With these components together, you can have a updated language every time the language is changed. The video shows an application which uses this code.

Bonus Material: Kivy’s Update is Mighty

In the kv language, variables are accessed via identifiers and not via global variables. As such, the kv language can register every time a Property (like “StringProperty” and “ObjectProperty”) is used. It an register when they change. Thus you can automatically update formulas whenever a value changes.

in this example you can see how a value is subtracted but still, an automatic update is possible:

    Slider:
        min: 0
        max: 100
        value: 100 - other_slider.value

 

Credits

Thanks to Mathieu Virbel for the kivy-gettext-example. It was the basis of this work and an inspiration.