Commit 215bf2d2 authored by David Boddie's avatar David Boddie 💬
Browse files

Merge branch 'adaptive-ui-tutorial' into 'master'

Adaptive ui tutorial

See merge request Librem5/developer.puri.sm!218
parents a003b4b0 1620176b
Pipeline #4949 passed with stages
in 9 minutes and 51 seconds
......@@ -6,7 +6,9 @@ build_html:
stage: build
before_script:
- apt-get -y update
- apt-get -y install make python3-sphinx libxml2-utils flake8
- apt-get -y install make python3-sphinx libxml2-utils flake8 git
- git submodule init
- git submodule update --remote
tags:
- librem5
script:
......
[submodule "Apps/Tutorials/Adaptive_UI/apps/part1"]
path = Apps/Tutorials/Adaptive_UI/apps/part1
url = https://source.puri.sm/librem5-examples/adaptive-ui-title-bar.git
[submodule "Apps/Tutorials/Adaptive_UI/apps/part2"]
path = Apps/Tutorials/Adaptive_UI/apps/part2
url = https://source.puri.sm/librem5-examples/adaptive-ui-leaflet.git
[submodule "Apps/Tutorials/Adaptive_UI/apps/part3"]
path = Apps/Tutorials/Adaptive_UI/apps/part3
url = https://source.puri.sm/librem5-examples/adaptive-ui-two-leaflets.git
.. Use the multi-part tutorial template.
.. _Adaptive_UI_tutorial_building:
.. include:: ../common/Building_the_Apps.txt
.. |project| replace:: https://source.puri.sm/librem5-examples
.. |repo| replace:: adaptive-ui-title-bar.git adaptive-ui-leaflet.git adaptive-ui-two-leaflets.git
.. Use the multi-part tutorial template.
.. include:: ../common/Getting_the_Apps.txt
.. _Adaptive_UI_tutorial_part2:
Part 2: Using a Leaflet
=======================
.. contents::
:local:
Overview
--------
When run, the application described in this part of the tutorial shows a window
containing text labels that can be shown side-by-side or individually depending
on the available screen space.
.. image:: images/leaflet.png
:scale: 25%
:align: center
:alt: A screenshot of the application running in the phone environment
The main source code for the application can be found in the ``main.py`` file
within the ``src`` directory. The purpose of the other files is explained in
other tutorials, such as :ref:`First_Application`.
The Program
-----------
The main program is longer than in the :ref:`previous part <Adaptive_UI_tutorial_part1>`
of this tutorial, so we will not show it all here. We will focus on the parts
that are specific to this example.
Relevant Modules
~~~~~~~~~~~~~~~~
The program begins by importing the modules it needs to create a user
interface. In addition to the standard ``Gtk`` module we use the ``Handy``
module in order to access the widgets of the `libhandy library`_:
.. literalinclude:: apps/part2/src/main.py
:language: python3
:start-at: Handy
:end-at: Handy.init
When importing the ``Handy`` module, it is important to specify the version of
the API that will be used.
Creating a Title Bar
~~~~~~~~~~~~~~~~~~~~
The ``Application`` class provides the usual methods to set up the application
and perform tasks when it is run. In the ``do_activate`` method we set up the
user interface, beginning by creating a title bar:
.. literalinclude:: apps/part2/src/main.py
:language: python3
:start-at: do_activate
:end-at: set_titlebar
This is done in the same way as in the :ref:`previous part <Adaptive_UI_tutorial_part1>`
of this tutorial.
Using a Leaflet
~~~~~~~~~~~~~~~
The contents of the window itself are managed by a ``Leaflet`` from the `libhandy
library`_ which is created with two default attributes that describe how
transitions are performed -- these enable animations for layout changes inside
the window:
.. literalinclude:: apps/part2/src/main.py
:language: python3
:start-at: Handy.Leaflet
:end-at: )
The ``Leaflet`` widget is a container that manages the dimensions of its
children. We divide these into two pages that can be displayed side-by-side if
there is enough space, or are otherwise shown individually and can be flipped
between when the leaflet is *folded*.
Composing the Pages
~~~~~~~~~~~~~~~~~~~
We define the left page first, using a ``Gtk.Box`` that contains a label and a
button:
.. literalinclude:: apps/part2/src/main.py
:language: python3
:start-at: page1 =
:end-at: pack_start(more_button
The right page is defined in a similar way, with a label and button inside a
container:
.. literalinclude:: apps/part2/src/main.py
:language: python3
:start-at: page2 =
:end-at: pack_start(back_button
The buttons in these pages will be connected later in the method.
Adding Pages to the Leaflet
~~~~~~~~~~~~~~~~~~~~~~~~~~~
With the two pages defined, we add them to the leaflet using the standard ``add``
method:
.. literalinclude:: apps/part2/src/main.py
:language: python3
:start-at: self.content_leaflet.add
:end-at: window.add
We also add the leaflet to the window to ensure that it is shown.
Binding Properties and Connecting Signals
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Since the pages can be shown side-by-side, we want to hide the buttons in that
situation so that they are not confusing for users.
We bind each button's ``visible`` property to the ``folded`` state of the
content leaflet. This ensures that the button will only appear when the
leaflet is folded:
.. literalinclude:: apps/part2/src/main.py
:language: python3
:start-at: content_leaflet.bind_property
:end-at: back_button.connect
We also connect the ``clicked`` signal for each button to a method that will
show the page associated with that button. The widget is supplied as the last
argument to each connection.
Setting a Default Size
~~~~~~~~~~~~~~~~~~~~~~
Finally, we show the window and its contents:
.. literalinclude:: apps/part2/src/main.py
:language: python3
:start-at: window.set_default_size
:end-at: window.show_all
Note that we define a default size for the window. This helps to inform the
leaflet about its initial state when the window is shown -- whether it should be
folded or expanded. Without this information the buttons may appear when the
leaflet is expanded.
Handling Button Clicks
~~~~~~~~~~~~~~~~~~~~~~
The last thing to define is the ``show_page`` method that responds to the
``clicked`` signals from the buttons:
.. literalinclude:: apps/part2/src/main.py
:language: python3
:start-at: def show_page
:end-at: self.content_leaflet
Here, the page supplied as an argument is passed to the leaflet so that it
can be shown. The leaflet performs the transition defined earlier in order to
display it. Since a ``slide`` transition was specified, the current page slides
smoothly away and the new page slides into its place.
Running the Application
-----------------------
See the :ref:`Adaptive_UI_tutorial_building` and :ref:`Adaptive_UI_tutorial_packaging`
sections for information about building, packaging and running the application.
Summary
-------
This part of the tutorial showed the use of the ``Handy.Leaflet`` widget to
provide an adaptive container for the contents of a window, showing as much as
possible if there is enough space, and otherwise presenting its contents as a
foldable stack of pages.
Each page is defined as a ``Gtk.Box`` then added to the leaflet using the
leaflet's ``add`` method. Connections from buttons in the pages to a method
allow the current page to be changed on a button click, using the leaflet's
``set_visible_child`` method.
By binding the ``folded`` property of the leaflet to the ``visible`` properties
of the two buttons, we ensure that they are only visible when the leaflet is
folded.
In the next part of this tutorial we will use two leaflets to synchronize the
window's title bar and its visible contents.
.. include:: /links.txt
.. |app-name| replace:: Title Bar
.. |app-dir| replace:: ``part1``
.. |manifest| replace:: ``com.example.title_bar.json``
.. |manifest-path| replace:: apps/part1/com.example.title_bar.json
.. |app-id| replace:: com.example.title_bar
.. _Adaptive_UI_tutorial_packaging:
.. include:: ../common/Packaging_the_Apps.txt
.. _Adaptive_UI_tutorial_part1:
Part 1: Using a Title Bar
=========================
.. contents::
:local:
Overview
--------
When run, the application described in this part of the tutorial simply shows a
window with a title bar containing some text.
.. image:: images/title-bar.png
:scale: 25%
:align: center
:alt: A screenshot of the application running in the phone environment
The main source code for the application can be found in the ``main.py`` file
within the ``src`` directory. The purpose of the other files is explained in
other tutorials, such as the :ref:`First_Application` tutorial.
The Program
-----------
Because the application is very simple, we show the whole main program here to
provide an overview before looking at the details:
.. literalinclude:: apps/part1/src/main.py
:language: python3
Much of the is very similar to other examples and tutorials. We will focus on
the parts that are specific to this example.
Relevant Modules
~~~~~~~~~~~~~~~~
The program begins by importing the modules it needs to create a user
interface. In addition to the standard ``Gtk`` module we use the ``Handy``
module in order to access the widgets of the `libhandy library`_:
.. literalinclude:: apps/part1/src/main.py
:start-at: Handy
:end-at: Handy.init
:language: python3
When importing the ``Handy`` module, it is important to specify the version of
the API that will be used.
Creating the User Interface
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``Application`` class provides the usual methods to set up the application
and perform tasks when it is run. In the ``do_activate`` method we set up the
user interface:
.. literalinclude:: apps/part1/src/main.py
:start-at: do_activate
:end-at: set_titlebar
:language: python3
We use a ``Handy.TitleBar`` as the title bar instead of directly using a
``Gtk.HeaderBar`` widget. ``Handy.TitleBar`` makes it possible to use animations
between headers in the title bar, although in this case we only use a single
``Gtk.HeaderBar`` widget.
We also create a label to make the window more interesting, and we add it to
the window itself:
.. literalinclude:: apps/part1/src/main.py
:start-at: label =
:end-at: show_all
:language: python3
The window is shown using the ``show_all`` method so that both the window and
its contents are displayed.
Running the Application
-----------------------
See the :ref:`Adaptive_UI_tutorial_building` and :ref:`Adaptive_UI_tutorial_packaging`
sections for information about building, packaging and running the application.
Summary
-------
This part of the tutorial showed how a ``Handy.TitleBar`` widget to hold a
standard ``Gtk.HeaderBar``. The ``Handy.TitleBar`` is set as the title bar
instead of setting the ``Gtk.HeaderBar`` directly.
It is not necessary to use a ``Handy.TitleBar`` to hold a single widget.
However, by using one, we lay the foundations for later parts of the tutorial.
.. include:: /links.txt
.. _`application property`: https://developer.gnome.org/gtk3/stable/GtkWindow.html#GtkWindow--application
.. _`Application section`: https://python-gtk-3-tutorial.readthedocs.io/en/latest/application.html
.. _Adaptive_UI_tutorial_part3:
Part 3: Synchronizing Two Leaflets
==================================
.. contents::
:local:
Overview
--------
When run, the application described in this part of the tutorial shows a window
with two leaflets. One is used for the title bar, the other for the window
contents. When the window is resized or updated, the leaflets adapt and remain
in sync with each other.
.. |image1| image:: images/two-leaflets1.png
:scale: 25%
:alt: A screenshot of the application running in the phone environment
.. |image2| image:: images/two-leaflets2.png
:scale: 25%
:alt: A screenshot of the application running in the phone environment
.. centered:: |image1| |image2|
The main source code for the application can be found in the ``main.py`` file
within the ``src`` directory. The purpose of the other files is explained in
other tutorials, such as :ref:`First_Application`.
Design
------
We begin by looking at an overview of the window layout we want to create.
.. figure:: images/two-leaflets.svg
:width: 50%
:align: center
On the left is a title above a left page containing a button. On the right is a
subtitle containing a back button above a right page. When there is enough space
to display both pages we want to hide the buttons; otherwise, we will show them
so that the user can navigate between the pages.
The Program
-----------
We will not show all the main program here. Instead, we will focus on the parts
that have changed since the :ref:`previous part <Adaptive_UI_tutorial_part2>` of
this tutorial.
As before, almost everything is done in the ``do_activate`` method of the
``Application`` class where the widgets that make up the user interface are
organized using leaflets:
.. figure:: images/two-leaflets-leaflets.svg
:width: 70%
:align: center
As well as providing features for adaptive user interfaces, these two leaflets
also help us to structure the code. First, we create a leaflet to hold the
components of the title bar, then we create a leaflet to manage the main
contents of the window.
Creating a Title Bar
~~~~~~~~~~~~~~~~~~~~
In the ``do_activate`` method we create a application window and a title bar in a similar way to the :ref:`previous part <Adaptive_UI_tutorial_part2>` of this
tutorial. However, in this case, the title bar contains **two** ``Gtk.HeaderBar``
widgets instead of one, both contained within a ``Handy.Leaflet`` widget that we
refer to as the *title leaflet*:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: Handy.TitleBar
:end-before: header = Gtk.HeaderBar
The first header bar contains the main heading for the window and a close button:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: HeaderBar
:end-at: )
The second header bar contains a close button, but it is allowed to expand horizontally:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: sub_header
:end-at: sub_header.add
It also contains a back button that is not part of the normal collection of
window decorations, which we define separately. The subheader is allowed to
expand horizontally to fill any available space -- this causes the close button
to be placed in the correct position when the window is fully expanded.
We call the ``add`` method to add the header and subheader to the leaflet,
calling the ``child_set`` method to give them names that the leaflet can use to
refer to them:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: title_leaflet.add
:end-at: child_set(sub_header
We can then add the leaflet to the title bar and make the window use it:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: title_bar.add(
:end-at: set_titlebar
One final task remains for the title leaflet. We need to place the two header
bars in a ``Handy.HeaderGroup`` so that their layout and visibility can be
managed:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: header_group
:end-at: add_header_bar(sub_header)
This ensures that, for example, we do not see two close buttons when the window
is expanded.
Composing the Pages
~~~~~~~~~~~~~~~~~~~
The contents of the window itself are also managed by a ``Handy.Leaflet`` that
we refer to as the *content leaflet*:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: content_leaflet
:end-at: )
We define the left page first, using a ``Gtk.Box`` that contains a label and a
button similar to the ones in the previous part of this tutorial:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: page1 =
:end-at: pack_start(more_button
The right page is defined in a similar way, except that there is no need for a
button in the page for navigation because the back button in the subheader is
used for that purpose:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: page2 =
:end-at: pack_start(more_label
We use a ``Gtk.Box`` to hold the label so that we can control its placement in
the page.
Both pages are created with their ``hexpand_set`` properties set to ``True``.
This allows them to expand horizontally if their child widgets require more
space.
Adding Pages to the Leaflet
~~~~~~~~~~~~~~~~~~~~~~~~~~~
With the two pages defined, we add them to the leaflet in the same way as
before, assigning names to them with the ``child_set`` method before adding the
leaflet to the window:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: self.content_leaflet.add
:end-at: window.add
Note that we use the *same names* to refer to the two pages as we used for the
headers: ``left_page`` and ``right_page``. This helps us to synchronize the two
leaflets.
Aligning Headers and Content
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We also want to make sure that each header is horizontally aligned with the
content it is associated with.
.. figure:: images/two-leaflets-size-groups.svg
:width: 50%
:align: center
This is done by placing each header in a ``Gtk.SizeGroup`` with its
corresponding page widget. The main header with the first page, the subheader
with the second:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: left_page_size_group
:end-at: add_widget(page2)
Because each size group is used in ``horizontal`` mode, these place constraints
on the widgets in each group to ensure that their widths are synchronized.
Binding Properties and Connecting Signals
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We bind the ``visible-child-name`` property of the content leaflet to the
corresponding property in the title leaflet, causing them to be synchronized:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-after: Ensure that the header is updated when the page changes.
:end-at: )
This ensures that the appropriate header is always shown in the title bar for
the current page. It is also why we define names for the pages when we added
them to the leaflets.
Since the pages can be shown side-by-side, we want to hide the buttons in that
situation so that they are not confusing for users. To do this, we bind each
button's ``visible`` property to the ``folded`` state of the leaflet it is
associated with:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: title_leaflet.bind
:end-before: End of property bindings
This ensures that the buttons will only appear when the leaflets are folded.
There is no need to show either of the buttons when the window is fully expanded.
We also connect each button's ``clicked`` signal to a method that will show the
appropriate page when the user clicks it:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: back_button.connect
:end-at: more_button.connect
The connections only have an effect when the buttons are shown. It is not
possible for the user to click them when the leaflets are folded.
Setting a Default Size
~~~~~~~~~~~~~~~~~~~~~~
At the end of the ``do_activate`` method, we show the window and its contents:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: window.set_default_size
:end-at: window.show_all
We assign a default size to the window to help inform the leaflets about the
amount of available space to expect.
Turning the Page
~~~~~~~~~~~~~~~~
The last thing to define in the ``Application`` class is the ``show_page``
method that responds to ``clicked`` signals from the buttons:
.. literalinclude:: apps/part3/src/main.py
:language: python3
:start-at: def show_page
:end-at: self.content_leaflet
Here, the page supplied as an argument is passed to the content leaflet so
that it can be shown.
Although we change the ``visible-child`` property to change the current page,
the ``visible-child-name`` property will also be updated. This means that the
property binding that we set up earlier will ensure that the title leaflet will
also be updated, keeping the leaflets synchronized.
Running the Application
-----------------------
See the :ref:`Adaptive_UI_tutorial_building` and :ref:`Adaptive_UI_tutorial_packaging`
sections for information about building, packaging and running the application.
Summary
-------
This part of the tutorial showed how two ``Handy.Leaflet`` widgets can be
connected together to ensure that the ``Gtk.HeaderBar`` shown in a window's
title bar is synchronized with the corresponding widget in the window's main
area.
We add two ``Gtk.HeaderBar`` widgets to a leaflet in the title bar, and we
add two ``Gtk.Box`` widgets to a leaflet in the window's main area. Each leaflet
is responsible for showing one or both widgets it contains, depending on the
amount of space available.
We use a ``Handy.HeaderGroup`` to manage the header bars, avoiding problems with
duplicate window decorations, and we use ``Gtk.SizeGroup`` objects to keep
headers and pages aligned and sized correctly.
Each header bar is assigned a name using the title leaflet's ``child_set``
method. The corresponding page is assigned the same name using the content
leaflet's ``child_set`` method. By binding the content leaflet's