Chapter 16. Selection lists

Index

Creating a list layout manager
Selecting multiple items in a list, and callbacks
Enabling and disabling list items
Modifying the contents of a list
List locks
Lists with multiple columns
Selection lists, using the list layout manager.

A selection list is a vertical list, typically containing text labels. One or more items in the list get visually picked by clicking on them, or by using the keyboard.

The list layout manager gives an initial impression of a simple, basic layout manager. All it seems to do is show a vertical list of text labels, but it's versatility becomes more apparent when it gets used to underpin hierarchical lists, menus, combo-boxes, and tables.

/*
** Copyright 2017 Double Precision, Inc.
** See COPYING for distribution information.
*/

#include "config.h"
#include <x/mpobj.H>
#include <x/exception.H>
#include <x/destroy_callback.H>
#include <x/ref.H>
#include <x/obj.H>
#include <x/w/main_window.H>
#include <x/w/gridlayoutmanager.H>
#include <x/w/listlayoutmanager.H>
#include <x/w/focusable_container.H>
#include <x/w/gridfactory.H>
#include <x/w/button.H>
#include <x/w/label.H>
#include <string>
#include <iostream>

#include "close_flag.H"
#include "listlayoutmanager.H"

static const char * const lorem_ipsum[]={
	"Lorem ipsum",
	"dolor sit amet",
	"consectetur",
	"adipisicing",
	"elit",
	"sed do eiusmod",
	"tempor incididunt",
	"ut labore",
	"et dolore magna"
};

static size_t lorem_ipsum_idx=(size_t)-1;

// next_lorem_ipsum() always gets called from the library's internal
// execution thread (except for the initial list contents, but this doesn't
// count), so we don't need to worry about thread safety for the index
// counter. But even if there was a thread concurrency issue here this
// isn't critical for this example.

static const char *next_lorem_ipsum()
{
	size_t i=(lorem_ipsum_idx+1) %
		(sizeof(lorem_ipsum)/sizeof(lorem_ipsum[0]));

	lorem_ipsum_idx=i;

	return lorem_ipsum[i];
}


