Chapter 10. The item layout manager

Index

Items
Using singleton objects
The item layout manager

The item layout manager implements a common UI design pattern in combination with an input field: the input field provides a free-form text entry for typing in a list of objects or entities. The typed-in text gets parsed, validated, and converted into one or more individual items. The parsed items get displayed above or below the input field. Each item gets drawn as a small button with a label inside it, and an X that removes the item from the list.

Passing an x::w::new_itemlayoutmanager to a factory's create_focusable_container() creates a container that uses the x::w::itemlayoutmanager. itemlayoutmanager.C gives an example of creating a focusable container managed by the item layout manager, and installing an on_validate() callback for a text input field above it. The callback takes the typed-in contents, splits them into a comma-separated text strings, trims off each string's leading and trailing whitespace, and creates an x::w::label from each string.

The item layout manager draws a button around each display element created by its factory. Its container adjusts to any width, so it's typically positioned above or below the text input field, in a parent container that uses the grid layout manager. This sizes the item layout manager's container to its column's full width; the item layout manager lays out all items in its container up to its horizontal width, and creates as many rows as needed to show all items:

/*
** Copyright 2018-2019 Double Precision, Inc.
** See COPYING for distribution information.
*/

#include "config.h"
#include "close_flag.H"

#include <x/exception.H>
#include <x/destroy_callback.H>

#include <x/w/main_window.H>
#include <x/w/gridlayoutmanager.H>
#include <x/w/gridfactory.H>
#include <x/w/itemlayoutmanager.H>
#include <x/w/label.H>
#include <x/w/input_field.H>
#include <x/w/container.H>
#include <x/w/focusable_container.H>
#include <x/w/canvas.H>

#include <x/singletonptr.H>
#include <x/strtok.H>
#include <x/join.H>
#include <x/mutex.H>
#include <courier-unicode.h>
#include <unordered_set>
#include <vector>

// The application object.

class appObj : virtual public x::obj {

public:

	// The main window
	const x::w::main_window main_window;

	// The input text field where toppings get typed in, as text
	const x::w::input_field toppings_text_field;

	// The container with the list layout manager.
	const x::w::focusable_container toppings_list;

	// The list of items in the container
	//
	// This object gets accessed by both the library execution thread and
	// the main execution thread, hence it's wrapped inside a mutex.
	//
	// This vector parallels the items in the toppings_list container.
	// Those item are display elements, and this vector is the corresponding
	// topping names.
	typedef x::mpobj<std::vector<std::string>> toppings_t;

	toppings_t toppings;

	// Constructor
	appObj(const x::w::main_window &main_window,
	       const x::w::input_field &toppings_text_field,
	       const x::w::focusable_container &toppings_list)
		: main_window{main_window},
		  toppings_text_field{toppings_text_field},
		  toppings_list{toppings_list}
	{
	}

	// Item layout manager callback that gets invoked in response to
	// the click on the closing "X". We remove the item label from the
	// container, and also update our toppings list.
	void remove_toppings(const x::w::itemlayout_lock &lock,
			     size_t i)
	{
		toppings_t::lock t_lock{toppings};

		if (i >= t_lock->size())
			throw EXCEPTION("Shouldn't happen");

		// Remove the item label, in the container, and update our
		// vector.
		lock.layout_manager->remove_item(i);
		t_lock->erase(t_lock->begin()+i);
	}

	void add_toppings()
	{
		// Something was entered into the toppings text input field.
		//
		// Retrieve the string, split it by commas, then clear the
		// contents.
		std::vector<std::string> words;

		{
			x::w::input_lock lock{toppings_text_field};

			x::strtok_str(lock.get(), ",", words);

			toppings_text_field->set("");
		}

		add_toppings(words);
	}

	void add_toppings(const std::vector<std::string> &words)
	{
		std::unordered_set<std::string> dupes;

		// We must obtain the item layout manager lock first, then
		// the lock on the toppings vector. When the internal
		// execution thread invokes the remove_toppings() callback,
		// the internal execution thread acquires the itemlayout_lock
		// first, then passes it to the callback, which them obtains
		// the lock on the toppings vector.
		//
		// We must use the same locking order here, to avoid the
		// deadlock.
		x::w::itemlayout_lock i_lock
			{toppings_list->get_layoutmanager()};

		toppings_t::lock t_lock{toppings};

		// Go through each word that was split out of the comma-
		// separated string.
		for (auto &w:words)
		{
			auto b=w.begin();
			auto e=w.end();

			// Trim off the leading and trailing whitespace.
			x::trim(b, e);

			// Convert each trimmed word to lowercase, and
			// capitalize the first letter.
			auto word=unicode::iconvert::convert_tocase
				({b, e},
				 unicode_default_chset(),
				 unicode_tc,
				 unicode_lc);

			if (word.empty())
				continue;

			// The toppings vector is sorted, find where this
			// topping name belongs, in the vector.
			auto iter=std::lower_bound(t_lock->begin(),
						   t_lock->end(), word);

			// Is this topping already entered?
			if (iter != t_lock->end() && *iter == word)
			{
				dupes.insert(word);
				continue;
			}

			// Insert this topic at this position both in the
			// toppings vector and at the same corresponding
			// position in the container.

			size_t pos=iter-t_lock->begin();

			t_lock->insert(iter, word);

			// The item layout manager's insert_item() adds a new
			// item to the container positioned before an existing
			// item. There's also append_item() that adds a new
			// item to the end of the container after all existing
			// items.
			//
			// insert_item() and append_item() take a lambda, or
			// a callable object as a parameter. The lambda
			// gets called with an x::w::factory parameter. The
			// lambda must use this factory to create exactly
			// one display element, that represents the item's
			// label. Typically the lambda calls create_label(),
			// but any display element is permissible. In all
			// cases the lambda is responsible for show()ing the
			// newly-created label.
			i_lock.layout_manager->insert_item
				(pos,
				 [&]
				 (const x::w::factory &f)
				 {
					 f->create_label(word)->show();
				 });
		}

		// See if there were any dupes, and show an error message

		if (dupes.empty())
			return;

		std::vector<std::string>
			sorted_dupes{dupes.begin(), dupes.end()};

		std::sort(sorted_dupes.begin(),
			  sorted_dupes.end());

		auto list=x::join(sorted_dupes.begin(),
				  sorted_dupes.end(),
				  ", ");
		main_window->alert_message(list + " already ordered!");
	}
};

