for the time being, this is a loose collection of Conventions and Policies deemed adequate for work on the Lumiera UI-Layer |
The UI is loaded as Plug-In, which instantiates a GtkLumiera
object.
All of the UI activity happens within the blocking function GtkLumiera::run()
.
This UI activity boils down to run the GTK event loop; we use a dedicated thread,
and all of the GUI thus performs single-threaded and need not be thread-safe.
All UI activities thus need to be short running and with deterministic outcome.
It is strictly prohibited to spawn other threads from within the event loop.
We hold up a strict distinction between the UI mechanics and the core editing
concerns. The latter must not be implemented within the UI-Layer. Rather, you need
to send an act(CommandID)
message over the UI-Bus, which causes the corresponding
Steam-Layer command script to be dispatched within the Session thread. It is good
practice to give an immediate visual clue regarding the fact of sending such
a command (e.g. pressing a button). But be prepared that the actual feedback
of invoking a command happens asynchronously.
In short: there is no dedicated UI-Model. Stateful working data goes directly
into the widgets. However, some widgets or controllers are special, insofar they
correspond to and reflect some entities within the Session model. These special
entities must be implemented as subclasses of stage::model::Tangible
. These are
always connected to the UI-Bus, and they are populated and mutated by receiving
diff messages pushed up from the Steam-Layer.
There is an inner circle of UI backbone services. These are all mutually dependent
on each other and their start-up sequence is intricate. Implement global and cross-cutting
concerns here. In case some widget or controller needs access to such a service, then you
should prefer using lib::Depend<ServiceInterface>
, and install the corresponding service
via lib::DependInject<ServiceInterface>::ServiceInstance<ServiceImpl>
.
A Parent Container is an object managing a collection of children, with respect to
the structure of tangible UI elements. Irrespective if this container is also a
Gtk::Container
widget, or “just” a controller. The key point to turn something
into a Parent Container is the fact that this entity creates the children
in response to a (population) diff message.
a Parent Container has to wire each child properly, so to enable the adequate use of the UI element protocol. This includes
to ensure the child is attached to the UI-Bus (usually enforced by ctor call)
to install a suitable Expander
functor if the child supports expand/collapse
to install a suitable Revealer
when it is relevant for the child to be brought
into sight in response to some message (e.g. to indicate error state).
whenever a child is detached from the container, you need to ensure it is destroyed
right away, before there is any chance of processing some other UI event.
[
Invoking the destructor triggers a lot of magic here, especially it causes the child
to be properly detached from signals. We want to ensure this happens as soon as the
child is taken out of service. Do not use an “can collect garbage later” approach.]
Basically, Signals are just typed callback functors. However, the Sigc\++ library
helps to deal with the inherent danger of dangling references, and it allows to
manage and disconnect signal attachments. Thus, whenever cross wiring beyond the
given model structure can be expected within the UI, usage of sigc::signal<..>
should be preferred.
by itself, sigc::signal is a lightweight ref-counting smart-pointer. |
the names of signals are prefixed by the word signal
.
whenever a signal is used over several widgets and components,
there should be a typedef for the signal type,
e.g. using SignalBarf = sigc::signal<bool,Booh&>;
it is perfectly fine for a signal to be just a public member of some component, especially when the sole purpose of that signal is for someone else to connect or invoke it. However, when a signal is meant to be an internal implementation detail, then better make it private, optinally exposing it via accessor function.
a function intended to be connected to a signal is termed as “Slot”, and its
name should be prefixed by the word slot
, e.g. bool slotBarf (Booh& moo);
such a slot function should be noexcept
Be aware that GTK is written in C. And while GTKmm has some safeguards in place, better be sure no exception can emanate from event handling code.
TODO: probably we’ll need a common wrapper to do so… |
GTK is massive and compilation times tend to be problematic in the UI-Layer. Thus you should be diligent with the organisation of includes. Try to confine the more heavyweight includes within the implementation translation units. Use forward declarations and PImpl if possible. However, it is fine to write a mere implementation widget in header-only fashion, in case this improves the locality and readability of code.
using namespace Gtk and similar wildcard includes are prohibited. |
We are forced to observe a tricky include sequence, due to NoBug’s ERROR
macro,
and also in order to get I18N right.
Thus any actual translation unit should ensure that effectively the first include
is that of stage/gtk-base.hpp.
[you need not include it literally. It is
perfectly fine if you can be sure the first included header somehow drags in
gtk-base.hpp before any other header.]