static inline void create_main_window(const x::w::main_window &main_window,
				      const options &opts)
{
	x::w::gridlayoutmanager layout=main_window->get_layoutmanager();

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

	//
	// The first column in the window contains the list container.
	//
	// The second column in the window contains five buttons. The list
	// container is going to span the six rows in the first column.

	factory->rowspan(6);

	x::w::new_listlayoutmanager
		new_list{opts.bullets->value
			? x::w::bulleted_list : x::w::highlighted_list};

	// x::w::single_selection_type is the default:
	//
	// The default value is:
	//
	// new_list.selection_type=x::w::single_selection_type;
	//
	// Other possible values also include:
	//
	// x::w::single_optional_selection_type
	//
	// x::w::no_selection_type


	if (opts.multiple->value)
		new_list.selection_type=x::w::multiple_selection_type;

	// The list's height can be specified as a number of rows, both
	// the minimum and maximum (which could be the same). A scrollbar
	// gets added if the actual list exceeds the number of rows. The
	// number of rows gets multiplied by the default font's height,
	// so a list containing items that use different fonts may not
	// end up showing an exact number of lines specified, because of
	// that.
	if (opts.rows->isSet())
	{
		size_t min=opts.rows->value, max=min;

		if (opts.maxrows->isSet())
		{
			max=opts.maxrows->value;
		}

		new_list.height(min, max);
	}
	else if (opts.maxrows->isSet())
	{
		auto v=opts.maxrows->value;
		new_list.height(v); //
	}

	// Alternatively, the list's height gets set in millimeters, via
	// x::w::dim_axis_arg.
	else if (opts.height->isSet())
	{
		auto v=opts.height->value;
		new_list.height(x::w::dim_axis_arg{v});
	}

	// An optional callback that gets invoked whenever a list item gets
	// selected or unselected.
	//
	// NOTE: the usual rules apply: the callback cannot strongly capture
	// a reference to its display element (this includes the list layout
	// manager, which gets conveniently passed to the callback as a
	// parameter, if it wants it), or any of its parent display elements.


	new_list.selection_changed=
		[]
		(ONLY IN_THREAD,
		 const x::w::list_item_status_info_t &info)
		{
			std::cout << "Item #" << info.item_number << " was ";

			std::cout << (info.selected ? "selected":"unselected");

			std::cout << std::endl;
		};

	x::w::focusable_container list_container=
		factory->create_focusable_container
		([&]
		 (const auto &list_container)
		 {
			 // Initialize the contents of the list container.

			 x::w::listlayoutmanager l=list_container->get_layoutmanager();

			 // append_items()'s parameter is a
			 // std::vector<x::w::list_item_param>.
			 //
			 // list_item_param is (derived from) a std::variant,
			 // and can be constructed from a literal character
			 // string.

			 l->append_items({next_lorem_ipsum()});

			 // list_item_param can be constructed with an
			 // explicit text_param too, in order to use
			 // custom colors or fonts, for the given item.
			 // The resulting list may not show the number of
			 // rows requested in the new_listlayoutmanager.
			 // The resulting list height is sized based on the
			 // default font's height.

			 l->append_items({
					 x::w::text_param{next_lorem_ipsum()}
				 });
		 },
		 new_list);
	list_container->show();

	// Create the four buttons. The first button is on the same row as
	// the list container. The next three buttons are on a row of their
	// own, and because the list container spans four rows columns
	// all the four buttons end up in the second column.
	//
	// halign::fill all four buttons, so that they have the same,
	// uniform width, even though they have different labels and each
	// button would, by default be just wide enough for its label.

	auto insert_row=
		factory->halign(x::w::halign::fill)
		.create_special_button_with_label("Insert New Row");

	insert_row->show();

	factory=layout->append_row();

	auto append_row=factory->halign(x::w::halign::fill)
		.create_special_button_with_label("Append New Row");

	append_row->show();

	factory=layout->append_row();
	auto remove_row=factory->halign(x::w::halign::fill)
		.create_special_button_with_label("Remove Row");

	remove_row->show();

	factory=layout->append_row();
	auto replace_row=
		factory->halign(x::w::halign::fill)
		.create_special_button_with_label("Replace Row");

	replace_row->show();

	factory=layout->append_row();

	auto reset=
		factory->halign(x::w::halign::fill)
		.create_special_button_with_label("Reset");

	reset->show();

	factory=layout->append_row();

	auto show_me=
		factory->halign(x::w::halign::fill)
		.create_special_button_with_label("Show Selected Items");

	show_me->show();

	// Attach callbacks to the buttons. As is the case with all callbacks
	// they cannot capture references to their parent display elements.
	// However the list container is a sibling to the buttons, so this is
	// not a problem.
	//
	// The layout manager object acquires a lock on the container. Can't
	// capture the layout manager, since it would then exist for the
	// duration of the installed callback. The correct approach is to
	// capture the container and then construct the layout manager when
	// needed.

	insert_row->on_activate
		([list_container, counter=0]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &busy_mcguffin)
		 mutable
		 {
			 x::w::listlayoutmanager l=
				 list_container->get_layoutmanager();

			 // insert_items() inserts new items before an
			 // existing list item.
			 //
			 // An example of attaching a selection_changed
			 // callback to an individual list item by specifying
			 // it in the x::w::list_item_param.
			 //
			 // This insert_row() on_activated callback captured
			 // counter by value. Use this to count the number of
			 // new list items that get created here.
			 //
			 l->insert_items
				 (0, {
					 [counter]
					 (ONLY IN_THREAD,
					  const x::w::list_item_status_info_t
					  &info)
					 {
						 std::cout << "Item factory: "
							 "item #"
							   << counter
							   << (info.selected ?
							       " is":
							       " is not")
							   << " selected at"
							   << " position "
							   << info.item_number
							   << std::endl;
					 },

					 next_lorem_ipsum()
				 });
		 });

	append_row->on_activate
		([list_container]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &busy_mcguffin)
		 {
			 x::w::listlayoutmanager l=
				 list_container->get_layoutmanager();

			 // insert_items() and append_items() take
			 // a std::vector of list_item_param-s as parameters.
			 //
			 // Each list_item_param is constructible with either
			 // an explicit x::w::text_param, or with a
			 // std::string (UTF-8) or std::u32string (unicode).
			 //
			 // A plain const char pointer will work as well.

			 l->append_items({next_lorem_ipsum()});
		 });

	remove_row->on_activate
		([list_container]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &busy_mcguffin)
		 {
			 x::w::listlayoutmanager l=
				 list_container->get_layoutmanager();

			 // If the list is non-empty, remove the first list
			 // item.

			 if (l->size() == 0)
				 return;
			 l->remove_item(0);
		 });

	replace_row->on_activate
		([list_container]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &busy_mcguffin)
		 {
			 x::w::listlayoutmanager l=
				 list_container->get_layoutmanager();

			 if (l->size() == 0)
				 return;

			 // replace_items() works like insert_items(), except
			 // that the existing item gets removed.

			 l->replace_items(0, {next_lorem_ipsum()});
		 });

	reset->on_activate
		([list_container]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &busy_mcguffin)
		 {
			 x::w::listlayoutmanager l=
				 list_container->get_layoutmanager();

			 lorem_ipsum_idx=(size_t)-1;

			 // replace_all_items() is equivalent to removing
			 // all items from the list, then append_items().
			 //
			 // This effectively sets the new list of items.
			 //
			 l->replace_all_items({ next_lorem_ipsum(), next_lorem_ipsum()});
		 });

	show_me->on_activate
		([list_container]
		 (ONLY IN_THREAD,
		  const x::w::callback_trigger_t &trigger,
		  const x::w::busy &busy_mcguffin)
		 {
			 x::w::listlayoutmanager l=
				 list_container->get_layoutmanager();

			 // This callback gets executed by the library's
			 // internal connection thread, so this lock is
			 // not really necessary, since only the connection
			 // thread modifies the list.
			 //
			 // Acquiring a list_lock blocks other execution
			 // threads from accessing the list. The lock freezes
			 // the list state, so that the list's contents can
			 // be examined and modified, with the list's contents
			 // remaining consistent.

			 x::w::list_lock lock{l};

			 auto s=l->size();
			 size_t n=0;

			 for (size_t i=0; i<s; ++i)
				 if (l->selected(i))
				 {
					 ++n;
					 std::cout << "Item #"
						   << i << " is selected."
						   << std::endl;

				 }

			 std::cout << "Total # of selected items: " << n
				   << std::endl;
		 });
}

