.. _widget-creation:
======================
How to create a widget
======================
The aim of this page is to explain the main components of qtile widgets, how
they work, and how you can use them to create your own widgets.
.. note::
This page is not meant to be an exhaustive summary of everything needed to
make a widget.
It is highly recommended that users wishing to create their own widget refer
to the source documentation of existing widgets to familiarise themselves with
the code.
However, the detail below may prove helpful when read in conjunction with the
source code.
What is a widget?
=================
In Qtile, a widget is a small drawing that is displayed on the user's bar. The
widget can display text, images and drawings. In addition, the widget can be
configured to update based on timers, hooks, dbus_events etc. and can also
respond to mouse events (clicks, scrolls and hover).
Widget base classes
===================
Qtile provides a number of base classes for widgets than can be used to implement
commonly required features (e.g. display text).
Your widget should inherit one of these classes. Whichever base class you inherit
for your widget, if you override either the ``__init__`` and/or ``_configure``
methods, you should make sure that your widget calls the equivalent method from
the superclass.
.. code:: python
class MyCustomWidget(base._TextBox):
def __init__(self, **config):
super().__init__("", **config)
# My widget's initialisation code here
The functions of the various base classes are explained further below.
_Widget
-------
This is the base widget class that defines the core components required for a widget.
All other base classes are based off this class.
This is like a blank canvas so you're free to do what you want but you don't have any
of the extra functionality provided by the other base classes.
The ``base._Widget`` class is therefore typically used for widgets that want to draw
graphics on the widget as opposed to displaying text.
_TextBox
--------
The ``base._TextBox`` class builds on the bare widget and adds a ``drawer.TextLayout``
which is accessible via the ``self.layout`` property. The widget will adjust its size
to fit the amount of text to be displayed.
Text can be updated via the ``self.text`` property but note that this does not trigger
a redrawing of the widget.
Parameters including ``font``, ``fontsize``, ``fontshadow``, ``padding`` and
``foreground`` (font colour) can be configured. It is recommended not to hard-code
these parameters as users may wish to have consistency across units.
InLoopPollText
--------------
The ``base.InLoopPollText`` class builds on the ``base._TextBox`` by adding a timer to
periodically refresh the displayed text.
Widgets using this class should override the ``poll`` method to include a function that
returns the required text.
.. note::
This loop runs in the event loop so it is important that the poll method does not
call some blocking function. If this is required, widgets should inherit the
``base.ThreadPoolText`` class (see below).
ThreadPoolText
--------------
The ``base.ThreadPoolText`` class is very similar to the ``base.InLoopPollText`` class.
The key difference is that the ``poll`` method is run asynchronously and triggers a
callback once the function completes. This allows widgets to get text from
long-running functions without blocking Qtile.
Mixins
======
As well as inheriting from one of the base classes above, widgets can also inherit one
or more mixins to provide some additional functionality to the widget.
PaddingMixin
------------
This provides the ``padding(_x|_y|)`` attributes which can be used to change the appearance
of the widget.
If you use this mixin in your widget, you need to add the following line to your ``__init__``
method:
.. code:: python
self.add_defaults(base.PaddingMixin.defaults)
MarginMixin
-----------
The ``MarginMixin`` is essentially effectively exactly the same as the ``PaddingMixin`` but,
instead, it provides the ``margin(_x|_y|)`` attributes.
As above, if you use this mixin in your widget, you need to add the following line to your
``__init__`` method:
.. code:: python
self.add_defaults(base.MarginMixin.defaults)
Configuration
=============
Now you know which class to base your widget on, you need to know how the widget
gets configured.
Defining Parameters
-------------------
Each widget will likely have a number of parameters that users can change to
customise the look and feel and/or behaviour of the widget for their own needs.
The widget should therefore provide the default values of these parameters as a
class attribute called ``defaults``. The format of this attribute is a list of
tuples.
.. code:: python
defaults = [
("parameter_name",
default_parameter_value,
"Short text explaining what parameter does")
]
Users can override the default value when creating their ``config.py`` file.
.. code:: python
MyCustomWidget(parameter_name=updated_value)
Once the widget is initialised, these parameters are available at
``self.parameter_name``.
The __init__ method
-------------------
Parameters that should not be changed by users can be defined in the ``__init__``
method.
This method is run when the widgets are initially created. This happens before
the ``qtile`` object is available.
The _configure method
---------------------
The ``_configure`` method is called by the ``bar`` object and sets the
``self.bar`` and ``self.qtile`` attributes of the widget. It also creates the
``self.drawer`` attribute which is necessary for displaying any content.
Once this method has been run, your widget should be ready to display content as
the bar will draw once it has finished its configuration.
Calls to methods required to prepare the content for your widget should therefore
be made from this method rather than ``__init__``.
Displaying output
=================
A Qtile widget is just a drawing that is displayed at a certain location the
user's bar. The widget's job is therefore to create a small drawing surface that
can be placed in the appropriate location on the bar.
The "draw" method
-----------------
The ``draw`` method is called when the widget needs to update its appearance.
This can be triggered by the widget itself (e.g. if the content has changed) or
by the bar (e.g. if the bar needs to redraw its entire contents).
This method therefore needs to contain all the relevant code to draw the various
components that make up the widget. Examples of displaying text, icons and
drawings are set out below.
It is important to note that the bar controls the placing of the widget by
assigning the ``offsetx`` value (for horizontal positioning) and ``offsety``
value (for vertical positioning). Widgets should use this at the end of the
``draw`` method. Both ``offsetx`` and ``offsety`` are required as both values will
be set if the bar is drawing a border.
.. code:: python
self.drawer.draw(offsetx=self.offsetx, offsety=self.offsety, width=self.width)
.. note::
If you need to trigger a redrawing of your widget, you should call
``self.draw()`` if the width of your widget is unchanged. Otherwise you
need to call ``self.bar.draw()`` as this method means the bar recalculates
the position of all widgets.
Displaying text
---------------
Text is displayed by using a ``drawer.TextLayout`` object. If all you are doing is
displaying text then it's highly recommended that you use the ```base._TextBox``
superclass as this simplifies adding and updating text.
If you wish to implement this manually then you can create a your own ``drawer.TextLayout``
by using the ``self.drawer.textlayout`` method of the widget (only available after
the `_configure` method has been run). object to include in your widget.
Some additional formatting of Text can be displayed using pango markup and ensuring
the ``markup`` parameter is set to ``True``.
.. code:: python
self.textlayout = self.drawer.textlayout(
"Text",
"fffff", # Font colour
"sans", # Font family
12, # Font size
None, # Font shadow
markup=False, # Pango markup (False by default)
wrap=True # Wrap long lines (True by default)
)
Displaying icons and images
---------------------------
Qtile provides a helper library to convert images to a ``surface`` that can be
drawn by the widget. If the images are static then you should only load them
once when the widget is configured. Given the small size of the bar, this is
most commonly used to draw icons but the same method applies to other images.
.. code:: python
from libqtile import images
def setup_images(self):
self.surfaces = {}
# File names to load (will become keys to the `surfaces` dictionary)
names = (
"audio-volume-muted",
"audio-volume-low",
"audio-volume-medium",
"audio-volume-high"
)
d_images = images.Loader(self.imagefolder)(*names) # images.Loader can take more than one folder as an argument
for name, img in d_images.items():
new_height = self.bar.height - 1
img.resize(height=new_height) # Resize images to fit widget
self.surfaces[name] = img.pattern # Images added to the `surfaces` dictionary
Drawing the image is then just a matter of painting it to the relevant surface:
.. code:: python
def draw(self):
self.drawer.ctx.set_source(self.surfaces[img_name]) # Use correct key here for your image
self.drawer.ctx.paint()
self.drawer.draw(offsetx=self.offset, width=self.length)
Drawing shapes
--------------
It is possible to draw shapes directly to the widget. The ``Drawer`` class
(available in your widget after configuration as ``self.drawer``) provides some
basic functions ``rounded_rectangle``, ``rounded_fillrect``, ``rectangle`` and
``fillrect``.
In addition, you can access the `Cairo`_ context drawing functions via ``self.drawer.ctx``.
.. _Cairo: https://pycairo.readthedocs.io/en/latest/reference/context.html
For example, the following code can draw a wifi icon showing signal strength:
.. code:: python
import math
...
def to_rads(self, degrees):
return degrees * math.pi / 180.0
def draw_wifi(self, percentage):
WIFI_HEIGHT = 12
WIFI_ARC_DEGREES = 90
y_margin = (self.bar.height - WIFI_HEIGHT) / 2
half_arc = WIFI_ARC_DEGREES / 2
# Draw grey background
self.drawer.ctx.new_sub_path()
self.drawer.ctx.move_to(WIFI_HEIGHT, y_margin + WIFI_HEIGHT)
self.drawer.ctx.arc(WIFI_HEIGHT,
y_margin + WIFI_HEIGHT,
WIFI_HEIGHT,
self.to_rads(270 - half_arc),
self.to_rads(270 + half_arc))
self.drawer.set_source_rgb("666666")
self.drawer.ctx.fill()
# Draw white section to represent signal strength
self.drawer.ctx.new_sub_path()
self.drawer.ctx.move_to(WIFI_HEIGHT, y_margin + WIFI_HEIGHT)
self.drawer.ctx.arc(WIFI_HEIGHT
y_margin + WIFI_HEIGHT,
WIFI_HEIGHT * percentage,
self.to_rads(270 - half_arc),
self.to_rads(270 + half_arc))
self.drawer.set_source_rgb("ffffff")
self.drawer.ctx.fill()
This creates something looking like this: |wifi_image|.
.. |wifi_image| image:: ../../_static/widgets/widget_tutorial_wifi.png
Background
----------
At the start of the ``draw`` method, the widget should clear the drawer by drawing the
background. Usually this is done by including the following line at the start of the method:
.. code:: python
self.drawer.clear(self.background or self.bar.background)
The background can be a single colour or a list of colours which will result in a linear gradient
from top to bottom.
Updating the widget
===================
Widgets will usually need to update their content periodically. There are numerous ways
that this can be done. Some of the most common ones are summarised below.
Timers
------
A non-blocking timer can be called by using the ``self.timeout_add`` method.
.. code:: python
self.timeout_add(delay_in_seconds, method_to_call, (method_args))
.. note::
Consider using the ``ThreadPoolText`` superclass where you are calling a function
repeatedly and displaying its output as text.
Hooks
-----
Qtile has a number of hooks built in which are triggered on certain events.
The ``WindowCount`` widget is a good example of using hooks to trigger updates. It
includes the following method which is run when the widget is configured:
.. code:: python
from libqtile import hook
...
def _setup_hooks(self):
hook.subscribe.client_killed(self._win_killed)
hook.subscribe.client_managed(self._wincount)
hook.subscribe.current_screen_change(self._wincount)
hook.subscribe.setgroup(self._wincount)
Read the :ref:`ref-hooks` page for details of which hooks are available and which arguments
are passed to the callback function.
Using dbus
----------
Qtile uses ``dbus-next`` for interacting with dbus.
If you just want to listen for signals then Qtile provides a helper method called
``add_signal_receiver`` which can subscribe to a signal and trigger a callback
whenever that signal is broadcast.
.. note::
Qtile uses the ``asyncio`` based functions of ``dbus-next`` so your widget
must make sure, where necessary, calls to dbus are made via coroutines.
There is a ``_config_async`` coroutine in the base widget class which can
be overridden to provide an entry point for asyncio calls in your widget.
For example, the Mpris2 widget uses the following code:
.. code:: python
from libqtile.utils import add_signal_receiver
...
async def _config_async(self):
subscribe = await add_signal_receiver(
self.message, # Callback function
session_bus=True,
signal_name="PropertiesChanged",
bus_name=self.objname,
path="/org/mpris/MediaPlayer2",
dbus_interface="org.freedesktop.DBus.Properties")
``dbus-next`` can also be used to query properties, call methods etc. on dbus
interfaces. Refer to the `dbus-next documentation `_
for more information on how to use the module.
Mouse events
============
By default, widgets handle button presses and will call any function that is bound to the button in the
``mouse_callbacks`` dictionary. The dictionary keys are as follows:
- ``Button1``: Left click
- ``Button2``: Middle click
- ``Button3``: Right click
- ``Button4``: Scroll up
- ``Button5``: Scroll down
- ``Button6``: Scroll left
- ``Button7``: Scroll right
You can then define your button bindings in your widget (e.g. in ``__init__``):
.. code:: python
class MyWidget(widget.TextBox)
def __init__(self, *args, **config):
widget.TextBox.__init__(self, *args, **kwargs)
self.add_callbacks(
{
"Button1": self.left_click_method,
"Button3": self.right_click_method
}
)
.. note::
As well as functions, you can also bind ``LazyCall`` objects to button presses.
For example:
.. code:: python
self.add_callbacks(
{
"Button1": lazy.spawn("xterm"),
}
)
In addition to button presses, you can also respond to mouse enter and leave events.
For example, to make a clock show a longer date when you put your mouse over it, you
can do the following:
.. code:: python
class MouseOverClock(widget.Clock):
defaults = [
(
"long_format",
"%A %d %B %Y | %H:%M",
"Format to show when mouse is over widget."
)
]
def __init__(self, **config):
widget.Clock.__init__(self, **config)
self.add_defaults(MouseOverClock.defaults)
self.short_format = self.format
def mouse_enter(self, *args, **kwargs):
self.format = self.long_format
self.bar.draw()
def mouse_leave(self, *args, **kwargs):
self.format = self.short_format
self.bar.draw()
Exposing commands to the IPC interface
======================================
If you want to control your widget via ``lazy`` or scripting commands (such as ``qtile cmd-obj``), you
will need to expose the relevant methods in your widget. Exposing commands is done by adding the
``@expose_command()`` decorator to your method. For example:
.. code:: python
from libqtile.command.base import expose_command
from libqtile.widget import TextBox
class ExposedWidget(TextBox):
@expose_command()
def uppercase(self):
self.update(self.text.upper())
Text in the ``ExposedWidget`` can now be made into upper case by calling ``lazy.widget["exposedwidget"].uppercase()``
or ``qtile cmd-onj -o widget exposedwidget -f uppercase``.
If you want to expose a method under multiple names, you can pass these additional names to the decorator. For
example, decorating a method with:
.. code:: python
@expose_command(["extra", "additional"])
def mymethod(self):
...
will make make the method visible under ``mymethod``, ``extra`` and ``additional``.
Debugging
=========
You can use the ``logger`` object to record messages in the Qtile log file to help debug your
development.
.. code:: python
from libqtile.log_utils import logger
...
logger.debug("Callback function triggered")
.. note::
The default log level for the Qtile log is ``INFO`` so you may either want to
change this when debugging or use ``logger.info`` instead.
Debugging messages should be removed from your code before submitting pull
requests.
Submitting the widget to the official repo
==========================================
The following sections are only relevant for users who wish for their widgets to
be submitted as a PR for inclusion in the main Qtile repo.
Including the widget in libqtile.widget
---------------------------------------
You should include your widget in the ``widgets`` dict in ``libqtile.widget.__init__.py``.
The relevant format is ``{"ClassName": "modulename"}``.
This has a number of benefits:
- Lazy imports
- Graceful handling of import errors (useful where widget relies on third party modules)
- Inclusion in basic unit testing (see below)
Testing
-------
Any new widgets should include an accompanying unit test.
Basic initialisation and configurations (using defaults) will automatically be tested by
``test/widgets/test_widget_init_configure.py`` if the widget has been included in
``libqtile.widget.__init__.py`` (see above).
However, where possible, it is strongly encouraged that widgets include additional unit
tests that test specific functionality of the widget (e.g. reaction to hooks).
See :ref:`unit-testing` for more.
Documentation
-------------
It is really important that we maintain good documentation for Qtile. Any new widgets must
therefore include sufficient documentation in order for users to understand how to
use/configure the widget.
The majority of the documentation is generated automatically from your module. The widget's
docstring will be used as the description of the widget. Any parameters defined in the
widget's ``defaults`` attribute will also be displayed. It is essential that there is a
clear explanation of each new parameter defined by the widget.
Screenshots
~~~~~~~~~~~
While not essential, it is strongly recommended that the documentation includes one or more
screenshots.
Screenshots can be generated automatically with a minimal amount of coding by using the fixtures
created by Qtile's test suite.
A screenshot file must satisfy the following criteria:
- Be named ``ss_[widgetname].py``
- Any function that takes a screenshot must be prefixed with ``ss_``
- Define a pytest fixture named ``widget``
An example screenshot file is below:
.. code:: python
import pytest
from libqtile.widget import wttr
RESPONSE = "London: +17°C"
@pytest.fixture
def widget(monkeypatch):
def result(self):
return RESPONSE
monkeypatch.setattr("libqtile.widget.wttr.Wttr.fetch", result)
yield wttr.Wttr
@pytest.mark.parametrize(
"screenshot_manager",
[
{"location": {"London": "Home"}}
],
indirect=True
)
def ss_wttr(screenshot_manager):
screenshot_manager.take_screenshot()
The ``widget`` fixture returns the widget class (not an instance of the widget). Any monkeypatching
of the widget should be included in this fixture.
The screenshot function (here, called ``ss_wttr``) must take an argument called ``screenshot_manager``.
The function can also be parameterized, in which case, each dict object will be used
to configure the widget for the screenshot (and the configuration will be displayed in the docs). If
you want to include parameterizations but also want to show the default configuration, you should include
an empty dict (``{}``) as the first object in the list.
Taking a screenshot is then as simple as calling ``screenshot_manager.take_screenshot()``. The method
can be called multiple times in the same function.
``screenshot_manager.take_screenshot()`` only takes a picture of the widget. If you need to take a screenshot
of the bar then you need a few extra steps:
.. code:: python
def ss_bar_screenshot(screenshot_manager):
# Generate a filename for the screenshot
target = screenshot_manager.target()
# Get the bar object
bar = screenshot_manager.c.bar["top"]
# Take a screenshot. Will take screenshot of whole bar unless
# a `width` parameter is set.
bar.take_screenshot(target, width=width)
Getting help
============
If you still need help with developing your widget then please submit a question in the
`qtile-dev group `_ or submit an issue
on the github page if you believe there's an error in the codebase.