typedef x::singletonptr<appObj> app;

auto create_mainwindow(const x::w::main_window &main_window)
{
	// Create the main application window and its important display
	// elements.
	x::w::gridlayoutmanager
		layout=main_window->get_layoutmanager();

	layout->row_alignment(0, x::w::valign::middle);

	x::w::gridfactory factory=layout->append_row();

	// A label and an input field on the first row in the main window.
	factory->create_label("Pizza toppings:");

	x::w::input_field_config config{30};

	auto field=factory->create_input_field("", config);

	// Install a manual on_validate() callback that gets invoked by
	// "Enter" in the input field, or when tabbing out of it.

	field->on_validate([]
			   (ONLY IN_THREAD,
			    const x::w::callback_trigger_t &triggering_event)
			   {
				   app my_app;

				   if (my_app)
					   my_app->add_toppings();
				   return true;
			   });

	factory=layout->append_row();

	// The grid has two columns. Put an empty canvas place holder in
	// the first column on the 2nd row, below the "Pizza toppings:" label.
	factory->create_canvas();

	// And create a focusable container that uses the item layout manager.
	//
	// The callback that gets invoked by each item's "X", that's
	// intuitively indicates to click there to remove the item.
	x::w::new_itemlayoutmanager nilm
		{
		 []
		 (ONLY IN_THREAD,
		  size_t i,
		  const x::w::itemlayout_lock &lock,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &mcguffin)
		 {
			 app my_app;

			 if (my_app)
				 my_app->remove_toppings(lock, i);
		 }
		};

	auto container=factory->create_focusable_container
		([]
		 (const auto &c)
		 {
			 // Initially nothing here
		 },
		 nilm);

	return std::tuple{field, container};
}

void itemlayoutmanager()
{
	x::destroy_callback::base::guard guard;

	auto close_flag=close_flag_ref::create();

	x::w::input_fieldptr toppings_field;
	x::w::focusable_containerptr toppings_list;

	auto main_window=x::w::main_window::create
		([&]
		 (const auto &main_window)
		 {
			 std::tie(toppings_field, toppings_list)=
				 create_mainwindow(main_window);
		 });

	main_window->on_disconnect([]
				   {
					   _exit(1);
				   });

	guard(main_window->connection_mcguffin());

	// Create the application object, with the fields.

	app my_app{x::ref<appObj>::create(main_window,
					  toppings_field,
					  toppings_list)};

	main_window->set_window_title("Sam's pizzeria");
	main_window->set_window_class("main",
				      "itemlayoutmanager@examples.w.libcxx.com");
	// Put an initial item into the list.
	my_app->add_toppings({"cheese"});
	main_window->on_delete
		([close_flag]
		 (ONLY IN_THREAD,
		  const x::w::busy &ignore)
		 {
			 close_flag->close();
		 });

	main_window->show_all();

	close_flag->wait();

	// Show what's been ordered:

	auto toppings=my_app->toppings.get();

	std::cout << "Ordered: " << x::join(toppings.begin(),
					    toppings.end(),
					    ", ") << std::endl;
}

int main(int argc, char **argv)
{
	try {
		itemlayoutmanager();
	} catch (const x::exception &e)
	{
		e->caught();
		exit(1);
	}
	return 0;
}

itemlayoutmanager.C creates a text input field. Free-form text gets typed into the text input field. The text gets parsed as a comma-separated list of pizza toppings. Anything goes, anything that gets typed gets parsed as a pizza topping.

Items

append_item() and insert_item() add one item to the container. A lambda, or some other suitable callable object, gets passed as a parameter. append_item() and insert_item() invoke the callback with a factory. The callback must use the factory to create and show() exactly one display element, which becomes the new item. Each item has an X next to it, which invokes a callback that uses remove_item() to remove itself from the container.