void testlistlayoutmanager(const options &opts)
{
	x::destroy_callback::base::guard guard;

	auto close_flag=close_flag_ref::create();

	auto main_window=x::w::main_window
		::create([&]
			 (const auto &main_window)
			 {
				 create_main_window(main_window, opts);
			 });

	main_window->set_window_title("List layout manager");
	main_window->set_window_class("main",
				      "listlayoutmanager@examples.w.libcxx.com");

	guard(main_window->connection_mcguffin());

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

	main_window->on_delete
		([close_flag]
		 (ONLY IN_THREAD,
		  const x::w::busy &ignore)
		 {
			 close_flag->close();
		 });

	main_window->show();

	close_flag->wait();
}

int main(int argc, char **argv)
{
	try {
		options opts;

		opts.parse(argc, argv);

		testlistlayoutmanager(opts);
	} catch (const x::exception &e)
	{
		e->caught();
		exit(1);
	}
	return 0;
}

Creating a list layout manager

#include <x/w/focusable_container.H>
#include <x/w/listlayoutmanager.H>

x::w::new_listlayoutmanager new_list;

x::w::focusable_container list=f->create_focusable_container(
       []
       (const x::w::focusable_container &c)
       {
            x::w::listlayoutmanager l=c->get_layoutmanager();
       },
       new_list);

x::w::listlayoutmanager llmanager=list->get_layoutmanager();

A factory's create_focusable_container() creates a container that processes focusable events. The first parameter to create_focusable_container() is a creator lambda. Passing an x::w::new_listlayoutmanager for the second parameter creates a selection list. The newly-created x::w::focusable_container's get_layoutmanager() returns an x::w::listlayoutmanager.

new_list.height(6);
	

The selection list consists of rows of items. The selection list automatically sizes itself to be wide enough to show its widest item (this is the default behavior). The selection list's default height is four rows. height(6) sets the list's height to six rows. The selection list provides a vertical scrollbar for scrolling the list vertically when the selection list has more items than its set number of rows. listlayoutmanager.C's --rows option sets the number of rows in the shown list.

new_list.height(4, 10);
	

This example sets the list's minimum height to four rows and maximum height to 10. The list's height starts to grow after four items, and stops growing and starts scrolling after ten.

Note

The list's height gets computed based on the list items' default font's height and the number of rows in the list. The list's height does not come up to be an even number of rows if the list contains separators or items with custom fonts.

An item in a list gets selected by clicking on it, or by tabbing to the selection list, pressing Cursor-Down to highlight the first item in the list, then using Cursor-Down and Cursor-Up to highlight the list item, and Enter to select it.

x::w::new_listlayoutmanager new_list{x::w::bulleted_list};
	

The default list style changes the background color of selected list items. Setting x::w::new_listlayoutmanager's constructor's optional parameter to x::w::bulleted_list reserves some room to the left of each item, and draws a bullet there next to each item when it's selected. listlayoutmanager.C's --bullets option does that.