Product SiteDocumentation Site

6.2. Graphical user interface

6.2.1. Basic features

Since the textual interface uses a custom toolkit developed for the Anaconda installer, let's start with the Graphical user interface using standard Gtk toolkit that should be more familiar to developers. Again, first we need a subdirectory (subpackage) in the addon's top-level directory (package). The one for the GUI code must be named gui and since there are more types of objects it can provide, it needs to have subdirectories itself. We will start with the most important and most common one — spokes. As it was described in the Section 6.1, “Kickstart support” every part of the addon has to contain at least one module with a definition of a class inherited from a particular class defined by the API. In case of the kickstart support this class was the AddonData class in case of the GUI support there are multiple such classes. But the only recommended one for an addon is the NormalSpoke class defined in the pyanaconda.ui.gui.spokes package. As its name suggests it is a class for the normal spoke screen described in the Section 3, “Hub&Spoke model”. To implement a new class inherited from the NormalSpoke class we need to define the folowing class attributes required by the API:
  • builderObjects that should list all top-level objects from the spoke's .glade file that should be, with their children objects (recursively), exposed to the spoke or should be an empty list if everything should be exposed to the spoke (not recommended),
  • mainWidgetName containing the id of the main window widget [5] as defined in the .glade file,
  • uiFile containing the name of the .glade file,
  • category containing the class of the category the spoke belongs to,
  • icon containing the identifier of the icon that will be used for the spoke on the hub and
  • title defining the title that will be used for the spoke on the hub.
The code with all those definitions will then look like this:
# will never be translated
_ = lambda x: x
N_ = lambda x: x

# the path to addons is in sys.path so we can import things from org_fedora_hello_world
from org_fedora_hello_world.categories.hello_world import HelloWorldCategory
from pyanaconda.ui.gui.spokes import NormalSpoke

# export only the spoke, no helper functions, classes or constants
__all__ = ["HelloWorldSpoke"]

class HelloWorldSpoke(NormalSpoke):
    """
    Class for the Hello world spoke. This spoke will be in the Hello world
    category and thus on the Summary hub. It is a very simple example of
    a unit for the Anaconda's graphical user interface.

    :see: pyanaconda.ui.common.UIObject
    :see: pyanaconda.ui.common.Spoke
    :see: pyanaconda.ui.gui.GUIObject

    """

    ### class attributes defined by API ###

    # list all top-level objects from the .glade file that should be exposed
    # to the spoke or leave empty to extract everything
    builderObjects = ["helloWorldSpokeWindow", "buttonImage"]

    # the name of the main window widget
    mainWidgetName = "helloWorldSpokeWindow"

    # name of the .glade file in the same directory as this source
    uiFile = "hello_world.glade"

    # category this spoke belongs to
    category = HelloWorldCategory

    # spoke icon (will be displayed on the hub)
    # preferred are the -symbolic icons as these are used in Anaconda's spokes
    icon = "face-cool-symbolic"

    # title of the spoke (will be displayed on the hub)
    title = N_("_HELLO WORLD")
In the begining two common functions for translations are defined, but with the unusual definition for the _ function. This is caused by the fact that our addon is not meant to have translations. Then we can again see the usage of the __all__ variable to export only the spoke class followed by the first lines of its definition including the definitions of attributes mentioned above. Their values are referencing the widgets defined in the org_fedora_hello_world/gui/spokes/hello.glade file included in the Hello world addon's sources (if you want to open the file, see the begining of the Section 7, “Deploying and testing an Anaconda addon” that lists the packages that are needed). Only two of the attributes deserve a further comment. The first one is the category attribute the value of which is the HelloWorldCategory class imported from the org_fedora_hello_world.categories module. We will get to the HelloWorldCategory definition later, but for now note what was mentioned in the comment just before the import:

Important

The path to addons is in sys.path so things can be imported from the org_fedora_hello_world package.
The second attribute that deserves a comment is the title attribute whose definition contains two underscores. The former one is part of the N_ function name that marks the string for translation, but returns the non-translated version of the string (translation is done later). The latter one is part of the title itself and makes the spoke reachable from the hub with the Alt+H keyboard shortcut.
What usually follows the header of the class definition and the class attributes definitions is the constructor that initializes an instance of the class. In case of the Anaconda installer's GUI objects there are two methods initializing a new instance — common Python's __init__ method and the initialize method. The reason for two such functions is that the GUI objects may be created in memory at one time and fully initialized (which can take a longer time) at a different time. Thus the __init__ method should only call the parent's __init__ method and e.g. initialize non-GUI attributes. On the other hand the initialize method that is called when the installer's graphical user interface initializes should finish the full initialization of the spoke. This is how these two methods look in our case (note the number and description of the arguments passed to the __init__ method):
    def __init__(self, data, storage, payload, instclass):
        """
        :see: pyanaconda.ui.common.Spoke.__init__
        :param data: data object passed to every spoke to load/store data
                     from/to it
        :type data: pykickstart.base.BaseHandler
        :param storage: object storing storage-related information
                        (disks, partitioning, bootloader, etc.)
        :type storage: blivet.Blivet
        :param payload: object storing packaging-related information
        :type payload: pyanaconda.packaging.Payload
        :param instclass: distribution-specific information
        :type instclass: pyanaconda.installclass.BaseInstallClass

        """

        NormalSpoke.__init__(self, data, storage, payload, instclass)

    def initialize(self):
        """
        The initialize method that is called after the instance is created.
        The difference between __init__ and this method is that this may take
        a long time and thus could be called in a separated thread.

        :see: pyanaconda.ui.common.UIObject.initialize

        """

        NormalSpoke.initialize(self)
        self._entry = self.builder.get_object("textEntry")
