The introduction explains how an internal execution thread draws the application windows, and processes keyboard and pointer clicks, by invoking various callbacks in response to them. Furthermore, the section called “Callbacks” explains that callbacks should not engage in lengthy activities, but notify the main application execution thread before returning, so that the internal execution thread can proceed with its chores.
Another pointer or keyboard click results in the appropriate callback getting invoked again even if the main application execution thread is still busy with its previous task. The main application thread won't get around to processing the next message from the callback until it's done. This does not result in optimal user experience. The application can also maintain an internal status indication of some kind, that the callbacks can check to see if it's busy with its current task and not take any action.
This chapter gives an example of a different way to stop keyboard
and pointer processing. One of the parameters to callbacks is
x::w::busy
:
#include <x/w/busy.H> // ... button->on_activate ([] (ONLY IN_THREAD, const x::w::callback_trigger_t &trigger, const x::w::busy &get_busy) { // ...
Calling one of x::w::busy
's methods returns an
opaque “mcguffin”, a generic reference-counted object,
x::ref<x::obj>
.
Mcguffins are a design pattern from the
base LibCXX library.
The internal execution thread ignores button and keypress event while
the mcguffin object exists, but continues to update and redraw the
application window.
No explicit action is needed to resume input processing, which occurs automatically when the mcguffin's object gets destroyed. The sequence of events is as follows:
The internal connection thread executes the callback.
The callback acquires
a busy mcguffin, and puts the
x::ref<x::obj>
as part of a message to the main application thread, or starts
a new execution thread and passes the mcguffin to the new
execution thread.
The callback returns and the internal execution thread continues.
Because the mcguffin gets included in the message sent to the main thread, or a new thread, the mcguffin object remains in existance. The main application thread typically receives the message and the mcguffin as one of its parameters, and does what it needs to do.
When it's done, the main application thread discards the original message, or a new execution thread terminates. This removes the last reference to the mcguffin. The mcguffin gets destroyed and the library's internal execution thread resumes the usual processing of button or keypress events.
busy.C
gives a general example of this approach:
/* ** Copyright 2017-2021 Double Precision, Inc. ** See COPYING for distribution information. */ #include "config.h" #include <x/mpobj.H> #include <x/appid.H> #include <x/exception.H> #include <x/destroy_callback.H> #include <x/ref.H> #include <x/obj.H> #include <x/threadmsgdispatcher.H> #include <x/w/main_window.H> #include <x/w/button.H> #include <x/w/gridlayoutmanager.H> #include <x/w/gridfactory.H> #include <x/w/text_param_literals.H> #include <x/w/font_literals.H> #include <x/w/screen.H> #include <x/w/connection.H> #include <string> #include <iostream> #include <errno.h> #include <poll.h> #include <chrono> std::string x::appid() noexcept { return "busy.examples.w.libcxx.com"; } typedef x::ref<x::obj> mcguffin_t; typedef x::ptr<x::obj> mcguffinptr_t; ///////////////////////////////////////////////////////////////////////////// // // An example of handling user events in a message-based thread to handle // user UI events. // // Use LibCXX library's x::threadmsgdispatcherObj framework. The // busy.xml file defines two messages for this thread. A stylesheet generates // an #include file, that we pull in below, that generates skeleton code to // implement two methods in this object, were_busy() and // window_close_button_pressed(). // // These method send a message into the internal message dispatcher queue. // When processed, the include file provides declarations of // dispatch_were_busy() and dispatch_close_button_pressed(), which get // implemented below, to process the messages. class busythreadObj : public x::threadmsgdispatcherObj { public: busythreadObj(); ~busythreadObj(); // Invoked from the main program to execute the mainloop() directly. void run_directly(const x::w::main_window &main_window); // The official entry-point for a new, real, execution thread that // uses x::start_threadmsgdispatcher. void run(x::ptr<x::obj> &startup_mcguffin); #include "busy.inc.H" private: // The main message processing loop. void mainloop(msgqueue_auto &); // The "window_close_button" message clears this flag, and mainloop() // stops. bool is_running; // The mcguffin sent via the "were_busy" message gets stored here, // and after five seconds this object gets removed. This will be // the last reference to the mcguffin, and its destruction then // re-enables keyboard and mouse processing. // // Note that this is a pointer to an object inside run(). This is // not strictly necessary, but is extra "insurance". This can be an // ordinary class member, but if run() returns for some reason, the // mcguffin will still remain in the class instance, and if the // main application thread still has its own reference to this // busythread instance the mcguffin does not get destroyed. // // This way if run() terminates for some reason, the mcguffin gets // automatically destroyed as well. "currently_busy" is accessed only // in run(). mcguffinptr_t *currently_busy; // The clock that tells us when the five seconds expire. typedef std::chrono::steady_clock busy_clock_t; busy_clock_t::time_point busy_until; }; busythreadObj::busythreadObj()=default; busythreadObj::~busythreadObj()=default; // A were_busy message was dispatched. // Take the mcguffin from the message, and stash it away in our object. // Note that this can be invoked only from run(), indirectly, as part of // the event(). void busythreadObj::dispatch_were_busy(const mcguffin_t &mcguffin) { *currently_busy=mcguffin; // And start the clock running. busy_until=busy_clock_t::now() + std::chrono::seconds(5); } // A window_close_button_pressed() message was dispatched. void busythreadObj::dispatch_window_close_button_pressed() { // Make the mainloop() finish things up. is_running=false; } // Direct invocation. void busythreadObj::run_directly(const x::w::main_window &main_window) { msgqueue_auto q{this}; // Show the main window. This must be done after constructing // the msgqueue_auto. For more details see LibCXX base library's // documentation. Showing the main application window starts // processing of messages from the display server. If this is done // before run_directly() gets called, from the main function below, // there's a theoretical possibility that one of the callbacks sends // a message to this thread before the internal message queue gets // initialized, resulting in message loss. No memory leak will result, // just the click gets quietly ignored. // // Showing the main window only after our message queue gets // constructed prevents this theoretical race condition from // happening. main_window->show_all(); mainloop(q); } void busythreadObj::run(x::ptr<x::obj> &startup_mcguffin) { // And when we're using x::start_threadmsgdispatcher, and start a // real execution thread, we just construct the msgqueue_auto, and // release the startup mcguffin. x::start_threadmsgdispatcher() // returns in the main execution thread, which then proceeds to // show the main window. msgqueue_auto q{this}; startup_mcguffin=nullptr; mainloop(q); } void busythreadObj::mainloop(msgqueue_auto &q) { // Note that "is_running" gets updated here, and in // dispatch_window_close_button_pressed(). The dispatch function // gets called by this execution thread, from event(), so this // class member is only accessed by this execution thread (or the // "pseudo"-execution start, if we run_directly()), so there are // no multithreading-related issues with is_running, and there's no // need for any mutexes or condition variables. is_running=true; mcguffinptr_t currently_busy_instance; currently_busy= ¤tly_busy_instance; // Take the threadmsgdispatcher-provided event queue, and put its // event file descriptor in non-blocking mode, so we can handle // poll()ing in order to support the busy mcguffin's timer // expiration. auto eventfd=q->get_eventfd(); struct pollfd pfd[1]; pfd[0].fd=eventfd->get_fd(); eventfd->nonblock(true); while (is_running) { // If a message was received, dispatch the message. if (!q->empty()) { try { q.event(); } catch (const x::exception &e) { // Report any exceptions. e->caught(); } continue; } // poll() either until the event file descriptor fires // (indicating a new message in threadmsgdispatcher's // message queue), or until the busy mcguffin expires, // if we have one. int expiration= -1; if (*currently_busy) { auto now=busy_clock_t::now(); if (now >= busy_until) { // busy mcguffin's timer has expired. Drop our // ref on the mcguffin object. Mcguffin's // destruction re-enables input processing. *currently_busy=nullptr; } else { // compute when the mcguffin expires, so we // go back here when that happens. expiration=std::chrono::duration_cast <std::chrono::milliseconds> (busy_until-now).count(); if (expiration == 0) // Edge case, CPU spikes. expiration=1; } } pfd[0].events=POLLIN; if (poll(pfd, 1, expiration) < 0) std::cerr << "poll: " << strerror(errno) << std::endl; } } // Main window's creator, factored out for readability. static inline void create_main_window(const x::w::main_window &main_window, const x::ref<busythreadObj> &mythread) { auto layout=main_window->gridlayout(); x::w::gridfactory factory=layout->append_row(); // Create two buttons. Install a callback that executes when // the button gets selected. // // Button callbacks received two parameters. Of interest is the // second one, x::w::busy. Calling its methods creates and returns // a "mcguffin". An opaque reference-counted handle for an object, // x::ref<x::obj>. As long as this object exists, the library does // not process any keyboard or pointer clicks, but continues to // handle all other display server messages (redraws, resizing, // etc...) // // We take the mcguffin, and send it to our own pseudo-"thread". This // keeps the underlying in existence. The thread takes the handle // stashes it away, then five seconds later gets rid of it, which // reenables input processing. factory->create_button ({"Shade"})->on_activate ([mythread] (ONLY IN_THREAD, const x::w::callback_trigger_t &ignore, const x::w::busy &get_busy) { mythread->were_busy(get_busy.get_shade_busy_mcguffin()); }); factory=layout->append_row(); factory->create_button ({"Pointer"})->on_activate ([mythread] (ONLY IN_THREAD, const x::w::callback_trigger_t &ignore, const x::w::busy &get_busy) { mythread->were_busy(get_busy.get_wait_busy_mcguffin()); }); } void busy() { x::destroy_callback::base::guard guard; // Create a mythread object. auto mythread=x::ref<busythreadObj>::create(); auto main_window=x::w::main_window ::create([&] (const auto &main_window) { create_main_window(main_window, mythread); }); main_window->set_window_title("Very busy!"); guard(main_window->connection_mcguffin()); main_window->on_disconnect([] { _exit(1); }); main_window->on_delete ([mythread] (ONLY IN_THREAD, const auto &ignore) { mythread->window_close_button_pressed(); }); // We use LibCXX base library's threadmsgdispatcher framework to // base our mythreadObj on. Typically one uses // x::start_threadmsgdispatcher() to start a new execution thread to // run() the object. // // We can do that, of course, but there's no real need, in this // simple example, so we can run_directly(). #if 1 mythread->run_directly(main_window); #else // But if we insist on doing it the long way, we'll start the // execution thread. auto thread=x::start_threadmsgdispatcher(mythread); // Now that the execution thread was started, we can show() the // main window. main_window->show_all(); // And just wait for the thread to terminate. thread->wait(); #endif } int main(int argc, char **argv) { try { busy(); } catch (const x::exception &e) { e->caught(); exit(1); } return 0; }
busy.C
uses several classes, templates, and tools
from the base LibCXX library.
x::threadmsgdispatcherObj
is a design pattern for a message-oriented execution thread, with
a stylesheet that uses a simple XML file that
defines messages, and generates some skeleton code to implement each
message as one class method that sends a message to the execution thread,
and a second method that the execution thread runs, the dispatch method,
that receives it. The
Makefile in the examples
directory runs the stylesheet to create busy.inc.H
that gets pulled into the class definition.
busy.C
creates a window with two buttons.
Pressing either of the two buttons stops the window from reacting to
any additional button clicks, or keys (the only keys that do anything
here are the tab keys that manually switch the input focus between the
buttons, but this is sufficient for demonstration purposes) for
five seconds.
One of the two buttons' callbacks uses
get_shade_busy_mcguffin
(), and the other one
uses get_wait_busy_mcguffin
():
[mythread] (ONLY IN_THREAD, const x::w::callback_trigger_t &ignore, const x::w::busy &get_busy) { mythread->were_busy(get_busy.get_wait_busy_mcguffin()); }
The callbacks call one of these two methods, and call the
were_busy
() from the thread dispatcher object,
which send a message to the object's thread, that includes the
acquired mcguffin as part of the message, then returns to the library's
execution thread.
Both
get_shade_busy_mcguffin
() and
get_wait_busy_mcguffin
() produce mcguffins
that block button and key press event processing during their
existence.
get_shade_busy_mcguffin
()
draws a theme-specified shade over the entire application window to
make it look washed out and indicate that it's inactive, and
get_wait_busy_mcguffin
()
changes the window's pointer to a “please wait” shape.
This provides visual indication that the main application is running.
The execution thread object receives and dispatches the message, and arranges for the mcguffin to exist for five seconds before destroying it, at which point the window's appearance gets restored to normal.