Product SiteDocumentation Site

6.2. Graphical user interface

This section will describe adding support for the graphical user interface (GUI) to your add-on. Before you begin, make sure that your add-on already includes support for Kickstart as described in the previous section.

Note

Before you start developing add-ons with support for the graphical interface, make sure to install the anaconda-widgets and anaconda-widgets-devel packages, which contain Gtk widgets specific for Anaconda such as SpokeWindow.

6.2.1. Basic features

Similarly to Kickstart support in add-ons, GUI support requires every part of the add-on to contain at least one module with a definition of a class inherited from a particular class defined by the API. In case of graphical support, the only recommended class is NormalSpoke, which is defined in pyanaconda.ui.gui.spokes. As the class name suggests, it is a class for the normal spoke type of screen as described in Section 3, “The Hub & Spoke model”.
To implement a new class inherited from NormalSpoke, you must define the following class attributes which are required by the API:
  • builderObjects - lists 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 - contains the id of the main window widget [4] as defined in the .glade file
  • uiFile - contains the name of the .glade file
  • category - contains the class of the category the spoke belongs to
  • icon - contains the identifier of the icon that will be used for the spoke on the hub
  • title defines the title that will be used for the spoke on the hub
Example module with all required definitions is shown in the following example:

Example 7. Defining Attributes Required for the Normalspoke Class

# 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")
The __all__ attribute is used to export the spoke class, followed by the first lines of its definition including definitions of attributes mentioned above. The values of these attributes are referencing widgets defined in org_fedora_hello_world/gui/spokes/hello.glade file.
Two other notable attributes are present. The first is category, which has its value imported from the HelloWorldCategory class from the org_fedora_hello_world.categories module. The HelloWorldCategory class will be discussed later, but for now, note that the path to add-ons is in sys.path so that things can be imported from the org_fedora_hello_world package.
The second notable attribute in the example is title, which contains two underscores in its definition. The first one is part of the N_ function name which marks the string for translation, but returns the non-translated version of the string (translation is done later). The second underscore marks the beginning of the title itself and makes the spoke reachable using 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 graphical interface objects there are two methods initializing a new instance: the __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. Therefore, the __init__ method should only call the parent's __init__ method and (for example) 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.
In the sample Hello World add-on, these two methods are defined as follows (note the number and description of the arguments passed to the __init__ method):

Example 8. Defining the __init__ and initialize Methods

    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")
Note the data parameter passed to the __init__ method. This is the in-memory tree-like representation of the Kickstart file where all data is stored. In one of the ancestors' __init__ methods it is stored in the self.data attribute, which allows all other methods in the class to read and modify the structure.
Because the HelloWorldData class has already been defined in Section 6.1, “Kickstart Support”, there already is a subtree in self.data for this add-on, and its root (an instance of the class) 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 both important when the spoke is created. However, the main role of the spoke is to be visited by an user who wants to change or review the values this spoke shows and sets. To enable this, three other methods are available:
  • refresh - called when the spoke is about to be visited; This method refreshes the state of the spoke (mainly its UI elements) to make sure that current values stored in the self.data structure are displayed
  • apply - called when the spoke is left and used to store values from UI elements back into the self.data structure
  • execute - called when the spoke is left and used to perform any runtime changes based on the new state of the spoke
These functions are implemented in the sample Hello World add-on in the following way:

Example 9. Defining the refresh, apply and execute Methods

    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
You can use several additional methods to control the spoke's state:
  • ready - determines whether the spoke is ready to be visited; if the value is false, the spoke is not accessible (e.g. the Package Selection spoke before a package source is configured)
  • completed - determines if the spoke has been completed
  • mandatory - determines if the spoke is mandatory or not (e.g. the Installation Destination spoke, which must be always visited, even if you want to use automatic partitioning)
All of these attributes need to be dynamically determined based on the current state of the installation process. Below is a sample implementation of these methods in the Hello World add-on, which requires some value to be set in the text attribute of the HelloWorldData class:

Example 10. Defining the ready, completed and mandatory Methods

    @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
After defining these properties, the spoke can control its accessibility and completeness, but it cannot provide a summary of the values configured within - you must visit the spoke to see how it is configured, which may not be desired. For this reason, an additional property called status exists; this property contains a single line of text with a short summary of configured values, which can then be displayed in the hub under the spoke title.
The status property is defined in the Hello World example add-on as follows:

Example 11. Defining the status Property

    @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")
After defining all properties described in this chapter, the add-on has full support for the graphical user interface as well as Kickstart. Note that the example demonstrated here is very simple and does not contain any controls; knowledge of Python Gtk programming is required to develop a functional, interactive spoke in the GUI.
One notable restriction is that each spoke must have its own main window - an instance of the SpokeWindow widget. This widget, along with some other widgets specific to Anaconda, is found in the anaconda-widgets package. Other files required for development of add-ons with GUI support (such as Glade definitions) can be found in the anaconda-widgets-devel package.
Once your graphical interface support module contains all necessary methods you can continue with the following section to add support for the text-based user interface, or you can continue with Section 7, “Deploying and Testing an Anaconda Add-on” and test the add-on.


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