Both methods are very simple, but still there are few things deserving a comment. The most important one is the data parametr passed to the __init__ method. It is the in-memory tree-like representation of the kickstart file where all the data is stored. In one of the ancestors' __init__ methods it is stored in the self.data attribute so we can read and modify it in all other methods of the class. Since we have defined the HelloWorldData class in Section 6.1, “Kickstart support” there is a subtree in self.data for our addon and its root (an instance of the HelloWorldData) is available as self.data.addons.org_fedora_hello_world. One of the other things an ancestor's __init__ does is initializing an instance of the GtkBuilder with the spoke's .glade file and storing it as self.builder. This is used in the initialize method to get the GtkTextEntry used to show and modify the text from the kickstart file's %addon section.
The __init__ and initialize methods are the two methods that play their roles when the spoke is created. However, the main role of the spoke is to be visited by user who wants to change or review some values it shows and sets. There are three methods — refresh, apply and execute — that handle things that need to be done when the spoke is entered and left. The refresh method is called when the spoke is about to be visited by the user and its responsibility is to refresh the spoke's state (mainly it's UI elements) to reflect the current values stored in the self.data structure. The apply and execute methods are called when the spoke is left and they should store values set in the UI elements to the self.data structure and do all runtime changes the spoke requires based on its current state, respectively.
The implementations of those three functions are very simple in the Hello world addon:
    def refresh(self):
        """
        The refresh method that is called every time the spoke is displayed.
        It should update the UI elements according to the contents of
        self.data.

        :see: pyanaconda.ui.common.UIObject.refresh

        """

        self._entry.set_text(self.data.addons.org_fedora_hello_world.text)

    def apply(self):
        """
        The apply method that is called when the spoke is left. It should
        update the contents of self.data with values set in the GUI elements.

        """

        self.data.addons.org_fedora_hello_world.text = self._entry.get_text()

    def execute(self):
        """
        The excecute method that is called when the spoke is left. It is
        supposed to do all changes to the runtime environment according to
        the values set in the GUI elements.

        """

        # nothing to do here
        pass
So far we have covered methods that can be used to instantiate and visit spoke. It may seem like everything that is needed, but not every spoke can be visited anytime (e.g. What would be the point of software selection being shown before the repository is set?) and while values shown and controlled by some spokes are crucial to the installation process and cannot be omitted, some spokes allow modification of optional values with minor effect on the installed system. That's why all spokes have the ready, completed and mandatory properties. As their names suggest, these properties determine if the spoke is ready to be visited, if the spoke is completed (i.e. all values it requires to be set are set) and if the spoke is mandatory to be completed for the installation to continue. All these attributes of the spoke need to be dynamically determined based on the current state of the installer/installation process. Here comes the trivial implementation of those properties from the Hello world addon which requires some value to be set in the HelloWorldData's text attribute:
    @property
    def ready(self):
        """
        The ready property that tells whether the spoke is ready (can be visited)
        or not. The spoke is made (in)sensitive based on the returned value.

        :rtype: bool

        """

        # this spoke is always ready
        return True

    @property
    def completed(self):
        """
        The completed property that tells whether all mandatory items on the
        spoke are set, or not. The spoke will be marked on the hub as completed
        or uncompleted acording to the returned value.

        :rtype: bool

        """

        return bool(self.data.addons.org_fedora_hello_world.text)

    @property
    def mandatory(self):
        """
        The mandatory property that tells whether the spoke is mandatory to be
        completed to continue in the installation process.

        :rtype: bool

        """

        # this is an optional spoke that is not mandatory to be completed
        return False
With those three properties defined, a spoke can tell users whether they may, have to or cannot visit the spoke. Nevertheless, users seeing the hub need to decide whether to visit the spoke or not. That's why every spoke also has the status property, which is supposed to provide a short (one-line) summary describing values set on the spoke. Since the only value managed by the spoke is the text it shows, allows to edit and stores in the self.data structure, it is only logical to use that text as the status. Also, the status should warn user if no text is set:
    @property
    def status(self):
        """
        The status property that is a brief string describing the state of the
        spoke. It should describe whether all values are set and if possible
        also the values themselves. The returned value will appear on the hub
        below the spoke's title.

        :rtype: str

        """

        text = self.data.addons.org_fedora_hello_world.text

        # If --reverse was specified in the kickstart, reverse the text
        if self.data.addons.org_fedora_hello_world.reverse:
            text = text[::-1]

        if text:
            return _("Text set: %s") % text
        else:
            return _("Text not set")
And that's it! Less than 60 lines of code (the real code lines) are needed to implement the basic GUI of an addon. Of course, it is a trivial addon that does nothing useful, but anything else is just a common Python Gtk programming with some minor specific restrictions. For example, as was mentioned in the begining of this section, every spoke has to have its main window — an instance of the SpokeWindow widget. This widget (together with some more Anaconda-specific widgets) exists in the anaconda-widgets package and files needed for development (e.g. Glade definitions) live in the anaconda-widgets-devel package.


[5] an instance of the SpokeWindow widget which is a custom widget created for the Anaconda installer