some insights regarding the start-up of GTK and related framework aspects
As outlined on the overview page, we need to discern between
toolkit aspects, and GTK-the-framework. Moreover, we use GTK though the C++ bindings (gtkmm
), which also
add a thin layer of software abstractions.
Initialisation
So in order to “use GTK” …
-
we need to build our application against the appropriate headers of GTK--, which also implies to install the corresponding development packages (or a source tree) of the libraries involved.
-
when our main application decides to use the GTK-UI, it loads the GUI plug-in — at this point the dynamic loader also requires and loads the runtime libraries of GTK, Glib, GDL, Cairo, Pango, plus the runtime libraries of the C++ wrappers (Gtkmm, Glibmm, Gdlmm)
-
at this point, the various layers of software comprising “the GTK” will require some very specific initialisation hooks to be invoked. Especially
-
GTK itself requires the invocation of
gtk_init()
in order to attach to the windowing system. This might also parse additional framework specific command lines and react on some environment variables, init the gettext library and maybe activate interactive UI debugging. But nothing beyond that -
the
gtkmm
C++ wrappers deal with a lot of additional ceremony, required by the plain-C implementation of the GTK core. Especially, they automatically register any “object type” with the GObject-system of Glib. Moreover, some virtual function tables will be populated and several “object functions” and signals need to be wrapped, so client code can invoke the more convenient C++ equivalents. All of this additional initialisation is effected by invokingGtk::Main::init_gtkmm_internals()
or the (equivalent) static function withinGtk::Application
-
finally, any features of GTK-the-framework need to be initialised and prepared for use:
-
lifecycle events need to be issued. GTK uses “signals” for this purpose
[not to be confused with the signals on operating system level]
-
registration of document types with the desktop environment
-
the application instance will connect to the D-bus and install the necessary callbacks, possibly even waiting for an “activation event”
-
depending on the specific way of activation, behaviour and error response of the application instance need to be controlled in distinct ways — maybe just rendering content or even silently terminating altogether after a timeout has passed
-
-
For reasons outlined above,
Lumiera does not need nor use a Gtk::Application
.
[In fact, most of the framework
functionality is actually handled within the base class Gio::Application
, which corresponds
to the plain-C type GApplication
.]
Thus, we leave out the third stage and deal with all those
application global aspects through means appropriate to our specific purpose.
-
our Subsystem runner boots the UI subsystem (
stage::GuiFacade
), which in turn loads the UI-plug-in (target/modules/gtk_gui.lum). -
within this plug-in, the class
stage::GtkLumiera
governs the complete UI lifecycle. -
this class holds a member
stage::ctl::UiManager
— which is our “UI main object” and mimics the design of the lateGtk::Main
. -
we inherit from our own
stage::ctrl::ApplicationBase
class, to ensure invocation of all the initialisation and clean-up functions required by GTK -
we instantiate a class
stage::ctrl::GlobalCtx
to hold and provide all global services used throughout the UI layer, including population of the menu and global actions -
within the global context, there is also
stage::ctrl::WindowLocator
, to keep track of all top-level application windows, and to support direct navigation to relevant entities within the UI, based on abstracted UI-coordinates. -
finally,
GtkLumiera
invokes the functions-
UiManager::createApplicationWindow()
to create the visible interface frame -
UiManager::performMainLoop()
to activategtk_main()
— the blocking GTK event loop
-
Running a GTK application
The event processing is what makes an UI application “live”. However, event processing must not be confused with graphical presentation. In GTK, it is perfectly valid to create and even show a widget prior to entering the event loop. Yet it is a very fundamental design decision within GTK to operate all of the UI concerns synchronously, from within a single dedicated UI thread. We consider this a very wise design decision, and expand it to all of Lumiera’s UI concerns, including the UI-Bus; any “interaction mechanics” has to happen within this single thread, and is completely decoupled from all other application functionality like editing actions and rendering, which happen in separate threads and respond asynchronously by dispatching their reactions back into the UI event thread.
Due to the shifted scope between the old-style Gtk::Main
and the corresponding gtk_main()
C-function on one side, and the newer, more framework-centric Gtk::Application
, it might seem
on first sight, that both will perform different tasks. However, a closer look reveals that
during the redesign for GTK-3, the old “main” functionality has been retrofitted to rely
on the newer, more generic GIO-Framework. More specifically
-
the classical
gtk_main()
without much ado invokesg_main_loop_run()
, which in turn immediately starts to “pull” the GIO main context by repeatedly invokingg_main_context_iterate()
-
whereas the
Gtk::Application::run()
invokesGio::Application::run()
and from there the new application-level main functiong_application_run(gobj(), argc, argv)
. Which in turn handles all the above mentioned framework concerns (e.g. D-Bus registration / activation). Then it entersg_main_context_iteration()
which — similar tog_main_loop_run()
— starts “pulling” the GIO main context by repeatedly invokingg_main_context_iterate()
Thus, once we reach the actual operative state, both paths of activating a GTK application behave
essentially the same.
[This is the state of affairs during the GTK-3 lifetime cycle,
as verified in 8/2018 based on the source code of GTK 3.22
. Note though that GTK-4 is “around
the corner” — let’s see what awesome innovations we have to face then…]
The application activation signal
However, the old-style approach seems to lack a feature offered by Gio::Application
:
the activation signal, which is a feature solely present at the “application-framework” level
and used internally for the convenience function to “run an ApplicationWindow object”
(→ Gtk::Application::run(Gtk::Window&)
). In fact, this happens to be a lifecycle event,
and can be used by connecting a SigC++ slot to the application object’s signal_activation()
.
Outside the realm of GTK-the-framework this feature turns out to be not so useful; especially, the
signal is emitted shortly before entering the event loop, and not from within (as you’d might expect).
A better alternative is to rely on the Glib::signal_idle()
rsp. Glib::signal_timeout()
. Both allow
to invoke a slot only once, and both ensure the invocation really happens from within the event loop.
[Verified as of 8/2018: uses g_main_context_default()
, which creates the default context
on demand. This is also the default context pulled when running the event loop. So indeed we’re
able to attach an “event source” (i.e. an action or closure) even before the loop is running.
Use Glib::PRIORITY_LOW
to schedule after any (re)drawing activities.]
SigC++ trackable
Any closure or callback based system of wiring and activation suffers from the inherent danger of invoking
a dangling reference. Within an interactive UI environment, this problem becomes quite acid, since widgets
will be created and destroyed through arbitrary interactions, yet still need to be connected to “live state”.
When building UI applications with Gtkmm (the C++ wrapper of GTK), this problem is dealt with by inheriting
all widget classes from the sigc::trackable
mix-in. This mechanism automatically detaches a signal slot
when the corresponding target widget goes out of scope. However, this solution only works reliably when
all slots are created and connected from within a single thread (the UI event thread!). Moreover, we
can not possibly “track” a slot created from a C++ language lambda or functor — which sometimes
is even the only option, unless we want to incur a dependency on the SigC++ library. In Lumiera,
we have a strict policy to prohibit any dependency on GTK libraries outside the UI layer.
Shutdown
The GTK main loop terminates after invocation of ‘gtk_main_quit()` (or in case of serious internal errors).
Typically, this function is somehow bound to a widget interaction, like clicking on the “close” button.
In Lumiera, this concern is managed by the stage::ctrl::WindowLocator
, which keeps track of all top-level
windows and terminates the UI when the last one is closed. Moreover, the UI can deliberately be closed
by sending an event over the `GuiNotification::triggerGuiShutdown(message)’ call.
After leaving the main loop, the external façade interfaces are closed. By general architectonic reasoning,
no event can be processed and no signal can be invoked any more — and thus we’re free to destroy all widgets,
services and the backbone of the UI. After that, the UI subsystem signals termination, which causes all other
subsystems to shut down as well.
[We are aware that there is a race between closing the façade and actually
ceasing other subsystem’s activities, which might cause those other activities to fail with an exception]